Skip to main content

G-11 Audit evidence (WORM)

Brick G of the Enterprise AI Platform. Technology: Hash chained eai.audit_events + immutable WORM object storage (S3 Object Lock compliance mode). French version: ../../fr/bricks/G-11-audit-evidence.md.

Role and responsibilities​

  • Record every mutation and policy decision with prev_hash/hash chain and sequence per tenant.
  • Export signed periods to WORM storage and verify integrity at any time.

Position and interactions​

  • Execution path: synchronous write on every mutation, batch export.
  • Written in the same transaction as the business change (eai.audit.audit()).

Open source and developed parts​

  • Open source reused: PostgreSQL, S3 Object Lock (SeaweedFS, Ceph RGW or cloud).
  • Developed in the platform: Hash chain, verify endpoint, export with manifest and sha256, evidence mapping to controls.

Data model​

Control Plane tables (schema eai): eai.audit_events, eai.audit_exports.

Every tenant table has tenant_id uuid NOT NULL and a row level security policy on current_setting('app.tenant_id', true)::uuid. ClickHouse usage_events is the analytics replica of eai.usage_events (ReplacingMergeTree keyed by event_id).

Contract: contracts/v1/audit-event.schema.json (schema_version 1.0). Usage is always reported with contracts/v1/usage-event.schema.json, audit with contracts/v1/audit-event.schema.json.

API endpoints​

MethodPath
GET/v1/audit/events
GET/v1/audit/events/{id}
GET/v1/audit/verify
POST/v1/audit/export

GET /v1/audit/events​

Response:

{
"items": [
{
"event_id": "aud_901",
"sequence": 90311,
"action": "model.invoke",
"decision": "allow",
"outcome": "success",
"hash": "9c1e..."
}
],
"next_cursor": "c_2"
}

GET /v1/audit/events/{id}​

Response:

{
"event_id": "aud_901",
"actor": {
"type": "agent",
"id": "agt_ap"
},
"resource": {
"type": "model",
"id": "gpt-4o-mini"
},
"prev_hash": "77ab...",
"hash": "9c1e..."
}

GET /v1/audit/verify​

Response:

{
"valid": true,
"checked": 90311,
"first_sequence": 1,
"last_sequence": 90311,
"broken_at": null
}

POST /v1/audit/export​

The format field selects ndjson (default) or csv (one row per event, nested objects as JSON cells, the canonical event in the last event column); the signature covers the file in the chosen format. Another value gives a 422.

Request:

{
"from": "2026-09-01T00:00:00Z",
"to": "2026-10-01T00:00:00Z",
"retention_days": 3650
}

Response:

{
"export_id": "aex_3",
"status": "completed",
"uri": "s3://eai-audit-worm/tenant/2026-09.jsonl",
"sha256": "e3b0c442...",
"object_lock": "COMPLIANCE"
}

SDK examples​

Python​

from enterprise_ai import EnterpriseAI

client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_live_xxx", tenant="acme", locale="en")
client.audit.events(action="model.invoke", since="2026-10-01T00:00:00Z")
client.audit.get("aud_901")
client.audit.verify()
client.audit.export(since="2026-09-01T00:00:00Z", until="2026-10-01T00:00:00Z")

TypeScript​

import { EnterpriseAI } from "@enterprise-ai/sdk";

const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY, tenant: "acme", locale: "en" });
await client.audit.list({ action: "model.invoke", from: "2026-10-01T00:00:00Z" });
await client.audit.get("aud_901");
await client.audit.verify();
await client.audit.export({ from: "2026-09-01T00:00:00Z", to: "2026-10-01T00:00:00Z" });

Java​

EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.locale(Locale.ENGLISH)
.build();
client.audit().events(Map.of("action", "model.invoke", "from", "2026-10-01T00:00:00Z"));
client.audit().event("aud_901");
client.audit().verify();
client.audit().export(Map.of("from", "2026-09-01T00:00:00Z", "to", "2026-10-01T00:00:00Z"));

Go​

client, err := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")), eai.WithTenant("acme"), eai.WithLocale("en"))
if err != nil { log.Fatal(err) }
ctx := context.Background()
_, err = client.Audit.Events(ctx, eai.WithQuery(map[string]string{"action": "model.invoke", "from": "2026-10-01T00:00:00Z"}))
_, err = client.Audit.Event(ctx, "aud_901")
_, err = client.Audit.Verify(ctx)
_, err = client.Audit.Export(ctx, map[string]any{"from": "2026-09-01T00:00:00Z", "to": "2026-10-01T00:00:00Z"})

Configuration​

Environment variables: EAI_S3_BUCKET.

Helm values keys (deploy/helm/enterprise-ai/values.yaml): controlPlane.config.EAI_S3_BUCKET.

Security​

  • No UPDATE or DELETE grants on eai.audit_events for the application role.
  • Readers: auditor and security_officer roles.
  • Tenant resolved from the credential, never from the body; PostgreSQL row level security on every tenant table.
  • Every mutation and policy decision writes a hash chained audit event (G-11).
  • Errors use the standard envelope with message_i18n en and fr.

Observability​

  • Daily verify job; alert on broken_at not null.
  • W3C traceparent propagated, X-Request-Id returned, spans carry eai.tenant_id, eai.project, eai.agent_id, eai.run_id.

FinOps units​

  • WORM storage cost allocated per tenant (storage_gb_hours).

Failure modes and resilience​

  • Audit write fails -> the business transaction is rolled back (no unaudited change).

Standalone adoption​

Usable as an append-only evidence ledger by other systems through the audit API.

Acceptance tests​

  1. Tampering one row makes GET /v1/audit/verify return valid=false with broken_at.
  2. Deleting an exported object before retention fails at storage level.

Evidence Service (kit section 11)​

The eai.domains.evidence domain (routes /v1/evidence, migrations V150 to V154) completes the chain:

  • transactional outbox eai.audit_outbox written in the transaction of the audited change, drained by the audit_gateway job (SIEM HTTP and NATS JetStream, idempotency key = event id, exponential backoff, no sampling, sequence gap detection);
  • Ed25519 signed seals (eai.audit_seals) over ranges of events: sha256 Merkle root of the RFC 8785 canonical events, hash of the previous seal, key id; verified by GET /v1/evidence/seals/{id}/verify and offline by python -m eai.domains.evidence.verify bundle.json;
  • integrity monitor (audit_integrity job) recomputing chains, seals, signatures and timestamps, opening an eai.audit_integrity_alerts alert and a security.audit.integrity_alert event;
  • three distinct timestamps: event_time (from the caller, lead bounded by EAI_AUDIT_CLOCK_SKEW_SECONDS), ingested_at (database clock at insert) and sealed_at;
  • sensitive payloads encrypted with a Tink envelope (key outside the database: OpenBao transit), only the ciphertext digest is chained; decryption reserved to the auditor and security_officer roles, with a reason, itself audited;
  • legal hold (tenant, case, time range) blocking purge and erasure, released by two distinct approvers, purge evidence (counts and digests, never the contents);
  • execution configuration of a run (model, prompt, tool, skill, plugin) rebuilt by GET /v1/evidence/runs/{id}/configuration.

Tests: services/control-plane/tests/test_evidence_gateway.py, services/control-plane/tests/test_evidence_config_redaction.py.

Implementation roadmap