From 2e1502f39d8f7357fca35c6c18cc2c0dadcf0da3 Mon Sep 17 00:00:00 2001 From: Christopher Haugen Date: Tue, 11 Aug 2026 16:50:54 +0200 Subject: [PATCH] fix: spawn launcher services without implicit shell (#16093) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19deb2d0-75af-4fcf-ac1d-a6933c00769f --- __tests__/scripts/dev-process-utils.test.ts | 25 +++++++++++++++++++++ scripts/dev-process-utils.mjs | 7 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/__tests__/scripts/dev-process-utils.test.ts b/__tests__/scripts/dev-process-utils.test.ts index 82abc5f742..df12205a89 100644 --- a/__tests__/scripts/dev-process-utils.test.ts +++ b/__tests__/scripts/dev-process-utils.test.ts @@ -1,3 +1,6 @@ +import { spawn } from "node:child_process"; +import { once } from "node:events"; + import { describe, expect, it } from "vitest"; import { @@ -30,6 +33,28 @@ describe("dev process utils", () => { detached: process.platform !== "win32", }); }); + + it("passes shell metacharacters to services as literal arguments", async () => { + const constraint = "agent-client-protocol<0.11"; + const child = spawn( + process.execPath, + ["-e", "process.stdout.write(process.argv[1])", constraint], + getProcessTreeSpawnOptions({ + shell: true, + stdio: ["ignore", "pipe", "pipe"], + }), + ); + let stdout = ""; + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { + stdout += chunk; + }); + + const [exitCode] = await once(child, "exit"); + + expect(exitCode).toBe(0); + expect(stdout).toBe(constraint); + }); }); describe("resolveWindowsCommand", () => { diff --git a/scripts/dev-process-utils.mjs b/scripts/dev-process-utils.mjs index 78dac1df6f..d38796949d 100644 --- a/scripts/dev-process-utils.mjs +++ b/scripts/dev-process-utils.mjs @@ -15,7 +15,11 @@ export function isProcessRunning(proc) { } /** - * Add spawn options needed for process-tree cleanup. + * Add spawn options needed for safe service launches and process-tree cleanup. + * + * Arguments must bypass shell parsing so values such as version constraints + * containing `<` are forwarded literally. Callers that need shell behavior + * must invoke the shell explicitly as the command. * * On POSIX, `detached: true` makes the spawned service the leader of a new * process group. Later we can signal `-pid` to terminate that whole group, @@ -30,6 +34,7 @@ export function isProcessRunning(proc) { export function getProcessTreeSpawnOptions(options = {}) { return { ...options, + shell: false, detached: process.platform !== "win32", }; }