From 52daf5fbd3c87b3c622fe482ee2abce7e1f26b63 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 05:37:44 +0100 Subject: [PATCH] fix(acpx): stage Claude ACP adapter runtime dependency --- CHANGELOG.md | 1 + extensions/acpx/package.json | 2 +- extensions/acpx/src/codex-auth-bridge.test.ts | 178 ++++++++++++- extensions/acpx/src/codex-auth-bridge.ts | 250 ++++++++++++++++-- extensions/acpx/src/manifest.test.ts | 3 + pnpm-lock.yaml | 6 +- 6 files changed, 408 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 648edba2a16..33719af2c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Export/session: keep inline export HTML scripts and vendor libraries injected after template formatting so generated session exports open with the app code, markdown renderer, and syntax highlighter present. Fixes #41862 and #49957; carries forward #41861 and #68947. Thanks @briannewman, @martenzi, and @armanddp. +- Agents/ACPX: stage the patched Claude ACP adapter as an ACPX runtime dependency and route known Codex/Claude ACP commands through local wrappers, so Gateway runtime no longer depends on live `npx` adapter resolution. Fixes #73202. Thanks @joerod26. - Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868. - Control UI/models: request the configured Gateway model-list view so dashboards with only `models.providers.*.models` show those configured models first instead of flooding the picker with the full built-in catalog. Fixes #65405. Thanks @wbyanclaw. - CLI/models: keep default-model and allowlist pickers on explicit `models.providers.*.models` entries when `models.mode` is `replace` instead of loading the full built-in catalog. Fixes #64950. Thanks @mrozentsvayg. diff --git a/extensions/acpx/package.json b/extensions/acpx/package.json index abcee2cf85d..d0cc1f0a9c2 100644 --- a/extensions/acpx/package.json +++ b/extensions/acpx/package.json @@ -4,11 +4,11 @@ "description": "OpenClaw ACP runtime backend", "type": "module", "dependencies": { + "@agentclientprotocol/claude-agent-acp": "0.31.0", "@zed-industries/codex-acp": "0.12.0", "acpx": "0.6.1" }, "devDependencies": { - "@agentclientprotocol/claude-agent-acp": "0.31.0", "@openclaw/plugin-sdk": "workspace:*" }, "openclaw": { diff --git a/extensions/acpx/src/codex-auth-bridge.test.ts b/extensions/acpx/src/codex-auth-bridge.test.ts index 44065e9f752..1683d1bc3d0 100644 --- a/extensions/acpx/src/codex-auth-bridge.test.ts +++ b/extensions/acpx/src/codex-auth-bridge.test.ts @@ -21,6 +21,10 @@ async function makeTempDir(): Promise { return dir; } +function quoteArg(value: string): string { + return JSON.stringify(value); +} + function restoreEnv(name: keyof typeof previousEnv): void { const value = previousEnv[name]; if (value === undefined) { @@ -42,11 +46,25 @@ function generatedCodexPaths(stateDir: string): { }; } +function generatedClaudePaths(stateDir: string): { + wrapperPath: string; +} { + const baseDir = path.join(stateDir, "acpx"); + return { + wrapperPath: path.join(baseDir, "claude-agent-acp-wrapper.mjs"), + }; +} + function expectCodexWrapperCommand(command: string | undefined, wrapperPath: string): void { expect(command).toContain(process.execPath); expect(command).toContain(wrapperPath); } +function expectClaudeWrapperCommand(command: string | undefined, wrapperPath: string): void { + expect(command).toContain(process.execPath); + expect(command).toContain(wrapperPath); +} + afterEach(async () => { restoreEnv("CODEX_HOME"); restoreEnv("OPENCLAW_AGENT_DIR"); @@ -62,6 +80,7 @@ describe("prepareAcpxCodexAuthConfig", () => { const agentDir = path.join(root, "agent"); const stateDir = path.join(root, "state"); const generated = generatedCodexPaths(stateDir); + const generatedClaude = generatedClaudePaths(stateDir); const installedBinPath = path.join( root, "node_modules", @@ -84,7 +103,9 @@ describe("prepareAcpxCodexAuthConfig", () => { }); expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath); + expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath); await expect(fs.access(generated.wrapperPath)).resolves.toBeUndefined(); + await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined(); const wrapper = await fs.readFile(generated.wrapperPath, "utf8"); expect(wrapper).toContain(JSON.stringify(installedBinPath)); expect(wrapper).toContain("defaultArgs = [installedBinPath]"); @@ -114,6 +135,28 @@ describe("prepareAcpxCodexAuthConfig", () => { expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1"); }); + it("falls back to the patched Claude ACP package when the local adapter is unavailable", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const generated = generatedClaudePaths(stateDir); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: {}, + workspaceDir: root, + }); + + await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + resolveInstalledClaudeAcpBinPath: async () => undefined, + }); + + const wrapper = await fs.readFile(generated.wrapperPath, "utf8"); + expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.0"'); + expect(wrapper).toContain('"--", "claude-agent-acp"'); + expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0"); + expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.1"); + }); + it("uses the bundled Codex ACP dependency by default when it is installed", async () => { const root = await makeTempDir(); const stateDir = path.join(root, "state"); @@ -134,6 +177,26 @@ describe("prepareAcpxCodexAuthConfig", () => { expect(wrapper).toContain("defaultArgs = [installedBinPath]"); }); + it("uses the bundled Claude ACP dependency by default when it is installed", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const generated = generatedClaudePaths(stateDir); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: {}, + workspaceDir: root, + }); + + await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + }); + + const wrapper = await fs.readFile(generated.wrapperPath, "utf8"); + expect(wrapper).toContain("@agentclientprotocol/claude-agent-acp"); + expect(wrapper).toContain("dist/index.js"); + expect(wrapper).toContain("defaultArgs = [installedBinPath]"); + }); + it("launches the locally installed Codex ACP bin with isolated CODEX_HOME", async () => { const root = await makeTempDir(); const stateDir = path.join(root, "state"); @@ -164,6 +227,39 @@ describe("prepareAcpxCodexAuthConfig", () => { expect(path.resolve(String(launched.codexHome))).toBe(expectedCodexHome); }); + it("launches the locally installed Claude ACP bin without going through npm", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const generated = generatedClaudePaths(stateDir); + const installedBinPath = path.join(root, "claude-agent-acp-bin.js"); + await fs.writeFile( + installedBinPath, + "console.log(JSON.stringify({ argv: process.argv.slice(2), codexHome: process.env.CODEX_HOME ?? null }));\n", + "utf8", + ); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: {}, + workspaceDir: root, + }); + + await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + resolveInstalledClaudeAcpBinPath: async () => installedBinPath, + }); + + const { stdout } = await execFileAsync( + process.execPath, + [generated.wrapperPath, "--permission-mode", "bypass"], + { + cwd: root, + }, + ); + const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown }; + expect(launched.argv).toEqual(["--permission-mode", "bypass"]); + expect(launched.codexHome).toBeNull(); + }); + it("does not copy source Codex auth", async () => { const root = await makeTempDir(); const sourceCodexHome = path.join(root, "source-codex"); @@ -208,7 +304,7 @@ describe("prepareAcpxCodexAuthConfig", () => { ).rejects.toMatchObject({ code: "ENOENT" }); }); - it("wraps an explicitly configured Codex agent command with isolated CODEX_HOME", async () => { + it("normalizes an explicitly configured Codex ACP command to the local wrapper", async () => { const root = await makeTempDir(); const sourceCodexHome = path.join(root, "source-codex"); const stateDir = path.join(root, "state"); @@ -237,8 +333,9 @@ describe("prepareAcpxCodexAuthConfig", () => { }); expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath); - expect(resolved.agents.codex).toContain("npx @zed-industries/codex-acp@0.12.0"); - expect(resolved.agents.codex).toContain("-c 'model=\"gpt-5.4\"'"); + expect(resolved.agents.codex).not.toContain("npx @zed-industries/codex-acp@0.12.0"); + expect(resolved.agents.codex).toContain(quoteArg("-c")); + expect(resolved.agents.codex).toContain(quoteArg('model="gpt-5.4"')); const isolatedConfig = await fs.readFile(generated.configPath, "utf8"); expect(isolatedConfig).not.toContain("notify"); expect(isolatedConfig).not.toContain("SkyComputerUseClient"); @@ -247,4 +344,79 @@ describe("prepareAcpxCodexAuthConfig", () => { expect(wrapper).toContain("CODEX_HOME: codexHome"); expect(wrapper).not.toContain(sourceCodexHome); }); + + it("normalizes an explicitly configured Claude ACP npx command to the local wrapper", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const generated = generatedClaudePaths(stateDir); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: { + agents: { + claude: { + command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.0 --permission-mode bypass", + }, + }, + }, + workspaceDir: root, + }); + + const resolved = await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"), + }); + + expectClaudeWrapperCommand(resolved.agents.claude, generated.wrapperPath); + expect(resolved.agents.claude).not.toContain("npx -y @agentclientprotocol/claude-agent-acp"); + expect(resolved.agents.claude).toContain("--permission-mode"); + expect(resolved.agents.claude).toContain("bypass"); + }); + + it("leaves a custom Claude agent command alone", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: { + agents: { + claude: { + command: "node ./custom-claude-wrapper.mjs --flag", + }, + }, + }, + workspaceDir: root, + }); + + const resolved = await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"), + }); + + expect(resolved.agents.claude).toBe("node ./custom-claude-wrapper.mjs --flag"); + }); + + it("does not normalize custom Claude commands that only mention the package name", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const command = + "node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.0 --flag"; + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: { + agents: { + claude: { + command, + }, + }, + }, + workspaceDir: root, + }); + + const resolved = await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"), + }); + + expect(resolved.agents.claude).toBe(command); + }); }); diff --git a/extensions/acpx/src/codex-auth-bridge.ts b/extensions/acpx/src/codex-auth-bridge.ts index e304421659d..762ed5b45f6 100644 --- a/extensions/acpx/src/codex-auth-bridge.ts +++ b/extensions/acpx/src/codex-auth-bridge.ts @@ -6,6 +6,10 @@ import type { ResolvedAcpxPluginConfig } from "./config.js"; const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp"; const CODEX_ACP_PACKAGE_RANGE = "^0.12.0"; const CODEX_ACP_BIN = "codex-acp"; +const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp"; +const CLAUDE_ACP_PACKAGE_VERSION = "0.31.0"; +const CLAUDE_ACP_BIN = "claude-agent-acp"; +const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured"; const requireFromHere = createRequire(import.meta.url); type PackageManifest = { @@ -17,16 +21,68 @@ function quoteCommandPart(value: string): string { return JSON.stringify(value); } +function splitCommandParts(value: string): string[] { + const parts: string[] = []; + let current = ""; + let quote: "'" | '"' | null = null; + let escaping = false; + + for (const ch of value) { + if (escaping) { + current += ch; + escaping = false; + continue; + } + if (ch === "\\" && quote !== "'") { + escaping = true; + continue; + } + if (quote) { + if (ch === quote) { + quote = null; + } else { + current += ch; + } + continue; + } + if (ch === "'" || ch === '"') { + quote = ch; + continue; + } + if (/\s/.test(ch)) { + if (current) { + parts.push(current); + current = ""; + } + continue; + } + current += ch; + } + + if (escaping) { + current += "\\"; + } + if (current) { + parts.push(current); + } + return parts; +} + +function basename(value: string): string { + return value.split(/[\\/]/).pop() ?? value; +} + function resolvePackageBinPath( packageJsonPath: string, manifest: PackageManifest, + binName: string, ): string | undefined { const { bin } = manifest; const relativeBinPath = typeof bin === "string" ? bin : bin && typeof bin === "object" - ? (bin as Record)[CODEX_ACP_BIN] + ? (bin as Record)[binName] : undefined; if (typeof relativeBinPath !== "string" || relativeBinPath.trim() === "") { return undefined; @@ -34,16 +90,17 @@ function resolvePackageBinPath( return path.resolve(path.dirname(packageJsonPath), relativeBinPath); } -async function resolveInstalledCodexAcpBinPath(): Promise { +async function resolveInstalledAcpPackageBinPath( + packageName: string, + binName: string, +): Promise { try { - // Keep OpenClaw's isolated CODEX_HOME wrapper, but launch the plugin-local - // Codex ACP adapter when runtime-deps staging made it available. - const packageJsonPath = requireFromHere.resolve(`${CODEX_ACP_PACKAGE}/package.json`); + const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`); const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as PackageManifest; - if (manifest.name !== CODEX_ACP_PACKAGE) { + if (manifest.name !== packageName) { return undefined; } - const binPath = resolvePackageBinPath(packageJsonPath, manifest); + const binPath = resolvePackageBinPath(packageJsonPath, manifest, binName); if (!binPath) { return undefined; } @@ -54,18 +111,30 @@ async function resolveInstalledCodexAcpBinPath(): Promise { } } -function buildCodexAcpWrapperScript(installedBinPath?: string): string { +async function resolveInstalledCodexAcpBinPath(): Promise { + // Keep OpenClaw's isolated CODEX_HOME wrapper, but launch the plugin-local + // Codex ACP adapter when runtime-deps staging made it available. + return await resolveInstalledAcpPackageBinPath(CODEX_ACP_PACKAGE, CODEX_ACP_BIN); +} + +async function resolveInstalledClaudeAcpBinPath(): Promise { + return await resolveInstalledAcpPackageBinPath(CLAUDE_ACP_PACKAGE, CLAUDE_ACP_BIN); +} + +function buildAdapterWrapperScript(params: { + displayName: string; + packageSpec: string; + binName: string; + installedBinPath?: string; + envSetup: string; +}): string { return `#!/usr/bin/env node import { existsSync } from "node:fs"; import path from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; -const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url)); -const env = { - ...process.env, - CODEX_HOME: codexHome, -}; +${params.envSetup} const configuredArgs = process.argv.slice(2); function resolveNpmCliPath() { @@ -82,7 +151,7 @@ function resolveNpmCliPath() { } const npmCliPath = resolveNpmCliPath(); -const installedBinPath = ${installedBinPath ? quoteCommandPart(installedBinPath) : "undefined"}; +const installedBinPath = ${params.installedBinPath ? quoteCommandPart(params.installedBinPath) : "undefined"}; let defaultCommand; let defaultArgs; if (installedBinPath) { @@ -90,13 +159,22 @@ if (installedBinPath) { defaultArgs = [installedBinPath]; } else if (npmCliPath) { defaultCommand = process.execPath; - defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}", "--", "${CODEX_ACP_BIN}"]; + defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"]; } else { defaultCommand = process.platform === "win32" ? "npx.cmd" : "npx"; - defaultArgs = ["--yes", "--package", "${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}", "--", "${CODEX_ACP_BIN}"]; + defaultArgs = ["--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"]; +} +const command = + configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}" ? configuredArgs[1] : defaultCommand; +const args = + configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}" + ? configuredArgs.slice(2) + : [...defaultArgs, ...configuredArgs]; + +if (!command) { + console.error("[openclaw] missing configured ${params.displayName} ACP command"); + process.exit(1); } -const command = configuredArgs[0] ?? defaultCommand; -const args = configuredArgs.length > 0 ? configuredArgs.slice(1) : defaultArgs; const child = spawn(command, args, { env, @@ -111,7 +189,7 @@ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { } child.on("error", (error) => { - console.error(\`[openclaw] failed to launch isolated Codex ACP wrapper: \${error.message}\`); + console.error(\`[openclaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`); process.exit(1); }); @@ -124,6 +202,33 @@ child.on("exit", (code, signal) => { `; } +function buildCodexAcpWrapperScript(installedBinPath?: string): string { + return buildAdapterWrapperScript({ + displayName: "Codex", + packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}`, + binName: CODEX_ACP_BIN, + installedBinPath, + envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url)); +const env = { + ...process.env, + CODEX_HOME: codexHome, +};`, + }); +} + +function buildClaudeAcpWrapperScript(installedBinPath?: string): string { + return buildAdapterWrapperScript({ + displayName: "Claude", + // This package is patched in OpenClaw; fallback must not float to an unpatched newer release. + packageSpec: `${CLAUDE_ACP_PACKAGE}@${CLAUDE_ACP_PACKAGE_VERSION}`, + binName: CLAUDE_ACP_BIN, + installedBinPath, + envSetup: `const env = { + ...process.env, +};`, + }); +} + async function prepareIsolatedCodexHome(baseDir: string): Promise { const codexHome = path.join(baseDir, "codex-home"); await fs.mkdir(codexHome, { recursive: true }); @@ -145,11 +250,99 @@ async function writeCodexAcpWrapper(baseDir: string, installedBinPath?: string): return wrapperPath; } +async function writeClaudeAcpWrapper(baseDir: string, installedBinPath?: string): Promise { + await fs.mkdir(baseDir, { recursive: true }); + const wrapperPath = path.join(baseDir, "claude-agent-acp-wrapper.mjs"); + await fs.writeFile(wrapperPath, buildClaudeAcpWrapperScript(installedBinPath), { + encoding: "utf8", + }); + await fs.chmod(wrapperPath, 0o755); + return wrapperPath; +} + +function buildWrapperCommand(wrapperPath: string, args: string[] = []): string { + return [process.execPath, wrapperPath, ...args].map(quoteCommandPart).join(" "); +} + +function isAcpPackageSpec(value: string, packageName: string): boolean { + const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`^${escapedPackageName}(?:@.+)?$`, "i").test(value.trim()); +} + +function isAcpBinName(value: string, binName: string): boolean { + const commandName = basename(value); + const escapedBinName = binName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`^${escapedBinName}(?:\\.exe|\\.[cm]?js)?$`, "i").test(commandName); +} + +function isPackageRunnerCommand(value: string): boolean { + return /^(?:npx|npm|pnpm|bunx)(?:\.cmd|\.exe)?$/i.test(basename(value)); +} + +function extractConfiguredAdapterArgs(params: { + configuredCommand?: string; + packageName: string; + binName: string; +}): string[] | undefined { + const trimmedConfiguredCommand = params.configuredCommand?.trim(); + if (!trimmedConfiguredCommand) { + return []; + } + const parts = splitCommandParts(trimmedConfiguredCommand); + if (!parts.length) { + return []; + } + + const packageIndex = parts.findIndex((part) => isAcpPackageSpec(part, params.packageName)); + if (packageIndex >= 0) { + if (!isPackageRunnerCommand(parts[0] ?? "")) { + return undefined; + } + const afterPackage = parts.slice(packageIndex + 1); + if (afterPackage[0] === "--" && isAcpBinName(afterPackage[1] ?? "", params.binName)) { + return afterPackage.slice(2); + } + if (isAcpBinName(afterPackage[0] ?? "", params.binName)) { + return afterPackage.slice(1); + } + return afterPackage[0] === "--" ? afterPackage.slice(1) : afterPackage; + } + + if (isAcpBinName(parts[0] ?? "", params.binName)) { + return parts.slice(1); + } + if (basename(parts[0] ?? "") === "node" && isAcpBinName(parts[1] ?? "", params.binName)) { + return parts.slice(2); + } + + return undefined; +} + function buildCodexAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string { - const baseCommand = `${quoteCommandPart(process.execPath)} ${quoteCommandPart(wrapperPath)}`; - const trimmedConfiguredCommand = configuredCommand?.trim(); - // ACPX stores agent commands as shell-like strings and splits them before spawn. - return trimmedConfiguredCommand ? `${baseCommand} ${trimmedConfiguredCommand}` : baseCommand; + const configuredAdapterArgs = extractConfiguredAdapterArgs({ + configuredCommand, + packageName: CODEX_ACP_PACKAGE, + binName: CODEX_ACP_BIN, + }); + if (configuredAdapterArgs) { + return buildWrapperCommand(wrapperPath, configuredAdapterArgs); + } + return buildWrapperCommand(wrapperPath, [ + RUN_CONFIGURED_COMMAND_SENTINEL, + ...splitCommandParts(configuredCommand?.trim() ?? ""), + ]); +} + +function buildClaudeAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string { + const configuredAdapterArgs = extractConfiguredAdapterArgs({ + configuredCommand, + packageName: CLAUDE_ACP_PACKAGE, + binName: CLAUDE_ACP_BIN, + }); + if (configuredAdapterArgs) { + return buildWrapperCommand(wrapperPath, configuredAdapterArgs); + } + return configuredCommand?.trim() || buildWrapperCommand(wrapperPath); } export async function prepareAcpxCodexAuthConfig(params: { @@ -157,21 +350,28 @@ export async function prepareAcpxCodexAuthConfig(params: { stateDir: string; logger?: unknown; resolveInstalledCodexAcpBinPath?: () => Promise; + resolveInstalledClaudeAcpBinPath?: () => Promise; }): Promise { void params.logger; const codexBaseDir = path.join(params.stateDir, "acpx"); await prepareIsolatedCodexHome(codexBaseDir); - const installedBinPath = await ( + const installedCodexBinPath = await ( params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath )(); - const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedBinPath); + const installedClaudeBinPath = await ( + params.resolveInstalledClaudeAcpBinPath ?? resolveInstalledClaudeAcpBinPath + )(); + const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedCodexBinPath); + const claudeWrapperPath = await writeClaudeAcpWrapper(codexBaseDir, installedClaudeBinPath); const configuredCodexCommand = params.pluginConfig.agents.codex; + const configuredClaudeCommand = params.pluginConfig.agents.claude; return { ...params.pluginConfig, agents: { ...params.pluginConfig.agents, codex: buildCodexAcpWrapperCommand(wrapperPath, configuredCodexCommand), + claude: buildClaudeAcpWrapperCommand(claudeWrapperPath, configuredClaudeCommand), }, }; } diff --git a/extensions/acpx/src/manifest.test.ts b/extensions/acpx/src/manifest.test.ts index 43dd9fb551c..bdc75396439 100644 --- a/extensions/acpx/src/manifest.test.ts +++ b/extensions/acpx/src/manifest.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; type AcpxPackageManifest = { dependencies?: Record; + devDependencies?: Record; openclaw?: { bundle?: { stageRuntimeDependencies?: boolean; @@ -18,6 +19,8 @@ describe("acpx package manifest", () => { expect(packageJson.dependencies?.acpx).toBeDefined(); expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.12.0"); + expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.0"); + expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined(); expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4bf4171265..2ec509f294b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,9 @@ importers: extensions/acpx: dependencies: + '@agentclientprotocol/claude-agent-acp': + specifier: 0.31.0 + version: 0.31.0(patch_hash=e8b472d71289ac8de9813c57d79abac524889ca96f279f6f3ad08043434f6615) '@zed-industries/codex-acp': specifier: 0.12.0 version: 0.12.0 @@ -224,9 +227,6 @@ importers: specifier: 0.6.1 version: 0.6.1 devDependencies: - '@agentclientprotocol/claude-agent-acp': - specifier: 0.31.0 - version: 0.31.0(patch_hash=e8b472d71289ac8de9813c57d79abac524889ca96f279f6f3ad08043434f6615) '@openclaw/plugin-sdk': specifier: workspace:* version: link:../../packages/plugin-sdk