Skip to content

fix(sandbox): 文件沙盒开启时 auto-worktree 失败改为 fail-closed,杜绝回退真实目录 - #921

Open
ITcathyh wants to merge 5 commits into
deepcoldy:masterfrom
ITcathyh:fix-sandbox-escape
Open

fix(sandbox): 文件沙盒开启时 auto-worktree 失败改为 fail-closed,杜绝回退真实目录#921
ITcathyh wants to merge 5 commits into
deepcoldy:masterfrom
ITcathyh:fix-sandbox-escape

Conversation

@ITcathyh

Copy link
Copy Markdown
Contributor

优先级:P0

根因

文件沙盒会把工作目录以读写方式绑定到宿主机,auto-worktree 是避免 Agent 写入真实项目的隔离层。此前 auto-worktree 创建失败时会静默回退真实默认目录,导致沙盒会话的新文件直接落入真实项目。

改动

  • 本地文件沙盒实际生效时,auto-worktree 失败改为抛出 AutoWorktreeFailClosedError,关闭挂起会话并提示修复默认目录或通过 /repo 选择目录。
  • fail-closed 判定与 worker 的真实开关对齐,覆盖 config.sandbox、legacy readIsolationBOTMUX_SANDBOX=1
  • riff 是远端执行后端,不会启动本地 CLI 或写入本地工作目录,因此从整个本地沙盒 union 中豁免;沙盒关闭时仍保持原有降级行为。
  • 补齐 pendingRepo 在 worktree 创建期间被 notifier 接管的护栏,避免迟到的失败处理误关新会话。

影响面

涉及 auto-worktree 启动路径及本地 sandbox/readIsolation 会话;普通非沙盒会话继续允许降级,riff 远端后端行为不变。PTY/Tmux 的后续 worker 运行路径未改动。

验证

  • 新增 sandbox、legacy readIsolation、riff + sandbox、riff + BOTMUX_SANDBOX=1 以及接管竞态回归。
  • pnpm build 通过。
  • default-worktree、card-handler-repo-select、riff-sandbox-bypass、sandbox、dashboard-create-session 等 8 个测试文件 188 项通过。
  • trigger-session、notifier 等 6 个测试文件 103 项通过。

@ITcathyh
ITcathyh requested a review from deepcoldy as a code owner August 18, 2026 14:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/services/default-worktree.ts Outdated
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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +887 to +889
try {
await notify(t('worktree.auto_fail_closed', { dir: baseDir, error: e.reason }, localeForBot(larkAppId)));
} catch { /* notices are best-effort — never mask the refusal */ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个修复 🙏 方向完全正确——文件沙盒 DIRECT 模式把 workingDir 读写直绑真实宿主目录,auto-worktree 是唯一的隔离屏障,失败时静默回退真实目录确实是逃逸,改成 fail-closed 是对的。构建通过、测试覆盖也很扎实。

自动评审时发现一处会在合入当前 master 时立即触发的问题,供参考(这是初步的自动评审意见,最终以维护者审阅为准):

failClosed 谓词只豁免了 backend === 'riff',漏了 fully-remote 的 mojo。

PR 的 base 早于 mojo 后端(#803)合入,在那个 base 上 worker 的豁免就等价于「非 riff」,所以写死 === 'riff' 当时是精确的。但当前 master 已经有 mojo,worker 侧的真实判定是 !localSandboxApplies(backend, mojoConfig)——它会同时豁免 riff 和「可证明 fully-remote」的 mojo

把本 PR 合到当前 master 后实测:一个 mojo { cloud: true }(fully-remote)+ sandbox 的 bot,在非 git 默认目录上会被 failClosed 误拒,但 worker 侧 localSandboxApplies 判定它 本就不施加本地沙盒(远端执行,没有本地进程会写真实目录)。这正是 riff 豁免要防的「远端后端被误 brick」,只是换成了 mojo。

建议修法(避免手抄 worker 的 union 再次漂移):把 worker 那段 sandbox 判定抽成一个依赖轻的共享纯谓词failClosed 与 worker 复用同一份,形状类似:

localSandboxApplies(effectiveBackend, effectiveMojoConfig) && (sandbox || readIsolation || <no-transport 强制隔离> || BOTMUX_SANDBOX)

两点要点:① 远端豁免要包住整个 union(含 env 强制项);② mojo 的远端证明不止 cloud/localDaemon,还包含顶层 wrapperCli 和合并后的 top-level/mojo env——所以不能简单 === 'mojo' 一律豁免(本地 mojo 仍需 fail-closed),要传完整的 effective mojo 配置。

另外 @ 的自动评审已在 PR 里留了两条 inline(apiOnly / 无 Lark transport 会话会被 forkWorker 强制 readIsolation,同样要纳入谓词;以及 refusal cleanup 在 await notify() 之后不是原子的、接管竞态下可能误关已被接管的会话),可一并参考。再次感谢!

huangyuhang.edu and others added 5 commits August 19, 2026 08:44
文件沙盒(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>
@ITcathyh
ITcathyh force-pushed the fix-sandbox-escape branch from 13a8c62 to 2b5547f Compare August 19, 2026 01:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants