Repository navigation
[Bug][Windows launcher] 经 codex.cmd 启动破坏 argv:嵌入引号拆分、%变量% 展开和 & 被解释 #2196
Description
问题
Windows 默认 npm 安装的 Codex 经 codex.cmd 启动时,bili 没有完整保留用户 argv:含嵌入双引号的提示词被拆分,TOML -c 参数丢引号,%COMSPEC% 被展开,& 被作为 cmd 语法处理。
普通带空格提示词可以工作,因此已有“空格路径修复”不能证明参数透传正确。对真实 codex.exe 的对照已确认同一提示词完整到达客户端。
环境
Windows 11,Node.js 24.21.0,Codex CLI 0.160.0,billion-context 0.1.185;源码基线 e5a0066de8705dd7d38e5e9b5ed8a1085b7f720c。使用未修改的已发布 dist,隔离 CODEX_HOME / XDG,禁自动更新。本地 mock Responses upstream,无付费模型调用。
实测结果
为避免外层 shell 先改变参数,使用 Python subprocess.run([...], shell=False) 直接启动 node <published bili dist/index.js> codex -- <args>。argv 捕获 fixture 是行为等同 npm shim 的 .cmd:"node.exe" "capture.mjs" %*;capture 用 process.argv.slice(2) 记录接收到的数组,并在 --help 时声明支持 --no-daemon。
以下省略 bili 自动添加的 --no-daemon:
| 输入 argv | 捕获的 argv / 实际行为 |
|---|---|
['exec', 'hello world'] |
['exec', 'hello world'],对照成功 |
['exec', 'Please say "hello world" exactly'] |
['exec', 'Please', 'say', 'hello world', 'exactly'] |
['-c', 'model_providers.demo.name="My Provider"', 'exec', 'hello'] |
['-c', 'model_providers.demo.name=My Provider', 'exec', 'hello'],TOML 引号消失 |
['exec', 'literal %COMSPEC% marker'] |
百分号变量被替换为本机 cmd.exe 路径 |
['exec', 'hello&rem'] |
只收到 ['exec', 'hello'];rem 被 cmd 当作第二条命令(无害复现) |
真实 Codex 对照:
bili + 默认 codex.cmd + 提示词 Please say "hello world" exactly
→ exit 2: error: unexpected argument 'say' found
bili + BILI_CLIENT_BIN=实际 codex.exe + 完全相同 argv
→ exit 0,Codex user 输入完整保留 Please say "hello world" exactly
因此不是 Codex 不接受双引号,而是 bili 的 Windows shim 启动路径改变了 argv。&rem 只证明参数被当作 cmd 语法;本次未执行破坏性命令。若其他程序把未经信任的文本作为 launcher 参数传入,该机制还可能产生命令解释风险。
根因与测试缺口
- src/launcher.ts:4066
quoteWinToken()只给“有空白且没有双引号”的 token 加引号。嵌入引号、无空白的 cmd 元字符、空字符串没有可靠保护。 - L4087 把
.cmd/.bat和无扩展名客户端拼成cmd.exe /d /s /c字符串,设置windowsVerbatimArguments:true;npm shim 随后再用%*转发。 - L4128 的 Windows 解析优先
.cmd。 - tests/fix-679-windows-launch.test.ts:33 明确把“embedded double quotes stay bare”作为正确期望;L24 同样固定空参数变成空字符串的拼接结果。它们验证实现输出,没有验证子进程最终收到的 argv。
- 真实 Windows 子进程测试覆盖普通空格路径和直接
.exe,缺少实际.cmd+ 双引号/百分号/元字符往返。
修复建议
- Codex 路径优先绕开 shell:识别可信的官方 npm 安装布局,用真实 Node 执行官方
@openai/codex/bin/codex.js,或定位匹配平台的官方原生codex.exe。保持 argv 数组,由 Node 的直接 spawn 处理 Windows 参数;官方 JS wrapper 可保留其 vendor/PATH 初始化行为。 - 对任意用户自定义
.cmd/.bat不应猜测或任意解析脚本内容。明确该路径的透传契约;如仍支持 cmd fallback,需经真实 cmd/shim 往返证明,无法无损传递时明确报错并提示选择原生/JS 入口。 - 不要只删除用户引号、再给所有 token 加一层引号,或转移本例一个
-c到配置文件就宣称解决通用 argv 问题。根因是把用户 argv 重新送入 shell 语法解释。 BILI_CLIENT_BIN=<实际 codex.exe>已在本次实测中验证可绕过这个问题,可作为临时解决办法;具体路径随安装方式变化。
建议回归与验收
- 真实 Windows
.cmdfixture 和官方 Codex wrapper 两条路径,断言子进程 argv 与调用方数组逐项相等。 - 覆盖空参数、普通空白、嵌入双引号、TOML/JSON、Unicode、末尾反斜杠、
%VAR%、!VAR!、&|<>^();包含空格的客户端/Node路径仍工作。 - 用无害 marker 检查 shell 元字符不产生额外命令或文件副作用。
- 真实
bili codex exec对带引号提示词成功;对合法字符串-c使用客户端配置读取结果验证,不仅检查拼接文本。
历史关联:#679 / #682 修复空格截断,#681 / #683 将 bili 自己的 MCP 配置移至 overlay,#1292 / #1293 对 Claude JSON 参数做文件载体处理。本报告跟踪用户通用参数在当前 Codex launcher 路径上的剩余损坏。
Activity
ranxianglei commentedon Oct 5, 2026
已修复并开 PR:**#2205 #2196)。基线 e5a0066(与你的源码基线一致),分支 2026-10-06_win-cmd-argv-fix。
结构化七问
codex.cmdshim,bili 经它启动时用户 argv 被 cmd.exe 行解析器重新解释——嵌入双引号拆词、TOML-c丢引号、%VAR%展开、&|<>^()执行命令语法、空参数消失。带空格的普通提示词恰好能幸存,所以 [bug] Windows: a space in the client or -e path truncates the launch (spawn shell:true, unquoted) — and silently kills compression when only the command is quoted #679 的空格修复掩盖了它。planClientSpawn把.cmd/.bat/无扩展名客户端拼成comspec /d /s /c "<line>"字符串,cmd.exe 的行解析器没有任何转义机制,quoteWinToken的"有空白才加引号"策略对嵌入引号/元字符/百分号/空串都无保护。你对照实验(BILI_CLIENT_BIN 指向真实 codex.exe 即完整)直接证明了损坏点在 bili 的 shim 启动路径而非 Codex 侧。codex.exe优先于 shim;.cmd/.bat命中若位于可信 npm 布局旁(<dir>/node_modules/@openai/codex,package.json name 为@openai/codex、有可解析的"codex"bin 条目、且 shim 文本引用该包——手写 shim 放在无关树旁不会劫持启动),则升级为node <官方 bin/codex.js>。这完整保留官方 wrapper 的 vendor 二进制查找/env 初始化/信号转发行为,而 Node 自己的 CreateProcess argv 编码逐字无损携带参数。未识别布局(yarn-classic.bin、无本地链接的 pnpm store shim)保留旧 cmd 路径并受下述契约约束。BILI_CLIENT_BIN覆盖优先级不变。planClientSpawn与 dsh-channel 的planDshSpawn(同一机制族)在拼行前对每个 token(含命令本身)做安全集校验(src/win-cmd.ts);不安全 token 在任何进程启动之前抛出带位置+类别+可操作提示(BILI_CLIENT_BIN/BILI_DSH_BIN)的错误。直接 spawn 的.exe不受影响(Node 自身无损编码 argv)。错误信息只说位置和类别、绝不回显 token 内容(prompt 常含密钥,日志脱敏纪律)。resolveClientCommand新增可选 platform 参数(默认 process.platform),既有调用点零改动。本地定向验证:typecheck 干净,新测试文件+fix-679+dsh-refresh+dsh-native+fix-1292 共 108 过 0 挂,launcher.test.ts 269 过 0 挂;全量套件与真实 Windows 往返交给 CI。有意的行为变更(旧→新):① 不安全 argv 从静默损坏/解释 → 启动前明确报错退出(所有客户端的 .cmd/.bat/无扩展名路径 + dsh channel);② codex Windows 同目录解析顺序 .cmd/.bat/.exe → .exe/.cmd/.bat;③ npm shim 启动从 bili→cmd→codex.cmd→node→codex.js 变为 bili→node→codex.js(少一层重解析点)。原因见 PR 描述表格。kernel/,不动 update.ts。BILI_CLIENT_BIN是既有旋钮(语义与优先级不变),现作为 power-user 绕行手段写入 CLIENTS.md(中英同步)。验收覆盖(对应你列的建议):windows-latest CI 新增硬门禁跑
tests/win-cmd-argv.test.ts——子进程实际收到的 argv 与调用方数组逐项相等(不是检查拼接文本),语料含空参数、普通空格、嵌入双引号、TOML-c、JSON、Unicode、末尾反斜杠(奇/偶)、%COMSPEC%、!VAR!、&|<>^();两条路径都测(官方 wrapper 形态入口 + 直接 spawn 腿);并对真实全局@openai/codex安装做布局检测断言 + wrapper 经纯 node 启动--help成功。元字符用例均为无害 marker,无副作用断言依赖。待 CI 全绿后由你合并。临时绕行:
BILI_CLIENT_BIN=<实际 codex.exe>(你已验证有效,合并后仍可用)。test: fix eager null-stderr interpolation in the wrapper --help probe…
ranxianglei commentedon Oct 5, 2026
补充:第一轮 CI 的 windows 硬门禁红了一次——核心断言全部通过(真实 Windows 上完整语料往返、直接 spawn 腿、真实全局
@openai/codex布局检测三项全绿),挂的是我新增的 wrapper--help探测测试自身的断言写法错误(stderr 设为 ignore 后为 null,错误消息模板在状态判断前就求值了)。已修(3b28155)并重跑,非产品代码变更。ranxianglei commentedon Oct 5, 2026
CI 全绿确认:最终提交 3b28155 上全部检查通过——含 ubuntu/windows 双版本全量测试矩阵、windows codex 硬门禁(真实
@openai/codex布局检测 + 完整语料 argv 往返)、全部 e2e lane 与 guard。PR #2205 可以合并(Closes #2196)。Merge pull request #2205 from ranxianglei/2026-10-06_win-cmd-argv-fix
fix: keep user argv out of cmd.exe on the Windows launcher path (ranx…
test(launcher): opencode-inline spawn assertion is platform-aware — w…