# How shared API account attribution works across agent sessions

A shared API account is sometimes the right operating choice. Vendors may issue one organization token, rate limits may belong to that account, and replacing it with a pile of near identical keys can create more secrets without improving control.

The mistake is treating that account as an actor. It is an external credential principal: the identity the vendor recognizes. When several local agents use it, you need a second record that states which local process made each request, what work it was authorized to do, and how that process obtained authority to use the credential. If you do not collect that record at execution time, you will be guessing later.

This is less glamorous than giving every agent a name and a colorful dashboard. It is also what holds up when two agents race, one retries, a human revokes a run halfway through, and the vendor's audit page says only `automation-service`.

## A vendor account and an actor are different identities

A shared vendor account answers, "Which credential did the provider accept?" Actor attribution answers, "Which local principal caused this specific operation?" Those answers often differ, and forcing them into one field creates bad audit trails.

Keep at least four identities distinct:

- **External account**: the vendor tenant, service user, OAuth client, or API key identity visible to the remote API.
- **Execution session**: one launched agent process with a fresh, unguessable session ID.
- **Initiating principal**: the person, CI job, or parent service that started that session.
- **Work reference**: the issue, change request, deployment, repository, or explicit task that explains why the call was made.

The external account can be stable for months. The execution session should not be. A task reference can recur across sessions. The initiator can be a person at a keyboard one day and an automated build the next. Each field has a different lifetime and a different question behind it.

This distinction is not a matter of terminology. Suppose `vendor-prod` deletes a remote deployment. The provider can truthfully say that `vendor-prod` performed the deletion. Your local record must show whether session `s_7f3...` came from a release agent started by Maya, whether it had approval for this run, and whether the deletion was a direct call or a retry after a timeout. A single `actor=vendor-prod` field hides every fact that could change the incident response.

RFC 8693 makes a related distinction in OAuth delegation. It separates the subject on whose behalf authority exists from the current actor using the JWT `act` claim. It also says resource servers should make access decisions from the top level token claims and the current actor, not from historical nested actors. That is a useful boundary for local agent systems too: retain lineage for investigation, but make authorization decisions from a clear current session identity rather than a long, ambiguous story of prior calls.

Do not call a vendor account "the agent." The account may be used by agents, scripts, emergency operators, and migration jobs. Give it a precise name such as `external_principal`, then make the local session the actor for your own audit record.

## Session identity must come from the launcher

The process that launches an agent should create its session identity before the agent can request an external action. Do not let the agent select it. An agent that can choose `session_id=release-approved` can make a later review actively misleading.

A practical session record contains enough material to identify the executable and enough context to explain the work:

```json
{
  "session_id": "ses_01JQ6EXAMPLE3K5A",
  "started_at": "2026-07-22T15:04:18Z",
  "initiator": {
    "kind": "human",
    "id": "maya@example.test"
  },
  "agent": {
    "process_id": 48192,
    "binary_authority": "signed-local-agent",
    "launch_path": "/workspace/payments"
  },
  "work": {
    "kind": "change_request",
    "id": "CR-1842"
  },
  "parent_session_id": null
}
```

The exact field names are less important than ownership. The launcher owns `session_id`, start time, executable identity, and parent session. A human or calling system supplies the work reference, but the gateway should record who supplied it. The agent can propose a description of its task, yet that text should never replace the launcher supplied identity.

Process IDs alone are weak evidence. Operating systems reuse them, logs outlive processes, and a process ID says little about who launched the binary. Code signing authority is more useful on a local developer machine because it ties the approval decision to the signed process family. Even then, record the executable path and launch context when available. A familiar signature does not prove that every invocation had the same purpose.

Use a new session for each new agent process. Reusing a session because the task number is the same turns an approval for a short experiment into a durable permission bucket. Long running agents need an explicit policy decision: either retain one session and make its duration visible, or rotate the session at a defined boundary such as a new pull request or a resumed terminal run. Do not quietly do both.

## Attribution must be captured before credential injection

The safest place to bind an actor to a request is immediately before a trusted component applies the shared credential and sends the request. Anything earlier can be changed by the agent. Anything later may be absent, summarized, or overwritten by the vendor.

Build a call envelope that the agent cannot author in full. The agent supplies the requested action. The gateway adds the session identity, authorization decision, call ID, and external credential reference. Store the envelope before transmitting the request, then append the outcome when it returns.

```json
{
  "call_id": "call_01JQ6F9K4W7D",
  "session_id": "ses_01JQ6EXAMPLE3K5A",
  "external_principal": "vendor-prod",
  "channel": "http",
  "request": {
    "method": "POST",
    "host": "api.vendor.example",
    "path_template": "/v1/deployments/{id}",
    "operation": "create_deployment"
  },
  "authorization": {
    "vault_unlocked": true,
    "session_authorized": true,
    "per_call_approval": false
  },
  "work_id": "CR-1842",
  "attempt": 1,
  "created_at": "2026-07-22T15:08:34Z"
}
```

Notice what is missing: the bearer token, complete request body, and a free form claim that the agent is trustworthy. A log that stores secrets to prove attribution has failed its first job. A log that records every byte of a body can also expose customer data, source code, or regulated records. Capture a normalized operation name, a route template, selected nonsecret identifiers, and a content digest when the payload itself matters to a review.

This design also separates intent from effect. An agent may request `create_deployment`, but the remote service may return a validation error. The call journal should preserve both facts. Later, you can answer whether the agent attempted the action without claiming that the action succeeded.

Sallyport follows this placement: the agent connects through its MCP shim while the app retains the secret and executes the HTTP or SSH action. The resulting session and activity records can therefore bind a local run to a use of a shared credential without putting the credential into the agent context.

## Agent supplied headers are evidence, not proof

Teams often add headers such as `X-Agent-Name`, `X-Task-ID`, or `X-Run-ID` and call the problem solved. Those fields can help correlate remote logs, but an agent that controls the request can also omit, alter, or replay them. They are labels, not an authority boundary.

You can forward attribution headers when the vendor accepts them, provided the gateway follows three rules. First, remove any agent supplied versions of reserved headers. Second, generate the final values from the recorded session and call envelope. Third, treat the vendor's receipt of the header as supplementary evidence, not as your source of truth.

For example, reserve this small header set inside the gateway:

```text
X-Execution-Session: ses_01JQ6EXAMPLE3K5A
X-Action-Call: call_01JQ6F9K4W7D
X-Work-Reference: CR-1842
```

Do not put an email address, a prompt, a branch name with customer data, or a raw command into a header merely because it is convenient. Headers pass through proxies, tracing systems, error reports, and vendor support tooling. Use opaque IDs, then resolve them against your protected local journal.

Some providers reject unknown headers, strip them, or do not display them in audit views. That is normal. The remote API is not required to become your identity system. Your gateway should work even when the provider accepts only its ordinary authorization scheme.

There is one more trap: a signed header is not a substitute for local logging. A request signature can prove that a gateway signed some request, but it does not preserve the human approval, process identity, task context, or result unless you record those details locally. Signatures protect transport claims. They do not create an investigation record by themselves.

## Retries need lineage rather than a single timestamp

Autonomous agents retry. HTTP clients retry. SSH commands can be rerun after a disconnected terminal. If your audit trail writes one line per intended action, it hides the mechanics that cause duplicate changes. If it writes only raw requests, it makes a single intended action look like several unrelated decisions.

Model both levels. Give the intended operation an `operation_id`, then give every network attempt a separate `call_id`. Associate retries with the prior attempt and record why the retry occurred.

```json
{
  "operation_id": "op_01JQ6F8P0Z",
  "call_id": "call_01JQ6F9K4W7D",
  "attempt": 2,
  "retries_call_id": "call_01JQ6F79S2M1",
  "retry_reason": "connection_closed_before_response",
  "idempotency_key": "idem_94c2e1",
  "vendor_request_id": "req_8d71"
}
```

The `retry_reason` matters. A `429` response means the vendor received the first request and refused it due to rate limiting. A timeout after bytes left your gateway does not tell you whether the vendor completed the action. Those cases demand different operational responses, and they should not collapse into `failed`.

Use idempotency keys for operations that create or change remote state when the vendor supports them. Generate the key in the trusted gateway or have the launcher provide it with the work record. Do not let a model create a fresh key every time it revises its own plan, because you lose the ability to recognize a replayed operation.

A common failure looks like this. Session A asks to create a deployment, the connection drops, and its client retries. Session B begins moments later with the same task reference, sees no visible deployment, and asks again. The vendor now has two deployments. A useful journal shows two sessions, two intended operations, their individual attempts, and any shared idempotency key. A weak journal shows four `POST /deployments` lines under one service account and leaves the team to reconstruct the rest from timestamps.

## Approval must bind to a process, not a friendly name

A prompt that asks, "Allow release agent to use production?" sounds sensible until two release agents exist, one launched from a trusted repository and one from a copied directory. A name is presentation text. The approval needs to bind to the execution session and the process authority that created it.

Per session approval is a good default for agents that make several related calls. It gives the operator a chance to see who is asking, then avoids turning a short task into a page of identical prompts. The approval should expire when that process exits. A new process, even one with the same task text, should ask again.

Use per call approval for operations with a high consequence or an unusually wide scope. This is not a replacement for session approval. It answers a narrower question: should this particular use of this credential proceed now? A team that uses per call approval for every harmless read will train people to approve without reading. That is approval fatigue by design, not a human failure.

Record the decision as an event with a stable reference:

```json
{
  "approval_id": "apr_01JQ6G3C",
  "session_id": "ses_01JQ6EXAMPLE3K5A",
  "scope": "session",
  "decision": "approved",
  "decided_at": "2026-07-22T15:06:11Z",
  "process_authority": "signed-local-agent"
}
```

Do not record a bare boolean on every call and pretend that proves consent. The boolean says that an approval existed. The approval event tells reviewers when it happened, what it covered, and which session it authorized. If an operator revokes the session later, preserve that revocation as a new event. Deleting the old approval makes the record look cleaner and the incident harder to understand.

## Vendor audit logs should corroborate your record

Vendor logs are useful, but they normally describe the vendor's identity model rather than yours. A shared token may appear as a service user, an OAuth application, a token hash, an installation, or an IP address. That can help confirm that an external call occurred, but it rarely tells you which local agent session selected the action.

GitHub's documentation is a concrete example of the distinction. Calls made with a GitHub App user to server token can show the user as the audit actor while identifying the programmatic access type as that token form. GitHub enterprise audit events also expose fields such as actor, token information, request ID, and user agent for many event types. That is useful provider side evidence, yet the meaning of those fields belongs to GitHub's authorization model, not to your local agent session model.

Join remote and local records with stable correlators where possible:

- Save a vendor request ID returned in a response header or body.
- Save your outbound call ID and a normalized operation name.
- Record the response status, completion time, and safe resource identifiers.
- Preserve the external principal used for the call.
- Keep the local session ID as the authoritative execution identity.

Avoid joining primarily on time. Clock skew, asynchronous vendor processing, retries, and queues all make a close timestamp weaker than it looks. Timestamps are still useful for narrowing a search, but they should not decide attribution when a request ID or idempotency key exists.

Some vendors can issue short lived delegated tokens, OAuth tokens on behalf of a user, or app installation tokens. Use those capabilities when they give the provider meaningful actor visibility and fit your privilege model. GitHub, for example, documents a distinction between an app acting on behalf of a user and other programmatic access types. That is better provider side attribution than a shared static token, but it does not remove the need to identify the local process that initiated the call.

## Separate credentials only when they buy a real boundary

The usual advice is "give every agent its own API key." It is popular because it is easy to explain and because it makes a vendor audit screen look tidier. It is not always the right control.

Separate credentials earn their operational cost when they create a meaningful boundary. That may be different scopes for a read only discovery agent and a deployment agent, independent vendor revocation, separate billing, or a provider audit record that identifies the distinct principal. If each key has the same broad scope, the same rotation owner, and the same local execution path, you have multiplied secret inventory more than you have improved attribution.

Use this test before creating another vendor identity:

1. Can the new credential receive less privilege than the shared one?
2. Can you revoke it without disrupting unrelated work?
3. Will the provider record it as a distinct actor in a way responders can use?
4. Can you rotate and retire it without leaving forgotten copies in local tooling?
5. Does it remove a meaningful authorization decision from the local gateway?

If the answer is no to most of these, keep the shared external account and improve the local actor record. That approach gives responders the detail they actually need: which agent session made the call, under whose initiation, for which work item, with which approval, and with what result.

There is a limit to this argument. If a credential grants production administration and an experimental agent does not need that power, do not share it merely because your logs are excellent. Attribution explains an action after the fact. Scope limits which actions can occur at all.

## A tamper evident journal must preserve ordering and denial

An audit record that stores only successful vendor calls is a partial narrative. Denied calls, locked vault attempts, rejected approvals, and revoked sessions often explain why an incident did not become worse. They also expose an agent that kept asking for an action after it lost authority.

Write an event when the session starts, when an authorization decision occurs, when a call is prepared, when the external action finishes, and when a session is revoked or exits. Link the records by IDs rather than copying a large mutable object into each line. The chain should make sequence visible without forcing every consumer to reconstruct state from prose.

Tamper evidence matters because local attribution is often the only source that distinguishes concurrent agents using the same vendor account. If a person with local access can alter a call record after an incident, the team has rebuilt the same trust problem one layer closer to home. Hash chained logs help reviewers detect changes, but they do not decide what fields to record. You still need a complete event model.

Sallyport projects its Sessions and Activity journals from one encrypted, hash chained audit log and can verify that chain offline with `sp audit verify`. The important operational point is not the command name. It is that a session approval, an individual action, and a later revocation can be checked against the same ordered record.

Do not use a vendor account label as your final answer when an incident reviewer asks who did something. Follow the record from external principal to call ID, from call ID to execution session, from session to initiator and approval, then from the response to the vendor's own request identifier. If any one of those links is missing, fix that capture point before the next shared credential becomes a mystery.
