# 为 AI 代理写入它们无法修改的只写审计日志

AI 代理做出错误决定后，仍然可以写出一份看似完美的工作说明。这份说明不是证据。如果代理能够查看、选择、修改或避开审查人员用来评估它的记录，那么审计轨迹就会变成代理可以继续优化的另一种输出。

只写审计日志解决的是一个范围明确但很重要的问题：请求外部操作的流程，不应同时控制该操作的记录。操作系统记录它观察到的内容，返回代理所需的操作结果，并将供审查的记录放在代理无法触及的地方。这不会让代理变得诚实，却能让不诚实、困惑或遭到入侵的代理更难在事后编排故事。

当代理可以部署代码、调用付费 API、修改云设置、发送邮件或执行 SSH 命令时，这种区别尤其重要。在这些场景中，聊天记录是很弱的证据。聊天记录只能记录声明和被挑选出来的工具输出。由执行约束点生成的记录，则可以记录请求、授权、执行和结果，即使代理希望保持沉默。

## 代理不能同时控制操作和证据

代理对证据的权限，应少于它对任务本身的权限。给代理一个名为 `audit.jsonl` 的文件，再要求它追加记录，得到的是活动日记，不是审计日志。它可以漏掉某一行，删去不利的参数，在调用失败前先写入成功，或创建一份审查人员永远看不到的副本。

即使代理会遵守指令，这个问题也依然存在。代理经常会决定调用哪些工具、捕获哪些异常，以及保留哪些上下文。提示注入可能要求它避开某个请求的记录。程序错误可能让请求经过未接入记录的库。恶意依赖甚至可能直接向网络发起调用。如果同一个进程同时拥有操作权限和记录权限，这些故障都可能留下整洁却不完整的轨迹。

应明确分开这些能力：

- 代理请求操作，并获得有边界的结果。
- 执行器持有凭据、发送请求，并创建事件记录。
- 日志通过代理无法读取或修改的路径接受记录。
- 审查人员通过独立界面读取并验证记录。

这是一条能力边界，不是对行为的承诺。代理不应拥有日志文件访问权限、查询自身条目的 API、能够重写日志的凭据，也不应能够设置事件时间戳。如果代理需要解释自己的推理，请将说明保存为单独的运行产物，并标记为代理提供的材料。不要把它和执行记录混在一起。

团队经常混淆意图和证据。意图是代理声称自己想做什么，例如“重启预发布服务”。证据则是执行组件观察到的事实：代理在某个时间请求了特定命令，审批已经发生，执行器连接到了指定的主机引用，命令以特定状态退出。两者都很有用，但回答的是不同问题，也需要不同的信任等级。

## 只写意味着分离能力，而不是延迟查看

只写意味着执行者可以让记录存在，却无法访问已存储的记录或其未来的审查上下文。它不意味着代理得不到反馈。SSH 任务仍然需要 stdout、stderr 和退出状态。HTTP 任务仍然需要响应状态和正文，或从中提取的有边界内容。删除结果只会让代理无法工作，并不会让证据更可靠。

这条边界有四个实际组成部分。第一，代理使用请求接口，而不是凭据。第二，执行器根据自己的观察创建记录，而不是相信代理提供的 JSON。第三，代理不能列出、获取、修改或删除记录。第四，审查人员稍后可以检查记录，不依赖代理的记忆或聊天历史。

延迟读取的设计不符合这一标准。假设代理把事件写入数据库，并得到承诺，在会话结束前不能查询该表。它仍然知道数据库结构，可以调整参数字符串、选择遗漏内容、尝试制造事务失败，并通过错误返回时间的差异了解记录是否成功写入。它甚至可能持有数据库凭据，而其他代码路径可以滥用这些凭据。

当事件路径没有向外暴露日志状态的响应时，只写设计更可靠。执行器可以确认请求的操作，但不应返回事件 ID、序列号、审查人员批注、风险标签，或某条特定记录已经永久保存的确认。这些细节会给执行者提供可用于优化的信息。

这并不要求把每个代理都视为敌人。代理可能因为许多日常原因变得不安全，例如遭到注入的指令、失控循环、工具错误、过期依赖，或操作人员授予了范围过大的任务。设计应当经受住的不只是善意。

## 仅有只追加日志并不能解决问题

只追加存储可以发现某些篡改，但不能证明记录器看到了每个相关操作，也不能阻止代理在存储前安排误导性记录。团队经常购买不可变存储，却仍把直接网络凭据放在代理环境中。这样一来，核心缺口依然存在。

考虑两条部署路径。第一种路径中，代理从环境变量读取 API 令牌，直接调用部署服务。另一个库尝试记录请求。代理可以绕过这个库，派生另一个进程，或使用不同的 HTTP 客户端。日志可能完全符合只追加要求，但部署操作仍然不会出现在其中。

第二种路径中，代理可以请求执行器进行部署，但永远拿不到令牌。执行器拥有唯一能够附加凭据的路径。它在发送请求前记录请求，并在收到响应后更新事件。日志只能描述经过执行器的操作，但代理没有获授权的绕行路径。

RFC 6962，也就是 Certificate Transparency 规范，描述了一种 Merkle 哈希树，可以高效证明某个叶节点属于一棵树，并证明较新的树扩展了较旧的树。这是很有用的完整性模式，但它没有声称树能够证明现实世界中的所有事件都被完整捕获。没有进入树的部署请求，不可能出现在包含证明中。

线性哈希链也有同样的限制。记录可以包含前一条记录的摘要。如果有人编辑、删除或重排存储记录，验证会在受影响的位置失败。然而，一条干净的链仍然可能只覆盖精心选择的操作子集。在庆祝哈希链之前，先把记录器放到操作路径上。

还有第二个限制：管理员同样可能威胁到这条轨迹。如果操作人员能够修改日志数据库，并在没有外部见证的情况下重置链根，那么这种设计主要只能防止随手修改。离线验证、受保护的检查点和权限分离，能为审查人员提供更好的信任基础。但这些措施都不能让日志变成机器上所有活动的完整记录。

## 让记录器位于执行路径上

记录器必须在凭据附加且外部请求离开受控系统的位置观察操作。在代理框架层记录太早，只在远程服务记录又往往太晚，还可能缺少授权上下文。执行网关可以同时看到它接受的请求和收到的结果。

对于 HTTP，网关应接受受限请求，选择存储的凭据，由自己附加凭据，发送请求，并返回允许返回的部分响应。代理不应提交原始 `Authorization` 请求头，而应引用内部凭据名称，由网关解析。

请求结构可以足够小，便于检查和记录：

```json
{
  "action": "http.request",
  "credential_ref": "deploy-api",
  "method": "POST",
  "url": "https://deploy.example.internal/releases",
  "headers": {"content-type": "application/json"},
  "body": {"revision": "a18f3c"}
}
```

执行器应拒绝调用方为该路径提供的凭据请求头。这样可以防止代理将另一个令牌偷偷带入请求，同时在日志中留下误导性的 `credential_ref`。执行器还应在记录 URL 前对其进行规范化，因为原始 URL 可能包含用户信息、经过编码的路径技巧，或查询参数中的意外机密。

SSH 也遵循同一规则。代理请求针对某个主机引用执行命令，执行器选择配置的身份、主机验证行为和连接方式。如果代理能够读取私钥并自行调用 SSH 客户端，审计系统就不能再声称覆盖它的远程操作。

不要把网关和通用代理混为一谈。代理试图观察广泛的流量，操作网关则提供定义明确的操作，并持有执行这些操作的权限。范围更窄的设计更容易说明记录的含义：该执行器在该授权下接受了这项操作，并获得了这个结果。

## 在外部世界返回结果前记录事件

可信记录需要在执行器发送操作前写入开始事件，在收到结果后再写入完成事件。如果只在成功后写入记录，那么超时、崩溃、进程被终止或网络中断，都可能让重要请求从历史中消失。

使用由执行器生成的一个操作标识符，不要由代理生成。开始记录应包含会话标识符、执行进程身份、操作类型、规范化目标、安全的请求摘要、授权决定，以及由记录器生成的时间戳。将状态标记为 `started`。

执行器收到响应或失败后，写入引用同一操作标识符的完成记录。记录应包含 `succeeded`、`rejected`、`timed_out`、`transport_error` 或 `executor_error` 等结果类别，以及响应摘要和有边界的元数据。这样审查人员就能区分“网关拒绝了请求”和“远程系统可能收到了请求，但没有返回响应”。

后一种区别会带来实际影响。假设代理发送 `POST /releases`，远程服务接受请求后连接断开。盲目重试可能生成两次发布。因为客户端没有看到响应，就把第一次尝试称为失败，这是错误的。日志应显示执行器尝试了调用，传输在开始发送后失败，而最终远程状态仍然未知，直到另一个来源进行确认。

一对最小记录可能如下所示：

```json
{"seq":481,"action_id":"01HV...","state":"started","session":"run-72","type":"http.request","target":"deploy.example.internal/releases","request_sha256":"...","authorized":true,"previous_sha256":"..."}
{"seq":482,"action_id":"01HV...","state":"completed","result":"transport_error","sent":true,"response_sha256":null,"previous_sha256":"..."}
```

上述字段用于说明结构，不是通用模式。序列值和摘要必须由记录器控制。代理可以提供任务标签，但要将其标记为不可信输入并限制长度。否则，代理可能把机密、给审查人员的指令或误导性文字放入看起来很正式的字段中。

如果两条记录之间发生崩溃，就会留下未完成操作。请保留它。未完成记录是需要调查的证据，不是恢复时应该清理的杂物。重启后，如果系统有可靠的方法，可以将其标记为未解决，但绝不能把历史改写成整洁的成功或失败。

## 模式必须同时保留意图和结果

只保存“命令已执行”的日志，在事故期间几乎无法回答问题。保存每个请求和响应的每个字节，又会制造敏感数据堆。实用的中间方案是记录足够的结构化上下文，用来识别操作并评估授权，同时尽量减少机密和不受控制的载荷。

记录调用进程的身份时，应使用操作系统能够确认的依据。在 macOS 上，代码签名机构比代理自报的名称更适合作为会话锚点。进程 ID 会重复使用，显示名称也可能撒谎。记录足够的会话上下文，让审查人员能够判断是否有新进程启动了这次运行，以及批准是否适用于该进程。

对于 HTTP 操作，保留方法、规范化的权限和路径、所选凭据引用、允许的请求头、请求正文摘要、决策结果，以及可用时的响应状态。对于 SSH 操作，保留主机引用、适用时的远程账户引用、命令或根据敏感程度保存命令摘要、所选身份引用、主机验证结果、退出状态和输出摘要。

不要存储原始凭据。不要让 bearer 令牌偷偷进入 URL、自定义请求头、命令参数或捕获的输出。存储后再脱敏不如从一开始就避免收集，因为机密已经进入备份、副本或审查人员导出的文件。

摘要需要谨慎使用。摘要只能针对审查人员已经拥有的材料证明相等，无法告诉人类发生了什么。对于部署载荷，存储摘要加上代码仓库版本可能很合适。对于具有破坏性的数据库命令，可能需要保存规范化的命令表示，因为审查人员需要看到具体指令才能判断证据。

为允许操作的策略或审批决定保留单独字段。之后的审查人员应能回答：执行器是因为会话已获批准而允许操作，还是因为操作人员批准了这一次使用，或者因为保险库处于解锁状态？如果人工控制模型依赖这些区别，就不要把它们合并成模糊的 `allowed: true`。

## 审查需要独立访问和独立验证

审查人员需要的不只是可搜索的表格。他们还需要确认记录序列是否内部一致，以及系统是否保持了既定边界。调查一次运行的人，不应先依赖代理概述自己的行为。

哈希链提供了具体的检查方式。每条记录包含前一条存储记录的摘要，记录器则计算当前规范化记录的摘要。验证器按顺序读取序列，重新计算每个摘要，并检查每条前序引用是否匹配。修改旧目标、删除令人尴尬的失败记录，或交换两条记录，都会破坏后续链条。

规范化很重要。如果一个组件对 JSON 文本计算哈希，另一个组件却先解析再重新序列化，那么无害的空白或字段顺序变化也可能导致验证失败。应明确字段顺序、字符编码、空值处理、时间戳精度和摘要算法，然后为记录格式设置版本。验证器应知道自己能否验证某个版本，而不是猜测。

验证应当能够脱离创建记录的进程运行。如果检查完整性必须依赖同一个运行中的服务，那么遭到入侵的服务就可以谎报链条是否损坏。将加密记录流复制到审查机器或受保护的存档中，再针对副本运行验证器。

Sallyport 将 Sessions 和 Activity 日志投射到一个加密、哈希链式审计日志中，`sp audit verify` 可以在没有保险库密钥的情况下，对密文离线验证这条链。这个方向是正确的：检查证据不应要求暴露执行操作所需的机密。

完整性验证只能回答一个有限的问题：这些记录是否构成验证器所预期的序列？它不能证明每个服务器响应都是真实的，不能证明目标系统没有其他访问路径，也不能证明管理员从未用较旧但有效的副本替换整个日志。如果环境需要防止回滚，请保留受保护的检查点，或将签名后的根发送到独立的留存系统。

## 会悄悄破坏轨迹的失败模式

最具破坏性的设计，在演示中通常看起来很合理。它们会在重试、服务中断，或代理以意外方式使用同一权限时暴露问题。

第一种失败是从代理 SDK 记录日志。它很受欢迎，因为只需几行代码，还能为开发者提供熟悉的跟踪视图。但当代理获取凭据、调用另一个程序，或访问 SDK 没有包装的目标时，它无法提供执行记录。应把 SDK 跟踪视为调试数据。

第二种失败是只写一条成功记录。只在得到正常响应后写入记录，会抹掉不确定性。网络操作可能产生不明确的结果，远程命令也可能在连接关闭前改变状态。分别记录开始和完成，然后保留未完成的开始记录。

第三种失败是把机密放进审计载荷。团队有时会说，审查人员需要完整信息才能重现操作。审查人员很少需要 bearer 令牌来理解一次操作，而复制出来的机密会让日志变成高价值目标。应改为存储凭据引用和请求摘要。

第四种失败是给审查人员一个可修改的搜索索引，却把它当成事实来源。搜索索引可能丢失字段、使文档过期，也允许修复。应保留持久化事件流作为权威记录，根据它构建搜索视图，并在视图可疑时让审查人员返回原始记录。

第五种失败是允许宽泛的逃生通道。“在生产环境中运行任意 shell”这类命令，会让精心设计的凭据处理和事件模型失去意义。有时操作人员确实需要特殊访问权限。应将这种访问设为单独批准的操作，并明确记录，而不是把它藏在代理的常规路径中。

## 人工审批和只写设计解决的是不同风险

审批可以在不安全操作发生前阻止它。只写设计则在操作请求发生后保留证据。当代理使用影响重大的凭据时，团队两者都需要。

当人类信任某个特定代理进程在限定时间内工作时，会话审批很合适。对于每次使用都值得人工关注的凭据或操作，则适合逐次审批。任何一种审批都不能代替执行记录。获批准的会话仍可能发起意外调用，代理也仍可能错误描述自己做过的事情。

审批界面应以人类能够评估的方式标识调用进程。像“发布助手”这样的自报标签很弱。已确认的代码签名机构能让操作人员更有把握地判断哪个程序请求了权限。审批决定应成为事件上下文的一部分，但代理不应控制系统如何描述这个身份。

让保险库门控和审批保持独立。即使之前已经批准会话，锁定的保险库也应拒绝操作。这样就有了简单的紧急停止方式：锁定权限，执行器就停止使用存储的凭据。日志也应记录拒绝。连续的拒绝请求可能说明代理在循环、探测，或在操作人员撤销权限后仍继续运行。

除非确实需要，否则不要把它做成庞大的规则语言。事故期间没人能检查的规则，只会制造虚假的安全感。一组清晰的小型控制、受限的操作接口，以及代理无法编排的记录，通常比大量条件例外更可靠。

## 围绕可能伤害你的操作建立边界

从滥用后会迫使你进行艰难调查的外部操作开始。它可能是生产环境 SSH 命令、发布 API 调用、账户变更或付费请求。把凭据从代理环境中移除，强制该操作经过执行器，并在接入更多工具前测试失败路径。

可以针对设计运行下面这组简短测试：

1. 请求一个操作，然后在请求进行期间终止代理进程。确认日志仍保留开始事件。
2. 让远程目标返回错误，确认完成记录能够区分拒绝、远程失败和传输失败。
3. 使用调用方提供的凭据请求头或私钥尝试相同操作。确认执行器会拒绝它。
4. 给代理提供正常接口，然后尝试列出、修改或删除审计记录。确认它没有任何路径可以做到这一点。
5. 将存储的日志复制到其他地方，在不依赖执行器实时状态的情况下验证哈希链。

这些测试能发现正常演示会掩盖的设计缺口，也会迫使团队准确回答一个令人不舒服的问题：代理仍能在记录网关之外执行哪些操作？如果答案包括生产权限，那么记录从设计上就是不完整的。应明确说明、关闭这条路径，或缩小对审计轨迹所作的承诺。

一条有用的审计记录，应在请求离开前开始，在观察到的最佳结果出现后结束，并始终处于代理控制之外。在第一次事故迫使你相信代理的事件版本之前，先建立这条边界。
