From 648c0512c82768727655ead5ef4117f17347226a Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:07:28 +0000 Subject: [PATCH 1/5] fix(plugin): select a SQLite-capable Node runtime on Linux --- .../mcp-app/tests/test_deep_scan_executor.ts | 13 ++++++- .../mcp-app/tests/test_node_launcher.ts | 36 +++++++++++++++++++ .../scripts/launch_codex_security_mcp | 6 ++-- 3 files changed, 51 insertions(+), 4 deletions(-) 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..717a82ea67 100644 --- a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts +++ b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts @@ -247,6 +247,42 @@ launcherTest( }, ); +test( + "Linux database helpers skip an automatic Node runtime without SQLite", + { skip: process.platform !== "linux" }, + async () => { + const root = await directories.create("codex-security-linux-launcher-"); + const withoutSqlite = path.join(root, "node-without-sqlite"); + 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 result = spawnSync( + "/bin/sh", + [launcher, "--helper", "database-info"], + { + encoding: "utf8", + input: JSON.stringify(root), + env: { + HOME: root, + PATH: path.dirname(process.execPath), + CODEX_BROWSER_USE_NODE_PATH: withoutSqlite, + }, + }, + ); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { + databasePath: path.join(root, "workbench.sqlite3"), + }); + }, +); + 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 f19251085d..6e0dccd7c1 100755 --- a/plugins/codex-security/scripts/launch_codex_security_mcp +++ b/plugins/codex-security/scripts/launch_codex_security_mcp @@ -57,12 +57,12 @@ can_load_native() { 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 ' + require("node:sqlite"); if (process.platform === "darwin") { - require("node:sqlite"); require(require("node:path").join(process.argv[1], "native", `${process.platform}-${process.arch}`, "unix.node")); } From e35fdd9f83dd9c8d8f7bf837df179c09b382ecd8 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:26:37 +0000 Subject: [PATCH 2/5] fix(plugin): limit Linux SQLite fallback to system installations --- .../mcp-app/tests/test_node_launcher.ts | 39 ++++++++++++++++--- .../scripts/launch_codex_security_mcp | 29 +++++++++++--- 2 files changed, 57 insertions(+), 11 deletions(-) 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 717a82ea67..40b23b5535 100644 --- a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts +++ b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts @@ -1,10 +1,12 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; import { copyFile, mkdir, readdir, readFile, + realpath, symlink, writeFile, } from "node:fs/promises"; @@ -248,11 +250,17 @@ launcherTest( ); test( - "Linux database helpers skip an automatic Node runtime without SQLite", + "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`, @@ -267,19 +275,38 @@ test( "/bin/sh", [launcher, "--helper", "database-info"], { + cwd: root, encoding: "utf8", input: JSON.stringify(root), env: { HOME: root, - PATH: path.dirname(process.execPath), + PATH: root, CODEX_BROWSER_USE_NODE_PATH: withoutSqlite, }, }, ); - assert.equal(result.status, 0, result.stderr); - assert.deepEqual(JSON.parse(result.stdout), { - databasePath: path.join(root, "workbench.sqlite3"), - }); + assert.equal(existsSync(marker), false); + const systemBins = ["/usr/local/bin", "/usr/bin", "/bin"]; + const systemNodes = await Promise.all( + systemBins.map((bin) => + realpath(path.join(bin, "node")).catch(() => null), + ), + ); + const hasSystemSqlite = systemNodes.some( + (node) => + node !== null && + systemBins.includes(path.dirname(node)) && + spawnSync(node, ["-e", 'require("node:sqlite")']).status === 0, + ); + if (hasSystemSqlite) { + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { + databasePath: path.join(root, "workbench.sqlite3"), + }); + } else { + assert.notEqual(result.status, 0); + assert.match(result.stderr, /node:sqlite/); + } }, ); diff --git a/plugins/codex-security/scripts/launch_codex_security_mcp b/plugins/codex-security/scripts/launch_codex_security_mcp index 6e0dccd7c1..be8424b665 100755 --- a/plugins/codex-security/scripts/launch_codex_security_mcp +++ b/plugins/codex-security/scripts/launch_codex_security_mcp @@ -54,6 +54,7 @@ fi first_node= can_load_native() { + selected_node=$1 if [ -z "$first_node" ]; then first_node=$1 fi @@ -61,12 +62,30 @@ can_load_native() { # 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 ' - require("node:sqlite"); - if (process.platform === "darwin") { + if (process.platform === "linux") { + try { require("node:sqlite"); } catch { console.log("linux-without-sqlite"); } + } 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) || return 1 + [ "$_native_probe_output" = linux-without-sqlite ] || return 0 + + # 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 + [ -x "$system_candidate" ] || continue + system_node=$(command -p realpath "$system_candidate" 2>&-) || continue + case "${system_node%/*}" in + /usr/local/bin|/usr/bin|/bin) + if _native_probe_output=$("$system_node" -e 'require("node:sqlite")' &1); then + selected_node=$system_node + break + fi ;; + esac + done + return 0 } for candidate in \ @@ -76,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 @@ -85,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#*:} ;; From fb5a16b0e556bb414e130aaccf1ab6c632ec3b36 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:17:46 +0000 Subject: [PATCH 3/5] fix(plugin): distinguish missing SQLite by probe exit status --- .../mcp-app/tests/test_node_launcher.ts | 1 + .../codex-security/scripts/launch_codex_security_mcp | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) 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 40b23b5535..e12e64447a 100644 --- a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts +++ b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts @@ -281,6 +281,7 @@ test( env: { HOME: root, PATH: root, + NODE_DEBUG: "module", CODEX_BROWSER_USE_NODE_PATH: withoutSqlite, }, }, diff --git a/plugins/codex-security/scripts/launch_codex_security_mcp b/plugins/codex-security/scripts/launch_codex_security_mcp index be8424b665..fa1ef0488c 100755 --- a/plugins/codex-security/scripts/launch_codex_security_mcp +++ b/plugins/codex-security/scripts/launch_codex_security_mcp @@ -61,16 +61,19 @@ can_load_native() { # 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 _native_probe_output=$("$1" -e ' if (process.platform === "linux") { - try { require("node:sqlite"); } catch { console.log("linux-without-sqlite"); } + 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) || return 1 - [ "$_native_probe_output" = linux-without-sqlite ] || return 0 + ' "$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. From d4f75b1ec483dfada10dd539fc7abec29f0fff65 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:06:26 +0000 Subject: [PATCH 4/5] fix(plugin): follow fixed system Node installation symlinks --- .../mcp-app/tests/test_node_launcher.ts | 77 ++++++++++--------- .../scripts/launch_codex_security_mcp | 14 ++-- 2 files changed, 46 insertions(+), 45 deletions(-) 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 e12e64447a..144cdb16eb 100644 --- a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts +++ b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts @@ -6,7 +6,6 @@ import { mkdir, readdir, readFile, - realpath, symlink, writeFile, } from "node:fs/promises"; @@ -271,43 +270,49 @@ test( "scripts", "launch_codex_security_mcp", ); - const result = spawnSync( - "/bin/sh", - [launcher, "--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, - }, - }, + const installedNode = path.join(root, "installed-node"); + const systemLink = path.join(root, "system-node"); + 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 run = (candidate: string) => + spawnSync( + "/bin/sh", + [ + "-c", + source.replace(systemPaths, shellQuote(candidate)), + launcher, + "--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, + }, + }, + ); + 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); - const systemBins = ["/usr/local/bin", "/usr/bin", "/bin"]; - const systemNodes = await Promise.all( - systemBins.map((bin) => - realpath(path.join(bin, "node")).catch(() => null), - ), - ); - const hasSystemSqlite = systemNodes.some( - (node) => - node !== null && - systemBins.includes(path.dirname(node)) && - spawnSync(node, ["-e", 'require("node:sqlite")']).status === 0, - ); - if (hasSystemSqlite) { - assert.equal(result.status, 0, result.stderr); - assert.deepEqual(JSON.parse(result.stdout), { - databasePath: path.join(root, "workbench.sqlite3"), - }); - } else { - assert.notEqual(result.status, 0); - assert.match(result.stderr, /node:sqlite/); - } }, ); diff --git a/plugins/codex-security/scripts/launch_codex_security_mcp b/plugins/codex-security/scripts/launch_codex_security_mcp index 9c04046bfb..909107203a 100755 --- a/plugins/codex-security/scripts/launch_codex_security_mcp +++ b/plugins/codex-security/scripts/launch_codex_security_mcp @@ -79,15 +79,11 @@ can_load_native() { # 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 - [ -x "$system_candidate" ] || continue - system_node=$(command -p realpath "$system_candidate" 2>&-) || continue - case "${system_node%/*}" in - /usr/local/bin|/usr/bin|/bin) - if _native_probe_output=$("$system_node" -e 'require("node:sqlite")' &1); then - selected_node=$system_node - break - fi ;; - esac + if [ -x "$system_candidate" ] && + _native_probe_output=$("$system_candidate" -e 'require("node:sqlite")' &1); then + selected_node=$system_candidate + break + fi done return 0 } From d547c698a9f3823bfaf5e8097ee7b79032eee4ec Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:15:47 +0000 Subject: [PATCH 5/5] test(plugin): pass the system Node fixture path as data --- .../mcp-app/tests/test_node_launcher.ts | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) 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 144cdb16eb..2bc2b2f173 100644 --- a/plugins/codex-security/mcp-app/tests/test_node_launcher.ts +++ b/plugins/codex-security/mcp-app/tests/test_node_launcher.ts @@ -271,7 +271,7 @@ test( "launch_codex_security_mcp", ); const installedNode = path.join(root, "installed-node"); - const systemLink = path.join(root, "system-node"); + const systemLink = path.join(root, "system Node's link"); await writeFile( installedNode, `#!/bin/sh\nexec ${shellQuote(process.execPath)} "$@"\n`, @@ -282,28 +282,26 @@ test( // 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", - [ - "-c", - source.replace(systemPaths, shellQuote(candidate)), - launcher, - "--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, - }, + 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), {