AI Agent Audit Trails: What to Log for SOC 2 Type II (2026)

The event schema auditors actually ask for, field by field

Namita Garg

HoD, Data Services

13 min read  ·  Wed Jul 15 2026

A steel band with crimped seals in sequence, each clamping the one before it, the newest glowing violet

Every audit log you have designed rests on an assumption nobody wrote down: the actor was either a person or a deterministic piece of software. For a person you record who and when; for a service, the code plus the inputs explains the output.

An agent breaks both halves. A non-deterministic system took the action, on behalf of a user, through a tool with side effects, chosen at runtime for reasons you cannot reproduce. Your logs say POST /refunds returned 201 and the caller was agent-runtime — true, useless, and misleading to an auditor sampling it, because it names the wrong actor. For a deterministic service the code and inputs *are* the evidence; for an agent they do not, so the record is the only evidence that will ever exist.

*This is engineering guidance, not legal or audit advice.* SOC 2 is an attestation against the AICPA's Trust Services Criteria — not a certification, and not a checklist of log fields. The criteria say you authenticate principals, restrict and monitor access, detect anomalies and control changes; what follows is what evidences that for an agent. Your auditor's interpretation governs.

What an agent breaks that a normal service does not

Non-determinism kills replay as evidence. Same prompt and model, different output; sampling and provider-side routing both contribute, and temperature zero does not remove it. Any control story ending "we can re-run it and show you" is dead.

Delegated authority splits the actor in two. A human asked; a machine did. If the agent uses the human's token, your logs claim the human clicked the button — a false record, and what CC6-style logical access controls exist to prevent. A shared service account is the opposite failure: everything belongs to svc-agent. Log both identities and the scopes the agent held.

Tool calls are side effects chosen at runtime. What the system *could* do is the tool registry; what it *did* was picked mid-inference, which makes the tool boundary your real control point — and the authorisation model in MCP server development more important than the transport. Recent MCP revisions moved to OAuth 2.1 with resource indicators precisely to stop tokens reaching servers they were not issued for.

Prompts and model versions are a change-management surface. A prompt edit changes behaviour as surely as a code change, and CC8 asks how changes are authorised, tested, approved and deployed. A product manager editing a production system prompt in a vendor console with no ticket is an uncontrolled change surface — as is a floating model alias.

The event schema

Emit one event per meaningful action: run start, each model call, each tool call, each approval, run completion. Never batch a run into one blob — auditors sample individual actions, and you must hand over one record without exporting a whole conversation.

01{
02 "schema_version": "1.4.0",
03 "event_id": "01J8ZK4V6R3QW9N2XKQ0F7T5AB",
04 "event_type": "agent.tool.call",
05 "occurred_at": "2026-07-29T14:22:07.418Z",
06 "recorded_at": "2026-07-29T14:22:07.602Z",
07 "sequence": 41,
08 "data_region": "eu-west-1",
09
10 "trace": {
11 "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
12 "span_id": "00f067aa0ba902b7",
13 "session_id": "sess_9f2c41d8",
14 "run_id": "run_7b41ce02",
15 "turn": 3
16 },
17
18 "actor": {
19 "type": "human",
20 "principal_id": "u_88213",
21 "auth_method": "saml+mfa",
22 "auth_time": "2026-07-29T13:58:11Z",
23 "roles": ["billing.analyst"]
24 },
25
26 "agent": {
27 "identity": "spiffe://acme/ns/agents/sa/refund-agent",
28 "version": "2026.07.3"
29 },
30
31 "authority": {
32 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
33 "delegation": "act",
34 "token_id": "jti_5c02ab91",
35 "scopes": ["orders:read", "refunds:create"],
36 "constraints": { "max_refund_usd": 250 },
37 "expires_at": "2026-07-29T14:28:11Z"
38 },
39
40 "model": {
41 "provider": "openai",
42 "requested_model": "gpt-5.1",
43 "resolved_model": "gpt-5.1-2026-05-14",
44 "system_fingerprint": "fp_2f9c81",
45 "temperature": 0.2
46 },
47
48 "prompt": {
49 "template_id": "refund_triage",
50 "template_version": "v11",
51 "template_hash": "sha256:1d9e77b2",
52 "content_ref": "s3://agent-payloads/2026/07/29/01J8ZK4V.enc"
53 },
54
55 "tool": {
56 "name": "issue_refund",
57 "server": "mcp://billing-tools.internal",
58 "arguments": {
59 "order_id": "A-99231",
60 "amount_usd": 129.0,
61 "customer_email": "<redacted:email#h:7c31a2>"
62 },
63 "arguments_hash": "sha256:ba7130ff",
64 "idempotency_key": "refund:A-99231:v1"
65 },
66
67 "authorization": {
68 "decision": "allow",
69 "engine": "cedar",
70 "policy_bundle": "sha256:0ae4c118",
71 "matched_rule": "refund_under_limit"
72 },
73
74 "approval": {
75 "required": true,
76 "approver_principal_id": "u_10442",
77 "approver_roles": ["billing.supervisor"],
78 "decision": "approved",
79 "decided_at": "2026-07-29T14:21:55Z",
80 "presented_hash": "sha256:ba7130ff",
81 "expires_at": "2026-07-29T14:36:55Z"
82 },
83
84 "result": {
85 "status": "success",
86 "outcome": "refund_issued",
87 "side_effect": true,
88 "reversible": true,
89 "external_ref": "stripe:re_3PZk91"
90 },
91
92 "usage": {
93 "input_tokens": 4210,
94 "output_tokens": 288,
95 "cost_usd": 0.0231,
96 "price_list_version": "2026-06-01",
97 "latency_ms": { "model": 1840, "tool": 612, "total": 2604 }
98 },
99
100 "redaction": {
101 "policy_version": "redact-2026.05",
102 "fields_redacted": ["tool.arguments.customer_email"],
103 "count": 1
104 },
105
106 "integrity": {
107 "prev_event_hash": "sha256:41ad6c39",
108 "event_hash": "sha256:c70bd845"
109 }
110}

Identity, time, correlation. event_id is a ULID, so records sort by creation without a database. schema_version matters because your schema changes mid-window while the auditor reads month one beside month eleven. sequence is monotonic per run, because millisecond timestamps collide. occurred_at versus recorded_at exposes ingestion lag; both are RFC 3339, UTC, explicit offset. trace_id and span_id follow W3C Trace Context, run_id is one invocation of the loop and session_id the conversation — so "show me everything around this action" is a query on each.

Actor, agent, authority. actor is the human principal with auth_method and auth_time, because "was this under a properly authenticated session" is a logical-access question. agent is a separate workload identity — a SPIFFE ID or equivalent, never a human's credential or a shared account. authority records the delegation: token exchange carrying the act claim from RFC 8693, the scopes and constraints that bounded it, when it expired. Log the jti, never the token.

Model, prompt, tool. You ask for an alias; the provider resolves a dated snapshot that moves under you, so log resolved_model, plus system_fingerprint where the provider exposes one — the only honest answer to which model decided. Log prompt identity, version and hash, not rendered text: the hash proves which template ran, the text is customer data. arguments are stored post-redaction while arguments_hash covers the pre-redaction payload, so the record binds to what executed. idempotency_key separates two refunds from one logged twice.

Decision, outcome, cost. The authorisation decision, policy bundle hash and matched rule turn "we have access controls" into "here is the enforcement decision for this action under this policy version". status is not outcome — the call succeeded, versus what changed in the world — and side_effect and reversible let a responder filter to consequential actions. Keep price_list_version beside cost_usd, because vendor prices change. Most of this is ordinary LLMOps instrumentation, so one emit path serves both.

What not to log

The instinct under audit pressure is to log everything, and it backfires. The criteria include confidentiality and privacy categories, and an audit store full of raw prompts is a second copy of your most sensitive data: replicated, long-retained, broadly readable, invisible to your data subject request tooling. Teams fail confidentiality *because* of their logging.

Redact inside the emitting process, before the event leaves it — not in the pipeline, not at the sink. Anything reaching a broker unredacted is in backups, replicas and dead-letter queues you will never fully clean.

  • No raw prompt or completion text. Log the template hash and a content_ref into a separately encrypted payload store with its own keys, retention and grants.
  • No secrets or tokens — identifiers only: jti, key ID, credential fingerprint. Tool arguments are model-generated, and a model that has seen a key in context will occasionally emit it.
  • No raw tool results. A list_customers call returns thousands of records; log the count, the status and a hash.
  • No direct identifiers where a pseudonym works. Use a keyed HMAC with the key in a KMS: a plain hash of an email is reversible by brute force, and pseudonymised data is still personal data under GDPR.

Then record what you removed — policy version, detector version, count. That answers "how do you know redaction worked?": a field was captured and stripped by a known policy, not an ambiguous absence.

Immutability and retention

An audit log the application can update is not evidence. Three layers.

Append-only at the application. No UPDATE, no DELETE, no ORM access to the audit table. Write through a dedicated credential granted INSERT and nothing else, and prove it with the grant, not a code review.

Immutable at the storage layer. Object storage with WORM is the practical system of record: S3 Object Lock, Azure immutable blob storage with a locked time-based policy, GCS bucket lock. On S3, Governance mode can be overridden by anyone holding s3:BypassGovernanceRetention; Compliance mode cannot be overridden by anyone, including the account root, until retention expires. Auditors read that difference.

Tamper-evident by hash chain. Each event carries the previous event's hash, so alteration or deletion breaks the chain at a detectable point. Hourly, write a checkpoint holding the head hash and sequence range, sign it with a KMS key the application cannot otherwise use, and store it apart from the events. Anchoring it with an RFC 3161 trusted timestamp makes the chain provable to an outside party — the pattern CloudTrail uses for log file integrity validation.

Retention is a tension, not a number. SOC 2 sets none; auditors want coverage across the observation window plus history to test the prior period, which puts most teams at twelve to eighteen months. Pulling the other way are data minimisation, storage limitation and the right to erasure. Resolve it structurally: tier by event class, keeping authorisation decisions, approvals and side-effecting calls long and payloads short; keep personal data out of the immutable tier, so erasure touches the payload store rather than the ledger; and crypto-shred that store, so deleting a subject key renders payloads unrecoverable while the chain stays intact. Whether that satisfies a given erasure request is for counsel.

Human-in-the-loop as an auditable control

"A human approves high-value actions" is a control auditors like, and most implementations do not survive testing. A row saying approved_by = 'jane' is a UI event. Six things make it a control.

  1. An authenticated approver identity — a principal ID from your IdP with the authentication method, not a display name typed into a form.
  2. The authority they held at that moment. Roles get reassigned, and capturing the entitlement as it stood is what lets you test the control a year later.
  3. What they were actually shown. Hash the rendered proposal, or you can prove someone clicked approve but not what they approved — with a generated proposal, that is the whole question.
  4. A binding to the executed action. The executor recomputes the hash of the payload it is about to send and refuses if it does not match presented_hash. Without it an agent can propose one thing and execute another with the record still looking clean — the difference between an approval gate and approval theatre.
  5. Separation of duties and expiry. Approver must not be requester, and approvals must expire. A standing "approve all" is not a control; an auditor treats it as one exception per action covered.
  6. Symmetry on rejection and timeout. Log denials and expiries at the same fidelity; a population of only approvals says the gate is decorative.

Record the denominator too — how many actions required approval versus how many got one. Testing operating effectiveness samples the population, not the happy path.

Where to put agent audit logs

Most teams need three of these, not one.

Application DBObject storage + WORMSIEMLLM observability tool
ImmutabilityWeak — grants only; DBAs can writeStrong — Object Lock, provableMedium — vendor-controlledWeak — often editable
Tamper evidenceYou build itRetention plus your hash chainVaries by vendorNot a design goal
QueryabilityExcellent for run_id lookupsPoor directly; good via AthenaExcellent for correlation, alertingExcellent for traces, prompt diffs
Retention costHigh — OLTP prices for cold dataLowest by an order of magnitudeHigh — priced on ingestHigh, and tiers are short
Auditor familiarityHighHigh — the standard archive answerHighestLow — explain from scratch
Best whenIn-product evidence, joins to business dataSystem of recordDetection and investigationDebugging, evaluating prompts

Be plain about the last column. LLM observability tools are excellent at what they are for and are not an audit system of record. They give the best view of a run that exists: nested spans, prompt and completion side by side, token and latency attribution, a link from a trace to its eval. They are also mutable, sampled by default, retained for weeks not years, hosted by a subprocessor you must disclose, and built on a schema the vendor controls. Use one; do not point your auditor at it.

So: emit one canonical event to an append-only stream, write the immutable copy to WORM storage, forward a subset to the SIEM, and keep a queryable projection in your database — rebuildable, unlike the ledger.

Readiness checklist

  1. Give every agent its own workload identity. No shared service accounts, no acting under the end user's credential.
  2. Implement delegation so each event carries the human principal, the agent actor and the scopes granted.
  3. Assign a trace ID at the edge and propagate it through every model call, tool call and downstream service.
  4. Pin dated model snapshots and log the resolved version, not the alias you requested.
  5. Move prompts into version control with an ID, version and hash, through the same review process as code.
  6. Authorise every side-effecting tool call, logging the decision, policy bundle hash and matched rule — allows *and* denies.
  7. Redact inside the emitting process, version the policy, and test the detector against labelled fixtures in CI.
  8. Split payloads from the audit stream: separate store, keys, retention and grants.
  9. Write the immutable copy to WORM object storage with hash chaining and a signed hourly checkpoint.
  10. Set retention per event class, enforce it with lifecycle rules, and write down the rationale.
  11. Bind approvals to a payload hash, verify at execution, and log denials, timeouts and the denominator.
  12. Run an evidence drill. Take a random side-effecting action from six months ago and reconstruct who authorised it, under what policy, with which model and prompt version, and what changed. Over an hour means you are not ready.

FAQ

Does SOC 2 actually require an AI agent audit trail?

Not in those words — there is no list of required log fields, and anyone selling you one is selling their own checklist. The criteria are outcome-shaped: authenticate principals, restrict access, monitor for anomalies, control changes. The auditor samples actions the agent took and asks who authorised each; a record showing only that a service account called an API cannot evidence that, and you get an exception.

Is an LLM trace the same thing as an audit log?

No, and conflating them is the most common mistake here. A trace is a debugging artefact: sampled, mutable, kept for weeks, organised around spans and latency. An audit record is evidence: complete, immutable, tamper-evident, retained for the audit window, organised around actor, authority, decision and outcome. Emit one canonical event to both — a trace store cannot be your system of record, because you cannot show its contents were unaltered.

How long do we have to keep agent logs?

SOC 2 does not specify. The working answer is the observation window plus margin to test the prior period — twelve to eighteen months for most teams. Other regimes are prescriptive where SOC 2 is not: PCI DSS twelve months with three immediately available, HIPAA's six-year retention commonly read to cover audit records, an EU AI Act minimum for high-risk system logs on a timetable that has been subject to proposed amendment, so check its current status. Take the longest applicable window; keep payloads far shorter.

How do we handle PII in prompts?

Assume it will be there, because users paste things and retrieval pulls records in. Redact inside the emitting process with a maintained detector rather than hand-written regexes, and record the policy version and count so absence is provable. Hold real values only in the encrypted payload store. Then check the second-order leaks, where this usually fails: exception messages, stack traces, gateway request bodies, your observability vendor's payloads.

What if the model provider is a subprocessor?

Then their controls are in scope for your customers. Contractually: disclosure in your subprocessor list, a data processing agreement with obligations flowed down, and their assurance report — read its scope section and the complementary user entity controls, not the logo. Practically, their logs are not your logs, so your trail must stand alone: what you sent by hash and template version, what came back, which model produced it, what happened next.

Do we need this for internal-only agents?

If the agent touches systems or data in audit scope, yes. An internal agent that can read the customer database or change production configuration is exactly the privileged non-human access that gets tested. They are also where delegation is worst: built on a broad service account with full read access "for now", the hardest finding to remediate late. Lighter payload retention is defensible for low-consequence actions — agree that with your auditor in advance.

The hard parts here are rarely the schema. They are the delegation model, redaction that survives a detector regression, and getting the immutable path in place before the observation window opens rather than after. That work sits between agent architecture and LLMOps, and we do it as a scoped engagement: a review of your agent and its logging, a gap list mapped to the criteria your auditor will test, and a reference implementation.