AIControl Docs

Audit Log

Every gateway call_tool request made with a valid agent-scoped token — regardless of decision — produces an immutable audit record in PostgreSQL, including malformed requests and calls to a server that is not registered or not active. The record is queued to a local write-ahead log before the response is returned to the agent and shipped to PostgreSQL within a fraction of a second (a review decision is written to PostgreSQL synchronously). There are no gaps in the trail.


Audit event schema

The audit_events table. Fields a compliance engineer will care about:

ColumnTypeDescription
idUUIDImmutable event identifier
session_idUUIDGroups all tool calls within one agent run
sequence_numberintegerCall order within the session, as supplied by the caller (0 on a malformed request that carried none)
agent_idUUIDFK to the agents table
agent_namestringDenormalized agent name — queryable without a join
tool_namestringThe tool that was called
tool_parametersJSONBFull parameters at time of call. For http_get, http_post, http_put, http_delete and http_patch, a domain key extracted from url is added
decisionstringallow, deny, review, or error (the downstream server returned a JSON-RPC error)
decision_reasontextWhy this decision was made
policy_idUUIDFK to the policy that fired; null when no policy matched
policy_namestringDenormalized policy name — queryable without a join
workflowstringThe business-process workflow the call belongs to, as reported by the SDK adapter. Context for grouping the trail, not a binding axis
duration_msintegerAIControl gateway latency in milliseconds
input_tokens / output_tokens / cost_usdinteger / integer / numericUsage the agent reported on the call, if any
risk_deltaintegerAmount this event added to the session's risk score
enforcedbooleanfalse when the agent is in observe mode and the decision was recorded but not applied
bypassbooleanReserved; always false
created_attimestampServer-set, immutable
tool_responseJSONBReserved for the raw tool response (the API returns it truncated to 200 characters). Always null via the MCP gateway path — the gateway does not currently record it.

tool_parameters is stored as JSONB — it supports indexed queries on specific parameter values.

policy_id and policy_name are null when no policy fired. When a policy fires, both are populated.

The dashboard and GET /audit-events also return a nested policy object (name, scope, effect) alongside each event when one fired, and can group the log view by workflow or by session.


Decision reasons

decision_reasonMeaning
response_scan_cleanThe call was allowed, forwarded, and the downstream response passed the scan. This is the reason on a normal allowed gateway call, including one that matched no policy
no_matching_policyNo policy matched — the engine's default allow applies (the gateway records the scan outcome instead once the call has been forwarded)
server_approved_tools_gateThe registered server has an approved_tools list and the tool isn't on it. Checked first
tool_not_approved_for_agentThe agent's approved-tools allowlist didn't include this tool. Checked before any policy is evaluated
policy_matched:<policy_name>A policy matched. decision (deny or review) and policy_name tell you the rest — there's no separate reason code per condition type
review_requires_business_license: <reason>A policy routed the call to review on a Community install, where it is enforced as deny
hitl_approved_retryAn identical call was approved by a reviewer within the retry window, so the retry is allowed
response_scan_flagged:<categories>The downstream response was flagged. decision is deny with MCP_RESPONSE_SCAN_POLICY=block, allow with log
upstream_unreachable, upstream_http_error:<status>, upstream_invalid_responseThe call was allowed but the downstream server failed; decision is allow
upstream_jsonrpc_error:<code>The downstream server returned a JSON-RPC error; decision is error
malformed_request:<detail>Missing or invalid name, arguments, session_id or sequence_number, or an unavailable server. decision is deny
evaluation_error:*The policy engine failed to evaluate the call. Always resolves to deny
evaluation_pipeline_error:<exception>Loading policies or usage counts failed. Always resolves to deny

Viewing the audit log

Open the Audit log page in the React dashboard at http://localhost:3000. You can filter by agent, decision, and tool name, and group the log by workflow or by session. Each row shows the session, tool, decision, reason, policy, and latency.

For programmatic access, query the audit_events table directly:

sql
-- All deny events in the last 7 days
SELECT
  created_at,
  agent_name,
  tool_name,
  tool_parameters,
  decision_reason,
  policy_name
FROM audit_events
WHERE decision = 'deny'
  AND created_at > now() - interval '7 days'
ORDER BY created_at DESC;
sql
-- All events for a specific session
SELECT
  sequence_number,
  tool_name,
  tool_parameters,
  decision,
  decision_reason,
  duration_ms
FROM audit_events
WHERE session_id = 'b3e1f9a2-...'
ORDER BY sequence_number;
sql
-- Deny events by policy, for SOC2 compliance export
SELECT
  ae.created_at,
  ae.agent_name,
  ae.tool_name,
  ae.tool_parameters,
  ae.decision_reason,
  p.name AS policy_name,
  p.compliance_frameworks
FROM audit_events ae
JOIN policies p ON ae.policy_id = p.id
WHERE ae.decision = 'deny'
  AND 'SOC2' = ANY(p.compliance_frameworks)
ORDER BY ae.created_at DESC;

What is and isn't stored

Stored in every audit event:

  • Full tool parameters as submitted by the agent (JSONB)
  • The policy that fired (policy_id and policy_name); its compliance framework tags are on the policy row
  • Decision and reason
  • Intercept latency

Not stored:

  • Tool execution results — AIControl governs the call, not the response
  • LLM prompt or completion content — AIControl operates at the tool layer only
  • Agent-to-agent communication — only tool calls that pass through the gateway are recorded

Immutability

Audit records are append-only. The API and dashboard have no delete or update endpoint for audit_events. The created_at timestamp is server-set and not modifiable by clients.

If you need to enforce immutability at the database level, configure your PostgreSQL user to have INSERT-only access to audit_events and remove UPDATE/DELETE grants.


Retention

AIControl runs a scheduled background purge that deletes audit_events (and any hitl_reviews referencing them) once they're older than your plan's retention window:

PlanRetention window
Community7 days
Business30 days
Enterprise90 days
Trial90 days

The purge is skipped, and the install reports itself degraded in the logs, when a stored activation code is expired or fails verification, or when a legacy paid install has no signed code yet: deleting is irreversible, so an install is not purged at the Community window just because its code lapsed. Enter a renewed code and the next run purges at your plan's window. Until then, the dashboard and GET /audit-events only show the last 7 days.

The purge deletes in batches of 5,000 rows, so a first run on a large install completes without holding a long lock. It runs on a fixed interval (retention_purge_interval_hours, default 24 hours) — it is not manual and is not something you need to schedule yourself. This is the one sanctioned exception to audit_events being otherwise append-only: no other code path deletes or updates a row in it.

For regulated industries, most customers retain audit events for the duration required by their applicable framework (SOC2: 1 year, GDPR: varies by purpose, HIPAA: 6 years) — all of these exceed AIControl's built-in retention windows, so customers subject to those frameworks must archive audit events elsewhere before they age out.


Backup

The audit log lives in the aicontrol PostgreSQL database. Back it up using standard PostgreSQL tooling:

bash
# Dump the audit_events table
docker compose exec postgres pg_dump \
  -U aicontrol \
  -t audit_events \
  aicontrol > audit_events_$(date +%Y%m%d).sql

For production deployments, configure continuous WAL archiving or use a managed PostgreSQL service with automated backups.


CSV Export (Enterprise)

Enterprise installations can export filtered audit events as a CSV file. The export honours the same filters as the list endpoint.

Via the dashboard: Open the Audit log page, apply any filters, and click Export. The file downloads immediately.

Via the API:

GET /audit-events/export
Authorization: Bearer <human-token>

Requires a human token (a dashboard login). Query parameters (all optional):

ParameterDescription
decisionFilter by allow, deny, review, or error
agent_idFilter by agent UUID
tool_nameCase-insensitive substring match on the tool name
date_fromISO 8601 start date
date_toISO 8601 end date

Response: text/csv with Content-Disposition: attachment; filename=audit_events.csv

Columns: id, created_at, agent_name, tool_name, tool_parameters, decision, decision_reason, policy_name, session_id, duration_ms

Text fields that start with =, +, -, @, a tab, or a carriage return are prefixed with ' so a spreadsheet does not run agent-supplied content as a formula. agent_name, tool_name, tool_parameters, decision_reason and policy_name come from the calling agent or its tool arguments.

Maximum 10,000 rows per export. Returns 402 for community installations.


SIEM Export (Business)

Business and Enterprise installations can stream every audit event to an outbound SIEM target as it's written, instead of pulling a CSV on demand. Two target types are supported:

  • OTLP — ships each event as an OTel GenAI semantic-convention log record to a collector at target_url.
  • Webhook — POSTs each event as flat JSON to target_url.

Delivery is checkpoint-on-success-only: a target that's unreachable or returns a non-2xx response doesn't advance its checkpoint, so the event is retried on the next dispatch rather than dropped. Configure targets via POST /audit-export-configs (admin only); see API Reference for the full schema. This code ships in the licensed API image only (see Getting Started); the endpoints return 402 there on a Community plan and do not exist in the community image.

This is separate from CSV Export above — SIEM export is a continuous push of every event to your own collector, CSV export is an on-demand pull of a filtered set.


Human review records

When a tool call returns decision: review on a Business or Enterprise plan, a corresponding record is created in the hitl_reviews table. Review records link to their audit event via audit_event_id.

Review records are accessible via GET /reviews and GET /reviews/{id}. See API Reference for the full schema.