# Can an approval audit trail prove who approved an agent action?

An approval record that says only "approved" does not answer the question that matters after an agent touches production: who allowed which action, under what authority, and what happened next? It records a comforting moment in a user interface, then leaves an investigator to infer the rest.

An approval audit trail has to preserve the chain from an agent process to a human decision, from that decision to a credential use, and from the credential use to a completed HTTP request or SSH command. If those are separate records without durable links, a reviewer can tell a plausible story. They cannot prove one.

This distinction becomes painful when the action succeeded but nobody remembers approving it, when an agent restarted halfway through a task, or when someone asks whether a click and a Touch ID confirmation carried the same meaning. They do not. Treating them as interchangeable produces a log that looks complete until the first serious review.

## An approval event must answer more than "yes"

A useful approval event says what the human approved, why the system asked, how the decision was made, and where the grant stops. The visible button click is only one field in that event.

Capture these facts when the decision occurs:

- A unique approval identifier and an event timestamp with an offset.
- The approval method, such as `click` or `touch_id`.
- The local account or other known approver identity, plus the evidence used to make that attribution.
- The scope of the grant: a session or one credential use for one call.
- The request that caused the prompt, identified by a stable request or call identifier.

Do not write `user=alex` merely because a machine has an account named Alex. That may be the best available attribution, and it is still worth recording, but call it what it is: local account context. If a biometric prompt succeeded, record that a biometric enrolled on that device authorized the event. Those statements are stronger than "someone named Alex approved it" because they do not pretend the audit log knows more than it does.

NIST SP 800-171 Rev. 3 gives a sensible starting point for audit content: timestamps, source and destination addresses, user or process identifiers, event descriptions, applicable access controls, and outcomes. It also notes that detailed records may include privileged commands and individual identities behind shared accounts. That is a good floor for agent actions, not a finished design. An agent approval flow needs the relationships between the decision, the credential, and the call as first-class data.

The easy mistake is to record an approval as an attribute on the final action, such as `approved=true`. That collapses an event into a label. You lose the time between the request and the decision, the source of the decision, the scope of the consent, and any later revoke. It also makes it impossible to distinguish a user approval from a default allow, a cached grant, or an automation rule.

A decision should have its own record even when the answer is no. A rejected prompt can explain why an agent failed to deploy. An expired prompt can explain why the agent retried. A vault lock can explain why the system refused to make a network call at all. These are materially different events, and a later reviewer should not have to deduce the difference from an absent success record.

## Five identities keep the timeline honest

A complete timeline needs five separate identities. Combining them saves columns in a table and destroys meaning in an investigation.

The first is the **agent process**. Record a process or run identifier, the executable or code-signing authority that launched it, its start time, and its end time. The human should be able to answer, "Which running program asked for this?" A project name or a chat transcript is not enough. Two copies of the same coding agent can run at once, and one may be benign while the other is pointed at a different repository.

The second is the **session**. A session is the bounded relationship between one agent process and the gateway. It must have its own identifier because a process may make many calls, and because session authorization often applies across several calls. When the process exits, the session should end. If a new process starts later, it should create a new session even if it has the same executable, the same local account, and the same task description.

The third is the **approver context**. This includes the device account, any authenticated application user, and the approval method. Do not make the approver field carry facts it cannot support. `local_account=maya`, `method=touch_id`, and `device_id=...` are clear. `human=maya` claims more. In some environments that claim is reasonable; in others, a shared workstation or an unlocked desktop breaks it immediately.

The fourth is the **credential reference**. This identifies the authority the gateway used, not the secret itself. A stable opaque credential ID, a human-readable label, the channel, and the credential type are normally enough for activity review. A bearer token is not an audit field. An SSH private key fingerprint can itself become sensitive context, so decide whether your investigators need it before spreading it through ordinary logs.

The fifth is the **executed operation**. For HTTP, this is the resolved destination identity, request method, normalized path, selected nonsecret request facts, response status, and timing. For SSH, it is the host identity, remote account, command or a digest of the approved command, exit status, and timing. The event must say what ran, not merely what was requested.

These identities form a graph, not a flat row:

```text
agent_process
  -> session
    -> approval_decision
      -> credential_use
        -> executed_call
```

A flat activity screen can present that graph as one line when the user needs speed. Keep the underlying links anyway. The display is for people scanning a day of work. The identifiers are for the person who has to explain one call six weeks later.

## A click and Touch ID are different evidence

A click records interaction with an approval control in the current user interface. Touch ID records a successful biometric authorization through the operating system, in addition to the interaction that initiated it. Both can authorize an action. They should not share a vague value such as `approved_manually`.

Use an explicit method field with a controlled set of values. For example:

```json
{
  "approval_id": "apr_01J8K4VY5Q",
  "occurred_at": "2026-07-22T14:18:06.184Z",
  "decision": "approved",
  "method": "touch_id",
  "approver": {
    "local_account": "maya",
    "identity_assurance": "device_account_and_biometric"
  },
  "scope": "credential_use",
  "session_id": "ses_01J8K4TE0M",
  "requested_call_id": "call_01J8K4VPM2"
}
```

The exact field names are not sacred. The separation is. `method` tells you how the approval completed. `identity_assurance` tells you what the system can responsibly say about the person. `scope` tells you what the decision authorized. `requested_call_id` ties the approval to a request that existed before the human saw a prompt.

A click can be the right choice for low-friction confirmation, especially when a person is already watching an agent work. Touch ID adds a stronger local confirmation step for a sensitive operation, but it does not magically supply a corporate identity, a reason for the decision, or an endorsement of every later call. If a team needs a named employee approval with an external identity provider, it needs a flow that records that identity provider's assertion. Do not silently borrow that assurance from a local biometric event.

The converse error is just as bad: treating Touch ID as decorative. If an action required biometric approval and the log reduces that event to `approved=true`, the record cannot show that the stricter control actually ran. The review loses evidence that could separate a deliberate confirmation from an accidental click on a broad session prompt.

Record unsuccessful biometric attempts carefully. An audit trail usually needs to know that the requested action did not receive approval, but it rarely needs every operating-system level authentication failure. A useful event is `decision=denied_or_cancelled`, `method=touch_id`, and a reason such as `user_cancelled` where the platform makes that distinction available. Do not turn an action gateway into a biometric telemetry collector.

## Session consent and per-call consent have different scope

Session authorization grants a bounded agent process permission to operate after a human reviews the process identity. Per-call approval grants consent for one credential use and one operation. Calling both "approval" without recording scope makes the later timeline misleading.

Consider an agent process that starts at 09:00. The gateway shows an authorization card that identifies the process's code-signing authority. A developer clicks approve. At 09:20, the agent makes an HTTP request with a credential whose settings do not require per-call confirmation. That call may be allowed because the session remains authorized. The correct timeline shows two separate facts:

1. At 09:00, the developer approved session `ses_...` for the lifetime of that process.
2. At 09:20, that session used credential `cred_...` to make call `call_...`.

It should not create a fictional 09:20 user approval. The developer did not see and approve that exact call. The earlier grant covered it.

Now change one setting: the credential requires approval on every use. At 09:20, the gateway asks again and the developer approves with Touch ID. The new event must point to `call_...`, state `scope=credential_use`, and include `method=touch_id`. The session approval remains relevant because it explains why the agent could reach the credential request. It does not replace the second decision.

This distinction matters most when an agent makes a surprising call late in a session. If the log says "approved" beside it, the reviewer needs to know whether that means a human approved the executable thirty minutes earlier or approved this exact credential use three seconds earlier. Those facts have very different implications for prompt design, credential settings, and incident response.

Do not solve the ambiguity by making every call require approval. That recommendation is popular because it sounds safe and it produces a satisfying number of records. It also trains people to approve repetitive prompts without reading them, then leaves them unable to tell the one exceptional call from the routine ones. Put per-call approval on credentials whose use needs fresh human confirmation. Keep session authorization on by default to bind the agent process to an accountable approval boundary.

A revoke also needs scope. If an operator revokes a session, write the revoke event against the session and record the effective time. Do not overwrite the old approval. If a user disables or removes a credential, record that change separately. An audit timeline should show why a later call was denied without rewriting history to make the former grant disappear.

## Credential records must identify authority without exposing it

The credential-use record is where many teams make a dangerous trade: they add secret material to make an investigation easier. That is a bad bargain. Logs are copied, indexed, exported, and retained longer than the process that generated them. A secret in an activity journal turns every log reader into a credential holder.

Give every stored credential an opaque immutable identifier, such as `cred_01J8K...`. Pair it with a label that helps a human recognize purpose, such as `payments-readonly` or `staging-deploy`. Record the channel and injection mode, for example `http_bearer`, `http_custom_header`, or `ssh_key`. That gives an investigator enough context to ask the right question without copying the key into the record.

A practical credential-use event can look like this:

```json
{
  "credential_use_id": "use_01J8K4WHD7",
  "occurred_at": "2026-07-22T14:18:06.221Z",
  "credential": {
    "id": "cred_01J7ZB7F8P",
    "label": "inventory-production",
    "channel": "http",
    "injection": "bearer"
  },
  "session_id": "ses_01J8K4TE0M",
  "approval_id": "apr_01J8K4VY5Q",
  "call_id": "call_01J8K4VPM2",
  "secret_exposed_to_agent": false
}
```

The `secret_exposed_to_agent` field may seem redundant when the gateway design guarantees it. Keep it if the timeline can include multiple execution paths or migrations. It makes the security property inspectable in the same record as the action. If every supported path has the same guarantee, the field can be implicit in the system design and documented once.

Separate credential selection from credential use. An agent can request a credential by label, but no credential has been used until the gateway begins the outbound operation. This matters for denials. If Touch ID is cancelled before the request leaves the machine, write an attempted call and a denied approval event. Do not write a successful credential-use event. Otherwise your audit count will claim that a production credential was used when it was not.

For SSH, avoid treating a host alias as the complete destination identity. `prod-db` is readable, but aliases can change. Record the configured target and the host identity evidence your connection flow verifies. If the agent asked for `prod-db` but the resolved target differed, that difference belongs in the execution record. It is exactly the sort of detail that matters after a bad deployment.

## The executed call is the evidence that the action happened

Approval proves consent. Credential selection proves intended authority. Only an execution record tells you whether the gateway attempted the outside-world operation and what result came back.

For HTTP calls, record the operation in a normalized form. Keep the request method, destination origin or service identity, canonical path, selected query-field names when useful, response status, start and finish timestamps, and a result reference. Decide deliberately which request and response fields are safe to retain. Authorization headers, cookies, token-shaped values, full request bodies, and raw response bodies should not land in a general activity timeline.

A request digest can help prove that an approved payload and an executed payload matched, but only if you define its input exactly. Hashing a JSON body without canonicalizing field order creates false mismatches. Hashing a body that contains a small predictable value can still help an attacker confirm guesses. Use a digest for integrity correlation when the payload is already protected elsewhere, not as a universal substitute for content handling.

For SSH, log the remote account, destination identity, command representation, exit status, and start and end times. A full command line can contain secrets in environment assignments, temporary URLs, or arguments. One sensible compromise is to store a safe rendered command for routine review and a protected full representation or digest for investigation. Do not claim a digest is readable evidence. It tells you that two values match; it does not tell a reviewer what the command did.

RFC 5424 separates a timestamp and message identity from structured data because parsers need dependable fields rather than prose they have to guess at. Its timestamp format also carries a time offset and permits fractional seconds. You do not need to emit syslog, but the design lesson holds: keep event types and correlation fields structured, then reserve human text for explanation.

Use distinct event types. `call.requested`, `call.dispatched`, `call.completed`, and `call.failed_before_dispatch` say more than an overloaded `call` event with a status field that changes meaning. The extra records let you answer whether a network timeout happened after credential injection, whether a local validation blocked the request first, and whether the remote service returned a response.

Time alone cannot establish order across machines. Use UTC timestamps with offsets and retain a monotonic sequence number inside each local audit log. If the remote API returns its own request ID, store it as a remote correlation value. That gives an investigator a way to compare the local timeline with a vendor's records without pretending the clocks match perfectly.

## A broken timeline hides in ordinary success logs

Picture a deployment agent that receives session approval at 10:02. It reads a repository, prepares a release, then calls a production deployment endpoint at 10:17. The endpoint accepts the request. At 10:18, the developer notices the wrong environment was selected.

A weak log contains this:

```text
10:02 approved agent
10:17 deployment API call succeeded
```

That log answers almost nothing. Was the 10:17 call covered by the 10:02 approval? Did the credential demand a second prompt? Which process made the call? Did the agent use the intended deployment credential or a broader token? Did the system send the request to production, or did a redirect or configuration error move it there? Did the human click approve, use Touch ID, or never see an action-specific prompt?

A useful timeline instead reads like this:

```text
10:02:11  session.opened       ses_71  process=proc_44 signer=known_authority
10:02:14  approval.approved    apr_02  method=click scope=session session=ses_71 account=maya
10:17:03  call.requested       call_88 POST deploy.example/release target=production session=ses_71
10:17:04  credential.selected  use_53  credential=cred_prod_deploy call=call_88
10:17:04  call.dispatched      call_88 destination=deploy.example
10:17:06  call.completed       call_88 status=202 remote_request=req_914
```

This record may establish that the agent had valid session authorization but did not receive a call-specific approval. That is not proof that the deployment was desired. It is evidence about how the control operated. The team can then decide whether the production credential should require per-call approval, whether the prompt needs to display the target environment more plainly, or whether the agent should not have access to that credential at all.

Now add per-call confirmation. The correct additional entry is not another generic `approved` line. It should state the call it approved and its scope:

```text
10:17:04  approval.approved    apr_03  method=touch_id scope=credential_use
          session=ses_71 call=call_88 credential=cred_prod_deploy account=maya
```

If the agent retries after a timeout, give the retry a new call ID. It may reuse an existing session grant, but a per-call credential rule should create a new approval requirement for the retry. Recording the retry as though it were the original call produces a false impression that one confirmation covered two external actions.

## Audit integrity needs a separate claim

An audit log can be complete enough to explain a sequence and still be easy to alter. It can be append-only in intention and still allow an administrator or malware with local access to remove the inconvenient lines. Treat content and integrity as separate properties.

A hash-chained journal links each record to the preceding record through a cryptographic digest. Change an old record and the later chain no longer verifies. This is useful because an exported activity screen can be challenged against the underlying journal rather than accepted on trust. It does not prove that the original system recorded every event, that a compromised writer did not create false records, or that a valid approval was a good decision. Those are different claims that require different controls.

Verify at the boundary where evidence leaves the system. An investigator should be able to take the encrypted record stream, run an offline integrity check, and learn whether the sequence is intact without exposing credentials just to validate a chain. The verification output should identify the checked range, the chain status, and the first failing sequence if verification fails.

Sallyport projects its Sessions and Activity journals from one write-blind encrypted, hash-chained audit log, and `sp audit verify` can verify the chain offline over ciphertext without a vault key. That arrangement matters because the session decision and the individual operation remain views of the same evidence, not independently editable stories.

Do not use integrity verification as a reason to keep excessive data forever. Retention, access control, and redaction still matter. A perfectly preserved log full of secrets is an incident waiting for a convenient search query. Define who can inspect raw records, who can export them, how long records remain available, and which fields are safe in routine views.

## Build the timeline around joins, then test the ugly cases

A schema review should start with a blunt question: can an investigator start with any executed action and walk backward to the approval without guessing? If not, add the missing identifier before you polish the activity screen.

Run a small test matrix against the implementation. You do not need a large simulation. You need cases that expose scope and ordering errors:

- Start a new agent process, approve its session with a click, and make a low-risk call.
- Use a credential that requires per-call approval, authorize it with Touch ID, and confirm that the call points to that approval.
- Cancel the biometric prompt and confirm that no successful credential-use record appears.
- End the agent process, start it again, and verify that the new process cannot inherit the old session grant.
- Force a remote failure after dispatch and check that the timeline distinguishes dispatch from completion.

Inspect the result from both directions. Begin with the approval and list every action that relied on it. Then begin with the executed call and trace the process, session, decision, and credential. The first view finds grants that were broader or longer-lived than expected. The second finds calls with broken or ambiguous evidence.

Keep the language in the interface as strict as the data model. "Session approved by click" is clear. "Production deployment credential approved with Touch ID for this call" is clear. "Approved" is a decorative status word that asks the reader to invent the most important details.

The first time someone asks who approved an agent action, do not hand them a screenshot with a green badge. Hand them a timeline that shows the process, the decision method, the scope, the credential authority, the exact call, and the result. Anything less may be convenient during a demo. It will not hold up when the action matters.
