Skip to main content

Compliance Audit Trail

The audit service stores two related integrity records:

  1. An append-only, tenant-scoped audit log with hash-chain and HMAC signatures.
  2. 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:

PayloadEmitted by
turn_started, turn_sealed, cap_token_issuedGateway
prompt_generated, tool_called, tool_returned, turn_failedAgent runtime
model_invoked, model_responseInference router
guardrail_verdictGuardrail
memory_opMemory
rag_chunks_retrievedKnowledge

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.sealed row 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:

EndpointPurpose
GET /v1/receiptsList sealed receipts for the caller tenant.
GET /v1/receipts/{turn_id}Return receipt metadata, envelope, and events.
GET /v1/receipts/{turn_id}/proofReceipt-shaped alias for the proof export.

Direct audit-service development endpoints:

EndpointPurpose
GET /v1/turns/{turn_id}Return ordered events and envelope status for a tenant turn.
GET /v1/turns/{turn_id}/proofReturn 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:

StatusMeaning
pendingUI has a turn ID but no sealed envelope yet.
sealedAudit has produced a signed envelope.
verifiedClient/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

VariablePurpose
AUDIT_SIGNING_KEYActive HMAC key. Required, non-placeholder, at least 32 characters.
AUDIT_KEY_VERSIONVersion label stored with new rows.
AUDIT_SIGNING_KEY_PREVIOUSPrevious key for verification during rotation.
AUDIT_KEY_VERSION_PREVIOUSVersion 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.