Skip to content

Governance

Governance in cairn is deterministic machinery, not prompt engineering: policy rules, approval gates, budget caps, and audit are engine seams that operators and templates inherit — they cannot opt out.

A policy is declarative YAML: an applies_to filter plus rules.

name: high-risk-action-policy
applies_to:
environments: [production] # AND-ed whitelists; empty = don't filter
risks: [high] # low | medium | high
# action_types: [...], capabilities: [...]
rules:
- effect: require_hitl # allow_auto | require_hitl | require_dry_run | deny
reason: "High-risk actions need a human."
channels: ["slack:#prod-oncall"]
approval_role: senior-sre

Precedence is strictness: when several policies match, the most restrictive effect wins (deny > require_hitl > require_dry_run > allow_auto). When no policy matches, the default is require_hitl in production and allow_auto elsewhere — production fails closed.

Policies come from three layers, merged last-wins-by-name: pack-bundled defaults, a user overlay directory (~/.cairn/policies, override with OBS_POLICIES_DIR), and console-authored policies in the DB. Pack and overlay policies always apply; console policies are gated by the active environment’s policies = [...] attach-list.

Whether an operation pauses for a human is resolved from four authorities — the operator’s own requires_approval, the profile, the policy decision, and the pack binding — strictness-only: any authority can turn a gate on, none can turn another’s off. A broken policy fails safe (gate on).

A gated run parks as awaiting_approval on a durable interrupt() — the pause survives restarts on a non-memory checkpointer — and resumes from its checkpoint when resolved:

  • APIPOST /runs/{id}/resume with {decision, approver, note}. A missing decision is a 422, never a silent approve; resuming a run that isn’t awaiting is a 409.
  • CLIcairn remote approve|reject <run_id> (or --watch to stream to the pause and answer inline); fully local runs can gate in the terminal with cairn run -I.
  • Console — the Runs view’s awaiting filter is the gate queue; approve/reject inline. GET /runs/{id}/pending-approval exposes exactly what is being approved.

Stale gates don’t linger: a reaper sweeps awaiting_approval runs past a TTL (CAIRN_APPROVAL_TTL_HOURS, default 7 days) to a terminal expired status with an audit event, and POST /approvals/dismiss bulk-dismisses.

Every LLM call is costed at the gateway chokepoint and accrued on the run’s cost ledger. When the cumulative cost exceeds the configured cap, the run raises BudgetExceeded and stops — a fine-tune or agent loop cannot quietly overspend. Cost lands on the run record (llm_usd / gpu_usd / storage_usd / total_usd) and rolls up in the console’s Insights and Gateway views. Governance-scoped config knobs (e.g. the cost gate’s max_usd) are environment-owned — see Configuration.

The audit trail is an append-only, hash-chained log: each record stores hash = sha256(prev_hash + canonical(event)), so any tampering breaks the chain. Recorded events include model calls, gated operations, approvals (who, when, which channel), policy decisions, and lineage records.

  • The default sink is the zero-dependency audit/store operator writing audit.jsonl (OBS_AUDIT_LOG); the sink is a profile binding (audit.sink), so richer backends swap in without touching the engine.
  • cairn lineage <subject> verifies the chain as it reads and exits non-zero on a broken link.
  • The console’s Audit view renders the same log — filterable by category, actor, run, and time, with CSV export.