Zero 架构 · 第二课
executeToolCall:工具是「哑的」,循环才是守门人
第一课里,内层循环有一行不起眼的调用:
toolResult := executeToolCall(...)
(loop.go:482)。
那一行背后是整个 agent 里唯一会产生真实副作用的地方 —— 写文件、跑
shell、发网络请求。所以它也是安全模型的全部重量所在。这一课我们拆开它。
对齐 mission:你的目标之一是「说清一个危险工具调用的权限/沙箱判定路径」。这一课
就交付它 —— 把 executeToolCall 看成一串必须逐层通过的关卡。
先看工具长什么样。Tool 接口只有五个方法:
types.go:119
type Tool interface {
Name() string
Description() string
Parameters() Schema
Safety() Safety // ← 只「声明」危险等级,不「执行」任何检查
Run(ctx, args) Result // ← 直接干活,默认自己不做权限判断
}
注意:Run 里没有权限逻辑。工具只通过 Safety() 声明
自己有多危险 —— 一个三值枚举:
types.go:32
PermissionAllow —— 安全,直接放行(如读文件、grep)PermissionPrompt —— 需要问用户(如写文件、跑 shell)PermissionDeny —— 默认拒绝
真正的执行判断全部由 executeToolCall 在调用 Run
之前完成。这是刻意的架构决策:把策略(policy)从机制(mechanism)里
剥出来。工具作者只需诚实声明危险等级,写工具时完全不必操心权限、沙箱、密钥擦除 ——
这些横切关注点在循环里集中处理一次。
Run 是纯机制,executeToolCall 是策略。所有关卡都架在
一次纯粹的工具执行外面 —— 这就是「纵深防御」的形状。
一个工具调用要抵达真正的 Run,得依次穿过这些门。任何一道都可能提前返回一个
ToolResult,让工具根本不执行:
把模型给的 JSON 参数解成 map[string]any;解析失败直接返回错误结果,让模型
重试。
loop.go:776
如果本次运行有 EnabledTools 允许名单或 DisabledTools 拒绝名单,
不在名单内的工具在这里就被挡下(tool_search 作为发现网关被特别豁免)。
loop.go:791
实现了 PrePermissionRejecter 的工具可以在权限流之前先否决非法参数。
loop.go:811
紧接着,ask_user 和 request_permissions 被拦截出来 ——
它们需要路由到交互前端,而不是阻塞在工具内部。
loop.go:822
三种情况下这道调用天然被授权:处于 unsafe 权限模式;工具对这组参数的
有效权限是 Allow;或者命中了之前保存的命令前缀授权。
loop.go:829
这里有个精妙点:权限可以随参数变化。工具可选实现 ArgsPermissioner,
于是 effectivePermission 会按具体参数决定 —— 比如
rm -rf / 需要问,而 ls 不用,尽管都是同一个 shell 工具。
loop.go:1665
若配置了沙箱引擎,Sandbox.Evaluate 先给出一个 Decision
(放行 / 提示 / 拒绝),连同原因(如「网络被阻断」)。
loop.go:859
shouldRequestPermission 综合工具的静态权限 + 沙箱决定,判断要不要弹窗。
loop.go:1672
如果要,就通过 OnPermissionRequest 回调问前端,然后用一个大 switch
处理用户的回答:允许一次 / 本会话允许 / 允许该命令前缀 / 永久允许 / 取消 / 拒绝 ——
每个分支还负责按对应的作用域(turn / session / 持久)去授予网络、文件系统等附加权限,
并登记 cleanup 以便运行结束时回收。
loop.go:865
用户配置的 beforeTool 钩子可以在最后一刻否决调用(非零退出即阻断)。
loop.go:1002
只有全部通过,才真正执行:
registry.RunWithOptions(...),把 permissionGranted、沙箱、
文件版本追踪器等一并传入。
loop.go:1021
执行完还有三个收尾动作,同样在循环里而非工具里:
Output 已脱敏。
loop.go:1079
最后打包成一个 ToolResult 返回给第一课的内层循环,append 进 messages。
loop.go:1082
Zero 的 Tool 接口对权限检查负什么责任?
为什么同一个 shell 工具,ls 直接放行而 rm -rf 要问用户?
把权限、沙箱、密钥擦除放在工具「外面」的循环里,主要收益是?
less +774 ../zero/internal/agent/loop.go # executeToolCall 全貌
less +119 ../zero/internal/tools/types.go # Tool 接口 + Safety
读 executeToolCall 时,别陷进那个巨大的权限 switch(⑥)细节 ——
先把七道关卡的顺序认出来,看清「哪些分支会提前 return 让 Run
不被执行」。顺序本身就是安全模型。
switch 的每个分支分别对应 TUI 上哪个选项?」、
「沙箱 Decision 是怎么算出来的?」(那会引出沙箱那一课),或者
「命令前缀授权是怎么持久化和匹配的?」