Skip to main content
Every governance decision writes exactly one record to an append-only log. Each record chains the one before it with a cryptographic hash, so 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

Each AuditEvent is a frozen dataclass with these fields:

The hash chain

When you call record(), 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.
Because every record’s hash folds in the previous hash, the records form a chain. Change any field of any historical record and its hash no longer matches. Delete or reorder a record and the 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.
It returns True only if no record was added, removed, reordered, or altered.

The args digest

The log never stores raw call arguments. It stores args_digest, a SHA-256 of the canonicalized arguments. Canonicalization uses sorted keys and compact separators, so the digest is stable across dict ordering.
Two details matter. 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 digest lets you prove later that a specific set of arguments was the one seen at decision time, without persisting the arguments themselves. Keep the originals elsewhere if you need to reproduce the exact values.

The optional off-box key

By default the chain uses plain SHA-256. Pass key= to switch every link to HMAC-SHA256:
The same 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.
Without a key held off-box, the audit log is tamper-evident against outside edits but not tamper-proof against the process that writes it. For production, use AuditLog(key=...) and anchor the head hash to an external notary or WORM sink. External anchoring is on the roadmap, not shipped.

Persisting to disk

Pass path= to mirror every record as one JSON line (JSONL):
The file write happens inside the same lock as the append, so the file and the in-memory list stay in step. Today the JSONL mirror is an append-only export. It is not yet read back and reconciled on startup, so 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:
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.
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.
The disk mirror is written but not read back and reconciled on startup.

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.