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 | 检查内容 | 方法 | 默认行为 | 故障模式 |
|---|---|---|---|---|
| Permission | Goose 模式 + 用户权限规则(AlwaysAllow/NeverAllow/AskBefore) | 规则匹配 | 根据模式决定 | Fail-Closed |
| Security | 模式匹配的 Prompt 注入检测(如 curl | bash) | 正则/模式匹配 | 检测到 → RequireApproval | Fail-Open |
| Adversary | LLM 驱动的对抗性审查 | 调用 LLM 判断 ALLOW/BLOCK | 出错时 → Allow(fail-open) | Fail-Open |
| Egress | 检测 Shell/Web 工具中的出站流量(URL、SSH、SCP、S3 推送等) | 正则提取 | 始终 Allow,仅信息标记 | N/A |
| Repetition | 检测同一工具 + 同参数的重复调用 | 计数 + 滑动窗口 | 超过阈值 → Deny | Fail-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 AllowFail-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 Inspectorstruct 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 | 故障模式 | 原因 |
|---|---|---|
| Permission | Fail-Closed | 权限是基线,无法判断时应要求确认 |
| Security | Fail-Open | 注入检测是额外保护,不应阻塞正常使用 |
| Adversary | Fail-Open | LLM 审查失败时回退到其他 Inspector |
| Egress | 始终 Allow | 仅用于审计,不做阻断 |
| Repetition | Fail-Open | 重复检测是辅助功能,不应阻止正常调用 |
设计启示:安全链中的每一层都应该明确自己的故障模式。将「最关键的检查」(权限)放在最前面并采用 fail-closed,「额外增强检查」(注入检测、对抗审查)放在后面且采用 fail-open。
审查延迟累积
问题:每层 Inspector 都增加延迟,特别是 Adversary Inspector 需要额外调用 LLM,可能导致每次工具调用增加数百毫秒到数秒的延迟。
对策:
- Inspector 支持
is_enabled()开关,可按需禁用 - 轻量 Inspector(正则匹配)在前,重型 Inspector(LLM 调用)在后
- 可以配置 Inspector 超时时间,超时后按 fail-open 策略处理
- 短路优化:当 Permission Inspector 已经 Deny 时,后续 Inspector 可以跳过(但 Goose 选择串行全部执行,以保证审计日志完整性)
审查规则的可解释性
问题:用户不知道某个工具为什么被阻止。
对策:
- 每个
InspectionResult包含reason和inspector_name RequireApproval附带格式化的告警消息,展示给用户确认- UI 展示是哪个 Inspector 触发的阻止及原因
- 审计日志记录所有 Inspector 的独立审查结果
设计取舍
串行审查 vs 并行审查
| 维度 | 串行审查(Goose 采用) | 并行审查 |
|---|---|---|
| 延迟 | 各层累加 | 取最慢的一层 |
| 依赖关系 | 后层可以看到前层结果 | 各层独立 |
| 短路优化 | 可以在遇到 Deny 时提前终止 | 必须等待全部完成 |
| 实现复杂度 | 低 | 高(需要并发结果合并) |
| 审计完整性 | 保证所有 Inspector 都执行 | 短路可能导致部分 Inspector 未执行 |
Goose 选择串行审查的原因:
- Inspector 数量有限(5 个),累加延迟可接受
- 审计日志需要记录每个 Inspector 的独立审查结果
- 前层审查结果可以为后层提供上下文(如 Permission Inspector 的基线决定后续审查的起点)
- 实现简单,易于调试和扩展
模式匹配 vs LLM 审查
| 维度 | 模式匹配(Security Inspector) | LLM 审查(Adversary Inspector) |
|---|---|---|
| 速度 | 极快(正则匹配,微秒级) | 慢(需要 LLM 调用,数百毫秒到数秒) |
| 覆盖率 | 只能检测已知模式 | 可以理解语义,检测未知攻击 |
| 误报率 | 低(规则精确) | 较高(LLM 主观判断) |
| 维护成本 | 需要持续更新规则库 | 依赖 LLM 能力,规则文件可自定义 |
| 可靠性 | 确定性强 | 受 LLM 可用性影响 |
推荐策略:模式匹配作为第一道快速防线,LLM 审查作为深度补充。两者结合可以兼顾速度和覆盖率。Goose 还允许用户通过 ~/.config/goose/adversary.md 自定义对抗审查规则,在模式匹配和 LLM 审查之间建立了桥梁。
Inspector 模式 vs 单一权限检查
| 维度 | 单一权限检查 | Inspector 链 |
|---|---|---|
| 实现复杂度 | 低 | 中 |
| 可扩展性 | 差(改一处影响全局) | 好(独立插拔) |
| 审计能力 | 弱 | 强(每层独立日志) |
| 适用场景 | 简单 Agent | 企业级安全需求 |
当 Agent 只需要区分「允许/拒绝」时,单一权限检查足够。当需要多维度安全审查(注入检测、对抗分析、出口流量、重复检测)时,Inspector 链的可维护性和可审计性明显更优。
参考来源
- Prompt Injection Attacks against LLMs, Perez et al., 2022
- OWASP Top 10 for Large Language Model Applications
- Goose 源码:
crates/goose/src/tool_inspection.rs、crates/goose/src/permission/permission_inspector.rs、crates/goose/src/security/adversary_inspector.rs、crates/goose/src/security/egress_inspector.rs、crates/goose/src/security/security_inspector.rs