diff --git a/plugins/codex-security/mcp-app/tests/test_deep_scan_executor.ts b/plugins/codex-security/mcp-app/tests/test_deep_scan_executor.ts index ee1e5351e4..fa364131b5 100644 --- a/plugins/codex-security/mcp-app/tests/test_deep_scan_executor.ts +++ b/plugins/codex-security/mcp-app/tests/test_deep_scan_executor.ts @@ -1457,7 +1457,7 @@ async function testWorkerRuntimeSettings() { "GIT_SSH_COMMAND", "GIT_CONFIG_GLOBAL", "CODEX_CLI_PATH", - "CODEX_MCP_NODE_PATH", + "CODEX_BROWSER_USE_NODE_PATH", "CODEX_HOME", "CODEX_SECURITY_STATE_DIR", "CODEX_SECURITY_CONFIG_PATH", @@ -1498,6 +1498,8 @@ async function testWorkerRuntimeSettings() { { account: null, requiresOpenaiAuth: false }, process.platform === "win32" && configuration === "", ); + const browserNode = path.join(fixture.root, "browser node"); + process.env.CODEX_BROWSER_USE_NODE_PATH = browserNode; const python = path.join(fixture.root, "selected venv", "bin", "python"); const helperPython = path.join( fixture.root, @@ -1749,6 +1751,10 @@ env_key = "SYNTHETIC_GATEWAY_KEY"`, ? undefined : { ...workerScanEnvironment(fixture.root, index), + CODEX_BROWSER_USE_NODE_PATH: path.join( + fixture.root, + `scan browser node ${index}`, + ), SYNTHETIC_GATEWAY_KEY: providerKeys[index], SYNTHETIC_HEADER_VALUE: providerHeaders[index], [headerKey]: ` synthetic-canonical-header-${index}\t`, @@ -2278,6 +2284,11 @@ env_key = "SYNTHETIC_GATEWAY_KEY"`, nodePaths[index], ); assert.equal(process.env.CODEX_MCP_NODE_PATH, nodePaths.at(-1)); + assert.equal( + invocation.runtimeEnvironment.CODEX_BROWSER_USE_NODE_PATH, + selectedProvider.environment?.CODEX_BROWSER_USE_NODE_PATH ?? + browserNode, + ); assert.equal(invocation.providerHeader, providerHeaders[index]); assert.equal(workerLaunch.environment!.CODEX_API_KEY, undefined); diff --git a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts index 51fe18eafc..2bc2b2f173 100644 --- a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts +++ b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; import { copyFile, mkdir, @@ -247,6 +248,72 @@ launcherTest( }, ); +test( + "Linux helpers use a system SQLite fallback without probing the checkout PATH", + { skip: process.platform !== "linux" }, + async () => { + const root = await directories.create("codex-security-linux-launcher-"); + const withoutSqlite = path.join(root, "node-without-sqlite"); + const marker = path.join(root, "path-node-ran"); + await writeFile( + path.join(root, "node"), + `#!/bin/sh\n: > ${shellQuote(marker)}\nexit 0\n`, + { mode: 0o755 }, + ); + await writeFile( + withoutSqlite, + `#!/bin/sh\nexec ${shellQuote(process.execPath)} --no-experimental-sqlite "$@"\n`, + { mode: 0o755 }, + ); + const launcher = path.join( + pluginRoot, + "scripts", + "launch_codex_security_mcp", + ); + const installedNode = path.join(root, "installed-node"); + const systemLink = path.join(root, "system Node's link"); + await writeFile( + installedNode, + `#!/bin/sh\nexec ${shellQuote(process.execPath)} "$@"\n`, + { mode: 0o755 }, + ); + await symlink(installedNode, systemLink); + const source = await readFile(launcher, "utf8"); + // Relocate only the fixed system paths so the test can supply a system symlink. + const systemPaths = "/usr/local/bin/node /usr/bin/node /bin/node"; + assert.ok(source.includes(systemPaths)); + const fixtureLauncher = path.join(root, "scripts", "launch"); + await mkdir(path.dirname(fixtureLauncher)); + await symlink(path.join(pluginRoot, "mcp"), path.join(root, "mcp")); + await writeFile( + fixtureLauncher, + source.replace(systemPaths, '"$FIXTURE_SYSTEM_NODE"'), + ); + const run = (candidate: string) => + spawnSync("/bin/sh", [fixtureLauncher, "--helper", "database-info"], { + cwd: root, + encoding: "utf8", + input: JSON.stringify(root), + env: { + HOME: root, + PATH: root, + NODE_DEBUG: "module", + CODEX_BROWSER_USE_NODE_PATH: withoutSqlite, + FIXTURE_SYSTEM_NODE: candidate, + }, + }); + const linked = run(systemLink); + assert.equal(linked.status, 0, linked.stderr); + assert.deepEqual(JSON.parse(linked.stdout), { + databasePath: path.join(root, "workbench.sqlite3"), + }); + const missing = run(path.join(root, "missing-node")); + assert.notEqual(missing.status, 0); + assert.match(missing.stderr, /node:sqlite/); + assert.equal(existsSync(marker), false); + }, +); + launcherTest( "compatible managed runtimes retain priority over PATH", async () => { diff --git a/plugins/codex-security/scripts/launch_codex_security_mcp b/plugins/codex-security/scripts/launch_codex_security_mcp index c5f462d5e7..909107203a 100755 --- a/plugins/codex-security/scripts/launch_codex_security_mcp +++ b/plugins/codex-security/scripts/launch_codex_security_mcp @@ -55,19 +55,37 @@ fi first_node= can_load_native() { + selected_node=$1 if [ -z "$first_node" ]; then first_node=$1 fi - # Some managed macOS runtimes enforce library validation and cannot load the - # plugin's addon. Check before starting a server or helper with side effects. + # Database helpers need SQLite. On macOS, library validation can also + # prevent a managed runtime from loading the plugin's addon. # Capture output without opening a writable file for read-only callers. - _native_probe_output=$("$1" -e ' - if (process.platform === "darwin") { + if _native_probe_output=$("$1" -e ' + if (process.platform === "linux") { + try { require("node:sqlite"); } catch { process.exitCode = 78; } + } else if (process.platform === "darwin") { require("node:sqlite"); require(require("node:path").join(process.argv[1], "native", `${process.platform}-${process.arch}`, "unix.node")); } - ' "$launcher_dir/../mcp" &1) + ' "$launcher_dir/../mcp" &1); then + return 0 + elif [ "$?" -ne 78 ]; then + return 1 + fi + + # This Linux runtime can still run helpers without SQLite. Prefer a system + # installation if available, without expanding discovery into the caller's PATH. + for system_candidate in /usr/local/bin/node /usr/bin/node /bin/node; do + if [ -x "$system_candidate" ] && + _native_probe_output=$("$system_candidate" -e 'require("node:sqlite")' &1); then + selected_node=$system_candidate + break + fi + done + return 0 } for candidate in \ @@ -77,7 +95,7 @@ for candidate in \ "$cache_root/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node" do if [ -n "$candidate" ] && [ -x "$candidate" ] && can_load_native "$candidate"; then - launch_node "$candidate" "$@" + launch_node "$selected_node" "$@" fi done @@ -86,7 +104,7 @@ while :; do path_entry=${remaining_path%%:*} node_path=${path_entry:-.}/node if [ -x "$node_path" ] && can_load_native "$node_path"; then - launch_node "$node_path" "$@" + launch_node "$selected_node" "$@" fi case "$remaining_path" in *:*) remaining_path=${remaining_path#*:} ;;