# 授权证据：证明 AI 代理操作已完成

批准记录能告诉你某个人允许代理尝试某件事，但它不能说明请求是否到达服务、远程主机是否接受命令，或预期变更是否真的发生。把批准当成完成证明，会产生看似令人放心的审计轨迹，直到事故调查人员问出一句：「究竟发生了什么？」

AI 代理让这个区别变得更加紧迫，因为它们在处理普通开发任务时，可能执行大量真实操作。一个人可能只批准一次代理进程，随后这个进程创建工单、修改部署设置、查询生产 API，或运行远程命令。每项操作都需要含义不同的证据。人类的决定和最终结果应放在同一次调查中，但两者不能互相替代。

## 批准记录的是权限，不是完成

授权证据回答的是：经过识别的决策者是否在某个时间允许执行一项明确操作。执行证据回答的是：操作网关尝试了什么，以及目标返回了什么。只保存第一个答案的审计设计，存在一个很大的缺口。

假设代理获准调用一个用于创建访问令牌的 API。网关发出了请求，但连接在服务完成处理之后、调用方收到响应之前断开。批准仍然有效。写着「已批准」的日志无法告诉审阅者令牌是否存在。如果因为网关已经发出字节，就把日志写成「已完成」，情况会更糟，因为这会把不确定性伪装成事实。

SSH 也会出现同样的问题。某人批准运行 `deploy.sh`。SSH 客户端完成身份验证，远程 shell 启动，但网络连接在脚本运行期间中断。脚本完成了吗？它是否只修改了系统的一部分？连接记录无法回答。你需要远程退出状态，前提是客户端收到了它；如果没有，就需要清楚记录结果未知。

请把以下问题分开：

- 是否有人员或已批准的控制机制授权这个代理进程？
- 授权具体覆盖了什么能力？
- 网关是否将操作发送到了目标？
- 目标或传输层返回了什么结果？
- 后续审阅者能否确认历史没有被人修改？

许多团队把这五个问题压缩成一个名为 `agent_action` 的事件。对仪表板来说很方便，但当事实出现分歧时几乎没有用。应保存相互关联的事件，再在界面中将它们放在一起展示。

NIST Special Publication 800-53 Rev. 5 在控制项 AU-3 中也提出了类似的实际要求。审计记录应包含发生了什么、何时发生、在哪里发生、来源、结果，以及与事件关联的身份。重点不在清单式的措辞，而在于它坚持要求把结果写入记录。权限决定无法填补这一字段。

## 两类记录需要不同字段

清晰的审计模型会为一次请求的操作使用一条授权事件和一条或多条执行事件。它们共享关联标识符，但各自保留能够支撑自身声明的字段。

授权记录应在执行开始前识别请求。记录调用进程身份、人类决定、凭据引用、预期通道、目标和请求的操作。授权范围要精确到足以让审阅者判断后续调用是否仍在范围内。

一种实用的结构如下：

```json
{
  "event_type": "authorization.granted",
  "action_id": "act_01JX7K8N4Q",
  "session_id": "ses_01JX7JYQ2M",
  "time": "2025-03-08T14:21:18Z",
  "agent_process": {
    "pid": 4812,
    "code_signing_authority": "Example Development Team"
  },
  "human_decision": {
    "method": "local_confirmation",
    "actor": "local_user"
  },
  "requested_action": {
    "channel": "http",
    "credential_ref": "billing-api-prod",
    "method": "POST",
    "destination": "api.internal.example",
    "path": "/v1/refunds"
  }
}
```

默认情况下，这条记录不应包含 API 密钥、SSH 私钥、授权标头或原始请求正文。充满凭据的审计存储会变成第二条入侵路径。如果审阅者需要区分含有私密内容的请求，可以保留受保护部分的加密摘要，以及一份简短的脱敏摘要，并记录明确的处理规则。

执行记录从网关尝试操作时开始。它必须说明操作是否到达协议边界，以及返回了什么。对于 HTTP，记录方法、目标、响应状态、传输错误、响应大小和受限制的响应分类。对于 SSH，记录账户、主机身份、命令表示形式、退出状态、信号（如果有）以及客户端连接结果。

```json
{
  "event_type": "execution.finished",
  "action_id": "act_01JX7K8N4Q",
  "time": "2025-03-08T14:21:20Z",
  "channel": "http",
  "attempt": 1,
  "delivery": "response_received",
  "result": {
    "http_status": 201,
    "response_bytes": 428,
    "response_digest": "sha256:..."
  }
}
```

不要只写 `success: true` 作为结果字段。成功在不同协议和产品中的含义并不相同。HTTP 201 表示服务器报告资源已创建，HTTP 202 表示服务器接受了稍后处理的工作。SSH 退出状态 0 表示远程命令报告成功，但脚本仍可能忽略了内部失败。使用有明确类型的结果，才能让审阅者看到事实，而不是一个模糊的绿色标签。

## 可信操作边界必须创建关联关系

持有凭据并执行操作的组件应生成操作 ID。代理可以请求执行工作，但不能成为「获准做了什么」或「网关发送了什么」的记录来源。

当代理进程存在缺陷、遭到入侵，或只是被冗长的工具对话弄糊涂时，这一点尤其重要。如果由代理创建标识符，它可能把后来的结果附到早先的请求上，遗漏不方便的调用，或报告虚构的状态。即使代理运行正常，重启后也可能丢失上下文。操作边界能在同一位置看到请求、凭据选择、出站尝试和返回结果。

使用不带业务含义且永不重复的标识符。随机或按时间排序的唯一 ID 都可以。将它附加到每条本地记录上。在协议允许的情况下，也将它向外传递为请求标识符。这样，服务运营人员可以把自己的日志与网关记录关联起来，同时不暴露凭据。

对于 HTTP 请求，可以使用目标系统同意记录的专用标头，例如 `X-Action-ID`。不要把它和幂等键混淆。操作 ID 有助于调查，幂等键则告诉支持该机制的服务器识别重复的状态变更。一个值有时可以同时承担两种作用，但必须先由服务负责人确认其语义和保留期限。

```text
Authorization: action_id=act_01JX7K8N4Q
Outbound request: POST /v1/refunds
Request header: X-Action-ID: act_01JX7K8N4Q
Response: 201 Created
Execution record: action_id=act_01JX7K8N4Q, delivery=response_received
```

不要只依靠时间戳关联记录。时钟会漂移，请求会重叠，代理也可能在同一秒发出完全相同的调用。时间戳有助于重建顺序，但不能证明父子关系。

会话 ID 同样重要，但它回答的是更宽泛的问题：哪个代理进程运行发出了这次请求？两个 ID 都要保留。会话 ID 表示哪个进程获得了持续授权，操作 ID 表示哪一项具体操作获得了哪一个具体结果。

## HTTP 状态码是有局限的证据

HTTP 响应比本地批准提供更强的执行证据，但仍需要解释。记录最终状态和足以说明情况的上下文。不要把所有非 200 范围的状态都简单归为失败。

`200 OK` 或 `201 Created` 是服务器给出的明确报告。`204 No Content` 通常表示操作成功但没有响应正文。`202 Accepted` 则不同，它表示目标收到请求并准备异步处理，但最终工作仍可能在之后失败。执行记录应写明 `accepted_for_async_processing`，并在服务支持时，将后续状态检查或回调关联到同一操作。

重定向需要谨慎处理。如果网关会跟随重定向，应记录原始目标和最终目标，并说明每一跳是否应用了凭据注入规则。把授权标头转发到意外主机是凭据泄露，不是普通重定向。实际操作中，除非操作员明确配置了目标关系，否则应拒绝跨主机重定向。

客户端错误和服务器错误同样包含有用事实。`403` 证明服务拒绝了请求。`409` 可能说明已经存在重复或冲突状态。`429` 表示目标暂时拒绝处理。`500` 表示服务器报告了故障，但不能说明没有发生任何变更。响应正文可能有助于澄清结果，但只能在脱敏并限制大小后保存。错误响应往往比成功响应包含更多运行细节。

传输失败需要独立的结果值。记录 `dns_failure`、`tls_validation_failure`、`connect_timeout`、`write_interrupted`、`response_timeout` 和 `connection_reset` 等区别。这些结果能告诉审阅者确定性在哪里结束。

最危险的情况，是网关开始写入请求后操作被中断。服务可能已经执行了操作。应将状态标记为 `unknown_remote_outcome`，不要因为客户端没有响应就标记为失败。如果调用会改变状态，代理应暂停并采用安全的核对方法。可以使用幂等键查询服务，通过请求标识符读取资源，或请人检查目标系统。

## SSH 需要远程进程证据

SSH 审计记录常常只停留在「已连接到主机」。这只能证明客户端建立了 SSH 会话，不能说明命令的结果。如果没有记录主机密钥验证，它甚至不能确认到底是哪台主机作出了响应。

对于每次 SSH 操作，记录目标主机名、可用时的解析地址、已验证的主机密钥指纹、请求的账户、身份验证方法引用、规范化命令和最终客户端结果。如果远程命令启动，记录其退出状态和终止信号。标准输出和标准错误应视为敏感运行数据，不要自动写入日志。

规范化命令能在减少暴露的同时保留审阅价值。例如，不要保留包含秘密参数的命令，而应记录可执行文件、固定选项、敏感参数的位置，以及受保护值的摘要。

```text
requested_command: /usr/local/bin/rotate-service-token --project payments --token [redacted]
command_digest: sha256:...
remote_exit_status: 0
remote_signal: null
connection_result: clean_close
```

退出状态 0 是远程命令提供的证据，不是每项业务效果都已发生的证明。编写不当的 shell 脚本可能运行 `curl`、忽略错误，却仍然以 0 退出。如果你能控制脚本，应让必要操作失败时明确报错并返回非零值。如果不能控制，就准确记录命令结果，不要做出超出证据范围的声明。

交互式 SSH 会话需要保持警惕。它会在「权限已授予」和「实际执行了哪些命令」之间制造巨大的空白。受限制的远程命令比授予代理通用交互式 shell 更容易产生可靠证据。如果任务需要多个命令，应使用经过审查且具有明确退出状态的脚本，或为每条命令创建单独的操作记录。它没有代理自由操作终端那么炫，但调查起来容易得多。

必须保持主机验证开启。如果客户端在网络攻击期间，或主机被错误替换后接受了未验证的主机，那么「代理在 `build-01` 上运行了命令」这条记录意义有限。将已验证的主机密钥指纹保存在执行事件中，审阅者才能区分主机名标签和加密身份。

## 超时应以不确定性结束，而不是引发重试风暴

最容易让团队陷入麻烦的故障，通常从一个改变状态的 API 调用开始，以超时结束。代理获得了权限，网关建立连接并发送请求。目标可能已经完成变更，但响应丢失，也可能目标根本没有收到最后几个字节。对调用方来说，这两条路径看起来很相似。

设想代理通过 API 创建生产事故工单。它发送：

```text
POST /v1/incidents
Idempotency-Key: inc_72f9c
X-Action-ID: act_01JX7K8N4Q
```

客户端等待后记录 `response_timeout`。只记录批准的审计轨迹会说某人批准了一个工单，简单的操作日志会说工单创建失败。这两种说法都不安全。

正确的执行记录应说明网关尝试发送请求，但没有收到响应，同时保留操作 ID 和幂等键引用。随后，代理应在服务提供该查询能力时，询问与 `inc_72f9c` 关联的状态。如果服务报告已有工单，就记录一条指向原始操作的核对事件。如果没有记录，并且服务的幂等契约允许重试，则使用同一个幂等键重试一次，不要换用新键。

整个过程包含三个独立事实：

1. 某人批准了原始请求。
2. 网关无法确认初始结果。
3. 后续读取或幂等重放确定了最终状态。

核对完成后不要删除超时记录。它解释了为什么之后会发生那次操作，也能显示团队是否存在会被漂亮的成功率掩盖的传输问题。

SSH 也有对应的故障。远程命令可能在本地客户端失去会话后继续运行。除非命令明确具有幂等性，否则不要自动重试。优先使用远程操作 ID、权限受控的状态文件，或能够报告状态的目标 API。如果这些都不存在，就记录结果未知，并要求人工审查。重复部署命令造成的损害已经够多了，不需要代理再以机器速度重复它们。

## 会话授权和逐次调用授权应对不同风险

会话授权表明某个代理进程可以在本次运行期间使用获准的操作通道。当人预计只会进行一段短暂且影响较低的工作时，它能减少反复批准带来的疲劳，但不会把后续调用变成逐项审查的决定。

逐次调用授权会为每次使用选定凭据记录人类决定。对于每个目标、状态变更或远程命令都值得重新关注的操作，应使用这种方式。执行记录仍然不可缺少。某人可能批准一个破坏性 API 调用，而服务可能拒绝它、只处理其中一部分，或使其超时。

当审阅者问「谁批准了这次变更？」时，两者的区别就很明显。会话记录可能回答：「这个经过签名的代理进程获准调用此通道。」逐次调用记录可以回答：「某人在这个时间批准了这项具体请求。」但两者都不能回答：「它真的发生了吗？」只有最终执行证据才能回答这个问题。

批准提示应显示足够的信息，让人能够做出有意义的决定，包括调用进程身份、凭据引用、目标、方法或命令，以及请求是否会改变状态。只写着「允许代理访问吗？」的提示，虽然记录了一个点击，却几乎没有记录有用意图。

不要在还不能生成连贯证据之前，就用庞大的策略语言解决问题。团队常常因为想减少提示而急于制定规则。规则可以限制操作，但不能取代能够区分批准、尝试、响应和未知结果的审计模型。先让事实清晰可见，再决定哪些地方可以安全自动化。

## 哈希链保护历史，但不能让声明变真实

追加式哈希链日志可以让后续篡改变得可检测。每个事件都会包含或贡献一个依赖先前记录的摘要。如果有人修改旧授权、删除失败请求或重新排列顺序，验证就会失败，除非对方能从改动点开始重写整条链，并替换受信任的链头。

这一点对代理活动很重要，因为最令人难堪的记录往往正是最想被删除的记录：被拒绝的操作、失败的部署、发送到错误服务的请求，或持续时间超过预期的会话。只能由管理员悄悄编辑的日志，经不起严肃审查。

但哈希链不能让虚假记录变成真实记录。如果不可信的代理提供「HTTP 201」，而日志忠实地将这个谎言加入哈希链，那么链只能证明这个谎言被保存了。采集器必须在操作边界观察事件。网关应在收到响应或检测到传输失败后，创建执行结果。

NIST SP 800-92《Guide to Computer Security Log Management》提醒我们，日志需要在生成、传输、存储、分析和处置各阶段受到保护。实际经验不只是把文本文件集中起来，而是要保留原始事件来源、保护记录顺序、控制谁可以修改记录，并让人能够在不获得操作所用秘密的广泛权限下完成验证。

Sallyport 将代理运行和单独调用记录在同一份只写、加密且经过哈希链保护的审计日志中。其 `sp audit verify` 命令可以在离线状态下对密文验证哈希链，无需保险库密钥。这样，检查历史完整性的审阅者与能够使用生产凭据的进程彼此分离。

验证命令应给出明确结果，并标出检查范围。例如：

```text
$ sp audit verify
verified: 1847 records
chain: valid
first_record: 2025-03-01T08:15:02Z
last_record: 2025-03-08T14:21:20Z
```

有效的哈希链不能决定外部 API 是否履行了承诺，但它能证明网关保存的批准记录和收到的结果没有在未被发现的情况下发生改变。

## 审查关联关系，而不是孤立的事件数量

如果团队只在不同图表中统计批准、成功调用和拒绝调用，却从不检查它们之间的关联，审计就会失败。有效的审查单位是一条操作时间线，包括请求、授权决定、执行尝试、结果和任何核对过程。

先检查没有匹配伙伴的记录。没有执行事件的授权可能表示请求被取消、网关崩溃或日志故障。没有先前授权的执行事件可能暴露绕过机制。没有核对记录的未知远程结果不是已经关闭的失败，而是尚未完成的运行工作。

使用一组小型查询或报告，强制回答以下问题：

- 哪些获准的状态变更操作最终结果未知？
- 哪些调用收到的响应超出了授权范围？
- 哪些 SSH 命令结束时没有退出状态？
- 哪些会话授权在预期任务结束后仍产生了操作？
- 哪些操作 ID 出现在本地记录中，却没有出现在目标服务日志里？

最后一个查询需要谨慎。目标日志中缺少记录，可能意味着目标没有保留请求 ID、时间窗口不正确，或日志由另一个团队负责。应将它标记为调查信号，而不是违规证据。

Sallyport 的 session journal 和 activity journal 让这种区分变得实用：一个视图显示代理运行及其撤销状态，另一个视图显示单独调用。审阅者应通过会话 ID 和操作 ID 在两者之间切换，而不是把任何一个日志视为完整事实。

保持明确的确定性词汇。`authorized`、`attempted`、`response_received`、`remote_exit_received`、`denied`、`failed_before_send` 和 `unknown_remote_outcome` 都比单一的成功标志更容易解释和辩护。当团队一致使用这些词时，困难情况就不会再消失在令人安心的绿色仪表板里。

## 在扩大代理权限前建立证据链

起步不需要复杂的控制平面。将凭据使用置于可信操作边界之后，在那里创建操作 ID，将人类决定与结果分开记录，并保留未知结果，不要把它们改写成失败。这样，事故响应人员就有了一条可以与服务和主机记录相互核对的事件序列。

然后在故障情况下测试记录，不要只测试正常路径。批准请求后在发送完成时切断连接。运行一个返回非零退出状态的 SSH 命令。制造 DNS 故障。撤销代理会话，并确认后续调用会收到拒绝记录。检查审阅者是否无需打开源文件或询问代理其意图，就能解释每个事件。

可以允许代理执行操作，而不让它持有凭据。但不能允许它重新定义发生过什么。保留决定记录，保留执行结果；当网络拒绝配合时，让不确定性保持可见。
