跳转到内容

插件式 Inspector 安全链

学习目标

  • 理解 ToolInspectionManagerToolInspector trait 的设计模式
  • 掌握 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名称职责来源文件
PermissionInspectorpermission基于用户权限配置、只读标注、SmartApprove 缓存决定工具是否需要审批permission/permission_inspector.rs
SecurityInspectorsecurity基于模式匹配检测恶意命令(如 `curlbash`、数据外泄)
EgressInspectoregress提取并记录所有出站目标(URL、Git 远程、S3、SSH 等)security/egress_inspector.rs
AdversaryInspectoradversary基于用户自定义规则(adversary.md)用 LLM 审查工具调用security/adversary_inspector.rs

ToolInspectionManager:顺序执行与结果收集

Manager 实现(伪代码):

// 伪代码:ToolInspectionManager
pub 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 的 DenyRequireApproval 决定。这意味着:

  • 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 PipelineHook System
触发时机工具执行前(同步检查)多个生命周期事件(PreToolUse/PostToolUse/…)
实现方式Rust trait,编译期集成JSON 配置 + 外部脚本,运行期扩展
执行环境进程内子进程(sh -c
决策能力Allow/Deny/RequireApprovalAllow/Deny(通过退出码 2)
插件来源核心代码外部插件 hooks/hooks.json
失败策略记录错误,继续其他 Inspectorfail-open(允许),不阻塞主流程

Hook 事件(完整列表):

  • PreToolUse — 工具执行前(可阻断)
  • PostToolUse — 工具执行后
  • PostToolUseFailure — 工具执行失败后
  • SessionStart / SessionEnd — 会话生命周期
  • UserPromptSubmit — 用户提交提示前
  • BeforeReadFile / AfterFileEdit — 文件操作
  • BeforeShellExecution / AfterShellExecution — Shell 执行
  • Stop — Agent 停止
  • SubagentStart / SubagentStop — 子 Agent 生命周期

问题与规避

Inspector 顺序影响结果

问题:Inspector 按注册顺序执行,但合并逻辑中 DenyRequireApproval 总是优先于 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