Audit
Definition
The audit log is the hash-chained record of every decision the gateway makes — both allows and denies — together with the action that prompted it. It is the accountability layer: for any agent it answers what did it do, when, and was it permitted? Audit records use one wire format regardless of which interception layer observed the action, so the gateway presents a single unified history.
Read the guarantee precisely. The chain is tamper-evident, not tamper-proof and not immutable. The digest is unkeyed, so it is not a signature; it covers the JSONL sink, while the database mirror stores no chain metadata; the log is append-only by convention rather than by constraint, since retention pruning deletes rows; and emission is best-effort.
aasm audit verify-chaindistinguishesINCOMPLETE(entries never reached the sink, chain otherwise intact) fromFAIL(an entry was altered or removed), but a loss at the head or tail is indistinguishable from a deletion there, because there is no neighbouring entry to anchor against. Everything below describes what the chain does detect — within those bounds.
The unit of the log is an AuditEntry. Each entry carries a monotonic seq, a
nanosecond timestamp_ns, an AuditEventType (for example
ToolCallIntercepted, PolicyViolation, CredentialLeakBlocked,
ApprovalGranted, BudgetLimitExceeded), the agent_id and session_id, a
serialized payload, optional lineage (team, org, delegation chain), and the
hash-chain fields below.
How it works
Every entry commits to its tamper-meaningful fields via a SHA-256 hash that
includes the hash of the preceding entry, forming a tamper-evident chain. The
genesis entry uses [0u8; 32] as its previous_hash. The hash input opens with a
fixed 84-byte prefix plus the payload:
SHA-256(
seq.to_be_bytes() // 8 bytes
|| timestamp_ns.to_be_bytes() // 8 bytes
|| (event_type as u32).to_be_bytes() // 4 bytes
|| agent_id.as_bytes() // 16 bytes
|| session_id.as_bytes() // 16 bytes
|| previous_hash // 32 bytes
|| payload.as_bytes() // variable
|| <optional sections, append-only>
)
The sequence is not closed. Lineage (root_agent_id, parent_agent_id,
team_id, delegation_reason, spawned_by_tool, depth, org_id), redaction
findings, policy_doc_id, and decision_id are each appended after the payload —
but only when present, and each new field is appended last when it is introduced.
A field that is None contributes zero bytes, so an entry written before a
given field existed hashes to exactly the same value afterwards and chains already
on disk keep verifying across upgrades. Variable-length fields are
length-prefixed so the concatenation stays unambiguous as the tail grows. That
append-only discipline, not a frozen field list, is what makes the chain both
extensible and tamper-evident.
An AuditLog is a session-scoped sequence that enforces two invariants on every
append: each entry’s seq is exactly one greater than the last, and each
entry’s previous_hash equals the prior entry’s entry_hash. verify_chain
re-validates both across the whole log in O(n) and re-computes every hash, so any
altered field — even via unsafe — is detected. It returns true for an empty log,
which is vacuously valid: a passing verify_chain proves no entry was tampered
with, not that any entry exists.
flowchart LR
G["entry 0<br/>prev = 0x00…"] --> E1["entry 1<br/>prev = hash(0)"]
E1 --> E2["entry 2<br/>prev = hash(1)"]
E2 --> E3["entry 3<br/>prev = hash(2)"]
Records are kept free of secrets. Before any event is persisted it passes a
write-boundary sanitizer that strips banned keys (prompts, completions, full
tool payloads, packet bodies) and a credential scanner that redacts detected
secrets — only the [REDACTED:<kind>] label and byte offset are stored, never
the raw secret. Heartbeats are collapsed into a “last seen” update rather than
written per beat.
Example
The hash chain makes tampering detectable. Mutating any persisted field causes re-verification to fail:
#![allow(unused)]
fn main() {
let mut log = AuditLog::new(agent_id, session_id);
log.next_entry(AuditEventType::ToolCallIntercepted, ts, payload);
log.next_entry(AuditEventType::PolicyViolation, ts, payload);
assert!(log.verify_chain()); // true for an untouched log
// Altering any entry's payload, seq, or hash would flip this to false.
}
Retention and export are operational concerns: the unified trail is what compliance export reads to produce a verifiable record for downstream systems.
Related
- Policy — the decisions that produce audit entries.
- Approval — the
Approval*events recorded per transition. - Trace — the per-session view over governed actions.
- Audit and assurance — sanitizer, redaction, and the publish path.
- Compliance export — retention and export.
- API reference —
aa-core(AuditEntry,AuditLog) andaa-security(scanner, redaction) rustdoc entry points.
Last updated: 2026-09-28 by Chisanan232