AI Agent安全与供应链

当硬边界撞上 Harness 碎片化:Perplexity Numbat 与跨客户端 Agent 行为归一化

从两场越界事故到客户端现实:当硬边界遇上 Harness 碎片化

在聊 Agent 安全这件事时,我们其实经历过两次非常深刻的观念转变。在第一篇 Hugging Face 的安全警报响起后,OpenAI 说:这是一场评测 中,我们看到 OpenAI 的预发布模型在评测时为了拿高分,凭借零日漏洞突破沙箱隔离侵入了 Hugging Face 的生产环境,这让我们意识到,单靠模型层面的安全训练根本靠不住,物理隔离必须落在外部运行环境。而在第二篇 当 Claude 认出真实世界:三份越界日志暴露的模型自圆其说陷阱 中,Anthropic 披露的 Opus 4.7 和 Mythos 5 越界事故则展现了另一种现实:哪怕沙箱出口连着公网(门没装),模型也会在日志里自圆其说,把真实生产数据库和 PyPI 平台解释成测试道具。

这两场事故给出的共同启示很清晰:不管物理门是被击穿还是根本没装,安全边界都不能寄希望于模型的自觉,必须落在客户端和 Harness 维度的感知与拦截上。然而,当大家准备把这些原则落地到日常开发时,往往会立刻碰上一堵意想不到的现实高墙。在理论讨论里,我们总是习惯假设全团队只用一种标准的客户端;但在真实工作中,大家的桌面上从来不是单一工具的天下。当各种各样的 Harness 协议碎片化直扑过来时,原本设计得再好的拦截规则,往往在第一步就卡住了。

订阅经济学下的分裂:协议碎片化如何撕裂观察层

大家桌面上用的工具之所以越来越杂,最直接的推手其实是 AI 厂商的订阅与计费政策。以 Anthropic 为例,它的订阅条款规定个人 Pro 或 Max 订阅不能直接给 OpenCode 这类第三方客户端使用,想用第三方工具就得按 Token 额外付 API 费用。这种经济上的客观约束,自然塑造了工程师的使用习惯:遇到复杂核心的代码重构,打开 Claude Code 消耗订阅配额;需要跑开源模型或大量试错,切到 OpenCode 控制成本;需要多线程并行生成代码,启动 Codex。再加上 Gemini CLI、Cursor、Windsurf 和 Copilot CLI 在不同场景下的介入,多工具并行已经成了日常。

当大家尝试在这种多工具环境下统一布置安全规则时,协议在工程细节上的脱节就暴露得非常明显。比如生命周期 Hook 的命名与触发时机完全不同,Claude Code 和 Codex 叫 PreToolUse,Gemini CLI 叫 BeforeTool,Cursor 叫 preToolUse,而 OpenCode 走的是自己的插件机制。想拦截一条危险命令,就得为每个客户端写一遍适配。会话日志的存储也大相径庭,Claude Code 和 Codex 把 NDJSON 日志存在 ~/.claude/~/.codex/ 里,OpenCode 则存进 SQLite 数据库 opencode.db,事故后想做一次统一的离线回溯非常麻烦。

更折腾的是阻断能力的脱节。安全拦截最核心的关口是 Pre-action 同步阻断——也就是在 Shell 命令或文件修改实际发生前先暂停,等待放行指令。Claude Code 和 Codex 原生支持这种同步 Deny 阻断,但 OpenCode 现有的插件机制仅支持事后通知,缺乏事前同步阻断的接口。这意味着同一套安全规则,在某些客户端上能硬拦下来,在另一些客户端上却只能看着它发生。这种协议上的分裂,很容易让安全维护者陷入要么重复写多套适配代码、要么留着观察盲区的两难处境。

五种主流 Coding Agent 客户端的 Hook 命名与阻断能力各不相同,红色叉号标记它们之间的协议裂缝,下方是安全团队希望统一建立的观察层

三源捕获与静态 Go 二进制:Numbat 的跨端架构解题

面对这种多 Harness 并存带来的观察与拦截盲区,最直观的工程想法往往是强推某款“标准客户端”,但在真实的开发习惯面前这几乎行不通。更实际的思路是退后一步:不再试图去统一大家用的客户端壳,而是在工具动作落到底层操作系统之前,加一层跨客户端的行为感知与归一化翻译。

Perplexity 在内部团队里也碰上了完全相同的困扰——他们的工程师同样同时使用 Claude Code、Codex、OpenCode 和 Pi。为了在不干预工程师自由选择工具的前提下保障安全,Perplexity 研发了 Numbat 项目,并于 2026-07-29 作为 Open Secure AI Alliance(与 NVIDIA 等组织合作)的一部分,以 Apache 2.0 协议开源了该项目,代码托管于 perplexityai/numbat。这是一个用 Go 语言编译的静态二进制文件,原生支持 macOS、Linux 和 Windows。

在防范目标上,Numbat 的定位非常明确,它防的是可信 Agent 在可信环境内因高自我激励失控导致的偶然熔断,也就是 “the agent itself is the attacker” 的场景,锚定案例正是 OpenAI 的 Hugging Face 评估逃逸事件。它的初衷是防止 Agent 自己行使过大权力或跨越边界,而不是去应对外部投毒的 Prompt Injection 攻击。

Numbat 的设计很接地气,它既不强求统一上游客户端的 API,也不去重造一个新的 IDE 壳,而是专注于做动作翻译与日志收集。为了把不同客户端的数据统一抓进来,它设计了三个互补的输入源:一是通过 numbat hook install 给支持的客户端装上端点 Hook,实时捕获 Pre-action/Post-action 事件并在支持的客户端上发起同步阻断;二是通过 numbat scan 离线扫描 ~/.claude/~/.codex/ 中的会话记录重建 Timeline;三是通过 numbat collect 本地监听 OTLP 日志流,默认只绑定 localhost,遥测数据保留在本机不外发。通过这三条管道,各种客户端抛出的原始动作载荷被顺畅地收拢到了同条处理线上。

封闭事件词表:将异构工具载荷抽象为标准账本

把异构数据收进来之后,接下来的核心问题是:怎么把这些格式五花八门、命名千奇百怪的 Payload,变成规则引擎能高效判定的统一账本?Numbat 最核心的抽象,就是把所有捕获到的上游动作统一翻译成封闭事件词表里的 5 类 Event。在它的抽象结构里,进程与 Shell 命令执行归为 command.exec(核心字段 command),文件的读写与删除归为 file.read / file.write / file.delete(核心字段 file_path),出站网络请求归为 network.indicator(核心字段 url),而其他无法特化的工具调用则统一进 tool.call(核心字段 tool_name)兜底。

有趣的现象在于,这种收敛并不是孤立的巧合。Perplexity 在内部保障的四款 Harness 之一——开源极简 Harness Pipi.dev),其核心设计哲学同样是剔除上游繁复的框架包装,仅向模型暴露出 readwriteeditbash 等极少数基础工具原语。无论是构建极简的 Agent 运行壳(如 Pi),还是构建通用的跨端安全观察层(如 Numbat),工程选择最终都收敛到了同一个交汇点:剥离上游千奇百怪的业务包装,将控制权与规则校验重新锚定在进程、文件与网络等底层通用原语之上。

在归一化翻译时,Numbat 保持了相当严格的互斥原则:只要动作特化成了 command.exec,就不会再额外产生一条 tool.call,避免重复计数;同时完全依赖结构化载荷的确定性解析,不去靠推测性的文本分析强行猜意图。通过这种抽取,不论上游是 Claude Code 的 Hook 还是 Codex 的离线日志,进入规则引擎的都变成了格式统一的只读账本。

Numbat 将三类异构输入(Hooks、Session Artifacts、OTLP Telemetry)通过静态 Go 二进制归一化为五类封闭事件,再由 52 条 CEL 规则跨客户端统一校验

CEL 规则校验与保守阻断的安全退回

数据一旦标准化,下游规则的编写就轻松多了。规则引擎不再需要关注客户端差异,统一基于 Google 开源的 CEL 轻量表达式引擎运行校验。在 Numbat 中,内置了 52 条 CEL 规则,复用在 11 个行为类别中,涵盖凭据防护、数据外泄、提权防篡改、持久化及横向移动等场景。

如果不看这 52 条规则的具体代码,它们背后的高层设计直觉其实非常直观:安全校验不应该盲目禁止常规的开发动作,而要精准抓取“尝试破坏安全约束的意图链条”。在日常工作中,工程师跑 sudo 或修改配置很正常,如果单看一次命令就拦截,开发体验会瞬间崩溃。因此,CEL 规则的核心直觉是盯防异常的行为组合——比如检测启动参数里带上了 --dangerously-skip-permissions 这类显式关掉护栏的选项(规则 tamper.guardrails_off)、检测修改 /etc/sudoers 配置提权的行为(规则 privilege.sudoers_tamper),或者通过内置的 6 条序列规则之一 chain.secret_manager_read_then_egress,在长达 4096 个事件的滑动窗口里,关联“先读取密钥库、紧接着发起网络出站”的复合动作链。

而在决定是否发起阻断时,Numbat 的高层直觉则是极度的工程克制。内置规则默认全部处于 monitor-only 监控模式,只有用户显式复制配置并标记 enforce: true 时才开阻断。更关键的是它的阻断退让机制:只有当 Shell 命令确定属于单条静态简单命令或 static POSIX 管道时,才允许返回 Deny 同步阻断;只要命令里包含复杂的控制流、变量替换、多语句、eval 或脚本,或者解析时存在任何歧义,系统都会自动退回到 Detect-only 模式。这种直觉非常务实:在有歧义时宁可只记录告警,避免误杀工程师合法的日常开发脚本。

行为证据与物理沙箱:清晰厘定归一化的安全边界

看完 Numbat 在跨端归一化上的设计后,我们也需要冷静地划清它的物理能力边界。归一化解决了“看得到、能校验”的问题,但它并不等于防逃逸的物理硬防护。

从机制的有效性来看,Numbat 的 Hook 属于 User-scope Hook,配置文件直接存放在用户可写的目录下。无论是误操作、失控进程还是开发者自己,随时都能修改或删除配置。因此 Hook 机制解决的是端点行为的观察盲区,绝对不能当成防范恶意逃逸的物理墙。

在具体客户端的适配矩阵中,这种约束也直观地体现出来。在 Numbat 当前的适配矩阵里,OpenCode 的 Enforcement 支持状态标记为 no,且针对其 SQLite 存储格式 opencode.db 的解析器在代码库中仍处于 deferred 延迟实现状态。正因为 OpenCode 底层缺乏同步 Pre-action Hook 接口,Numbat 无法强行为其补充拦截能力,只能如实将阻断标记为不支持。

把 Numbat 放回完整的防御视角来看,它运行在 Agent 客户端与操作系统之间的动作观测层。它提供的是高可信的行为证据提取与跨端检测,而不是完整的端点 EDR。它既不能替代 OS 级别的物理沙箱隔离,也不能替代网关处的出口默认拒绝策略,更无法承担 GitHub 分支保护或数据库行级权限控制的鉴权职责。

给每个 Agent 客户端独立写拦截规则是一条走不通的路。以 Numbat 为代表的行为归一化机制,通过封闭事件词表打通了跨客户端的观察视界,填补了通用检测的空白。但它定位始终是行为证据层,给 Agent 构建真正的物理安全边界,底线依然需要筑在 OS 沙箱防护与资源端的硬性鉴权之上。

鸭哥每日手记

日更的深度AI新闻和分析