插件式 Inspector 安全链
学习目标
- 理解
ToolInspectionManager与ToolInspectortrait 的设计模式 - 掌握 Inspector 的注册、顺序执行与结果合并机制
- 理解”只能升级、不能降级”的合并策略
- 掌握权限持久化(AlwaysAllow/AlwaysDeny)机制
- 了解 Inspector Pipeline 与 Hook 系统的关系
- 理解
action_required审批结果的回传路径
项目实践
ToolInspector trait 设计
所有检查器统一实现 ToolInspector trait:
// 伪代码:ToolInspector trait 定义#[async_trait]pub trait ToolInspector: Send + Sync { /// 检查器名称(用于日志与调试) fn name(&self) -> &'static str;
/// 检查工具调用请求 async fn inspect( &self, session_id: &str, tool_requests: &[ToolRequest], messages: &[Message], goose_mode: GooseMode, ) -> Result<Vec<InspectionResult>>;
/// 检查器是否启用 fn is_enabled(&self) -> bool { true }
/// 支持向下转型到具体类型 fn as_any(&self) -> &dyn std::any::Any;}检查结果类型:
// 伪代码:检查结果pub struct InspectionResult { pub tool_request_id: String, // 关联的工具调用 ID pub action: InspectionAction, // 检查决定 pub reason: String, // 原因说明 pub confidence: f32, // 置信度 pub inspector_name: String, // 检查器名称 pub finding_id: Option<String>, // 安全发现 ID}
pub enum InspectionAction { Allow, // 允许执行 Deny, // 拒绝执行 RequireApproval(Option<String>), // 需要用户审批(含可选警告)}已注册的 Inspector
Goose 内置了多个 Inspector,各自负责不同的安全维度:
| Inspector | 名称 | 职责 | 来源文件 |
|---|---|---|---|
| PermissionInspector | permission | 基于用户权限配置、只读标注、SmartApprove 缓存决定工具是否需要审批 | permission/permission_inspector.rs |
| SecurityInspector | security | 基于模式匹配检测恶意命令(如 `curl | bash`、数据外泄) |
| EgressInspector | egress | 提取并记录所有出站目标(URL、Git 远程、S3、SSH 等) | security/egress_inspector.rs |
| AdversaryInspector | adversary | 基于用户自定义规则(adversary.md)用 LLM 审查工具调用 | security/adversary_inspector.rs |
ToolInspectionManager:顺序执行与结果收集
Manager 实现(伪代码):
// 伪代码:ToolInspectionManagerpub struct ToolInspectionManager { inspectors: Vec<Box<dyn ToolInspector>>,}
impl ToolInspectionManager { /// 添加检查器(按添加顺序执行) pub fn add_inspector(&mut self, inspector: Box<dyn ToolInspector>) { self.inspectors.push(inspector); }
/// 运行所有检查器 pub async fn inspect_tools( &self, session_id: &str, tool_requests: &[ToolRequest], messages: &[Message], goose_mode: GooseMode, ) -> Result<Vec<InspectionResult>> { let mut all_results = Vec::new();
for inspector in &self.inspectors { if !inspector.is_enabled() { continue; }
// 异步执行单个 Inspector match inspector.inspect(session_id, tool_requests, messages, goose_mode).await { Ok(results) => all_results.extend(results), Err(e) => { // 单个 Inspector 失败不影响其他 Inspector tracing::error!(inspector = inspector.name(), error = %e, "Tool inspector failed"); } } }
Ok(all_results) }}合并逻辑:只能升级、不能降级
多个 Inspector 的结果通过 apply_inspection_results_to_permissions 合并:
// 伪代码:合并逻辑pub fn apply_inspection_results_to_permissions( mut permission_result: PermissionCheckResult, inspection_results: &[InspectionResult],) -> PermissionCheckResult { for result in inspection_results { match result.action { InspectionAction::Deny => { // 降级:从 approved 和 needs_approval 中移除,加入 denied permission_result.approved.retain(|r| r.id != result.tool_request_id); permission_result.needs_approval.retain(|r| r.id != result.tool_request_id); if !permission_result.denied.iter().any(|r| r.id == result.tool_request_id) { permission_result.denied.push(request.clone()); } }
InspectionAction::RequireApproval(_) => { // 升级:从 approved 中移除,加入 needs_approval permission_result.approved.retain(|r| r.id != result.tool_request_id); if !permission_result.needs_approval.iter().any(|r| r.id == result.tool_request_id) { permission_result.needs_approval.push(request.clone()); } }
InspectionAction::Allow => { // 不降级:如果已被其他 Inspector 拒绝或要求审批,保持不变 // 这是"只能升级不能降级"的核心逻辑 } } } permission_result}关键安全属性:Allow 操作不覆盖其他 Inspector 的 Deny 或 RequireApproval 决定。这意味着:
- EgressInspector 记录日志但返回
Allow,不会覆盖 SecurityInspector 的Deny - 多个 Inspector 的结果是”与”关系:只要有一个拒绝,最终就拒绝
审批回传路径
当工具调用需要审批时,通过 action_required 路由回传给前端:
权限持久化
PermissionManager 将用户权限持久化到磁盘:
// 伪代码:权限级别pub enum PermissionLevel { AlwaysAllow, // 始终允许(用户选择"记住") NeverAllow, // 始终拒绝 AskBefore, // 每次询问}
// 伪代码:权限管理器impl PermissionManager { /// 获取用户持久化权限 pub fn get_user_permission(&self, tool_name: &str) -> Option<PermissionLevel>;
/// 更新用户权限(AlwaysAllow/AlwaysDeny) pub fn update_user_permission(&self, tool_name: &str, level: PermissionLevel);
/// SmartApprove 模式下的 LLM 缓存 pub fn get_smart_approve_permission(&self, tool_name: &str) -> Option<PermissionLevel>; pub fn update_smart_approve_permission(&self, tool_name: &str, level: PermissionLevel);}SmartApprove 模式:首次调用时,使用 LLM 判断工具是否只读,并将结果缓存。后续相同工具的调用直接使用缓存结果,不再询问用户。
与 Hook 系统的关系
Inspector 和 Hook 是两个独立但互补的安全机制:
| 维度 | Inspector Pipeline | Hook System |
|---|---|---|
| 触发时机 | 工具执行前(同步检查) | 多个生命周期事件(PreToolUse/PostToolUse/…) |
| 实现方式 | Rust trait,编译期集成 | JSON 配置 + 外部脚本,运行期扩展 |
| 执行环境 | 进程内 | 子进程(sh -c) |
| 决策能力 | Allow/Deny/RequireApproval | Allow/Deny(通过退出码 2) |
| 插件来源 | 核心代码 | 外部插件 hooks/hooks.json |
| 失败策略 | 记录错误,继续其他 Inspector | fail-open(允许),不阻塞主流程 |
Hook 事件(完整列表):
PreToolUse— 工具执行前(可阻断)PostToolUse— 工具执行后PostToolUseFailure— 工具执行失败后SessionStart/SessionEnd— 会话生命周期UserPromptSubmit— 用户提交提示前BeforeReadFile/AfterFileEdit— 文件操作BeforeShellExecution/AfterShellExecution— Shell 执行Stop— Agent 停止SubagentStart/SubagentStop— 子 Agent 生命周期
问题与规避
Inspector 顺序影响结果
问题:Inspector 按注册顺序执行,但合并逻辑中 Deny 和 RequireApproval 总是优先于 Allow,所以顺序不影响最终安全决策。
规避:
- 日志记录的顺序与注册顺序一致,便于调试
- PermissionInspector 应该最先运行(建立基线决策),其他 Inspector 作为覆盖
- 失败的 Inspector 被记录错误但不阻塞后续 Inspector
fail-open 策略差异
问题:不同组件在出错时有不同的 fail-open 策略:
- Inspector 失败:记录错误,继续其他 Inspector(不影响主流程)
- Hook 失败:记录警告,视为 Allow(不阻塞工具执行)
规避:
- Inspector 的 fail-open 是安全的,因为其他 Inspector 仍能正常工作
- Hook 的 fail-open 是设计选择:“a misbehaving hook MUST NOT block” — 故障的 Hook 不应阻止 Agent 工作
- 关键安全检查(PermissionInspector)是进程内代码,不会因外部因素失败
EgressInspector 的 confidence = 0.0
问题:EgressInspector 返回 confidence: 0.0,因为它只做日志记录,不做安全判断。
规避:
confidence字段在合并逻辑中不使用(只关注action字段)- EgressInspector 总是返回
Allow,不改变安全决策 - 其价值在于
tracing::info!日志中的出站目标信息,用于审计
AdversaryInspector 的 LLM 延迟
问题:AdversaryInspector 使用 LLM 审查工具调用,会增加额外的 API 调用延迟。
规避:
- 仅在
adversary.md文件存在时启用(is_enabled()检查配置文件) - 默认只审查
shell工具,不审查所有工具 - 用户可以通过配置
tools:行控制审查范围
设计取舍
Inspector Pipeline vs 单一权限检查
| 方案 | 优势 | 劣势 |
|---|---|---|
| Inspector Pipeline(Goose 选择) | 职责分离、可扩展、独立测试、灵活组合 | 多个 Inspector 串行执行、合并逻辑复杂 |
| 单一权限检查 | 简单直接、执行快速 | 功能耦合、难以扩展、测试困难 |
Goose 的取舍:选择 Inspector Pipeline,因为安全需求随时间增长(从基础的权限检查到模式匹配、LLM 审查、出站审计),Pipeline 模式允许独立添加新的 Inspector 而不影响现有逻辑。
为什么选择 trait 而非闭包
ToolInspector 使用 trait 而非闭包函数:
- trait 支持多方法(
name()、is_enabled()、as_any()) - trait 支持向下转型(
as_any()),便于 Manager 获取具体类型 - trait 在编译期保证类型安全
- 新的 Inspector 只需实现 trait 即可注册
参考来源
- ToolInspectionManager:
/home/yuexiaoliang/projects/agent-coursecraft/.op/goose/crates/goose/src/tool_inspection.rs - PermissionInspector:
/home/yuexiaoliang/projects/agent-coursecraft/.op/goose/crates/goose/src/permission/permission_inspector.rs - SecurityInspector:
/home/yuexiaoliang/projects/agent-coursecraft/.op/goose/crates/goose/src/security/security_inspector.rs - EgressInspector:
/home/yuexiaoliang/projects/agent-coursecraft/.op/goose/crates/goose/src/security/egress_inspector.rs - AdversaryInspector:
/home/yuexiaoliang/projects/agent-coursecraft/.op/goose/crates/goose/src/security/adversary_inspector.rs - Hook System:
/home/yuexiaoliang/projects/agent-coursecraft/.op/goose/crates/goose/src/hooks/mod.rs