Article
沙箱挡住了 git mv,模型转头用了 mv
Agent 在沙箱里执行 git mv 被拒绝之后,接下来应该发生什么?
在 Orca 里,留给模型的正路是调用 request_permissions,申请放开 .git。9 月 28 日一个在 boss-skill 仓库里跑的会话,DeepSeek 没有走这条路:git mv 被拒之后,它换成了 mv。文件挪过去了,git 的索引里没有这次重命名。
沙箱把 .git 设成只读是对的。问题出在撞上这面墙之后,没有一条模型真的会走的路。
.git 为什么只读
auto-edit 模式的设计是拿沙箱换免审批:bash 命令不逐条询问,直接在 workspace-write 沙箱里运行。工作区可写,网络关闭,.git、.agents、.codex 三个目录只读。
.git 必须保护。能写 .git/config 的代码,可以设置 core.fsmonitor 或 core.hooksPath;能写 .git/hooks 的代码,可以放一个 pre-commit。这些东西不会在沙箱里执行,它们等的是之后用户自己在终端里跑 git status、git commit 的那一刻,在沙箱外面执行。沙箱管得住它里面跑的进程,管不住被埋进 .git、等着以后被用户的 git 调起来的东西。

撞墙之后的三条路
被挡住之后,能走的路有三条。
第一条是运行时自己判断「这是一次沙箱拒绝」,问用户要不要放开,再重试。这条路 8 月 30 日被有意关掉了(那次重构的来龙去脉):它的依据是命令自己的输出,看到 Operation not permitted 就当成沙箱拒绝。可输出是命令写的,一条命令完全可以自己打印这段文字,借运行时之口去申请更大的权限。那篇的结论是,修复动作要消费可验证的拒绝回执,不能消费一段长得像系统错误的文本。一个月过去,执行层的拒绝回执在生产代码里还没有产出方,这条路就一直关着。
第二条是模型自己调用 request_permissions。工具一直都在,那次会话里 DeepSeek 没有调它。v0.5.1 给 bash 的失败结果补上了诊断提示 sandbox_diagnostic,写明被拒的路径、该申请的目录,还特意说明 git 报 index.lock 的 Operation not permitted 不是残留的锁文件,不要去删;可它没有点名该用哪个工具去申请。
第三条是模型自己找的:换一个不碰 .git 的命令。git mv 换成 mv 能把活干完,代价是 git 不知道这里发生过一次重命名。换成 git commit,就没有这样的替代品了。
本机检出的 Codex(10 月 9 日的 4bad6d78e9)也默认把项目根下的 .git 设成只读,它的 shell 工具给了模型一个参数 sandbox_permissions: "require_escalated",再配一句给用户看的 justification:模型自己申请让这一条命令到沙箱外运行,用户批准一次。这个设计成立的前提是模型会申请。那次会话里 DeepSeek 没有申请,所以这一步得换一个发起者。

在命令跑之前认出它
v0.5.4 让运行时自己发起:bash 命令进沙箱之前,先从命令文本判断它会不会写 .git。判断只看文本,不运行命令,也不读输出,结果是四种之一:
| 判定 | 例子 | 处理 |
|---|---|---|
需要写 .git | commit add rm mv checkout switch merge rebase stash,branch 和 tag 的创建与删除 | 运行前询问 |
| 只读 | status log diff show blame rev-parse,branch 和 tag 的列表 | 照常运行 |
| 拒绝快速审批 | config 和 remote 的写法,submodule worktree hook filter-branch init;带 -c --config-env --exec-path --git-dir --work-tree 的调用;任何 GIT_*= 赋值 | 照常运行,撞墙后只有诊断提示 |
| 无法识别 | 含 $(…)、反引号、eval、heredoc、sh -c "git …" | 照常运行,不猜 |
第三类是故意不给快捷方式的。这些写法要么本身就在改配置,要么能在调用时临时塞进一条配置、或者把 git 指到别的目录,碰的正是上面说的那些执行入口。
解析的规则也偏保守:尊重单引号、双引号和反斜杠转义;在没加引号的 ; && || | &、换行和括号处拆成几段,只有某一段需要写 .git、并且没有任何一段被拒绝或无法识别,才触发询问;开头的 NAME=value,以及 command、env、time、nice、nohup 这类包装会跳过;git -C <dir> 只在目录仍在工作区内时算数。fetch、pull、push 不在范围里:auto-edit 下网络是关的,只放开 .git 它们照样失败,批准了也没用。
询问还有几个前提:这次 bash 确实跑在 workspace-write 沙箱里;工作目录就是仓库根,那里的 .git 是真目录(不是文件,也不是符号链接),.git/config 是文件,.git/hooks 是目录;现有的授权还没有放开 .git。hooks 目录必须事先存在,是因为 Linux 上的只读挂载只能挂到已经存在的路径上:一个没有 hooks 目录的仓库,放开 .git 就等于允许命令新建一个 hooks 目录,再往里放脚本。

只放开这一条命令要写的地方
批准之后,这一条命令运行时 .git 可写,但 config、config.worktree、hooks/、modules/ 仍然只读。commit、add、merge、rebase、stash 写的是 index、objects、refs、logs、HEAD 这一类文件,用不到那几项;而能让代码在沙箱外执行的入口,hooks、core.fsmonitor、core.hooksPath、core.pager、alias、filter 驱动,都在 config 和 hooks 里,modules/ 里放的又是各个子模块自己的 config 和 hooks。下一条命令开始,.git 回到只读。
三个平台的做法不一样:
- macOS 用 Seatbelt,在放开
.git的规则后面追加这几个路径的deny file-write*,后出现的规则优先。 - Linux 用 bwrap,把这几个路径重新挂成只读。只读路径嵌在可写路径里,本来就需要 bwrap;机器上没有 bwrap 时,这条命令直接不跑,不降级。
- Windows 的沙箱适配器把
.git整块列为拒绝目录,表达不了「放开一部分」,这一版在 Windows 上没有快速审批,只有诊断提示。
代价是会写配置的 git 操作照样失败,比如 git branch --set-upstream-to、git checkout --track。它们走诊断提示;确实需要时,用 request_permissions 或者 full-auto。诊断提示这次也补上了下一步:被拒的路径落在 .git、.agents、.codex 里时,提示直接写出「调用 request_permissions,fileSystem.write 填这个目录」。它仍然标着「非权威」,不授予任何权限。
测试里有一条是在真实的 Seatbelt 下跑的:在只放开 .git、保留 config 和 hooks 只读的授权下,同一条命令往 .git 写一个探针文件,再往 .git/config 追加一行,再往 hooks/ 里写 pre-commit。跑完之后,探针文件在,config 一个字没变,pre-commit 不存在。界面这一侧的用例则验证:选「允许这一次」之后,紧跟着的下一条 git 写命令会再问一次。
「本会话允许」记的是一个开关
auto-edit 下的询问有三个选项:允许这一次、本会话允许 git 写命令、拒绝。拒绝时命令不运行,模型收到 denied。
「本会话允许」很容易被实现成「本会话放开 .git」,这次刻意没有这么做。权限请求本身不带任何目录,只带一段说明;Orca 原有的会话级权限持久化,本来就拒绝在会话范围内保存受保护的元数据目录,理由写在代码里:protected metadata paths are limited to the current turn。所以「本会话」记下的只是一个开关:之后识别出的 git 写命令不再询问,但每一条仍然只拿到自己那一次运行的授权。
这个开关只在内存里,进程重启或恢复会话之后,第一条 git 写命令会重新询问。子代理是独立的线程,不共享这个开关。
suggest 模式本来就逐条审批命令,所以不单独弹窗:审批预览里多一行「这条命令会写 <repo>/.git(config 和 hooks 仍只读)」,批准命令即授权。被权限规则自动放行、没经过审批弹窗的命令,按 auto-edit 的方式再问一次。背后只有一条规则:.git 的写权限,只能来自一次明确提到 .git 的审批,或者本会话的那个开关,普通的命令放行规则不算数。
.husky/ 还开着
这一版覆盖不到的地方,设计文档里列了一串:git worktree 的链接工作树(那里的 .git 是个文件),工作区只是更大仓库里的一个子目录,bash 通过 workdir 在子目录里跑 git 写命令,fetch、pull、push 需要的网络授权,会写配置的 git 操作,以及 Windows。这些情况都只有诊断提示。
还有一处跟 .git 的保护无关,这次也没有解决:husky 这类工具会把 core.hooksPath 指向工作区里的 .husky/。这个目录在工作区里,Agent 本来就能改;用户之后在沙箱外手动 git commit,执行的就是被改过的脚本。设计文档里记了一句,Codex 也有同样的问题。
.git/hooks 这次守住了,同样的一扇门,换了个位置开在工作区里。
Keep Reading