Audit
PartialThe audit log is how an operator answers “who changed this, and to what?” without trusting memory: every mutation is recorded once, at the source.
The model
Section titled “The model”audit_log is ground truth (not derived): one row per mutation, carrying actor, verb,
resource, resource_id, and the old -> new diff.
- Write-time mandatory. Every API write emits one
audit_login the same transaction as the data write, a storage-layer responsibility, not per-handler discipline, so it cannot be forgotten or bypassed. - The actor is resolved by IAM (identity and access): the human, service, or node. On the read side a human actor is additionally resolved to a username; a service or node actor surfaces as its principal id.
- An AI-accepted suggestion is one row. An AI tool acts via OAuth as a
humanorserviceprincipal, so the actor is that principal, attributed and audited like any caller; the AI-sourced marking rides alongside the row (AI). - Ground truth a backtest reads. Operator-driven transitions and config changes are not recomputable from collected data, so the audit log is what a rule backtest reads for them: alarm ack and snooze (alarms and actions), and every config change a reconcile consumes.
- Secret decrypts are always audited and never filterable. Every read of secret material
emits an
audit_log(a credential decrypt), and that subset cannot be filtered away. - Other reads are not audited at the storage layer. Optional read-audit is config-driven at the API layer (per-resource opt-in or a verbosity setting), off by default.
Reading the trail
Section titled “Reading the trail”The read surface is GET /audit-log, gated by the admin-sensitive audit:read:admin (which a two-token
wildcard cannot reach, so only admin and owner see the security trail). It returns rows newest first,
filterable by resource and verb, and pages backward with a before timestamp plus a limit
(default 100, capped at 500). Each event also carries the old and new row images the write
recorded, completing the “and to what?” half of the question: a create has only new, a delete only
old, an update both, and auth events neither. Redaction is owned by the write side (sealed secret
material and credential hashes are never written into an image), so the read passes the images
through verbatim. The console renders the trail as the Admin > Audit page, where a row opens into a
drawer with the field-level before/after diff.
Beside the in-transaction estate lane there is a second write lane for auth events. Login and logout
run on read / no-transaction paths, so they emit through a standalone non-transactional seam
(WriteAuthEvent), recorded under resource = 'auth' with the verbs login, logout, login_failed
(a wrong password on a real account), login_denied (a correct password on a disabled account),
login_locked (an attempt inside the lockout window), and revoke_session (an admin ending another
principal’s session). An impersonated action records both actors: actor_principal_id is the
impersonated principal and real_actor_principal_id the admin behind it, and both usernames are
denormalized onto the row (actor_username, real_actor_username) with the foreign keys going
ON DELETE SET NULL, so the trail stays attributable even after its actor is purged
(ADR-0016).
Retention and integrity
Section titled “Retention and integrity”Audit carries the longest retention of any ground-truth log (compliance); the retention partitioning itself is still Design, per the status note above. It is append-only by construction.
Who consumes it
Section titled “Who consumes it”- Backtest: a rule backtest reads operator transitions and config changes from here, since they are not recomputable.
- Reconcile: config changes arrive as
audit_logrows, so reconcile reacts to them. - The alarm projection: ack and snooze come from audit.