Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. A chat export is not an audit trail. It omits denials, versions, and integrity data.
  2. Consequential actions get evidence packs — especially anything customer-visible or money-adjacent.
  3. Append-only with integrity. Hash chains (ledger reprise, Ch 16) make silent edits detectable.
  4. Attribution fields are first-class: actor, tenant, policy_version, approval_id, evidence_ids.
  5. Retention and redaction are product requirements, not afterthoughts.
  6. 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

FailureSymptomFix
Trace-only loggingMissing approvals/policyDedicated audit events
Mutable DB rowsSilent history editsAppend-only + hash chain
PII sprawlPacks unshareableRedaction layers; field allowlists
Orphan sendsSend without eventExecutor refuses without audit write
Clock skewImpossible timelinesStore monotonic seq + server time
Huge packsUnusable exportsSummaries + 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

  1. Mechanical. Append three events; verify chain; mutate middle payload; verify fails.
  2. Export. Build evidence_pack for a fixture case including one deny and one send; write a one-page reconstruction narrative from the pack alone.
  3. 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