Audit & compliance · · 7 min read
What an agent audit row should carry
The fields a SIEM-grade record needs for an agent's autonomous actions, mapped to OCSF, with redaction, retention, and tamper-evidence considerations.
When a team first puts agents on production systems, the default is to pipe application logs into the SIEM. The rows look like service-account requests, because the agent runs under a service identity, calls a resource, and gets a response. The SIEM has the action. The chain back to a person is not in the row, and neither is the policy decision.
The shape of the row matters more than the storage layer. A SIEM-grade record for an autonomous agent action needs a specific set of fields, mapped to an interchange schema, redacted at the boundary, retained on a tiered policy, and signed so it works as evidence later.
The minimum useful schema#
A single row needs these fields:
agent_id: which agent identity ran this. It is distinct from the service account the agent borrowed credentials from, and it is the durable identifier for every action a particular agent has taken.operator_id: the human who launched the run. Empty for autonomous starts (scheduled triggers, sub-agent spawns). The investigative path runs through this field: "show me everything Jane's runs did last week" has to be answerable without a join.run_id: the unique id of the current agent run.parent_run_id: if a sub-agent emitted the row, the run id of the parent. Sub-agents misbehave, and the parent context is how you trace why.tool_or_resource: what was called.pg.writediffers frompg.read, andgithub.repos.deletediffers fromgithub.repos.read. Put the distinction in the row, and skip inferring it from log text.arguments: the arguments the agent passed. Secret-bearing fields are redacted and the rest are kept verbatim. Redaction policy is part of the schema.policy_id: which policy version evaluated the action. Without it, a team investigating months later cannot tell which policy allowed the action.decision:allowed,allowed_with_approval,denied, orsuperseded.supersededcovers actions the agent attempted, the gate allowed, and a containment event later rolled back.outcome: what the called system returned (status code, error class, or success).timestamp: when the action ran, not when the row was emitted. Keep the two apart for clock-skew investigations.
The row is denormalized so it stands alone: no joins to identity, policy, or run tables.
What the row should let you answer#
A security analyst should be able to answer six questions with a single query against the row:
- Which agent did this?
- On whose behalf?
- Against which resource?
- With what arguments?
- Did the policy gate allow it, and under which policy version?
- What was the outcome?
Needing another table for any of these means the schema needs another field. This shortens audit lag: answering "who approved this write" takes one query, not an afternoon reconstructing it from three systems.
Mapping to OCSF#
The Open Cybersecurity Schema Framework (OCSF) is the cross-vendor standard for security event interchange. It defines categories, event classes, and an attribute dictionary that lets SIEMs from different vendors consume the same events. AWS Security Lake is OCSF-native (docs).
As of mid-2026, OCSF has no dedicated event class for autonomous agent activity. Agent actions fit under existing classes: process activity (the agent calling a tool), authentication activity (identity issuance), and API activity (outbound calls). The fields above map onto the OCSF attribute dictionary as follows:
actor.user.uidholds the operator.actor.process.uidholds the agent identity.target.resource.nameholdstool_or_resource.metadata.event_codeholds the decision.unmappedholdspolicy_idand the run-graph fields until a dedicated class lands.
A row written for OCSF compatibility ingests into any OCSF-aware SIEM without custom parsers. Write the row once in the richest schema you can support, then map fields outward.
A sample row#
In JSON, an agent's github.repos.delete action against the acme/proto-docs repository, allowed by policy version v2026.06, looks like this:
One row answers who acted, under which policy, and what happened.
{
"timestamp": "2026-06-17T14:23:11.421Z",
"agent_id": "agent_4f2a_codeReview_v1",
"operator_id": "user_jhaas",
"run_id": "run_01HZK4M7",
"parent_run_id": null,
"tool_or_resource": "github.repos.delete",
"arguments": {
"owner": "acme",
"repo": "proto-docs",
"token": "[REDACTED:credential]"
},
"policy_id": "policy_v2026.06.12",
"decision": "allowed_with_approval",
"approval_id": "approval_01HZK4M8",
"outcome": {
"status": 204,
"duration_ms": 312
}
}
The row answers the six questions above with no join.
Two fields agent runtimes often omit#
Runtimes most often forget policy_id and parent_run_id.
Without policy_id, the row cannot show which policy allowed the action. Take a customer who reports unauthorized access on May 1. The investigation runs on June 15, and the policy was changed on May 12 in response to a different incident. Did the May 1 action fire under the old policy or the new one? Without the version on the row, the investigator reconstructs it from change logs and best-guess timestamps, which does not meet the evidentiary standard of "on the row at write time." The investigation ends inconclusive or escalates to forensic preservation, which means freezing systems that should still be serving traffic.
Without parent_run_id, a misbehaving sub-agent cannot be traced to the parent run that spawned it. Agent stacks fan out: a top-level run produces sub-agents for parallel work, and those spawn their own. When one commits an action that escapes the operator's intent, the run graph is the only thread back.
Both fields are cheap to add at write time and nearly impossible to backfill.
Field-level redaction#
Some arguments must not appear in the audit row verbatim: credentials, OAuth tokens, customer PII (in jurisdictions that regulate PII separately from access logs), and any field the model risk team has classified as sensitive. Redaction happens at the boundary, in the runtime that emits the row, and not in a SIEM-side post-processor.
The row should support three redaction modes:
- Masked. Replace the value with a typed placeholder (
[REDACTED:credential],[REDACTED:pii_email]). The shape of the call survives and the secret does not. - Hashed. Replace the value with a stable hash. Investigators can confirm "this row touched the same row id as that row" without seeing the id. Use a per-tenant pepper so hashes cannot be correlated across tenants.
- Verbatim. Keep the value as-is and rely on the SIEM's downstream access controls. Use this only for arguments with no sensitivity classification.
Choose hashing when investigators need to correlate across rows, and masking when they do not.
Three-tier retention#
Audit rows lose value per byte of storage over time, so retention works best in tiers:
- Hot tier, 30 to 90 days. Indexed, queryable on every field, supports real-time alerts. This is the SIEM's normal storage layer.
- Warm tier, 1 to 3 years. Cheaper storage, queryable for compliance audits and slower investigations. An object store with periodic indexing is common.
- Cold tier, 3 to 7 years, or longer where regulation requires. Write-once, immutable storage: an object store with object-lock, or a WORM-compliant archive. Read access is rare and authorized per investigation.
Confirm the retention period and retrieval requirements with the legal and security owners before moving records out of the queryable store.
The reasoning trace question#
A common impulse is to log the agent's reasoning text (chain of thought, planning trace, stated intent) into the audit row. This is mostly wrong, for three reasons.
- Volume. A single run can produce tens of thousands of tokens of internal reasoning. Indexed in a SIEM, that storage and query cost swamps the action rows themselves.
- No evidentiary weight. The model says it intends to do X, and the security team cares what the model did. The audit row already records the action, and the reasoning text adds explanation without proof.
- Ingested content. The reasoning text often contains content the agent pulled from customer data or third-party APIs, which carries its own privacy and licensing implications. Persisting it in the audit log creates a much larger data-handling surface than the access decision requires.
Keep a structured reasoning record in a separate run-store instead. The audit row references it by id, and the run-store has its own retention, redaction, and access controls. Investigators pull the reasoning when needed, and the SIEM does not carry it.
Tamper-evidence#
To serve as evidence for a regulator, a legal proceeding, or an internal investigation, the audit row has to be tamper-evident. The minimum is hash-chaining: each row carries a hash that incorporates the previous row's hash, so modifying an old row breaks the chain for every row after it. This is the primitive under blockchain audit logs and Sigstore's transparency log, and it does not require a blockchain.
A higher bar is signing. Each row is signed by the runtime that emitted it, with the signing key rotated through your KMS. Investigators verify rows independently, and an attacker with database access cannot forge rows without the key.
For most enterprises shipping agents in 2026, hash-chaining is the right baseline. Signing is the right target for regulated use cases such as healthcare, financial services, and the public sector. Hash-chaining costs almost nothing at write time, and retrofitting it means re-emitting history, which most teams will not do.
Deixic keeps a tamper-evident history of policy and approval decisions and their results. The schema above is a design checklist for your own runtime and export pipeline. See evidence and activity.