mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(acp): strip provider auth env for child ACP processes (openclaw#42250)
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
ACPX_PINNED_VERSION,
|
||||
createAcpxPluginConfigSchema,
|
||||
resolveAcpxPluginConfig,
|
||||
toAcpMcpServers,
|
||||
} from "./config.js";
|
||||
|
||||
describe("acpx plugin config parsing", () => {
|
||||
@@ -20,9 +19,9 @@ describe("acpx plugin config parsing", () => {
|
||||
expect(resolved.command).toBe(ACPX_BUNDLED_BIN);
|
||||
expect(resolved.expectedVersion).toBe(ACPX_PINNED_VERSION);
|
||||
expect(resolved.allowPluginLocalInstall).toBe(true);
|
||||
expect(resolved.stripProviderAuthEnvVars).toBe(true);
|
||||
expect(resolved.cwd).toBe(path.resolve("/tmp/workspace"));
|
||||
expect(resolved.strictWindowsCmdWrapper).toBe(true);
|
||||
expect(resolved.mcpServers).toEqual({});
|
||||
});
|
||||
|
||||
it("accepts command override and disables plugin-local auto-install", () => {
|
||||
@@ -37,6 +36,7 @@ describe("acpx plugin config parsing", () => {
|
||||
expect(resolved.command).toBe(path.resolve(command));
|
||||
expect(resolved.expectedVersion).toBeUndefined();
|
||||
expect(resolved.allowPluginLocalInstall).toBe(false);
|
||||
expect(resolved.stripProviderAuthEnvVars).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves relative command paths against workspace directory", () => {
|
||||
@@ -50,6 +50,7 @@ describe("acpx plugin config parsing", () => {
|
||||
expect(resolved.command).toBe(path.resolve("/home/user/repos/openclaw", "../acpx/dist/cli.js"));
|
||||
expect(resolved.expectedVersion).toBeUndefined();
|
||||
expect(resolved.allowPluginLocalInstall).toBe(false);
|
||||
expect(resolved.stripProviderAuthEnvVars).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps bare command names as-is", () => {
|
||||
@@ -63,6 +64,7 @@ describe("acpx plugin config parsing", () => {
|
||||
expect(resolved.command).toBe("acpx");
|
||||
expect(resolved.expectedVersion).toBeUndefined();
|
||||
expect(resolved.allowPluginLocalInstall).toBe(false);
|
||||
expect(resolved.stripProviderAuthEnvVars).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts exact expectedVersion override", () => {
|
||||
@@ -78,6 +80,7 @@ describe("acpx plugin config parsing", () => {
|
||||
expect(resolved.command).toBe(path.resolve(command));
|
||||
expect(resolved.expectedVersion).toBe("0.1.99");
|
||||
expect(resolved.allowPluginLocalInstall).toBe(false);
|
||||
expect(resolved.stripProviderAuthEnvVars).toBe(false);
|
||||
});
|
||||
|
||||
it("treats expectedVersion=any as no version constraint", () => {
|
||||
@@ -134,97 +137,4 @@ describe("acpx plugin config parsing", () => {
|
||||
}),
|
||||
).toThrow("strictWindowsCmdWrapper must be a boolean");
|
||||
});
|
||||
|
||||
it("accepts mcp server maps", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
mcpServers: {
|
||||
canva: {
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
|
||||
env: {
|
||||
CANVA_TOKEN: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(resolved.mcpServers).toEqual({
|
||||
canva: {
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
|
||||
env: {
|
||||
CANVA_TOKEN: "secret",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid mcp server definitions", () => {
|
||||
expect(() =>
|
||||
resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
mcpServers: {
|
||||
canva: {
|
||||
command: "npx",
|
||||
args: ["-y", 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
).toThrow(
|
||||
"mcpServers.canva must have a command string, optional args array, and optional env object",
|
||||
);
|
||||
});
|
||||
|
||||
it("schema accepts mcp server config", () => {
|
||||
const schema = createAcpxPluginConfigSchema();
|
||||
if (!schema.safeParse) {
|
||||
throw new Error("acpx config schema missing safeParse");
|
||||
}
|
||||
const parsed = schema.safeParse({
|
||||
mcpServers: {
|
||||
canva: {
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-remote@latest"],
|
||||
env: {
|
||||
CANVA_TOKEN: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toAcpMcpServers", () => {
|
||||
it("converts plugin config maps into ACP stdio MCP entries", () => {
|
||||
expect(
|
||||
toAcpMcpServers({
|
||||
canva: {
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
|
||||
env: {
|
||||
CANVA_TOKEN: "secret",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
name: "canva",
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
|
||||
env: [
|
||||
{
|
||||
name: "CANVA_TOKEN",
|
||||
value: "secret",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ export type ResolvedAcpxPluginConfig = {
|
||||
command: string;
|
||||
expectedVersion?: string;
|
||||
allowPluginLocalInstall: boolean;
|
||||
stripProviderAuthEnvVars: boolean;
|
||||
installCommand: string;
|
||||
cwd: string;
|
||||
permissionMode: AcpxPermissionMode;
|
||||
@@ -332,6 +333,7 @@ export function resolveAcpxPluginConfig(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
const allowPluginLocalInstall = command === ACPX_BUNDLED_BIN;
|
||||
const stripProviderAuthEnvVars = command === ACPX_BUNDLED_BIN;
|
||||
const configuredExpectedVersion = normalized.expectedVersion;
|
||||
const expectedVersion =
|
||||
configuredExpectedVersion === ACPX_VERSION_ANY
|
||||
@@ -343,6 +345,7 @@ export function resolveAcpxPluginConfig(params: {
|
||||
command,
|
||||
expectedVersion,
|
||||
allowPluginLocalInstall,
|
||||
stripProviderAuthEnvVars,
|
||||
installCommand,
|
||||
cwd,
|
||||
permissionMode: normalized.permissionMode ?? DEFAULT_PERMISSION_MODE,
|
||||
|
||||
@@ -77,6 +77,7 @@ describe("acpx ensure", () => {
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
args: ["--version"],
|
||||
cwd: "/plugin",
|
||||
stripProviderAuthEnvVars: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +149,30 @@ describe("acpx ensure", () => {
|
||||
command: "/custom/acpx",
|
||||
args: ["--help"],
|
||||
cwd: "/custom",
|
||||
stripProviderAuthEnvVars: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards stripProviderAuthEnvVars to version checks", async () => {
|
||||
spawnAndCollectMock.mockResolvedValueOnce({
|
||||
stdout: "Usage: acpx [options]\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await checkAcpxVersion({
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
cwd: "/plugin",
|
||||
expectedVersion: undefined,
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
|
||||
expect(spawnAndCollectMock).toHaveBeenCalledWith({
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
args: ["--help"],
|
||||
cwd: "/plugin",
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,6 +211,54 @@ describe("acpx ensure", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("threads stripProviderAuthEnvVars through version probes and install", async () => {
|
||||
spawnAndCollectMock
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "acpx 0.0.9\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
error: null,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: "added 1 package\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
error: null,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
stdout: `acpx ${ACPX_PINNED_VERSION}\n`,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await ensureAcpx({
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
pluginRoot: "/plugin",
|
||||
expectedVersion: ACPX_PINNED_VERSION,
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
|
||||
expect(spawnAndCollectMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
args: ["--version"],
|
||||
cwd: "/plugin",
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
expect(spawnAndCollectMock.mock.calls[1]?.[0]).toMatchObject({
|
||||
command: "npm",
|
||||
args: ["install", "--omit=dev", "--no-save", `acpx@${ACPX_PINNED_VERSION}`],
|
||||
cwd: "/plugin",
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
expect(spawnAndCollectMock.mock.calls[2]?.[0]).toMatchObject({
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
args: ["--version"],
|
||||
cwd: "/plugin",
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails with actionable error when npm install fails", async () => {
|
||||
spawnAndCollectMock
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -102,6 +102,7 @@ export async function checkAcpxVersion(params: {
|
||||
command: string;
|
||||
cwd?: string;
|
||||
expectedVersion?: string;
|
||||
stripProviderAuthEnvVars?: boolean;
|
||||
spawnOptions?: SpawnCommandOptions;
|
||||
}): Promise<AcpxVersionCheckResult> {
|
||||
const expectedVersion = params.expectedVersion?.trim() || undefined;
|
||||
@@ -113,6 +114,7 @@ export async function checkAcpxVersion(params: {
|
||||
command: params.command,
|
||||
args: probeArgs,
|
||||
cwd,
|
||||
stripProviderAuthEnvVars: params.stripProviderAuthEnvVars,
|
||||
};
|
||||
let result: Awaited<ReturnType<typeof spawnAndCollect>>;
|
||||
try {
|
||||
@@ -198,6 +200,7 @@ export async function ensureAcpx(params: {
|
||||
pluginRoot?: string;
|
||||
expectedVersion?: string;
|
||||
allowInstall?: boolean;
|
||||
stripProviderAuthEnvVars?: boolean;
|
||||
spawnOptions?: SpawnCommandOptions;
|
||||
}): Promise<void> {
|
||||
if (pendingEnsure) {
|
||||
@@ -214,6 +217,7 @@ export async function ensureAcpx(params: {
|
||||
command: params.command,
|
||||
cwd: pluginRoot,
|
||||
expectedVersion,
|
||||
stripProviderAuthEnvVars: params.stripProviderAuthEnvVars,
|
||||
spawnOptions: params.spawnOptions,
|
||||
});
|
||||
if (precheck.ok) {
|
||||
@@ -231,6 +235,7 @@ export async function ensureAcpx(params: {
|
||||
command: "npm",
|
||||
args: ["install", "--omit=dev", "--no-save", `acpx@${installVersion}`],
|
||||
cwd: pluginRoot,
|
||||
stripProviderAuthEnvVars: params.stripProviderAuthEnvVars,
|
||||
});
|
||||
|
||||
if (install.error) {
|
||||
@@ -252,6 +257,7 @@ export async function ensureAcpx(params: {
|
||||
command: params.command,
|
||||
cwd: pluginRoot,
|
||||
expectedVersion,
|
||||
stripProviderAuthEnvVars: params.stripProviderAuthEnvVars,
|
||||
spawnOptions: params.spawnOptions,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { spawnAndCollectMock } = vi.hoisted(() => ({
|
||||
spawnAndCollectMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./process.js", () => ({
|
||||
spawnAndCollect: spawnAndCollectMock,
|
||||
}));
|
||||
|
||||
import { __testing, resolveAcpxAgentCommand } from "./mcp-agent-command.js";
|
||||
|
||||
describe("resolveAcpxAgentCommand", () => {
|
||||
it("threads stripProviderAuthEnvVars through the config show probe", async () => {
|
||||
spawnAndCollectMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
agents: {
|
||||
codex: {
|
||||
command: "custom-codex",
|
||||
},
|
||||
},
|
||||
}),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const command = await resolveAcpxAgentCommand({
|
||||
acpxCommand: "/plugin/node_modules/.bin/acpx",
|
||||
cwd: "/plugin",
|
||||
agent: "codex",
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
|
||||
expect(command).toBe("custom-codex");
|
||||
expect(spawnAndCollectMock).toHaveBeenCalledWith(
|
||||
{
|
||||
command: "/plugin/node_modules/.bin/acpx",
|
||||
args: ["--cwd", "/plugin", "config", "show"],
|
||||
cwd: "/plugin",
|
||||
stripProviderAuthEnvVars: true,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildMcpProxyAgentCommand", () => {
|
||||
it("escapes Windows-style proxy paths without double-escaping backslashes", () => {
|
||||
const quoted = __testing.quoteCommandPart(
|
||||
"C:\\repo\\extensions\\acpx\\src\\runtime-internals\\mcp-proxy.mjs",
|
||||
);
|
||||
|
||||
expect(quoted).toBe(
|
||||
'"C:\\\\repo\\\\extensions\\\\acpx\\\\src\\\\runtime-internals\\\\mcp-proxy.mjs"',
|
||||
);
|
||||
expect(quoted).not.toContain("\\\\\\");
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,10 @@ function quoteCommandPart(value: string): string {
|
||||
return `"${value.replace(/["\\]/g, "\\$&")}"`;
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
quoteCommandPart,
|
||||
};
|
||||
|
||||
function toCommandLine(parts: string[]): string {
|
||||
return parts.map(quoteCommandPart).join(" ");
|
||||
}
|
||||
@@ -62,6 +66,7 @@ function readConfiguredAgentOverrides(value: unknown): Record<string, string> {
|
||||
async function loadAgentOverrides(params: {
|
||||
acpxCommand: string;
|
||||
cwd: string;
|
||||
stripProviderAuthEnvVars?: boolean;
|
||||
spawnOptions?: SpawnCommandOptions;
|
||||
}): Promise<Record<string, string>> {
|
||||
const result = await spawnAndCollect(
|
||||
@@ -69,6 +74,7 @@ async function loadAgentOverrides(params: {
|
||||
command: params.acpxCommand,
|
||||
args: ["--cwd", params.cwd, "config", "show"],
|
||||
cwd: params.cwd,
|
||||
stripProviderAuthEnvVars: params.stripProviderAuthEnvVars,
|
||||
},
|
||||
params.spawnOptions,
|
||||
);
|
||||
@@ -87,12 +93,14 @@ export async function resolveAcpxAgentCommand(params: {
|
||||
acpxCommand: string;
|
||||
cwd: string;
|
||||
agent: string;
|
||||
stripProviderAuthEnvVars?: boolean;
|
||||
spawnOptions?: SpawnCommandOptions;
|
||||
}): Promise<string> {
|
||||
const normalizedAgent = normalizeAgentName(params.agent);
|
||||
const overrides = await loadAgentOverrides({
|
||||
acpxCommand: params.acpxCommand,
|
||||
cwd: params.cwd,
|
||||
stripProviderAuthEnvVars: params.stripProviderAuthEnvVars,
|
||||
spawnOptions: params.spawnOptions,
|
||||
});
|
||||
return overrides[normalizedAgent] ?? ACPX_BUILTIN_AGENT_COMMANDS[normalizedAgent] ?? params.agent;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
||||
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 { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createWindowsCmdShimFixture } from "../../../shared/windows-cmd-shim-test-fixtures.js";
|
||||
import {
|
||||
resolveSpawnCommand,
|
||||
@@ -28,6 +28,7 @@ async function createTempDir(): Promise<string> {
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (!dir) {
|
||||
@@ -289,4 +290,99 @@ describe("spawnAndCollect", () => {
|
||||
const result = await resultPromise;
|
||||
expect(result.error?.name).toBe("AbortError");
|
||||
});
|
||||
|
||||
it("strips shared provider auth env vars from spawned acpx children", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-secret");
|
||||
vi.stubEnv("GITHUB_TOKEN", "gh-secret");
|
||||
vi.stubEnv("HF_TOKEN", "hf-secret");
|
||||
vi.stubEnv("OPENCLAW_API_KEY", "keep-me");
|
||||
|
||||
const result = await spawnAndCollect({
|
||||
command: process.execPath,
|
||||
args: [
|
||||
"-e",
|
||||
"process.stdout.write(JSON.stringify({openai:process.env.OPENAI_API_KEY,github:process.env.GITHUB_TOKEN,hf:process.env.HF_TOKEN,openclaw:process.env.OPENCLAW_API_KEY,shell:process.env.OPENCLAW_SHELL}))",
|
||||
],
|
||||
cwd: process.cwd(),
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.error).toBeNull();
|
||||
|
||||
const parsed = JSON.parse(result.stdout) as {
|
||||
openai?: string;
|
||||
github?: string;
|
||||
hf?: string;
|
||||
openclaw?: string;
|
||||
shell?: string;
|
||||
};
|
||||
expect(parsed.openai).toBeUndefined();
|
||||
expect(parsed.github).toBeUndefined();
|
||||
expect(parsed.hf).toBeUndefined();
|
||||
expect(parsed.openclaw).toBe("keep-me");
|
||||
expect(parsed.shell).toBe("acp");
|
||||
});
|
||||
|
||||
it("strips provider auth env vars case-insensitively", async () => {
|
||||
vi.stubEnv("OpenAI_Api_Key", "openai-secret");
|
||||
vi.stubEnv("Github_Token", "gh-secret");
|
||||
vi.stubEnv("OPENCLAW_API_KEY", "keep-me");
|
||||
|
||||
const result = await spawnAndCollect({
|
||||
command: process.execPath,
|
||||
args: [
|
||||
"-e",
|
||||
"process.stdout.write(JSON.stringify({openai:process.env.OpenAI_Api_Key,github:process.env.Github_Token,openclaw:process.env.OPENCLAW_API_KEY,shell:process.env.OPENCLAW_SHELL}))",
|
||||
],
|
||||
cwd: process.cwd(),
|
||||
stripProviderAuthEnvVars: true,
|
||||
});
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.error).toBeNull();
|
||||
|
||||
const parsed = JSON.parse(result.stdout) as {
|
||||
openai?: string;
|
||||
github?: string;
|
||||
openclaw?: string;
|
||||
shell?: string;
|
||||
};
|
||||
expect(parsed.openai).toBeUndefined();
|
||||
expect(parsed.github).toBeUndefined();
|
||||
expect(parsed.openclaw).toBe("keep-me");
|
||||
expect(parsed.shell).toBe("acp");
|
||||
});
|
||||
|
||||
it("preserves provider auth env vars for explicit custom commands by default", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-secret");
|
||||
vi.stubEnv("GITHUB_TOKEN", "gh-secret");
|
||||
vi.stubEnv("HF_TOKEN", "hf-secret");
|
||||
vi.stubEnv("OPENCLAW_API_KEY", "keep-me");
|
||||
|
||||
const result = await spawnAndCollect({
|
||||
command: process.execPath,
|
||||
args: [
|
||||
"-e",
|
||||
"process.stdout.write(JSON.stringify({openai:process.env.OPENAI_API_KEY,github:process.env.GITHUB_TOKEN,hf:process.env.HF_TOKEN,openclaw:process.env.OPENCLAW_API_KEY,shell:process.env.OPENCLAW_SHELL}))",
|
||||
],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.error).toBeNull();
|
||||
|
||||
const parsed = JSON.parse(result.stdout) as {
|
||||
openai?: string;
|
||||
github?: string;
|
||||
hf?: string;
|
||||
openclaw?: string;
|
||||
shell?: string;
|
||||
};
|
||||
expect(parsed.openai).toBe("openai-secret");
|
||||
expect(parsed.github).toBe("gh-secret");
|
||||
expect(parsed.hf).toBe("hf-secret");
|
||||
expect(parsed.openclaw).toBe("keep-me");
|
||||
expect(parsed.shell).toBe("acp");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,9 @@ import type {
|
||||
} from "openclaw/plugin-sdk/acpx";
|
||||
import {
|
||||
applyWindowsSpawnProgramPolicy,
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
materializeWindowsSpawnProgram,
|
||||
omitEnvKeysCaseInsensitive,
|
||||
resolveWindowsSpawnProgramCandidate,
|
||||
} from "openclaw/plugin-sdk/acpx";
|
||||
|
||||
@@ -125,6 +127,7 @@ export function spawnWithResolvedCommand(
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
stripProviderAuthEnvVars?: boolean;
|
||||
},
|
||||
options?: SpawnCommandOptions,
|
||||
): ChildProcessWithoutNullStreams {
|
||||
@@ -136,9 +139,15 @@ export function spawnWithResolvedCommand(
|
||||
options,
|
||||
);
|
||||
|
||||
const childEnv = omitEnvKeysCaseInsensitive(
|
||||
process.env,
|
||||
params.stripProviderAuthEnvVars ? listKnownProviderAuthEnvVarNames() : [],
|
||||
);
|
||||
childEnv.OPENCLAW_SHELL = "acp";
|
||||
|
||||
return spawn(resolved.command, resolved.args, {
|
||||
cwd: params.cwd,
|
||||
env: { ...process.env, OPENCLAW_SHELL: "acp" },
|
||||
env: childEnv,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
shell: resolved.shell,
|
||||
windowsHide: resolved.windowsHide,
|
||||
@@ -180,6 +189,7 @@ export async function spawnAndCollect(
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
stripProviderAuthEnvVars?: boolean;
|
||||
},
|
||||
options?: SpawnCommandOptions,
|
||||
runtime?: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { runAcpRuntimeAdapterContract } from "../../../src/acp/runtime/adapter-contract.testkit.js";
|
||||
import { AcpxRuntime, decodeAcpxRuntimeHandleState } from "./runtime.js";
|
||||
import {
|
||||
@@ -19,13 +19,14 @@ beforeAll(async () => {
|
||||
{
|
||||
command: "/definitely/missing/acpx",
|
||||
allowPluginLocalInstall: false,
|
||||
stripProviderAuthEnvVars: false,
|
||||
installCommand: "n/a",
|
||||
cwd: process.cwd(),
|
||||
mcpServers: {},
|
||||
permissionMode: "approve-reads",
|
||||
nonInteractivePermissions: "fail",
|
||||
strictWindowsCmdWrapper: true,
|
||||
queueOwnerTtlSeconds: 0.1,
|
||||
mcpServers: {},
|
||||
},
|
||||
{ logger: NOOP_LOGGER },
|
||||
);
|
||||
@@ -165,7 +166,7 @@ describe("AcpxRuntime", () => {
|
||||
for await (const _event of runtime.runTurn({
|
||||
handle,
|
||||
text: "describe this image",
|
||||
attachments: [{ mediaType: "image/png", data: "aW1hZ2UtYnl0ZXM=" }],
|
||||
attachments: [{ mediaType: "image/png", data: "aW1hZ2UtYnl0ZXM=" }], // pragma: allowlist secret
|
||||
mode: "prompt",
|
||||
requestId: "req-image",
|
||||
})) {
|
||||
@@ -186,6 +187,40 @@ describe("AcpxRuntime", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves provider auth env vars when runtime uses a custom acpx command", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-secret"); // pragma: allowlist secret
|
||||
vi.stubEnv("GITHUB_TOKEN", "gh-secret"); // pragma: allowlist secret
|
||||
|
||||
try {
|
||||
const { runtime, logPath } = await createMockRuntimeFixture();
|
||||
const handle = await runtime.ensureSession({
|
||||
sessionKey: "agent:codex:acp:custom-env",
|
||||
agent: "codex",
|
||||
mode: "persistent",
|
||||
});
|
||||
|
||||
for await (const _event of runtime.runTurn({
|
||||
handle,
|
||||
text: "custom-env",
|
||||
mode: "prompt",
|
||||
requestId: "req-custom-env",
|
||||
})) {
|
||||
// Drain events; assertions inspect the mock runtime log.
|
||||
}
|
||||
|
||||
const logs = await readMockRuntimeLogEntries(logPath);
|
||||
const prompt = logs.find(
|
||||
(entry) =>
|
||||
entry.kind === "prompt" &&
|
||||
String(entry.sessionName ?? "") === "agent:codex:acp:custom-env",
|
||||
);
|
||||
expect(prompt?.openaiApiKey).toBe("openai-secret");
|
||||
expect(prompt?.githubToken).toBe("gh-secret");
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves leading spaces across streamed text deltas", async () => {
|
||||
const runtime = sharedFixture?.runtime;
|
||||
expect(runtime).toBeDefined();
|
||||
@@ -395,7 +430,7 @@ describe("AcpxRuntime", () => {
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-remote@latest", "https://mcp.canva.com/mcp"],
|
||||
env: {
|
||||
CANVA_TOKEN: "secret",
|
||||
CANVA_TOKEN: "secret", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -170,6 +170,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
command: this.config.command,
|
||||
cwd: this.config.cwd,
|
||||
expectedVersion: this.config.expectedVersion,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
spawnOptions: this.spawnCommandOptions,
|
||||
});
|
||||
if (!versionCheck.ok) {
|
||||
@@ -183,6 +184,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
command: this.config.command,
|
||||
args: ["--help"],
|
||||
cwd: this.config.cwd,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
},
|
||||
this.spawnCommandOptions,
|
||||
);
|
||||
@@ -309,6 +311,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
command: this.config.command,
|
||||
args,
|
||||
cwd: state.cwd,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
},
|
||||
this.spawnCommandOptions,
|
||||
);
|
||||
@@ -495,6 +498,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
command: this.config.command,
|
||||
cwd: this.config.cwd,
|
||||
expectedVersion: this.config.expectedVersion,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
spawnOptions: this.spawnCommandOptions,
|
||||
});
|
||||
if (!versionCheck.ok) {
|
||||
@@ -518,6 +522,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
command: this.config.command,
|
||||
args: ["--help"],
|
||||
cwd: this.config.cwd,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
},
|
||||
this.spawnCommandOptions,
|
||||
);
|
||||
@@ -683,6 +688,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
acpxCommand: this.config.command,
|
||||
cwd: params.cwd,
|
||||
agent: params.agent,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
spawnOptions: this.spawnCommandOptions,
|
||||
});
|
||||
const resolved = buildMcpProxyAgentCommand({
|
||||
@@ -705,6 +711,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
command: this.config.command,
|
||||
args: params.args,
|
||||
cwd: params.cwd,
|
||||
stripProviderAuthEnvVars: this.config.stripProviderAuthEnvVars,
|
||||
},
|
||||
this.spawnCommandOptions,
|
||||
{
|
||||
|
||||
@@ -89,6 +89,11 @@ describe("createAcpxRuntimeService", () => {
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(ensureAcpxSpy).toHaveBeenCalledOnce();
|
||||
expect(ensureAcpxSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
stripProviderAuthEnvVars: true,
|
||||
}),
|
||||
);
|
||||
expect(probeAvailabilitySpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
@@ -59,9 +59,8 @@ export function createAcpxRuntimeService(
|
||||
});
|
||||
const expectedVersionLabel = pluginConfig.expectedVersion ?? "any";
|
||||
const installLabel = pluginConfig.allowPluginLocalInstall ? "enabled" : "disabled";
|
||||
const mcpServerCount = Object.keys(pluginConfig.mcpServers).length;
|
||||
ctx.logger.info(
|
||||
`acpx runtime backend registered (command: ${pluginConfig.command}, expectedVersion: ${expectedVersionLabel}, pluginLocalInstall: ${installLabel}${mcpServerCount > 0 ? `, mcpServers: ${mcpServerCount}` : ""})`,
|
||||
`acpx runtime backend registered (command: ${pluginConfig.command}, expectedVersion: ${expectedVersionLabel}, pluginLocalInstall: ${installLabel})`,
|
||||
);
|
||||
|
||||
lifecycleRevision += 1;
|
||||
@@ -73,6 +72,7 @@ export function createAcpxRuntimeService(
|
||||
logger: ctx.logger,
|
||||
expectedVersion: pluginConfig.expectedVersion,
|
||||
allowInstall: pluginConfig.allowPluginLocalInstall,
|
||||
stripProviderAuthEnvVars: pluginConfig.stripProviderAuthEnvVars,
|
||||
spawnOptions: {
|
||||
strictWindowsCmdWrapper: pluginConfig.strictWindowsCmdWrapper,
|
||||
},
|
||||
|
||||
@@ -204,6 +204,8 @@ if (command === "prompt") {
|
||||
sessionName: sessionFromOption,
|
||||
stdinText,
|
||||
openclawShell,
|
||||
openaiApiKey: process.env.OPENAI_API_KEY || "",
|
||||
githubToken: process.env.GITHUB_TOKEN || "",
|
||||
});
|
||||
const requestId = "req-1";
|
||||
|
||||
@@ -326,6 +328,7 @@ export async function createMockRuntimeFixture(params?: {
|
||||
const config: ResolvedAcpxPluginConfig = {
|
||||
command: scriptPath,
|
||||
allowPluginLocalInstall: false,
|
||||
stripProviderAuthEnvVars: false,
|
||||
installCommand: "n/a",
|
||||
cwd: dir,
|
||||
permissionMode: params?.permissionMode ?? "approve-all",
|
||||
@@ -378,6 +381,7 @@ export async function readMockRuntimeLogEntries(
|
||||
|
||||
export async function cleanupMockRuntimeFixtures(): Promise<void> {
|
||||
delete process.env.MOCK_ACPX_LOG;
|
||||
delete process.env.MOCK_ACPX_CONFIG_SHOW_AGENTS;
|
||||
sharedMockCliScriptPath = null;
|
||||
logFileSequence = 0;
|
||||
while (tempDirs.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user