windows: unify non-core spawn handling across acp qmd and docker (openclaw#31750) thanks @Takhoffman

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on pre-existing unrelated src/slack/monitor/events/messages.ts typing errors)
- pnpm vitest run src/acp/client.test.ts src/memory/qmd-manager.test.ts src/agents/sandbox/docker.execDockerRaw.enoent.test.ts src/agents/sandbox/docker.windows.test.ts extensions/acpx/src/runtime-internals/process.test.ts

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-03-02 08:05:39 -06:00
committed by GitHub
parent 32c7242974
commit cd653c55d7
7 changed files with 305 additions and 8 deletions

View File

@@ -1,5 +1,9 @@
import { spawn } from "node:child_process";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
materializeWindowsSpawnProgram,
resolveWindowsSpawnProgram,
} from "../../plugin-sdk/windows-spawn.js";
import { sanitizeEnvVars } from "./sanitize-env-vars.js";
type ExecDockerRawOptions = {
@@ -26,13 +30,49 @@ function createAbortError(): Error {
return err;
}
type DockerSpawnRuntime = {
platform: NodeJS.Platform;
env: NodeJS.ProcessEnv;
execPath: string;
};
const DEFAULT_DOCKER_SPAWN_RUNTIME: DockerSpawnRuntime = {
platform: process.platform,
env: process.env,
execPath: process.execPath,
};
export function resolveDockerSpawnInvocation(
args: string[],
runtime: DockerSpawnRuntime = DEFAULT_DOCKER_SPAWN_RUNTIME,
): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
const program = resolveWindowsSpawnProgram({
command: "docker",
platform: runtime.platform,
env: runtime.env,
execPath: runtime.execPath,
packageName: "docker",
allowShellFallback: true,
});
const resolved = materializeWindowsSpawnProgram(program, args);
return {
command: resolved.command,
args: resolved.argv,
shell: resolved.shell,
windowsHide: resolved.windowsHide,
};
}
export function execDockerRaw(
args: string[],
opts?: ExecDockerRawOptions,
): Promise<ExecDockerRawResult> {
return new Promise<ExecDockerRawResult>((resolve, reject) => {
const child = spawn("docker", args, {
const spawnInvocation = resolveDockerSpawnInvocation(args);
const child = spawn(spawnInvocation.command, spawnInvocation.args, {
stdio: ["pipe", "pipe", "pipe"],
shell: spawnInvocation.shell,
windowsHide: spawnInvocation.windowsHide,
});
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];

View File

@@ -0,0 +1,79 @@
import { mkdir, 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 { resolveDockerSpawnInvocation } from "./docker.js";
const tempDirs: string[] = [];
async function createTempDir(): Promise<string> {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-docker-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 });
}
});
describe("resolveDockerSpawnInvocation", () => {
it("keeps non-windows invocation unchanged", () => {
const resolved = resolveDockerSpawnInvocation(["version"], {
platform: "darwin",
env: {},
execPath: "/usr/bin/node",
});
expect(resolved).toEqual({
command: "docker",
args: ["version"],
shell: undefined,
windowsHide: undefined,
});
});
it("prefers docker.exe entrypoint over cmd shell fallback on windows", async () => {
const dir = await createTempDir();
const exePath = path.join(dir, "docker.exe");
const cmdPath = path.join(dir, "docker.cmd");
await writeFile(exePath, "", "utf8");
await writeFile(cmdPath, `@ECHO off\r\n"%~dp0\\docker.exe" %*\r\n`, "utf8");
const resolved = resolveDockerSpawnInvocation(["version"], {
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
});
expect(resolved).toEqual({
command: exePath,
args: ["version"],
shell: undefined,
windowsHide: true,
});
});
it("falls back to shell mode when only unresolved docker.cmd wrapper exists", async () => {
const dir = await createTempDir();
const cmdPath = path.join(dir, "docker.cmd");
await mkdir(path.dirname(cmdPath), { recursive: true });
await writeFile(cmdPath, "@ECHO off\r\necho docker\r\n", "utf8");
const resolved = resolveDockerSpawnInvocation(["ps"], {
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
});
expect(path.normalize(resolved.command).toLowerCase()).toBe(
path.normalize(cmdPath).toLowerCase(),
);
expect(resolved.args).toEqual(["ps"]);
expect(resolved.shell).toBe(true);
expect(resolved.windowsHide).toBeUndefined();
});
});