fix(sandbox): 修复 Claude 沙盒启动与消息提交#515
Conversation
ejjcc
left a comment
There was a problem hiding this comment.
整体方向和覆盖面都很好,HOME overlay、IPC capability、Unicode 分块的处理也比较扎实。但这里有两处冷启动状态同步/预检问题,建议合入前修掉:
-
src/adapters/hook-installer.ts:188-195:全局 Claudeenv只做 additive merge,无法传播删除。假设第一次冷启动继承了ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL,之后管理员从共享~/.claude/settings.json删除这些键(切回 OAuth、撤销代理或 provider),下一次冷启动仍会从 per-botsettings.json的localEnv保留旧值。结果是已撤销的凭证和 provider 配置会无限期继续生效,与“每次冷启动从 authoritative global config refresh”相反。需要区分 per-bot-only 键和上次继承的键,或直接以共享env替换受管理的 provider/auth/proxy 键;请补一个“共享键被删除后本地同步删除”的测试。 -
src/adapters/hook-installer.ts:161-168:ready preflight 没有验证当前sessionStartCommand,只要配置里存在任意*cli.js session-ready就返回 true。installHook()本身会吞掉写入错误;因此升级后若旧 checkout 的 hook 仍在、但本次因权限/只读文件等原因未能替换,预检会错误通过并武装 gate,旧命令执行失败后首条消息仍会等完整 timeout——正是本 PR 想消除的失败模式。这里应至少比对规范化后的当前 command(或验证其中 node/cli.js 路径仍可执行),并补 stale-hook + install failure 的回归测试。
CI build 当前是绿的;以上是静态路径 review 发现的两个阻塞项。由于该 PR 来自当前 GitHub 账号,平台不允许 self-review 使用 Request changes,因此以 comment review 提交。
deepcoldy
left a comment
There was a problem hiding this comment.
发现 1 个仍需合入前处理的 ready-preflight 问题:
[P1] src/worker.ts:6484-6488:capability 预检只判断路径存在,可能对不可读/陈旧 token 错误放行。
publishSandboxRelayCapability() 在 replaceManagedOriginCapabilityFile() 写失败后只记日志,仍会把新的 token 赋给 sandboxRelayCapability;这里随后只检查 existsSync(readyCapabilityPath)。因此旧文件、坏 JSON,尤其是同名目录都会让预检返回 true。Linux sandbox 的 outbox 对子进程可写,上一代 CLI 可以留下这类状态;例如 capability 路径为目录时,下一次原子 rename 返回 EISDIR,但 existsSync 仍为 true。worker 会武装 ready gate,而 botmux session-ready 读不到当前 capability,daemon 返回 403,首条消息又会等待完整 READY_SIGNAL_TIMEOUT_MS,正好绕过本 PR 新增的 fail-fast 目标。
本地最小复现得到:publishError=EISDIR, exists=true, readableClaim=null。建议让 publish 返回明确成功状态,或在预检中用 readManagedOriginCapability(...) 读取并比对 sandboxRelayCapability.token;同时补 stale/invalid/directory capability path 的回归测试。
deepcoldy
left a comment
There was a problem hiding this comment.
复审通过。原 blocker 已由 bf1e49f7 闭合:
- capability 写盘失败时撤销旧 generation,不再发布新内存/daemon token;
- ready preflight 实读 claim 并比对当前 token,拒绝 stale、malformed、directory/missing transport;
- 新增异常路径回归测试。
验证:pnpm build 通过;相关 9 个测试文件 274/274 通过;GitHub Actions build 通过。完整 unit 9443/9446,剩余 3 个仍为本 PR 未改动的 scheduler 本地时区基线失败。
一句话说明
这次修复覆盖的是一条完整链路:
飞书首轮消息 → Worker 准备隔离环境 → Claude 启动并发出 ready → Worker 输入 prompt → Claude 写入 JSONL → botmux 确认消息已提交。
旧实现中,这条链路在 Linux 文件沙盒 / 读隔离组合下有多个相互放大的缺陷,因此不是单独补一个 hook 就能解决。
用户可见问题
开启沙盒后,新话题的第一条消息可能出现以下现象:
根因与对应修复
CLAUDE_CONFIG_DIR,看不到共享~/.claude/settings.json中的 provider env;SessionStart hook 也可能安装在 Claude 实际不读取的位置env合并到 per-botsettings.json,再把 botmux hooks 安装到同一个有效配置文件;文件权限收紧为0600session-ready使用该端口回调,并继续携带现有轮换 capability 做鉴权readyPattern + quiescence路径cwd=HOME时只创建一个最终生效的 project overlay;实际 upper/work 放在/var/tmp/botmux-sbx/<session>/,稳定 land 路径保留为 symlinkhome-upper,但cwd=HOME时真正生效的是 project overlayhome-upper,HOME 项目监听proj-upperENOTDIR修复后的启动时序
sequenceDiagram autonumber participant L as 飞书话题 participant D as botmux Daemon participant W as Worker participant B as Linux bwrap / overlay participant C as Claude Code L->>D: 首轮消息 D->>W: 初始化会话并暂存首轮 prompt W->>W: 合并共享 Claude env 到 per-bot settings W->>W: 安装 SessionStart / AskUserQuestion hooks W->>W: 创建轮换 capability 并注入 daemon IPC 端口 W->>B: 准备 overlay、敏感路径 masks 和 outbox B->>C: 使用隔离后的配置启动 Claude C->>D: SessionStart hook 调用 /api/session-ready<br/>携带 sessionId 与 capability D-->>W: session_ready IPC W->>W: 释放首轮 ready gate W->>C: 按 96 UTF-8 字节分块、限速输入 prompt C->>B: 提交 user turn 并写入 JSONL B-->>W: Bridge 在实际 winning upper 中观察到记录 W-->>D: 确认首轮消息已提交这里的 daemon 端口本身不是凭证。
/api/session-ready仍校验当前会话的轮换 capability;只是把“如何找到 daemon”从被 mask 的 registry 文件,补充为 Worker 明确注入的 loopback 地址。Overlay 特殊场景
普通项目目录与
cwd=HOME的落盘位置不同:flowchart LR subgraph Normal["普通项目:HOME 与 project 不重叠"] H1["真实 HOME lower"] --> HO["home overlay"] HU["/var/tmp/.../home-upper"] --> HO P1["真实 project lower"] --> PO["project overlay"] PU1["data/sandboxes/.../proj-upper"] --> PO HO --> CH1["Claude HOME"] PO --> CW1["项目工作目录"] end subgraph HomeProject["cwd = HOME:只保留一个 winning overlay"] H2["真实 HOME / project lower"] --> WO["project overlay"] PU2["/var/tmp/.../proj-upper"] --> WO WO --> CH2["Claude HOME 与项目工作目录"] LP["稳定 land 路径"] -. symlink .-> PU2 end兼容旧沙盒目录时:
proj-upper为空:自动转换为外部 scratch + symlink。proj-upper非空:fail closed,保留原 changeset 并要求先 land / 备份,避免静默丢失 overlay whiteout 或 xattr。代码落点
src/worker.ts、src/adapters/hook-installer.tssrc/cli.ts、src/utils/daemon-discovery.ts、src/utils/ready-gate.tssrc/adapters/cli/claude-code.tssrc/adapters/backend/sandbox.tstest/*影响面
部署与会话兼容
验证
pnpm build:通过,且已 rebase 到最新origin/master后重跑。test/sandbox.test.tstest/hook-installer.test.tstest/ready-gate.test.tstest/daemon-discovery.test.tstest/write-input.test.tstest/session-ready-cli.test.tstest/workflow-cli.test.tstest/preset-export-cli.test.tstest/whiteboard-cli.test.tstest/workflow-c0-isolation.test.tsbuild:通过。cwd=HOME使用单一 project overlay,不再出现 HOME overlay 被遮蔽。本机完整测试基线说明
完整
pnpm test试跑未宣称全绿:本机 Node 24 / Linux 环境中,test/v3-worker-fence.test.ts有 3 个进程发现用例返回 fail-closedambiguous;已在临时 detached、未修改的origin/master上复现完全相同结果,与本 PR 无关。此外还有本机 npm
EALLOWSCRIPTS、/proc/self/comm=MainThread等宿主环境项。最初并行 build/test 导致的dist/cli.js missing已改为串行补跑,相关 68 个用例全部通过。