Audit API
All routes require an audit-reader bearer (the dedicated reader token, or the admin token). Every route here is read-only — audit rows are written only by the decision path, and the database refuses to change them afterward.
GET /v1/audit/actions/{action_id}/chain
The full delegation chain for one recorded action, back to the consenting human.
Response (ChainRead):
{
"root": {"user_sub": "user:alice", "scope": "kb-search-agent", "consented_at": "...", "session_id": "..."},
"hops": [
{"hop": 1, "action_id": "...", "acting_agent": "user:alice", "target": "mcp://kb.internal/search",
"target_kind": "tool", "data_class": "kb.public", "decision": "allow", "deny_reason": null,
"enforced": true, "ts": "..."}
],
"complete": true,
"break": null
}
When complete: false, break names at_hop and a reason (LEGACY_TOKEN,
PARENT_NOT_FOUND, PARENT_MISMATCH, PARENT_INCOMPLETE, or LEGACY_RECORD) — see
Query a delegation chain for what each means.
GET /v1/audit/decisions
Filtered decision history, newest first, paginated, stable under concurrent appends.
Query params: limit, cursor, session_id, agent, outcome, deny_reason,
enforced, target_kind, since, until (all optional filters)
Response (AuditDecisionPage): a Page of decision rows plus pagination info, same
envelope shape as GET /v1/agents.
GET /v1/decisions — deprecated
Kept for one phase for existing observe-mode tooling; served by the same query as
GET /v1/audit/decisions with Phase 2's filter set (session_id, outcome, deny_reason,
enforced). New callers should use GET /v1/audit/decisions instead.
?enforced=false&outcome=deny is the observe-mode report proper: everything that would have
been blocked, while nothing was.