mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
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:
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Android/gateway: require TLS for non-loopback remote gateway endpoints while still allowing local loopback and emulator cleartext setup flows. (#58475) Thanks @eleqtrizit.
|
- Android/gateway: require TLS for non-loopback remote gateway endpoints while still allowing local loopback and emulator cleartext setup flows. (#58475) Thanks @eleqtrizit.
|
||||||
- Exec/Windows: hide transient console windows for `runExec` and `runCommandWithTimeout` child-process launches, matching other Windows exec paths and stopping visible shell flashes during tool runs. (#59466) Thanks @lawrence3699.
|
- Exec/Windows: hide transient console windows for `runExec` and `runCommandWithTimeout` child-process launches, matching other Windows exec paths and stopping visible shell flashes during tool runs. (#59466) Thanks @lawrence3699.
|
||||||
- Plugins/OpenAI: enable reference-image edits for `gpt-image-1` by routing edit calls to `/images/edits` with multipart image uploads, and update image-generation capability/docs metadata accordingly.
|
- Plugins/OpenAI: enable reference-image edits for `gpt-image-1` by routing edit calls to `/images/edits` with multipart image uploads, and update image-generation capability/docs metadata accordingly.
|
||||||
|
- ACP/Windows spawn: fail closed on unresolved `.cmd` and `.bat` OpenClaw wrappers unless a caller explicitly opts into shell fallback, so Windows ACP launches do not silently drop into shell-mediated execution when wrapper unwrapping fails. (#58436) Thanks @eleqtrizit.
|
||||||
- Exec/Windows: prefer strict-inline-eval denial over generic allowlist prompts for interpreter carriers, while keeping persisted Windows allow-always approvals argv-bound. (#59780) Thanks @luoyanglang.
|
- Exec/Windows: prefer strict-inline-eval denial over generic allowlist prompts for interpreter carriers, while keeping persisted Windows allow-always approvals argv-bound. (#59780) Thanks @luoyanglang.
|
||||||
|
|
||||||
## 2026.4.2-beta.1
|
## 2026.4.2-beta.1
|
||||||
|
|||||||
@@ -49,6 +49,29 @@ is a small, self-contained module with a clear purpose and documented contract.
|
|||||||
## How to migrate
|
## How to migrate
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
|
<Step title="Audit Windows wrapper fallback behavior">
|
||||||
|
If your plugin uses `openclaw/plugin-sdk/windows-spawn`, unresolved Windows
|
||||||
|
`.cmd`/`.bat` wrappers now fail closed unless you explicitly pass
|
||||||
|
`allowShellFallback: true`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const program = applyWindowsSpawnProgramPolicy({ candidate });
|
||||||
|
|
||||||
|
// After
|
||||||
|
const program = applyWindowsSpawnProgramPolicy({
|
||||||
|
candidate,
|
||||||
|
// Only set this for trusted compatibility callers that intentionally
|
||||||
|
// accept shell-mediated fallback.
|
||||||
|
allowShellFallback: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If your caller does not intentionally rely on shell fallback, do not set
|
||||||
|
`allowShellFallback` and handle the thrown error instead.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
<Step title="Find deprecated imports">
|
<Step title="Find deprecated imports">
|
||||||
Search your plugin for imports from either deprecated surface:
|
Search your plugin for imports from either deprecated surface:
|
||||||
|
|
||||||
|
|||||||
@@ -272,26 +272,21 @@ describe("resolveAcpClientSpawnInvocation", () => {
|
|||||||
expect(resolved.windowsHide).toBe(true);
|
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 dir = await createTempDir();
|
||||||
const shimPath = path.join(dir, "openclaw.cmd");
|
const shimPath = path.join(dir, "openclaw.cmd");
|
||||||
await writeFile(shimPath, "@ECHO off\r\necho wrapper\r\n", "utf8");
|
await writeFile(shimPath, "@ECHO off\r\necho wrapper\r\n", "utf8");
|
||||||
|
|
||||||
const resolved = resolveAcpClientSpawnInvocation(
|
expect(() =>
|
||||||
{ serverCommand: shimPath, serverArgs: ["acp"] },
|
resolveAcpClientSpawnInvocation(
|
||||||
{
|
{ serverCommand: shimPath, serverArgs: ["acp"] },
|
||||||
platform: "win32",
|
{
|
||||||
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
|
platform: "win32",
|
||||||
execPath: "C:\\node\\node.exe",
|
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
|
||||||
},
|
execPath: "C:\\node\\node.exe",
|
||||||
);
|
},
|
||||||
|
),
|
||||||
expect(resolved).toEqual({
|
).toThrow(/without shell execution/);
|
||||||
command: shimPath,
|
|
||||||
args: ["acp"],
|
|
||||||
shell: true,
|
|
||||||
windowsHide: undefined,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ export function resolveAcpClientSpawnInvocation(
|
|||||||
env: runtime.env,
|
env: runtime.env,
|
||||||
execPath: runtime.execPath,
|
execPath: runtime.execPath,
|
||||||
packageName: "openclaw",
|
packageName: "openclaw",
|
||||||
allowShellFallback: true,
|
|
||||||
});
|
});
|
||||||
const resolved = materializeWindowsSpawnProgram(program, params.serverArgs);
|
const resolved = materializeWindowsSpawnProgram(program, params.serverArgs);
|
||||||
return {
|
return {
|
||||||
|
|||||||
68
src/plugin-sdk/windows-spawn.test.ts
Normal file
68
src/plugin-sdk/windows-spawn.test.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,6 +37,7 @@ export type ResolveWindowsSpawnProgramParams = {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
execPath?: string;
|
execPath?: string;
|
||||||
packageName?: string;
|
packageName?: string;
|
||||||
|
/** Trusted compatibility escape hatch for callers that intentionally accept shell-mediated wrapper execution. */
|
||||||
allowShellFallback?: boolean;
|
allowShellFallback?: boolean;
|
||||||
};
|
};
|
||||||
export type ResolveWindowsSpawnProgramCandidateParams = Omit<
|
export type ResolveWindowsSpawnProgramCandidateParams = Omit<
|
||||||
@@ -265,7 +266,7 @@ export function applyWindowsSpawnProgramPolicy(params: {
|
|||||||
windowsHide: params.candidate.windowsHide,
|
windowsHide: params.candidate.windowsHide,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (params.allowShellFallback !== false) {
|
if (params.allowShellFallback === true) {
|
||||||
return {
|
return {
|
||||||
command: params.candidate.command,
|
command: params.candidate.command,
|
||||||
leadingArgv: [],
|
leadingArgv: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user