Usage Guide: Audit¶
Overview¶
This guide shows how to query the audit trail for a given run_id — both in-process via AuditRepository and over HTTP via GET /runs/{run_id}/timeline. The records returned are the same NodeAuditRecord objects the orchestrator wrote as it executed each node, so the timeline is the canonical replay log for a run.
Minimal example¶
from zeroth.governance.audit import AuditQuery, AuditRepository
# 1. In-process: query by run_id using the repository directly.
audit_repo = AuditRepository(database)
records = await audit_repo.list(AuditQuery(run_id=run_id))
for record in records:
enforcement = (record.execution_metadata or {}).get("enforcement")
policy_note = ""
if isinstance(enforcement, dict):
policy_note = f" — policy: {enforcement.get('decision')}"
print(f"[{record.node_id}] status={record.status}{policy_note}")
if record.cost_usd is not None:
print(f" cost: ${record.cost_usd:.4f} tokens: {record.token_usage}")
for tc in record.tool_calls:
print(f" tool: {tc.alias}({tc.arguments}) -> {tc.outcome or tc.error}")
# 2. Over HTTP: the same records via the service API.
timeline = await client.get(
f"/runs/{run_id}/timeline",
headers={"X-API-Key": "demo-operator-key"},
)
timeline.raise_for_status()
for entry in timeline.json()["entries"]:
print(entry["node_id"], entry["status"])
list_by_run, list_by_thread, list_by_node, list_by_graph_version, and list_by_deployment are one-line shortcuts wrapping AuditQuery. For a single node's detail, audit_repo.get(audit_id) returns one record or None.
Common patterns¶
- Build a timeline view. Pass records to
AuditTimelineAssembler().assemble(run_id, records)to get aAuditTimelineordered by start time. - Produce compliance evidence.
build_summary(records)andcollect_policy_events(records)flatten the trail into review-friendly blobs. - Verify chain integrity.
AuditContinuityVerifier(audit_repo).verify_run(run_id)walksprevious_record_digest→record_digestand returnsAuditContinuityReport— use it before exporting evidence. - Redact on write. Configure
PayloadSanitizer(AuditRedactionConfig(redact_keys={"secret","token"}))so secrets are scrubbed before persistence, not filtered out on read.
Pitfalls¶
- Forgetting
deployment_ref. Cross-tenant queries must scope bydeployment_refortenant_id;run_idalone is globally unique but leaks run counts across tenants. - Mutating records.
NodeAuditRecordis immutable from the consumer's perspective; to "amend" an audit you must write a new record whosesupersedes_audit_idpoints at the old one. - Skipping the sanitizer. If you bypass
ApprovalService/ orchestrator and write audit records directly, secret material can end up ininput_snapshot— always run payloads throughPayloadSanitizerfirst. - Duplicate
audit_id.AuditRepository.writeraisesValueErroron duplicates; generate IDs via the default factory, never reuse. - Assuming chain verification is automatic. Chains are computed on write but only verified when you call
AuditContinuityVerifier. Run the verifier before shipping evidence to a regulator.
Reference cross-link¶
See the Python API reference for zeroth.governance.audit.
- HTTP API:
GET /runs/{run_id}/timeline,GET /deployments/{ref}/audits?run_id=... - Related: Concept: audit, Usage Guide: policy, Usage Guide: approvals, Tutorial: governance walkthrough.