# 共享 API 账户如何跨代理会话进行归属

共享 API 账户有时是合适的运行方式。供应商可能只发放一个组织令牌，速率限制可能归属于该账户，而把它替换成一堆几乎相同的密钥，只会增加秘密数量，并不会改善控制能力。

错误在于把这个账户当成行为主体。它是外部凭据主体，也就是供应商能够识别的身份。当多个本地代理使用它时，你还需要另一份记录，说明每个请求由哪个本地进程发出、该进程获准执行什么工作，以及它如何获得使用凭据的权限。如果不在执行时收集这些信息，之后只能靠猜。

给每个代理起名字、配上色彩鲜艳的仪表板并不那么吸引人。但当两个代理同时竞争、一个代理重试、人工操作员在运行中途撤销权限，而供应商的审计页面只显示 `automation-service` 时，真正经得住考验的正是这套记录。

## 供应商账户和行为主体是两种不同的身份

共享供应商账户回答的是：「供应商接受了哪个凭据？」行为主体归属回答的是：「哪个本地主体导致了这次具体操作？」这两个答案经常不同。把它们强行放进同一个字段，会产生糟糕的审计轨迹。

至少要区分以下四种身份：

- **外部账户**：远程 API 能看到的供应商租户、服务用户、OAuth 客户端或 API 密钥身份。
- **执行会话**：一个启动的代理进程，以及一个新生成、不可猜测的会话 ID。
- **发起主体**：启动该会话的人、CI 任务或父级服务。
- **工作引用**：说明调用原因的问题单、变更请求、部署、代码库或明确任务。

外部账户可以稳定存在数月，但执行会话不应如此。任务引用可以在多个会话中重复，发起者今天可能是坐在键盘前的人，明天也可能是自动化构建任务。每个字段的生命周期不同，背后要回答的问题也不同。

这不是术语问题。假设 `vendor-prod` 删除了远程部署。供应商可以准确地说，是 `vendor-prod` 执行了删除。但你的本地记录必须说明，`s_7f3...` 会话是否来自 Maya 启动的发布代理、它是否获准执行这次运行，以及删除是直接调用还是超时后的重试。单独一个 `actor=vendor-prod` 字段，会隐藏所有可能改变事故响应的信息。

RFC 8693 在 OAuth 委托中也做了类似区分。它将权限所代表的主体与当前使用 JWT `act` 声明的行为主体分开。它还指出，资源服务器应根据令牌顶层声明和当前行为主体做访问决策，而不是根据历史嵌套主体做决策。这对本地代理系统同样有参考价值：保留完整的来源链，便于调查；但授权决策应基于明确的当前会话身份，而不是一长串含义模糊的历史调用。

不要把供应商账户称为「代理」。这个账户可能被代理、脚本、紧急操作员和迁移任务共同使用。可以给它一个明确的名称，例如 `external_principal`，然后把本地会话作为自己审计记录中的行为主体。

## 会话身份必须由启动器生成

启动代理的进程应在代理能够请求外部操作前创建会话身份。不要让代理自行选择。能够选择 `session_id=release-approved` 的代理，会让后续审查变得极具误导性。

实用的会话记录应包含足够的信息来识别可执行文件，也要包含能够说明工作内容的上下文：

```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
}
```

具体字段名没有所有权重要。启动器负责 `session_id`、启动时间、可执行文件身份和父会话。人工操作员或调用系统提供工作引用，但网关应记录是谁提供了它。代理可以提出任务描述，但这段文字绝不能取代由启动器提供的身份信息。

单靠进程 ID 不是可靠证据。操作系统会重复使用进程 ID，日志的生命周期通常长于进程，而进程 ID 很少能说明是谁启动了该二进制文件。在本地开发机上，代码签名权限更有用，因为它能将审批决定与已签名的进程族关联起来。即便如此，只要条件允许，也应记录可执行文件路径和启动上下文。熟悉的签名并不能证明每次运行的目的都相同。

每启动一个新的代理进程，就使用一个新的会话。因为任务编号相同就复用会话，会把一次短期实验的审批变成长期权限桶。长时间运行的代理需要明确的策略：要么保留一个会话，并让持续时间清晰可见；要么在新拉取请求、恢复终端运行等明确边界处轮换会话。不要悄悄同时采用两种方式。

## 必须在注入凭据前捕获归属信息

将行为主体绑定到请求的最安全位置，是可信组件应用共享凭据并发送请求之前。更早的信息可能被代理修改，更晚的信息可能缺失、被概括，或被供应商覆盖。

构建一个代理无法完整编写的调用信封。代理提供请求的操作，网关加入会话身份、授权决定、调用 ID 和外部凭据引用。发送请求前先保存信封，响应返回后再追加结果。

```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"
}
```

注意其中没有什么：bearer token、完整请求正文，以及代理值得信任的自由格式声明。为了证明归属而把秘密写入日志，说明日志已经失去了首要职责。把正文的每个字节都记录下来，也可能暴露客户数据、源代码或受监管记录。当载荷本身对审查很重要时，可以记录规范化的操作名称、路由模板、经过筛选的非秘密标识符和内容摘要。

这种设计还将意图与结果分开。代理可能请求 `create_deployment`，但远程服务可能返回验证错误。调用日志应保留这两个事实。之后你可以回答代理是否尝试过该操作，而不必声称操作已经成功。

Sallyport 采用的正是这种位置关系：代理通过 MCP shim 连接，而应用保留秘密并执行 HTTP 或 SSH 操作。这样，生成的会话和活动记录可以将本地运行与共享凭据的使用绑定起来，同时不把凭据放进代理上下文。

## 代理提供的请求头是证据，不是证明

团队经常添加 `X-Agent-Name`、`X-Task-ID` 或 `X-Run-ID` 等请求头，然后认为问题已经解决。这些字段有助于关联远程日志，但能够控制请求的代理也能省略、修改或重放它们。它们是标签，不是权限边界。

如果供应商接受归属请求头，可以将它们转发过去，但网关要遵守三条规则。第一，删除代理提供的保留请求头版本。第二，根据已记录的会话和调用信封生成最终值。第三，将供应商收到该请求头视为补充证据，而不是事实来源。

例如，可以在网关内部保留以下请求头集合：

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

不要仅仅因为方便，就把电子邮件地址、提示词、包含客户数据的分支名称或原始命令放进请求头。请求头会经过代理、追踪系统、错误报告和供应商支持工具。应使用不透明 ID，再通过受保护的本地日志解析它们。

有些供应商会拒绝未知请求头、将其删除，或不在审计视图中显示它们。这很正常。远程 API 不需要成为你的身份系统。即使供应商只接受普通授权方式，你的网关也应继续工作。

还有一个陷阱：签名请求头不能替代本地日志。请求签名可以证明网关为某个请求生成过签名，但除非你在本地记录，否则它不会保留人工审批、进程身份、任务上下文或结果。签名保护的是传输声明，本身不会创建调查记录。

## 重试需要来源链，而不是一个时间戳

自主代理会重试，HTTP 客户端会重试，SSH 命令也可能在终端断开后再次运行。如果审计轨迹只为每个预期操作写一行，就会隐藏导致重复变更的过程。如果只记录原始请求，又会让一个预期操作看起来像多个互不相关的决定。

应同时建模两个层次。为预期操作分配 `operation_id`，再为每次网络尝试分配独立的 `call_id`。将重试与上一次尝试关联起来，并记录重试原因。

```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"
}
```

`retry_reason` 很重要。`429` 响应意味着供应商收到了第一次请求，只是因为速率限制拒绝处理。网关已经发送数据后发生超时，则无法说明供应商是否完成了操作。这两种情况需要不同的运维响应，不应都简单归为 `failed`。

对于创建或改变远程状态的操作，只要供应商支持，就应使用幂等键。可以由可信网关生成，也可以由启动器随工作记录一起提供。不要让模型每次修改自己的计划时都生成新密钥，否则就无法识别被重放的操作。

一个常见故障是这样的：会话 A 请求创建部署，连接断开，客户端进行重试。片刻后会话 B 带着同一个任务引用启动，发现没有可见的部署，于是再次发起请求。供应商最终得到两个部署。好的日志会显示两个会话、两个预期操作、各自的尝试，以及可能共享的幂等键。薄弱的日志只显示同一个服务账户下的四行 `POST /deployments`，剩下的只能由团队根据时间戳重建。

## 审批必须绑定进程，而不是友好名称

「允许发布代理使用生产环境？」听起来很合理，直到你发现有两个发布代理，一个从可信代码库启动，另一个从复制出来的目录启动。名称只是展示文本。审批应绑定到执行会话，以及创建该会话的进程权限。

对于需要执行多个相关调用的代理，按会话审批是不错的默认选择。操作员可以先确认是谁在请求，之后不必面对一页重复提示。进程退出时，审批也应失效。即使新进程使用相同的任务文本，也应再次请求审批。

对于影响重大或范围异常广的操作，应采用按调用审批。这不是会话审批的替代品，而是回答一个更具体的问题：现在是否应允许这次特定凭据的特定使用？如果团队对每次无害读取都要求审批，人们最终会养成不阅读就点击的习惯。这是审批疲劳的设计结果，而不是人的过错。

将决定记录为带有稳定引用的事件：

```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"
}
```

不要在每次调用中只记录一个布尔值，然后假装它能证明同意。布尔值只能说明存在审批。审批事件会告诉审查人员审批发生的时间、覆盖的范围，以及它授权了哪个会话。如果操作员之后撤销会话，应将撤销保存为新事件。删除旧审批会让记录看起来更整洁，却让事故更难理解。

## 供应商审计日志应当佐证你的记录

供应商日志很有用，但它们通常描述的是供应商自己的身份模型，而不是你的身份模型。共享令牌可能显示为服务用户、OAuth 应用、令牌哈希、安装实例或 IP 地址。这可以帮助确认外部调用确实发生过，却很少能说明是哪个本地代理会话选择了该操作。

GitHub 的文档很好地说明了这种区别。使用 GitHub App 用户到服务器令牌发起的调用，审计行为主体可能显示为该用户，同时将程序化访问类型标识为这种令牌形式。GitHub Enterprise 审计事件还会针对许多事件类型提供行为主体、令牌信息、请求 ID 和用户代理等字段。这些都是有用的供应商侧证据，但这些字段的含义属于 GitHub 的授权模型，而不是你的本地代理会话模型。

在可能的情况下，使用稳定的关联标识连接远程和本地记录：

- 保存响应头或响应正文中返回的供应商请求 ID。
- 保存出站调用 ID 和规范化的操作名称。
- 记录响应状态、完成时间和安全的资源标识符。
- 保留调用使用的外部主体。
- 将本地会话 ID作为权威的执行身份。

不要主要依靠时间进行关联。时钟偏差、供应商异步处理、重试和队列都会让接近的时间戳变得不如看上去可靠。时间戳仍适合缩小搜索范围，但当请求 ID 或幂等键存在时，不应由时间戳决定归属。

有些供应商可以发放短期委托令牌、代表用户获取的 OAuth 令牌或应用安装令牌。如果这些能力能让供应商看到有意义的行为主体，并且符合你的权限模型，就应使用它们。例如，GitHub 文档区分了应用代表用户执行操作与其他程序化访问类型。这比共享静态令牌能提供更好的供应商侧归属信息，但仍不能免除识别发起调用的本地进程的必要。

## 只有在能形成真正边界时才分开凭据

常见建议是「给每个代理一个独立 API 密钥」。它很容易解释，也能让供应商审计页面看起来更整齐，所以很受欢迎。但它并不总是正确的控制方式。

当独立凭据能形成有意义的边界时，它们才值得承担运营成本。这可能意味着只读发现代理和部署代理拥有不同范围、供应商可以独立撤销、账单分开，或供应商审计记录能以响应人员可用的方式识别不同主体。如果每个密钥都拥有相同的宽泛权限、由同一方负责轮换，并经过相同的本地执行路径，那么你增加的主要是秘密清单，而不是归属能力。

创建新的供应商身份前，先回答以下问题：

1. 新凭据是否可以比共享凭据拥有更少的权限？
2. 是否可以在不影响无关工作的情况下撤销它？
3. 供应商是否会将它记录为独立行为主体，并让响应人员真正使用这些信息？
4. 是否可以轮换和退役它，而不会在本地工具中遗留忘记删除的副本？
5. 它是否能让本地网关少承担一个有意义的授权决定？

如果大多数答案是否定的，就保留共享外部账户，改善本地行为主体记录。这样能提供响应人员真正需要的细节：哪个代理会话发起了调用、由谁启动、对应哪个工作项、经过什么审批，以及结果如何。

这个观点也有边界。如果某个凭据授予生产环境管理权限，而实验代理并不需要这些权限，就不要因为日志记录得很好而共享它。归属可以在事后解释发生了什么，范围限制则决定哪些事情从一开始就可能发生。

## 防篡改日志必须保留顺序和拒绝事件

只记录成功的供应商调用，会形成一段不完整的叙述。被拒绝的调用、保险库锁定时的尝试、被拒的审批和被撤销的会话，往往能说明事故为什么没有进一步恶化，也能暴露代理在失去权限后仍不断请求某项操作。

在会话启动、授权决定发生、调用准备完成、外部操作结束，以及会话被撤销或退出时，都写入事件。通过 ID 关联记录，不要把一个大型可变对象复制到每一行。链条应当让顺序清晰可见，同时不要求每个使用者从散文描述中重新推导状态。

防篡改能力很重要，因为本地归属记录往往是区分多个代理并发使用同一供应商账户的唯一来源。如果拥有本地访问权限的人可以在事故发生后修改调用记录，团队只是把同一个信任问题搬到了更靠近本地的一层。哈希链式日志可以帮助审查人员发现变化，但不会替你决定应记录哪些字段。你仍然需要完整的事件模型。

Sallyport 将 Sessions 和 Activity 日志投影自同一份加密、哈希链式审计日志，并可以使用 `sp audit verify` 离线验证这条链。重要的不是命令名称，而是会话审批、单次操作和之后的撤销都能在同一份有序记录中得到核验。

当事故审查人员问是谁执行了某项操作时，不要用供应商账户标签作为最终答案。沿着记录从外部主体找到调用 ID，再从调用 ID找到执行会话，从会话找到发起者和审批，最后从响应找到供应商自己的请求标识。如果其中任何一个连接缺失，就应在下一个共享凭据变成谜团之前修复对应的捕获点。
