mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:24:35 +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:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.
|
- Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.
|
||||||
- Android/Nodes reliability: reject `facing=both` when `deviceId` is set to avoid mislabeled duplicate captures, allow notification `open`/`reply` on non-clearable entries while still gating dismiss, trigger listener rebind before notification actions, and scale invoke-result ack timeout to invoke budget for large clip payloads. (#28260) Thanks @obviyus.
|
- Android/Nodes reliability: reject `facing=both` when `deviceId` is set to avoid mislabeled duplicate captures, allow notification `open`/`reply` on non-clearable entries while still gating dismiss, trigger listener rebind before notification actions, and scale invoke-result ack timeout to invoke budget for large clip payloads. (#28260) Thanks @obviyus.
|
||||||
- Windows/Plugin install: avoid `spawn EINVAL` on Windows npm/npx invocations by resolving to `node` + npm CLI scripts instead of spawning `.cmd` directly. Landed from contributor PR #31147 by @codertony. Thanks @codertony.
|
- Windows/Plugin install: avoid `spawn EINVAL` on Windows npm/npx invocations by resolving to `node` + npm CLI scripts instead of spawning `.cmd` directly. Landed from contributor PR #31147 by @codertony. Thanks @codertony.
|
||||||
|
- Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for `.cmd` shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.
|
||||||
- LINE/Voice transcription: classify M4A voice media as `audio/mp4` (not `video/mp4`) by checking the MPEG-4 `ftyp` major brand (`M4A ` / `M4B `), restoring voice transcription for LINE voice messages. Landed from contributor PR #31151 by @scoootscooob. Thanks @scoootscooob.
|
- LINE/Voice transcription: classify M4A voice media as `audio/mp4` (not `video/mp4`) by checking the MPEG-4 `ftyp` major brand (`M4A ` / `M4B `), restoring voice transcription for LINE voice messages. Landed from contributor PR #31151 by @scoootscooob. Thanks @scoootscooob.
|
||||||
- Slack/Announce target account routing: enable session-backed announce-target lookup for Slack so multi-account announces resolve the correct `accountId` instead of defaulting to bot-token context. Landed from contributor PR #31028 by @taw0002. Thanks @taw0002.
|
- Slack/Announce target account routing: enable session-backed announce-target lookup for Slack so multi-account announces resolve the correct `accountId` instead of defaulting to bot-token context. Landed from contributor PR #31028 by @taw0002. Thanks @taw0002.
|
||||||
- Android/Voice screen TTS: stream assistant speech via ElevenLabs WebSocket in Talk Mode, stop cleanly on speaker mute/barge-in, and ignore stale out-of-order stream events. (#29521) Thanks @gregmousseau.
|
- Android/Voice screen TTS: stream assistant speech via ElevenLabs WebSocket in Talk Mode, stop cleanly on speaker mute/barge-in, and ignore stale out-of-order stream events. (#29521) Thanks @gregmousseau.
|
||||||
|
|||||||
@@ -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 type { RequestPermissionRequest } from "@agentclientprotocol/sdk";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { resolveAcpClientSpawnEnv, resolvePermissionRequest } from "./client.js";
|
import {
|
||||||
|
resolveAcpClientSpawnEnv,
|
||||||
|
resolveAcpClientSpawnInvocation,
|
||||||
|
resolvePermissionRequest,
|
||||||
|
} from "./client.js";
|
||||||
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
|
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
|
||||||
|
|
||||||
function makePermissionRequest(
|
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", () => {
|
describe("resolveAcpClientSpawnEnv", () => {
|
||||||
it("sets OPENCLAW_SHELL marker and preserves existing env values", () => {
|
it("sets OPENCLAW_SHELL marker and preserves existing env values", () => {
|
||||||
const env = resolveAcpClientSpawnEnv({
|
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", () => {
|
describe("resolvePermissionRequest", () => {
|
||||||
it("auto-approves safe tools without prompting", async () => {
|
it("auto-approves safe tools without prompting", async () => {
|
||||||
const prompt = vi.fn(async () => true);
|
const prompt = vi.fn(async () => true);
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
import { isKnownCoreToolId } from "../agents/tool-catalog.js";
|
import { isKnownCoreToolId } from "../agents/tool-catalog.js";
|
||||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.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";
|
import { DANGEROUS_ACP_TOOLS } from "../security/dangerous-tools.js";
|
||||||
|
|
||||||
const SAFE_AUTO_APPROVE_TOOL_IDS = new Set(["read", "search", "web_search", "memory_search"]);
|
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" };
|
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 {
|
function resolveSelfEntryPath(): string | null {
|
||||||
// Prefer a path relative to the built module location (dist/acp/client.js -> dist/entry.js).
|
// Prefer a path relative to the built module location (dist/acp/client.js -> dist/entry.js).
|
||||||
try {
|
try {
|
||||||
@@ -413,13 +450,24 @@ export async function createAcpClient(opts: AcpClientOptions = {}): Promise<AcpC
|
|||||||
const entryPath = resolveSelfEntryPath();
|
const entryPath = resolveSelfEntryPath();
|
||||||
const serverCommand = opts.serverCommand ?? (entryPath ? process.execPath : "openclaw");
|
const serverCommand = opts.serverCommand ?? (entryPath ? process.execPath : "openclaw");
|
||||||
const effectiveArgs = opts.serverCommand || !entryPath ? serverArgs : [entryPath, ...serverArgs];
|
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"],
|
stdio: ["pipe", "pipe", "inherit"],
|
||||||
cwd,
|
cwd,
|
||||||
env: resolveAcpClientSpawnEnv(),
|
env: spawnEnv,
|
||||||
|
shell: spawnInvocation.shell,
|
||||||
|
windowsHide: spawnInvocation.windowsHide,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!agent.stdin || !agent.stdout) {
|
if (!agent.stdin || !agent.stdout) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
|
import {
|
||||||
|
materializeWindowsSpawnProgram,
|
||||||
|
resolveWindowsSpawnProgram,
|
||||||
|
} from "../../plugin-sdk/windows-spawn.js";
|
||||||
import { sanitizeEnvVars } from "./sanitize-env-vars.js";
|
import { sanitizeEnvVars } from "./sanitize-env-vars.js";
|
||||||
|
|
||||||
type ExecDockerRawOptions = {
|
type ExecDockerRawOptions = {
|
||||||
@@ -26,13 +30,49 @@ function createAbortError(): Error {
|
|||||||
return err;
|
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(
|
export function execDockerRaw(
|
||||||
args: string[],
|
args: string[],
|
||||||
opts?: ExecDockerRawOptions,
|
opts?: ExecDockerRawOptions,
|
||||||
): Promise<ExecDockerRawResult> {
|
): Promise<ExecDockerRawResult> {
|
||||||
return new Promise<ExecDockerRawResult>((resolve, reject) => {
|
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"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
shell: spawnInvocation.shell,
|
||||||
|
windowsHide: spawnInvocation.windowsHide,
|
||||||
});
|
});
|
||||||
const stdoutChunks: Buffer[] = [];
|
const stdoutChunks: Buffer[] = [];
|
||||||
const stderrChunks: Buffer[] = [];
|
const stderrChunks: Buffer[] = [];
|
||||||
|
|||||||
79
src/agents/sandbox/docker.windows.test.ts
Normal file
79
src/agents/sandbox/docker.windows.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -899,6 +899,8 @@ describe("QmdMemoryManager", () => {
|
|||||||
expect(qmdCalls.length).toBeGreaterThan(0);
|
expect(qmdCalls.length).toBeGreaterThan(0);
|
||||||
for (const call of qmdCalls) {
|
for (const call of qmdCalls) {
|
||||||
expect(call[0]).toBe("qmd.cmd");
|
expect(call[0]).toBe("qmd.cmd");
|
||||||
|
const options = call[2] as { shell?: boolean } | undefined;
|
||||||
|
expect(options?.shell).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await manager.close();
|
await manager.close();
|
||||||
@@ -1408,6 +1410,8 @@ describe("QmdMemoryManager", () => {
|
|||||||
);
|
);
|
||||||
expect(mcporterCall).toBeDefined();
|
expect(mcporterCall).toBeDefined();
|
||||||
expect(mcporterCall?.[0]).toBe("mcporter.cmd");
|
expect(mcporterCall?.[0]).toBe("mcporter.cmd");
|
||||||
|
const options = mcporterCall?.[2] as { shell?: boolean } | undefined;
|
||||||
|
expect(options?.shell).toBe(true);
|
||||||
|
|
||||||
await manager.close();
|
await manager.close();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
import {
|
||||||
|
materializeWindowsSpawnProgram,
|
||||||
|
resolveWindowsSpawnProgram,
|
||||||
|
} from "../plugin-sdk/windows-spawn.js";
|
||||||
import { isFileMissingError, statRegularFile } from "./fs-utils.js";
|
import { isFileMissingError, statRegularFile } from "./fs-utils.js";
|
||||||
import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js";
|
import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js";
|
||||||
import {
|
import {
|
||||||
@@ -65,6 +69,23 @@ function resolveWindowsCommandShim(command: string): string {
|
|||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSpawnInvocation(params: {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env: NodeJS.ProcessEnv;
|
||||||
|
packageName: string;
|
||||||
|
}) {
|
||||||
|
const program = resolveWindowsSpawnProgram({
|
||||||
|
command: resolveWindowsCommandShim(params.command),
|
||||||
|
platform: process.platform,
|
||||||
|
env: params.env,
|
||||||
|
execPath: process.execPath,
|
||||||
|
packageName: params.packageName,
|
||||||
|
allowShellFallback: true,
|
||||||
|
});
|
||||||
|
return materializeWindowsSpawnProgram(program, params.args);
|
||||||
|
}
|
||||||
|
|
||||||
function hasHanScript(value: string): boolean {
|
function hasHanScript(value: string): boolean {
|
||||||
return HAN_SCRIPT_RE.test(value);
|
return HAN_SCRIPT_RE.test(value);
|
||||||
}
|
}
|
||||||
@@ -1066,9 +1087,17 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
opts?: { timeoutMs?: number; discardOutput?: boolean },
|
opts?: { timeoutMs?: number; discardOutput?: boolean },
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const child = spawn(resolveWindowsCommandShim(this.qmd.command), args, {
|
const spawnInvocation = resolveSpawnInvocation({
|
||||||
|
command: this.qmd.command,
|
||||||
|
args,
|
||||||
|
env: this.env,
|
||||||
|
packageName: "qmd",
|
||||||
|
});
|
||||||
|
const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
|
||||||
env: this.env,
|
env: this.env,
|
||||||
cwd: this.workspaceDir,
|
cwd: this.workspaceDir,
|
||||||
|
shell: spawnInvocation.shell,
|
||||||
|
windowsHide: spawnInvocation.windowsHide,
|
||||||
});
|
});
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
@@ -1164,10 +1193,18 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
opts?: { timeoutMs?: number },
|
opts?: { timeoutMs?: number },
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const child = spawn(resolveWindowsCommandShim("mcporter"), args, {
|
const spawnInvocation = resolveSpawnInvocation({
|
||||||
|
command: "mcporter",
|
||||||
|
args,
|
||||||
|
env: this.env,
|
||||||
|
packageName: "mcporter",
|
||||||
|
});
|
||||||
|
const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
|
||||||
// Keep mcporter and direct qmd commands on the same agent-scoped XDG state.
|
// Keep mcporter and direct qmd commands on the same agent-scoped XDG state.
|
||||||
env: this.env,
|
env: this.env,
|
||||||
cwd: this.workspaceDir,
|
cwd: this.workspaceDir,
|
||||||
|
shell: spawnInvocation.shell,
|
||||||
|
windowsHide: spawnInvocation.windowsHide,
|
||||||
});
|
});
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user