From 112cb5af45c17088f723badc771f0704d7d8dbad Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 18 Apr 2026 10:37:10 +0200 Subject: [PATCH] refactor(llm-security): parameterize buildSandboxedWorker with workerPath --- .../scanners/lib/vsix-sandbox.mjs | 43 +++++++++++++++++-- .../tests/scanners/vsix-sandbox.test.mjs | 31 +++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/plugins/llm-security/scanners/lib/vsix-sandbox.mjs b/plugins/llm-security/scanners/lib/vsix-sandbox.mjs index b0cdcaa..ee4b250 100644 --- a/plugins/llm-security/scanners/lib/vsix-sandbox.mjs +++ b/plugins/llm-security/scanners/lib/vsix-sandbox.mjs @@ -18,7 +18,10 @@ import { fileURLToPath } from 'node:url'; import { dirname, resolve as resolvePath } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const WORKER_PATH = resolvePath(__dirname, 'vsix-fetch-worker.mjs'); +const DEFAULT_VSIX_WORKER_PATH = resolvePath(__dirname, 'vsix-fetch-worker.mjs'); +const DEFAULT_JETBRAINS_WORKER_PATH = resolvePath(__dirname, 'jetbrains-fetch-worker.mjs'); +// Backward-compat alias — older internal refs may still import `WORKER_PATH`. +const WORKER_PATH = DEFAULT_VSIX_WORKER_PATH; const WORKER_TIMEOUT_MS = 35_000; // fetch is 30s, give worker 5s of slack const MAX_OUTPUT_BYTES = 1024 * 1024; // 1MB JSON cap (output is tiny in practice) @@ -76,10 +79,13 @@ export function buildBwrapArgs(allowedWritePath, innerArgs) { * * @param {string} tmpDir writable temp dir for the worker * @param {string[]} workerArgs argv for the worker (after `node `) + * @param {string} [workerPath=DEFAULT_VSIX_WORKER_PATH] absolute path to the + * worker module. Defaults to the VSIX worker for backward compat; Step 12 + * passes `DEFAULT_JETBRAINS_WORKER_PATH` for JetBrains plugin fetches. * @returns {{cmd:string, args:string[], sandbox: 'sandbox-exec'|'bwrap'|null}} */ -export function buildSandboxedWorker(tmpDir, workerArgs) { - const innerArgs = ['node', WORKER_PATH, ...workerArgs]; +export function buildSandboxedWorker(tmpDir, workerArgs, workerPath = DEFAULT_VSIX_WORKER_PATH) { + const innerArgs = ['node', workerPath, ...workerArgs]; const profile = buildSandboxProfile(tmpDir); if (profile) { @@ -105,8 +111,29 @@ export function buildSandboxedWorker(tmpDir, workerArgs) { * @returns {Promise<{ok:boolean, sandbox:'sandbox-exec'|'bwrap'|null, payload:object}>} */ export function runVsixWorker(url, tmpDir, opts = {}) { + return runPluginWorker( + DEFAULT_VSIX_WORKER_PATH, + ['--url', url, '--tmpdir', tmpDir], + tmpDir, + opts, + ); +} + +/** + * Generalized sandboxed-worker runner. Spawns any worker module, captures a + * single JSON line of stdout, enforces the same timeout / output cap as + * `runVsixWorker`. Step 12 uses this for the JetBrains worker — it must NOT + * duplicate spawn/parse logic. + * + * @param {string} workerPath absolute path to the worker .mjs file + * @param {string[]} workerArgs argv for the worker + * @param {string} tmpDir writable temp dir for the worker + * @param {{allowFallback?: boolean}} [opts] + * @returns {Promise<{ok:boolean, sandbox:'sandbox-exec'|'bwrap'|null, payload:object}>} + */ +export function runPluginWorker(workerPath, workerArgs, tmpDir, opts = {}) { const { allowFallback = true } = opts; - const { cmd, args, sandbox } = buildSandboxedWorker(tmpDir, ['--url', url, '--tmpdir', tmpDir]); + const { cmd, args, sandbox } = buildSandboxedWorker(tmpDir, workerArgs, workerPath); if (!sandbox && !allowFallback) { return Promise.reject(new Error('no OS sandbox available and fallback disabled')); @@ -162,8 +189,16 @@ export function runVsixWorker(url, tmpDir, opts = {}) { }); } +export { + DEFAULT_VSIX_WORKER_PATH, + DEFAULT_JETBRAINS_WORKER_PATH, +}; + export const __testing = { WORKER_PATH, + DEFAULT_VSIX_WORKER_PATH, + DEFAULT_JETBRAINS_WORKER_PATH, WORKER_TIMEOUT_MS, MAX_OUTPUT_BYTES, + resolveWorkerPath: (name) => resolvePath(__dirname, name), }; diff --git a/plugins/llm-security/tests/scanners/vsix-sandbox.test.mjs b/plugins/llm-security/tests/scanners/vsix-sandbox.test.mjs index 805d382..532fa41 100644 --- a/plugins/llm-security/tests/scanners/vsix-sandbox.test.mjs +++ b/plugins/llm-security/tests/scanners/vsix-sandbox.test.mjs @@ -11,6 +11,9 @@ import { buildBwrapArgs, buildSandboxedWorker, runVsixWorker, + runPluginWorker, + DEFAULT_VSIX_WORKER_PATH, + DEFAULT_JETBRAINS_WORKER_PATH, __testing, } from '../../scanners/lib/vsix-sandbox.mjs'; @@ -69,6 +72,34 @@ describe('vsix-sandbox — buildSandboxedWorker', () => { assert.match(joined, /--url https:\/\/example\//); assert.match(joined, /--tmpdir \/tmp/); }); + + it('honors explicit workerPath parameter (Step 10 generalization)', () => { + const custom = '/path/to/other-worker.mjs'; + const { args } = buildSandboxedWorker('/tmp', ['--foo'], custom); + const joined = args.join(' '); + // Must contain the custom path, and NOT the default VSIX worker. + assert.ok(joined.includes(custom), `expected custom worker in args: ${joined}`); + assert.ok(!joined.includes('vsix-fetch-worker.mjs'), + `custom worker should have replaced default: ${joined}`); + assert.match(joined, /--foo/); + }); + + it('defaults to DEFAULT_VSIX_WORKER_PATH when workerPath omitted', () => { + const { args } = buildSandboxedWorker('/tmp', ['--url', 'https://x/', '--tmpdir', '/tmp']); + assert.ok(args.some((a) => a === DEFAULT_VSIX_WORKER_PATH), + `expected DEFAULT_VSIX_WORKER_PATH in args: ${args.join(' ')}`); + }); + + it('exports DEFAULT_JETBRAINS_WORKER_PATH (Step 10 constant for Step 12)', () => { + assert.ok(typeof DEFAULT_JETBRAINS_WORKER_PATH === 'string'); + assert.match(DEFAULT_JETBRAINS_WORKER_PATH, /jetbrains-fetch-worker\.mjs$/); + }); +}); + +describe('vsix-sandbox — runPluginWorker (generalized runner)', () => { + it('is exported as a function', () => { + assert.equal(typeof runPluginWorker, 'function'); + }); }); describe('vsix-sandbox — runVsixWorker (live worker, no network)', () => {