Skip to content

fix(codex): 修复软链接启动时找不到 code mode host - #1043

Open
Duang777 wants to merge 1 commit into
deepcoldy:masterfrom
Duang777:codex/fix-codex-host-symlink
Open

fix(codex): 修复软链接启动时找不到 code mode host#1043
Duang777 wants to merge 1 commit into
deepcoldy:masterfrom
Duang777:codex/fix-codex-host-symlink

Conversation

@Duang777

Copy link
Copy Markdown

问题

当普通 Codex 适配器通过 PATH 中的软链接启动 App 内置 Codex 时,Codex 会按软链接所在目录查找 codex-code-mode-host,导致辅助进程缺失并反复失败。

修复

  • 启动普通 Codex 前将可执行文件解析为 canonical realpath
  • 补充 resolveCommandReal 的适用契约说明
  • 增加 App bundle + PATH 软链接的回归测试

验证

  • bun run build
  • bun run test -- test/cli-adapters.test.ts(407/407)
  • git diff --check

范围

仅影响普通 codex 适配器的可执行文件解析;其他 CLI 适配器和 codex-app 行为不变。

Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
@Duang777
Duang777 requested a review from deepcoldy as a code owner August 27, 2026 15:54
@deepcoldy

Copy link
Copy Markdown
Owner

感谢 PR!我先做了一轮自动化初步评审(实跑验证,非纸面推理),把结论整理如下。这只是自动评审的初步意见,最终以维护者审阅为准。

先说结论:方向合理,但我在本机实测得到的证据不太支持「Linux 上普通 codex 走软链接会找不到 codex-code-mode-host」这个前提,同时这个改法会带来一处用户可见的路径回退。想请你补充一下复现信息,我们好判断是收窄改法还是保留现状。

一、根因实测:Linux 上 codex 自己就已经 canonical 了

本机有真的 codex 0.149.1 standalone(~/.local/bin/codex…/releases/0.149.1-…/bin/codex,两跳软链接),我用 strace 直接看它怎么找 host:

readlink("/proc/self/exe", "/root/.codex/packages/standalone/releases/0.149.1-…/bin/codex") = 85
stat("…/releases/0.149.1-…/codex-package.json") = 0

关键是内核给 /proc/self/exe 返回的本来就是解析完软链接的真实路径,与用什么路径去 exec 它无关。做了一组正负对照:

  • 负向:host 只放在软链接旁边,真实 bin 目录里没有 → codex 探测的是 hostless/codex-code-mode-host(真实 bin 旁),没去看软链接旁边
  • 正向:host 只放在真实 bin 旁边,通过软链接启动 + -c features.code_mode_host=true 跑真实 turn → 正常完成,exit 0,探测路径是 pos/realdir/codex-code-mode-host

即:软链接启动不影响 host 定位。另外 codex 里有 CodeModeHostConfigToml.disable_in_process_fallback 字段,host 缺失时还有 in-process 兜底(实测 -c features.code_mode_host=true -c code_mode=true 在 host 不同目录时仍 exit 0),不是「反复失败」。

再补两条,说明沙箱下也已经覆盖:

所以想请你确认:这个 bug 是在 macOS 上复现的吗? 如果是,那是合理的——Rust current_exe() 在 macOS 走 _NSGetExecutablePath,不像 Linux 的 /proc/self/exe 会自动解析软链接,行为确实可能不同。但 PR 描述和测试都没点明平台,而 daemon 实际跑在 Linux。方便的话贴一下失败时的报错原文和 codex --version / 安装形态,能帮我们把范围钉准。

二、一处用户可见回退:resolvedBin 同时是展示/持久化字符串

registry.ts 原来的注释其实专门写过不要对 resolvedBin 这么做("NOT needed for resolvedBin alone… that would also rewrite the many display/probe call sites, where the user-facing PATH entry is the more useful string"),这个 PR 把那段注释替换掉了,但那些 call site 还在。实跑对照(用本 PR 编出来的 dist):

master : /root/.local/bin/codex
本 PR  : /root/.codex/packages/standalone/releases/0.149.1-x86_64-unknown-linux-musl/bin/codex

这个字符串会流到三个用户会复制去执行落盘的地方:

  1. cli-runtime-updates.jsonbinPath / updateCommandcli-runtime-update.ts:713/719daemon.ts:4227dashboard.ts:3973 传进去的 resolveBin)。本机线上这个文件现在是:
    "binPath": "/root/.local/bin/codex",
    "updateCommand": "/root/.local/bin/codex update",
    "installationPath": "/root/.codex/packages/standalone/releases/0.149.1-…/bin/codex"
    注意 canonical 形态已经单独存在 installationPath了,binPath 刻意保留 PATH 入口。改了之后建议给用户执行的命令会变成版本钉死的那条,而且会落盘(TTL 24h,CLI_RUNTIME_UPDATE_CHECK_INTERVAL_MS),还会显示在 Dashboard(settings-page.tsx:1891)和 owner 的飞书卡片(cli_update.binary / cli_update.command)。
  2. Dashboard「复现命令」worker.ts:14038 reproduceBaseBin → 剪贴板 sessions.ts:411 copySpawnCommand)。
  3. local-terminal-opener.ts:127/142(这条目前看没有生产调用方,先只当观察项)。

为什么这不只是好看不好看的问题:本机 releases/ 下保留了 4 个版本(0.144.6 / 0.145.0 / 0.147.0 / 0.149.1,最早的是 7 月),实测各自 --version 分别就是各自的老版本。所以升级后那条版本钉死的路径大概率不是报错,而是静默跑一个旧版本——比 ENOENT 更难发现。而软链接形态永远跟着 current

三、建议的收窄改法

如果确认 macOS 上确有此问题,建议不要动 resolvedBin 的语义,而是二选一:

  • (a) 只在真正 spawn 的那一个点 canonical 化(worker.ts:14029spawnBin);
  • (b)codex-app.ts:60-62 的既有写法,加一个单独的 canonical 访问器,让 binPath / reproduceBaseBin 继续保留 PATH 入口。

这样 bug 修掉,展示/落盘/复制粘贴的字符串也不回退。

另外如果最终确认要改,coco.ts:142traex.ts:228 是同一类(traex/coco 都是 codex fork,也有 traex-code-mode-host),到时候建议一并对齐,别只改一个。

四、我这边跑过的验证

相对当前 master(b569b0efb)trial-merge:merge-tree 0 冲突tsc --noEmit 0 报错,bun run build 通过。

  • cli-adapters + resolve-command-shell-parse + sandbox 三套件:443 全绿
  • 扩到全部 codex/adapter/sandbox/isolation 相关 83 个文件:1562 passed / 12 failed;在干净 master 上跑同样 3 个失败文件 → 同样 12 failedworkflow-c0-isolationcodex-notifier-settings-uiworker-codex-app-turn-routing,属既有 env 基线)⇒ 零回归
  • 反向变异:把 resolveCommandReal 退回 resolveCommand → 新测变红(1 failed / 406 passed),测试有牙 ✅
  • registry.ts 的改动核实为纯注释(剥掉注释行后与 master 逐字节相同);git diff --check 干净

再次感谢,这个 PR 的方向(把 spawn 路径和授权路径对齐)是对的,主要是想把「哪个平台、哪条路径」钉准,以及避免顺带改掉用户可见的路径字符串。以上均为自动评审的初步意见,最终以维护者审阅为准

@deepcoldy

Copy link
Copy Markdown
Owner

补充第二轮实测 —— 上一条评论里我有两处说错了,先更正,并把结论从「请确认平台」收紧为更明确的判据。 同样是自动评审的初步意见,最终以维护者审阅为准。

更正一:你描述的失败我复现出来了,我上一条说的「有 in-process 兜底、不是反复失败」是错的

用硬链接构造出「/proc/self/exe 指向的目录里没有 host」的场景(硬链接的 realpath 就是它自己,所以能绕过内核那次解析),配上 code_mode_host.disable_in_process_fallback=true,跑真实 turn:

ERROR codex_core::tools::router: error=failed to spawn code-mode host
  /tmp/hl/linkdir/codex-code-mode-host: No such file or directory (os error 2)
codex: The shell runner hit an environment error; I'll retry once.
ERROR codex_core::tools::router: error=failed to spawn code-mode host
  /tmp/hl/linkdir/codex-code-mode-host: No such file or directory (os error 2)

它确实会重试、确实连续失败,工具调用直接不可用 —— 和你 PR 描述的「辅助进程缺失并反复失败」逐字对上。所以这个失败模式是真的,我上一条把它说成「有兜底、不算反复失败」是我搞错了,抱歉。

更正二:macOS 上这个前提不成立 —— 因为 codex 自己会先 canonicalize

我上一条留了个问号说「macOS 的 _NSGetExecutablePath 不解软链接,所以那里可能真有问题」。这个方向对,但结论错了,因为 codex 在拿到 exe 路径之后自己又做了一次 canonicalize。syscall 级证据 —— 读完 /proc/self/exe 紧接着就是逐段 readlink 走整条路径(这就是 Rust fs::canonicalize 在 musl 上的形态):

readlink("/proc/self/exe", "/tmp/hl/realbin/codex", 256) = 21
readlink("/tmp", …)                    = -1 EINVAL
readlink("/tmp/hl", …)                 = -1 EINVAL
readlink("/tmp/hl/realbin", …)         = -1 EINVAL
readlink("/tmp/hl/realbin/codex", …)   = -1 EINVAL     ← 逐段解析 = fs::canonicalize

关键在于这次 canonicalize 与 current_exe() 是否解软链接无关:不管平台给的是软链接路径还是真实路径,codex 都会自己解析回真实目录再找 host。端到端验证(disable_in_process_fallback=true,host 放真实 bin 旁,软链接目录里没有):

execve("/tmp/hl/realbin/codex-code-mode-host", […]) = 0     ← 找到并成功拉起
PROBE_OK2

而且我特意按你说的 App bundle 形态又跑了一遍(Contents/Resources/{codex,codex-code-mode-host},故意不放 codex-package.json 以走非 standalone 的 fallback 分支,经软链接启动):

探测到: /tmp/hl/appb/Contents/Resources/codex-code-mode-host
输出:   APPBUNDLE_OK2       # 且全程 0 次 "failed to spawn code-mode host"

App bundle + PATH 软链接这个组合本身不会触发该 bug,因为 codex 内部那次 canonicalize 已经把目录纠正回 Contents/Resources

那么第一节里我复现出来的失败要满足什么条件?exe 的 canonical 路径所在目录里真的没有 host(我是用硬链接 + 只把 host 放在别处硬凑出来的)。这种布局下 resolveCommandReal() 返回的是那个没有 host 的目录 —— 所以本 PR 的改动对这个真实失败场景同样无效

所以结论收紧为

这个前提在任何平台都不成立,改动没有必要性(而不是我上一条说的「请确认是不是 macOS」):codex 自己 canonicalize,botmux 的 standalone / App bundle 两种安装形态都构造不出该失败;而能真触发失败的布局,这个改动也修不了。

同时上一条提的第二点仍然成立:resolvedBin 同时是展示 / 落盘 / 复制粘贴的字符串(cli-runtime-updates.jsonbinPath+updateCommand 会落盘并显示在 Dashboard 和 owner 卡片;Dashboard「复现命令」进剪贴板),改成版本钉死的 canonical 路径后,因为老 release 目录是保留的,升级后会静默跑旧版本而不是报错。canonical 形态本来就已经单独存在 installationPath 字段里了。

建议:这个 PR 可以先撤下或转成 draft。如果你在真实环境里确实遇到了 failed to spawn code-mode host,非常欢迎贴出报错原文 + codex --version + ls -la 你的 codex 安装目录(含 host 二进制位置) —— 从上面的分析看,真实原因更可能是那个安装里 host 二进制缺失或不在 canonical bin 目录旁,那是另一个问题,改 botmux 的路径解析修不掉。

再次感谢你把这个问题提出来 —— 排查过程本身很有价值(我自己上一轮也判错了两处,是按你的场景复现才纠正过来的)。以上仍为自动评审初步意见,最终以维护者审阅为准

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