从零搭建 Agent Harness 系列(十九)最小权限:Grant 收窄、工作区沙箱与授权落盘
系列十八收口了阶段七。阶段八要解决的不是取消,而是授权太粗:一次「本会话允许 bash」,等于这个会话里任意命令都自动过;read_file 标成 Safe,../etc/passwd 也会自动执行。
本文对应 go-tiny-claw 四次提交:
1 | 4604fbf feat: 将 Grant 匹配从工具名缩小到工作区与参数摘要 |
一、原来的 Grant 只认工具名
系列十三的 AllowSession 存的是:
1 | g.grants.Save(ctx, Grant{ |
查找同样只看这两项。Grant 里其实已经有 WorkDir,但 Gate 没填,Engine 构造 Request 时也没带工作区。
于是会出现:
1 | 用户允许 bash("go test") |
最小权限的第一刀不是落盘,而是把匹配键改对。磁盘上如果还按工具名存,持久化只会把错误范围写死。
二、匹配键加上工作区和参数摘要
现在的钥匙是:
1 | SessionID + WorkDir + ToolName + ArgumentDigest |
参数不能直接当 map key:LLM 可能把 {"a":1,"b":2} 写成 {"b":2,"a":1}。DigestArguments 先 Unmarshal 再 Marshal(Go 会对 object 的 key 排序),然后 sha256。空参数当成 {}。非法 JSON 对原始字节做 hash,避免和合法 {} 撞上。
Gate 保存时把四元组写全:
1 | if decision == AllowSession { |
Has 用请求现算摘要,不要先取出 Grant 再比。找不到键,说明不是同一条授权。
Engine 构造审批请求时补上 session.WorkDir。测试里如果手动 Save 过期 Grant,也必须带上 ArgumentDigest,否则测到的是「键对不上」,不是「过期删除」。
三、路径沙箱是硬边界
filepath.Join 有两个坑:
1 | Join("/work", "../etc/passwd") → 还没清掉 .. |
read_file 又是 RiskSafe,这两类请求会自动执行。审批解决不了这件事:逃逸必须在执行前被拒绝。
规则放在 internal/sandbox,不依赖 tools 或 approval:
1 | sandbox |
ResolveWithinWorkspace 的步骤是:
- 拒绝空路径、空工作区、绝对路径
- 对 workDir 和 Join 结果做
Abs+Clean - 用
filepath.Rel看相对路径是不是..开头
不要用 strings.HasPrefix(resolved, root):/work 会误放行 /work-evil。
read_file、write_file、edit_file 都先走这个函数,失败直接返回,不再问用户。bash 这次没接:cmd.Dir 只是起始目录,命令里仍可以 cd ..。
四、破坏性命令由 sandbox 判定,Policy 只做映射
拒绝表属于「什么命令危险」,和路径逃不逃逸是一类规则。Policy 只回答危险了怎么办。
1 | sandbox.IsDestructiveCommand(command) ← 只收 string |
匹配是启发式子串,不是完整 shell 解析。空白压扁、转小写之后查:
1 | rm -rf / rm -fr / mkfs / dd of=/dev/ / shutdown / reboot / 叉弹 |
go test ./... 仍会询问。cmd=rm; $cmd -rf 这种绕过挡不住,以后再收。
Grant 测试里「不同参数再问」不能再用 rm -rf / 当第二条命令,否则现在会直接 Deny,测不到摘要是否收窄。
五、授权落到 .claw/grants.json
匹配键稳定之后才落盘。文件在 {workDir}/.claw/grants.json。
FileGrantStore 的 Has / Save / Revoke 和内存版同一把钥匙。写入先写 .tmp 再 Rename;落盘失败把内存里那次改动撤掉。过期条目在 Has 时删除并写回。
工厂不能每个 Runtime New 一份文件 Store:两个 Session 会互相覆盖 JSON。RuntimeFactory 按工作区懒加载一次,SessionID 仍在键里,会话之间不串授权。
单元测试里的 Engine 继续用 MemoryGrantStore。CLI / Channel 才走文件。
六、测试要证明什么
Grant 收窄:
- 相同参数复用,Handler 只问一次
- 不同参数再问
- JSON 键顺序不同视为同一 Grant
- 不同 WorkDir 不复用
- 过期 Grant 先命中再删掉
路径沙箱:相对路径成功;..、嵌套 ..、绝对路径、空路径、空工作区失败。
命令拒绝:破坏性命令 IsDestructiveCommand == true;go test 为 false。Gate 对 rm -rf / 不调用 Handler。
文件存储:重新 NewFileGrantStore 后相同参数仍命中;不同参数不命中;过期失效;没有文件也能创建。
七、阶段八还没做完的
1 | 审计:谁批的、何时、批了哪次调用 |
路线图里的参数级风险,目前只覆盖了「明显破坏性 bash 直接拒绝」。工作区内 write_file 仍询问,这是产品选择,不是漏做。
总结
阶段八先改匹配,再加硬边界,最后才落盘:
1 | Grant 认参数摘要和工作区 |
授权持久化之前,如果钥匙还是工具名,磁盘上存的也会是错误范围。下一阶段可以补审计,或把 bash 也关进工作区。