mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 15:44:31 +00:00
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:
@@ -1,6 +1,13 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import type { RequestPermissionRequest } from "@agentclientprotocol/sdk";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveAcpClientSpawnEnv, resolvePermissionRequest } from "./client.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
resolveAcpClientSpawnEnv,
|
||||
resolveAcpClientSpawnInvocation,
|
||||
resolvePermissionRequest,
|
||||
} from "./client.js";
|
||||
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
|
||||
|
||||
function makePermissionRequest(
|
||||
@@ -28,6 +35,24 @@ function makePermissionRequest(
|
||||
};
|
||||
}
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(): Promise<string> {
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-acp-client-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("resolveAcpClientSpawnEnv", () => {
|
||||
it("sets OPENCLAW_SHELL marker and preserves existing env values", () => {
|
||||
const env = resolveAcpClientSpawnEnv({
|
||||
@@ -48,6 +73,69 @@ describe("resolveAcpClientSpawnEnv", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAcpClientSpawnInvocation", () => {
|
||||
it("keeps non-windows invocation unchanged", () => {
|
||||
const resolved = resolveAcpClientSpawnInvocation(
|
||||
{ serverCommand: "openclaw", serverArgs: ["acp", "--verbose"] },
|
||||
{
|
||||
platform: "darwin",
|
||||
env: {},
|
||||
execPath: "/usr/bin/node",
|
||||
},
|
||||
);
|
||||
expect(resolved).toEqual({
|
||||
command: "openclaw",
|
||||
args: ["acp", "--verbose"],
|
||||
shell: undefined,
|
||||
windowsHide: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("unwraps .cmd shim entrypoint on windows", async () => {
|
||||
const dir = await createTempDir();
|
||||
const scriptPath = path.join(dir, "openclaw", "dist", "entry.js");
|
||||
const shimPath = path.join(dir, "openclaw.cmd");
|
||||
await mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
await writeFile(scriptPath, "console.log('ok')\n", "utf8");
|
||||
await writeFile(shimPath, `@ECHO off\r\n"%~dp0\\openclaw\\dist\\entry.js" %*\r\n`, "utf8");
|
||||
|
||||
const resolved = resolveAcpClientSpawnInvocation(
|
||||
{ serverCommand: shimPath, serverArgs: ["acp", "--verbose"] },
|
||||
{
|
||||
platform: "win32",
|
||||
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
|
||||
execPath: "C:\\node\\node.exe",
|
||||
},
|
||||
);
|
||||
expect(resolved.command).toBe("C:\\node\\node.exe");
|
||||
expect(resolved.args).toEqual([scriptPath, "acp", "--verbose"]);
|
||||
expect(resolved.shell).toBeUndefined();
|
||||
expect(resolved.windowsHide).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to shell mode 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePermissionRequest", () => {
|
||||
it("auto-approves safe tools without prompting", async () => {
|
||||
const prompt = vi.fn(async () => true);
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { isKnownCoreToolId } from "../agents/tool-catalog.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
import {
|
||||
materializeWindowsSpawnProgram,
|
||||
resolveWindowsSpawnProgram,
|
||||
} from "../plugin-sdk/windows-spawn.js";
|
||||
import { DANGEROUS_ACP_TOOLS } from "../security/dangerous-tools.js";
|
||||
|
||||
const SAFE_AUTO_APPROVE_TOOL_IDS = new Set(["read", "search", "web_search", "memory_search"]);
|
||||
@@ -348,6 +352,39 @@ export function resolveAcpClientSpawnEnv(
|
||||
return { ...baseEnv, OPENCLAW_SHELL: "acp-client" };
|
||||
}
|
||||
|
||||
type AcpSpawnRuntime = {
|
||||
platform: NodeJS.Platform;
|
||||
env: NodeJS.ProcessEnv;
|
||||
execPath: string;
|
||||
};
|
||||
|
||||
const DEFAULT_ACP_SPAWN_RUNTIME: AcpSpawnRuntime = {
|
||||
platform: process.platform,
|
||||
env: process.env,
|
||||
execPath: process.execPath,
|
||||
};
|
||||
|
||||
export function resolveAcpClientSpawnInvocation(
|
||||
params: { serverCommand: string; serverArgs: string[] },
|
||||
runtime: AcpSpawnRuntime = DEFAULT_ACP_SPAWN_RUNTIME,
|
||||
): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
|
||||
const program = resolveWindowsSpawnProgram({
|
||||
command: params.serverCommand,
|
||||
platform: runtime.platform,
|
||||
env: runtime.env,
|
||||
execPath: runtime.execPath,
|
||||
packageName: "openclaw",
|
||||
allowShellFallback: true,
|
||||
});
|
||||
const resolved = materializeWindowsSpawnProgram(program, params.serverArgs);
|
||||
return {
|
||||
command: resolved.command,
|
||||
args: resolved.argv,
|
||||
shell: resolved.shell,
|
||||
windowsHide: resolved.windowsHide,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSelfEntryPath(): string | null {
|
||||
// Prefer a path relative to the built module location (dist/acp/client.js -> dist/entry.js).
|
||||
try {
|
||||
@@ -413,13 +450,24 @@ export async function createAcpClient(opts: AcpClientOptions = {}): Promise<AcpC
|
||||
const entryPath = resolveSelfEntryPath();
|
||||
const serverCommand = opts.serverCommand ?? (entryPath ? process.execPath : "openclaw");
|
||||
const effectiveArgs = opts.serverCommand || !entryPath ? serverArgs : [entryPath, ...serverArgs];
|
||||
const spawnEnv = resolveAcpClientSpawnEnv();
|
||||
const spawnInvocation = resolveAcpClientSpawnInvocation(
|
||||
{ serverCommand, serverArgs: effectiveArgs },
|
||||
{
|
||||
platform: process.platform,
|
||||
env: spawnEnv,
|
||||
execPath: process.execPath,
|
||||
},
|
||||
);
|
||||
|
||||
log(`spawning: ${serverCommand} ${effectiveArgs.join(" ")}`);
|
||||
log(`spawning: ${spawnInvocation.command} ${spawnInvocation.args.join(" ")}`);
|
||||
|
||||
const agent = spawn(serverCommand, effectiveArgs, {
|
||||
const agent = spawn(spawnInvocation.command, spawnInvocation.args, {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
cwd,
|
||||
env: resolveAcpClientSpawnEnv(),
|
||||
env: spawnEnv,
|
||||
shell: spawnInvocation.shell,
|
||||
windowsHide: spawnInvocation.windowsHide,
|
||||
});
|
||||
|
||||
if (!agent.stdin || !agent.stdout) {
|
||||
|
||||
Reference in New Issue
Block a user