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.
Policies
Section titled “Policies”A policy is declarative YAML: an applies_to filter plus rules.
name: high-risk-action-policyapplies_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-srePrecedence 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.
Human-in-the-loop gates
Section titled “Human-in-the-loop gates”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:
- API —
POST /runs/{id}/resumewith{decision, approver, note}. A missing decision is a 422, never a silent approve; resuming a run that isn’t awaiting is a 409. - CLI —
cairn remote approve|reject <run_id>(or--watchto stream to the pause and answer inline); fully local runs can gate in the terminal withcairn run -I. - Console — the Runs view’s awaiting filter is the gate queue;
approve/reject inline.
GET /runs/{id}/pending-approvalexposes 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.
Budget
Section titled “Budget”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/storeoperator writingaudit.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.