Governance foundation
Included from Basic tierpacks:security-governance
The foundation primitives: the shared governance objects every
governance pack consumes — built once as base-platform machinery, so no
pack re-invents them. Packs gate surfaces and content over these
objects, never the primitives themselves. The API surface is
GovernanceService (see opschain docs api-v2); every event the
machinery emits is catalogued on opschain docs events; the per-RPC
determination × pack × step-up table is opschain docs governance-matrix. Hands-on guides: opschain learn governance-controls-evidence, governance-campaigns and
governance-waivers.
Controls and the catalog
A control is a named, owned, reviewable statement about enforcement
— not an enforcement instrument itself. Controls wrap existing
mechanisms: instrument_refs points at the rows that
implement the control by {kind, id} — an authorisation policy, a
notification mandate, a maintenance posture, a trust grant — leaving
every instrument's schema untouched, and the authz decide path never
consults the catalog. A control with zero instrument refs is legal: a
procedural control attested by humans (e.g. "licence keys are custodied
offline"), which is what makes the catalog a complete compliance surface
rather than a code mirror.
Attributes: key (unique, immutable), name, domain
(security | change | agent, immutable), description,
instrument_refs, owner_role (the role accountable for reviews) and
status (active | retired — retired controls stay in the catalog
because attestations cite them).
The evidence ledger and attestations
An attestation is the dated statement that a named control was
reviewed/verified: control, attestor, authn strength, verdict
(verified | verified_with_exceptions | failed), notes and
evidence_refs — typed citations, never copies: an
artifact by sha256: blob digest, an audit-event id, a campaign id, an
export manifest hash, a waiver id.
Attestations land in the evidence ledger: a separate append-only
store (the events spine is disqualified — its rows are updated in place
by live write paths) where every row carries prev_hash/row_hash, a
SHA-256 chain from genesis to head. The database enforces the posture: a
guard trigger refuses rewriting any hashed content after insert.
VerifyEvidenceLedger recomputes the whole chain and names the first
broken row.
Legal hold: a flagged ledger row is exempt from every purge/retention path until explicitly unflagged. The guard triggers refuse every content UPDATE and TRUNCATE, and permit a DELETE only for rows a verified sealed archive segment covers (see sealed-segment archival below) — a held row refuses even then, naming the exemption, and the archival sweep never seals past a held row. Flagging and unflagging are audited administrative acts on the step-up floor. The flag is administrative metadata outside the hashed content, so holds never break the chain.
A held entry's typed citations extend the hold to the evidence it cites
: transcript citations pin transcript exchanges
against the retention sweep, and run citations pin a run's log evidence
— its sealed log_segments against the retention sweep and its
log_lines owners against the migrate-logs purge — until the hold is
unflagged. Each exemption set is re-read every batch, so a hold placed
mid-sweep still bites.
Gating: ledger recording is base (campaign closure appends its
attestation unentitled); reading the ledger as the auditor, verifying,
writing attestations and administering legal holds are
packs:security-governance surfaces. Reading and
verifying ride the /governance/evidence readable determination and
attestation appends its updatable twin (the user plane); legal hold
stays an admin-plane operational act. Attestation writes and legal-hold
changes require a stepped-up session (47).
Sealed-segment archival — the ledger's retention
The ledger can never ride row purges — deleting rows breaks the chain —
so its retention is a chain operation, run by the retention sweep on
the retention.evidence_ledger_days hot window (default 730 days; the
mechanics, boundary rules and failure modes are on
opschain docs retention): the aged contiguous prefix is verified,
exported to the blobstore as a chain-preserving segment, read back and
re-verified, sealed inside the chain by a segment_seal ledger row
carrying the segment's range, closing hash, row count and blob digest,
and only then deleted — evidence before purge, enforced by the database
(a delete without a covering verified segment refuses). Verification
seeds at the sealed boundary's closing hash, so the chain verifies
across archived segments, and the verify verdict reports the boundary
(archived_through_seq) and the segment count. A legal-held row caps
the segment, an unverifiable prefix refuses archival with the loud
warn:governance:ledger_archive_blocked event, and archived segments
are kept to the operator's regulatory horizon — the sweep never deletes
them.
The export lane — pull, hash-chained
ExportEvidenceLedger (readable on /governance/evidence + the pack)
is the auditor-facing pull lane: one batch of rows after a after_seq
cursor, in chain order, every row carrying its stored
prev_hash/row_hash verbatim, with the batch's SHA-256 over the
canonical NDJSON encoding (opschain-evidence-export/v1). Batches
chain — the next batch's first row links to the previous batch's
closing hash — so the accumulated export re-verifies offline without
the server. The CLI verb is opschain governance evidence export
(opschain docs cli/governance-evidence-export), which re-encodes the
served rows and refuses on a digest mismatch before writing anything.
Pushing to a SIEM is a separate transport on its own trigger; this lane
is the archival-integrity half.
The posture report
GetGovernancePostureReport (readable on /governance/evidence + the
pack; CLI opschain governance posture) renders the governance posture
in the shared audit-report grammar — findings, coverage, framework
mapping, honest posture statement — over the control catalog, the
ledger's hot window and the exceptions register: per-control findings
(control_attestation_failed high, control_never_attested and
control_review_overdue medium, control_attested_with_exceptions
low, waiver_active info — an excused control is posture an auditor
reads, never a silent pass), per-domain coverage (the mapping onto the
security/change/agent domains), the continuous-control-monitoring
metric set (control coverage, attestation verdicts, campaign progress
and overdue reviews, waiver expiries, legal-hold counts, ledger size
and archived boundary) and the chain-verify verdict. Every report
states its honest posture: a tool-assisted attestation of recorded
posture, never a certification.
Review campaigns and cadences
A campaign turns a point-in-time dataset into a governed sign-off:
items (subject_kind/subject_id + reviewer) each carry a decision —
retain | revoke | flag — recorded in the shared approval
grammar (the step-gate audit:…:approve|reject event family, never a
new one). A revoke decision does not auto-revoke in v1: it opens a named work item for the operator. Closing a
fully-decided campaign appends the closing attestation citing the
campaign: all-retain closes verified, any revoke or flag closes
verified_with_exceptions. Campaign decisions and closure are step-up
gated, and both are reviewer verbs on the user plane: updatable on
/governance/campaigns — a reviewer is an
ordinary user with a grant, never a server admin. The campaign reads ride
the same family's readable determination, and ListReviewCampaigns pages
newest first with an open/closed status filter.
A cadence is "this control is reviewed every N": a 5-field cron
schedule riding the existing scheduler leader tick. A due cadence opens a
campaign of its campaign_kind, due before the next cron occurrence, and
advances next_due_at; concurrent engines claim disjoint due sets, so a
firing happens exactly once, and while a cadence's previous campaign is
still open the schedule advances without opening a duplicate. An open
campaign past due_at plus the cadence's grace_seconds draws the loud
warn:review_cadences:lapsed event once — overdue reviews are themselves
compliance evidence. A campaign with no items cannot close: nothing was
reviewed, so nothing can honestly be attested (the pack kinds populate
items; the first is the access review below).
The access-review campaign
The access_review campaign kind is the security pack's first built-in
campaign content — the foundation's proving consumer. Its items are sourced
server-side from the live access-review dataset (the same data
opschain admin authz review-export exports, which stays base): every
grant at the per-identity (rule × principal) grain — a user grant keys
on its user_id, never the ambiguous username — every
superuser/block system assignment, and the api-token and
runner-registration-token inventories. Each item carries a
subject_summary: the point-in-time human statement the reviewer decides
on, with every identity badged by its provider, so the campaign
reviews a snapshot that cannot drift with the live rows.
Two ways in, one object: OpenAccessReviewCampaign (admin + the
packs:security-governance entitlement — the campaign kind is pack
content behind the one securityGovernanceGate, never a parallel gate)
opens one on demand with an optional reviewer and due date; a cadence
whose campaign_kind is access_review sources the same items when it
fires on the leader tick, in the firing's transaction, so a sourcing
failure rolls the firing back and retries next tick. Decisions, closure
and the closing attestation are the foundation machinery unchanged.
The secret-rotation campaign
The secret_rotation campaign kind is the rotation-cadence content:
"secrets under this vault are rotated every N, attested", on the same
foundation objects. A cadence whose campaign_kind is
secret_rotation fires a campaign whose items are the vault's secret
paths at firing time — one secret_path item per node, the same
value-free enumeration posture as opschain secret audit: paths are
listed, values are never read or recorded anywhere. Reviewers retain
(rotated) or flag each path and close the campaign into the ordinary
attestation. Sourcing needs a listing-capable vault backend (the file
store and the KV backends both enumerate); a vault that cannot
enumerate rolls the firing back loudly and retries, never opening an
unclosable empty campaign.
Work items — revoke opens work
A revoke decision does not auto-revoke in v1 — in the decision's own
transaction it opens a named work item (Revoke <subject_kind> <subject_id>, carrying the subject's snapshot summary), one per campaign
item however often the decision is re-recorded. The operator remediates by
hand and records it with CompleteGovernanceWorkItem (updatable on
/governance/work_items + step-up governance.work_item_complete —
remediation is operator-user work; audited as
api:governance_work_items:complete); a completed item refuses a second
completion rather than overwriting the recorded notes.
ListGovernanceWorkItems lists them newest first, filterable by
open/done, behind the same family's readable determination. The
work-item surfaces stay base (no pack) so an entitlement lapse never
hides open remediation work; auto-remediation is deliberately later pack
content.
The waiver register
A waiver is the exception shape, built once:
time-boxed (mandatory future expires_at; renewal is
review-before-renewal, a new waiver), never self-granted (the
recording caller is the approver and must differ from requested_by —
refused at the API and by a schema constraint), reasoned (justification
plus a typed external reference) and scoped to a control plus an estate
path. ListWaivers is the register, soonest expiry first (readable on
/governance/waivers); granting stays an admin-plane act. Waivers are
distinct from break-glass: a waiver excuses a control for a scope and a
period; break-glass is an authentication posture.
Pack entitlement strings
The canonical pack vocabulary is registered in pkg/licence: packs:agents,
packs:security-governance, packs:change-governance,
packs:agent-governance, packs:ai-agents, packs:mintpress, plus the
separately-licensed families packs:ai-agents-<domain> (hard dependency
on packs:ai-agents) and packs:bindings-<framework>. All are
pack-shaped entitlements enforced through the one gate idiom
(CheckFeature/CheckPackUse, the typed PACK_ENTITLEMENT_REQUIRED
refusal naming the pack, funnel posture — controls ungated, admin/attest
surfaces gated, polite non-destructive lapse). Registration fixes the
spellings; the binding packs themselves are future, demo-feedback-gated
content.
Plane placement
Governance is a feature domain whose users are security officers, risk
owners, reviewers and auditors — not server administrators. The
reviewer/auditor-facing verbs live on the user plane behind the delegable
/governance/<surface> rule-path family (controls, campaigns,
evidence, waivers, work_items — the metadata-writer
pattern): a path-RBAC rule granting readable there delegates the reads,
updatable the reviewer writes, and admin status never implies either. The
operational verbs — catalog curation, cadence configuration, hand-opening
campaigns, legal hold, waiver grants — stay behind the /admin updatable
determination. In the GUI the domain lives under /governance, never
/admin. The CLI mirrors the same split: the feature verbs ride
a normal login under the top-level opschain governance noun (controls,
campaigns, evidence, waivers, work-items, owners), the operational subset
under opschain admin governance (cadences, controls curation, evidence
hold/unhold, waivers create, campaigns open), and each verb's manual
lives at opschain docs cli/<command-path>.
What changes governance behaviour
GovernanceServicereviewer verbs (governance rule-path family + step-up + pack, per surface) and operational verbs (admin + step-up + pack, per surface) — seeopschain docs api-v2.- The scheduler leader tick drives cadence firing and lapse detection; a halted or quiesced system pauses the sweeps with the rest of the tick.
- The
packs:security-governanceentitlement gates the evidence-ledger read/verify/export/attest/legal-hold surfaces and the posture report; a lapsed entitlement freezes that administration without touching recording or enforcement. retention.evidence_ledger_days(config.yaml) sets the ledger's hot window; the retention sweep runs the sealed-segment archival on the scheduler leader (opschain docs retention).