跳转到内容

04 工具调用安全审查链(Inspector 模式)

04 工具调用安全审查链(Inspector 模式)

学习目标

读完本章后,你将能够:

  • 理解 Goose 的 ToolInspectionManager 架构与五层 Inspector 的职责分工
  • 掌握 ToolInspector trait 和 InspectionAction 枚举的设计
  • 理解「只能升级不能降级」的合并逻辑及其安全意义
  • 评估串行审查 vs 并行审查、模式匹配 vs LLM 审查的设计取舍

前置知识


核心概念

为什么需要审查链

在 Goose 中,Agent 每次调用工具(Shell 命令、文件读写、网络请求等)之前,必须回答一组安全问题:

  • 用户是否授权此操作?(权限基线)
  • 工具参数中是否包含注入攻击?(安全检测)
  • 此操作是否是恶意的对抗性请求?(对抗审查)
  • 是否会向外部发送敏感数据?(出口流量分析)
  • 是否陷入了重复调用的死循环?(重复检测)

将这些问题硬编码到单一函数中会导致逻辑耦合、难以扩展、审计困难。Goose 采用 Inspector 模式——将每种检查独立为可插拔的模块,通过串行流水线依次审查。

整体架构

核心接口

// Inspector 抽象接口
trait ToolInspector {
fn name(&self) -> &str; // 审查器名称
fn is_enabled(&self) -> bool; // 是否启用
async fn inspect(&self, request) -> Vec<InspectionResult>; // 执行审查
}
// 审查动作枚举
enum InspectionAction {
Allow, // 允许执行
Deny, // 拒绝执行
RequireApproval(Option<String>), // 需要用户确认,附带警告信息
}
// 单个审查结果
struct InspectionResult {
tool_request_id: String, // 关联的工具请求 ID
action: InspectionAction, // 审查结论
reason: String, // 原因说明
confidence: f32, // 审查置信度 [0.0, 1.0]
inspector_name: String, // 来源 Inspector 名称
finding_id: String, // 发现项唯一 ID
}

五层 Inspector 详解

Inspector检查内容方法默认行为故障模式
PermissionGoose 模式 + 用户权限规则(AlwaysAllow/NeverAllow/AskBefore)规则匹配根据模式决定Fail-Closed
Security模式匹配的 Prompt 注入检测(如 curl | bash正则/模式匹配检测到 → RequireApprovalFail-Open
AdversaryLLM 驱动的对抗性审查调用 LLM 判断 ALLOW/BLOCK出错时 → Allow(fail-open)Fail-Open
Egress检测 Shell/Web 工具中的出站流量(URL、SSH、SCP、S3 推送等)正则提取始终 Allow,仅信息标记N/A
Repetition检测同一工具 + 同参数的重复调用计数 + 滑动窗口超过阈值 → DenyFail-Open

项目实践

Permission Inspector:权限基线

Permission Inspector 是审查链的第一层,建立权限的初始状态。它根据 Goose 的运行模式决定工具的初始权限:

// 伪代码:Permission Inspector 逻辑
match goose_mode:
"Auto": 所有工具默认 approved
"Approve": 所有工具需要用户确认
"SmartApprove": LLM 缓存辅助的权限判断
"Chat": 跳过所有工具调用
// 应用用户自定义规则
for rule in user_rules:
if rule matches tool:
apply rule (AlwaysAllow / NeverAllow / AskBefore)
// 一等扩展工具不受权限管理器约束
if tool is first_class_extension:
return Allow

关键设计:Permission Inspector 的结果作为后续 Inspector 的基线。其他 Inspector 只能在此基础上增加限制(escalate),不能解除限制(de-escalate)。

Security Inspector:Prompt 注入检测

Security Inspector 使用模式匹配检测常见的 Prompt 注入攻击模式:

// 伪代码:Security Inspector 检测规则
dangerous_patterns = [
r"curl\s+\S+\s*\|\s*(bash|sh|zsh)", // 管道执行
r"wget\s+\S+\s*-O-\s*\|", // 下载并执行
r"eval\(", // eval 注入
r"__import__\(", // Python 导入注入
]
for pattern in dangerous_patterns:
if pattern matches tool_arguments:
return RequireApproval("检测到可疑模式: {pattern}")
return Allow // 未检测到注入

为什么是 fail-open? 当注入检测逻辑自身出错时(如正则表达式异常),默认允许工具执行,而非阻断所有工具。注入检测是额外保护层,不应成为正常使用的障碍。

Adversary Inspector:LLM 对抗审查

Adversary Inspector 是审查链中最深层的防御,使用 LLM 本身来判断工具调用的安全性:

// 伪代码:Adversary Inspector 流程
1. 读取用户自定义规则文件 ~/.config/goose/adversary.md
2. 构建 LLM 审查请求:
系统提示: "你是一个安全审查员,判断以下工具调用是否安全"
上下文:
- 原始用户任务
- 近期用户消息(最近 N 条)
- 工具调用描述(工具名 + 参数)
输出格式: "ALLOW" 或 "BLOCK"
3. 调用 LLM 获取判断结果
4. 处理结果:
if LLM 返回 "BLOCK":
return Deny(reason="对抗审查拒绝此操作")
else:
return Allow

Fail-Open 策略:当 LLM 调用失败时(网络错误、API 限流、超时),Adversary Inspector 默认返回 Allow。这是可用性优先的设计——安全审查不应成为系统不可用的原因。

Egress Inspector:出口流量分析

Egress Inspector 的设计定位是信息标记而非阻断。它始终返回 Allow,置信度为 0.0,仅将检测到的出站目标记录在结果中:

// 伪代码:Egress Inspector 扫描
egress_patterns = [
r"https?://\S+", // HTTP 请求
r"ssh\s+\S+@\S+", // SSH 连接
r"scp\s+\S+:\S+", // SCP 传输
r"sftp://\S+", // SFTP 传输
r"docker\s+push", // Docker 推送
r"npm\s+publish", // 包发布
r"cargo\s+publish", // Rust 包发布
r"pip\s+upload", // Python 包上传
r"aws\s+s3\s+cp", // S3 上传
r"gsutil\s+cp", // GCS 上传
]
for pattern in egress_patterns:
if pattern matches tool_arguments:
record_outbound_target(pattern_match)
return Allow(confidence=0.0, findings=outbound_targets) // 仅标记,不阻断

Repetition Inspector:重复调用检测

Repetition Inspector 通过滑动窗口检测同一工具 + 同参数的重复调用,防止 Agent 陷入循环:

// 伪代码:Repetition Inspector
struct RepetitionTracker {
window: Vec<(tool_name, args_hash, timestamp)>, // 滑动窗口
max_repeats: usize, // 最大重复次数
window_duration: Duration, // 滑动窗口时长
}
fn inspect(request):
// 清理窗口中的过期记录
window.remove_expired(window_duration)
// 统计当前工具 + 参数的重复次数
count = window.count_same(request.tool_name, request.args_hash)
if count >= max_repeats:
return Deny(reason="重复调用 {count} 次,疑似循环")
else:
window.add(request.tool_name, request.args_hash, now())
return Allow

串行审查与结果合并

Inspector 按注册顺序串行执行。合并逻辑遵循**「只能升级,不能降级」**原则:

关键安全原则:任何 Inspector 都可以阻止工具执行(escalate),但没有任何 Inspector 可以解除其他 Inspector 的阻止(de-escalate)。这保证了「最严格的 Inspector 决定上限」。

合并逻辑的伪代码:

// 伪代码:合并审查结果
fn apply_inspection_results(base_result, additional_results):
final_approved = base_result.approved.clone()
final_needs_approval = base_result.needs_approval.clone()
final_denied = base_result.denied.clone()
for result in additional_results:
match result.action:
Deny:
// 从 approved 和 needs_approval 中移除
final_approved.remove(result.tool_id)
final_needs_approval.remove(result.tool_id)
// 加入 denied
final_denied.add(result.tool_id)
RequireApproval(reason):
// 从 approved 中移除(如果存在)
final_approved.remove(result.tool_id)
// 加入 needs_approval
final_needs_approval.add(result.tool_id, reason)
Allow:
// 不覆盖已有的 deny 或 approval 决定
pass // 不能降级
return PermissionCheckResult(final_approved, final_needs_approval, final_denied)

用户确认与持久化

对于需要确认的工具,Goose 提供三级持久化选项:

// 伪代码:用户确认流程
1. 发送 Message::assistant().with_action_required(...) 给前端
附带安全告警和 RequireApproval 的 reason
2. 等待 tool_confirmation_router.register() 返回用户决策
3. 处理用户选择:
AllowOnce:
本次允许执行
不修改持久化规则
AlwaysAllow:
本次允许执行
持久化为 AlwaysAllow 规则
下次同类工具自动允许
AlwaysDeny:
本次拒绝
持久化为 NeverAllow 规则
下次同类工具自动拒绝

问题与规避

Fail-Open vs Fail-Closed

问题:当某个 Inspector 自身发生故障时(如 LLM 不可用),应该默认允许还是默认拒绝?

对策

Inspector故障模式原因
PermissionFail-Closed权限是基线,无法判断时应要求确认
SecurityFail-Open注入检测是额外保护,不应阻塞正常使用
AdversaryFail-OpenLLM 审查失败时回退到其他 Inspector
Egress始终 Allow仅用于审计,不做阻断
RepetitionFail-Open重复检测是辅助功能,不应阻止正常调用

设计启示:安全链中的每一层都应该明确自己的故障模式。将「最关键的检查」(权限)放在最前面并采用 fail-closed,「额外增强检查」(注入检测、对抗审查)放在后面且采用 fail-open。

审查延迟累积

问题:每层 Inspector 都增加延迟,特别是 Adversary Inspector 需要额外调用 LLM,可能导致每次工具调用增加数百毫秒到数秒的延迟。

对策

  • Inspector 支持 is_enabled() 开关,可按需禁用
  • 轻量 Inspector(正则匹配)在前,重型 Inspector(LLM 调用)在后
  • 可以配置 Inspector 超时时间,超时后按 fail-open 策略处理
  • 短路优化:当 Permission Inspector 已经 Deny 时,后续 Inspector 可以跳过(但 Goose 选择串行全部执行,以保证审计日志完整性)

审查规则的可解释性

问题:用户不知道某个工具为什么被阻止。

对策

  • 每个 InspectionResult 包含 reasoninspector_name
  • RequireApproval 附带格式化的告警消息,展示给用户确认
  • UI 展示是哪个 Inspector 触发的阻止及原因
  • 审计日志记录所有 Inspector 的独立审查结果

设计取舍

串行审查 vs 并行审查

维度串行审查(Goose 采用)并行审查
延迟各层累加取最慢的一层
依赖关系后层可以看到前层结果各层独立
短路优化可以在遇到 Deny 时提前终止必须等待全部完成
实现复杂度高(需要并发结果合并)
审计完整性保证所有 Inspector 都执行短路可能导致部分 Inspector 未执行

Goose 选择串行审查的原因:

  1. Inspector 数量有限(5 个),累加延迟可接受
  2. 审计日志需要记录每个 Inspector 的独立审查结果
  3. 前层审查结果可以为后层提供上下文(如 Permission Inspector 的基线决定后续审查的起点)
  4. 实现简单,易于调试和扩展

模式匹配 vs LLM 审查

维度模式匹配(Security Inspector)LLM 审查(Adversary Inspector)
速度极快(正则匹配,微秒级)慢(需要 LLM 调用,数百毫秒到数秒)
覆盖率只能检测已知模式可以理解语义,检测未知攻击
误报率低(规则精确)较高(LLM 主观判断)
维护成本需要持续更新规则库依赖 LLM 能力,规则文件可自定义
可靠性确定性强受 LLM 可用性影响

推荐策略:模式匹配作为第一道快速防线,LLM 审查作为深度补充。两者结合可以兼顾速度和覆盖率。Goose 还允许用户通过 ~/.config/goose/adversary.md 自定义对抗审查规则,在模式匹配和 LLM 审查之间建立了桥梁。

Inspector 模式 vs 单一权限检查

维度单一权限检查Inspector 链
实现复杂度
可扩展性差(改一处影响全局)好(独立插拔)
审计能力强(每层独立日志)
适用场景简单 Agent企业级安全需求

当 Agent 只需要区分「允许/拒绝」时,单一权限检查足够。当需要多维度安全审查(注入检测、对抗分析、出口流量、重复检测)时,Inspector 链的可维护性和可审计性明显更优。


参考来源