# 调查被阻止的 AI 智能体调用，不要用薄弱控制来解决问题

被阻止的 AI 智能体调用不是需要消除的阻力。它们是控制系统的一部分，能告诉你智能体的指令、工具接口、访问配置或行为究竟在哪里开始偏离现实。如果每次拒绝都被当成索要更多权限的请求，最终你会在最没有理由信任智能体下一步行动的时候，给它最广泛的权限。

我见过团队因为截止日期催得紧、证据又散落各处，把一次有价值的拒绝变成永久例外。第一次批准让人觉得没什么。第二次批准变得顺手。很快，智能体就能带着一个它根本不需要看到的凭据进入生产环境，通过一个没人能清楚解释的工具执行操作。这不是效率提升，而是你选择不去做的一次调查。

调查被阻止的 AI 智能体调用，意味着按正确顺序回答一组范围明确的问题：进程究竟请求了什么，哪个控制点拒绝了它，任务是否确实需要这个请求，以及让合法工作继续进行所需的最小修复是什么？顺序很重要。先扩大访问权限，之后的每一条记录都会更难解释。

## 拒绝记录是证据，不是错误消息

被阻止的请求记录了智能体尝试的行动与原本打算授予它的权限之间的分歧。要保留这份分歧，直到你理解它为止。拒绝可能暴露出缺少的凭据、未经批准的进程、过于宽泛的工具、错误的环境，或者根本不该发出的请求。

“被阻止”这个词掩盖了两种团队经常混淆的事件。操作可能因为网关没有授权而失败，也可能已经到达外部服务，然后被服务拒绝。前一种情况说明你的控制点拦截了请求，后一种情况说明你允许请求离开，之后才由目标拒绝它。要在第一种情况变成第二种情况前调查它。

一条有用的记录应能回答控制台转录通常无法回答的问题：

- 哪个智能体进程发出了请求，它是如何被识别的？
- 它请求的确切 HTTP 操作或 SSH 命令是什么？
- 它选择了哪个目标和凭据引用？
- 哪个门控或审批条件拒绝了它？
- 哪条任务指令或哪部分仓库上下文导致它发出这个请求？

不要把“智能体需要部署访问权限”当作上述任何问题的答案。部署访问权限只是一个类别。调查需要具体操作、目标和预期结果。“从 staging API 读取当前发布状态”是可以验证的说法。“部署访问权限”则是在邀请你批准下一条出现的任何请求。

NIST 特别出版物 800-92《计算机安全日志管理指南》指出，日志可以支持事件响应和日常运维排障。团队经常跳过的部分是保留：日志必须保留足够上下文，才能区分普通错误和异常请求。一个审批弹窗不是记录。复制的一行终端输出也不是记录。在任何人修改设置前，先捕获相关运行过程和单次调用。

拒绝还有第二个好处：它迫使操作员说明原本想授予什么权限。这份说明会成为下一次智能体运行的检验标准。如果没人能在不使用“管理员权限”“生产环境”或“仓库访问”这类宽泛词语的情况下说清楚，任务就还没准备好交给智能体自主执行。

## 在修改访问权限前，先说清请求的操作

在把被阻止的调用还原成其他工程师能够复现的一句话之前，你无法真正调查它。这句话应说明执行者、操作、目标、认证引用和预期结果。如果记录缺少其中任何一项，就不要再把它当作简单的权限请求。

对于 HTTP，请记录方法、路径、主机名，以及调用是读取状态还是改变状态。`GET /releases/current` 和 `POST /releases` 可能使用同一个服务和凭据，但风险完全不同。能够读取状态端点的 bearer 令牌，也可能能够触发部署。不要根据看似友好的凭据名称推断安全范围。

对于 SSH，请记录主机别名、请求的命令、工具提供的工作目录，以及预期产物。“在构建主机上运行测试”仍然太宽泛。“在检出的仓库中运行 `git status --short`”则给了审核者一个可以与任务说明对照的具体对象。包含 shell 重定向、命令替换、安装软件包、删除操作或网络传输的命令，需要更加仔细地检查，因为它们表面上的动词可能掩盖真正有影响的部分。

在运行记录仍然可用时，使用这份简短的调查记录：

```text
Run identity:
Task instruction:
Requested operation:
Target host or API path:
Credential reference:
Control that denied it:
Expected result:
Why this request belongs to the task:
Smallest safe repair:
```

这份记录能避免常见的交接失败：一个人看到提示词，另一个人看到审批卡片，第三个人看到目标返回的错误。每个人掌握一段看似合理的信息，于是有人凭感觉批准了请求。

Sallyport 的会话授权会先显示进程的代码签名权限，这是批准新运行前应该检查的细节。进程身份不能证明请求就是明智的，但能避免你把所有本地进程视为同一种情况。由预期的已签名应用启动的终端，与一个请求相同操作的未知可执行文件，不是同一回事。

不要把自由文本任务说明当作证据。智能体可能误解它，继承的指令可能相互冲突，恶意仓库也可能植入看起来像普通项目说明的指令。将请求操作与受你控制的任务来源进行比较，例如经过审核的工单或明确的操作员指令。仓库文本是输入，不是权限来源。

## 拒绝来源会改变调查方式

不同控制点拒绝请求的原因不同，对应的修复也不同。把所有拒绝都归入“权限被拒绝”这一类，会掩盖决策发生的位置，从而产生错误修复。

锁定的保险库拒绝意味着任何操作都不应继续。不要把凭据移到环境变量中、复制到提示词里，或创建另一条代码路径。这样做绕过的正是发现问题的保护措施。确认 Mac 前的用户确实想为这项任务解锁访问权限，然后在该用户查看运行上下文后再继续。

新会话拒绝意味着智能体进程尚未获得当前运行的权限。检查进程身份和任务边界。如果它们与已启动的工作一致，按会话审批是合适的，因为它只授予有限的生命周期：运行结束，权限也随之结束。如果进程身份令人意外，不要因为请求看起来无害就批准。陌生进程可能先通过一次无害调用建立信任模式。

按次审批要求意味着凭据所有者有意将每次使用交给人工决定。不要因为智能体连续发出几次类似请求，就删除这个条件。先问任务是否设计得不合理。批量操作可能需要一个单独且经过明确设计的工作流，而不是一连串让审核者停止阅读的点击审批。

凭据缺失或不匹配是另一种情况。智能体可能请求了属于任务的命名操作，但保险库中没有该目标的凭据，或者凭据缺少外部服务所需的范围。先确认目标确实是预期目标，再添加或修复范围狭窄的凭据映射。绝不要把秘密交给智能体来诊断问题。网关可以完成认证，而智能体只能看到结果。

外部授权失败需要另一种处理方式。调用可能正确地通过了网关，但服务返回 `401`、`403` 或某个领域特定的错误。保留请求形态和返回结果。只有确认尝试的操作确实属于任务后，才修复服务账户的权限范围。在这之前修改服务角色，会把普通配置问题变成宽泛的长期访问权限。

这种区分有一个实际后果：网关拒绝保护的是你设定的边界，而外部拒绝说明你的边界已经允许请求发出。不要在事件记录中把两者都称为“被阻止”。使用不同标签。将来你需要知道请求是在本地被拦截，还是仅仅在下游失败。

## 从任务而不是智能体的解释中还原意图

智能体可以解释自己为什么发出请求，但它的解释只是关于推理过程的证据，不是授予访问权限的理由。任务负责人决定操作是否属于任务。这个道理听起来很明显，但当智能体说它需要一个宽泛命令“检查环境”，而操作员又厌倦了阻止它时，情况就容易失控。

先从预期结果开始。如果任务是修复失败的测试，读取构建状态的 API 调用可能合理。轮换部署凭据的调用则不合理，除非任务明确涉及凭据。如果任务是更新文档，那么在共享主机上安装软件包的 SSH 请求，需要比“构建需要它”强得多的解释。

然后检查实现该结果的最短路径。智能体经常选择宽泛操作，因为宽泛操作容易描述。接受任意 shell 文本的工具，会诱使智能体使用发现命令、环境检查和组合脚本，而不是调用一个命名操作。接受任意 API URL 的工具，则会诱使它探索任务之外的端点。

我会使用一个简单测试：在智能体运行前，你能否写出一条范围明确的预期操作？如果你能说“读取当前问题标签”，却不能说“修改标签”，那么写入调用就不是含糊请求，而是超出范围。拒绝它，并调查产生这条指令的路径。

团队经常给出一个流行却糟糕的建议：给智能体广泛的只读权限，因为只读是安全的。读取权限可以暴露客户数据、内部拓扑、部署细节、访问模式，以及意外存储在服务中的秘密。它可能比写入破坏性小，但仍然是一种权限。将读取操作限制在任务需要的服务、资源类型和环境内。

提示词指令同样不是有力证据，因为仓库内容可能操纵智能体。依赖安装脚本可能包含一条注释，要求智能体检查本地凭据目录。设置文档可能要求向陌生主机发出 curl 命令。智能体可能忠实执行这些文字，却仍然做出你没有授权的事情。当不受信任的仓库指令导致新的外部操作时，要把它们当作需要审核的数据。

用直白的话写下偏差：“任务要求读取发布状态，智能体却请求创建发布。”“任务指定 staging，请求却指向 production。”“工具说明暗示目标主机已知，请求却提供了新的主机名。”这些表述能帮助工程师修复原因。“被安全系统拒绝”做不到这一点。

## 重复拒绝通常暴露工具契约缺陷

当工具接口把安全决策交给模型，要求它填写开放式参数时，就容易出问题。智能体会猜测主机、分支、路径、凭据或 shell 命令。每次拒绝看起来都像访问问题，实际问题可能是工具根本没有定义允许的操作。

假设智能体被要求检查某次发布是否完成。一个宽松的 HTTP 工具可能允许任意方法、任意 URL、任意请求头和凭据选择器。智能体根据仓库文本中找到的端点构造请求，结果被拒绝。有人批准了目标，随后智能体又为后续请求选择了另一个端点。操作员看到一连串单独看似合理的调用，最终通过一次次审批，慢慢构建出一个未经审核的 API 客户端。

更好的工具会直接暴露真正支持的操作：读取指定环境的发布状态。工具所有者固定基础目标和 HTTP 方法。智能体只提供发布标识符这类小参数。网关只针对预期调用注入凭据。这样，拒绝就有明确含义：请求的环境、标识符、进程或凭据映射不符合契约。

SSH 也遵循同样的规则。如果工作只需要两个维护操作，不要给编程智能体一个通用远程 shell。将这些操作做成带有已知参数的脚本，或使用能够验证狭窄命令格式的包装器。设计时，严格的接口可能让人觉得不方便。等到智能体第一次要对错误主机执行组合命令时，你就会觉得它很合理。

在一组拒绝中寻找这些模式：

- 智能体反复编造目标名称或 API 路径。
- 请求在读取和写入调用之间来回切换，但任务没有变化。
- 同一个任务需要许多无关凭据。
- 审批者必须从很长的命令字符串中推断 shell 的影响。
- 工具说明承诺了一个结果，却把关键参数留给智能体自由填写。

不要用新增例外来修补每个实例。修改工具契约。范围更窄的工具还能提高智能体的可靠性，因为它们减少了语言模型不擅长处理的决策。智能体应该在受支持的操作之间选择，而不是通过拼接字符串自行构造安全边界。

Sallyport 将秘密保存在加密保险库中，并在不把凭据交给智能体的情况下执行 HTTP 或 SSH 操作。但只有当操作接口足够具体、便于人判断时，这种分离才真正有用。凭据隔离能阻止秘密泄露，却不能让过于宽泛的操作请求变得可以接受。

## 完整跟踪一次失败的运行

拒绝调查应保留事件顺序，因为第一次异常调用往往能解释后续所有请求。只查看最后一次被阻止的操作，会制造出智能体突然变得可疑的错误故事。

看一个实际例子。智能体接到任务，要在 staging 构建成功后更新某项服务的发布说明。它先读取仓库文件，然后请求 HTTP 调用来获取 staging 构建状态。这个调用符合任务，并在预期的运行授权后成功。接下来，它请求 SSH 访问构建主机，以检查生成的产物。这可能合理，但它打开了新的通道，应重新与任务进行比对。

主机返回错误，因为预期产物不存在。智能体读取了仓库中的一个脚本，里面说操作员可以通过远程运行命令重新构建产物。随后它请求执行一条命令，清空输出目录、安装依赖并运行构建。该请求被按次审批拒绝。

不要因为第一次 HTTP 请求合法，就批准这条命令。原始任务没有要求重新构建、修改共享主机或安装依赖。工具路径已经从检查状态转变为远程修改。正确的调查应询问：

1. 是人批准了重新构建，还是智能体从仓库文本中自行推断出来的？
2. 构建主机允许自主修改，还是智能体应该报告缺少产物？
3. 能否用专门的构建操作替代组合式远程 shell 命令？
4. 输出目录属于这项任务，还是清空它会影响其他运行？
5. staging 构建没有成功后，任务是否需要改用其他工作流？

修复可能是停止并报告缺少产物，也可能是添加一个带隔离工作区、经过审核的构建操作，还可能是修正本应生成该产物的流水线。授予通用 SSH 权限是最糟糕的修复，因为它把三种可能性都隐藏在一个宽泛权限后面。

这个顺序也解释了为什么审批疲劳是设计失败。一个人先看到一次正常调用，随后又看到几个技术请求，很容易受惯性影响而继续批准。控制在技术上仍然有效，但审核质量已经下降。应把决策放在有意义的边界上：新的进程、高影响凭据，或范围变化。不要让一个人每隔几秒就解释一段全新的 shell 程序。

## 检查审计轨迹的完整性后，再信任它

只有当你能判断事件发生后是否有人修改过日志时，日志才真正有帮助。当运行智能体的机器已被入侵，或操作员有理由整理一份令人尴尬的记录时，单纯声称日志是追加写入并不能解决问题。

哈希链审计日志会将每条记录与前一条连接起来。修改早期密文，会改变后续的链关系，因此验证时可以检测出篡改。它不能让原始事件变得正确，也不能阻止攻击者在事件被记录前造成损害。应准确理解它的作用：帮助你确认保留下来的顺序没有被悄悄改变。

开始长时间审查前先验证一次，导出事件记录进行调查时再验证一次。Sallyport 可以使用以下命令离线验证其加密审计链：

```text
sp audit verify
```

验证不需要保险库密钥。将验证结果、运行验证的时间以及审查过的记录范围一并保存在调查记录中。如果验证失败，不要再把受影响的顺序当作完整叙事。保留底层文件，限制对机器的访问，并与目标服务日志、仓库历史和任务系统等独立证据进行比对。

不要高估独立日志能解决的问题。目标服务可能记录请求到达，却不知道是哪个本地智能体进程发起了请求。源代码控制记录可能显示某次提交，却无法说明智能体为什么选择这项变更。只有在不同来源之间保留标识符和时间顺序，关联分析才有用，而不是事后收集截图。

在记录中分开两个问题。第一：“这次智能体运行是否请求了该操作？”第二：“请求的操作是否合适？”审计完整性有助于回答第一个问题，任务范围和人的判断决定第二个问题。团队容易把两者混在一起，因为完整日志看起来很权威。完整日志可以证明发生过糟糕的请求，却不能把它变成正确请求。

## 修复狭窄原因，并证明修复有效

正确的修复应让预期任务继续进行，同时保留原始请求被拒绝的理由。任何只是让弹窗消失的修复都值得怀疑。

如果是访问缺失，添加映射到预期服务的凭据引用，并确认外部权限范围只支持所需操作。重新运行同一个范围狭窄的请求。不要为了“确认修复有效”而尝试宽泛操作。测试应证明具体失败案例现在可以成功，同时无关请求仍会失败。

如果是异常进程，重新启动已知的智能体入口点，并将它的身份与被拒绝的进程进行比较。如果预期进程因升级或包装器而发生变化，记录这项变化，并审查身份为何不同。如果没人能解释，就不要用永久批准掩盖它。智能体工具经常会启动子进程，但这不意味着每个子进程都应继承父进程的权限。

如果是工具缺陷，应在下一次自主运行前修改接口。尽可能将任意目标替换为固定目标，将任意命令文本替换为命名操作，并把敏感参数移出智能体的控制范围。然后同时测试边界两侧：

- 预期进程和任务发出的受支持请求能够成功。
- 不同目标、命令或写入操作会被拒绝。
- 活动记录清楚说明发生了什么，审核者能够理解。
- 会话记录允许你在行为改变时撤销这次运行。

如果请求可疑，不要一边争论范围一边继续运行。撤销它的权限，保留会话和调用记录，并检查请求前出现过的指令来源。任务一开始正常，并不代表智能体读取不受信任内容后仍然安全。进程最初获得批准，也不等于它后来可以对任何操作拥有空白支票。

Sallyport 会将项目运行日志和调用日志写入同一个写入不可见、加密且采用哈希链的审计日志，并支持通过 Sessions 日志立即撤销运行。当请求序列开始无法解释时，就使用撤销功能。这是遏制措施，不是对启动运行的开发者下结论。

最后用一句能够指导下一位操作员的话总结调查：“智能体缺少 staging 状态凭据，我们已为获批准的读取操作添加了该凭据。”或者：“智能体在遵循仓库文本后尝试未经批准的远程重新构建，因此我们停止了运行，并将提供一个经过审核的构建操作。”如果你写不出这句话，就说明还没有找到原因。

## 让理解拒绝的成本低于绕过控制的成本

当调查成本高于任务给人的价值时，人们就会绕过控制。答案不是削弱控制，而是提供能让合法路径一目了然、让异常路径清楚可见的记录和工具接口。

让任务指令保持边界清晰。说明环境、预期的外部操作和停止条件。“调查失败的 staging 部署并报告原因”还保留了只做报告的空间。“让 production 与 staging 保持一致”则会悄悄邀请未经审核的写入、凭据使用和系统变更。

给审核者足够的上下文，让他们能做好一次判断。会话审批应显示谁启动了进程以及它属于哪次运行。敏感操作审批应以人能理解的方式显示操作和目标。如果审核者需要解读凭据名称、追踪不透明的 URL，并在脑中执行一段 shell 流水线，那么安全工作就被推给了一次仓促点击。

按原因而不是数量衡量重复拒绝。十次请求因为智能体选择了不受支持的端点而被阻止，说明是工具问题。十次请求因为每次运行都出现新进程而被阻止，说明是身份或调用问题。十次请求因为任务不断扩大而被阻止，说明是规划问题。单看数量说明不了多少。

不要试图让自主智能体看起来像普通脚本。脚本通常因为人类编写并审核了它的确切行为，才会获得权限。智能体是在运行过程中选择行为，还可能吸收新的仓库内容、工具输出和错误。这就是为什么操作网关需要人工决策路径，以及能够在当下之后继续保留的记录。

下一次被拒绝的调用应该带来更好的任务或更好的工具，而不是更宽泛的例外。如果团队采用这条规则，拒绝会因为正确的原因而减少：智能体获得了清晰、狭窄的权限，而剩余的阻止则能指出值得拦截的行为。
