# Mac 睡眠和唤醒后如何管理代理权限

已批准的代理进程不应仅仅因为 Mac 唤醒后内存中仍保留着相同的进程，就自动继承原有权限。睡眠、显示器变黑、屏幕锁定、合上盖子和唤醒是不同的操作系统事件，但它们都带来同一个安全问题：批准这次运行的人，是否仍在场并且能够介入？

把这个问题当作授权决策，而不是电源管理细节。如果允许编程代理持有 API 或 SSH 权限，一次过期的批准就可能把普通的喝咖啡、通勤或夜间暂停，变成无人看管的凭据使用。解决方法不是堆出一座规则迷宫，而是建立一个小型状态模型、清楚的过期策略，以及能够覆盖那些人们通常会跳过的尴尬转换的测试。

## 睡眠不是一个单一事件

macOS 区分系统睡眠和显示器睡眠，这一点很重要，因为黑屏并不能证明代理已经停止。Apple 为 `willSleep`、`didWake`、`screensDidSleep` 和 `screensDidWake` 提供了单独的 NSWorkspace 通知；睡眠和唤醒通知不会携带用户数据来说明转换发生的原因。

笔记本电脑可能只是调暗或关闭显示器，而构建任务、网络传输或本地进程仍在继续。台式 Mac 也可以在没有活动显示器的情况下运行。连接电源和外设的笔记本电脑，行为可能与使用电池时不同。你不能根据一个像素变黑，就推断出权限决定。

设计时应分别使用以下四种事实：

- **显示器状态** 表示屏幕是进入睡眠还是恢复唤醒。
- **电源状态** 表示机器是否准备睡眠，或是否刚从睡眠中唤醒。
- **用户在场状态** 表示会话是活动、锁定、已注销，还是已经切换到其他用户。
- **保管库状态** 表示是否允许使用密钥。

团队经常把前三项压缩成一个名为 `isAwake` 的布尔值。这种捷径会泄露权限。屏幕睡眠时，代理可能仍在运行。机器也可能在锁屏界面唤醒。用户可以锁定屏幕，而不让系统进入睡眠。每种情况都需要单独定义预期结果。

正确的默认规则严格但容易解释：受保护操作需要已解锁的保管库，以及属于当前权限周期的当前批准。唤醒、锁屏、用户会话变化、手动撤销或保管库锁定，都可以推进这个周期。一旦周期推进，来自旧周期的调用就必须失败。

## 屏幕锁定应结束交互式权限

屏幕锁定是最明确的信号，说明交互式批准应该停止。Mac 可能继续运行，长期存在的终端进程可能仍保留套接字和内存，但批准操作的用户已经离开了交互会话。继续使用之前的一次点击，发送 API 请求或 SSH 命令，很难为其辩护。

这并不意味着每个代理都必须在锁屏时退出。是否终止本地计算是另一个决定。如果不需要经过需要凭据的网关，模型仍可以读取代码库、编译代码或准备补丁。边界在于外部操作。保留工作，移除权限。

这个区分很有用，因为它避免了全有或全无的选择。你不必在冻结代理和完全开放代理之间二选一。让代理继续执行本地工作区内安全的任务，然后让下一次受保护调用返回明确的拒绝：

```text
authorization_denied
reason: authority_epoch_changed
required: unlock_vault_and_approve_session
```

有用的拒绝信息应说明发生了什么，但不能泄露秘密，也不能假装请求是因为网络问题失败。代理可以暂停、记录阻塞原因并等待用户，而不是反复尝试同一个可能造成破坏的命令。

不要因为锁屏来自空闲计时器，就为它设置例外。这种情况往往正是用户忘记代理仍在运行的时候。手动锁屏和自动锁屏表达了不同的人类意图，但都不能证明用户仍然能够批准生产环境变更。

只有一种狭窄情况可以考虑在锁屏后保留权限：用户明确授予了一个范围严格受限、专门用于无人看管任务的能力。这应该是独立的运行类型，而不是交互式批准中的隐藏例外。如果你的夜间自动化和白天由对话驱动的代理行为完全一样，就说明风险还没有被分开。

## 唤醒事件应开启新的权限周期

即使已批准的进程继续存在，唤醒也应使交互式批准失效。进程可能在睡眠前暂停，之后用相同的 PID、相同的环境变量和相同的打开文件描述符恢复。以上任何一点都不能证明旧的人类决定仍然适用。

权限周期是一个单调递增的值，用来标记授权可能有效的连续时间段。当系统跨过你关心的边界时，应在处理下一次操作前推进周期。进程不需要协商这一变化，而是在下一次请求时发现它。

最小授权记录可以是这样：

```json
{
  "session_nonce": "6a018d62-2e94-4d4a-9e79-1f4e4b5ca501",
  "process_id": 84172,
  "process_start_marker": "2026-07-22T14:03:18Z",
  "signing_authority": "approved-agent-binary",
  "authority_epoch": 27,
  "approved_at": "2026-07-22T14:04:01Z",
  "per_call_approval": false
}
```

进程 ID 只是其中一个字段。macOS 可能在进程退出后重新使用 PID，因此如果网关忘记清理旧记录，之后再次看到同一个数字，单独依靠 PID 就会有风险。应将批准绑定到新的 nonce 和已观察到的进程实例。使用代码签名权限作为可执行文件的身份信号，同时仍要求每个新进程运行都进行新的会话批准。

事件处理器看到 `willSleep` 时，应记录撤销意图，并立即停止接受新的受保护调用。Apple 表示，观察者最多可以延迟睡眠处理 30 秒，但不要利用这段时间完成待处理操作队列。应拒绝这些操作，或将其标记为已中断。用户没有授权在合盖期间进行最后一轮操作。

机器报告唤醒后，如果需要就再次推进周期，并保持保管库闸门关闭，直到用户完成解锁要求。这样可以应对不完美的事件顺序。电源事件在边缘情况下很复杂，比起让一个带有秘密的调用在转换期间漏过去，额外要求一次批准更安全。

## 合盖需要单独测试

对人来说，合上笔记本盖子就像发出睡眠命令，但软件不应假定这个物理动作会清晰地对应某一个操作系统事件。电源来源、外接显示器、扩展坞和系统设置都可能改变机器行为。唯一诚实的做法，是测试团队实际使用的硬件和配置。

安全策略仍然可以很简单：一旦观察到可靠的相关边界，合盖就结束交互式权限。在普通便携设备配置中，`willSleep` 会给你一个提前阻止新操作的机会。如果某种配置在合盖后仍保持唤醒，就使用用户会话或显示器边界作为保守的后备措施。不要等待一个名为 `lidClosed` 的完美语义标签。你真正需要阻止的是无人看管的凭据使用。

让代理持有一个无害的凭据能力来进行测试，例如向测试 API 写入标记，或对一次性 SSH 主机运行无害命令：

1. 启动新的代理进程并批准其会话。
2. 确认一次受保护调用成功，然后让代理准备执行下一次调用。
3. 合上盖子，保持足够长时间以触发预期的电源行为，然后重新打开。
4. 不解锁，也不重新批准，让代理重试受保护调用。
5. 确认网关拒绝这次重试，并且在拒绝前记录了权限转换。

如果团队会使用这些模式，请在连接扩展坞、使用电池和连接外接显示器时重复测试。测试端点必须无害。目的在于观察权限行为，而不是发现生产部署能否在执行到一半时被中断。

这里的失败通常看起来非常正常：操作日志显示睡眠期间没有调用，然后唤醒后的第一次调用成功。如果用户面对的是锁屏界面，却从未批准恢复后的运行，这仍然是失败。时间间隔正是测试的重点。

## 单独依靠显示器睡眠并不适合作为撤销触发器

仅在显示器睡眠时撤销权限是安全的，但对于显示器会在日常工作中进入睡眠的台式 Mac 来说，可能过于打扰。单独让权限跨过显示器睡眠虽然方便，但当显示器超时同时也意味着用户无人看管时，很容易出错。

选择一条规则，并明确说明取舍。对于高影响凭据，应在屏幕锁定和系统睡眠时撤销，而不是只在显示器睡眠时撤销。如果某台机器的显示器睡眠总是可靠地发生在锁屏前，你也可以选择在显示器睡眠时额外撤销，但要接受更多批准提示。关键是不要把任何一种选择称为「显而易见」。它取决于部署环境和凭据能够造成的影响。

Apple 将屏幕通知和系统通知分开提供，这正好提醒我们不要假定两者含义相同。监控两者，分别记录两者，并测试你附加到每一种事件上的策略。

一个实用的矩阵可以避免规则变成口口相传的经验：

| 转换 | 本地代理工作 | 现有会话批准 | 依赖保管库的操作 |
| --- | --- | --- | --- |
| 显示器进入睡眠 | 可以继续 | 由你声明的策略决定 | 通常暂停，或只允许低风险使用 |
| 屏幕锁定 | 可以继续 | 结束 | 拒绝，直到重新批准 |
| 系统开始睡眠 | 自然暂停 | 立即结束 | 拒绝新的调用 |
| 系统在锁屏界面唤醒 | 可以在本地恢复 | 保持结束状态 | 拒绝，直到解锁并批准 |
| 用户解锁 | 可以继续 | 仍然结束 | 要求新的会话批准 |

用户解锁 Mac 是为了重新进入桌面。这一动作不应悄悄恢复代理之前的权限。解锁电脑和批准外部操作有关联，但回答的是两个不同的问题。

## 夜间运行需要独立的约定

夜间运行的代理不应继承下午交互式会话的权限。人们在查看差异、终端或请求卡片时批准交互式工作。夜间运行则是明确决定让某件事在没有即时监督的情况下继续。

把任务约定限定到可以用一句话解释的程度。「运行测试并准备一个拉取请求」是清楚的。「完成任务所需的事情都做」不是约定，而是一张空白支票。

对于无人看管的工作，应把本地操作和外部操作分开。如果工作被限制在本地，可以允许仓库分析、分支上的编辑、运行测试和生成构件。生产 API 变更、部署、发布软件包、写入共享数据库或通过 SSH 访问重要机器等敏感外部影响，都应要求新的人类批准。

有些夜间任务确实必须调用外部服务。此时应使用专用凭据，或使用影响范围与任务相匹配的测试环境。不要因为管理员凭据已经存在于保管库中，就重复使用它。常见的理由是用户当晚早些时候已经批准了代理。但那次批准针对的是可见的交互式运行，不包括用户入睡后剩下的所有工作。

如果任务确实需要重复执行外部操作，可以设置有边界的操作预算。边界应按目标、方法和影响来定义，而不是依赖模糊的置信度分数。例如，无人看管的测试任务可以向一个固定的预发布端点发送固定请求，但不能切换主机、改变 HTTP 方法或使用 SSH。如果任务需要更多权限，就暂停等待。

Sallyport 的逐会话授权会为每个新的代理进程建立独立的批准边界。让夜间工作在新启动的进程中进行，并让它的受保护操作与其他无人看管的运行一样接受审查。

## 日志必须证明拒绝，而不只是记录活动

一条写着「Mac 已唤醒」的记录，不能证明权限已经结束。你需要边界两侧的证据：触发策略转换的操作系统事件，以及网关拒绝的下一次受保护操作。

macOS 为电源侧调查提供了一个有用的起点：

```sh
pmset -g log | grep -E 'Sleep|Wake|DarkWake|Display'
```

具体行会因硬件和 macOS 版本而异，但输出应包含带时间戳的记录，以及 `Sleep`、`Wake`、`DarkWake` 或显示器转换等事件名称。每次测试都保存相应时间窗口。不要把它作为授权的真实来源，因为它不了解你的保管库或代理身份。

你自己的日志应回答另一组问题：

```text
14:20:16.402 session_approved process=84172 epoch=27 authority=approved-agent-binary
14:22:04.118 power_will_sleep epoch=27
14:22:04.119 authority_revoked old_epoch=27 new_epoch=28 reason=system_sleep
14:25:38.771 power_did_wake epoch=28
14:25:44.025 action_denied process=84172 request=ssh.exec reason=authority_epoch_changed
```

顺序很重要。如果操作出现在撤销记录之前，就说明发现了竞态。如果没有拒绝记录，只是因为测试代理悄悄退出了，那么你无法证明持久进程的行为。应安排测试，让同一个长期运行的进程在每次转换后尝试受保护调用。

防篡改审计轨迹还有第二个好处：它让你可以事后确认事件和操作顺序没有被改写成更好看的故事。Sallyport 会从写入盲化、加密并采用哈希链的审计日志生成 Sessions 和 Activity 日志，而 `sp audit verify` 可以在离线状态下对密文验证哈希链。这对测试很有用，因为验证成功说明记录顺序没有被悄悄改写，但它不能弥补薄弱的撤销策略。

## 竞态条件发生在边界处

最危险的错误通常出现在 Mac 开始睡眠或用户锁屏时，请求已经在执行中。只在接受连接时检查批准的网关，可能让排队的工作在权限转换后继续执行。只在注入凭据后检查的网关，则可能在发现撤销前，已经把凭据泄露给辅助程序。

应在特权操作开始前立即检查权限。也就是说，在注入凭据前、打开带有可用密钥的 SSH 辅助程序前，以及发送 HTTP 请求前都要检查。如果一个操作有多个特权阶段，就应在系统可能跨过权限边界的每个阶段再次检查。

基本流程可以是这样：

```text
receive request
identify process and session nonce
read current authority epoch
compare request grant epoch to current epoch
check vault gate
check per-call approval when required
inject credential and execute action
append result to audit log
```

让权限周期的读取和提交特权执行尽可能靠近。分布式 HTTP 操作一旦字节离开 Mac，就无法做到完全可逆，但你可以阻止旧授权启动它。

不要试图通过延迟睡眠来解决所有边界情况。Apple 的 `willSleep` 通知允许短暂延迟处理，但为了完成代理操作而阻止笔记本睡眠，反而颠倒了优先级。机器正在离开交互状态。撤销访问、记录中断，让用户决定恢复什么。

对于可能造成高额成本或不可逆影响的密钥，逐次调用批准是更清楚的办法。因为每次使用都会在使用时询问用户，睡眠和唤醒就不那么复杂了。这不是跳过会话撤销的理由，而是针对较小范围凭据的第二道防线。

## 测试人们真正会执行的转换

好的测试计划不会从通知处理器的单元测试开始。单元测试有帮助，但故障往往存在于真实的电源转换、真实的锁屏界面，以及能够超出终端窗口生命周期的代理进程中。

构建一个能够等待、接收信号，然后请求无害受保护操作的测试代理。每次测试都使用新的运行标识符。运行前写下预期结果，否则意外结果事后很容易被解释成「应该没问题」。

在每种受支持的 Mac 配置上，至少测试以下情况：

- 代理空闲时锁定屏幕，然后在解锁前后分别重试已批准的操作。
- 从 Apple 菜单让系统进入睡眠，唤醒到锁屏界面，再使用原进程重试。
- 在系统保持唤醒时让显示器进入睡眠，确认行为符合你选择的显示器策略。
- 合上并重新打开笔记本盖子，分别测试电池模式和团队实际使用的桌面配置。
- 让一个明确设置为长时间运行的代理过夜，然后检查电源日志、操作日志和恢复后的第一次受保护调用。

为每次测试填写一张简短结果表：预期权限周期、观察到的电源事件、原进程是否存活、保管库是否锁定，以及第一次受保护调用是否被拒绝。这可以发现一个常见错误：团队测试了应用是否收到事件，却从未测试操作层是否在之后拒绝调用。

还要测试糟糕的时序。在受保护操作等待网络响应时锁定屏幕，然后检查重试或后续操作是否使用旧授权运行。在睡眠前极短时间内启动操作。断开并重新连接扩展坞。唤醒后重启代理进程，确认它无法借用旧进程的批准记录。

要通过这些测试，你不需要庞大的策略引擎。你需要严格的保管库闸门、逐进程批准记录、可撤销的权限周期，以及在使用凭据前立即检查这些条件的操作路径。

## 先写清结果，再强制执行策略

好的权限策略可以写在一页纸上，因为它描述的是可观察结果，而不是一堆猜测出来的操作系统标签。明确锁屏、睡眠、唤醒、合盖、注销和手动撤销后，受保护操作会发生什么。明确本地工作能否继续，以及用户必须做什么才能恢复。

对于大多数交互式 AI 编程代理，策略可以这样写：

> 受保护操作需要已解锁的保管库，以及当前进程在当前权限周期内获得的批准。锁屏、睡眠、用户会话丢失、保管库锁定和手动撤销都会结束该批准。唤醒和解锁不会恢复它。代理必须在下一次受保护操作前请求新的批准。

这条规则的代价是，人们回到 Mac 后需要再次批准。接受这个代价。另一种做法，是要求用户记住合上盖子前仍在运行的每个代理进程，然后相信它们能在数小时后机器唤醒时表现得合乎预期。

在把代理设置称为适合无人看管工作之前，先运行这些测试。如果锁屏或唤醒后请求无需用户做出新决定就成功了，你就找到了一个存活时间超过授权时刻的权限。
