# 代理会话恢复后应保留旧授权吗？

恢复后的代理对话绝不能因为继承了文字，就继承权限。模型可能仍有同一个计划、同一段工具历史和同样自信的语气。但这些都无法告诉你：发起访问的进程是不是此前获批的进程，凭据保险库是否可用，或者网关是否仍保有可信的授权记录。

应把恢复视为一项需要检查的连续性声明，而不是客户端可以自行声称拥有的权利。如果进程身份、保险库状态或网关状态发生变化，旧决定就已经结束。下一次受保护操作前，重新请求授权。

只有看到真实工作中的重启方式，你才会明白这条规则并不苛刻。编程代理会自我更新。编辑器会在崩溃后重新启动辅助程序。用户会结束卡死的进程，再启动一个新的进程。笔记本电脑进入睡眠状态时，保险库可能自动锁定。菜单栏应用会在升级后重启。对话通常会如此顺畅地重新连接，以至于用户看到的像是一条没有中断的线程。把这条线程当作权限的安全代码，会悄悄把多个独立执行合并成一次授权。

## 恢复后的对话不会携带权限

对话是上下文证据，授权则是针对当前执行者尝试当前操作所作的决定。两者必须分开。

代理系统通常会保存一个长期存在的对话标识符、一份记录，以及可能还有工具调用历史。这些信息有助于在断开连接后恢复工作。它们可以告诉代理此前想做什么、修改了哪些文件，以及哪个 API 请求执行到一半失败了。但它们无法识别当前连接到工具的操作系统进程。

想象一个常见流程。代理准备部署请求并获得会话授权。请求发出前，网关重启。代理客户端重新连接，载入旧记录，并表示自己正在恢复同一次运行。如果网关接受这一说法并恢复之前的授权，那么这份授权现在覆盖的是一个网关重启后尚未检查的进程。

新进程可能没有问题。它也可能是刚安装的构建版本、从另一个目录启动的包装可执行文件，或者找到缓存恢复令牌的第二个工具客户端。仅凭对话内容，你无法区分这些情况。

所以，「用户已经批准过这个任务」不是正确的判断标准。用户批准的是一个在明确条件下、身份可识别的代理运行。当这些条件不再成立，或者已经无法证明仍然成立时，授权就结束。

Model Context Protocol 让这个区别更清楚。它的传输文档说明，对于 stdio，客户端会将服务器作为子进程启动，并通过标准输入和标准输出交换 JSON-RPC 消息。这是一种活跃的进程关系，不是保存在记录中的持久授权。MCP 更新日志也在较新的 Streamable HTTP 工作中移除了协议层会话，并建议有状态服务器使用由服务器明确生成的句柄。这一变化本身不能解决授权问题，但可以避免把传输会话标识符误当成身份凭据。

在设计中要区分这些词：

- 对话是可能比进程存续更久的应用记录。
- 进程是有明确生命周期的操作系统实例。
- 会话授权是在该生命周期内授予某个已识别进程的权限。
- 凭据使用是一项必须满足当前保险库和网关条件的操作。

团队会混淆这些概念，是因为顺利运行的演示让四者看起来总是一起变化。生产环境中的重启会把它们拆开。

## 进程身份不止一个组成部分

单独的进程标识符不足以长期绑定授权，因为进程退出后，操作系统可能重新使用这个标识符。单独的进程路径也不够，因为该路径下的文件可能发生变化。单独的代码签名同样不够，因为同一个已签名程序可以同时运行在多个位置。

在授权时收集多项事实，用它们构成并绑定你向用户展示的身份。在 macOS 上，一个有用的起点是运行中的进程、它的可执行文件身份和代码签名机构。Apple 将 designated requirements 记录为识别已签名代码的代码要求。当应用没有提供明确要求时，它通常由签名机构和内嵌标识符构成。这样，用户检查到的就不只是一个原始 PID。

不过，不要把签名机构当成万能答案。拥有相同 designated requirement 的两个独立进程，仍然是两个独立进程。已签名代理可能启动未签名辅助程序。本地构建的开发版本可能只有临时签名。授权后控制了已获批进程的攻击者，也不会因为原始签名看起来正确就变得安全。

使用由多项有意义事实组成的进程绑定：

```text
approval_subject = {
  process_id: 48192,
  process_start_time: "2026-07-22T14:18:03Z",
  executable_file_id: "volume:.../inode:...",
  executable_hash: "sha256:...",
  signing_requirement: "anchor ... and identifier ...",
  parent_process_id: 48001,
  launch_nonce: "random-128-bit-value"
}
```

具体字段会因平台而异，但原则不变：网关需要足够的证据，在旧主体消失后拒绝旧授权。启动随机数很重要，因为它来自网关对新进程完成检查之后。客户端不能从本地缓存中安全地恢复这个值，再把它称为连续性证明。

你不必向用户展示所有字段。事实上，这通常会让授权卡片更难理解。展示签名机构、可执行文件名称和清晰的操作渠道说明即可。完整绑定保存在日志中，供日后检查。

有一个重要边界需要直说。如果网关无法看到或证明发起请求的进程，它就不能授予特定于进程的授权。它仍可以将授权绑定到另一个可信主体，但不应声称自己验证过该进程。把客户端提供的进程名称称为「身份」，正是薄弱设计获得官方式名称的方式。

## 进程重启会结束会话授权

即使替代进程拥有完全相同的代码、参数和对话记录，会话授权也应在原进程退出时结束。

这条规则能捕捉许多看似普通、直到造成损害才显出问题的情况。假设代理运行在编辑器扩展中。扩展崩溃后，监督程序启动替代进程，替代进程重新载入旧任务状态。新进程拥有相同的项目目录，也很可能使用同一个用户账户。但它没有获得原先崩溃进程的权限。

代理派生子进程时也是如此。父进程可能先获得授权，再启动辅助程序执行 shell 命令或发起网络调用。如果授权只适用于父进程，辅助程序就必须通过受控委托经由父进程行动，或者申请自己的授权。把 bearer token 沿进程树向下传递，会让授权以你正试图避免的方式变得可携带。

不要用很长的过期时间来修补这个问题。任何替代进程都能恢复的五分钟授权，并不是五分钟的会话授权，而是一个贴着友好标签的五分钟 bearer 凭据。

更好的规则很简单：

```text
if current.process_id != approved.process_id:
    deny("approval belongs to a different process")

if current.process_start_time != approved.process_start_time:
    deny("process lifetime changed")

if current.launch_nonce != approved.launch_nonce:
    deny("gateway has not bound this run")
```

网关应在判断请求是否符合会话授权条件前执行这些检查，不要只在代理重新连接时检查。对于会复用工作的客户端架构，进程可能在客户端内部发生变化，旧授权对象也可能在内存中存活得比预期更久。

用户可能会抱怨，替代进程显然还是同一个代理。这通常说明授权卡片需要更顺畅，而不是说明你应该抹掉边界。解释代理已经重启，并显示新的签名方。多一次明确点击的成本，低于调查一次由未检查进程发起的意外生产调用。

## 保险库状态变化会取消操作权

保险库状态不是后台细节。保险库锁定后，任何依赖其可用性的旧决定都必须停止为需要凭据的操作授予权限。

需要避免两类失败。第一类很明显：网关在保险库锁定后仍继续使用已解密的凭据。第二类更隐蔽：网关在锁定期间将需要凭据的操作排队，保险库解锁后又因为旧会话授权仍然存在而自动执行。第二类失败会把之后的一次人工解锁，变成对更早请求的意外批准。

Apple 的 Keychain 文档清楚地划出了边界。访问控制可以要求应用在尝试取回项目时确认用户在场，Apple 也建议选择适合应用的最严格可访问性设置。Secure Enclave 可以在不向用户空间软件暴露底层生物识别数据的情况下，为加密操作设置门槛。这些机制很有用，但不会替你决定网关的语义。应用必须明确：保险库的门关闭后，已经批准的代理运行应如何处理。

最安全的做法是维护一个保险库纪元。每当保险库锁定、解锁、重置或失去受保护会话时，都递增该值。每条授权记录都附带当前纪元。纪元不匹配，就不能授权需要凭据的操作。

```text
approved_vault_epoch = 17
current_vault_epoch = 18

if approved_vault_epoch != current_vault_epoch:
    require_new_session_approval()
```

这不意味着每次解锁都必须变成令人厌烦的流程。如果代理没有活动会话，就不会发生任何事。如果解锁后某个请求需要凭据，网关可以显示新的授权卡片并说明原因：此前授权之后，保险库状态发生了变化。对于设置为每次使用都需要授权的凭据，仍要执行逐次调用检查。会话授权和逐次调用授权回答的是不同问题。

会话授权询问的是：这个已识别进程在当前运行期间是否可以使用这个渠道。逐次调用授权询问的是：用户现在是否愿意允许这一次具体使用。把会话授权当成逐次调用决定的替代品，会破坏将凭据标记为逐次授权的意义。

## 网关重启会清除授权记忆

网关重启后，应使内存中的会话授权失效，因为重启后的网关无法证明旧授权记录仍然完整、未被篡改，并且仍然绑定着现实中的活动主体。

有些团队会持久化授权令牌，并在重启后重新载入。动机可以理解：用户已经点击过一次，代理还在工作，重启不应让流程失败。但持久化令牌很容易变成可重复使用的凭据。客户端重新连接并发回令牌，网关识别它，旧权限就在没有重新检查进程的情况下恢复。

这种设计也让恢复变得含糊不清。网关是在发送操作前崩溃的吗？还是已经发送操作，却在记录响应前崩溃？远程服务是否在重试后收到了两次请求？把授权恢复和请求重试当成一个操作，会把身份恢复与交付恢复混在一起。它们需要不同的控制措施。

为每个网关生命周期生成一个只存在于当前内存中的启动标识符，并将其加入每条授权记录。重启后，当前启动标识符会改变，因此此前的会话记录都无法匹配。

```text
approval = {
  gateway_boot_id: "b7f9...",
  process_binding: "...",
  vault_epoch: 17,
  approved_at: "2026-07-22T14:20:11Z"
}

if approval.gateway_boot_id != gateway.current_boot_id:
    require_new_session_approval()
```

网关可以持久化一条审计事件，说明某次授权曾经发生。但它不应把这条事件重新载入为活动权限。审计历史用于解释发生过什么，不会重新创造一段活跃的权威关系。

Sallyport 采用了这种形态：保险库保存在带签名的菜单栏应用中，保险库锁定时拒绝操作。它的逐会话授权绑定到新的代理进程，用户可以立即撤销已记录的运行。只有在重启或锁定无法悄悄把旧运行重新接到新决定上时，这些边界才真正有用。

## 将授权绑定到客户端无法重放的证据

授权记录需要由服务器创建、并且代表当前状态的证据。客户端提供的恢复令牌可以帮助网关找到对话或显示有用的标签，但不能证明授权仍然有效。

最小可行模式使用四个会变化的值：

1. 网关启动时生成随机启动标识符。
2. 网关识别出新连接的进程后，生成随机运行随机数。
3. 保险库维护一个在受保护可用性变化时更新的纪元。
4. 只有用户批准已识别的运行后，网关才创建授权标识符。

之后，网关针对每个值检查每项操作。客户端可以请求操作，但无法伪造出同时匹配新启动标识符和新运行随机数的授权。

下面是团队可以调整的简化状态模型。它有意把对话引用只作为显示上下文保存，而不作为授权字段。

```json
{
  "run": {
    "conversation_ref": "worktree-cleanup-42",
    "process": {
      "pid": 48192,
      "started_at": "2026-07-22T14:18:03Z",
      "signing_requirement": "recorded-at-approval",
      "launch_nonce": "gateway-generated"
    }
  },
  "approval": {
    "id": "gateway-generated",
    "gateway_boot_id": "gateway-generated",
    "vault_epoch": 17,
    "expires_when_process_exits": true
  }
}
```

注意这里缺少什么：没有可重复使用的 `resume_authorized` 标志，也没有会把已死亡进程变成活动主体的过期时间戳。你可以保留较短的超时时间作为额外限制，但超时不是身份。

代理重新连接时，应让它重复初始化和进程注册。网关随后应返回以下结果之一：

```text
REAUTH_REQUIRED process_changed
REAUTH_REQUIRED gateway_restarted
REAUTH_REQUIRED vault_state_changed
RETRY_SAFE previous_action_not_started
STATUS_UNKNOWN inspect_activity_journal
```

`RETRY_SAFE` 与 `STATUS_UNKNOWN` 的区别很重要。只有在拥有持久证据证明操作没有开始时，网关才能说可以安全重试。如果网关将 HTTP 请求交给网络栈后断电，诚实的答案可能是未知。代理应先检查目标或活动日志，再尝试重复操作。

## 重试操作与恢复授权是两回事

重连协议应先恢复通信，再建立新的权限，最后决定是否重试。把这些步骤合并，会产生重复请求和继承下来的授权。

受保护操作失去连接时，可以按以下顺序处理：

1. 代理重新连接，并初始化新的网关关系。
2. 网关识别当前进程，并将它与当前运行记录比较。
3. 网关检查启动标识符和当前保险库纪元。
4. 如果任何权限事实发生变化，网关必须在使用凭据前请求新的会话授权。
5. 网关报告它是否知道此前的操作没有开始、已经完成，或者状态未知。

顺序很重要。不要让代理在建立当前授权前重发原始请求。否则重连客户端就有机会围绕旧请求施压：「我已经获批了，只要完成它。」人们常会看到相同的文字，就以为请求只是无害的延续。网关应在描述操作前，让变化后的条件清楚可见。

对于支持幂等键的 HTTP API，应使用幂等键。为逻辑业务操作生成密钥，在发送前持久化记录，并且只有重新建立授权后才能在重试时复用。幂等键可以帮助远程服务识别重复交付，但不能为重试授权。

SSH 的重试需要更谨慎，因为连接中断前，远程命令可能已经对机器做了部分修改。优先使用会写入明确标记的操作，或在再次修改前查询现有状态。`mkdir` 这样的命令可以配合预期目录检查来降低风险。轮换凭据或重启服务的命令，在传输失败后通常应报告未知状态，直到代理读取远程状态。

不要用乐观措辞掩盖这种不确定性。代理在失去响应后说「部署已完成」，是在捏造确定性。日志应说明没有观察到操作结果，代理也应继续调查。

## 丑陋的失败能揭示正确边界

最能说明问题的测试不是干净的重连，而是在操作进行到一半时重启，然后观察代理是否努力继续执行。

设想一个拥有会话授权的代理，它通过 HTTP API 更新问题跟踪器。代理准备请求并获批，随后调用网关。网关注入凭据并开始发送请求。就在这时，网关应用重启。代理使用此前的对话引用和缓存的本地工具状态重新连接。

薄弱设计会接受旧引用、恢复授权并重试请求。问题跟踪器可能收到两条评论。更糟的是，这份授权可能适用于现在提供缓存状态的任何进程。

谨慎的设计会产生一个不那么漂亮、但更可靠的结果。重启后的网关生成新的启动标识符，拒绝旧会话授权，并要求对当前连接的进程重新授权。它检查活动日志。如果日志记录请求已经完成，就返回结果。如果记录表明没有请求离开网关，就允许在新授权下重试。如果操作已经越过交付边界，但结果丢失，就报告未知状态。代理读取问题跟踪器，再判断是否有必要再次写入。

这比信任恢复令牌需要更多工作，但这也是可追踪的操作系统与把歧义隐藏到用户发现重复修改之间的区别。

在认定恢复行为安全前，至少测试以下情况：

- 代理获批后结束进程，再启动一个使用相同对话引用的替代进程。
- 在已获批代理仍然运行并尝试另一次需要凭据的调用时重启网关。
- 在已获批代理等待重试时锁定并解锁保险库。
- 启动两个签名完全相同的代理进程，确保其中一个的授权不会覆盖另一个。
- 在网关开始外部请求后丢弃网络响应，确认重试路径会报告交付不确定性。

这些测试会抓住一个尤其常见的错误：工程师只测试合法客户端是否能够恢复，却不测试其他客户端能否借用恢复路径。

## 日志必须说明权限为何结束

审计日志应把权限变化记录为一等事件，不要让调查人员只能从工具调用之间的空白推断发生了什么。

记录会话授权时，保存所使用的进程身份事实、网关启动标识符、保险库纪元以及面向用户显示的主体。记录结束事件时，给出明确原因：进程退出、进程身份不匹配、网关重启、保险库锁定、手动撤销或授权过期。被拒绝的操作和没有发生的操作也应分别记录，因为它们代表不同事实。

有用的活动序列可以是：

```text
14:20:11 session_approved run=R31 signer="Example Developer ID" boot=B8 vault=17
14:23:04 gateway_restarted previous_boot=B8 current_boot=C2
14:23:06 action_denied run=R31 reason=gateway_restarted
14:23:09 session_approved run=R32 signer="Example Developer ID" boot=C2 vault=17
14:23:12 http_action_started run=R32 request=Q44
14:23:13 http_action_result run=R32 request=Q44 status=201
```

重点不是在每条日志中暴露秘密值或完整请求正文，而是保留因果链：谁发起了请求，使用了哪项权限，哪个状态发生了变化，以及网关是否真的执行了操作。

这里还需要防篡改证据，因为只有能发现后续改写，授权历史才有用。Sallyport 会从不可写入的加密哈希链审计日志中生成 Sessions 和 Activity 日志，`sp audit verify` 可以在离线状态下对密文验证哈希链，而不需要保险库密钥。这样，审查者无需打开驱动操作的秘密，也能验证事件顺序。

不要让审计日志承担预防责任。一份完美记录如果说明旧授权被重复使用，它只是错误决定的证据，并不能修复错误。实时网关必须在注入凭据前，当绑定不再匹配时拒绝操作。

## 重新授权应具体到值得打断用户

发生有意义的状态变化后重新授权是合理的，但含糊的提示会教用户直接点击通过。授权卡片应说明发生了什么变化，以及现在是谁在请求。

避免使用「会话已过期，要重新授权吗？」这样的通用信息。这会让用户把事件理解成计时器。应直接显示破坏连续性的条件，例如：「网关已重启。是否允许当前新连接的进程在本次运行中使用 Git 托管 API？」如果进程身份发生变化，显示新的签名机构，或明确说明程序未签名。如果保险库锁定，说明解锁保险库不会恢复此前的代理授权。

这也是判断某个渠道的会话授权是否过于宽泛的时机。如果恢复后的代理想要执行生产环境 SSH 修改，即使新的会话决定已经通过，逐次调用授权也可能是更合理的选择。会话通过所有检查，并不意味着每条命令都同样安全。

不要为了缓解授权疲劳而让授权可以转移。应减少不必要的变化，在进程身份稳定时清楚展示它，并让每个决定的范围容易理解。用户可以批准一个具体请求，但不能安全地批准这样一个承诺：任何拥有旧记录的未来进程都可以行动。

实现规则可以写在便签上：恢复时保留上下文，但根据实时证据重建权限。进程、保险库或网关发生变化后，旧授权属于过去的运行。
