ACPX: pin 0.1.15 and tolerate missing --version in health check

This commit is contained in:
Onur
2026-03-01 20:01:03 +01:00
committed by Onur Solmaz
parent f81c2e75d2
commit b12c909ea2
6 changed files with 126 additions and 13 deletions

View File

@@ -41,7 +41,7 @@
},
"expectedVersion": {
"label": "Expected acpx Version",
"help": "Exact version to enforce (for example 0.1.14) or \"any\" to skip strict version matching."
"help": "Exact version to enforce (for example 0.1.15) or \"any\" to skip strict version matching."
},
"cwd": {
"label": "Default Working Directory",

View File

@@ -4,7 +4,7 @@
"description": "OpenClaw ACP runtime backend via acpx",
"type": "module",
"dependencies": {
"acpx": "0.1.14"
"acpx": "0.1.15"
},
"openclaw": {
"extensions": [

View File

@@ -8,7 +8,7 @@ export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
export const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
export const ACPX_PINNED_VERSION = "0.1.14";
export const ACPX_PINNED_VERSION = "0.1.15";
export const ACPX_VERSION_ANY = "any";
const ACPX_BIN_NAME = process.platform === "win32" ? "acpx.cmd" : "acpx";
export const ACPX_PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

View File

@@ -1,4 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
ACPX_LOCAL_INSTALL_COMMAND,
ACPX_PINNED_VERSION,
@@ -20,12 +23,37 @@ vi.mock("./runtime-internals/process.js", () => ({
import { checkAcpxVersion, ensureAcpx } from "./ensure.js";
describe("acpx ensure", () => {
const tempDirs: string[] = [];
beforeEach(() => {
resolveSpawnFailureMock.mockReset();
resolveSpawnFailureMock.mockReturnValue(null);
spawnAndCollectMock.mockReset();
});
function makeTempAcpxInstall(version: string): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-ensure-test-"));
tempDirs.push(root);
const packageRoot = path.join(root, "node_modules", "acpx");
fs.mkdirSync(path.join(packageRoot, "dist"), { recursive: true });
fs.mkdirSync(path.join(root, "node_modules", ".bin"), { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "acpx", version }, null, 2),
"utf8",
);
fs.writeFileSync(path.join(packageRoot, "dist", "cli.js"), "#!/usr/bin/env node\n", "utf8");
const binPath = path.join(root, "node_modules", ".bin", "acpx");
fs.symlinkSync(path.join(packageRoot, "dist", "cli.js"), binPath);
return binPath;
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it("accepts the pinned acpx version", async () => {
spawnAndCollectMock.mockResolvedValueOnce({
stdout: `acpx ${ACPX_PINNED_VERSION}\n`,
@@ -75,6 +103,28 @@ describe("acpx ensure", () => {
});
});
it("falls back to package.json version when --version is unsupported", async () => {
const command = makeTempAcpxInstall(ACPX_PINNED_VERSION);
spawnAndCollectMock.mockResolvedValueOnce({
stdout: "",
stderr: "error: unknown option '--version'",
code: 2,
error: null,
});
const result = await checkAcpxVersion({
command,
cwd: path.dirname(path.dirname(command)),
expectedVersion: ACPX_PINNED_VERSION,
});
expect(result).toEqual({
ok: true,
version: ACPX_PINNED_VERSION,
expectedVersion: ACPX_PINNED_VERSION,
});
});
it("accepts command availability when expectedVersion is unset", async () => {
spawnAndCollectMock.mockResolvedValueOnce({
stdout: "Usage: acpx [options]\n",

View File

@@ -1,3 +1,5 @@
import fs from "node:fs";
import path from "node:path";
import type { PluginLogger } from "openclaw/plugin-sdk";
import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js";
import { resolveSpawnFailure, spawnAndCollect } from "./runtime-internals/process.js";
@@ -29,6 +31,47 @@ function isExpectedVersionConfigured(value: string | undefined): value is string
return typeof value === "string" && value.trim().length > 0;
}
function supportsPathResolution(command: string): boolean {
return path.isAbsolute(command) || command.includes("/") || command.includes("\\");
}
function isUnsupportedVersionProbe(stdout: string, stderr: string): boolean {
const combined = `${stdout}\n${stderr}`.toLowerCase();
return combined.includes("unknown option") && combined.includes("--version");
}
function resolveVersionFromPackage(command: string, cwd: string): string | null {
if (!supportsPathResolution(command)) {
return null;
}
const commandPath = path.isAbsolute(command) ? command : path.resolve(cwd, command);
let current: string;
try {
current = path.dirname(fs.realpathSync(commandPath));
} catch {
return null;
}
while (true) {
const packageJsonPath = path.join(current, "package.json");
try {
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
name?: unknown;
version?: unknown;
};
if (parsed.name === "acpx" && typeof parsed.version === "string" && parsed.version.trim()) {
return parsed.version.trim();
}
} catch {
// no-op; continue walking up
}
const parent = path.dirname(current);
if (parent === current) {
return null;
}
current = parent;
}
}
export async function checkAcpxVersion(params: {
command: string;
cwd?: string;
@@ -66,6 +109,26 @@ export async function checkAcpxVersion(params: {
}
if ((result.code ?? 0) !== 0) {
if (hasExpectedVersion && isUnsupportedVersionProbe(result.stdout, result.stderr)) {
const installedVersion = resolveVersionFromPackage(params.command, cwd);
if (installedVersion) {
if (expectedVersion && installedVersion !== expectedVersion) {
return {
ok: false,
reason: "version-mismatch",
message: `acpx version mismatch: found ${installedVersion}, expected ${expectedVersion}`,
expectedVersion,
installCommand,
installedVersion,
};
}
return {
ok: true,
version: installedVersion,
expectedVersion,
};
}
}
const stderr = result.stderr.trim();
return {
ok: false,

18
pnpm-lock.yaml generated
View File

@@ -264,8 +264,8 @@ importers:
extensions/acpx:
dependencies:
acpx:
specifier: 0.1.14
version: 0.1.14(zod@4.3.6)
specifier: 0.1.15
version: 0.1.15(zod@4.3.6)
extensions/bluebubbles: {}
@@ -3131,8 +3131,8 @@ packages:
link-preview-js:
optional: true
'@whiskeysockets/libsignal-node@git+https://github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
resolution: {commit: 1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67, repo: https://github.com/whiskeysockets/libsignal-node.git, type: git}
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
resolution: {tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
version: 2.0.1
abbrev@1.1.1:
@@ -3160,8 +3160,8 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acpx@0.1.14:
resolution: {integrity: sha512-kq1tU7VCOLW3dIK77PpGoJPMsIqmnOSiQJGsWfWiOYgTXYIsbNtP04ilsaobgDd/MUgjo9ttXD1abziQ3OH5Pg==}
acpx@0.1.15:
resolution: {integrity: sha512-1r+tmPT9Oe2Ulv5b4r7O2hCCq5CHVru/H2tcPeTpZek9jR1zBQoBfZ/RcK+9sC9/mnDvWYO5R7Iae64v2LMO+A==}
engines: {node: '>=18'}
hasBin: true
@@ -9007,7 +9007,7 @@ snapshots:
'@cacheable/node-cache': 1.7.6
'@hapi/boom': 9.1.4
async-mutex: 0.5.0
libsignal: '@whiskeysockets/libsignal-node@git+https://github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
lru-cache: 11.2.6
music-metadata: 11.12.1
p-queue: 9.1.0
@@ -9022,7 +9022,7 @@ snapshots:
- supports-color
- utf-8-validate
'@whiskeysockets/libsignal-node@git+https://github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
dependencies:
curve25519-js: 0.0.4
protobufjs: 6.8.8
@@ -9050,7 +9050,7 @@ snapshots:
acorn@8.16.0: {}
acpx@0.1.14(zod@4.3.6):
acpx@0.1.15(zod@4.3.6):
dependencies:
'@agentclientprotocol/sdk': 0.14.1(zod@4.3.6)
commander: 13.1.0