# 代理重启后的授权：让权限自动过期

Mac 重启为团队提供了一个清晰的技术边界。应该利用它。代理进程会终止，内存会消失，所有针对这次特定运行的批准也应该随之失效。试图让自主工作在重启后悄无声息地继续，通常会把一种范围明确、便于检查的权限变成长期访问，而过期时间却说不清楚。

这并不意味着重启应该抹去一切。团队需要保留能够解释过往操作的证据、支持未来工作的加密凭据，以及足够的任务上下文，以便有意地恢复工作。原则很简单：保留记录和受保护材料，丢弃活动授权。然后要求有人批准新启动的进程，之后它才能与外部世界通信。

## 代理重启后的授权必须从空白运行状态开始

代理重启后的授权应该从以下状态开始：没有活动进程授权，没有已解锁的保险库，没有继承的会话密钥，也没有新进程可以继续使用的既有批准。重启会终止获得批准的那个对象。仅仅因为替代进程使用相同的代码检出、命令或代理名称，就把它视为原进程的延续，这是身份判断错误。

有人会认为，这会让操作系统更新或断电后的恢复变得麻烦。它确实会增加一次有意设置的暂停。这次暂停迫使操作人员查看当前想要获得权限的进程，而不是几个小时前在不同条件下批准的进程。

清晰的重启边界有四个实用特点：

- 清除内存中的访问令牌、已解密的密钥句柄、排队的确认请求和进程标识符等易失性材料。
- 防止代理在无人值守期间继续携带批准，而操作人员可能早已不在场。
- 为团队提供可靠的审计标记，用来重建某个操作发生在重启之前还是之后。
- 暴露对本地缓存、后台辅助程序和连接复用器的隐性依赖。

不要把重启和用户注销混为一谈。注销也应该结束代理授权，但重启更容易测试，因为它会终止几乎所有普通进程。如果某项授权在重启后仍然存在，就说明有人有意保存了它，或者创建了一个处于代理生命周期之外的辅助程序。两种情况都值得检查。

即使代理二进制文件经过代码签名且没有变化，这条规则仍然适用。代码签名可以帮助操作人员识别程序的发布者，但无法证明当前运行的进程与上一次运行使用了相同的指令、环境变量、代码仓库状态、工具配置或操作人员意图。重启后的签名进程仍然可能收到危险提示。

计划中的任务也一样。假设代理准备了一次数据库迁移并请求批准，但 Mac 在执行前重启了。计划可以保存在工作目录中，但执行它的权限不能保留。重启后，代理应该再次展示预定操作，由操作人员判断这项工作是否仍然正确。

许多设计正是在这里变得粗心。它们保存一条写着“已批准”的持久记录，然后把它称为会话。这条记录会变成可转移的权限，因为后来的进程可以声称自己拥有它。会话授权需要与某个进程实例保持活动绑定，并且拥有短暂、明确的有效期。进程退出后，授权记录应该显示它已经结束，而不能继续处于可复用状态。

## 保留证据和配置，丢弃活动授权

团队应该保存能够解释工作过程的事实，以及可以安全复用的配置，同时删除或使所有能够立即授予权限的对象失效。将这些类别放入不同的存储和生命周期范围。把它们混在一起，就会出现常见的问题：审计记录意外变成授权令牌。

下面的划分在实践中效果很好：

| 跨重启保留 | 重启时过期 |
|---|---|
| 只追加的操作历史和批准决定 | 代理进程授权及其运行标识 |
| 加密的 API 和 SSH 凭据材料 | 已解锁的保险库状态和已解密的凭据句柄 |
| 端点定义、允许选择的凭据和任务引用 | 内存中的 bearer 令牌和 HTTP 连接状态 |
| 为过往运行记录的进程签名授权 | SSH 控制套接字和运行中的辅助进程 |
| 待处理任务描述及其此前状态 | 批准对话框、排队操作和重试权限 |

第一列支持连续性。第二列防止连续性变成无声的权限保留。

保留中断工作的状态，但让状态只承担描述作用。好的记录会写明：运行 `R-1842` 请求执行一条 SSH 命令并获得批准，但在执行前因为主机重启而停止。糟糕的记录会写成：运行 `R-1842` 可以在下次启动后执行剩余操作。前者让操作人员做出决定，后者却在不知道后续进程情况的前提下提前替人做了决定。

凭据存储也需要同样准确的表述。保存在保险库中的 API 密钥可以以加密形式跨重启存在，但解密后的形式不应仅仅因为机器很快重启就继续可用。锁定保险库会建立一个明确的节点，让 Mac 前的人重新证明自己在场。这与决定代理进程是否可以使用某个凭据，是两项不同的决定。

Sallyport 遵循这种分离方式：保险库锁定时，保险库网关会阻止所有操作；每次会话授权适用于新连接的代理进程，而不是某个被记住的任务名称。这是两个不同的决定，把它们合并会让事后调查困难得多。

不要把批准藏在便利功能中来持久化。下面这些例子乍看无害，但叠加起来就会产生问题：

- 启动代理重新启动 MCP 客户端，并把旧会话文件交给它。
- SSH 客户端在 `/tmp` 或用户缓存目录中保留控制套接字。
- 脚本把 bearer 令牌复制到环境文件中，以便重启后继续重试。
- 任务运行器发现有未完成的任务，在确认目标是否变化前就执行它。

每项功能都声称是在保留进度，但它们也可能在不告诉操作人员当前由谁或什么持有权限的情况下保留授权。

改用中断记录。记录任务引用、旧运行标识、预定操作列表的摘要、目标名称，以及诸如 `stopped_by_reboot` 这样的状态。不要放入可用凭据、Cookie、批准令牌，或启动器可以执行的指令。下一次运行时，把这条记录作为上下文展示给操作人员。上下文有助于检查，授权应该来自新的决定。

## 重启不是凭据轮换事件

重启应该让代理权限过期，但不应该自动轮换 API 密钥或 SSH 密钥。这些控制措施针对不同的故障模式。授权过期限制谁可以使用现有凭据，以及可以使用多长时间。轮换则是在怀疑凭据暴露、丢失、被滥用或访问需求发生变化时替换凭据。

如果把每次重启都当作暴露事件，团队会浪费时间并破坏集成。如果用例行轮换掩盖泄露的凭据，却不查明它从哪里泄露，也会产生虚假的安全感。轮换密钥无法修复这样的设计：它把密钥交给代理、写进对话记录，或写入 shell 历史。

NIST Special Publication 800-63B 将会话管理与验证器生命周期分开。其指导原则把会话终止和重新验证视为明确的控制措施，而验证器替换则针对另一类问题。这种区分非常适合代理系统。重启时结束活动代理运行，只有在证据或政策要求时才轮换底层凭据。

如果重启本身发生在可信的暴露事件之后，可以在重启后进行轮换。例如，发现密钥进入提示日志、发现未知进程访问了代理环境、笔记本丢失，或得知前团队成员仍保留复制的凭据。这些情况下，重启只是附带事件，真正推动轮换的是疑似泄露。

长期有效的 bearer 凭据需要格外谨慎，因为只要网络允许，它们可能在任何地方生效。如果代理曾经拿到过明文值，重启过期赖以成立的清晰边界就已经消失。机器重启前，代理可能已经存储或传输了该值。之后的新会话批准无法把它收回来。

SSH 也有自己的陷阱。私钥可能仍然安全地保存在本地保险库中，但已有的 SSH 连接可以在连接结束前继续执行远程通道。SSH 连接复用也可能留下本地控制套接字，供后来的客户端使用。正常情况下，重启应该清除这两类状态，但不要依赖假设。测试团队实际使用的客户端选项和辅助程序行为。

实际政策可以这样制定：保留加密的源凭据，重启时锁定它们，结束所有授权和活动传输，然后在网关再次使用凭据前，要求新进程获得新的授权。根据暴露情况和人员变化设置轮换触发条件，而不是根据任意的启动事件轮换。

这项政策也能让事件响应保持真实。如果操作人员说“我们已经重启了，所以访问权限已重置”，就要追问凭据是否曾离开受保护存储，以及远程服务提供商是否保留独立会话。重启只会重置本地运行时状态。除非云服务提供商收到撤销或轮换事件，否则它不会让云端令牌失效。

## 设备解锁、人在场和进程批准是不同的事实

安全恢复需要分别回答三个问题：Mac 能否访问受保护的凭据？是否有一名可追责的人在场？哪个进程正在请求使用这些凭据？如果设计用同一个信号回答三个问题，就会赋予这个信号过多含义。

设备解锁控制对本地用户环境的访问。它可以说明有人通过了 Mac 的登录保护。在支持的硬件上，保险库可以使用 Secure Enclave 和 Touch ID，在设备锁定时让秘密保持不可用。这能保护静态材料，也能建立清晰的操作边界，但它并不能具体说明代理框架接下来启动的是哪个进程。

人在场是某个时间点的事实。生物识别确认或一次点击可以为某个具体决定证明人在场。如果让一次在场检查默默批准直到下次重启前的所有外部操作，这个时间点的含义就远超操作人员原本的意图。当编程代理可以持续运行数小时、读取不断变化的代码仓库文件，或接受拉取请求和问题评论中的指令时，风险会进一步增加。

进程批准回答的是一个更窄的问题：我是否授权这个刚刚运行的程序，在本次运行期间调用网关？批准界面应该使用代码签名授权等持久证据来识别进程，不能要求操作人员解读可变的进程标题。`agent` 这样的名称不是身份，任何人都可以使用它。

顺序很重要。首先，保险库必须可用。然后，网关识别进程。接着，操作人员批准该进程执行请求的运行。对于标记为特别敏感的凭据，每次使用都再次请求确认。这样团队得到的是三个各司其职的控制点，而不是一个含义过大的“允许代理”按钮。

不要用 Mac 账户名代替进程身份。共享本地账户可以运行多个终端会话、构建工具、编辑器和代理宿主。如果一项批准跟随整个账户，那么恶意 shell 命令或第二个代理就可能使用原本为其他进程准备的权限。

批准决定还不应该假装回答它无法回答的范围问题。进程级授权表示谁可以在某次运行期间调用网关，但不应该默默意味着所有凭据或所有操作永久开放。将它与凭据选择结合起来，并在适当时要求每次调用确认。这样，高影响凭据就不会继承低风险 API 调用的便利权限。

人们很容易想用复杂的策略语言解决问题：根据时间、源路径、分支名称、主机名、命令模式和提示内容设置条件。这类系统可以服务于专门的安全团队，但当普通开发者无法预测结果时，也会制造另一种风险。少量清晰可见的决定更容易在重启后执行，也更容易在审查时解释。

## 恢复指定运行，而不是宽泛的团队权限

只要让工作项持久存在，让授权保持短暂，团队就可以安全地恢复中断的工作。重启后的代理应该获得足够的上下文来继续工作，但必须像新进程一样重新获得权限。记住整个项目的批准是错误的捷径，因为无关工作也会继承旧的操作人员决定。

为每次重要运行设置一个持久引用，并使用人们已经熟悉的标识。开发任务可以使用代码仓库路径和分支。运维任务可能更适合使用工单号、变更请求、环境名称或事件标识。引用本身不授予权限，它只是让人能够把重启后的运行与预期工作进行对照。

有用的恢复卡片或终端提示应包含五项信息：

1. 之前的运行标识和结束原因，例如 `stopped_by_reboot`。
2. 工作引用，以及旧运行使用的代码仓库修订版本或部署产物。
3. 新进程提出的下一项外部操作，包括目标和凭据名称。
4. 当前进程的身份证据，而不只是旧进程的身份。
5. 批准本次运行、拒绝本次运行，或查看之前操作记录的选项。

不要未经检查就恢复整个操作队列。Mac 停机期间，外部世界可能已经改变。拉取请求可能被强制推送，DNS 记录可能指向别处，部署可能已经通过其他途径完成，维护窗口也可能已经关闭。代理早先形成了计划，并不意味着之后产生副作用仍然合适。

考虑一个通过 HTTP API 更新服务器集群的代理。重启前，它成功修改了主机 A 到 D，然后准备调用 E 到 H。Mac 重启。启动后，代理找到旧队列并尝试继续。粗心的实现会复用令牌并发送针对 E 到 H 的请求。更安全的实现会读取中断记录，创建新的运行，请求授权，获取当前状态，并展示剩余的预定调用。它可能发现另一名操作人员已经修改了 F 和 G。新的授权给了操作人员发现这一变化的机会。

重试行为需要明确的边界。如果网关因为保险库锁定或进程没有批准而拒绝调用，客户端应该停止并报告被阻止的操作。它不应循环重试、反复打开提示、退回到直接网络访问，或使用环境变量中的凭据替代。临时网络故障后的重试只有在该调用仍然拥有有效授权时才合理。

这种方式不要求代理忘记工作。保留计划、命令输出、代码仓库差异，以及描述中断情况的通俗说明。把这些文件视为新决定的证据。设计会议中，这个区别听起来很小；发生事件时，它却非常重要：保存的计划解释了意图，而继承的授权会在没有新的责任决定时执行操作。

对于敏感操作，要求重启后的代理在提出下一项操作前重新读取当前状态。这对破坏性 API 调用和效果取决于当前主机状态的 SSH 命令尤其有用。额外的读取不是权限，而是检查旧计划是否仍然描述现实世界。

## HTTP 和 SSH 需要明确的重启规则

HTTP API 和 SSH 在重启后都需要新的代理授权，但它们的隐藏状态不同。笼统地说“会话已重置”会漏掉故障。应分别写明每个通道的重置行为，然后测试代理实际使用的路径。

对于 HTTP，要区分凭据与远程服务签发的访问令牌或 Cookie。网关可以在本地加密保存凭据，只有获得批准后，才将凭据注入请求。代理应该收到响应，而不是 bearer 凭据。重启后，丢弃本地缓存的访问令牌、为重试保存的请求头、自动化使用的浏览器式 Cookie 罐，以及开放的连接状态。

如果客户端之后提交仍然有效的刷新令牌或 Cookie，服务提供商可能会在重启后继续保留远程会话。因此代理不应该持有这些材料。否则，它可以直接调用服务提供商，绕过本地重启规则。应把凭据注入和令牌刷新放在边界的操作侧，由获得新授权的进程调用。

对于 SSH，要终止客户端连接并检查多路复用。OpenSSH 可以通过 `ControlMaster` 和 `ControlPath` 复用主连接，这对交互式用户很方便，却可能让人不清楚哪个调用拥有远程会话。代理网关应该使用无状态执行路径，或使用能在代理运行结束时干净终止所有辅助程序的生命周期。不要假设本地界面关闭后远程命令就停止了。

对两个通道都使用下面这套可复现的重启测试：

1. 启动一次代理运行，批准针对非生产目标的无害 HTTP 请求或 SSH 命令。
2. 记录运行标识、进程身份、预定请求，以及上一次已完成操作的时间。
3. 在代理执行第二项预先安排的操作前重启 Mac。
4. 不修改任务文件，重新启动代理宿主，然后要求它执行第二项操作。
5. 确认第一次尝试在保险库可用且新进程获得批准前会被拒绝。然后检查记录，确保第二项操作属于不同的运行标识。

如果网关支持命令行审计验证器，请在测试前后运行验证器：

```
sp audit verify
```

该命令应该报告加密审计链是否通过验证，而且不应要求解锁保险库。不要编写自动化程序去解析面向人的命令所输出的虚构成功文本。检查文档规定的退出状态，并将命令输出与测试记录一并保存。Sallyport 会从防写、哈希链加密审计日志中生成会话日志和单次调用日志，因此验证器可以在重启后为操作人员提供离线完整性检查。

测试也应该覆盖失败路径。尝试使用旧环境变量、缓存的 HTTP 客户端配置、SSH 控制套接字和第二个本地代理进程。如果其中任何一个能在没有新授权的情况下抵达目标，重启边界就只是装饰。修复绕过路径，不要再给操作人员增加一条提醒。

## 审计记录必须解释旧运行和新运行

重启后的审计轨迹应该告诉调查人员一次运行在哪里停止，以及另一次不同的运行从哪里开始。即使同一个用户、代码仓库和代理框架继续执行同一任务，也必须让这种分离清晰可见。如果记录把两次事件合并成一个长会话，就无法回答是谁批准了重启后的操作。

明确保存旧运行的终止状态。可用状态包括正常退出、因保险库锁定而拒绝、等待批准而拒绝、主机关机、网络故障和操作人员撤销。新进程启动时不要覆盖这个状态。重启记录应该指向之前的运行，而不是与它合并。

对于新运行，记录授权时提交的进程身份凭证、批准时间和第一次外部调用。第一次调用很重要，因为批准并不总意味着实际使用。操作人员可能批准了一个最终在执行前退出的运行。区分批准和执行，可以避免审查时把“允许执行”误说成“已经执行”。

单次调用记录应该保留足够的上下文，让人能够重建操作，同时不保存秘密。对于 HTTP，记录目标、方法、凭据名称、结果类别，以及经过脱敏的请求元数据。对于 SSH，记录目标、账户名称、命令或经批准的命令摘要、退出状态和结果元数据。命令输出的具体保留方式取决于其敏感性，但不要抹去操作发生过这一事实。

哈希链可以更容易地发现之后的篡改，但不能让篡改变得不可想象。它的价值在于每条记录都会提交之前的记录，离线验证能够发现链条断裂。它不能证明获得批准的操作就是明智的，也不能阻止有权发起操作的人。团队仍然需要审查和严格的批准边界。

让审计验证器处于正常代理路径之外。能够改写或认可自身证据的代理，会形成循环信任主张。操作人员应该能够独立运行验证，包括在保险库仍然锁定时运行。他们还应该在审计数据副本中修改记录，测试系统的反应，以便在真正需要时知道预期的失败行为。

即时撤销也需要记录范围。如果操作人员在重启后撤销一个会话，日志应该显示哪个运行失去了权限，以及之后哪些调用被拒绝。除非确实发生了全局禁用，否则不要使用“代理访问已禁用”这种笼统表述。准确的记录可以避免团队猜测另一个进程是否仍然持有批准。

## 在自动化替你决定之前，先确定过期规则

安全的重启政策应该短到每位开发者都能准确复述：重启会锁定受保护材料，结束所有代理进程授权，同时保留证据和不可执行的任务上下文。新启动的进程获得新的审查授权。只有在暴露情况或生命周期规则要求时，才轮换凭据。

用运维语言写下这项政策，并为每个例外指定负责人。如果团队声称需要在重启期间保持自动化不中断，就追问：会继续执行什么操作？由哪个账户负责？如何监控？为什么不能接受人工批准边界？这可能描述的是服务账户工作负载，而不是交互式编程代理。应为它设计独立方案，不要悄悄把代理会话变成服务器凭据。

为意外重启后的第一个工作日设定可预测的处理流程。操作人员验证审计完整性，检查旧运行最后完成的调用，在适当时解锁受保护材料，启动新的代理进程，检查恢复的任务，然后只批准仍然符合当前情况的工作。与其撤销一项在无人意识到批准仍然有效时执行的操作，这种小幅中断要容易得多。

不要承诺重启逻辑本身就能让自主工作安全。它只能创造一个清晰的决定点。决定的质量仍然取决于明确的进程身份、范围狭窄的凭据使用、容易理解的操作说明，以及其他人之后也能检查的记录。

下次重启中断真实任务时，不要急着添加“自动继续”开关。保留计划，保留证据，让新进程在使用权限前再次请求批准。
