Skip to main content

Audit Log

Concepts

Every request that passes through AIControls is recorded — the identity, the tool or model, the governance decision, and the outcome. The Audit Log is your authoritative trail for security review, incident investigation, and compliance evidence.

What is logged​

Configuration changes have a separate Configuration audit view. Its retention is independent of the AI request log. See that guide for the current configuration coverage and export limits.

Every event in the audit log captures:

FieldDescription
TimestampUTC time of the request
IdentityDeveloper email or agent ID, plus team membership at the time of the request
Request typeMCP tool call, LLM request, or a Cross-App Access (XAA) Authorization Server event (token issuance) — shown as a distinct XAA tag in the Audit Log and Events views
Tool / ModelThe MCP tool name (e.g. bash, read_file) or model identifier (e.g. claude-sonnet-4-6)
ServerThe MCP server (datasource) the tool call targeted, e.g. github-mcp. Empty for LLM requests.
DecisionA governance decision such as Allow, Mutated, Deny, Audit, Warn, Warn Ask, or Approval required for a historical pre-call requirement. A held request is pending in Security → Approvals, not in its final Activity row; after review, that row records Allow/Mutated or Deny.
PolicyThe policy rule that produced the decision, if any
Binding (binding_name)The Access Binding that permitted or governed this request, when the decision was reached through a team→server binding rather than a hand-written policy alone
Grant ID (grant_jti)JWT ID of the Cross-App Access grant that authorized the call, when the request arrived via XAA/ID-JAG — ties the event back to the specific token exchange
Governed agent (client_id)Which governed agent made the call, when multiple governed agents share one Cross-App Access delegator — see Governed agents sharing a delegator
Pod (pod_name, pod_uid)The Kubernetes pod that made the call. The pod UID is what tells two agents apart when they share a service account — the agent ID (<namespace>/<serviceaccount>) cannot. Empty for agents that do not present a projected service account token.
Node (node_name)The node the calling pod was scheduled on
Service account UID (service_account_uid)UID of the service account the calling pod ran as
Workload metadata (workload_metadata)Metadata about the calling agent that AIControls looked up from a trusted source when it made the decision: the source, the source's version of the record, and its labels. The caller cannot supply it. See Trusted workload metadata.
ResourceFile path, repository, namespace, or other resource accessed
Tokens / CostToken count and estimated cost for LLM requests. Cost is priced by the model the client requested, not by an upstream's aliased model ID, which is recorded separately as the upstream model.
Pool (pool_name, pool_attempt)For a request through an upstream pool, the pool and which attempt this row is, such as attempt 1 of 2
Failure category (failure_category)The kind of failure a failed upstream attempt hit: provider error, connection error, rate limited, quota or credit exhausted, or key rejected. A pool request that failed over shows a later attempt row.
Provider family (pool_family)For a cross-provider pool, the provider family the session is held to: its pinned family, or the family that served a new session's first request
SessionThe session this request belongs to

Pod identity comes from the projected service account token's kubernetes.io claims, which Kubernetes binds to the pod. AIControls never derives it from the request source address, because a proxy in front of AIControls can set X-Forwarded-For to anything.

Trusted workload metadata​

When AIControls can look up the calling agent in a trusted source, the event records what that source said at decision time. A later change to the agent cannot alter it.

SourceWhen it is recordedVersionLabels
platform-registryThe agent resolved through the Nirmata platformPlatform record versionPlatform-registered labels
okta-ai-agentA Cross-App Access call, to an MCP tool or an LLM, from an agent provisioned through Okta Agent OIDCThe Okta AI Agent's lastUpdated time, in RFC 3339 format with fractional secondsokta.agent_id, okta.agent_name, okta.agent_status, okta.agent_platform
entra-idAn Entra ID app-only token verified against a configured Entra provider, on an MCP tool call or an LLM requestThe token's uti and issue time, as <uti>@<iat>entra.app_id, entra.tenant_id, entra.object_id, entra.app_name, entra.roles

Policies see the same labels on object.agent.labels. Okta lookups are cached for five minutes and fail open: if Okta cannot be reached, the call proceeds and the event records no metadata.

The event detail shows the metadata in the Identity step. JSON exports carry it as workloadMetadata, and so do the audit records AIControls sends to the Nirmata platform and the aicontrols_audit_query tool on the admin MCP server. CSV exports carry it as workloadMetadataSource, workloadMetadataVersion and workloadMetadataLabels, with the labels as a JSON object.

MCP tool calls and LLM requests are stored in the same log. The Gateway tab (Security → Activity → Gateway) filters by identity, decision, tool or model, request type, and time range; clicking any row opens the full event detail, including request arguments and the policy evaluation trace.

In an All activity drawer, choose Open in Gateway to navigate within the event’s session. Gateway’s Within session controls also work when the event was opened by a direct link outside the current page.

The combined Activity feed may return an empty filtered page while older matching events remain. Continue paging when a next-page cursor is present; exports follow those cursors automatically. If any backing source cannot be read, the Activity counts request fails instead of reporting zero or partial totals. Retry after the source recovers.

When persistent storage is disabled, gateway activity, its counts, and Activity exports use the configured in-memory audit history on the replica serving the request. This history is lost when that replica restarts.

Security events in Activity carry a source-qualified reference, such as audit:42 or baseline:42. Use that reference when linking to event details; different sources can reuse the same record ID.

Prompt capture​

For LLM requests, the audit log also stores the text of the last user turn — the prompt snippet shown in the event detail drawer. This is captured the same way for the wire formats AIControls recognizes: Anthropic Messages, OpenAI Chat Completions, the OpenAI Responses API (/v1/responses, used by Codex CLI and other Responses-API clients), and Gemini/Vertex generateContent — including Claude-on-Bedrock and Claude-on-Vertex traffic through the cloud proxy, since those use the same Anthropic Messages and generateContent shapes. Other Bedrock model families reached through the cloud proxy — Amazon Nova/Titan, Meta Llama, Mistral, Cohere, and others — use their own provider-specific request shapes and are not yet parsed for prompt capture, so the snippet is empty for those. MCP tool calls store their argument values, and intercepted outbound HTTP(S) calls store the first bytes of the request body.

Prompt capture limit (Settings → General → Data Retention & Session Limits) caps how many characters of the prompt are kept per event. The default is 5,000; setting it to 0 disables prompt capture entirely, and the audit log then records the request without its text.

Full capture​

Full capture stores the complete, untruncated prompt, response, and model reasoning for every LLM request — independent of the prompt snippet above, which is always truncated and stored inline on the event itself. It's an Enterprise-tier capability, off by default.

Turn it on in Settings → Data:

  • Enable full capture — stores the complete prompt and response for every LLM request, redacted the same way as the rest of the audit record, then encrypted and compressed before it's written to storage.
  • Capture reasoning — also stores extended-thinking/reasoning text, when the model returns it. Off by default, since reasoning traces can be large.
  • Retention age and Retention capacity — bound how long, and how much total storage, captured records are kept before the oldest are pruned (or archived, if an archive destination is configured).

Full capture is a distinct control from the retention and redaction settings above — turning it on does not change what appears in the Security → Activity → Gateway table. Once enabled, a View full capture button on the event's detail opens the complete prompt/reasoning/response — see Full Capture Viewer. The same viewer is also reachable from a session's Replay tab.

Redaction of sensitive values​

Before an audit record is written, AIControls redacts sensitive values from the prompt snippet, the tool arguments, and the outbound HTTP body snippet. Two classes are removed:

  • Credentials — private keys, cloud access keys, source-control and chat tokens, sk- style API keys, database connection strings, and high-entropy api_key=-style assignments.
  • Personal data — email addresses, phone numbers, US Social Security numbers, Indian PAN and Aadhaar numbers, IBANs, and payment card numbers.

Each detected value is replaced by a typed placeholder, so the record still shows what kind of value was there:

DetectedStored as
Email address[REDACTED:EMAIL:@example.com] — the domain is kept
Payment card[REDACTED:CREDIT_CARD:****1234] — the last four digits are kept
Phone number[REDACTED:PHONE]
US SSN[REDACTED:SSN]
Indian PAN[REDACTED:PAN]
Aadhaar number[REDACTED:AADHAAR]
IBAN[REDACTED:IBAN]
Credentials[REDACTED:AWS_KEY:AKIA], [REDACTED:PRIVATE_KEY], …

Some details are worth knowing:

  • Checksums are verified. Payment card, Aadhaar, and IBAN values are only redacted when their check digits validate, so a 16-digit order number or a 12-digit ticket ID stays readable.
  • Phone numbers are matched in international form as well as national form. A number written with a leading + and a country code is redacted whole — the country code is part of the placeholder, not left beside it — in the groupings countries actually use (two five-digit groups, a two-digit area code plus two four-digit groups, three-three-four, and the run of digits with no separators at all). National forms are still matched: 415-555-0134, (415) 555-0134, and ten consecutive digits. Digit runs that are not phone-shaped — timestamps, order IDs, version strings, ports and process IDs — stay readable. Redaction and the content-safety scanner share one definition of what a phone number looks like, so a number that is redacted from the record is also a number the scanner can see.
  • A single occurrence is enough. Content-safety alerting only raises a PII finding once a request contains several email addresses or phone numbers, to avoid noise. Redaction has no such threshold — one address is redacted.
  • Private IP addresses are not redacted. RFC 1918 addresses (10.x, 172.16–31.x, 192.168.x) are kept as-is so audit records stay useful for infrastructure debugging.
  • Email placeholders keep the full domain, not just the top-level domain: alice@acme-internal.example becomes [REDACTED:EMAIL:@acme-internal.example]. The domain is what makes the record useful — it tells you whether an address was internal, a customer's, or a personal webmail account. It is also the part of the trade-off you should weigh: in a small tenant, a retained domain sitting on the same row as the identity and the timestamp can be enough to re-identify the individual. Where that matters, disable prompt capture (set the Prompt capture limit to 0) rather than relying on redaction alone.
Denied requests show no PII findings

The Security tab on a session derives its PII findings from the stored audit record, so it only ever sees redacted text. For a request that was denied, that means a prompt containing five email addresses shows no PII findings there — the addresses were already replaced by placeholders before the finding pass ran.

This is not a detection failure. The block was decided from the original request, before redaction, and the request never reached the model. Read the decision and the policy that produced it — not the absence of a PII finding — when reviewing a denied call. Allowed requests are scanned on the response and are unaffected.

Redaction applies to the audit record only. The request forwarded to the model or MCP server is never altered by it, and policy evaluation and content scanning always see the original text — a policy reading object.llm.currentPrompt (or object.request.hasPII) is unaffected by this setting. If you need the forwarded request de-identified as well, that is a separate control — see content safety policies.

Redact personal data in audit records (Settings → General → Data Retention & Session Limits) turns the personal-data pass off. It is on by default. Turning it off makes the audit log store prompts, tool arguments, and outbound request bodies verbatim, which means the audit database — and every configured audit destination, including object-store export and OpenTelemetry trace export — must be treated as a sensitive-data store. Credential redaction always stays on.

Redaction happens at write time, so it applies to everything downstream: the stored record, the live event stream, exports, and external audit destinations. Events recorded before you upgraded keep whatever text they were stored with; they are not rewritten, and age out under your normal retention window.

Self-hosted: replicas without the admin interface

The toggle is stored in your workspace database and every replica picks it up within a minute. The exception is a replica running with the admin and UI interface switched off, or running purely as an authorization endpoint: those never read the stored setting and stay on the value from their configuration, which defaults to redaction on. The effect is safe but confusing — after turning redaction off you may still see redacted records from part of the fleet. In that topology, set the redaction value in your Helm values rather than in the UI.

Decisions​

Each event records a decision, such as Allow, Deny, Audit, Warn, or Approval required. A call waiting for human review appears in Security → Approvals; when it resumes, its final request row records Allow/Mutated or Deny, while a separate lifecycle row records the reviewer's decision. A historical pre-call authorization row can still say Approval required. See Decision Types for the full set of decisions and their meanings.

Approval and exception records​

A reviewer's decision on a held call (hitl_approved / hitl_denied) and each step of an exception's lifecycle (exception_requested, exception_granted, and so on) are recorded as their own rows, separate from the request they concern. These rows describe a decision about a request, not a request that travelled through the proxy, so the Activity detail drawer renders them differently:

  • The header tag reads APPROVAL or EXCEPTION rather than MCP or LLM.
  • The detail shows who decided, when, the held tool or model, the agent, the approval request ID, and the audit summary of the decision. The reviewer's typed note is stored on the approval request itself (visible in Security → Approvals), not on this row. There is no request-flow diagram, policy-evaluation step, or destination, because none of those happened in this event.
  • An approval record states only that the reviewer approved. Whether the held call then ran is recorded on the original request's own row — look for the same tool and agent at or after the approval time. A call whose caller disconnected during the hold has an approval record and no completed request.

The live Pulse feed labels these rows the same way and does not count them toward an agent's call totals.

Retention and export​

Audit events are stored in your workspace's SQLite database. The default retention window is 90 days; older events are purged automatically. To extend retention, configure an S3-compatible object store in Settings → Integrations — events are streamed there in real time and retained indefinitely.

Self-hosted deployments can additionally configure an intermediate tier: storage.snippetRetentionDays (set to a positive number) clears an event's stored prompt and response snippets — the most sensitive fields on the record — while leaving the rest of the row (tool name, decision, cost, timing) intact and searchable. This tier works independently of storage.auditRetentionDays, including when audit retention is disabled (rows kept forever, snippets still tiered down). The one configuration to avoid is setting storage.snippetRetentionDays to a value at or past a positive storage.auditRetentionDays — rows then purge whole before their snippets would ever tier down, making the snippet tier a silent no-op (the server logs a startup warning if you do this). A snippet cleared this way still shows in Session Replay — Replay's Steps and Phases are derived from the event's other fields, not its snippets — just without the prompt/response text for that turn.

Export the current filtered view to CSV from the Security → Activity → Gateway toolbar. The export includes all fields in the table view and can be used directly for compliance reporting.

Configuration change evidence​

Configuration changes have a separate audit trail that records the actor, affected resource, and changed fields. Credential values, including encrypted keys and encrypted private keys, appear as [REDACTED] in the before and after evidence. Rotating a key still records a change; identifiers such as the provider key ID remain visible so you can identify the credential that changed.

Identity-mapping changes retain the authenticated request's actor when available. When the mapping operation receives only the validated administrator identity, that identity is recorded with an unknown authentication method. Operations without a caller identity are attributed to the system.

Configuration history also covers external guardrail definitions and enablement, PromptGuard settings, detector toggles, AI Analyst enablement, Kubernetes posture mappings, and AI Posture Assessment settings. These changes are committed together with their audit evidence. If that evidence cannot be saved, the configuration change is rolled back.

Settings requests validate their input before saving any field. Invalid model-budget or egress rules therefore leave all settings in that request unchanged and create no configuration-change evidence. Each subsequent setting write commits with its own evidence; a storage failure or concurrent-edit conflict can still interrupt a save after earlier settings have committed.

See also​