fix(acpx): stage Claude ACP adapter runtime dependency

This commit is contained in:
Peter Steinberger
2026-04-28 05:37:44 +01:00
parent 59bd7e47e8
commit 52daf5fbd3
6 changed files with 408 additions and 32 deletions

View File

@@ -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.

View File

@@ -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": {

View File

@@ -21,6 +21,10 @@ async function makeTempDir(): Promise<string> {
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);
});
});

View File

@@ -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<string, unknown>)[CODEX_ACP_BIN]
? (bin as Record<string, unknown>)[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<string | undefined> {
async function resolveInstalledAcpPackageBinPath(
packageName: string,
binName: string,
): Promise<string | undefined> {
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<string | undefined> {
}
}
function buildCodexAcpWrapperScript(installedBinPath?: string): string {
async function resolveInstalledCodexAcpBinPath(): Promise<string | undefined> {
// 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<string | undefined> {
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<string> {
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<string> {
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<string | undefined>;
resolveInstalledClaudeAcpBinPath?: () => Promise<string | undefined>;
}): Promise<ResolvedAcpxPluginConfig> {
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),
},
};
}

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
type AcpxPackageManifest = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
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);
});
});

6
pnpm-lock.yaml generated
View File

@@ -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