Administrator
Published on 2026-08-06 / 9 Visits
0
0

Agent 安全钩子需要能力矩阵:先统一证据,再谈策略阻断

安装了安全钩子,不等于 AI Agent 已经处于控制之下。配置文件可能存在却没有被宿主加载,事件可能在动作执行后才到达,规则命中可能只用于监控,一条写着 deny 的记录也不能证明宿主最终执行了拒绝。

这些不是少数边缘情况,而是几种完全不同的能力。把它们压缩成一个“已支持”复选框,会让安全团队把可见性误当成控制力。

Agent 安全首先需要一张能力矩阵,然后才是更多规则。矩阵要说明何时采集、载荷有多完整、回调能否同步决策、失败时发生什么、证据能否持久保存。只有这些边界都清楚,团队才知道哪些策略适合阻断,哪些只能告警。

Numbat 是一个很适合观察这些边界的开源案例。它把 Agent hook、plugin、OTLP 日志和磁盘工件统一成一套事件模型,再用 CEL 规则检测。它的文档同时明确说明,内置规则默认全部只监控,阻断只适用于宿主支持的同步动作前回调。

这个架构原则并不限于某个工具:先统一证据,再声明宿主能力,最后只在完整控制链已经可测试的位置执行阻断。

为什么每个 Agent 各写一套规则无法扩展

不同编码 Agent 使用不同生命周期名称、载荷结构、信任机制和响应协议。同一条 shell 动作,可能以 PreToolUseBeforeToolpreToolUsetool.call 或插件回调出现。有的宿主会提供结构化参数与调用 ID,有的只能给出通用工具名。有的云端路径还会绕过本地桌面端使用的 hook。

如果每个回调都单独编写策略,策略层就与厂商传输层紧密耦合。宿主一升级,可见性和阻断语义都可能变化。规则开始漂移,例外越来越多,最终连两个告警是否描述同类动作都难以确认。

Numbat 的事件模型采用基础设施优先的做法,把支持的证据映射到一组封闭类型:

  • command.execcommand.result
  • file.readfile.writefile.delete
  • network.indicator
  • tool.calltool.result
  • 权限、提示、消息、配置和会话事件

分类刻意保持保守。无法安全识别的工具继续保留为 tool.call,系统也不会从自然语言描述里臆测结构化动作。归一化事件只陈述来源证据真正支持的内容,而不是检测器希望发生的内容。

统一事件模型让一条规则可以复用,但不会让所有来源突然具备相同能力。来源、置信度、采集时点和缺失字段仍然必须保留。能力矩阵的作用,就是让这些差异不会在标准化过程中被抹掉。

这也补充了我们对 Agent 生产可观测性缺口的讨论:真正可用的遥测,是 runtime 实际发出的证据,而不是平台希望能够读取的推理过程。

能力矩阵必须有七列

维度 必须回答的问题
采集时点 动作前、动作后、遥测阶段,还是只能从磁盘工件恢复?
载荷精度 工具名、参数、路径、URL、调用 ID、结果和退出码是否真实存在?
归一化类型 能否安全分类,还是必须保留为通用工具调用?
决策能力 这个回调能否同步请求拒绝,还是只能监控?
激活与信任 配置是否存在,宿主是否加载、信任并实际触发过?
失败语义 超时、解析错误、写入失败或 hook 崩溃时,是放行还是阻断?
持久证据 是否有本地记录、HTTP 直送,或可重新打开的磁盘工件?

矩阵行应细化到宿主和执行路径,不能只写产品名。比如“支持 Codex”过于宽泛,因为大多数本地 function tool 会进入 hook,hosted tool 却可能绕过本地路径。“支持 Cursor”也不够,因为本地 Agent、Cloud Agent 与 Tab 暴露的回调并不相同。

一张紧凑的宿主矩阵

以下示例来自 Numbat 当前的覆盖文档,目的不是给产品排名,而是展示可见性和控制力为何必须分列。

宿主或表面 证据来源 动作前阻断 关键限制
Claude Code 本地工件、生命周期 hook、OTLP 支持 PreToolUse 项目 hook 依赖信任,托管策略还要核对实际生效层
Codex 会话与历史、本地 hook、OTLP 支持 PreToolUse hosted tool 绕过本地 hook,非托管 hook 需要审核
Gemini CLI journal、hook、OTLP 支持 BeforeTool rewind 不会删除此前动作,项目级配置仍受信任控制
Cursor 本地 Agent transcript 与通用 pre/post tool hook 支持 preToolUse Cloud Agent 不加载用户 hook,且可能在 hook 启动前做只读探索
GitHub Copilot CLI event journal 与共享 hook 文件 支持 preToolUse 某些启动失败由宿主拒绝,超时却会放行
VS Code Copilot Agent Mode 共享 Copilot hook 的实时事件 支持 PreToolUse hook 仍是 Preview,该集成没有磁盘解析器
OpenCode plugin、可选 OTLP、早期 JSON 工件 不支持 实时采集仅监控,当前数据库取证仍未支持
Claude Cowork macOS 审计工件 不支持 只有事后可见性,没有实时 hook

这张表不能被理解为覆盖率排行榜。范围很广的 hook 仍可能只有低精度载荷,磁盘解析器可以提供优秀取证上下文但没有任何预防能力,动作前回调可以请求拒绝却不提供动作后的确认。

应用层 hook 与内核控制是互补关系

应用层 hook 拥有更丰富的语义位置。它可能在动作进入操作系统前获得工具名、参数、项目、会话和用户批准上下文。它的弱点是覆盖范围,hosted tool、被禁用的插件、未信任项目或未公开执行路径都可能绕过回调。

内核和 runtime 控制看到的是另一条边界。Linux 的 BPF LSM 程序可以从安全 hook 返回错误,Tetragon等系统也能在支持的资源事件上选择监控或执行动作。这一层更接近文件、进程、socket 与 workload 的实际副作用,也更难被某个应用路径遗漏。但它通常无法只凭 syscall 判断一个请求来自 prompt injection 还是用户的合法意图。

两层应该进入同一条审计链,却不能被当成可互换能力。应用事件解释 Agent 准备做什么,资源边界事件说明进程尝试或完成了什么,再由会话、进程、调用、路径和时间标识关联。高风险策略可以同时要求应用 hook 的语义判断与操作系统边界的防护。

监控与阻断是三组不同证明

Numbat 的阻断模型把控制过程分成五步:

  1. 模型提出工具动作。
  2. 宿主在执行前调用 hook。
  3. Numbat 验证并归一化载荷,计算规则并写入操作记录。
  4. 如果发生干净的阻断命中,Numbat 返回该宿主的原生拒绝响应。
  5. 宿主解释响应,决定执行、询问或拒绝。

Numbat 本身不会取消工具,它请求宿主拒绝。enforcement 记录只能证明 Numbat 算出了 denyno_override,不能证明响应已经送达,也不能证明宿主最终采取了什么动作。

因此,真实部署至少要完成三种证明:

  • 传感器证明:目标执行路径确实触发 hook,并传入预期载荷。
  • 决策证明:归一化事件命中了预定策略,并产生正确响应。
  • 结果证明:宿主执行了响应,或者动作后事件与其他端点信号确认了结果。

只要其中一项没被测试,系统就不能被视为具备可靠阻断能力。配置状态命令只能帮助完成安装检查。Numbat 文档明确说明,hook status 只证明其配置存在且可读,不能证明宿主已激活、事件已触发或证据已送达。

阻断范围应小于检测范围

检测可以容纳一些不确定性,因为人可以继续调查。阻断会中断真实工作,因此需要更强证据。

Numbat 的规则系统体现了这种不对称。CEL 规则可以检查原始命令文本,也可以检查经过解析的 shell_commands。检测支持更广泛的 shell 结构,但阻断只允许较小的静态子集。动态替换、多语句、循环、条件、子解释器、含歧义的管道、解析诊断和被截断的投影都只用于监控。

原则很简单:不要把不确定观察转换成确定控制。

一次干净拒绝还需要满足多项条件:回调必须是支持的同步动作前事件,载荷必须有效,规则要显式写入 enforce: true,相关投影必须成功计算,所选记录必须被输出端接受,依赖序列的规则还需要可用状态。畸形或超限载荷、相关规则错误和输出失败都会使 Numbat 放弃返回拒绝。

内置规则仍然全部只监控。即使安装时启用 enforce 模式,也不会把高严重级别发现自动变成阻断。严重性与控制效果是两个独立字段。

失败语义不能藏在脚注里

“失败时放行”经常被描述成一个全局开关,但 hook 生态里至少有这些失败边界:

  • 宿主未能启动 hook
  • hook 超时
  • 载荷无法归一化
  • 规则计算错误
  • 证据输出端不可用
  • 宿主拒绝或忽略返回的控制响应

Numbat 在自己的干净决策条件失败时,通常不会返回拒绝。但这不保证动作一定执行,因为宿主还可能套用其他规则或询问用户。宿主协议也存在例外。例如文档说明,Copilot CLI 的动作前 hook 如果启动失败、崩溃或非零退出,宿主可能直接拒绝;超时则允许。OpenClaw 的子进程和宿主超时又有不同边界。

所以能力矩阵必须逐个失败点记录语义。否则一句“fail open”可能掩盖一套混合系统,其中一种错误放行动作,另一种错误中断所有工作,而两者都没有留下完整证据。

持久记录决定阻断质量

没有审计轨迹的控制很难校准,也很难获得信任。Numbat 默认把 finding 和适用的 enforcement decision 写入本地 NDJSON。完整实时事件流需要显式启用。HTTP 直送只是一次发送尝试,并不是磁盘队列,接收端不可用时不会自动积压等待重发。磁盘取证也只能恢复宿主实际保存过的内容。

由此可以建立一条证据阶梯:

  1. 规则命中一个被观察事件。
  2. enforcement 记录显示系统计算了拒绝。
  3. 本地输出端持久接收了记录。
  4. 宿主响应路径已经通过实际测试。
  5. 动作结果或独立端点信号确认最终状态。

即使到了最后一步,措辞仍需精确。finding 是策略匹配,不是入侵证明。缺少退出码只代表来源没有提供,不能被解释为命令执行成功。

一条安全的部署顺序

1. 盘点全部执行路径

列出本地 CLI、IDE、桌面端、远程端、云端、gateway、subagent、MCP 与 hosted tool。只要 hook 行为不同,就应拆成独立矩阵行。

2. 先用监控模式安装

触发已知无害动作,确认来源、事件类型、参数、项目、会话、调用 ID 和持久输出。可以时,与宿主自身日志交叉比较。

3. 证明激活和交付

先检查配置存在,再验证宿主有效配置与信任状态。触发一个已知不命中和一个已知命中,确认本地记录与转发路径都能收到。

4. 测量误报成本

从定义狭窄、信号明确的规则开始。复杂原始命令模式应继续保持监控,直到团队理解解析语义和真实工作负载分布。

5. 测试负路径

主动测试畸形输入、超时、输出端不可用、插件禁用、项目未信任和宿主批准行为。记录每个边界是放行还是阻断。

6. 只阻断一小组策略

只有显式控制意图、静态证据、经过测试的原生拒绝传输和清晰恢复路径同时存在时,才启用阻断。宽泛异常规则继续留在监控层。

7. 宿主变化后重新校准

Agent 版本、回调结构、信任控制和云端路径会独立变化。能力矩阵应是有版本的运维工件,不是一次性架构图。

真正的基础设施是证据合同

安全 hook 只是组件。可持续系统是一份证据合同,让不同组件用一致语言描述动作,同时保留各自限制。

这份合同应该让人无法混淆:

  • 配置与激活
  • 动作前意图与已完成动作
  • finding 与入侵事实
  • 已计算拒绝与宿主已执行拒绝
  • 监控覆盖与阻断覆盖
  • 直送成功与持久留存

当这些差异进入能力矩阵,策略就可以在不同宿主间复用,同时不会制造虚假的统一性。团队可以共享一套归一化规则语言,再根据各宿主的证据质量、阻断能力、失败语义和审计能力做不同决策。

常见问题

所有 Agent hook 都能阻断动作吗?

不能。阻断要求同步动作前回调,以及宿主可以接受拒绝响应的协议。动作后 hook、OTLP 日志和磁盘工件都只能观察。Numbat 当前覆盖矩阵中的 OpenCode 也是只监控集成。

hook status 成功是否证明阻断有效?

不能。它只证明预期配置存在且可读。还需要验证宿主加载与信任、触发已知事件、确认持久交付,并实测宿主是否执行拒绝。

为什么要先统一事件,再写安全规则?

统一事件可以把策略含义与厂商回调名称分离。规则面向稳定的 file.writecommand.exec,同时用来源和能力元数据保留宿主差异。

Agent 安全 hook 应该失败时放行还是阻断?

没有统一答案。选择取决于动作风险、证据置信度、可用性要求、恢复路径和宿主语义。关键是逐层记录并测试失败行为。

参考资料


Comment