Agent action audit record: a practical model for control
Build an agent action audit record that captures process identity, targets, approvals, outcomes, revocations, and tamper evidence without logging secrets.

Agent actions need records that describe authority, not just activity. A line such as POST /deploy returned 200 cannot tell an investigator whether the right process made the request, whether a person allowed it, which system received it, or whether a later revoke stopped anything.
The practical unit is an immutable action record joined to session, approval, and revocation events. Keep the action record small enough to search and verify, but specific enough that a tired engineer can reconstruct a disputed call without inventing a story around missing fields.
An audit entry must describe an authorized attempt
An agent action audit record captures one attempted effect on an external system and the authority context present at that instant. An action can fail before it reaches the network, fail during delivery, receive a refusal from the target, or complete successfully. All four cases belong in the audit trail.
Teams often log one of two inadequate things. They log a chat transcript, which describes intent but not execution. Or they log raw request traffic, which produces too much secret material and still misses the human decision that permitted the call. Neither record answers whether a particular actor had authority to perform a particular operation.
Treat these as separate objects:
- A session identifies one running agent process and its lifetime.
- An approval records a human decision with a defined scope.
- An action records one requested external operation and its result.
- A revocation ends a session or authorization at a precise time.
The distinction has a consequence during an incident. If a person revokes a session at 14:03, you need to see every action before 14:03, every attempted action after 14:03, and the local decision that denied those later attempts. A single mutable row labeled "session approved: false" destroys that history.
Do not make the record model depend on whether the action succeeded. The request itself can show malicious intent, a configuration defect, or an agent that misunderstood its task. Failed calls are often the first useful evidence.
The actor is a process, not a friendly agent label
Record the process that actually requested the action. A model name, workspace name, or agent nickname may help a human scan a report, but none uniquely identifies the executable that held the connection at the time.
A useful actor object contains an internal session identifier, process ID, executable path, launch timestamp, and code signing authority when the operating system provides it. Include the parent process ID and parent executable when you can collect them reliably. The parent relationship often explains whether a terminal launched the agent, an editor extension spawned it, or an unexpected helper did.
Use a process identity snapshot rather than resolving the process later. Process IDs get reused. Paths can change. A later lookup cannot repair a missing observation.
{
"actor": {
"session_id": "ses_01J8Q1F9K9Z3",
"pid": 48217,
"started_at": "2025-02-18T21:14:06Z",
"executable": "/usr/local/bin/agent-runner",
"signing_authority": "Developer ID Application: Example Developer",
"parent_pid": 48091,
"parent_executable": "/Applications/Terminal.app"
}
}
The value in signing_authority should come from the platform's signing assessment, not a string supplied by the agent. An agent can call itself claude-code, deploy-helper, or trusted-agent. A label is decoration unless the operating system ties it to an executable identity.
Do not turn process identity into a false claim of authorship. A signed binary can act badly after a prompt injection, a compromised plugin, or a careless instruction. The field establishes which code authority initiated the request. It does not certify the wisdom of the request.
RFC 5424, the syslog protocol specification, separates header fields such as application name and process ID from structured data. That separation still makes sense for agent records. Put stable identity and timing in dedicated fields that machines can query. Put variable context, such as repository or task reference, in a namespaced object. When everything goes into one message string, every later investigation becomes text parsing.
A target needs both an address and a meaning
Record where an action went and what resource or command it sought to affect. Those are related facts, but they are not interchangeable.
For HTTP, the network destination might be api.example.internal, while the meaningful operation is POST /v1/releases/{release_id}/promote. Store the host, port, protocol, HTTP method, and a route template. Add a target system identifier that your team controls, such as release-service-prod. The identifier survives a hostname migration; the hostname helps diagnose the request that actually left the machine.
For SSH, store the host alias or host name, port, host key fingerprint or a reference to the known-host entry, and remote account label where that label is safe to retain. The target should also name the requested command class. restart-worker explains more than ssh succeeded, yet it reveals less than a full shell command containing customer paths and environment variables.
Use redaction before persistence, not after an analyst opens the log. URL query strings, request headers, shell arguments, and JSON bodies regularly contain tokens. A logging library that captures them "for debugging" will eventually produce an incident file full of credentials.
This record shape keeps target facts queryable without copying request content wholesale:
{
"target": {
"kind": "http",
"system_id": "release-service-prod",
"endpoint": {
"scheme": "https",
"host": "api.example.internal",
"port": 443,
"method": "POST",
"route_template": "/v1/releases/{release_id}/promote"
}
},
"operation": {
"name": "promote_release",
"request_fingerprint": "sha256:8e8c...",
"request_bytes": 286,
"redacted_parameters": {
"environment": "production",
"release_id": "rel_7b2"
}
}
}
The fingerprint only helps if you define canonicalization. Sort object fields, remove fields your redaction policy excludes, normalize text encoding, and then hash the resulting bytes. Record the canonicalization version. Otherwise two equivalent requests can produce unrelated digests, and a future schema change can make old records look suspect.
A digest cannot replace retained evidence when a regulator, contract, or incident process requires the original request. In that case, encrypt the evidence separately, restrict access, and store its content digest in the action record. Do not place the original body in the ordinary searchable journal merely because storage is cheap.
Requested operation and observed result need different fields
An agent request expresses intent. The result reports what the gateway observed after trying to execute that intent. Do not merge them into a vague status such as completed.
For a network call, record the transport phase where execution stopped, the protocol status where one exists, a bounded response summary, and the time spent. For SSH, record connection outcome, authentication outcome, remote exit code, and bounded standard output and error summaries. A target can accept a request and perform an asynchronous job later, so a 202 response does not mean the external change occurred.
Use an outcome vocabulary that states where the failure happened. For example:
denied_vault_lockedmeans the local secret boundary refused the call.denied_approvalmeans the required human decision did not permit it.network_errormeans the gateway could not establish or retain a connection.target_rejectedmeans the remote service returned a refusal.target_acceptedmeans the remote service accepted the request.
Keep the raw transport error out of the primary status field. Put a normalized code such as dns_lookup_failed or tls_validation_failed beside a short, redacted diagnostic. Engineers need aggregation; incident responders need enough local context to tell an expired certificate from a blocked hostname.
A complete result object might look like this:
{
"result": {
"outcome": "target_accepted",
"started_at": "2025-02-18T21:19:42.184Z",
"finished_at": "2025-02-18T21:19:43.021Z",
"duration_ms": 837,
"http_status": 202,
"response_fingerprint": "sha256:2a64...",
"response_summary": "promotion job accepted",
"evidence_ref": null
}
}
Avoid calling target_accepted a success in your schema. That word causes trouble later. If the service accepts a job and the job fails, the gateway did exactly what it should have done, while the business operation did not finish. A separate remote completion event, correlated by a job ID, can answer that later question.
Approval records must state what the person approved
An approval record needs a decision, a scope, a subject, and timestamps. "User approved agent" says too little. It leaves reviewers guessing whether the person authorized one API call, a particular credential, a session, or every future session from a similarly named process.
A session approval commonly permits calls by one identified process until that process exits or someone revokes it. A per-call approval applies to one use of one credential or operation. Record the scope directly, because the two controls produce very different risk.
{
"approval": {
"approval_id": "apr_01J8Q1P4Y5D6",
"decision": "approved",
"scope": "session",
"subject_session_id": "ses_01J8Q1F9K9Z3",
"approved_at": "2025-02-18T21:14:11Z",
"expires_at": "2025-02-18T22:02:53Z",
"approver_presence": "local_user_confirmation"
}
}
Do not claim more certainty than your interface can provide. If the application receives a local confirmation event, record that fact. Do not record a person's name, identity provider account, or biometric method unless the system truly authenticates and retains that association under a documented policy. Invented identity detail creates false confidence and creates privacy obligations.
Store the approval that governed an action as an identifier on the action record. Also store the authorization decision that occurred immediately before execution. This looks repetitive until you investigate a timing edge case: an old session approval may exist, but a per-call requirement may deny the action. The action needs both facts.
Approval fatigue is a design defect, not a reason to omit approval evidence. If a person must approve every harmless request, they will approve without reading. Reserve per-call confirmation for credentials or operations whose misuse needs direct attention, and make the record say why the gateway asked.
Revocation is an event with a cutoff time
Revocation ends authority going forward. It does not erase a session, retract an already delivered request, or change the approval decision that existed five minutes earlier.
Record the revocation target, the initiator type, the observed timestamp, and the enforcement result. If the gateway can terminate or block an active session, record whether it did so. If an action already crossed the network boundary, say that the revoke cannot recall it. Engineers need that unpleasant fact during response, not a comforting but false revoked label.
Consider this failure sequence:
- An agent process receives a session approval at 09:00 and submits a production change at 09:17.
- The operator notices an unexpected target and revokes the session at 09:18:04.
- The target responds to the 09:17 request at 09:18:07 because it had already queued work.
- The agent attempts another call at 09:18:09, and the gateway denies it.
A good journal preserves all four events. The action at 09:17 was authorized when it began. The response after revocation belongs to that earlier action. The denied attempt proves that the revoke took effect for later work. If you stamp every earlier action "revoked," you lose the sequence that explains the actual exposure.
Use a monotonic sequence number in addition to wall-clock time. Clocks can drift, users can change local time, and events can share a timestamp resolution. A sequence number establishes the order in which the journal accepted records. If you operate across hosts, preserve each host's local sequence and use correlation IDs rather than pretending that wall clocks establish a perfect global order.
Tamper evidence depends on append-only discipline
A hash chain makes undetected editing harder by including the prior record's digest in each new record. It does not turn an ordinary log file into proof that every expected event exists. Someone who controls the writer and its stored chain head may delete a suffix, start a new chain, or prevent records from reaching durable storage.
That limitation does not make hash chains pointless. They answer a narrower, useful question: did these records change, disappear from the middle, or arrive in a different order after the system wrote them? Keep the chain fields explicit.
{
"journal": {
"sequence": 1842,
"recorded_at": "2025-02-18T21:19:43.024Z",
"previous_hash": "sha256:68b1...",
"record_hash": "sha256:93f4...",
"hash_format": "canonical-json-v1"
}
}
Hash the complete canonical record excluding only record_hash itself. Do not hash a pretty printed representation whose whitespace, field order, or timestamp formatting changes between versions. Version the canonical format and retain the verifier for every format you emit.
NIST Special Publication 800-92, Guide to Computer Security Log Management, treats log generation, storage, analysis, and retention as separate responsibilities. That distinction cuts through a frequent mistake: teams add a hash to entries and call the job finished. You still need durable storage, access controls around the writer, a retention decision, regular verification, and an investigation procedure for a failed check.
Sallyport projects its Sessions and Activity journals from one write-blind encrypted, hash-chained audit log. Its sp audit verify command verifies the chain offline over ciphertext without requiring a vault key. That design separates a reader's ability to check journal integrity from the ability to use credentials.
A single correlation ID makes the record usable under pressure
Give every requested action an action ID before the gateway evaluates authorization. Carry it through the local decision, the network attempt, response handling, and any later remote completion callback. When an engineer sees a disputed deployment, they should search one identifier and retrieve the full timeline.
Use related IDs for different relationships. The action points to its session, approval, credential reference, target, and journal sequence. A task or conversation reference can point from the action back to agent context, but do not make the conversation transcript the source of truth. Prompt text changes, may contain private material, and often fails to describe the eventual request precisely.
A compact complete record can use this shape:
{
"schema_version": "1.0",
"action_id": "act_01J8Q2ABR8M7",
"event_type": "action.completed",
"actor": {"session_id": "ses_01J8Q1F9K9Z3", "pid": 48217},
"target": {"kind": "http", "system_id": "release-service-prod"},
"operation": {"name": "promote_release", "request_fingerprint": "sha256:8e8c..."},
"authorization": {"vault": "unlocked", "approval_id": "apr_01J8Q1P4Y5D6", "decision": "approved"},
"result": {"outcome": "target_accepted", "http_status": 202},
"journal": {"sequence": 1842, "previous_hash": "sha256:68b1..."}
}
Do not use the correlation ID as an authorization token. Generate it independently, make it unpredictable where external callers can observe it, and never accept possession of the ID as permission to read an action or issue a follow-up call.
Retention must preserve evidence without creating a second secret store
Audit data accumulates private material even when you redact aggressively. Target names can reveal customers, route parameters can reveal internal projects, response summaries can expose account state, and process paths can expose developer habits. Decide who may search records, who may export them, and how long each category remains available.
Keep searchable action metadata separate from any encrypted request or response evidence. Apply shorter retention to detailed evidence when the investigation need expires. Preserve hashes and linkage records long enough to prove that remaining summaries still correspond to what the system recorded.
Test the model with a real question from an incident review: "Which signed process requested access to this production target, under which approval, and did any attempt continue after revoke?" If a query needs an analyst to join unstructured messages, inspect client debug logs, or ask the agent what it remembers, the model is incomplete.
Sallyport's fixed decision ladder gives this record model clean boundaries: the vault gate, session authorization, and per-call credential approval each produce a distinct authorization fact. Keep those facts distinct in your own audit design. The record should show where authority stopped, rather than burying every denial under a generic failure code.
Build the schema before agents gain broad credentials. Retrofitting actor identity, approval scope, and revoke ordering after an incident means reconstructing authority from gaps. That is slow work, and it usually ends with someone saying "we think" when the record should have said exactly what happened.
FAQ
What is the difference between an agent session log and an action audit log?
A session log says that a process existed during a period of time. An action audit record says what that process asked to do, which target received the request, which credential boundary applied, whether a person approved it, and what happened. You need both because a revoked session may have made many calls before revocation.
Should an audit record store only successful agent actions?
No. A successful HTTP status only describes the response visible to the caller. Record the transport outcome, the status code, a bounded response summary, and any locally detected execution error so an investigator can distinguish refusal, timeout, and a completed remote change.
How should I identify a target system without logging secrets?
Use a stable internal target identifier and a redacted target description, such as an API host and route template or an SSH host alias and command class. Do not store raw credentials, authorization headers, tokens in URLs, or entire request bodies by default. A log that copies secrets has failed its own security purpose.
Why does the code signing authority belong in an agent audit trail?
Record the executable path, process ID, parent process ID where available, launch time, and code signing authority. The signing authority answers a different question than a process name: it helps you tell an expected binary from an unrelated program using a familiar label.
How do I record session approval versus per-action approval?
One approval can cover a process session, while another can cover one use of a sensitive credential. The record must name the approval scope and its decision. Without scope, reviewers often assume that one earlier approval authorized a later high-impact call when it did not.
Should revocation overwrite prior approved actions?
Keep the original action records immutable and add a revocation event that names the session or authorization it ended. Do not rewrite earlier records to say that they became unauthorized later. The time ordering tells you exactly which calls occurred before the revoke took effect.
Can a hash-chained audit log prove that an agent action was safe?
A hash chain detects deletion, insertion, or modification when you retain the expected chain state and verify the sequence. It does not decide whether an approved action was wise, and it cannot prove a remote system honored a request. Pair it with precise records and, where needed, remote-side logs.
What should I log for HTTP requests made by AI agents?
Start with a request fingerprint, not a raw body. Store the method, route template, selected nonsecret parameters, body length, and a cryptographic digest of a canonical redacted representation. Retain encrypted evidence separately only when your investigation and retention rules justify it.
What belongs in an SSH action audit record?
Capture the SSH destination, host key fingerprint or known-host reference, remote account label if it is not secret, command class, exit code, and bounded stdout and stderr summaries. Treat a full command line as sensitive because it often contains paths, identifiers, and accidental tokens.
What questions should an agent action audit trail answer?
Someone investigating an incident should reconstruct who launched the actor, what it requested, which approval governed it, which target received it, what result came back, and whether revocation changed the process's authority. If a record cannot answer one of those questions, add a field or an associated event.