feat(gateway): audit read API + chain verification (Step 6.6a) - #153
Merged
Conversation
Make the Step 0.7c hash-chain audit log usable + provably intact. build_app now
creates one AuditWriter over a tamper-evident SHA-256 hash-chain store, exposes
app.state.audit_store / audit_writer / audit_enabled, and hands the same writer
to the corpus router — so the events the writers append land in exactly the store
the read API serves (previously the corpus router held a private store the
gateway couldn't read). New AuditWriter.store property is the read accessor.
GET /v1/audit is tenant-scoped (a principal sees only its own tenant's events,
newest-first, with chain_verified inline), bounded by limit and filterable by
action / outcome. GET /v1/audit/verify reports whole-log hash-chain integrity
{ok, event_count} — content-free, so global verification leaks nothing
cross-tenant. The chain is the tamper-evidence mechanism; immutability at rest
(the WORM signed export) is Step 6.6b.
Read API on by default via cfg.audit.enabled (a passive, tenant-scoped
compliance record, unlike the behaviour-changing ACL/PII toggles); off → 404,
events still recorded. Adds AuditListResponse / AuditVerifyResponse wire types +
AuditError / AuditNotFoundError (→404). First of two slices for Step 6.6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First of two slices for Step 6.6 — Immutable audit log. Step 0.7c shipped the foundation (append-only
AuditStoreSPI,NoopAuditStoreSHA-256 hash chain,AuditWriter), but the log was write-only — only the corpus router wrotecorpus.route, into a store nothing could read or verify. 6.6a makes it usable + provably intact.build_appcreates a singleAuditWriterover a hash-chain store, exposesapp.state.audit_store/audit_writer/audit_enabled, and hands the same writer to the corpus router — so events the writers append land in exactly the store the read API serves (before this, the corpus router held a privateNoopAuditStorethe gateway couldn't read). NewAuditWriter.storeaccessor; default in-memoryNoopAuditStore, production injects a durable one.GET /v1/audit— tenant-scoped (a principal sees only its own tenant's events, filtered byctx.tenant_idat the boundary, same pattern asGET /v1/query/{id}/trace), newest-first, bounded bylimit([1,1000]), optionalaction/outcomefilters.AuditListResponsecarrieschain_verifiedso one read both returns the events and attests the log is untampered.GET /v1/audit/verify— whole-log hash-chain integrity (the chain is one global sequence across tenants), returning{ok, event_count}— a boolean + total, no event content, so global verification leaks nothing cross-tenant.cfg.audit.enabled(a passive, tenant-scoped compliance record, unlike the behaviour-changing ACL/PII toggles); off → 404 (AuditNotFoundError), events still recorded. AddsAuditListResponse/AuditVerifyResponsewire types +AuditError/AuditNotFoundError.Scope / deferred: read + verify only (WORM signed export is 6.6b). Today the populated event is
corpus.route(every query); expanding audit coverage (ACL/PII/ingest decisions as audit events) is a follow-up — the surface + chain are in place for it.Tests & gates
apps/gateway/tests/test_audit.py): tenant-scoped list, cross-tenant isolation, verify ok + tamper detection (corrupt a stored chain hash →ok=False,chain_verified=False),action/outcomefilters,limit/total, disabled→404, no-auth→401, shared-store wiring (a query'scorpus.routeevent is visible viaGET /v1/audit).tests/ packages/2315 passed, 1 skipped. Green: ruff,mypy --strict(298 files), RAG001, policy-coverage, log-schema, schema-drift + openapi-drift (dist/openapi.*+dist/rag.schema.*regenerated for the new endpoints +AuditConfig).Documentation
Test plan
uv run pytest apps/gateway/tests— 290 passeduv run pytest tests/ packages/ -m "not integration"— 2315 passed, 1 skippedmypy --strict(298 files) + RAG001 + policy-coverage + log-schema🤖 Generated with Claude Code