Zero 架构 · 第十三课
第二课的 executeToolCall 有一道关卡问沙箱:「这个工具调用能跑吗?」这一课就拆开回答那句话的引擎。
它的全部产出是一个 Decision{Action: Allow | Prompt | Deny} —— 放行、追问用户、或直接拒。
难点不在任何单条规则,而在这些规则以什么顺序叠加:一条严格有序的判定链
Engine.Evaluate
engine.go:277。
对齐 mission:一个自动跑命令的 coding agent,最危险的一步就是「执行」。既不能每条命令都烦用户点确认(那就退化成手工),
也不能默默放行一条 rm -rf。这一课的功夫,是把「什么时候敢自动放行、什么时候必须追问、什么时候直接拒」
编码成一条顺序即语义的链 —— 顺序错了,安全就漏了。
Evaluate 从上到下一路 return,谁先命中谁说了算。顺序是刻意设计的 —— 拒绝类的判断必须排在放行类之前,
否则一条本该拒的请求会被前面的「放行」提前接走。链条骨架:
| 顺位 | 判断 | 结果 |
|---|---|---|
| 1 | ctx 已取消 :281 | Deny |
| 2 | engine 为 nil / ModeDisabled :285,:299 | Allow(沙箱关) |
| 3 | Permission==Deny :302 | Deny |
| 4 | 持久化 deny 授权命中 :312 | Deny |
| 5 | 路径检查(工作区边界 / Deny·Allow 路径表):334,:338 | Deny 或攒起可追问块 |
| 6 | 网络拒绝 :350 | shell→Prompt,否则 Deny |
| 7 | 破坏性 shell 命令 :356 | Prompt / Deny |
| 8 | 持久化 allow → 会话 allow :374,:383 | Allow |
| 9 | 攒起的可追问路径块 :392 | Prompt |
| 10 | 工作区内写自动放行 :398 | Allow |
| 11 | 原生沙箱激活 → shell 自动放行 :405 | Allow |
| 12 | 兜底 :411 | Prompt |
rm -rf。
拒绝能被授权提前豁免,放行不能提前豁免安全检查 —— 这就是顺序编码的安全语义。
和第十二课那条「恢复逻辑先查 ctx.Err()」一模一样的纪律,在这里是判定链的第一步
:281:取消优先于一切业务判断。
一个在关停中的 agent,不该还在纠结「这条命令要不要追问」—— 先认取消,直接 deny 收场。
第 11 位是整条链的点睛之笔
:405:一条普通 shell 命令(网络/破坏性/路径都已在前面放过)
到底自动放行、还是追问用户?答案取决于 shellSandboxActive
:223:
if !(backend.Available && backend.Executable != "" &&
backend.CommandWrapping && backend.NativeIsolation) {
return false // ← engine.go:231:四个条件缺一不可
}
Windows 上有个诚实的降级
:240:命令要等 zero sandbox setup 写下标记后才真正被包裹;
标记没写时,引擎拒绝把它当「已沙箱化」自动放行,而是 fall through 去追问。
注释点破了原因:否则引擎会把一条其实在裸奔的命令当成「有沙箱保护」放行,直接击穿上面那条不变量。
引擎的判断必须与运行时真实发生的包裹严格镜像 —— 声称的安全和实际的安全必须一致。
想在执行前先问一句「这会被拦吗?」的调用方(批量确认、预检),不另写一套判断,而是
Precheck
:252 直接调 Evaluate 再把 deny 抽成 Block
:262。
一份策略,一个真相来源 —— 预检和真判定永不漂移。这和第十一课「流式与非流式共用一次遍历」是同一种「单一实现」的洁癖。
为什么持久化 deny 授权短路在前,而 allow 授权排在检查之后?
一条普通 shell 命令什么时候被自动放行而不再追问用户?
Windows 上标记未写时,引擎为何拒绝把 shell 命令当已沙箱化放行?
less +277 ../zero/internal/sandbox/engine.go # Evaluate:严格有序的判定链(全课核心)
less +312 ../zero/internal/sandbox/engine.go # 持久化 deny 短路 vs :374 allow 后置
less +223 ../zero/internal/sandbox/engine.go # shellSandboxActive:四条件齐备才算激活
less +240 ../zero/internal/sandbox/engine.go # Windows 标记未写→降级追问
less +252 ../zero/internal/sandbox/engine.go # Precheck 复用 Evaluate:一份策略不重复
读的顺序:先通读 Evaluate 从上到下的每个 return,记住「拒在前、放在后」这条脊椎。
再把第 4 位的持久化 deny 和第 8 位的 allow 对读 —— 体会「授权能豁免拒绝、不能豁免检查」。
最后啃 shellSandboxActive 那四个 && 条件和 Windows 降级注释 —— 那是「安全边界要么点击要么内核,绝不落空」的全部落地。
runner.go 怎么用 landlock/seccomp/sandbox-exec 真正把命令关进笼子?」、
「Classify(risk.go)怎么认出一条命令是破坏性/触网的?」,
或者「授权是怎么持久化、又怎么按 scope 匹配的(grants.go/grant_scope.go)?」