fix(sandbox): 修复 Linux 会话启动与 Codex 提交确认#490
Merged
deepcoldy merged 42 commits intoJul 16, 2026
Merged
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
fix(lark): 支持长连接使用环境代理
消息 list/get 全部读路径带上 with_sender_name=true,服务端直接返回 sender_name(user 和 bot 都给),parseApiMessage 透出为 senderName: - botmux history / quoted 输出发送者名字,多 bot 群里 agent 不用再猜 哪条消息是谁发的;第三方 bot(不在 bots.json / 观察名册里)也有名字 - 合并转发展开的 <participants> 带 name 属性(渲染器本就支持,之前 没有数据源) - dashboard 会话历史:本地花名册/contact 解析不到时兜底用服务端名字; 本地解析到的名字优先级不变 影响面:仅 API 读消息路径(history/quoted/合并转发/dashboard 历史)。 实时事件路径(receive_v1)不带 sender_name,不受影响;daemon 现有的 contact/花名册解析逻辑保持原样,服务端名字只作补充兜底。
- readRunEnvelope 仅把真实 ENOENT 视为 missing;symlink/目录等非普通文件 fail-closed 为 invalid(O_NOFOLLOW+fstat) - preparing 标记校验 owner PID/start(及可选 bootId),死 owner 可按 maxAge 清理,避免永久堆积 - 补充 dangling symlink、authority 不降级、preparing sweep 回归
- 创建会话弹窗新增「连续创建」开关(localStorage 持久化):打开时创建成功 不关闭弹窗,内联显示成功提示(含打开飞书群链接),清空内容/群名并保留 机器人勾选、协作模式等配置,便于连续创建多个会话 - 侧边菜单栏顶部新增「创建会话」「创建机器人」入口:任意页面可用,创建会话 跨页时先跳 #/sessions 再经 pending 标记自动打开弹窗(create-session-entry.ts) - 创建会话弹窗机器人列表新增搜索框:按名称/appId 过滤,被过滤的已勾选项 保持勾选,展示已选数量提示
Codex 首轮与 Claude 二次 review 均无阻塞问题;构建与定向测试通过,已确认基线失败与本 PR 无关。
review finding:横排覆盖原先写在文件靠前的 media query 里,被后面的 同 specificity 基础规则 display:grid 反超,900px 实测退化为两行全宽按钮。 挪到基础规则之后并补层叠顺序回归测试(断言 mobile override 的源位置 必须在顶层基础规则之后,纯存在性匹配抓不到这类回归)。
…event-target fix(dashboard): 修复筛选输入白屏
…-entries feat(dashboard): 创建会话连续创建开关 + 菜单栏创建入口 + 机器人搜索
deepcoldy
approved these changes
Jul 16, 2026
deepcoldy
left a comment
Owner
There was a problem hiding this comment.
Review 结论:无阻塞,可合并(head ec69f5a)
三个修复(merged 挂载点出 HOME、mountinfo 判活、worker 侧 CODEX_HOME 对齐)根因分析正确、实现自洽,升级兼容闭环完整。以下为逐项核对结论与实际验证。
实际验证
pnpm build通过(本地,Linux 5.15 root 环境)。test/sandbox.test.ts单独跑 56/56 绿(含本 PR 新增 2 例)。- 全量套件(
npx vitest run,全 projects):8636 passed / 13 failed / 19 skipped。13 个失败全部为本机预存环境失败,与本 PR 无关——其中 browser e2e ×16 文件(headless 无浏览器)、recall-frozen ×1、scheduler ×2、schedule-card ×1 是已记录基线;coco e2e ×7 与 multi-bot-session.e2e ×2 在 base commit 74ee3ab 对照 worktree 中逐一复现(9/9 相同失败),确认非 PR 引入。 - PR 波及面定向回归(sandbox / write-input / codex-transcript / cli-adapters / tmux-env ×2 / per-bot-env,7 文件):555/555 绿。
- 真实挂载 sanity:以 root 挂载 tmpfs 到含空格路径(
.../mnt test/with space)+ 普通路径,验证:isMounted的 mountinfo 解析正确(fields[4] 取 mount point、\040octal 解码命中含空格路径);unmountOverlay新回退链在非 FUSE 挂载上正常收敛(fusermount -u失败 →umount成功),卸载后isMounted归 false。
关键路径核对(修复正确性)
- FUSE 递归根因成立:旧
home-merged在 HOME 内、HOME 又是 home overlay 的 lower → fuse-overlayfs 自包含递归,bwrap 解析后续 bind 目标可能永久阻塞。移到/var/tmp/botmux-sbx/<sid>/出 HOME 是正解;mountOverlay自身 mkdir upper/work/merged,新挂载点创建无遗漏。 - 升级兼容闭环:
unmountSandboxOverlays/hasMountedSandboxOverlay同时覆盖新旧四个候选路径——旧 build 创建的活跃 tmux 会话升级后被 sweep 正确保护(liveSandboxSids第一道门 + legacy mount 检查),会话关闭时attachSandboxOutbox的 cleanup 也能卸掉 legacy 挂载。同 sid 在升级后 in-pane /clear 重进prepareSandbox会先卸旧四路径再挂新路径,不会叠挂。 - worker 侧 CODEX_HOME 修复链路成立:
codexHome()调用期动态读 env(codex-paths.ts 本就为此设计);提交确认(codex.ts:245 baseline+delta watch)、resume 发现(codex.ts:210)、transcript bridge(codex-transcript.ts)全部调用期解析;env 改写位于 spawnCli 早段,先于所有消费点。 - process.env 改写作用域安全:
forkWorker每会话独立 fork(worker-pool.ts:1822),worker 不跨会话复用,注释「A worker owns one session」属实;CODEX_HOME已在BOTMUX_INJECTED_ENV_KEYS(child-env.ts:60),tmux server global env 泄漏向量已被 #364 体系关闭;per-bot env 保留键拒绝CODEX_HOME覆盖;isolatedCodexHome!非空断言守卫链成立(else 分支蕴含 !isClaudeFam,此时必已赋值)。 - sandbox×读隔离可见性前提成立:读隔离时 BOT_HOME 经 authPaths real+writable bind(worker.ts:5336)直通真实文件系统,CLI 在 bwrap 内写
<BOT_HOME>/codexhost 侧 worker 可见——这是 worker 对齐修复能生效的前提,代码上确认。 - 无旧路径残留:运行时代码无 legacy merged 路径引用;daemon 侧 codex 路径消费者(adopt discovery / session-discovery)面向全局
~/.codex,语义本就正确,不受影响。codex-app 未声明supportsReadIsolation,读隔离门不会开,env 改写不波及。 - 与最新 master 无语义撞车:base 落后 master 3 个提交(#452/#480 等),均在 im 侧,不涉 sandbox/worker。
非阻塞观察(P3,可另 PR)
- sweep 枚举盲区:
sweepOrphanSandboxes仍只readdir(<dataDir>/sandboxes)。新布局下若 sessionRoot 已被 rm 而 vartmp 侧挂载卸载失败(两级 lazy detach 都失败的窄窗口),该 sid 永远不会被枚举 → 泄漏挂载失去 GC 重试机会。旧布局里 busy mountpoint 在 sessionRoot 内、rm 失败 → sid 留存可重试。建议 sweep 把readdir(VARTMP_ROOT)的 sid 并集进来。 - isMounted 用
resolve()不 realpath:入参虽都来自sandboxOverlayPaths(sessionRoot 已 canonicalize),但/var/tmp本身是 symlink 的环境会与 mountinfo 记录的 canonical 路径失配。一行换canonicalize()即可与仓库「realpath 归一双侧」惯例对齐。 - probe 脚本过期:
scripts/sandbox-{fixes,overlay,opaque-land}-probe.mjs仍探旧 merged 路径,用它们验证本修复会得到过期结论(非发布代码,顺手更新即可)。 - kernel-overlay 路径多一次空跑:
unmountOverlay现在对 root/kernel 挂载先试一次注定失败的fusermount -u,语义不变仅多一次 spawn,无需改。
feat: support Bun global auto-updates
…-permission-error fix(lark): 修正文档订阅权限错误提示
readRunEnvelope 此前先 open(O_RDONLY|O_NOFOLLOW) 再 fstat 判型, 无 writer 的 FIFO 会卡死 open 本身;该路径被 authority/host/daemon 的同步完整性检查广泛调用,一个 FIFO 即可阻塞 daemon event loop。 - 改为 lstat 先行:真 ENOENT 才 missing,symlink/FIFO/目录一律 invalid - open 改用 O_NOFOLLOW|O_NONBLOCK,fstat 复核 regular file 且 dev+ino 与 lstat 一致;lstat→open 竞态换成 FIFO/symlink/其它文件均 fail-closed - readRegularArtifact 同类收口(原先 lstat 通过后按路径重读仍有竞态窗口) - 补无 writer FIFO 回归测试:run.json 立即返回 invalid、工件立即抛 artifact_not_regular_file,均要求不阻塞
多 Agent 会议消费:per-member 有序可靠投递、独立 cursor/epoch、副作用 action gate(capability/owner/epoch fencing + actionId+inputHash 幂等)、角色预设与默认角色、Grok/Traex 可靠 terminal。含 relay origin fail-closed、durable 启动失败边界、隔离会话 IPC 鉴权收紧等安全硬化。Premium(Claude)+LastResort(Codex) 双审通过。
复审 18d98c9 两项跟进: P1:lstat 与 open/fstat/read 拆成两个错误边界。只有初始 lstat 的 ENOENT 才算 missing;lstat 已观察到 regular file 后,任何后续错误 (包括移除竞态导致的 open ENOENT)一律 fail-closed 为 invalid, missing-only 的 legacy 回退不得触发。补受控注入回归:真实 lstat 通过、 仅 open 抛 ENOENT 时返回 invalid,且完好文件随后仍正常读取。 P2:FIFO 回归测试改到子进程执行(--import tsx driver + execFileSync 15s 硬超时)。若未来重新引入阻塞 open,被杀死的是 子进程并抛 ETIMEDOUT 失败,而不是 vitest worker 永久挂死。
冲突解决要点(8 文件): - types.ts/worker.ts:init/message IPC 字段取并集(我方 chatType + master dispatchAttempt/vcMeetingImTurnOrigin);worker 保留 master VC relay 能力块, 去掉被 readProcessStartIdentity 取代的本地 readProcStarttime - session-marker.ts:双方重写取并集——master 的 dispatchAttempt 解析 + managed-origin capability 回退,叠加我方 procStart 身份认证与无 shell 的 readParentPid;authenticated 上下文透传 dispatchAttempt - cli.ts:v3 start/retry/grant 保留我方 postWorkflowDaemonMutation 完整信封 协议;其余 daemon 调用跟随 master 迁移到 fetchDaemonIpc(路由绑定 HMAC) - dashboard.ts:proxyToDaemon 非 workflow 路由走 master fetchDaemonIpc; workflow 变更保留域分离信封 + 响应验签;补 master 的 fetchDaemonUrl - dashboard-ipc-server.ts:保留我方 ipcHmacAuthorized 命名与 workflow-v3 不复用说明,叠加 master trustedHostRequests 短路;workflow v1 前缀 POST 加入 narrow-untrusted 白名单(handler 自带同 secret 的更强完整信封校验, 不与外层 ts:nonce HMAC 重复签名) - daemon.ts:VC 常量/身份/交付逻辑全收,剔除 v2 workflow 残留 import 与 workflowEventWatchers;client.js import 并集;修复重复 IncomingMessage - master VC 服务对已退役 workflows/events/idempotency 的引用改指 utils/canonical-input-hash(同源实现,导出等价) 验证:pnpm build 绿;隔离 HOME 全量 564 files / 9062 tests,除 test/vc-meeting-daemon-session.test.ts 4 例外全绿——该 4 例在未合并的 origin/master 基线(独立 worktree)隔离 HOME 下以相同方式失败,属 master 自带环境敏感问题,非本次合并回归;真实 HOME 下该文件 117/117 全绿。
沙盒(Linux bwrap)/ read-isolation(macOS)里的 chat CLI 读不到宿主进程树 marker、run 目录和 .dashboard-secret,`botmux workflow start/cancel/retry/grant` 三条腿全部失败。本提交新增 daemon 侧 session relay 通道: - CLI 侧(session-relay-client):以 worker 每轮轮换的 origin capability 文件 为探测器 + 凭据(宿主会话没有该文件 → 保持原 marker+签名信封路径不变), loopback POST /api/v3/session-runs/:runId/:mutation;端口取 daemon 发现目录, 被掩蔽时回退 BOTMUX_DAEMON_IPC_PORT - daemon 侧(session-relay):复用 /api/asks 的窄孔姿态——capability 对 liveOrigin 精确核验后,(caller, chat, bot) 三元组全部取自 daemon 自己的 活跃会话记录(请求体选不了身份),再按与宿主路径完全相同的 run 绑定规则 (authorizeV3RunMutationForCurrentTuple,从 authorizeV3DaemonCommand 中 逐字抽取共享)授权;payload 按 mutation 白名单裁剪后过共享 body parser; receiver 会话拒绝;最终调用与签名信封路由同一个 mutation 执行器 - 端口 plumbing:bwrap --setenv 与 tmux 注入白名单补 BOTMUX_DAEMON_IPC_PORT (仅路由标记,非凭据,所有可达路由独立鉴权) 影响面:cli.ts 四个 workflow 命令加 relay 前置分支(宿主路径行为不变); daemon 新路由挂 narrow-untrusted 白名单,签名信封路由零改动;child-env 白名单新增一个非敏感 key 波及所有后端(tmux/zellij 面板可见该端口)。 测试:v3-session-relay(授权矩阵 14 例:capability 有效/无效/缺失、未知会话、 receiver 拒绝、身份不完整、跨 bot、run 绑定不匹配/未绑定/缺失、payload 白名单 与共享 parser 复验、四 mutation 绿路)+ v3-session-relay-client(sandbox/ read-isolation/host 三态探测、端口回退链、URL/body 形状、传输错误); 隔离全量 565/566 文件绿(唯一红 = 已记录的上游 vc-meeting-daemon-session 非隔离性 4 例,与本改动无关)。
复审第四轮两项: P1 代际拼接——capability 只在消息真正 dequeue 进 CLI 时轮换(worker flushPending),而 lastCallerOpenId/quoteTargetId 在下一条消息到达时即前移 (daemon 入站处理);此前 relay 把 turn A 的 capability 与可能已属 turn B 的 caller 字段拼成授权视图,A 可借 B 的身份操作 B 绑定的 run。修法=镜像宿主 路径 current-turn-provenance 的 marker join:授权要求 liveOrigin.turnId === session.quoteTargetId(chat-scope 另比对 currentReplyTarget.turnId),失配一律 403 turn_provenance_stale。join 成立时 caller/quoteTarget 由同一条入站消息原子写入,即同代。两个方向(A 冒充 B / B 已排队时 A 操作自己的 run)均 fail-closed,与宿主路径同构。 P2 残留文件劫持——此前以「capability 文件存在」判定隔离态,SIGKILL/关闭 隔离后的残留文件会把健康宿主会话永久劫持到 relay(403 且不回退)。修法= 复用 resolveSessionContext 的既有优先级:可见的活体进程树 marker 一律优先 返回 null 走宿主路径;capability 文件只在 marker 不可见(bwrap 掩蔽 marker 目录+pid unshare / Seatbelt 拒读)时才作为探测回退。 回归:codex 最小复现(capability/turn=A、caller=B、run owner=B → 403)、 双 turn 指针一致性矩阵、活体 marker 压过残留文件、corrupt marker 同 resolveSessionContext 规则回退。隔离全量 566 files/9087 tests 除已记录 上游 vc-meeting-daemon-session 4 例外全绿。
代码复审通过,合入 Workflow v3 固化与 v2 runtime 下线改动。
PR deepcoldy#336 (45bc65e) 在接入 Genius 适配器时夹带引入了 ownedTopicGroupFollowup 无条件放行:单 bot 话题群里,bot 拥有话题会话后,任何有发言权限的人不 @ 也会 触发回复,不检查群内人数。c984c385 收紧到单 bot 群,但多人类维度仍放行。 本次移除该无条件放行,话题群与普通群统一走 regularGroupMentionMode (群聊 @ 策略): - 默认 'always':多人群里必须 @(恢复 deepcoldy#336 之前的契约) - 'topic':话题内免@续话(原条款已同时覆盖话题群 thread 与共享话题) - 'never'/'ambient':按各自语义生效 - 1人1bot solo 群仍走 userCount<=1 && botCount<=1 放行,体验不变 测试:翻转「话题群默认免@续话」断言为默认忽略;新增 mentionMode='topic' 话题群免@续话、solo 群免@保留两条回归。README 中「仅有一个机器人时无需 @」 表述同步修正。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
改了什么
rootless fuse-overlayfs 递归包含自身。
/proc/self/mountinfo判断挂载状态,避免损坏的 FUSE 挂载让
mountpoint检查本身卡死。CODEX_HOME,修复消息已提交却仍提示“未能确认提交”的误报。
为什么
统一文件沙箱启用后,Linux 非 root 环境会使用 fuse-overlayfs。
旧的
home-merged位于 HOME 内部,而 HOME 又是 overlay lower,导致 FUSE 递归挂载,bwrap 在 Codex 启动前阻塞。
修复挂载后,Codex 已能正常执行消息,但 CLI 将 history 和 rollout
写入机器人独立的
CODEX_HOME,worker 仍检查全局~/.codex,因此产生提交失败误报。
影响面
实际验证
pnpm build:通过。pnpm test:518 files / 8426 tests 全部通过。实际 FUSE + bwrap 启动验证通过。
CODEX_HOME被正确发现。与 #482 的关系
#482 正在重构沙箱并删除 overlay,因此合并后不再需要本 PR 的 FUSE
挂载修复;但其当前实现仍只设置子进程的
CODEX_HOME,worker 侧的路径对齐修复仍需保留。