Chapter 30 — Auditability and Evidence
Compliance asks Maya: why did Jordan receive a proactive email on O-1001 last Tuesday? “The chat looked fine” cannot name the policy version, approval ticket, send window check, or tool call that authorized it.
consequential action → evidence pack (actor, policy_version, approval_id, facts, tool result, state_hash)
Without an exportable chain, denials disappear, silent edits go undetected, and reconstruction requires a live model or an employee’s memory.
This chapter adds evidence packs and append-only audit events: hash-linked records that answer “who decided what, on which facts, under which policy version” — offline, with PII redaction rules built in.
First principles
- A chat export is not an audit trail. It omits denials, versions, and integrity data.
- Consequential actions get evidence packs — especially anything customer-visible or money-adjacent.
- Append-only with integrity. Hash chains (ledger reprise, Ch 16) make silent edits detectable.
- Attribution fields are first-class: actor, tenant, policy_version, approval_id, evidence_ids.
- Retention and redaction are product requirements, not afterthoughts.
- Reconstruction must work offline. The pack exports without the live model.
Concrete example — reconstruction drill
Question: Did we send email for O-1001 outside send window?
Pack shows:
event seq=4 kind=consequential_action
action=send_email
actor=agent:outreach(+minted send)
policy_version=shop_v1.0.0
approval_id=appr-77
evidence_ids=[oms:order_total, oms:channel_pref, required fields…]
tool_calls=[{idem_key, message_id}]
state_hash=…
chain_valid=true
Earlier events: policy deny at 21:30; later approve at 10:05; send at 10:06. Send-window question answered with timestamps, not vibes.
Diagram — evidence pack contents
evidence_pack(case_id)
├── chain_valid + head_hash
├── events[]
│ ├── model_call? model_id, prompt_version, token usage
│ ├── policy_verdict rule_ids, reasons, policy_version
│ ├── tool_call name, args hash, result hash, idem_key
│ ├── approval reviewer_id, status, body hash
│ └── consequential_action (send / money-adjacent)
└── pointers to object store (full traces, redacted)
Caption: Notice the pack is a justification graph flattened into a hash-linked log, not a transcript dump.
flowchart TB
A[Action] --> P[Policy verdict]
A --> M[Model versions]
A --> E[Evidence IDs]
A --> T[Tool calls]
A --> H[Approvals]
A --> S[State hash]
P & M & E & T & H & S --> Pack[Exportable pack]
Implementation
Module: shopops/audit.py.
from shopops.audit import AuditLog, record_consequential_action
log = AuditLog()
log.append(
case_id="O-1001",
tenant_id="store_northline",
kind="policy_verdict",
actor="policy:shop_v1",
payload={"effect": "deny", "rule_ids": ["send_window"]},
)
log.append(
case_id="O-1001",
tenant_id="store_northline",
kind="approval",
actor="human:rev-17",
payload={"approval_id": "appr-77", "status": "approved"},
)
record_consequential_action(
log,
case_id="O-1001",
tenant_id="store_northline",
actor="agent:outreach",
action="send_email",
model_id="mid-tools",
prompt_version="contact_draft@3",
policy_version="shop_v1.0.0",
evidence_ids=["oms:order_total", "required fields"],
tool_calls=[{"tool": "send_email", "idem_key": "O-1001:email:3"}],
approval_id="appr-77",
state_snapshot={"fsm": "completed", "channel": "email"},
)
pack = log.evidence_pack("O-1001")
assert pack["chain_valid"] is True
# Tamper test
log._events[1].payload["status"] = "forged" # type: ignore[index]
ok, reason = log.verify_case("O-1001")
assert ok is False
Teaching note: mutating frozen audit storage should be impossible in production (WORM object store / append-only log service). The in-memory tamper test exists to prove verification catches edits.
Failure modes
| Failure | Symptom | Fix |
|---|---|---|
| Trace-only logging | Missing approvals/policy | Dedicated audit events |
| Mutable DB rows | Silent history edits | Append-only + hash chain |
| PII sprawl | Packs unshareable | Redaction layers; field allowlists |
| Orphan sends | Send without event | Executor refuses without audit write |
| Clock skew | Impossible timelines | Store monotonic seq + server time |
| Huge packs | Unusable exports | Summaries + blob pointers |
Production considerations
- Align with model risk expectations (e.g. SR 11-7 style documentation of controls) — process, not theatre. [VERIFY applicability]
- Separate debug traces (high volume, shorter retention) from audit events (consequential, longer retention).
- Encrypt packs at rest; access itself audited.
- Regular fire drills: pull a random case pack and reconstruct decision path in <30 minutes.
- Legal hold vs TTL: retention policy is a first-class config per tenant.
Chapter summary
- Audits reconstruct consequential actions with integrity checks.
- Evidence packs link policy, tools, approvals, models, and state.
- Hash chains detect tampering; chat exports do not.
- Actor and tenant attribution are mandatory.
- Tamper tests belong in CI.
- Retention/redaction are part of the design.
- Executor should not side-effect without audit append.
- Drills beat documentation order notes.
Exercises
- Mechanical. Append three events; verify chain; mutate middle payload; verify fails.
- Export. Build
evidence_packfor a fixture case including one deny and one send; write a one-page reconstruction narrative from the pack alone. - Design. Specify which event kinds are audit-grade vs debug-trace-only for ShopOps.
References
- Board of Governors of the Federal Reserve System. SR 11-7: Guidance on Model Risk Management. 2011.
- Audit logging standards / WORM storage practices. [VERIFY]
- Kleppmann, 2017. Designing Data-Intensive Applications — logs as systems of record.
- Chapters 16, 23, 28–29