fix(sandbox): 文件沙盒开启时 auto-worktree 失败改为 fail-closed,杜绝回退真实目录 - #921
fix(sandbox): 文件沙盒开启时 auto-worktree 失败改为 fail-closed,杜绝回退真实目录#921ITcathyh wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 13a8c62ecc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| c.cliId, undefined, c.backendType, config.daemon.backendType, | ||
| ) === 'riff'; | ||
| if (riff) return false; | ||
| return c.sandbox === true || c.readIsolation === true || process.env.BOTMUX_SANDBOX === '1'; |
There was a problem hiding this comment.
Include API-only isolation in fail-closed detection
For an apiOnly: true bot with no explicit sandbox flags, this condition leaves failClosed false, but forkWorker later forces readIsolation for every no-transport session in src/core/worker-pool.ts:6859-6860. If auto-worktree creation fails during a Dashboard-created session, this helper therefore falls back to the real default directory and launches the forced sandbox there, preserving the exact real-project write exposure this change is intended to prevent; include the no-transport/API-only isolation condition here as well.
AGENTS.md reference: AGENTS.md:L68-L68
Useful? React with 👍 / 👎.
| try { | ||
| await notify(t('worktree.auto_fail_closed', { dir: baseDir, error: e.reason }, localeForBot(larkAppId))); | ||
| } catch { /* notices are best-effort — never mask the refusal */ } |
There was a problem hiding this comment.
Make refusal cleanup atomic with the takeover check
When pendingRepo is still true at the check above but a notifier adoption consumes it while this awaited network notification is in flight, execution resumes and removes the now-live session from activeSessions and marks its durable record closed. This reopens the takeover race the preceding guard is meant to prevent and leaves the adopted worker orphaned; perform the cleanup before awaiting the notice or recheck pendingRepo immediately afterward under the relevant admission/ownership guard.
Useful? React with 👍 / 👎.
|
感谢这个修复 🙏 方向完全正确——文件沙盒 DIRECT 模式把 workingDir 读写直绑真实宿主目录,auto-worktree 是唯一的隔离屏障,失败时静默回退真实目录确实是逃逸,改成 fail-closed 是对的。构建通过、测试覆盖也很扎实。 自动评审时发现一处会在合入当前 master 时立即触发的问题,供参考(这是初步的自动评审意见,最终以维护者审阅为准):
PR 的 base 早于 mojo 后端(#803)合入,在那个 base 上 worker 的豁免就等价于「非 riff」,所以写死 把本 PR 合到当前 master 后实测:一个 建议修法(避免手抄 worker 的 union 再次漂移):把 worker 那段 sandbox 判定抽成一个依赖轻的共享纯谓词, 两点要点:① 远端豁免要包住整个 union(含 env 强制项);② mojo 的远端证明不止 另外 @ 的自动评审已在 PR 里留了两条 inline(apiOnly / 无 Lark transport 会话会被 forkWorker 强制 readIsolation,同样要纳入谓词;以及 refusal cleanup 在 |
文件沙盒(bwrap/Seatbelt DIRECT 模式)将 workingDir 以读写方式直绑宿主机 真实目录,auto-worktree 是防止 Agent 写入污染真实项目的唯一屏障。此前 auto-worktree 创建失败(默认目录非 git 仓库 / git worktree 失败)时会回退 到真实默认目录启动会话,导致 Agent 在工作目录创建的新文件直接落盘到真实 项目——沙盒逃逸。 修复:当 bot 开启文件沙盒(config.sandbox=true 或 BOTMUX_SANDBOX=1)时, auto-worktree 失败不再回退,而是抛 AutoWorktreeFailClosedError; runAutoWorktreeCommit 捕获后关闭挂起会话并提示用户修复(将默认目录配置为 git 仓库后重发,或用 /repo 手动选择工作目录)。沙盒关闭时保持原有 degrade 行为不变。 Co-Authored-By: Claude <noreply@anthropic.com>
worker 的 sandboxRequested 判定为 sandbox || readIsolation || BOTMUX_SANDBOX, 而 auto-worktree fail-closed 只检查了 config.sandbox。legacy readIsolation bot 在未迁移的只读 BOTS_CONFIG 下仍会被 worker 关进沙盒,但 worktree 失败时不会 fail-closed——回退真实目录即逃逸。补齐 readIsolation 判定,使 fail-closed 跟踪 真实沙盒状态。新增 readIsolation 回归单测。 Co-Authored-By: Claude <noreply@anthropic.com>
1. runAutoWorktreeCommit 的 fail-closed 分支缺少成功路径已有的 pendingRepo 中途消费护栏:notifier 接管会在 worktree 构建(最长 30s) 期间清掉 pendingRepo 并启动自己的活会话,迟到的拒绝若继续 closeSession + publishClosedSessionPatch 会误杀刚接管的活会话。 补 `!ds.pendingRepo` 早退,与成功路径护栏对齐。 2. failClosed 判定未排除 riff 远端后端。worker 的真实沙盒状态是 `!riffRemoteBackend && (sandbox || readIsolation || BOTMUX_SANDBOX)` (localSandboxApplies):riff 无本地 CLI 进程,Agent 写操作落在 riff 自己的远端沙箱、永不触碰真实本地目录,本地逃逸 rationale 不成立。原实现会让 sandbox+riff 的受支持组合在 worktree 失败时 被误拒(可用性回退),且拒绝文案对 riff 失实。改用 resolvePairedSpawnBackendType 与 worker 判定严格对齐。 测试:新增两条回归(fail-closed 遇接管不关闭会话;riff+sandbox 仍 降级回退),pnpm build 通过,default-worktree / card-handler-repo-select / riff-sandbox-bypass / sandbox / dashboard-create-session 等 8 文件 188 项 + trigger-session / notifier 6 文件 103 项全绿。 Co-Authored-By: Claude <noreply@anthropic.com>
failClosed 判定把 BOTMUX_SANDBOX=1 放在最外层短路,先于 riff 豁免检查。 但 worker 的 sandboxRequested = !riffRemoteBackend && (sandbox || readIsolation || BOTMUX_SANDBOX)——对 riff bot 即使 BOTMUX_SANDBOX=1 也不应用本地沙盒(riff 无本地 CLI 进程,Agent 写操作落在 riff 自己的 远端沙箱)。原实现在 riff + BOTMUX_SANDBOX=1 组合下仍会 fail-closed, brick 一个明确不沙盒的 riff 会话。 修复:riff 守卫前置,豁免整个 union(含 env 检查),使 failClosed 与 worker 的 sandboxRequested 严格一致。补回归测试:riff bot + BOTMUX_SANDBOX=1 + worktree 失败 → 仍 degrade(不 fail-closed)。 Co-Authored-By: Claude <noreply@anthropic.com>
- 抽出 localSandboxRequested 共享纯谓词(localSandboxApplies 包住 sandbox/readIsolation/noTransport/BOTMUX_SANDBOX 整个 union),worker 的 sandboxRequested 与 auto-worktree fail-closed 复用同一份,杜绝两处判定再次漂移 - mojo 远端豁免传入完整 effective mojo 配置(buildEffectiveMojoConfig:含顶层 wrapperCli 与合并后的 top-level/mojo env),仅可证明 fully-remote 才豁免, 本地 mojo 仍 fail-closed - fail-closed 纳入 no-transport 强制隔离(apiOnly / HTTP 虚拟会话),与 forkWorker 对无 transport 会话强制 readIsolation 的行为对齐 - refusal cleanup 在 await notify() 之后重检 pendingRepo,避免通知在途时 接管消费 pendingRepo 后 cleanup 误关已被接管的会话 - 补回归单测:mojo fully-remote 不误拒、本地 mojo 仍拒绝、apiOnly/HTTP 虚拟 会话纳入谓词、通知在途接管竞态不误关 Co-Authored-By: Claude <noreply@anthropic.com>
13a8c62 to
2b5547f
Compare
优先级:P0
根因
文件沙盒会把工作目录以读写方式绑定到宿主机,auto-worktree 是避免 Agent 写入真实项目的隔离层。此前 auto-worktree 创建失败时会静默回退真实默认目录,导致沙盒会话的新文件直接落入真实项目。
改动
AutoWorktreeFailClosedError,关闭挂起会话并提示修复默认目录或通过/repo选择目录。config.sandbox、legacyreadIsolation和BOTMUX_SANDBOX=1。影响面
涉及 auto-worktree 启动路径及本地 sandbox/readIsolation 会话;普通非沙盒会话继续允许降级,riff 远端后端行为不变。PTY/Tmux 的后续 worker 运行路径未改动。
验证
BOTMUX_SANDBOX=1以及接管竞态回归。pnpm build通过。