fix(windows): reject unresolved cmd wrappers (#58436)

* fix(windows): reject unresolved cmd wrappers

* fix(windows): add wrapper policy coverage

* fix(windows): document wrapper fallback migration

* fix(windows): drop changelog entry from pr

* chore: add changelog for Windows wrapper fail-closed behavior

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
This commit is contained in:
Agustin Rivera
2026-04-02 10:35:50 -07:00
committed by GitHub
parent 3e452f2671
commit 5874a387ae
6 changed files with 105 additions and 18 deletions

View File

@@ -272,26 +272,21 @@ describe("resolveAcpClientSpawnInvocation", () => {
expect(resolved.windowsHide).toBe(true);
});
it("falls back to shell mode for unresolved wrappers on windows", async () => {
it("fails closed for unresolved wrappers on windows", async () => {
const dir = await createTempDir();
const shimPath = path.join(dir, "openclaw.cmd");
await writeFile(shimPath, "@ECHO off\r\necho wrapper\r\n", "utf8");
const resolved = resolveAcpClientSpawnInvocation(
{ serverCommand: shimPath, serverArgs: ["acp"] },
{
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
},
);
expect(resolved).toEqual({
command: shimPath,
args: ["acp"],
shell: true,
windowsHide: undefined,
});
expect(() =>
resolveAcpClientSpawnInvocation(
{ serverCommand: shimPath, serverArgs: ["acp"] },
{
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
},
),
).toThrow(/without shell execution/);
});
});

View File

@@ -260,7 +260,6 @@ export function resolveAcpClientSpawnInvocation(
env: runtime.env,
execPath: runtime.execPath,
packageName: "openclaw",
allowShellFallback: true,
});
const resolved = materializeWindowsSpawnProgram(program, params.serverArgs);
return {

View File

@@ -0,0 +1,68 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { materializeWindowsSpawnProgram, resolveWindowsSpawnProgram } from "./windows-spawn.js";
const tempDirs: string[] = [];
async function createTempDir(): Promise<string> {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-windows-spawn-test-"));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (!dir) {
continue;
}
await rm(dir, {
recursive: true,
force: true,
maxRetries: 8,
retryDelay: 8,
});
}
});
describe("resolveWindowsSpawnProgram", () => {
it("fails closed by default for unresolved windows wrappers", async () => {
const dir = await createTempDir();
const shimPath = path.join(dir, "wrapper.cmd");
await writeFile(shimPath, "@ECHO off\r\necho wrapper\r\n", "utf8");
expect(() =>
resolveWindowsSpawnProgram({
command: shimPath,
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
}),
).toThrow(/without shell execution/);
});
it("only returns shell fallback when explicitly opted in", async () => {
const dir = await createTempDir();
const shimPath = path.join(dir, "wrapper.cmd");
await writeFile(shimPath, "@ECHO off\r\necho wrapper\r\n", "utf8");
const resolved = resolveWindowsSpawnProgram({
command: shimPath,
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
allowShellFallback: true,
});
const invocation = materializeWindowsSpawnProgram(resolved, ["--cwd", "C:\\safe & calc.exe"]);
expect(invocation).toEqual({
command: shimPath,
argv: ["--cwd", "C:\\safe & calc.exe"],
resolution: "shell-fallback",
shell: true,
windowsHide: undefined,
});
});
});

View File

@@ -37,6 +37,7 @@ export type ResolveWindowsSpawnProgramParams = {
env?: NodeJS.ProcessEnv;
execPath?: string;
packageName?: string;
/** Trusted compatibility escape hatch for callers that intentionally accept shell-mediated wrapper execution. */
allowShellFallback?: boolean;
};
export type ResolveWindowsSpawnProgramCandidateParams = Omit<
@@ -265,7 +266,7 @@ export function applyWindowsSpawnProgramPolicy(params: {
windowsHide: params.candidate.windowsHide,
};
}
if (params.allowShellFallback !== false) {
if (params.allowShellFallback === true) {
return {
command: params.candidate.command,
leadingArgv: [],