# 智能体审计时间戳能经受时钟变化吗？

墙上时钟显示的时间是证据，但不能保证排序。智能体可能在主机时钟回拨、夏令时导致某个小时重复，或者主机从长时间休眠中醒来后被校正时，发出两次完全合理的调用。如果审计查看器只按显示时间给这些调用排序，它可能讲出一个听起来很可信、却完全错误的故事。

用序列号回答「这份日志先接受了哪条记录？」用墙上时钟时间戳回答「记录器报告的日历时间是什么？」这是两个不同的问题。团队出问题，往往是因为假装一个字段可以同时回答这两个问题。

对自主智能体来说，这个区别非常重要。审查人员可能需要确认，SSH 命令是否发生在批准之后，HTTP 调用是否被重试，或者撤销是否发生在下一步操作之前。答案必须经得住一台时间不准的笔记本电脑和一次尴尬的时间变化，而不能只是在表格里看起来整齐。

## 时间戳无法建立可靠的事件顺序

当时钟可能变化时，墙上时钟时间戳无法证明一个事件发生在另一个事件之前。它只能报告软件写入记录时观察到的时钟读数。

来看一组记录。某台机器在时间同步校正前快了十分钟：

```text
seq 841  2026-11-03T14:10:12.481Z  agent requested deploy status
seq 842  2026-11-03T14:00:13.107Z  agent requested deploy status
seq 843  2026-11-03T14:00:14.052Z  approval recorded
```

按时间戳排序会把 842 和 843 排在 841 前面。日志实际先接受了 841。这两种视图都不是笔误，它们回答的是不同问题。

即使时间戳持续增加，这一点仍然重要。时钟可能走得慢、突然向前跳，或者被用户校正。两个日历时间不断增加的记录，实际间隔可能比数值显示的更长，也可能更短。时间戳只是民用时间尺度上的一个观测坐标，并不能提供不间断的持续时间测量。

RFC 3339 讲清楚了存储方面的要求：涉及本地时间时，时间戳需要带偏移量；在交换数据时，用 `Z` 表示 UTC 可以消除偏移量歧义。这是很好的做法，但 RFC 3339 不会让主机时钟变成一个永远不会出错的见证者。它定义的是表示法，不是真相。

为每个只追加的审计流提供单调递增的 `seq`。应由负责串行化写入的组件分配它，而不是由每个智能体进程自行分配。如果两个智能体进程都在本地分配编号，再将记录上传，你得到的其实是两个顺序，却把它们称成了一个顺序。

序列号的主张范围有限，但很有用：

- 在同一个日志中，较小的 `seq` 先进入日志。
- 出现间隔意味着调查人员必须说明缺失、被扣留或被主动排除的记录去了哪里。
- 出现重复值，说明写入器、导入器或存储层发生了故障。
- 单独看这个值，无法说明另一台机器上的事件。

最后一点经常被忽略，因为看起来像全局编号的整数很容易给人权威感。它并没有这种能力。序列号需要命名空间。`journal_id=macbook-17, seq=841` 是有边界的陈述。表格里的 `seq=841` 却很容易诱导人们过度推断。

## 夏令时会重复本地时钟读数

在许多地区，夏令时会让本地时钟重复一个小时。秋季切换期间，01:15 会先以一个 UTC 偏移量出现，之后又以另一个偏移量出现。只保存 `2026-11-01 01:15:00` 的日志，已经丢失了区分这两次出现所需的信息。

不要等事后再猜操作人员指的是哪一次。事件写入时就应记录足够的上下文：

```json
{
  "journal_id": "build-mac-07",
  "seq": 842,
  "event_id": "01JXYZ...",
  "recorded_at_utc": "2026-11-01T08:15:00.000Z",
  "local_offset": "-07:00",
  "time_zone": "America/Los_Angeles",
  "event_type": "agent.http.requested"
}
```

在后一次出现时，同一个本地时钟显示可能带有 `local_offset: "-08:00"`，UTC 值也会晚一个小时。数字偏移量保留了你当时观察到的事实。时区名称有助于人们解释为什么会出现这个偏移量，但不应取代记录中的偏移量。

时区规则会变化。各国政府可能在几乎不考虑日志解析器的情况下修改夏令时的开始日期、结束日期，甚至取消夏令时。如果只保存本地日期和时区名称，几年后使用更新的时区数据库重新计算偏移量，历史证据的显示结果可能会与机器当时的记录不同。应保留原始 UTC 值和写入时捕获的偏移量。只有在查看器清楚标注这是当前日期换算结果时，才使用当前规则进行显示。

春季切换会带来另一种故障。有些本地时间根本不会出现。如果系统接受用户输入的跳过时段中的本地截止时间，例如 02:30，就必须拒绝它，或要求用户明确选择处理方式。把它静默改成 03:30，是伪装成日历运算的产品决策。

对审计记录来说，UTC 应当是规范值。显示本地时间可以帮助读者理解，但要在同一个字段中显示偏移量。`2026-11-01 01:15:00 -08:00` 比 `01:15` 更不简洁，却也是证据。

## 手动校正必须创建新记录

手动编辑会决定审计轨迹是继续有价值，还是变成一份润色过的活动动态。如果管理员直接修改时间戳，原始观察结果就消失了。调查人员无法再判断，是旧时钟错误、事件载荷错误，还是有人想让历史看起来不同。

保留原始记录，不要修改。添加一条更正记录，指出此前的事件，说明存在争议的字段，记录提出的更正值，并解释依据。更正不会删除第一条记录，而是增加一个后来的事实：有人针对第一条记录提出了一个主张，并说明了理由。

更正载荷可以很小，但仍然完成任务：

```json
{
  "seq": 913,
  "event_type": "audit.timestamp_corrected",
  "corrects_event_id": "01JXYZ...",
  "original_recorded_at_utc": "2026-11-03T14:10:12.481Z",
  "asserted_occurred_at_utc": "2026-11-03T14:00:12.481Z",
  "basis": "host time service report and neighboring journal entries",
  "actor": "admin-identifier"
}
```

只有在有证据支持修订后的时间时，才使用 `asserted_occurred_at_utc`。不要重新标记原始的 `recorded_at_utc`。它描述的是记录器的时钟读数，即使这个读数不准确，在历史上仍然是正确的。

在架构和措辞中都要区分以下三个概念：

1. `occurred_at` 表示操作实际发生的时间，前提是执行者能够确认它。
2. `observed_at` 表示某个特定采集器看到操作的时间。
3. `recorded_at` 表示审计写入器提交条目的时间。

它们可能相同，而且经常相同。但不应使用同一个字段名。

「直接修正错误数据」这个常见建议来自报表系统，因为报表中的错误数字应该从仪表盘上消失。审计系统承担着另一项任务，它要保存从观察到结论的过程。对普通读者来说，显式更正不那么方便，但它能阻止系统把不确定性洗成确定性。

## 网络时间校正可能改变墙上时钟

网络时间同步可以提高时钟准确性，但校正本身可能让时间戳变得出人意料。客户端可能逐渐调整运行速率，也可能在偏移量足够大或策略允许时直接跳变时钟。用户手动设置时间、虚拟机恢复运行，以及固件时钟中的错误值，都可能产生同样的可见结果：两条审计记录之间的日历时间发生了变化。

POSIX 的 `clock_gettime` 文档区分了实时时钟和单调时钟。`CLOCK_REALTIME` 跟踪日历时间，并且可以被设置。`CLOCK_MONOTONIC` 没有有用的日历起点，但不会被 `clock_settime` 设置。它适合在一个正在运行的系统中测量时间间隔。

这个区别给出了一个实用的数据规则。为调查人员记录墙上时钟时间。需要分析经过时间、超时行为或校正前后顺序时，再记录一个单调时钟采样。不要把单调时钟值序列化成日期。它的绝对值只有在特定启动过程和时钟域中才有意义。

RFC 8633 是 IETF 关于 NTP 运维的指导文件，其中明确讨论了大幅时间偏移，并指出操作人员不应在冷启动时盲目绕过 NTP 的 panic threshold。人们通常容易把每次校正都当成无害的日常维护，但当时间影响令牌有效期、数据保留、事件重建或重放检测时，校正并不无害。

审计系统应把时间异常报告成审计事实，而不是用排序规则把它们隐藏起来。一个有用的事件可以包含上一次的墙上时钟值、新值、估计差值、已知的变化来源，以及发现变化的进程。如果操作系统没有提供原因，就明确写出来。错误的解释比缺少解释更糟糕。

要为正常抖动保留少量容差。并发写入器之间相邻值反向相差几毫秒时，不要因为这种出乎意料的差异就创建严重的时钟变化事件。串行化后的 `seq` 已经提供了顺序。当墙上时钟回退超过你所声明的精度，或者向前跳跃的间隔与预期活动不符、需要复核时，再标记为不连续。

## 序列号需要明确的范围

序列号只有在分配它的日志内部才有效。记录离开原日志后，如果仍把序列号当作全局顺序，就会在分布式智能体系统中得出错误结论。

假设一台 Mac 上的编程智能体请求 API 操作，远程构建主机写入一条 SSH 操作。每台主机都有自己的日志：

```text
build-mac-07  seq 842  14:00:13Z  HTTP action requested
build-host-2  seq  91  14:00:14Z  SSH action accepted
build-mac-07  seq 843  14:00:15Z  HTTP result received
```

你可以说，在 `build-mac-07` 中，842 先于 843。你也可以说，`build-host-2` 在其报告的时间记录了 91。但仅凭这些字段，你无法证明远程主机接受 SSH 操作是在第一条 HTTP 请求之前还是之后。

要建立跨系统关系，应添加明确链接。从调用方传播到接收方的请求标识符，可以把请求事件与接收事件关联起来。接收方签署的回执可以提供更强的证据。中央采集器可以在收到记录时分配采集序列，但这个序列证明的是到达采集器的顺序，而不是来源处的发生顺序。

不要过度解读以下信息：

- 请求 ID 在两侧都保留时，可以证明关联关系，但不能证明传递时间。
- 中央接收序列可以证明接收顺序，但网络延迟可能改变到达顺序。
- 同步时钟可以缩小不确定范围，但无法消除不确定性。
- 分布式逻辑时钟在每个参与者都正确携带它时，可以表达因果关系，但不能提供墙上时钟时间。

对于许多智能体审计轨迹，你不需要一套宏大的分布式排序系统，而需要诚实的边界。为每个受信任日志保留本地序列，在操作边界加入关联 ID，并在每次导出中显示来源日志。这样，审查人员就能看到哪些证据很强，哪些地方只是推断。

## 保存足够的时间证据，以解释分歧

一个单独的 `timestamp` 字段算不上审计架构。它只是一个混入存储层的显示偏好。

使用一种能分别保存顺序、日历时间、来源身份和完整性材料的记录结构。具体字段名由你决定，但这些概念应该能经受导出和保留周期：

```json
{
  "journal_id": "build-mac-07",
  "seq": 842,
  "event_id": "01JXYZ...",
  "recorded_at_utc": "2026-11-03T14:00:13.107Z",
  "recorded_offset": "-08:00",
  "time_zone": "America/Los_Angeles",
  "monotonic_ns": 3982188001123,
  "boot_id": "boot-identifier",
  "event_type": "agent.http.requested",
  "correlation_id": "request-identifier",
  "actor_process": "process-identifier",
  "previous_hash": "hex-value",
  "record_hash": "hex-value"
}
```

`boot_id` 可以避免单调时钟值带来的常见错误。单调计数器可能在重启后重新开始，因此来自一次启动过程的 `3982188001123`，在没有更多上下文时不能与另一次启动过程中的同值进行比较。只有在确实会使用它时才保存它，并记录单位。名为 `monotonic_time`、却让读者猜测它表示纳秒、毫秒还是平台时钟滴答数的字段，等于浪费了这个字段。

`recorded_offset` 是写入器记录事件时生效的偏移量。它不能替代 UTC。它能让人看到来源的本地时间上下文，也能让格式化程序保留原始的民用时间解释。只有在平台能够可靠提供时，才加入命名时区。固定偏移量已经足以完成排序和重建。

哈希字段同样需要清晰的规则。应根据所有会改变记录含义的字段的规范表示计算记录哈希，包括 `seq`、时间戳、事件类型、执行者身份和载荷摘要。如果不同序列化程序可能改变空白、属性顺序或数字格式，就不要直接对格式化后的 JSON 字符串计算哈希。应先进行规范化。

当验证从受信任锚点开始，且每条记录都提交前一条记录时，哈希链可以发现被修改的记录。但它无法证明来源时钟准确，也无法证明事件发生在这台机器之外。它能证明的主张更有限，却依然重要：经过验证的历史保留了写入器产生的密码学关系。

这就是哈希链和序列应当配合使用的原因。序列提供本地顺序，哈希链让事后改写变得可见，时间戳提供日历上下文。它们谁也不能代替另外两者的工作。

## 调查时间倒退时，不要凭空编故事

当后一个序列的时间戳更早时，先检查已经拥有的证据。不要一开始就称其为重放、智能体错误或篡改。

可以按以下步骤进行简短调查：

1. 在解释时间值之前，验证日志的序列连续性和完整性结果。
2. 比较前后 UTC 值、本地偏移量、启动标识符和所有单调时钟采样。
3. 检查主机是否跨越了夏令时边界、重启、恢复运行，或记录了时间服务校正。
4. 沿着关联 ID 查看相邻日志，但除非回执或共享排序机制能够证明，否则要把跨主机时间标为估计值。
5. 如果证据支持更正或时间异常结论，就添加注释记录。

真实审查中经常会遇到这样的故障。智能体在 `09:02:04` 请求 API 操作，机器唤醒后网络时间将墙上时钟拨回四分钟，操作结果在 `08:58:07` 被记录。仪表盘按时间顺序排列，于是把结果显示在请求之前。操作人员认为结果被重放，封锁智能体并开始轮换凭据。

序列和单调时钟值讲述的是一个更简单的故事。请求是序列 117，结果是序列 118。两条记录具有相同的启动标识符。单调计数器前进了大约三秒。两条记录之间墙上时钟发生了变化。在这个日志中，结果发生在请求之后，只是校正期间的日历显示不准确。

这个结论仍然留下了一些有价值的问题。时钟为什么相差四分钟？操作是否依赖带时间限制的凭据？另一个系统是在校正前还是校正后收到请求的？调查应分别回答这些问题，不要因为查看器偏好整齐的时间顺序，就强行制造一个干净的时间线。

如果完整性检查失败，就不要再把日志当作确定无疑的时间线。保存导出结果，如果可能，在受影响的日志之外记录验证失败，并通过独立路径获取一份新副本。链验证失败并不能指出是谁或哪个进程修改了数据，它说明证据已经不足以支持「历史未被修改」这一主张。

## 防篡改轨迹需要离线验证

如果审计日志唯一的完整性证明依赖于生成它的在线服务，它的取证价值就很有限。审查人员需要能够导出记录，将它们带到另一台机器上，并在不使用保险库、不询问执行操作的智能体的情况下验证哈希链。

Sallyport 将 Sessions 和 Activity 日志投射到同一个加密、哈希链审计日志中，`sp audit verify` 可以在没有保险库密钥的情况下，对密文离线验证哈希链。

这个设计解决的是与时钟准确性不同的问题。离线验证可以告诉你，加密历史在审计设计下是否仍然保持内部一致，但不能证明主机的墙上时钟准确。报告中应分开表达这两个主张：「审计链验证通过」和「事件时间得到了独立来源的佐证」都很有用，但一个不代表另一个。

导出流程应保留日志身份、序列范围、验证结果，以及执行验证的软件版本。只有时间戳和操作文本的 CSV 是报告，不是审计证据。它丢失了日后质疑或确认报告所需的字段。

## 让查看器围绕分歧设计，而不只是围绕顺利排序设计

好的审计查看器不会隐藏时钟异常。在一个日志内部，它默认按日志序列排序；用户展开一行时，同时显示 UTC 和本地时间；发现时间戳倒退时，将其标记为时间不连续，而不是重新排列历史。

可以让读者选择不同视图，但要准确命名每个视图。「日志顺序」表示序列顺序。「报告的日历时间」表示按时间戳排序，因果上更晚的记录可能因此排在前面。「采集器到达顺序」表示另一个服务接收数据的顺序。避免使用笼统的「时间」排序，因为它会把重要的取证选择隐藏起来。

界面还应显示确定性的范围。当事件来自不同日志时，应按来源分组，或在每个序列号旁清楚显示来源。如果请求 ID 连接了多条记录，就在数据模型和界面中把这种关系显示为链接。除非系统能够为此提供依据，否则不要在不同主机之间画一条连续的时间线。

第一步很简单：检查现有导出中是否有不带偏移量的本地时间戳、不带日志身份的序列号，或原地编辑路径。任意一项都足以制造误导性的事件时间线。现在修正它，比智能体操作已经成为证据后再解释要便宜得多。
