Compliance Audit Trail
The audit service stores two related integrity records:
- An append-only, tenant-scoped audit log with hash-chain and HMAC signatures.
- Turn-event envelopes that seal a chat turn into a Merkle root and expose receipt/proof APIs.
Audit Log
Every audit row is signed with the active AUDIT_SIGNING_KEY and linked to the previous row for the same tenant. Verification checks:
- hash-chain continuity,
- HMAC signatures,
- per-tenant sequence continuity,
- append-only chain-head consistency.
POST /v1/audit requires X-Tenant-ID; the service overrides any conflicting body tenant_id.
curl http://localhost:8080/v1/audit \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "default",
"user_id": "alice",
"action": "knowledge.document.ingested",
"resource_type": "document",
"resource_id": "doc-123",
"detail": {"title": "Runbook"},
"result": "success"
}'
Query Events
curl "http://localhost:8080/v1/admin/audit?tenant_id=default&limit=20&offset=0" \
-H "Authorization: Bearer $TOKEN"
Supported filters are tenant_id, user_id, action, limit, and offset. Cross-tenant listing requires platform_admin or legacy admin.
Verify a Tenant Chain
curl "http://localhost:8080/v1/admin/audit/verify?tenant_id=default" \
-H "Authorization: Bearer $TOKEN"
tenant_id is required. Omit limit for a full chain walk, or pass limit=N for a spot check.
Turn Events
Services participating in a chat turn emit typed protobuf TurnEvent records:
| Payload | Emitted by |
|---|---|
turn_started, turn_sealed, cap_token_issued | Gateway |
prompt_generated, tool_called, tool_returned, turn_failed | Agent runtime |
model_invoked, model_response | Inference router |
guardrail_verdict | Guardrail |
memory_op | Memory |
rag_chunks_retrieved | Knowledge |
Events carry turn_id, event_id, tenant_id, principal_id, emitter_service, occurred_at, and service-local sequence.
Turn Envelopes
When a terminal event arrives, or when the background sealer times out a stalled turn, audit creates a sealed envelope:
- canonicalizes event payloads with RFC 8785-style stable JSON,
- hashes event leaves,
- computes a Merkle root,
- signs the envelope with the current audit key,
- appends a
turn.envelope.sealedrow to the tenant audit chain.
Late events after sealing are rejected. Duplicate event IDs are idempotent for retry safety.
Turn and Receipt APIs
Gateway-facing receipt endpoints:
| Endpoint | Purpose |
|---|---|
GET /v1/receipts | List sealed receipts for the caller tenant. |
GET /v1/receipts/{turn_id} | Return receipt metadata, envelope, and events. |
GET /v1/receipts/{turn_id}/proof | Receipt-shaped alias for the proof export. |
Direct audit-service development endpoints:
| Endpoint | Purpose |
|---|---|
GET /v1/turns/{turn_id} | Return ordered events and envelope status for a tenant turn. |
GET /v1/turns/{turn_id}/proof | Return proof for a sealed turn. |
The gateway currently proxies receipt paths, not /v1/turns/*.
Receipts require tenant context. Before a turn seals, receipt detail/proof returns 404.
curl "http://localhost:8080/v1/receipts/$TURN_ID" \
-H "Authorization: Bearer $TOKEN"
Receipt statuses:
| Status | Meaning |
|---|---|
pending | UI has a turn ID but no sealed envelope yet. |
sealed | Audit has produced a signed envelope. |
verified | Client/UI verified the receipt proof. |
Offline Verification
scripts/aibox-verify verifies exported receipt/proof JSON without contacting the running platform:
scripts/aibox-verify \
--receipt receipt.json \
--proof proof.json \
--key-version v1 \
--key "$AUDIT_SIGNING_KEY"
The verifier checks Merkle leaves, envelope HMAC, audit row HMAC, and chain suffix continuity from the receipt row to the exported head.
This is not public non-repudiation. It is valid against the exported proof, exported chain suffix, and keys supplied by the verifier. Publishing chain heads to an external transparency log is outside the current implementation.
Signing Keys
| Variable | Purpose |
|---|---|
AUDIT_SIGNING_KEY | Active HMAC key. Required, non-placeholder, at least 32 characters. |
AUDIT_KEY_VERSION | Version label stored with new rows. |
AUDIT_SIGNING_KEY_PREVIOUS | Previous key for verification during rotation. |
AUDIT_KEY_VERSION_PREVIOUS | Version label for the previous key. |
TURN_SEAL_* | Background sealing and timeout settings. |
The active key is resolved at insert/seal time so rotation can take effect without restarting the audit pod.