Audit log¶
agent_adapter.audit keeps a signed, tamper-evident record of the security-relevant
things your agents did: approvals asked, decided and used, denials, guardrail blocks,
budget stops, memory adds, deletes and exports, sandbox runs and recovered runs.
It never records arguments, results, prompts or memory text. An auditor can check that
nothing was edited, removed, reordered or cut, and export a time window as CSV.
flowchart LR
E["Executor events<br/>approval · budget · guard · memory · sandbox · recovery"] --> S["AuditSink<br/>keep security events only<br/>ids, counts, flags, digests"]
S --> F["FileAuditStore<br/>JSONL segments<br/>hash chain + HMAC"]
S --> C["CallableAuditStore<br/>your SIEM, queue or bucket"]
F --> V["verify(folder, key)"]
F --> X["export(folder, since, until, format)"]
| File | Does |
|---|---|
audit/sink.py |
AuditSink, audit(executor, folder=, key=, fail_closed=) |
audit/_writer.py |
the writer thread and its bounded queue |
audit/_actions.py |
which events count, and what each record keeps |
audit/_scrub.py |
short scalars only; secrets and PII masked; identities as digests |
audit/_chain.py |
canonical JSON, the key, HMACs, the hash chain |
audit/store.py |
FileAuditStore and the AuditStore port |
audit/callable.py |
CallableAuditStore: ship each record to your function |
audit/verify.py |
verify, read_entries, verify_records (all streaming) |
audit/export.py |
export, export_to as JSONL or CSV |
audit/errors.py |
AuditUnavailable (strict mode) |
cli/audit.py |
agent-adapter audit verify and audit export |
Attach it¶
executor = Executor(
registry,
approval=CallbackApproval(lambda request: request.arguments["amount"] <= 50),
hooks=[GuardrailHook(no_card_numbers)],
)
log = audit(executor, folder=folder, key=KEY)
The key signs every record. Pass key= or set AA_AUDIT_KEY (16 bytes or more; keep it
in a secret manager, not in code). Without a key audit() refuses to start. emit only
queues: a writer thread stores each record, so the event loop never waits on the disk.
Strict mode, cost and limits: Audit log in production.
What is recorded¶
| Action | From | Keeps |
|---|---|---|
approval.requested / decided / consumed |
the approval step and the call that used it | tool, approval id, approved, edited (never the edited values) |
approval.denied |
a denial, or no approval port | tool, approval id |
approval.expired |
a chat approval nobody decided in time or a stopped turn; a Redis or DynamoDB TTL that ran out, seen when executor.resume meets it |
tool (if known), approval id, outcome |
guardrail.blocked |
a blocked tool or model call | tool or model, stage |
guardrail.redacted |
a guard that rewrote a result or reply | guard, stage, count, kinds (email, card) |
limits.exceeded |
RunLimits stopping a call |
tool |
budget.reserved / warning / exceeded |
BudgetHook |
scope, window, amounts |
memory.added / deleted / exported |
any memory backend | backend, count, scope kind (user+session, never ids) |
sandbox.started / finished / failed |
any sandbox | backend, sizes, exit code |
checkpoint.recovered |
recover of a flow or tool loop |
step, replayed, rerun |
app.<name> |
your own ctx.emit("audit.<name>", ...) |
every key, cleaned; source: "app" |
Every value is cleaned: content keys are dropped, secret-named keys become ***, emails,
phone and card numbers and API keys in text are masked, and names of people or teams
(user_id, team, scope, ...) become a keyed digest. With the key you can check
whether a digest belongs to a given user; without it nobody can reverse it.
Only first-party events make first-party actions, and each record carries a signed
source (sdk or app). Your own events always become app.<name>, so
ctx.emit("audit.approval.decided") is recorded as app.approval.decided, never as a
decision. Other custom.* events are not recorded.
Verify and export¶
result = verify(folder, KEY)
print(result.summary())
print([entry["action"] for entry in read_entries(folder, KEY)])
csv_text = export(folder, format="csv", key=KEY)
print(csv_text.splitlines()[0])
export AA_AUDIT_KEY=... # the key is read from the environment, never the command line
agent-adapter audit verify .agent_adapter/audit
agent-adapter audit export .agent_adapter/audit --since 2026-11-01 --format csv --out audit.csv
verify exits 1 when anything is wrong, and lists each problem. export refuses a log
that does not verify. JSONL export keeps each signed line byte for byte; CSV cells that a
spreadsheet would run as a formula are quoted.
Everything streams: verify reads one line at a time, and read_entries(folder, key) and
export_to(out, folder, ...) check the whole log first, then yield or write records one
segment at a time. Memory stays flat however large the log grows.
Catch a change¶
segment = next(folder.glob("audit-*.jsonl"))
segment.write_bytes(segment.read_bytes().replace(b'"approved":false', b'"approved":true'))
print(verify(folder, KEY).summary())
How tampering shows¶
- Edits. Each record carries an HMAC-SHA256 over its canonical JSON. Changing a byte, or re-signing without the key, fails it.
- Deletions and reordering. Each record has a
seqand the SHA-256 of the record before it. A gap, a swap or a removed record breaks the chain. - Truncation. Each segment opens with an
openrecord and ends with a signedseal(record count).audit.headholds the signed end of the whole chain, so cutting the tail, or removing the last segments, shows. - Restart. Opening a folder checks its last segment against the head and continues the chain. A folder that does not check out, or another key, is refused. One writer per folder (an OS lock).
- Rollback and swaps. An older copy of a folder still verifies on its own. Keep the
anchorthatverifyprints somewhere the host cannot change, and pass it later (--anchor): a log that ends before it, or another log, fails.
Segments rotate at max_bytes (10 MB) and at each new UTC day.
What auditors get¶
- One line per security event: what, when (UTC), which run and call, and safe details.
- Proof of integrity:
agent-adapter audit verifypasses only when every record, the chain, the seals and the head agree with the operator's key; with--anchor, also that the log was not rolled back or replaced. - A CSV or JSONL export of any time window, each row still carrying its
prevhash andmac. - No secrets, no personal data, no prompts or tool arguments: the log can be shared with an auditor as it is.
Limits, strict mode and shipping records off the host: Audit log in production.