Repository navigation
v1.6.0 — policy automation
[1.6.0] - 2026-04-27
v1.6 "Policy automation" closes the v1.5 loop: operators
declare strike-threshold rules in[policy_automation], and
the recorder evaluates those rules inside every recordAction
transaction. Auto-mode rules record consequent actions in the
same transaction; flag-mode rules queue pending rows for
moderator review. Conservative idempotency, severity-ordered
rule selection, takedown-cascade auto-dismissal of pendings,
and a forensic audit chain that extends across all policy
events. Pending state is moderator-tier visibility only — the
public surface still shows what cairn-mod has done, not what
it might do.
Added
- Policy automation engine:
[policy_automation]config block,
PolicyAutomationPolicyconfig loader, pure-function policy
evaluator with crossing detection + idempotency + severity
ordering, recorder integration evaluating rules inside the
recordAction transaction. (#70, #71, #72, #73) - Pending action confirm flow:
tools.cairn.admin.confirmPendingAction
XRPC +WriteCommand::ConfirmPendingActionwriter command.
Confirmed pendings materialize assubject_actionsrows with
actor_kind='moderator'(the moderator takes responsibility)
andtriggered_by_policy_rulepreserved as forensic
provenance; full label emission via the v1.5 path. (#74) - Pending action dismiss flow:
tools.cairn.admin.dismissPendingAction
XRPC +WriteCommand::DismissPendingActionwriter command.
Audit-only rationale storage (the pending table itself has no
resolved_reasoncolumn; rationale lives in the audit row's
moderator_reasonfield). (#75) - Takedown-cascade auto-dismissal: every unresolved pending for
a subject auto-dismisses inside the same transaction as a
takedown row INSERT, regardless of takedown path (moderator-
recorded, policy-auto-recorded, or confirmed-pending-promoted).
Cascade audit rows reuse thepending_policy_action_dismissed
audit_log.action and discriminate via reason JSON's
triggered_byfield (takedown_terminalvsmoderator_dismissed),
cross-referencing the triggering takedown viatakedown_action_id.
(#76) - Pending action read XRPC:
tools.cairn.admin.listPendingActions
(paginated,subject+resolutionfilters, opaque id-cursor)
andtools.cairn.admin.getPendingAction(single row). Mod-or-
Admin role; direct sqlx queries against the pool (no writer
task involvement). (#77) - Operator CLI:
cairn moderator pending {list, view, confirm, dismiss}, HTTP-routed via the admin XRPC, tabular human output
by default,--jsonfor tooling. (#78) - New error variants:
PendingActionNotFound,
PendingAlreadyResolved,SubjectTakendown(defensive race-
closer on confirm). - New audit_log.action vocabulary:
pending_policy_action_confirmed,
pending_policy_action_dismissed. Pending creation rides the
precipitating action'ssubject_action_recordedaudit row's
policy_consequencefield (no separatepending_policy_action_created
audit kind). - Migration
0005_policy_automation.sql: extendssubject_actions
withactor_kind(CHECK in 'moderator' | 'policy', defaulting
to 'moderator' for backfill) andtriggered_by_policy_rule
(NULL for moderator-recorded actions); newpending_policy_actions
table with write-once-on-resolution trigger; partial indexes
on the active subset for the moderator review queue. - Comprehensive integration test coverage: idempotency contracts
through the writer task (#80) and end-to-end lifecycle
scenarios composing all v1.6 surfaces (#81). - Design doc §F22 (policy automation; 11 subsections covering
rule shape, threshold-crossing semantics, severity ordering,
auto-vs-flag mode, pending resolution, takedown cascade, schema- audit linkage, synthetic policy actor DID, public-tier non-
visibility, operator surfaces, deferred capabilities). New §4.2
disclosure 6 (pending visibility is moderator-tier only). §F21.9 - §18 roadmap updates. (#82)
- audit linkage, synthetic policy actor DID, public-tier non-
Changed
- Pending policy actions are moderator-tier visibility only — not
exposed via public XRPC (tools.cairn.public.getMyStrikeState
is unchanged from v1.5). Subscribers see what cairn-mod has
done, not what cairn-mod might do. See §4.2 disclosure 6. subject_actionsaudit row's reason JSON gains anactor_kind
discriminator ('moderator'vs'policy') and an optional
triggered_by_policy_rulefield. The precipitating action's
audit row also gains an optionalpolicy_consequencefield
({rule_fired, mode, auto_action_id | pending_action_id})
cross-referencing the consequence when a rule fires.pending_policy_action_dismissedaudit reason JSON carries a
triggered_bydiscriminator ('moderator_dismissed'for #75,
'takedown_terminal'for #76) so audit consumers can filter
the two shapes viajson_extract.MAINTAINERS.mdadds a "Development pattern" section disclosing
the AI-assisted development approach used to ship cairn-mod.
Fixed
- rustfmt drift in
tests/admin_subject_actions.rsfrom #76's
test rewrites — twoletbindings that were left in unwrapped
two-line form. Pure formatting fix; no logic change. (7a7628f)
Internal
- chainlink #79 closed as duplicate of #73; the
PolicyAutomationPolicyplumbing through writer-state landed
as part of #73's recorder integration per that session's
"subsume #79" decision.