从零搭建 Agent Harness 系列(二十)最小权限收口:符号链接、bash 关进工作区与 Grant 审计

系列十九把 Grant 收到「工作区 + 参数摘要」,文件路径做了词法沙箱,破坏性 bash 直接 Deny,授权落到 .claw/grants.json。当时还留下三件事:

1
2
3
符号链接跟随后仍可能逃出工作区
bash 可以 cd .. 或 cat /etc/passwd
Grant 落盘后看不出批了哪次、何时批的

本文对应 go-tiny-claw 三次提交:

1
2
3
f90da50  feat: 在持久化 Grant 中记录审批请求与时间
fd625bc feat: 解析文件路径时跟随符号链接并拒绝逃出工作区
b3473ba feat: 拒绝试图访问工作区外路径的 bash 命令

一、词法沙箱挡不住符号链接

ResolveWithinWorkspace 上一篇只做字符串:AbsCleanRelworkDir/link.txt → /etc/passwd 的相对名仍在区内,read_file 又是 RiskSafe,打开时内核会再 follow 一次。

所以要两道检查:

1
2
3
词法 Rel(root, resolved)     挡住 ../outside

EvalSymlinks 后再 Rel 挡住区内 link 指向区外

工作区自己也可能是链接。macOS 上 /var 常指向 /private/var。比较真实落点时,两边都要用解开后的根,否则区内路径会被误判成逃逸。

目标文件还不存在时(write_file 先解析、再 MkdirAll),不能对整条路径 EvalSymlinks。从父目录往上走到第一个存在的祖先,再拼回后面的相对段:

1
2
3
4
5
subdir/a.go 且 subdir 不存在
→ 跟到 workDir,拼回 subdir/a.go,区内,通过

out/new.go 且 out → /tmp/outside
→ 跟到 /tmp/outside/new.go,Rel 失败

返回值仍是词法路径,不是 follow 之后的真实路径。调用方写入的是用户给的那个名字;测试也比较 Abs(Join(workDir, ...))。已经确认目标在区内,Open(resolved) 再 follow 一次是安全的。

错误要拆开:真正越界报「路径超出工作区」;权限不够、符号链接环报「解析符号链接失败」。不要把 follow 失败一律收成越界。

文件工具的调用点不用改,它们已经走 ResolveWithinWorkspace

二、cmd.Dir 不是沙箱

bash 设了 cmd.Dir = workDir,只决定起始目录。cat /etc/passwdcd .. 仍然能跑。

再加一道启发式:CommandEscapesWorkspace 看命令文本,不解析 shell。

1
2
3
4
strings.Fields 切 token
绝对路径 → 拒
任一路径段是 .. → 拒
cd 无参数 / 绝对路径 / .. → 拒

foo/barcat a.gogo test ./... 放行。./... 里没有名为 .. 的路径段。

判定放在 internal/sandbox,和破坏性命令同一层。Policy 只做映射:破坏性 逃逸都是 PolicyDeny,不要问用户「要不要允许 cat /etc/passwd」。ExecuteCommandContext 之前再拒一次,换掉 Policy 也执行不了。

对外文案统一成「命令试图访问工作区外的路径」。sandbox 回答出没出界,不按绝对路径 / .. / cd 分三种给用户。

这不是 OS 隔离。cat $(echo /etc/passwd)、加引号的绝对路径,Fields 拆不开。v1 只挡明文。

三、Grant 要能回看批了什么

匹配键仍然是:

1
SessionID + WorkDir + ToolName + ArgumentDigest

审计字段不进钥匙。Has 只问「这条授权在不在」,不按 RequestID 查找。

AllowSession 落盘时补上:

1
2
3
4
5
Grant{
RequestID: request.ID,
Decision: decision,
ApprovedAt: time.Now(),
}

FileGrantStore 整份 Grant 序列化进 grants.json,重新加载后这三项还在。测试里用第二次 NewFileGrantStore 读回,确认不是只活在内存里。

还没记的是「谁批的」。AllowOnce / Deny 也不落盘,只有会话级允许会留下记录。完整审计日志是下一档,不是改匹配键。

四、测试要证明什么

符号链接:

  1. 指向区内文件的 link 解析成功,返回词法路径
  2. 指向区外文件的 link 失败,文案是超出工作区
  3. 父目录是区外 link、目标文件还不存在,同样失败
  4. a → b → a 的环报解析符号链接失败,不报越界

bash:

  1. go test ./...ls -lacat a.go 通过
  2. cat /etc/passwdcd ..cd /tmpls ../../ 失败,文案含「工作区外」
  3. Policy / Gate 对逃逸命令直接 Deny,不叫 Handler
  4. BashTool.Execute 在真正 bash -c 之前就返回错误

Grant 审计:落盘后再 NewRequestIDDecisionApprovedAt 还在。

五、阶段八收到哪里

路线图里的最小权限,现在是:

1
2
3
4
5
6
7
8
9
Grant 认工作区和参数摘要

文件路径:词法 + 跟随符号链接

破坏性 bash、逃出工作区的 bash → Deny

同一工作区一份 grants.json

会话级授权带上请求 ID 和批准时间

还不是完整隔离:

1
2
3
4
5
bash 仍是启发式,不是 bwrap / sandbox-exec
命令替换、引号绕过挡不住
检查和使用之间改掉 symlink(TOCTOU)
硬链接没有「指向哪」可 follow
AllowOnce / Deny 没有审计记录

工作区内的 write_file 仍询问,这是产品选择。参数级风险目前覆盖的是「明显破坏、明显逃逸直接拒绝」。

总结

十九做匹配和落盘,二十补硬边界的第二刀,并让磁盘上的 Grant 能回看:

1
2
3
4
5
路径先 Rel,再 follow

bash 文本里的绝对路径和 .. 直接拒绝

Grant 多记 RequestID / Decision / ApprovedAt

阶段八可以收口。下一阶段是路线图里的 Provider 可靠性与成本预算:超时、429、重试,以及单次 Run 的 Token 上限。