Obol keeps two accountability records, and they answer different questions. The audit log is human mutation history. It contains no invocations. A receipt is one tool call. Neither is a subset of the other, and the schema says so out loud: the audit action enum is documented as “Human mutation history only; invocation receipts have their own surface.”

What is recorded

apps/control/app/services/audit.py and record_audit in apps/control/app/services/operator.py write to the audit_log table. Only actions in a closed allowlist can be written — record_audit coerces the action into an AuditAction enum and raises on anything else, so a caller cannot invent an action name. The recorded actions, grouped by what they touch: The through-line is the ADR-0033 consequence that accountability is never delegated to the identity provider. An attacker who could forge dashboard sessions could mint a virtual key; the record of who did so is Obol’s own, keyed by workspace and actor.

Written in the same transaction

record_audit flushes; it does not commit. The row belongs to the same transaction as the act it describes, and callers commit both together. A minted key that exists always has a row saying who minted it — there is no window where the change landed and the accountability record did not.

Row shape

The actor must be a verified Operator object; passing anything else raises rather than writing an unattributed row.

How detail is bounded

detail never carries secrets, URLs, or raw arguments. It is filtered twice, on write and on read. On write, _validated_audit_detail rejects any key not in that specific action’s allowlist, walks the value tree, and caps the canonical JSON serialization at 4096 UTF-8 bytes. The sole URL-shaped exception is mcp_origin, which must be a canonical https://host origin with an IDNA round-trippable hostname — a reviewed remote MCP origin, and nothing else. On read, safe_detail independently intersects the stored keys with a fixed allowlist and re-checks each value’s type and size:
Readable detail keys
Values survive only if they are a string of at most 256 characters, a bool, an int64, or a list of at most 16 such strings; at most 16 keys are returned. A row written by an older, laxer version of the code still cannot project an unexpected field to a reader.

Querying

Audit events
Results are newest first, ordered by created_at DESC, id DESC, with the cursor encoding both so pagination is stable across rows sharing a timestamp. The query filters AuditLog.action.in_(ACTIONS) even without an action parameter, so a row carrying an action outside the current contract is never returned. Every response is validated against packages/proto/operator_audit_page.schema.json before it leaves. A database or validation failure returns 503 with Observe read model unavailable rather than a partial page.

Access

Reading requires workspace membership plus the audit.read action, which every role from viewer upward holds. Membership is resolved first, so a request for a workspace the operator does not belong to never reaches the query. The actor’s email is joined from the users table and dropped if it exceeds 320 characters.

Why receipts are separate

Receipts are produced by the gateway, not by a dashboard action, and they travel a different path. The gateway writes an invocation audit envelope to stream:invocation-audit:{workspace_id}; control’s arq worker runs drain_invocation_audit every ten seconds to project those envelopes into a queryable receipt read model (ADR-0032). Keeping that off the request path is what keeps Postgres off the invocation path. That surface has its own filters — state, evidence_trust, verification_state, tool, decision, key_id, connection_id — because the questions are different. In particular:
A receipt names its evidence class. Federated catalog routes produce untrusted or broker_attested evidence and can never be verified. “The audit log proves what happened” is true of control-plane mutations; the equivalent claim about tool calls is tier-dependent. See Evidence trust.

Receipts

Per-invocation records, evidence classes, and verification state.

Telemetry

Access logs, Prometheus metrics, and health endpoints.