# 如何为可靠的事故重建排序智能体活动

智能体活动的排序，决定事故报告是在解释发生了什么，还是只展示一堆时间戳。当自主编程智能体能够调用 API 并打开 SSH 会话时，调查人员需要确认是哪个进程执行了操作、它尝试了什么、哪些操作已经完成，以及智能体在选择下一步行动前收到了什么结果。

仅按时间排序远远不够。时钟会漂移，请求会重叠，响应可能乱序返回，而超时会掩盖一个实际上已经在远程端成功的操作。应构建能够保留会话上下文、持久本地顺序和不同生命周期时间点的记录。否则，第一次严重事故就会把审计轨迹变成一场围绕猜测的争论。

## 仅凭时间无法确定操作顺序

时间戳表示某个时钟在何时观察到事件，但不能证明这个事件一定发生在其他时间戳更晚的事件之前。这个区别听起来很学术，直到智能体在很短时间内发出两个调用，一个远程服务发生延迟，而第二个调用先返回。按 `completed_at` 排序得到的是响应顺序，不是智能体的决策顺序。

每次调用都有多个重要时刻。智能体决定调用工具，网关接受请求并进行授权，然后把工作分发给 HTTP 服务或 SSH helper。远程端可能接受请求，响应随后返回，网关再把结果交给智能体。如果把这些都当作一个事件，就会丢失解释故障所需的证据。

在每个会话中保留本地单调递增的 `call_sequence`。在网关接受调用时分配它，并且要早于网络操作。这个数字可以回答一个范围明确且有用的问题：“这个网关按什么顺序接受了来自该智能体进程的调用？”它不能声称代表远程执行顺序。无法证明的宽泛结论，不如范围诚实的记录。

为审计日志本身再使用一个持久序列。不同会话的调用可能重叠，授权、撤销、保险库锁定和验证事件也必须进入同一条证据流。日志序列告诉调查人员记录器的写入顺序，会话调用序列告诉他们一次智能体运行中的意图顺序。两个字段缺一不可。

不要用更高的时间戳精度代替序列号。增加小数位，只是更精确地记录时钟读数，并不能解决时钟调整，也不能建立并发写入者之间的全序。

RFC 3339 为墙上时钟时间提供了适合互操作的表示方式，其中包括明确的 UTC 偏移。导出和人工审查时使用 UTC 格式，例如 `2025-03-08T21:14:03.482Z`。RFC 3339 不保证因果顺序。因果顺序需要由记录器负责，这要求序列字段和清晰的事件边界。

## 会话标识的是执行进程，而不是模糊的任务

会话应将一次有序运行绑定到获得操作授权的具体智能体进程。进程建立连接时会话开始，进程退出、失去通道或操作员撤销授权时会话结束。不要把会话定义为“处理工单 184”或“下午的部署”。这些标签有助于搜索，却不能定义执行边界。

会话记录需要包含调查人员实际会问到的身份信息：哪个可执行程序建立了连接、由谁签名、哪个本地用户启动了它、使用什么传输方式连接，以及授权何时开始和结束。应记录操作系统或连接能够证明的标识，不要让智能体把自己的身份声明写入权威字段。

当有人把工具配置复制到另一个进程中时，这种区分尤其重要。操作请求可以声明它打算操作某个仓库，但网关应记录它观察到的进程身份。事故发生时，后一个说法更有依据。

一个实用的会话记录可以包含：

```json
{
  "event_id": "01JNRQ2Q9Y9J0R3E5P8F7K2X4M",
  "journal_sequence": 8124,
  "event_type": "session.opened",
  "occurred_at": "2025-03-08T21:14:02.901Z",
  "session_id": "sess_7f31c4",
  "process": {
    "pid": 48102,
    "code_signing_authority": "observed signing authority",
    "local_user": "developer account"
  }
}
```

智能体应接收不透明的会话句柄，而不是选择会话标识或修改元数据的权限。智能体仍然可以在单独的声明上下文字段中附加自己的运行标签、仓库路径或任务引用，并明确标记这些值由智能体提供。标签可以说明意图，但绝不能覆盖观察到的进程事实。

按会话管理授权还有第二个调查价值。它能记录与明确进程运行绑定的人为决定。如果该运行后来发起了有害调用，审查人员可以看到此前发生的授权事件，以及结束该运行的会话关闭或撤销事件。一个无声地适用于后续进程的批准，会造成任何调用日志都无法修复的缺口。

## 一次调用需要完整生命周期，而不是一行完成记录

有用的调用记录应保留一次尝试的生命周期。不要把尝试请求、网络分发、远程结果和返回给智能体的结果压缩成一个模糊的“成功”或“失败”字段。

从不可变的 `call_id` 和会话中的下一个 `call_sequence` 开始。在接触外部世界之前记录接受事件。如果策略或审批阻止了请求，接受事件和拒绝事件仍然重要。它们能证明意图和控制行为，但不会假装外部操作已经发生。

对于允许执行的 HTTP 操作，应记录以下独立事件边界：

1. `call.accepted` 记录网关接受请求的顺序。
2. `call.authorized` 或 `call.denied` 记录控制决定。
3. `call.dispatched` 记录网关已将请求交给网络客户端。
4. `call.result_received` 记录传输结果或远程响应。
5. `call.result_returned` 记录返回给智能体的结果。

名称可以不同，但语义不能不同。收到结果并不总是意味着结果已经返回。网关可能会编辑响应、拒绝格式错误的数据、失去与智能体的连接，或在准备结果时遇到内部故障。调查人员需要看到这个缺口。

在这些时刻发生时保留 `request_started_at`、`dispatched_at`、`result_received_at` 和 `result_returned_at`。没有发生的时刻使用 null。进程崩溃时不要编造结束时间。应在之后记录恢复事件，说明记录器发现了一次未完成的调用。

下面展示一个已完成请求的结构，其中没有暴露 bearer token 或完整响应正文：

```json
{
  "event_id": "01JNRQ3M8W7P0Q4R6S9T1V2X3Y",
  "journal_sequence": 8131,
  "event_type": "call.result_received",
  "occurred_at": "2025-03-08T21:14:05.841Z",
  "session_id": "sess_7f31c4",
  "call_id": "call_00017",
  "call_sequence": 17,
  "channel": "http",
  "target": "api.internal.example/v1/releases",
  "method": "POST",
  "dispatch_event_id": "01JNRQ3G2A...",
  "outcome": {
    "transport": "response",
    "http_status": 201,
    "response_digest": "sha256:..."
  }
}
```

请求和响应摘要让你可以比较保留的证据，而不必让每个日志阅读者都接触秘密或大型敏感载荷。摘要不会让秘密变得适合记录。低熵值、可预测标识符和短令牌仍然可能被猜出。应在捕获时排除凭据，再决定事故流程真正需要哪些载荷片段。

## 重试和超时会造成最棘手的歧义

超时意味着你不知道远程端是否执行了操作，并不意味着远程端什么也没做。团队反复犯这个错误，是因为应用日志常常把超时当作普通错误，而把下一次重试当成第一次尝试的替代品。

设想一个智能体通过 HTTP 请求创建发布版本。调用 41 在分发后收到请求超时。智能体读取失败结果，然后发出调用 42 进行重试。后来远程服务处理了两个请求。如果日志把调用 41 覆盖成最终状态“已重试”，调查人员会看到一个成功请求，却错过重复操作。

每次网络尝试都要有自己的 `call_id` 和 `call_sequence`。如果一次尝试直接跟随较早的尝试，添加 `retry_of`。保留智能体看到的重试原因，例如超时、连接重置或收到可重试状态。这样调查人员可以追踪整个链条，而不会把它压平。

完整序列可能如下：

```text
sequence 41  accepted       21:14:11.024Z  create release, request r_8d2
sequence 41  dispatched     21:14:11.027Z
sequence 41  result_received 21:14:41.031Z timeout
sequence 41  result_returned 21:14:41.034Z timeout returned to agent
sequence 42  accepted       21:14:42.112Z  retry_of call_00041, request r_8d2
sequence 42  dispatched     21:14:42.115Z
sequence 42  result_received 21:14:42.490Z HTTP 201
sequence 42  result_returned 21:14:42.493Z HTTP 201 returned to agent
```

重复的请求引用只有在远程 API 支持幂等机制或其他稳定的操作标识符时才有用。如果服务接受幂等键，应生成并记录一个不包含秘密的键，在同一项预期操作的所有重试中保持不变。如果服务不支持，应明确记录重试风险仍未解决。不要因为载荷看起来相似，就声称它具有幂等性。

SSH 带来了另一个问题。命令可能已经在远程执行，而连接却在客户端收到输出或退出代码前断开。记录命令分发、连接身份、主机引用和观察到的终止状态。将中断的 SSH 命令标记为“结果未知”，不要标记为“失败”。之后检查远程状态的命令可能减少不确定性，但不能改写原始结果。

不要把所有失败都变成终止事件。授权拒绝对该调用来说是终止状态，因为没有发生外部分发。本地 DNS 故障可能是这次尝试的终止状态。机器已经发出字节后发生的超时，其外部结果未知。这些类别会导致不同的事故决策。

## 记录两种时间，并说明它们的边界

墙上时钟时间便于跨系统阅读时间线。单调时间衡量经过的时间，不受网络时间同步或手动调钟影响。操作系统提供这两种时间时，应同时采集，并在架构中说明每种时间的含义。

每个日志事件都记录 RFC 3339 UTC 格式的 `occurred_at`。在运行中的会话里，再记录从该进程选择的单调时钟起点开始测量的 `monotonic_ns`。除非已经明确建立共享参考点，否则不要比较不同机器上的单调读数。它们只是本地测量值。

时钟校正可能产生这样的混乱记录：

```text
journal 901  wall 21:19:07.900Z  monotonic 5562019921  call accepted
journal 902  wall 21:18:58.104Z  monotonic 5562026310  call dispatched
```

墙上时钟向后跳了。日志序列和单调值仍然表明分发发生在接受之后。导出时应保留原始时间戳，不要悄悄排序并重写它们。如果能够观察到操作系统报告的重要时间变化，也应记录一个记录器事件，为这种差异提供解释。

NIST Special Publication 800-92《Guide to Computer Security Log Management》建议组织同步时钟，并在事故发生前定义日志数据要求。这一建议是正确的，但同步时钟本身无法给出智能体运行内部的顺序。时钟同步有助于与远程 API、CI 服务或主机日志进行关联，本地序列则负责建立记录器的顺序。

远程时间戳应使用独立字段。HTTP `Date` header、提供商请求 ID 和服务器生成的事件时间都属于外部声明。保留它们的来源和精确值，不要将它们复制到 `occurred_at`，也不要用它们重新编号本地日志。远程时间戳有助于之后协调不同系统，但它可能反映的是队列时间、另一台机器的时钟或生成响应的时间。

时长字段也需要精确定义。`gateway_duration_ms` 可以表示从接受到返回结果的时间，`network_duration_ms` 可以表示从分发到收到结果的时间。应在架构旁写明定义。否则，报告说一次调用用了 30 秒时，没人知道延迟发生在分发前、远程服务中，还是响应返回之后。

## 审计写入器必须在发布结果前确定顺序

如果并发工作线程在各自完成时随意写入记录，就无法重建顺序。应为审计写入器提供一条追加路径，由它分配日志序列、捕获事件时间、链接上一条记录，并在系统告诉智能体某个具有外部意义的状态变化已经发生前提交记录。

这不要求为所有网络活动加一把巨型锁。调用可以并发运行。记录器只需要一个范围很小的串行提交点。工作线程到达事件边界时，将事件提交给写入器。写入器分配下一个持久日志序列。最终顺序反映的是提交顺序，文档和导出文件必须准确说明这一点。

常见的故障模式是这样的。工作线程 A 接受调用 17，并开始一个缓慢请求。工作线程 B 接受调用 18，并很快完成。如果工作线程只追加完成记录，日志开头会是调用 18 的成功记录。调查人员无法判断调用 17 是仍在处理中、从未发送，还是被遗漏。调用 17 的接受和分发事件可以填补这个缺口。

哈希链可以为已提交的序列增加防篡改证据。每条记录包含上一条已提交记录的摘要，以及自身规范化内容的摘要。规范化非常重要。相同数据在哈希前必须产生相同字节。应明确字段顺序、UTF-8 编码、时间戳表示、null 处理和数字格式。“我们对 JSON 做哈希”不是规范，因为普通 JSON 对象的顺序并不构成安全属性。

概念上的记录可以使用这些字段：

```json
{
  "journal_sequence": 8131,
  "event_id": "01JNRQ3M8W7P0Q4R6S9T1V2X3Y",
  "previous_hash": "sha256:9c7d...",
  "record_hash": "sha256:04b1...",
  "payload": {"event_type": "call.result_received"}
}
```

只要验证者拥有预期的链锚点，有效链就能表明保留下来的记录彼此连接，且没有被未被发现地修改。但如果攻击者控制记录器并能阻止它写入记录，哈希链无法证明记录完整。不要把哈希链说成万能方案。它能让篡改变得可见，却无法记录记录器从未观察到的事件。

Sallyport 从同一份加密、哈希链式审计日志中生成 Sessions 和 Activity 日志。其 `sp audit verify` 命令可以在离线状态下对密文验证链，无需保险库密钥。这样，会话视图和单次调用视图都绑定到同一个有序来源，而不是要求调查人员协调两份独立日志。

## 构建能够保留不确定性的时间线

事故时间线应分别展示事实、观察结果和未解决的结果。把未知情况改写成确定动词的精致叙述，在紧张的审查中可能显得有用，却会制造一份之后的证据可以推翻的虚假记录。

假设智能体进程在 09:00:00 获得批准。它在 09:03:14 发出一条 SSH 命令。客户端在 09:03:16 失去连接。09:03:18，智能体通过 HTTP 查询目标系统，发现配置已经改变。证据支持多种解释：SSH 命令已经完成、另一个操作者改变了状态，或者之前排队的任务生效了。时间线必须说明证据支持什么结论，也必须说明它不能支持什么结论。

事故记录可以使用以下格式：

| 顺序 | 时间 | 证据 | 可以确定的事实 |
|---|---|---|---|
| 444 | 09:03:14.120Z | `call.dispatched` | 网关将 SSH 命令发送给 helper。 |
| 445 | 09:03:16.202Z | 传输断开 | 网关没有收到退出状态。 |
| 446 | 09:03:18.810Z | HTTP 查询响应 | 此时查询到的配置已经不同。 |
| 447 | 09:03:19.001Z | `call.result_returned` | 智能体收到了查询结果。 |

除非有直接证据将命令与远程变化连接起来，否则不要写“SSH 命令改变了配置”。远程审计记录、唯一操作 ID 或包含持久服务器端请求 ID 的响应，都可以提供这种连接。时间接近并不能证明因果关系。

调查人员还需要知道智能体在作出后续决定时看到了什么。这就是 `result_returned` 需要独立成事件的原因。如果远程响应已经到达，但智能体在收到响应前断开连接，那么智能体后来的操作并不是根据该响应作出的。如果响应确实到达了智能体，它可能解释某个危险行为分支。

在事故视图中同时展示会话轨道和调用轨道。会话轨道展示打开、批准、撤销、锁定和关闭事件。调用轨道展示接受、授权、分发和结果事件。平面列表仍可用于验证，但两个视图回答的是不同问题，不会互相混淆。

## 让秘密处理经得住事故审查

审计轨迹常常在最有用的时刻失效，因为有人想“只为这次调查”记录完整请求头、shell 环境和响应正文。这个决定可能把一次受控的智能体事故变成凭据泄露。

记录操作身份，不要记录秘密材料。对于 HTTP，记录方法、规范化的主机和路径、凭据引用或密钥标签、安全的请求头名称、请求摘要、响应状态、存在时的提供商请求 ID，以及经过谨慎选择的响应摘要。绝不要记录 authorization header、原始 API key、私钥或完整环境转储。在捕获时排除凭据，再决定事故流程真正需要哪些载荷片段。

对于 SSH，记录主机引用、政策允许时的账户引用、规范化命令表示、命令摘要、连接状态，以及收到时的退出状态。命令本身可能包含秘密。如果工作流允许任意 shell 文本，应将受保护的证据存储用于范围明确且经过授权的审查，或只记录经过编辑的形式和摘要。不要因为命令日志不含密码，就假设它没有风险。

Sallyport 将 API 和 SSH 凭据保存在加密保险库中，并在不把凭据交给智能体的情况下执行外部操作。这消除了智能体记录和日志变成秘密转储的一个常见原因，但目标、请求正文、命令参数和响应仍然可能敏感。

原始记录的访问权限应与验证权限分开。响应人员可能需要在无权阅读加密调用详情的情况下验证日志链。安全审查人员可能需要会话和目标元数据，却不需要载荷材料。这种分离能减少事故响应过程中把整份日志复制到聊天、工单或电子表格的需要。

导出证据时，应包含架构版本、导出时间、日志序列范围、验证结果和使用的编辑规则。按照正常控制措施保留原始受保护日志。导出文件是工作副本，不是源证据的替代品。

## 用一次刻意混乱的运行测试记录

顺利路径演示几乎无法证明事故重建能力。应测试那些会让排序变得模糊的情况：并发调用、延迟响应、时钟变化、进程退出、拒绝、撤销，以及超时后重试。

用两个获准的外部目标进行受控演练。让第一个调用等待后再返回。在第一个调用分发后启动第二个调用。在第三个调用分发后中断它。然后撤销会话，确认后续调用收到拒绝。导出日志，并交给没有编写这个场景的同事。

要求审查人员仅使用导出文件回答五个问题：

- 哪个进程获得了授权，授权何时结束？
- 该会话中网关按什么顺序接受调用？
- 哪些调用到达了分发边界？
- 每次后续调用之前，智能体收到了什么结果？
- 哪些结果仍然未知，而不是失败或成功？

如果对方必须问你某个字段是什么意思，就修正架构或导出文档。如果对方根据超时推断远程效果，就修正结果标签。如果对方无法区分重试和新操作，就添加关系字段和操作标识符。

保留演练产物。当你更换客户端库、引入并发、调整保留策略或增加新通道时，它们可以成为回归测试。排序错误往往来自看似无害的重构，因为开发者关注操作是否仍能正常完成，而证据路径的提交时间却悄悄发生了变化。

事故不会等你设计出更完善的日志系统。调用接受时分配会话序列，通过一个有序写入器提交生命周期事件，同时保留墙上时钟和单调时间，并让未知结果保持未知。这些选择能为调查人员提供一条可以辩护的序列，而不是一条需要不断解释的时间线。
