Skip to content

[Bug][Windows launcher] 经 codex.cmd 启动破坏 argv:嵌入引号拆分、%变量% 展开和 & 被解释 #2196

Closed

Description

@xiaozhu1337

问题

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 + 双引号/百分号/元字符往返。

修复建议

  1. Codex 路径优先绕开 shell:识别可信的官方 npm 安装布局,用真实 Node 执行官方 @openai/codex/bin/codex.js,或定位匹配平台的官方原生 codex.exe。保持 argv 数组,由 Node 的直接 spawn 处理 Windows 参数;官方 JS wrapper 可保留其 vendor/PATH 初始化行为。
  2. 对任意用户自定义 .cmd/.bat 不应猜测或任意解析脚本内容。明确该路径的透传契约;如仍支持 cmd fallback,需经真实 cmd/shim 往返证明,无法无损传递时明确报错并提示选择原生/JS 入口。
  3. 不要只删除用户引号、再给所有 token 加一层引号,或转移本例一个 -c 到配置文件就宣称解决通用 argv 问题。根因是把用户 argv 重新送入 shell 语法解释。
  4. BILI_CLIENT_BIN=<实际 codex.exe> 已在本次实测中验证可绕过这个问题,可作为临时解决办法;具体路径随安装方式变化。

建议回归与验收

  • 真实 Windows .cmd fixture 和官方 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

  1. ranxianglei commented on Oct 5, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    已修复并开 PR:**#2205 #2196)。基线 e5a0066(与你的源码基线一致),分支 2026-10-06_win-cmd-argv-fix。

    结构化七问

    1. 是什么问题:Windows 默认 npm 安装的 Codex 是 codex.cmd shim,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 的空格修复掩盖了它。
    2. 什么原因导致的:根因是把用户 argv 重新送入 shell 语法解释——planClientSpawn 把 .cmd/.bat/无扩展名客户端拼成 comspec /d /s /c "<line>" 字符串,cmd.exe 的行解析器没有任何转义机制,quoteWinToken 的"有空白才加引号"策略对嵌入引号/元字符/百分号/空串都无保护。你对照实验(BILI_CLIENT_BIN 指向真实 codex.exe 即完整)直接证明了损坏点在 bili 的 shim 启动路径而非 Codex 侧。
    3. 怎么修复的:两条互补措施,都是移除产生缺陷的机制而不是拦截症状:
      • codex 路径绕开 shell(仅 win32):每个 PATH 目录内原生 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 覆盖优先级不变。
      • 其余所有 comspec 包装点改为明确拒绝:launcher 的 planClientSpawn 与 dsh-channel 的 planDshSpawn(同一机制族)在拼行前对每个 token(含命令本身)做安全集校验(src/win-cmd.ts);不安全 token 在任何进程启动之前抛出带位置+类别+可操作提示(BILI_CLIENT_BIN/BILI_DSH_BIN)的错误。直接 spawn 的 .exe 不受影响(Node 自身无损编码 argv)。错误信息只说位置和类别、绝不回显 token 内容(prompt 常含密钥,日志脱敏纪律)。
    4. 是否有回归问题:审过既有行为——[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 安全 argv 的拼接结果逐字节不变(launcher 与 dsh 两侧都按字面量钉死回归测试);fix-1292 的 Claude settings 文件路径往返、dsh profile 参数、pi/opencode 等非 codex 客户端解析全部原样通过;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 描述表格。
    5. 是否破坏缓存:不涉及——不触碰 wire 格式、persistence、会话绑定、重试与任何缓存/去重键。
    6. 是否改了设计方向:不涉及架构转向;是在既有 [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 spawn 契约上收紧(疏:让 argv 不再进入 shell 解释,而不是堵:过滤/转义 payload)。未改 kernel/,不动 update.ts。
    7. 是否引入新的配置:没有新增配置面。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>(你已验证有效,合并后仍可用)。

  2. ranxianglei commented on Oct 5, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    补充:第一轮 CI 的 windows 硬门禁红了一次——核心断言全部通过(真实 Windows 上完整语料往返、直接 spawn 腿、真实全局 @openai/codex 布局检测三项全绿),挂的是我新增的 wrapper --help 探测测试自身的断言写法错误(stderr 设为 ignore 后为 null,错误消息模板在状态判断前就求值了)。已修(3b28155)并重跑,非产品代码变更。

  3. ranxianglei commented on Oct 5, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    CI 全绿确认:最终提交 3b28155 上全部检查通过——含 ubuntu/windows 双版本全量测试矩阵、windows codex 硬门禁(真实 @openai/codex 布局检测 + 完整语料 argv 往返)、全部 e2e lane 与 guard。PR #2205 可以合并(Closes #2196)。

  4. added a commit that references this issue on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions