diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 9226cf47..1878ab3a 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -80,6 +80,20 @@ export interface CodexCommand { export type ProcessEnvironment = Record; +function environmentValue( + environment: ProcessEnvironment, + requested: string, +): string | undefined { + const exact = environment[requested]?.trim(); + if (exact) return exact; + return Object.entries(environment) + .find( + ([name, value]) => + name.toUpperCase() === requested.toUpperCase() && value?.trim(), + )?.[1] + ?.trim(); +} + export interface PluginPythonOptions { configuredPath?: string; environment?: ProcessEnvironment; @@ -100,18 +114,10 @@ export interface WorkbenchCommandOptions { export function codexSecurityStateDirectory( environment: ProcessEnvironment = process.env, ): string { - const environmentValue = (requested: string): string | undefined => { - const exact = environment[requested]?.trim(); - if (exact) return exact; - return Object.entries(environment) - .find( - ([name, value]) => name.toUpperCase() === requested && value?.trim(), - )?.[1] - ?.trim(); - }; - const configured = environmentValue("CODEX_SECURITY_STATE_DIR"); + const configured = environmentValue(environment, "CODEX_SECURITY_STATE_DIR"); if (configured !== undefined) return resolve(expandHome(configured)); - const codexHome = environmentValue("CODEX_HOME") ?? join(homedir(), ".codex"); + const codexHome = + environmentValue(environment, "CODEX_HOME") ?? join(homedir(), ".codex"); return resolve(expandHome(codexHome), "state", "plugins", "codex-security"); } @@ -2293,11 +2299,34 @@ export function pluginExecutionEnvironment( return { ...environment, PYTHON: python, - CODEX_CLI_PATH: - environment["CODEX_CLI_PATH"]?.trim() || resolveCodexCommand().command, + CODEX_CLI_PATH: resolveNestedCodexPath(environment), }; } +export function resolveNestedCodexPath( + environment: ProcessEnvironment = process.env, + platform: NodeJS.Platform = process.platform, +): string { + const configured = environmentValue(environment, "CODEX_CLI_PATH"); + if (configured !== undefined && isSpawnableCodexPath(configured, platform)) { + return configured; + } + return resolveCodexCommand().command; +} + +function isSpawnableCodexPath( + value: string, + platform: NodeJS.Platform, +): boolean { + if (platform !== "win32") return true; + // CodexExec passes this path directly to spawn() without a shell. Windows + // cannot execute npm's extensionless/.cmd shims there, and MSIX package + // executables under WindowsApps can be denied to spawned MCP processes. + const windowsPath = value.replaceAll("/", "\\"); + if (/(?:^|\\)windowsapps(?:\\|$)/iu.test(windowsPath)) return false; + return [".exe", ".com"].includes(extname(value).toLowerCase()); +} + export async function cleanupSdkDirectory(path: string): Promise { await rm(path, { recursive: true, force: true }); } diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index a4780903..433d67b7 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -68,6 +68,7 @@ import { requireSecureCredentialHome, requireSecureOutputAncestry, requireTrustedOutputAncestor, + resolveNestedCodexPath, runWorkbench, setCodexSecurityCredentialLogout, streamWindowsCredentialAclDescriptors, @@ -1696,7 +1697,11 @@ describe("plugin runtime preparation", () => { }); test("preserves an explicit Codex executable override for nested workers", () => { - const configured = join(tmpdir(), "custom codex", "codex"); + const configured = join( + tmpdir(), + "custom codex", + process.platform === "win32" ? "codex.exe" : "codex", + ); expect( pluginExecutionEnvironment("/managed/python", { @@ -1715,6 +1720,27 @@ describe("plugin runtime preparation", () => { ).toBe(resolveCodexCommand().command); }); + test("keeps only spawnable Windows Codex overrides for nested workers", () => { + const fallback = resolveCodexCommand().command; + const executable = + "C:\\Users\\alice\\AppData\\Local\\OpenAI\\Codex\\bin\\0.146.0\\codex.exe"; + + expect( + resolveNestedCodexPath({ Codex_Cli_Path: ` ${executable} ` }, "win32"), + ).toBe(executable); + + for (const unusable of [ + "C:\\Users\\alice\\AppData\\Roaming\\npm\\codex", + "C:\\Users\\alice\\AppData\\Roaming\\npm\\codex.cmd", + "C:\\Program Files\\WindowsApps\\OpenAI.Codex_1\\app\\resources\\codex.exe", + " ", + ]) { + expect( + resolveNestedCodexPath({ CODEX_CLI_PATH: unusable }, "win32"), + ).toBe(fallback); + } + }); + test("selects the native Windows Codex executable package", () => { expect(codexPlatformPackage("win32", "x64")).toEqual({ packageName: "@openai/codex-win32-x64",