Compare commits

...

2 Commits

Author SHA1 Message Date
Austin Liu
42303a737b fix(windows): ensure windowsHide: true on open-sse qoder/devin child spawns 2026-07-26 14:10:58 +09:30
Austin Liu
19f68fa2f6 fix(windows): request shell when spawning bare qoder binary name on Windows (fixes #8590)
Post Node CVE-2024-27980, spawn('qodercli', [], { shell: false }) fails with ENOENT on Windows when command is a bare binary name without extension. Enabling shell mode for bare command names allows cmd.exe to resolve .cmd / .bat wrappers from PATH.
2026-07-26 14:08:01 +09:30
4 changed files with 34 additions and 8 deletions

View File

@@ -156,6 +156,7 @@ export class DevinCliExecutor extends BaseExecutor {
const child = spawn(devinBin, ["acp", "--agent-type", "summarizer"], {
env,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
// On Windows, devin.exe may need shell resolution
shell: process.platform === "win32",
});

View File

@@ -139,6 +139,7 @@ async function spawnQoderCli(options: SpawnQoderCliOptions): Promise<QoderCliRun
env,
cwd: options.cwd || undefined,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
...(useShell ? { shell: true } : {}),
});
} catch (err) {

View File

@@ -11,6 +11,7 @@
* we reuse it: `getCliRuntimeStatus("qoder")` returns an absolute `.cmd`/`.exe`
* `commandPath`, and `shouldUseShellForCommand()` tells us whether it needs cmd.exe.
*/
import path from "path";
import {
getCliRuntimeStatus,
getKnownToolPaths,
@@ -76,7 +77,16 @@ export async function resolveQoderCliInvocation(
/* fall back to the bare/explicit command — spawn will surface a real ENOENT */
}
const invocation: QoderCliInvocation = { command, useShell: shouldUseShell(command) };
// On Windows, if explicit CLI_QODER_BIN / command is a bare binary name like "qodercli" or "qoder" (no path or extension),
// spawn(cmd, { shell: false }) will fail with ENOENT post Node CVE-2024-27980.
// Enabling shell: true for bare command names on win32 lets system PATH resolution find qodercli.cmd / qoder.cmd.
const useShell =
shouldUseShell(command) ||
(process.platform === "win32" &&
!path.isAbsolute(command) &&
!path.basename(command).includes("."));
const invocation: QoderCliInvocation = { command, useShell };
if (cacheable) {
qoderInvocationCache = {
...invocation,

View File

@@ -53,11 +53,25 @@ test("cliRuntime enumerates qodercli.cmd under %APPDATA%\\npm on Windows", () =>
);
});
test("shouldUseShellForCommand: true for a .cmd wrapper, false for the bare name (Windows)", () => {
test("resolveQoderCliInvocation requests shell for explicit bare command CLI_QODER_BIN on Windows (#8590)", async () => {
setPlatform("win32");
const cmdPath = path.join(os.homedir(), "AppData", "Roaming", "npm", "qodercli.cmd");
assert.equal(cliRuntime.shouldUseShellForCommand(cmdPath), true);
assert.equal(cliRuntime.shouldUseShellForCommand("qodercli"), false);
process.env.CLI_QODER_BIN = "custom-qoder";
const invocation = await qoderResolve.resolveQoderCliInvocation("custom-qoder", {
getStatus: async () => ({
installed: false,
runnable: false,
command: "qodercli",
commandPath: null,
reason: "not_found",
runtimeMode: "auto",
requiresBinary: true,
}),
});
assert.equal(invocation.command, "custom-qoder");
assert.equal(invocation.useShell, true);
delete process.env.CLI_QODER_BIN;
});
test("resolveQoderCliInvocation picks the absolute .cmd path + shell when cliRuntime finds it", async () => {
@@ -83,7 +97,7 @@ test("resolveQoderCliInvocation picks the absolute .cmd path + shell when cliRun
assert.notEqual(invocation.command, "qodercli");
});
test("resolveQoderCliInvocation falls back to the bare command when cliRuntime finds nothing", async () => {
test("resolveQoderCliInvocation falls back to bare command + requests shell for bare name on Windows (#8590)", async () => {
setPlatform("win32");
delete process.env.CLI_QODER_BIN;
@@ -100,8 +114,8 @@ test("resolveQoderCliInvocation falls back to the bare command when cliRuntime f
});
assert.equal(invocation.command, "qodercli");
// A bare name is not a .cmd/.bat, so no shell is requested.
assert.equal(invocation.useShell, false);
// Bare name on Windows requires shell: true so cmd.exe can resolve qodercli.cmd / qodercli.bat / qodercli.exe on PATH (#8590)
assert.equal(invocation.useShell, true);
});
test("resolveQoderCliInvocation is inert on non-Windows (no shell, resolved path honored)", async () => {