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
6 changes: 4 additions & 2 deletions plugins/codex-security/scripts/launch_codex_security_mcp.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ if defined CODEX_BROWSER_USE_NODE_PATH if exist "%CODEX_BROWSER_USE_NODE_PATH%"
if defined CODEX_ELECTRON_RESOURCES_PATH if exist "%CODEX_ELECTRON_RESOURCES_PATH%\cua_node\bin\node.exe" ("%CODEX_ELECTRON_RESOURCES_PATH%\cua_node\bin\node.exe" "%CODEX_SECURITY_MCP_SCRIPT%" %* & exit)
if defined CODEX_CLI_PATH for %%I in ("%CODEX_CLI_PATH%") do if exist "%%~dpIcua_node\bin\node.exe" ("%%~dpIcua_node\bin\node.exe" "%CODEX_SECURITY_MCP_SCRIPT%" %* & exit)

where node >nul 2>&1
if not errorlevel 1 (node "%CODEX_SECURITY_MCP_SCRIPT%" %* & exit)
rem Resolve Node from PATH once and launch that exact executable.
set "CODEX_SECURITY_MCP_SEARCH_PATH=%PATH:"=%"
for %%I in (node.exe) do set "CODEX_SECURITY_MCP_NODE=%%~$CODEX_SECURITY_MCP_SEARCH_PATH:I"
if defined CODEX_SECURITY_MCP_NODE ("%CODEX_SECURITY_MCP_NODE%" "%CODEX_SECURITY_MCP_SCRIPT%" %* & exit)

echo Codex Security could not find a Node runtime. Reinstall or update Codex, or set CODEX_MCP_NODE_PATH to an executable Node runtime. 1>&2
exit /b 127
104 changes: 103 additions & 1 deletion sdk/typescript/tests-ts/mcp-launcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,119 @@
import { spawnSync } from "node:child_process";
import {
chmod,
copyFile,
mkdir,
mkdtemp,
readFile,
realpath,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { join, parse } from "node:path";
import { expect, test } from "bun:test";
import { PLUGIN_ROOT } from "./plugin-root.js";

test.skipIf(process.platform !== "win32")(
"launches the PATH Node executable independently of command extensions and the caller directory",
async () => {
const node = Bun.which("node");
if (node === null)
throw new Error("Node is required for the launcher test.");
const root = await realpath(
await mkdtemp(join(tmpdir(), "codex-security-launch-")),
);
try {
const caller = join(root, "caller");
const runtime = join(root, "Node runtime");
const scripts = join(root, "plugin", "scripts");
const server = join(root, "plugin", "mcp", "server.mjs");
await Promise.all(
[caller, runtime, scripts, join(root, "plugin", "mcp")].map(
(directory) => mkdir(directory, { recursive: true }),
),
);
const launcher = join(scripts, "launch_codex_security_mcp.cmd");
await copyFile(
join(PLUGIN_ROOT, "scripts", "launch_codex_security_mcp.cmd"),
launcher,
);
await copyFile(node, join(runtime, "node.exe"));
await writeFile(
server,
`console.log(JSON.stringify({ executable: process.execPath, cwd: process.cwd(), args: process.argv.slice(2) }));\nprocess.exitCode = 23;\n`,
);
const marker = join(root, "command-used");
for (const directory of [caller, runtime]) {
for (const name of ["node.cmd", "where.cmd"]) {
await writeFile(
join(directory, name),
`@echo off\n> "${marker}" echo used\nexit /b 0\n`,
);
}
}
// Native candidates in the caller directory must not participate either.
await copyFile(node, join(caller, "node.exe"));
await copyFile(node, join(caller, "where.exe"));
for (const path of [
runtime,
`"${runtime}"`,
`;.;;relative-bin;${runtime};`,
]) {
const result = spawnSync(
join(process.env["SystemRoot"]!, "System32", "cmd.exe"),
["/d", "/s", "/c", `""${launcher}" --stdio "argument with spaces""`],
{
cwd: caller,
env: {
SystemRoot: process.env["SystemRoot"],
PATH: path,
PATHEXT: ".CMD;.EXE;.BAT;.COM",
},
encoding: "utf8",
windowsHide: true,
windowsVerbatimArguments: true,
},
);
expect(
result.status,
`PATH=${path}\n${result.stderr || result.error?.message || ""}`,
).toBe(23);
expect(JSON.parse(result.stdout)).toEqual({
executable: join(runtime, "node.exe"),
cwd: parse(launcher).root,
args: ["--stdio", "argument with spaces"],
});
await expect(readFile(marker)).rejects.toMatchObject({
code: "ENOENT",
});
}
const missing = spawnSync(
join(process.env["SystemRoot"]!, "System32", "cmd.exe"),
["/d", "/s", "/c", `""${launcher}" --stdio"`],
{
cwd: caller,
env: {
SystemRoot: process.env["SystemRoot"],
PATH: ";.;relative-bin;",
},
encoding: "utf8",
windowsHide: true,
windowsVerbatimArguments: true,
},
);
expect(missing.status, missing.stderr || missing.error?.message).toBe(
127,
);
expect(missing.stdout).toBe("");
expect(missing.stderr).toContain("could not find a Node runtime");
await expect(readFile(marker)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await rm(root, { recursive: true, force: true });
}
},
);

test("starts the packaged MCP server with managed Node and an empty PATH", async () => {
const node = Bun.which("node");
if (node === null)
Expand Down
Loading