verify() can prove that no record was added, removed, reordered, or altered. This is the receipt that turns “the agent behaved” into something you can hand an auditor.
The AuditLog lives in infy/governance/audit.py. It is zero-dependency (stdlib hashlib, hmac, json, threading) and thread-safe.
The receipt
Governance writes one AuditEvent per tool decision. You read them off gov.audit:
gov.audit.events returns a copy of the list, so iterating it never mutates the log.
What a record contains
EachAuditEvent is a frozen dataclass with these fields:
The hash chain
When you callrecord(), the log takes a lock, reads the previous record’s hash, and computes this record’s hash over its own body plus that prev_hash. The first record chains from GENESIS, a string of 64 zeros.
prev_hash links no longer line up. Either way, verify() returns False.
The seq read, hash, list append, and file write all happen inside one
threading.Lock, so concurrent tool calls cannot interleave and corrupt the chain.How verify works
verify() walks the log from GENESIS, recomputing each record’s expected hash and checking two things per record: that its prev_hash equals the running previous hash, and that its stored hash equals the recomputed one.
True only if no record was added, removed, reordered, or altered.
The args digest
The log never stores raw call arguments. It storesargs_digest, a SHA-256 of the canonicalized arguments. Canonicalization uses sorted keys and compact separators, so the digest is stable across dict ordering.
allow_nan=False fails closed on NaN or Infinity rather than emitting invalid JSON. And “no arguments” is recorded as the empty string _NO_ARGS, which is distinct from the hash of {}, so an absent-args call and an empty-args call never collide.
The optional off-box key
By default the chain uses plain SHA-256. Passkey= to switch every link to HMAC-SHA256:
key is used by both record() and verify(). Hold it outside the agent process. That is the whole point of the next section.
Tamper-evident, not tamper-proof
Be clear-eyed about the threat model. A plain SHA-256 chain is self-anchored. An attacker who controls the process can edit a record and simply recompute every hash after it.verify() would pass on the forged chain. SHA-256 alone proves integrity against accidental corruption and against anyone who cannot rerun your code, not against the process itself.
The HMAC key closes that gap. If the key lives outside the agent process, forging the chain requires the key, which the compromised process does not have.
Persisting to disk
Passpath= to mirror every record as one JSON line (JSONL):
verify() checks the in-memory chain for the current process.
How it maps to compliance
The hash chain is the evidence artifact two frameworks expect.SOC 2
The chain supports the CC6, CC7, and CC8 criteria: a durable, integrity-protected record of who did what, under which decision, that database permissions alone cannot provide.
EU AI Act
Article 12 expects automatic logging of events over an AI system’s lifetime. An append-only, verifiable chain is the record-keeping evidence that obligation calls for.
The framework gives you the mechanism (an append-only, verifiable log with an off-box key option). Mapping it to a specific control set, retention policy, and external anchor is your deployment’s responsibility.
Known gaps
The audit log ships as an MVP. These limits are deferred on purpose, not silently missing:No external anchor yet
No external anchor yet
The head hash is not yet anchored to an external notary or WORM sink. Until it is, a process with the HMAC key could still regenerate the chain.
No approver identity capture
No approver identity capture
Records capture the decision but not who approved it. Segregation of duties (four-eyes) is on the roadmap, so a maker can currently approve their own action.
JSONL is export-only
JSONL is export-only
The disk mirror is written but not read back and reconciled on startup.
Related
Governance overview
How policy, risk, approval, and audit fit the agent loop.
Durable approval
Human-in-the-loop gates that can suspend a run and resume later.