Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ export interface CodexCommand {

export type ProcessEnvironment = Record<string, string | undefined>;

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;
Expand All @@ -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");
}

Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve current main's absolute-path normalization when resolving this conflict. pluginExecutionEnvironment() now delegates to resolveCodexCommand(environment), which turns a relative CODEX_CLI_PATH into an absolute path before passing it to a nested worker. This helper instead returns the trimmed relative value unchanged on non-Windows platforms, and does the same for a relative .exe or .com path on Windows. The worker can then resolve that value from a different working directory and fail to launch Codex.

On the conflict-resolved replay at 216212b7, resolveNestedCodexPath({ CODEX_CLI_PATH: "./bin/codex" }, "linux") returned "./bin/codex" instead of resolve("./bin/codex"). Normalizing an accepted override with the platform-specific path resolver made this reproduction and all four focused launcher tests pass. Please retain that normalization and add relative-path coverage for accepted non-Windows and Windows executables.

}
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<void> {
await rm(path, { recursive: true, force: true });
}
Expand Down
28 changes: 27 additions & 1 deletion sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
requireSecureCredentialHome,
requireSecureOutputAncestry,
requireTrustedOutputAncestor,
resolveNestedCodexPath,
runWorkbench,
setCodexSecurityCredentialLogout,
streamWindowsCredentialAclDescriptors,
Expand Down Expand Up @@ -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", {
Expand All @@ -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",
Expand Down
Loading