/v1/audit
The append-only, hash-chained audit log — list rows, fetch one, and verify integrity.
Every policy decision and capability-gated action is recorded to an append-only, SHA-256
hash-chained audit log. Each row's prev_hash equals the prior row's row_hash, so tampering is
detectable.
Set CORTEX_V1_AUDIT_PATH or the audit log is lost on every restart
By default the audit log is kept in memory only: a graceful restart (docker stop -t 120, then
start) empties GET /v1/audit, and the default boot log says nothing about it. Set
CORTEX_V1_AUDIT_PATH to a file on the data volume (for example /data/audit.jsonl) and the server
writes the rows there as JSONL and keeps them across restarts; the boot log then says
v1 audit log: persistent (JSONL) path=…. A path the server can't open falls back to in-memory with an
ERROR log line. This applies with or without an API key. Treat the setting as required for any
deployment that relies on the audit trail.
Audit row shape
{
"id": "audit_01HX...",
"ts": "2026-05-15T10:42:01Z",
"actor": "user:alice",
"capability": "forget.gdpr",
"decision": "allow",
"decided_by_tier": "scope",
"endpoint": "POST /v1/experience",
"response_status": 202,
"elapsed_ms": 12,
"gdpr": false,
"request": {
"method": "POST",
"path": "/v1/experience",
"body_bytes": 412,
"body_hash": "sha256:...",
"headers_hash": "sha256:...",
"tenant_pepper_version": 1
},
"request_id": "req_01HX...",
"tenant_id": "...",
"token_jti": "...",
"row_hash": "sha256:...",
"prev_hash": "sha256:..."
}Row fields
The request block is { body_bytes, body_hash ("sha256:…"), headers_hash, method, path, tenant_pepper_version }. The authentication layer's rows carry no scope; a row a route writes for its
own decision does, with decided_by_tier: "handler": a forget's (gdpr: true) and a derived-data
re-derivation's or unmerge's. endpoint includes the HTTP method ("POST /v1/experience"). There is no
event_id field; client_ip (the first X-Forwarded-For address) appears on the authentication layer's
row only when the request carries that header, and only to callers that may read other actors' rows.
row_hash and prev_hash form the hash chain. request_id is the request's X-Request-Id: since
v0.10.2 the authentication layer's row and a forget's gdpr row carry the id the response returned
(verified on v0.10.2).
Endpoints
GET /v1/audit→ the row list (append-only, hash-chained). The query filters aredecision,actorandcapability. To narrow by time or endpoint, filter on the rows'tsandendpointfields on your side.GET /v1/audit/{id}→ a single row.POST /v1/audit/verify { audit_id, body }→{ match, algorithm: "sha256", hashed_at, tenant_pepper_version }.match: truerequires the exact canonicalized row body.
Which rows record a capability
With an API key or tokens, every request is audited. Requests allowed at the deployment tier are
recorded with an empty capability (""), so ?capability= matches only rows decided at a finer tier
(for example memory.forget on a forget); filter those others by actor, decision or endpoint.
In no-auth mode (CORTEX_INSECURE_NO_AUTH=1) only operations such as forgets, which record their own
decision, produce rows.