逆向 Codex Computer Use,并把它封装成 MCP

软件
逆向 Codex Computer Use,并把它封装成 MCP

起点

最初的诉求很简单:搞清楚本机 Codex 的 computer use 是怎么封装和提供的。

我先按惯例去找它的工具实现,结果一路挖下去,最后变成了这样一条链路:逆向 Electron 应用 → 发现它是插件 → 确认插件在 Windows 上不发 → 从微软 Store 取包 → 拆开 MSIX → 摸清私有 npm 包 → 绕过宿主依赖 → 封装成自己的 MCP server。

这篇记录整个过程的思路和踩到的坑。

一、第一层:computer use 不是内核能力

在 codex.exe 里搜 computer_use,只搜到几个策略键:ComputerUseRequirements、allow_locked_computer_use。没有工具定义。这说明 computer use 不在 Rust 内核里,而是外挂的。

真正的位置在 app.asar 里。把 Electron 的 asar 解开后,搜 computer-use 会命中一段关键代码:

computerUse: false, computerUseNodeRepl: false

这两个是特性开关,两个都为 true 才启用。Windows 上还有额外的环境变量门:CODEX_ELECTRON_ENABLE_WINDOWS_COMPUTER_USE=1。

往下一层,找到了插件解析逻辑:它在 marketplace 里找一个 name 为 computer-use 且已安装、已启用的本地插件,然后从插件根目录推导路径,指向一个叫 @oai/sky 的私有 npm 包。

二、卡点:插件在 Windows 上根本不发

我查了三条渠道:应用内置捆绑市场只有 browser、chrome、latex;公开插件仓库 github.com/openai/plugins 的 57 个插件里没有;远程市场 openai-curated 的 65 个插件里也没有。

而且磁盘上 @oai/sky、codex-computer-use.exe、sky.node 一个都没有。

所以本机的 Codex 只为 macOS 打包了 computer-use。Windows 分支的代码在,二进制没发。

三、破局:找到官方更新渠道

去看更新机制,发现代码里写死了各渠道的分发方式:Prod 走 Microsoft Store,产品 ID 是 9PLM9XGG6VKS;PublicBeta 走 Store,9N8CJ4W95TBZ;Nightly 和 Owl 走 MSIX。

Windows 的更新器用的是 Windows.Services.Store 的 StorePackageUpdate API,而且有个硬约束:Windows updater requires packaged app identity。本机这份是从第三方站点下的免安装重打包版,没有包标识,官方更新器直接拒绝工作。

于是转向官方 Store 包本身。

四、取包:Store 不可用时的侧载路径

Store 用不了,就走"下载 MSIX + Add-AppxPackage 侧载"。

先查微软的目录 API 拿版本和包标识,返回的 PackageUri 只是占位主机名,真正的下载链接要走 FE3 交付 API。拿到之后得到 OpenAI.Codex_26.917.9434.0_x64__2p2nqsd0c76g0.msix,792 MB,比本机版本新约 4 个月。

下载后校验 SHA-1 一致,签名链是 Microsoft Marketplace CA,正牌 Store 通道。

先解包再决定装不装:MSIX 就是 zip,不需要安装就能看内容。搜了一下,新版确实带了 Windows 的 computer-use 载荷。

值得注意:新版把可执行文件改成了 app/ChatGPT.exe,Codex 桌面版已经并入 ChatGPT 客户端。

正式安装会失败——清单里有条 SYSTEM 级服务扩展,注册需要管理员权限。不想提权,就直接解包成便携版跑。

五、坑:MSIX 的 URL 编码路径

第一次解包后,@oai/sky 死活加载不了,报 Cannot find module ‘./$_StatsigGlobal’。

原因是 MSIX 把路径里的 @ 存成 %40、$ 存成 %24。真正的 AppX 部署会解码,而朴素的 zip 解包保留了编码名,于是 node_modules/@oai 变成了 node_modules/%40oai,模块解析全断。

重新解包时对每个路径做 URL 解码,1207 个路径得到修正,问题消失。

六、拆解:@oai/sky 的真实架构

接口文档给出的 API 面是 window2 风格,按窗口作用域,不是全局屏幕坐标:list_apps、list_windows、launch_app、get_window_state、click、press_key、type_text、scroll、set_value、drag、perform_secondary_action、activate_window。

设计上有两个值得说的点。

双寻址模式:每个动作既接受 element_index(来自无障碍树的元素索引),也接受窗口相对坐标。索引优先,坐标兜底。

感知默认只截图:get_window_state 默认 include_screenshot 为 true、include_text 为 false,返回 accessibility 为 null。要元素索引必须显式请求无障碍树。文档里明确写了"无障碍树弱的应用用默认值"。

实现栈是 SendInput + UI Automation + Windows.Graphics.Capture,截图即使在窗口被遮挡时也能拍到。

七、关键坑:sky 的模块加载期捕获

我一开始想直接 import { sky } from “@oai/sky” 用,结果在非 node_repl 环境里撞墙。

看 sky.js 的第一行就明白了:const n = globalThis.nodeRepl;

它在模块加载时捕获 globalThis.nodeRepl,然后据此选路径:nodeRepl 为 undefined 时走直连模式,自己 spawn helper;nodeRepl.rpc 是函数时走 node_repl RPC;nodeRepl 存在但没 rpc 时直接抛错。

我最初把钩子挂在 import 之前,正好踩中第三条。这点很隐蔽:sky 的行为取决于它在哪一刻被加载,而不是调用时传什么。

结论:在自己的进程里用,直接构造底层的 WindowsHelperTransport 更可控。

八、审批门:elicitations

第一次调 get_window_state 被拦:Computer Use requires app approval but elicitations are unavailable。

helper 强制要求宿主提供一个 elicitation 回调,否则任何触碰具体 app 的调用都被拒。默认路径是 globalThis.nodeRepl.config.createElicitation。

这解释了这套设计的安全模型:审批是逐应用的、且在执行中途反向发起。list_apps 这类只读调用不需要审批;一旦要碰某个 app,helper 就在调用过程中弹一次 Allow Codex to use X?。

顺带发现另一个约束:对浏览器目标,helper 还要先确定当前 URL 才能执行安全策略,确定不了就直接终止本次 computer use。这是刻意设计的浏览器策略闸门。

九、还有一层依赖:codex app-server

hook 挂好之后,又冒出新错误:failed to launch codex app-server: program not found。

codex-computer-use.exe 不是自足的——它是个瘦启动器,通过 CODEX_CLI_PATH 找到 codex CLI,拉起 codex app-server,用 JSON-RPC 跟它通信,然后才做实际的 Win32 自动化。

设置好这个环境变量后,链路就通了。

十、封装成 MCP

完整链路是:@oai/sky 的 WindowsHelperTransport → codex-computer-use.exe → codex app-server(经 CODEX_CLI_PATH 定位)→ UIA + SendInput + Windows.Graphics.Capture。

在这个之上封了一个 MCP server(手写 JSON-RPC over stdio,不引额外依赖),暴露 13 个工具,和 sky 的 API 面一一对应,外加一个 approval_log 用于审计。

几个实现要点:传输层单例复用,因为 helper 是常驻进程;窗口句柄按 app 和 window_id 传递,动作前用 get_window 重新水合,避免持有过期句柄;elicitation 钩子默认拒绝,CUA_AUTO_APPROVE=1 时放行,每次审批都写审计行;截图从 data URL 解出 base64,作为 MCP 的 image content block 返回。

验证结果:get_window_state 耗时 339ms,无障碍树 252 字符,截图 1440x853。

十一、注册

写进用户级配置后,claude mcp list 显示已连接。

小结

回头看,这件事的难点不在写代码,而在三个信息缺口:不知道 computer use 是插件,所以一直在内核里找;不知道 MSIX 会 URL 编码路径,所以模块解析一直失败;不知道 sky.js 在模块加载期捕获全局状态,所以钩子挂在错误的时机。

每个缺口都表现为一个看似无关的报错。解决它们靠的不是猜,而是回到产物本身:读 asar 目录头、读 zip 的中央目录、读被压缩的 JS 里的那一行 const n = globalThis.nodeRepl。

一个附带结论:官方把 Windows 的 computer use 藏在一个只随安装包分发的插件里,公网 npm 拿不到,插件仓库也没有。