From 5b340821062a345271a6a60b362a76ed14c369b4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 05:01:36 +0100 Subject: [PATCH] fix(codex): resolve Windows app-server shims --- CHANGELOG.md | 1 + .../src/app-server/transport-stdio.test.ts | 72 +++++++++++++++++++ .../codex/src/app-server/transport-stdio.ts | 45 +++++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 extensions/codex/src/app-server/transport-stdio.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1eed1c751..9de38895272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions. +- Codex harness/Windows: resolve npm-installed `codex.cmd` shims through PATHEXT before starting the native app-server, so `codex/*` models work without a manual `.exe` shim. Fixes #70913. - Slack/groups: classify MPIM group DMs as group chat context and suppress verbose tool/plan progress on Slack non-DM surfaces, so internal "Working…" traces no longer leak into rooms. Fixes #70912. - Agents/replay: stop OpenAI/Codex transcript replay from synthesizing missing tool results while still preserving synthetic repair on Anthropic, Gemini, and Bedrock transport-owned sessions. (#61556) Thanks @VictorJeon and @vincentkoc. - Telegram/media replies: parse remote markdown image syntax into outbound media payloads on the final reply path, so Telegram group chats stop falling back to plain-text image URLs when the model or a tool emits `![...](...)` instead of a `MEDIA:` token. (#66191) Thanks @apezam and @vincentkoc. diff --git a/extensions/codex/src/app-server/transport-stdio.test.ts b/extensions/codex/src/app-server/transport-stdio.test.ts new file mode 100644 index 00000000000..b4e61bd2da2 --- /dev/null +++ b/extensions/codex/src/app-server/transport-stdio.test.ts @@ -0,0 +1,72 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import type { CodexAppServerStartOptions } from "./config.js"; +import { resolveCodexAppServerSpawnInvocation } from "./transport-stdio.js"; + +const tempDirs: string[] = []; + +async function createTempDir(): Promise { + const dir = await mkdtemp(path.join(os.tmpdir(), "openclaw-codex-spawn-")); + tempDirs.push(dir); + return dir; +} + +afterEach(async () => { + for (const dir of tempDirs.splice(0)) { + await rm(dir, { recursive: true, force: true }); + } +}); + +function startOptions(command: string): CodexAppServerStartOptions { + return { + transport: "stdio", + command, + args: ["app-server", "--listen", "stdio://"], + headers: {}, + }; +} + +describe("resolveCodexAppServerSpawnInvocation", () => { + it("keeps non-Windows Codex app-server invocation unchanged", () => { + const resolved = resolveCodexAppServerSpawnInvocation(startOptions("codex"), { + platform: "darwin", + env: {}, + execPath: "/usr/local/bin/node", + }); + + expect(resolved).toEqual({ + command: "codex", + args: ["app-server", "--listen", "stdio://"], + shell: undefined, + windowsHide: undefined, + }); + }); + + it("resolves Windows npm .cmd Codex shims through Node instead of raw spawn", async () => { + const binDir = await createTempDir(); + const entryPath = path.join(binDir, "node_modules", "@openai", "codex", "bin", "codex.js"); + const shimPath = path.join(binDir, "codex.cmd"); + await mkdir(path.dirname(entryPath), { recursive: true }); + await writeFile(entryPath, "console.log('codex')\n", "utf8"); + await writeFile( + shimPath, + '@ECHO off\r\n"%~dp0\\node_modules\\@openai\\codex\\bin\\codex.js" %*\r\n', + "utf8", + ); + + const resolved = resolveCodexAppServerSpawnInvocation(startOptions("codex"), { + platform: "win32", + env: { PATH: binDir, PATHEXT: ".CMD;.EXE;.BAT" }, + execPath: "C:\\node\\node.exe", + }); + + expect(resolved).toEqual({ + command: "C:\\node\\node.exe", + args: [entryPath, "app-server", "--listen", "stdio://"], + shell: undefined, + windowsHide: true, + }); + }); +}); diff --git a/extensions/codex/src/app-server/transport-stdio.ts b/extensions/codex/src/app-server/transport-stdio.ts index 61b1ed6ae50..129b34e870a 100644 --- a/extensions/codex/src/app-server/transport-stdio.ts +++ b/extensions/codex/src/app-server/transport-stdio.ts @@ -1,7 +1,43 @@ import { spawn } from "node:child_process"; +import { + materializeWindowsSpawnProgram, + resolveWindowsSpawnProgram, +} from "openclaw/plugin-sdk/windows-spawn"; import type { CodexAppServerStartOptions } from "./config.js"; import type { CodexAppServerTransport } from "./transport.js"; +type CodexAppServerSpawnRuntime = { + platform: NodeJS.Platform; + env: NodeJS.ProcessEnv; + execPath: string; +}; + +const DEFAULT_SPAWN_RUNTIME: CodexAppServerSpawnRuntime = { + platform: process.platform, + env: process.env, + execPath: process.execPath, +}; + +export function resolveCodexAppServerSpawnInvocation( + options: CodexAppServerStartOptions, + runtime: CodexAppServerSpawnRuntime = DEFAULT_SPAWN_RUNTIME, +): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } { + const program = resolveWindowsSpawnProgram({ + command: options.command, + platform: runtime.platform, + env: runtime.env, + execPath: runtime.execPath, + packageName: "@openai/codex", + }); + const resolved = materializeWindowsSpawnProgram(program, options.args); + return { + command: resolved.command, + args: resolved.argv, + shell: resolved.shell, + windowsHide: resolved.windowsHide, + }; +} + export function createStdioTransport(options: CodexAppServerStartOptions): CodexAppServerTransport { const env = { ...process.env, @@ -10,9 +46,16 @@ export function createStdioTransport(options: CodexAppServerStartOptions): Codex for (const key of options.clearEnv ?? []) { delete env[key]; } - return spawn(options.command, options.args, { + const invocation = resolveCodexAppServerSpawnInvocation(options, { + platform: process.platform, + env, + execPath: process.execPath, + }); + return spawn(invocation.command, invocation.args, { env, detached: process.platform !== "win32", + shell: invocation.shell, stdio: ["pipe", "pipe", "pipe"], + windowsHide: invocation.windowsHide, }); }