Appearance
Audit Log

Every policy decision and every sensitive action in Agentcy produces an audit entry. The log is append-only, cryptographically ordered (monotonic seq per org), and retained for POLICY_AUDIT_RETENTION_DAYS (default 90).
What gets audited
| Category | Examples |
|---|---|
| Policy decisions | allow / deny on every middleware + tool call evaluation |
| Auth | login success/failure, API key create/delete, JWT validation errors |
| Connectors | source create/update/delete, OAuth token rotation, sync failures |
| Chat | approval decisions, tool call outcomes, conversation PATCH |
| Channels | binding create/update/delete, credential rotation, test runs |
| Platform admin | org create, invite, role change, plan change |
| Memory | memory create/update/delete (writes; recalls optional) |
Everything else (node reads, pagination, search) is not audited — volume would drown the log.
Shape
json
{
"id": "aud_01HABCDE",
"seq": 14201,
"ts": "2026-04-24T12:33:21.123Z",
"org_id": "org_…",
"user_id": "usr_…",
"api_key_id": null,
"category": "policy",
"action": "chat.tool.execute",
"decision": "deny",
"resource": { "connector": "kubernetes", "tool": "delete_pod", "tool_effect": "destructive" },
"policy_id": "pol_…",
"rule": "agentcy.connectors.deny",
"input": { /* full Rego input */ },
"context":{ "ip":"10.0.0.7","request_id":"req_…","latency_ms":12 }
}Read
bash
# Last 100 deny decisions
curl "http://…/policies/audit?decision=deny&limit=100" \
-H "authorization: Bearer $TOKEN" | jq
# Stream new entries (SSE)
curl -N "http://…/policies/audit/stream" \
-H "authorization: Bearer $TOKEN"
# One entry in detail
curl "http://…/policies/audit/aud_…" -H "authorization: Bearer $TOKEN" | jqFilters:
decision=allow|denyaction=<string>(exact match on full action path likesource.sync)connector=<name>user_id=<id>,api_key_id=<id>since=<RFC3339>,until=<RFC3339>resource.tool=<name>
Export
bash
# CSV of the last 7 days of denies
curl "http://…/policies/audit.csv?decision=deny&since=$(date -u -v -7d +%Y-%m-%dT00:00:00Z)" \
-H "authorization: Bearer $TOKEN" -o denies-last-7d.csv
# NDJSON stream of all entries for a month
curl "http://…/policies/audit.ndjson?since=2026-04-01&until=2026-05-01" \
-H "authorization: Bearer $TOKEN" > april.ndjsonForward to a SIEM
Agentcy can push every audit entry to an external collector. Configure a sink:
bash
curl -X POST http://…/policies/audit/sinks \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{
"kind":"http",
"url":"https://siem.internal/ingest",
"secret":"whsec_…",
"filter":{"decision":"deny"}
}'Supported sinks: http (HMAC-signed like outbound webhooks), s3 (NDJSON rolled hourly), loki.
Retention
POLICY_AUDIT_RETENTION_DAYS controls local retention. For longer archive, pair short local retention with an s3 sink:
env
POLICY_AUDIT_RETENTION_DAYS=30then push to S3 and Glacier-tier it.
Integrity
Entries are append-only at the DB level: the policy_audit_log table has no UPDATE or DELETE grants in the API's Postgres role. Admin deletion (for retention) goes through a cron job with a separate role.
Each entry includes prev_hash = SHA-256(prev_hash_prev || id || seq || ts || action || decision). Gaps in the chain (e.g. manual tampering) can be detected by replaying hashes.
Verify:
bash
curl "http://…/policies/audit/verify?from=aud_A&to=aud_B" \
-H "authorization: Bearer $TOKEN"
# -> { "status":"ok", "entries":14201, "hash_chain_valid":true }Performance
Writes are batched; a burst of 10k denies/second is handled without blocking the request path (mpsc channel → dedicated writer task). Reads use compound indexes on (org_id, ts DESC) and (org_id, decision, ts DESC).
Typical latency impact of enabling audit: < 1 ms per request.
Turning audit off
You can't turn off core audit. You can reduce verbosity:
env
POLICY_AUDIT_ALLOWS=false # default true; set false to only log denies
MEMORY_AUDIT_RECALLS=false # default false; set true to audit memory recallsGotchas
- PII lives in the log. Inputs include tool args, which may include customer data. Apply retention policy to the sink too.
- User deletion doesn't purge audit. That's by design — security events must outlive the user. The log keeps the
user_idas an opaque string after the user row is gone. - Clock drift. Entries use the API server's clock. Sync NTP.