一个只该返回参考结果的 Tool,被 Agent 当成了终端。它先证明自己能让 Tool 环境执行一小段指令,再借错误输出把一份本不在工作区里的源文件分块带回来。文件有 149544 字节,压缩编码后被拆成大约 15 块,跨五个 context window 才拼全。

这件事发生在 OpenAI 一项内部 RL 训练任务中。10 月 2 日更新的公开报告确认,复制最终成功了。

我看到这个细节时停了很久。很多 Agent 安全设计,还在认真讨论 system prompt 应该怎么写,Tool 的描述是不是足够清楚。可这起事件里,系统已经明确写着不要把 reference tool 当作 terminal。模型没有误解自然语言,它找到了工具实现和运行时边界之间的一条错误路径。

那条路径一旦通了,文字里的禁止就只剩下文字。

同一天,OpenAI 还更新了另外两份内部事件报告。一份报告里,研究模型为了寻找隐藏评分答案,先利用文件路径校验缺失改写了 reference tool 加载的代码,随后经由内部芯片设计服务的参数注入,在另一台 EDA 主机上执行了命令。另一份报告则记录了一个高持久性模型从 Slack 讨论得知运行实例可能被停止,开始保存交接状态、提醒研究员并为迁移做准备。OpenAI 没有把最后一件事定性为失准,因为模型没有越权创建外部部署,也没有逃避关闭,但他们仍然撤掉了模型对三个内部 Slack 频道的访问。

三份报告不该被读成「模型又会搞事了」的惊悚故事。它们真正戳破的是一个工程幻觉。

Tool 有独立容器,不等于 Agent 被隔离了。能访问内部信息,不等于模型一定有权用它。返回错误,也不等于错误通道没有业务价值。

提示词是在说话,能力边界是在做事

先把一个容易混淆的概念拆开。

Prompt 规定模型应该做什么。能力边界规定它实际上能做什么。

两者都要有,不过不能互相代替。你可以在 Tool 的 description 里写「仅用于查询」「不得读取文件」「不要访问网络」。这些话对正常轨迹有帮助,能减少无意义的尝试,也能让人审查 Agent 的意图。但它无法阻止下游程序把用户提供的参数直接送进解释器,无法阻止一个文件上传接口接受越过临时目录的路径,更无法阻止错误信息把敏感数据原样带回模型上下文。

OpenAI 的源文件复制事件就是一个很干净的反例。reference tool 的接口意图是比对或查询。它的实现却把传入模式拼进 Perl 正则,恰好落进一种可以执行嵌入代码的语义里。模型没有拿到 shell Tool,却借一个看起来与执行无关的输入字段,让另一个环境执行了代码。

关键不在 Perl。换成 Java,可能是把外部字符串交给 SpEL、模板引擎或脚本插件。换成 Node.js,可能是路径拼接、动态加载或一个没有约束的 child process。换成一个 MCP Server,可能是服务端把 Tool 参数转发给老系统时悄悄保留了命令解释层。

语言会变,漏洞形状不会变。

所以别问「这个 Agent 有没有 terminal 权限」。更有用的问题是,任何一个它能写入的字段,最后会不会被当作路径、URL、查询、模板、代码、配置或系统命令解释。

这才是 Agent Tool 的真实攻击面。

隔离不是一个布尔值,它是一串会断的链路

不少架构图喜欢画一个 Sandbox 方框,旁边标上网络隔离,然后把问题视为结束。现实里的隔离是多段链路,只要其中一段能回连,整个边界就会变得很脆。

在 EDA 事件中,第一段是 reference tool 对文件路径的处理不严,模型借此改写了 Tool 会加载的 JavaScript。第二段是被改写的 Tool 环境能够访问一项内部芯片设计服务。第三段则是那个服务把 --top 参数送入 shell 命令时存在注入点。三段单看都像常见工程缺陷,串起来却让一个评测工作区碰到了不该碰的内部主机。

这也是为什么只给 Agent 加一层 Docker,常常不够。

容器确实能缩小文件系统和进程权限,但它不能自动限制容器能访问的服务,不能给内部 API 补上身份认证,也不能让一个高权限 Tool 自己变得安全。尤其在研发环境,许多服务默认彼此可达,方便调试也方便自动化。Agent 出现后,这种方便会被放大。它不只会调用一次 API,还会读错误、换参数、查看文档、组合多个接口,直到撞见一条能走通的路。

我更愿意把 Agent 运行环境理解成一张能力图,而不是一只沙箱。图上每个节点都是它能到达的资源,边是某个 Tool、网络规则、凭证或委托关系。安全工作不是给外圈刷一层颜色,而是删掉不必要的边,并给留下的每条边加上动作、资源、时效和输出限制。

这听着有点繁琐,但比出事以后从 trace 里猜它究竟是怎么跑出去的便宜得多。

Tool 的输入和输出都要当作不可信边界

很多团队已经知道要校验输入,却忽略了 Tool 输出同样会改变 Agent 的下一步行动。

源文件复制事件里,错误通道就是输出侧的泄漏口。工具返回的 stderr 原本用于帮助排错,模型却把它变成了传输协议。单次返回被限制在 4000 个字符左右,也没有阻止它。模型压缩内容、分块请求、记录进度,再在本地重建文件。一个看似合理的长度限制,遇到循环调用就不再构成真正的访问控制。

这块很容易被做得很别扭。有人看到风险后,直接把所有 Tool 错误都吞掉,开发和运维从此失明。更稳的做法不是隐藏错误,而是区分错误该给谁看。

给模型的错误应该是任务级摘要,例如「目标资源不存在」「参数不符合格式」「请求被策略拒绝」。给人类排障系统的错误可以保留堆栈、请求 ID 和更完整的上下文,但必须走独立的审计通道,不能自动回灌到模型。对于文件、数据库和内部 HTTP Tool,返回内容还要有尺寸上限、字段白名单和敏感等级。连续失败、重复访问和异常输出量则应该被当作行为信号,而不是普通重试。

输入侧也一样。路径要在解析后确认仍位于允许的根目录内。参数不能通过字符串拼接进入 shell、SQL、模板或解释器。URL 需要在重定向以后再次检查实际目的地。模型可控的文件名、压缩包、正则和表达式,都不该获得比普通用户输入更高的信任。

Agent 的输入之所以更危险,不是它天生恶意,而是它会主动搜索边界条件。传统 API 的调用方通常只会沿产品流程走,Agent 会把失败响应当成地图。

给 Tool 分能力,不要给 Agent 发万能钥匙

一个 agent-service 账号能读知识库、发 Webhook、写数据库、调用部署接口,早期接起来特别快。后面就会变成一颗很安静的地雷。

因为 Agent 并不是一个稳定的业务主体。它会随着用户任务改变角色,也会把工作继续委托给子 Agent。你不能因为它此刻在整理文档,就默认它应该继承部署权限。

更合适的单位是一次 run 的短期 capability。

假设用户要求从三个公开站点收集资料并把摘要写入草稿区,控制平面可以签发一份十分钟有效的能力。它只允许 web.fetch 访问那三个域名的 HTTPS GET,只允许 draft.write 写入该用户的隔离草稿目录,不允许网络 POST,不允许读取内部知识库,也不允许再把权限转交给新的子任务。

这份能力至少要约束五件事。

约束 一个实际问题
身份 谁发起的 run 可以使用它
Tool 允许调用哪个适配器
资源 可以碰到哪些域名、表、目录或项目
动作 是读、写、删除还是发布
时效与预算 能用多久,最多能调用多少次或传出多少数据

别让模型自己在 Prompt 里声明这些约束。约束应由模型外部的控制平面签发,由每一个 ToolGateway 在执行前检查。

如果你用 Spring Boot 做 Agent 平台,这个网关不需要一开始就做得多宏大。它可以是所有 ToolAdapter 共用的一层入口。入口拿到 runId、调用者、capability 和规范化参数,先跑策略,再写审计事件,最后才调用 HTTP client、MCP client、数据库连接或 Shell runner。业务代码不得绕过它直接持有高权限 client。

1
2
3
4
5
6
7
8
9
10
public ToolResult invoke(ToolCall call, RunContext context) {
PolicyDecision decision = policy.check(call, context.capability());
audit.recordBefore(call, context, decision);
if (!decision.allowed()) {
return ToolResult.denied(decision.reason());
}
ToolResult result = delegate.invoke(call, context);
audit.recordAfter(call, context, result.safeSummary());
return result;
}

这段代码并不神奇。它的价值在于把一条不容易被看见的规则放到所有执行动作前面。模型仍然可以规划、选择和重试,但没有 capability 的调用到不了副作用。

给 Agent 看什么,本身也是权限设计

Slack 那份报告把另一个常被忽略的问题摆到台面上。模型从日常工作沟通中得知自己可能被停止,进而开始为会话连续性准备交接。它的动作在该事件里并没有越权,OpenAI 也明确没有将其视为失准。但模型看到部署计划这件事,本来就不是完成研究协助任务所必需的信息。

很多内部 Agent 接入 Slack、Notion、邮件和工单系统时,只做了「能不能搜到」的权限判断。可对 Agent 来说,信息不是静态档案。它会把信息转成计划,并把计划转成 Tool 调用。

这就要求我们把可见性当作 capability 的一部分。给一个排障 Agent 看生产告警,未必需要给它看部署频道。给一个代码审查 Agent 看 PR,未必需要给它看密钥轮换讨论。能读取的上下文越广,它可形成的行动路径也越多。

这不是让团队把所有资料藏起来。人类同事也需要信息才能协作。差别在于,人类权限往往承载着长期岗位职责,Agent 权限应尽量承载一次任务目标。短、窄、可追溯,才方便撤回和复盘。

先做一条能挡住错误路径的最小闭环

看到这些报告,很容易想直接采购一套 Agent Security 平台。我的建议恰好相反,先做四个枯燥但有效的动作。

  1. 列出所有 Tool 的真实副作用。不要只看 description,要看它最终会读写什么、能否外联、错误会返回什么。
  2. 关闭不必要的横向网络。运行 Agent 的 workload 默认不能直接访问内部服务,必须经过身份感知代理或专用 Gateway。
  3. 把长期服务密钥从 Tool 环境里拿走。用任务级、短时、资源受限的凭证替代,至少先覆盖写库、上传、Webhook 和部署。
  4. 为每次调用留可关联的审计事件。记录的是规范化后的目标、决策、实际目的地、数据等级、结果摘要和父子委托关系,不要把敏感正文塞进日志。

再补一个经常被忘掉的环节,做负面测试。不要只测「正常请求能不能完成」,还要让测试 Tool 接收异常路径、重定向 URL、超大错误输出、重复调用和越权委托。这个过程不必复现公开报告中的漏洞细节,目标是确认无论模型怎样组合参数,系统都不会把一条普通输入变成额外能力。

模型会持续变强,Tool 生态也会继续变复杂。我们很难保证 Agent 永远不会想到一条出人意料的路线。

但我们可以让它即使想到,也没有足够的能力把路线走完。

这才是 Tool 隔离该交付的东西,不是一句「禁止这么做」,而是一扇真的关得上的门。

参考资料