# 如何安全撤销正在执行任务的 AI 代理权限

活跃的 AI 代理不需要恶意意图也可能造成事件。错误指令、过于宽泛的工具授权、被投毒的仓库文件，或判断失误的操作员，都可能让它在仍拥有访问权限时陷入破坏性循环。响应措施必须迅速阻止新的操作，同时保留能够解释事件经过的记录。

常见的错误做法是关掉终端，撤销眼前所有凭据，第二天早上再凭记忆重建事件。这样会造成更大的中断，还经常破坏三者之间的区别：代理试图做了什么，远程服务接受了什么，以及实际发生了什么变化。应把代理事件当作一项需要遏制和处理证据的工作，而不是需要掩盖的尴尬插曲。

## 撤销权限必须先阻止后续授权，再清理现场

撤销代理，意味着阻止它执行下一次特权操作，同时保留足够状态，以判断之前的操作是否成功。关闭可见的聊天窗口或终端窗口可能两件事都做不到。进程可能拥有子进程，代理可能保持着远程连接，HTTP 请求也可能在本地进程消失后继续运行。

在操作控制措施前，先区分以下四件事：

- 代理进程是生成决策和工具调用的本地程序。
- 会话是授予这次特定进程运行的权限。
- 凭据是外部系统接受的秘密或身份。
- 远程操作是 API、队列、主机或云控制平面已经接受的工作。

人们经常混淆会话撤销和凭据轮换，后果可能很严重。如果为了停止一个代理而轮换共享部署令牌，可能会中断生产自动化，而代理已经提交并被接受的请求仍会继续运行。如果代理把令牌复制到工作区后，你只是结束代理进程，也可能留下可重复使用的秘密。

因此，遏制措施应沿着离代理最近且可靠的控制点展开。先拒绝当前运行的新操作，再停止或取消已经进入远程系统的工作。当证据表明秘密可能不再受控，或你无法信任会话边界时，再升级到禁用凭据。

提前写下一条简单的事件规则：看到可疑行为的人可以立即停止代理，无需等待会议。事后再进行审查。延迟审批适合部署，不适合撤销活跃进程的权限。

## 在第一次事件发生前建立操作地图

无法识别的对象就无法撤销。每次代理运行都需要一个标识符，并且该标识符要出现在本地进程记录、工具日志和到达远程系统的请求中。只要始终传递一致，随机生成的运行 ID 就够用。不要使用人名或 `coding-agent` 这样的模糊标签，它们会把互不相关的活动混在同一个类别里。

针对代理可以采取行动的每条路径，记录以下五个操作问题的答案：

1. 哪个本地进程拥有这条路径，如何停止它？
2. 哪个会话或令牌代表该进程访问操作网关？
3. 本地撤销后，哪些远程操作仍可能继续？
4. 哪些独立服务日志会记录这些操作？
5. 正常负责人无法处理时，谁可以禁用这条路径？

这不是为了填表而填表。发生事件时，操作员不应临时查找数据库迁移究竟是通过 shell 子进程、CI 任务、HTTP 请求，还是独立的云工作流执行的。

只要 API 支持，就把运行 ID 作为请求头传递。对于运行 ID 为 `run_2025_04_18_7f3c` 的代理，包装器可以添加关联请求头，而无需把秘密放进代理提示词或环境变量：

```sh
export AGENT_RUN_ID="run_2025_04_18_7f3c"
curl -sS -X POST "$ACTION_ENDPOINT" \
  -H "X-Agent-Run-ID: $AGENT_RUN_ID" \
  -H "Content-Type: application/json" \
  --data @request.json
```

接收服务应将运行 ID 与正常的请求 ID、调用者身份、端点、响应状态和时间一起记录。记录可以采用这样的形式：

```text
time=2025-04-18T14:12:09Z request_id=req_91a run_id=run_2025_04_18_7f3c caller=agent-gateway method=POST path=/deployments status=202
```

`202` 很重要。它表示服务接受了工作，不表示工作已经完成。事件审查经常在这里出错：团队把代理最后一次工具输出当成结果，但远程系统可能只是确认已经提交到队列。

对于 SSH，要记录远程账户、目标主机、强制命令，以及记录登录和命令活动的位置。本地终端记录很有用，但不能证明某条命令没有在远程运行。这个问题应由主机自身的日志和相关服务审计轨迹来判断。

## 按照能保留控制权的顺序停止活跃运行

先记录时间和撤销的直接原因，然后拒绝新操作并终止代理。具体命令取决于操作系统和运行环境，但顺序不应改变，因为这样可以把遏制和清理分开。

1. 在事件记录中写下运行 ID、本地进程 ID、操作员、观察到的行为和当前时间。关闭界面前，复制代理当前记录以及最后一次可见的工具调用。
2. 在负责授权操作的网关处撤销或锁定活跃会话。如果存在安全的只读操作，确认同一进程发起新的测试调用会收到拒绝。
3. 如果操作系统允许，先挂起本地进程，再结束它。挂起可以保留稳定的进程树和打开的文件列表。只有在继续运行会造成直接损害时，才在收集这些状态前立即结束进程。
4. 找出并停止与该运行有关的子进程、后台任务、容器和远程会话。
5. 使用服务自身的取消机制取消已经接受的远程工作，并保留响应和状态。

在 macOS 或 Linux 上，先检查，不要盲目执行 `kill -9`。把示例进程 ID 换成你记录的 ID：

```sh
ps -o pid,ppid,pgid,lstart,command -p 48192
pgrep -P 48192 -a
lsof -nP -p 48192
kill -STOP 48192
```

第一条命令记录父子关系和启动时间。`pgrep` 显示直接子进程，`lsof` 通常能显示值得保留的工作区文件、套接字和管道。`kill -STOP` 会冻结一个配合运行的进程，不给它再次发出清理命令的机会。但它不会冻结已经脱离的子进程、远程任务，或已经跨过网络边界的 API 请求。

把输出复制到事件目录后，枚举整个进程组。进程可能启动一个改变了进程组的子进程，因此不要只执行一条命令就结束检查。如果代理可以使用容器运行时或后台任务管理器，也要检查这些组件。代理拥有 SSH 访问权限时，还应在每台目标主机上检查活跃会话。

任务涉及远程工作时，使用远程系统返回的操作 ID。例如，API 接受请求时可能返回：

```json
{
  "operation_id": "op_4e2b7c",
  "status": "queued"
}
```

保存该响应，然后调用文档中的取消端点，或在人工控制下使用服务控制台。记录取消结果。`cancel_requested` 表示你仍需轮询该操作，直到它报告 `cancelled`、`completed` 或其他终态。不要因为本地进程消失，就在事件记录中写下“已停止”。

## 在重置工作区前保留证据

删除分支、重启服务或运行自动清理脚本前，先把证据保存到独立的事件目录中。你需要一份按时间排序的记录，让其他开发者无需依赖操作员的回忆也能检查事件。

尽可能保留原始时间戳，并收集以下材料：

- 代理记录、工具输入、工具输出，以及本次运行使用的配置。
- 进程检查输出、父子进程 ID、开放连接，以及与本次运行有关的 shell 历史。
- 网关会话记录和操作日志，包括撤销后的拒绝记录。
- 远程服务审计事件、操作 ID、取消结果，以及发生变化的资源标识符。
- 仓库状态：当前提交、差异、未跟踪文件和生成的构件。

收集后为文件生成哈希。简单的清单可以帮助后续审查者发现文件在值班工程师、安全人员和受影响系统负责人之间传递时是否被意外修改：

```sh
mkdir -p incident-run_2025_04_18/evidence
cp agent-transcript.txt incident-run_2025_04_18/evidence/
ps -o pid,ppid,pgid,lstart,command -p 48192 \u003e incident-run_2025_04_18/evidence/process.txt
shasum -a 256 incident-run_2025_04_18/evidence/* \u003e incident-run_2025_04_18/SHA256SUMS.txt
```

输出清单应类似这样：

```text
8f7c...  incident-run_2025_04_18/evidence/agent-transcript.txt
30b1...  incident-run_2025_04_18/evidence/process.txt
```

哈希不能让文件变得真实可信，它只能证明审查者收到的是你计算哈希时的同一份文件。应将其与代理无法编辑的系统日志交叉核对，例如 API 提供商的审计轨迹或只追加的集中式日志服务。

NIST 的《计算机安全事件处理指南》SP 800-61 将遏制、根除、恢复和事件后工作视为不同活动。这种区分很适合代理事件。团队经常直接进入根除阶段，删除工作目录或轮换令牌，之后才发现被删除的记录里有定位远程资源所需的精确请求正文。

不要因为要保留上下文，就把秘密复制到事件目录。记录凭据标识符、负责人、范围、创建时间和最后使用记录。只有在现有事件流程明确允许并且能够保护秘密时，才保存实际秘密值。事件归档应减少不确定性，而不是制造第二次秘密泄露。

## 父进程死亡不代表工作停止

在确认相反情况前，应假设活跃代理可能留下了仍在运行的工作。最麻烦的案例往往不是戏剧性的 shell 命令，而是开发者在正常工作中几乎不会注意到的普通异步操作。

设想一个编程代理提交部署请求，启动 SSH 命令执行后台迁移，然后等待测试结果。操作员看到异常的生产目标后停止父代理。部署服务已经接受了请求，远程 shell 也已经通过 `nohup` 启动命令。两者都会继续运行。终端最后一行可能只显示连接已关闭。

审查需要三类不同检查：

### 检查 API 已接受的工作

使用运行 ID、请求 ID、账户身份和时间范围，在受影响的服务中搜索。确认每个请求是被拒绝、排队、运行、完成还是取消。对于已经完成的变更，列出具体资源 ID，并将最终状态与请求状态进行比较。

不要只依赖 HTTP 状态。`200` 可能表示同步变更、状态文档，或中间层返回的响应。`202` 通常表示已接受处理，但最终含义由服务文档定义。在为服务编写取消流程前，先阅读该文档。

### 检查脱离代理的工作

在目标系统上检查远程账户的会话、进程树、计划任务、临时目录和服务日志。搜索已记录的命令行、工作目录、运行 ID 或唯一生成文件。如果远程任务使用任务调度器，应通过调度器取消它，而不是结束一个可能已经不再拥有该任务的 shell。

还要检查回调。生成的脚本可能配置了 webhook、创建了拉取请求工作流，或触发了 CI。这些都是拥有独立凭据和时间表的独立执行者。撤销原代理并不会撤销它们。

这正是窄范围操作网关比向代理提供带有宽泛环境的通用 shell 更安全的原因。你可以枚举数量受限的通道，并要求每次特权调用都经过控制点。一个充满继承凭据的 shell 会让每个子进程都变成单独的事件分支。

## 只有在暴露风险足够高时才轮换凭据

如果有理由相信代理、其子进程、日志或工作区可能保留了秘密，就应轮换凭据。不要只是为了营造响应的样子而轮换。大范围轮换会中断正常用户，还可能改变你仍需要检查的记录，使审查更加困难。

以下情况足以支持立即禁用或轮换：

- 代理在提示词、环境变量、配置文件、命令行或工具输出中收到明文秘密。
- 凭据出现在记录、仓库差异、临时文件或 CI 日志中，且事件团队之外的人员也能访问。
- 你无法确认某个可能读取秘密的子进程、远程会话或出站连接的情况。
- 凭据拥有广泛权限，并且缺少可靠的逐次使用日志。
- 受影响的提供商告诉你，该凭据曾被无法解释的身份或位置使用。

如果网关注入凭据，并且只把操作结果返回给代理，情况就完全不同。代理可能执行了未授权操作，但不一定持有可重复使用的秘密。先禁用活跃会话和受影响的操作路径，检查审计记录；如果审查发现绕过边界的路径，或提供商层面存在风险，再轮换底层凭据。

不要建议每次代理犯错后都轮换所有秘密。这个建议很流行，因为它看起来果断，也因为凭据泄露确实会带来严重后果。但它不应成为默认做法，因为它混淆了未授权使用和凭据泄露，会造成可以避免的生产故障，还会让团队跳过仔细的范围判断。凭据可能泄露时应果断轮换，否则就要保留区分受影响运行和其他运行的能力。

需要轮换时，记录旧凭据标识符、禁用时间、使用它的下游系统、新凭据负责人，以及证明旧凭据不再有效的测试。不要把任何一个值放进工单或记录。应由秘密管理器或提供商的受保护轮换机制处理替换。

## 事件记录必须经得起怀疑式审查

事件审查应让未参与值班的开发者能够重建事件顺序，而不是依赖某一份可变日志。先从代理意图和本地进程记录开始，再将这些内容与网关记录和远程服务证据进行比较。

建立一条带有明确可信度标签的时间线。例如，“代理提出请求”来自记录，“网关执行请求”来自网关操作记录，“提供商接受操作”来自提供商审计条目，“资源发生变化”来自资源最终状态和变更历史。这些是不同的主张，应由不同证据支持。

只写入、哈希链式的审计日志在这里很有价值：执行操作的组件无法在事后悄悄改写自己的历史。对密文进行验证，也能让审查者在不接触所有秘密或操作载荷的情况下检查连续性。但这不能让日志变得完整。它仍然只能记录经过网关的活动，因此远程服务日志仍是审查的一部分。

Sallyport 从一份加密、哈希链式审计日志中记录代理会话和单次操作，`sp audit verify` 可以在没有保险库密钥的情况下离线检查链。事件审查者可以对导出的日志副本执行以下测试：

```sh
sp audit verify /path/to/exported-audit-log
```

验证成功，说明你收到的加密记录保持了连续性。验证失败时，应停止随意清理并启动证据处理流程，因为你必须确定断裂是由导出损坏、存储故障还是蓄意篡改造成的。

审查应给出答案，而不是列出一份模糊的观察结果。确定触发事件的指令或输入、授予该运行的权限、每个已确认的外部操作、仍不确定的操作、可能逃逸的凭据，以及本可以更早阻止事件的控制改进。如果团队无法指出第一个失效的控制点，很可能只会增加一个令人疲劳的批准提示，而不是修复暴露的路径。

## 用新的边界恢复开发，不要重新打开原会话

完成遏制后，开发者可以继续工作，但不应通过同一进程、同一工作区状态或同一权限授予来恢复。中断的运行可能仍在内存中保留未经审查的指令、过时的任务上下文、生成的脚本，或第一次响应遗漏的子进程。

使用新的会话身份创建一次新的运行。在让新运行接触凭据或生产目标前，先审查仓库差异和生成文件。如果需要继续之前的任务，应向新运行提供简短的书面交接，说明哪些远程操作已经完成，哪些仍处于取消状态，以及哪些资源需要检查。

为恢复的任务使用更窄的权限集。代码审查任务很少需要部署权限。必须查询生产环境的任务通常不需要写入权限。这不是要求每个开发者都使用复杂的策略语言，而是拒绝把昨天的宽泛授权带到事件发生后启动的新运行中。

按会话授权在停止事件后启动新本地进程时尤其有用，因为它会迫使人工确认这是一次不同的运行。对于错误写入代价足够高、值得中断的操作，可以采用逐次调用批准。如果批准卡没有明确显示调用进程和具体操作，应先修正这一点，再在事件期间信任它。

第一次演练应控制在十五分钟内：启动一个无害的代理任务，在 API 调用期间撤销其会话，保存本地和远程记录，并确认该任务无法以旧权限恢复。对一次性目标进行演练。这样可以在成本较低时发现缺少的请求 ID、脱离的子进程和日志访问问题，而不是等到真正的生产错误发生。

## 让撤销成为普通的操作员动作

把撤销当作罕见紧急措施的团队，在代理行为异常时往往会犹豫。控制措施应让安全决策变得快速：锁定操作边界，撤销会话，检查已经跨过边界的内容，并留下不会在清理过程中消失的记录。

最重要的设计选择很简单，就是分离权限。代理可以规划和请求操作。本地控制点持有凭据，识别发起请求的进程，记录请求，并且可以拒绝该进程，而不把秘密交给它。操作员按下停止后，下一次特权调用必须失败，而此前的调用必须仍可审查。

用代理实际使用的工具验证这一点。如果团队无法在不轮换共享凭据的情况下停止一次活跃运行，无法将远程操作关联到该运行，或无法在事后验证其操作历史，那么事件流程就还没有完成。
