ACPX plugin: allow configurable command and expected version

This commit is contained in:
Onur
2026-02-28 10:37:02 +01:00
committed by Onur Solmaz
parent 134296276a
commit 921ebfb25e
10 changed files with 299 additions and 75 deletions

View File

@@ -1,12 +1,18 @@
{
"id": "acpx",
"name": "ACPX Runtime",
"description": "ACP runtime backend powered by a pinned plugin-local acpx CLI.",
"description": "ACP runtime backend powered by acpx with configurable command path and version policy.",
"skills": ["./skills"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"command": {
"type": "string"
},
"expectedVersion": {
"type": "string"
},
"cwd": {
"type": "string"
},
@@ -29,6 +35,14 @@
}
},
"uiHints": {
"command": {
"label": "acpx Command",
"help": "Optional path/command override for acpx (for example /home/user/repos/acpx/dist/cli.js). Leave unset to use plugin-local bundled acpx."
},
"expectedVersion": {
"label": "Expected acpx Version",
"help": "Exact version to enforce (for example 0.1.13) or \"any\" to skip strict version matching."
},
"cwd": {
"label": "Default Working Directory",
"help": "Default cwd for ACP session operations when not set per session."

View File

@@ -2,12 +2,13 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import {
ACPX_BUNDLED_BIN,
ACPX_PINNED_VERSION,
createAcpxPluginConfigSchema,
resolveAcpxPluginConfig,
} from "./config.js";
describe("acpx plugin config parsing", () => {
it("resolves a strict plugin-local acpx command", () => {
it("resolves bundled acpx with pinned version by default", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
cwd: "/tmp/workspace",
@@ -16,18 +17,74 @@ 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.cwd).toBe(path.resolve("/tmp/workspace"));
});
it("rejects command overrides", () => {
expect(() =>
resolveAcpxPluginConfig({
rawConfig: {
command: "acpx-custom",
},
workspaceDir: "/tmp/workspace",
}),
).toThrow("unknown config key: command");
it("accepts command override and disables plugin-local auto-install", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
command: "/home/user/repos/acpx/dist/cli.js",
},
workspaceDir: "/tmp/workspace",
});
expect(resolved.command).toBe("/home/user/repos/acpx/dist/cli.js");
expect(resolved.expectedVersion).toBeUndefined();
expect(resolved.allowPluginLocalInstall).toBe(false);
});
it("resolves relative command paths against workspace directory", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
command: "../acpx/dist/cli.js",
},
workspaceDir: "/home/user/repos/openclaw",
});
expect(resolved.command).toBe(path.resolve("/home/user/repos/openclaw", "../acpx/dist/cli.js"));
expect(resolved.expectedVersion).toBeUndefined();
expect(resolved.allowPluginLocalInstall).toBe(false);
});
it("keeps bare command names as-is", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
command: "acpx",
},
workspaceDir: "/tmp/workspace",
});
expect(resolved.command).toBe("acpx");
expect(resolved.expectedVersion).toBeUndefined();
expect(resolved.allowPluginLocalInstall).toBe(false);
});
it("accepts exact expectedVersion override", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
command: "/home/user/repos/acpx/dist/cli.js",
expectedVersion: "0.1.99",
},
workspaceDir: "/tmp/workspace",
});
expect(resolved.command).toBe("/home/user/repos/acpx/dist/cli.js");
expect(resolved.expectedVersion).toBe("0.1.99");
expect(resolved.allowPluginLocalInstall).toBe(false);
});
it("treats expectedVersion=any as no version constraint", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
command: "/home/user/repos/acpx/dist/cli.js",
expectedVersion: "any",
},
workspaceDir: "/tmp/workspace",
});
expect(resolved.expectedVersion).toBeUndefined();
});
it("rejects commandArgs overrides", () => {

View File

@@ -9,12 +9,18 @@ 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.13";
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)), "..");
export const ACPX_BUNDLED_BIN = path.join(ACPX_PLUGIN_ROOT, "node_modules", ".bin", ACPX_BIN_NAME);
export const ACPX_LOCAL_INSTALL_COMMAND = `npm install --omit=dev --no-save acpx@${ACPX_PINNED_VERSION}`;
export function buildAcpxLocalInstallCommand(version: string = ACPX_PINNED_VERSION): string {
return `npm install --omit=dev --no-save acpx@${version}`;
}
export const ACPX_LOCAL_INSTALL_COMMAND = buildAcpxLocalInstallCommand();
export type AcpxPluginConfig = {
command?: string;
expectedVersion?: string;
cwd?: string;
permissionMode?: AcpxPermissionMode;
nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy;
@@ -24,6 +30,9 @@ export type AcpxPluginConfig = {
export type ResolvedAcpxPluginConfig = {
command: string;
expectedVersion?: string;
allowPluginLocalInstall: boolean;
installCommand: string;
cwd: string;
permissionMode: AcpxPermissionMode;
nonInteractivePermissions: AcpxNonInteractivePermissionPolicy;
@@ -61,6 +70,8 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
return { ok: false, message: "expected config object" };
}
const allowedKeys = new Set([
"command",
"expectedVersion",
"cwd",
"permissionMode",
"nonInteractivePermissions",
@@ -73,6 +84,19 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
}
}
const command = value.command;
if (command !== undefined && (typeof command !== "string" || command.trim() === "")) {
return { ok: false, message: "command must be a non-empty string" };
}
const expectedVersion = value.expectedVersion;
if (
expectedVersion !== undefined &&
(typeof expectedVersion !== "string" || expectedVersion.trim() === "")
) {
return { ok: false, message: "expectedVersion must be a non-empty string" };
}
const cwd = value.cwd;
if (cwd !== undefined && (typeof cwd !== "string" || cwd.trim() === "")) {
return { ok: false, message: "cwd must be a non-empty string" };
@@ -122,6 +146,8 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
return {
ok: true,
value: {
command: typeof command === "string" ? command.trim() : undefined,
expectedVersion: typeof expectedVersion === "string" ? expectedVersion.trim() : undefined,
cwd: typeof cwd === "string" ? cwd.trim() : undefined,
permissionMode: typeof permissionMode === "string" ? permissionMode : undefined,
nonInteractivePermissions:
@@ -133,6 +159,18 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
};
}
function resolveConfiguredCommand(params: { configured?: string; workspaceDir?: string }): string {
const configured = params.configured?.trim();
if (!configured) {
return ACPX_BUNDLED_BIN;
}
if (path.isAbsolute(configured) || configured.includes(path.sep) || configured.includes("/")) {
const baseDir = params.workspaceDir?.trim() || process.cwd();
return path.resolve(baseDir, configured);
}
return configured;
}
export function createAcpxPluginConfigSchema(): OpenClawPluginConfigSchema {
return {
safeParse(value: unknown):
@@ -156,6 +194,8 @@ export function createAcpxPluginConfigSchema(): OpenClawPluginConfigSchema {
type: "object",
additionalProperties: false,
properties: {
command: { type: "string" },
expectedVersion: { type: "string" },
cwd: { type: "string" },
permissionMode: {
type: "string",
@@ -183,9 +223,23 @@ export function resolveAcpxPluginConfig(params: {
const normalized = parsed.value ?? {};
const fallbackCwd = params.workspaceDir?.trim() || process.cwd();
const cwd = path.resolve(normalized.cwd?.trim() || fallbackCwd);
const command = resolveConfiguredCommand({
configured: normalized.command,
workspaceDir: params.workspaceDir,
});
const allowPluginLocalInstall = command === ACPX_BUNDLED_BIN;
const configuredExpectedVersion = normalized.expectedVersion;
const expectedVersion =
configuredExpectedVersion === ACPX_VERSION_ANY
? undefined
: (configuredExpectedVersion ?? (allowPluginLocalInstall ? ACPX_PINNED_VERSION : undefined));
const installCommand = buildAcpxLocalInstallCommand(expectedVersion ?? ACPX_PINNED_VERSION);
return {
command: ACPX_BUNDLED_BIN,
command,
expectedVersion,
allowPluginLocalInstall,
installCommand,
cwd,
permissionMode: normalized.permissionMode ?? DEFAULT_PERMISSION_MODE,
nonInteractivePermissions:

View File

@@ -1,8 +1,14 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ACPX_LOCAL_INSTALL_COMMAND, ACPX_PINNED_VERSION } from "./config.js";
import {
ACPX_LOCAL_INSTALL_COMMAND,
ACPX_PINNED_VERSION,
buildAcpxLocalInstallCommand,
} from "./config.js";
const { resolveSpawnFailureMock, spawnAndCollectMock } = vi.hoisted(() => ({
resolveSpawnFailureMock: vi.fn(() => null),
resolveSpawnFailureMock: vi.fn<
(error: unknown, cwd: string) => "missing-command" | "missing-cwd" | null
>(() => null),
spawnAndCollectMock: vi.fn(),
}));
@@ -11,7 +17,7 @@ vi.mock("./runtime-internals/process.js", () => ({
spawnAndCollect: spawnAndCollectMock,
}));
import { checkPinnedAcpxVersion, ensurePinnedAcpx } from "./ensure.js";
import { checkAcpxVersion, ensureAcpx } from "./ensure.js";
describe("acpx ensure", () => {
beforeEach(() => {
@@ -28,7 +34,7 @@ describe("acpx ensure", () => {
error: null,
});
const result = await checkPinnedAcpxVersion({
const result = await checkAcpxVersion({
command: "/plugin/node_modules/.bin/acpx",
cwd: "/plugin",
expectedVersion: ACPX_PINNED_VERSION,
@@ -49,7 +55,7 @@ describe("acpx ensure", () => {
error: null,
});
const result = await checkPinnedAcpxVersion({
const result = await checkAcpxVersion({
command: "/plugin/node_modules/.bin/acpx",
cwd: "/plugin",
expectedVersion: ACPX_PINNED_VERSION,
@@ -64,6 +70,27 @@ describe("acpx ensure", () => {
});
});
it("accepts any installed version when expectedVersion is unset", async () => {
spawnAndCollectMock.mockResolvedValueOnce({
stdout: "acpx 9.9.9\n",
stderr: "",
code: 0,
error: null,
});
const result = await checkAcpxVersion({
command: "/custom/acpx",
cwd: "/custom",
expectedVersion: undefined,
});
expect(result).toEqual({
ok: true,
version: "9.9.9",
expectedVersion: undefined,
});
});
it("installs and verifies pinned acpx when precheck fails", async () => {
spawnAndCollectMock
.mockResolvedValueOnce({
@@ -85,7 +112,7 @@ describe("acpx ensure", () => {
error: null,
});
await ensurePinnedAcpx({
await ensureAcpx({
command: "/plugin/node_modules/.bin/acpx",
pluginRoot: "/plugin",
expectedVersion: ACPX_PINNED_VERSION,
@@ -115,11 +142,52 @@ describe("acpx ensure", () => {
});
await expect(
ensurePinnedAcpx({
ensureAcpx({
command: "/plugin/node_modules/.bin/acpx",
pluginRoot: "/plugin",
expectedVersion: ACPX_PINNED_VERSION,
}),
).rejects.toThrow("failed to install plugin-local acpx");
});
it("skips install path when allowInstall=false", async () => {
spawnAndCollectMock.mockResolvedValueOnce({
stdout: "",
stderr: "",
code: 0,
error: new Error("not found"),
});
resolveSpawnFailureMock.mockReturnValue("missing-command");
await expect(
ensureAcpx({
command: "/custom/acpx",
pluginRoot: "/plugin",
expectedVersion: undefined,
allowInstall: false,
}),
).rejects.toThrow("acpx command not found at /custom/acpx");
expect(spawnAndCollectMock).toHaveBeenCalledTimes(1);
});
it("uses expectedVersion for install command metadata", async () => {
spawnAndCollectMock.mockResolvedValueOnce({
stdout: "acpx 0.0.9\n",
stderr: "",
code: 0,
error: null,
});
const result = await checkAcpxVersion({
command: "/plugin/node_modules/.bin/acpx",
cwd: "/plugin",
expectedVersion: "0.2.0",
});
expect(result).toMatchObject({
ok: false,
installCommand: buildAcpxLocalInstallCommand("0.2.0"),
});
});
});

View File

@@ -1,5 +1,5 @@
import type { PluginLogger } from "openclaw/plugin-sdk";
import { ACPX_LOCAL_INSTALL_COMMAND, ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT } from "./config.js";
import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js";
import { resolveSpawnFailure, spawnAndCollect } from "./runtime-internals/process.js";
const SEMVER_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/;
@@ -8,13 +8,13 @@ export type AcpxVersionCheckResult =
| {
ok: true;
version: string;
expectedVersion: string;
expectedVersion?: string;
}
| {
ok: false;
reason: "missing-command" | "missing-version" | "version-mismatch" | "execution-failed";
message: string;
expectedVersion: string;
expectedVersion?: string;
installCommand: string;
installedVersion?: string;
};
@@ -25,12 +25,13 @@ function extractVersion(stdout: string, stderr: string): string | null {
return match?.[0] ?? null;
}
export async function checkPinnedAcpxVersion(params: {
export async function checkAcpxVersion(params: {
command: string;
cwd?: string;
expectedVersion?: string;
}): Promise<AcpxVersionCheckResult> {
const expectedVersion = params.expectedVersion ?? ACPX_PINNED_VERSION;
const expectedVersion = params.expectedVersion?.trim() || undefined;
const installCommand = buildAcpxLocalInstallCommand(expectedVersion ?? ACPX_PINNED_VERSION);
const cwd = params.cwd ?? ACPX_PLUGIN_ROOT;
const result = await spawnAndCollect({
command: params.command,
@@ -46,7 +47,7 @@ export async function checkPinnedAcpxVersion(params: {
reason: "missing-command",
message: `acpx command not found at ${params.command}`,
expectedVersion,
installCommand: ACPX_LOCAL_INSTALL_COMMAND,
installCommand,
};
}
return {
@@ -54,7 +55,7 @@ export async function checkPinnedAcpxVersion(params: {
reason: "execution-failed",
message: result.error.message,
expectedVersion,
installCommand: ACPX_LOCAL_INSTALL_COMMAND,
installCommand,
};
}
@@ -65,7 +66,7 @@ export async function checkPinnedAcpxVersion(params: {
reason: "execution-failed",
message: stderr || `acpx --version failed with code ${result.code ?? "unknown"}`,
expectedVersion,
installCommand: ACPX_LOCAL_INSTALL_COMMAND,
installCommand,
};
}
@@ -76,17 +77,17 @@ export async function checkPinnedAcpxVersion(params: {
reason: "missing-version",
message: "acpx --version output did not include a parseable version",
expectedVersion,
installCommand: ACPX_LOCAL_INSTALL_COMMAND,
installCommand,
};
}
if (installedVersion !== expectedVersion) {
if (expectedVersion && installedVersion !== expectedVersion) {
return {
ok: false,
reason: "version-mismatch",
message: `acpx version mismatch: found ${installedVersion}, expected ${expectedVersion}`,
expectedVersion,
installCommand: ACPX_LOCAL_INSTALL_COMMAND,
installCommand,
installedVersion,
};
}
@@ -100,11 +101,12 @@ export async function checkPinnedAcpxVersion(params: {
let pendingEnsure: Promise<void> | null = null;
export async function ensurePinnedAcpx(params: {
export async function ensureAcpx(params: {
command: string;
logger?: PluginLogger;
pluginRoot?: string;
expectedVersion?: string;
allowInstall?: boolean;
}): Promise<void> {
if (pendingEnsure) {
return await pendingEnsure;
@@ -112,9 +114,11 @@ export async function ensurePinnedAcpx(params: {
pendingEnsure = (async () => {
const pluginRoot = params.pluginRoot ?? ACPX_PLUGIN_ROOT;
const expectedVersion = params.expectedVersion ?? ACPX_PINNED_VERSION;
const expectedVersion = params.expectedVersion?.trim() || undefined;
const installVersion = expectedVersion ?? ACPX_PINNED_VERSION;
const allowInstall = params.allowInstall ?? true;
const precheck = await checkPinnedAcpxVersion({
const precheck = await checkAcpxVersion({
command: params.command,
cwd: pluginRoot,
expectedVersion,
@@ -122,6 +126,9 @@ export async function ensurePinnedAcpx(params: {
if (precheck.ok) {
return;
}
if (!allowInstall) {
throw new Error(precheck.message);
}
params.logger?.warn(
`acpx local binary unavailable or mismatched (${precheck.message}); running plugin-local install`,
@@ -129,7 +136,7 @@ export async function ensurePinnedAcpx(params: {
const install = await spawnAndCollect({
command: "npm",
args: ["install", "--omit=dev", "--no-save", `acpx@${expectedVersion}`],
args: ["install", "--omit=dev", "--no-save", `acpx@${installVersion}`],
cwd: pluginRoot,
});
@@ -148,7 +155,7 @@ export async function ensurePinnedAcpx(params: {
throw new Error(`failed to install plugin-local acpx: ${detail}`);
}
const postcheck = await checkPinnedAcpxVersion({
const postcheck = await checkAcpxVersion({
command: params.command,
cwd: pluginRoot,
expectedVersion,

View File

@@ -265,6 +265,8 @@ async function createMockRuntime(params?: {
const config: ResolvedAcpxPluginConfig = {
command: scriptPath,
allowPluginLocalInstall: false,
installCommand: "n/a",
cwd: dir,
permissionMode: params?.permissionMode ?? "approve-all",
nonInteractivePermissions: "fail",
@@ -581,6 +583,8 @@ describe("AcpxRuntime", () => {
const runtime = new AcpxRuntime(
{
command: "/definitely/missing/acpx",
allowPluginLocalInstall: false,
installCommand: "n/a",
cwd: process.cwd(),
permissionMode: "approve-reads",
nonInteractivePermissions: "fail",
@@ -603,6 +607,8 @@ describe("AcpxRuntime", () => {
const runtime = new AcpxRuntime(
{
command: "/definitely/missing/acpx",
allowPluginLocalInstall: false,
installCommand: "n/a",
cwd: process.cwd(),
permissionMode: "approve-reads",
nonInteractivePermissions: "fail",

View File

@@ -12,12 +12,8 @@ import type {
PluginLogger,
} from "openclaw/plugin-sdk";
import { AcpRuntimeError } from "openclaw/plugin-sdk";
import {
ACPX_LOCAL_INSTALL_COMMAND,
ACPX_PINNED_VERSION,
type ResolvedAcpxPluginConfig,
} from "./config.js";
import { checkPinnedAcpxVersion } from "./ensure.js";
import { type ResolvedAcpxPluginConfig } from "./config.js";
import { checkAcpxVersion } from "./ensure.js";
import {
parseJsonLines,
parsePromptEventLine,
@@ -121,10 +117,10 @@ export class AcpxRuntime implements AcpRuntime {
}
async probeAvailability(): Promise<void> {
const versionCheck = await checkPinnedAcpxVersion({
const versionCheck = await checkAcpxVersion({
command: this.config.command,
cwd: this.config.cwd,
expectedVersion: ACPX_PINNED_VERSION,
expectedVersion: this.config.expectedVersion,
});
if (!versionCheck.ok) {
this.healthy = false;
@@ -376,15 +372,15 @@ export class AcpxRuntime implements AcpRuntime {
}
async doctor(): Promise<AcpRuntimeDoctorReport> {
const versionCheck = await checkPinnedAcpxVersion({
const versionCheck = await checkAcpxVersion({
command: this.config.command,
cwd: this.config.cwd,
expectedVersion: ACPX_PINNED_VERSION,
expectedVersion: this.config.expectedVersion,
});
if (!versionCheck.ok) {
this.healthy = false;
const details = [
`expected=${versionCheck.expectedVersion}`,
versionCheck.expectedVersion ? `expected=${versionCheck.expectedVersion}` : null,
versionCheck.installedVersion ? `installed=${versionCheck.installedVersion}` : null,
].filter((detail): detail is string => Boolean(detail));
return {
@@ -410,7 +406,7 @@ export class AcpxRuntime implements AcpRuntime {
ok: false,
code: "ACP_BACKEND_UNAVAILABLE",
message: `acpx command not found: ${this.config.command}`,
installCommand: ACPX_LOCAL_INSTALL_COMMAND,
installCommand: this.config.installCommand,
};
}
if (spawnFailure === "missing-cwd") {
@@ -440,7 +436,7 @@ export class AcpxRuntime implements AcpRuntime {
this.healthy = true;
return {
ok: true,
message: `acpx command available (${this.config.command}, version ${versionCheck.version})`,
message: `acpx command available (${this.config.command}, version ${versionCheck.version}${this.config.expectedVersion ? `, expected ${this.config.expectedVersion}` : ""})`,
};
} catch (error) {
this.healthy = false;

View File

@@ -9,12 +9,12 @@ import {
import { ACPX_BUNDLED_BIN } from "./config.js";
import { createAcpxRuntimeService } from "./service.js";
const { ensurePinnedAcpxSpy } = vi.hoisted(() => ({
ensurePinnedAcpxSpy: vi.fn(async () => {}),
const { ensureAcpxSpy } = vi.hoisted(() => ({
ensureAcpxSpy: vi.fn(async () => {}),
}));
vi.mock("./ensure.js", () => ({
ensurePinnedAcpx: ensurePinnedAcpxSpy,
ensureAcpx: ensureAcpxSpy,
}));
type RuntimeStub = AcpRuntime & {
@@ -73,8 +73,8 @@ function createServiceContext(
describe("createAcpxRuntimeService", () => {
beforeEach(() => {
__testing.resetAcpRuntimeBackendsForTests();
ensurePinnedAcpxSpy.mockReset();
ensurePinnedAcpxSpy.mockImplementation(async () => {});
ensureAcpxSpy.mockReset();
ensureAcpxSpy.mockImplementation(async () => {});
});
it("registers and unregisters the acpx backend", async () => {
@@ -88,7 +88,7 @@ describe("createAcpxRuntimeService", () => {
expect(getAcpRuntimeBackend("acpx")?.runtime).toBe(runtime);
await vi.waitFor(() => {
expect(ensurePinnedAcpxSpy).toHaveBeenCalledOnce();
expect(ensureAcpxSpy).toHaveBeenCalledOnce();
expect(probeAvailabilitySpy).toHaveBeenCalledOnce();
});
@@ -132,6 +132,8 @@ describe("createAcpxRuntimeService", () => {
queueOwnerTtlSeconds: 0.25,
pluginConfig: expect.objectContaining({
command: ACPX_BUNDLED_BIN,
expectedVersion: "0.1.13",
allowPluginLocalInstall: true,
}),
}),
);
@@ -156,7 +158,7 @@ describe("createAcpxRuntimeService", () => {
it("does not block startup while acpx ensure runs", async () => {
const { runtime } = createRuntimeStub(true);
ensurePinnedAcpxSpy.mockImplementation(() => new Promise<void>(() => {}));
ensureAcpxSpy.mockImplementation(() => new Promise<void>(() => {}));
const service = createAcpxRuntimeService({
runtimeFactory: () => runtime,
});

View File

@@ -5,12 +5,8 @@ import type {
PluginLogger,
} from "openclaw/plugin-sdk";
import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk";
import {
ACPX_PINNED_VERSION,
resolveAcpxPluginConfig,
type ResolvedAcpxPluginConfig,
} from "./config.js";
import { ensurePinnedAcpx } from "./ensure.js";
import { resolveAcpxPluginConfig, type ResolvedAcpxPluginConfig } from "./config.js";
import { ensureAcpx } from "./ensure.js";
import { ACPX_BACKEND_ID, AcpxRuntime } from "./runtime.js";
type AcpxRuntimeLike = AcpRuntime & {
@@ -61,18 +57,21 @@ export function createAcpxRuntimeService(
runtime,
healthy: () => runtime?.isHealthy() ?? false,
});
const expectedVersionLabel = pluginConfig.expectedVersion ?? "any";
const installLabel = pluginConfig.allowPluginLocalInstall ? "enabled" : "disabled";
ctx.logger.info(
`acpx runtime backend registered (command: ${pluginConfig.command}, pinned: ${ACPX_PINNED_VERSION})`,
`acpx runtime backend registered (command: ${pluginConfig.command}, expectedVersion: ${expectedVersionLabel}, pluginLocalInstall: ${installLabel})`,
);
lifecycleRevision += 1;
const currentRevision = lifecycleRevision;
void (async () => {
try {
await ensurePinnedAcpx({
await ensureAcpx({
command: pluginConfig.command,
logger: ctx.logger,
expectedVersion: ACPX_PINNED_VERSION,
expectedVersion: pluginConfig.expectedVersion,
allowInstall: pluginConfig.allowPluginLocalInstall,
});
if (currentRevision !== lifecycleRevision) {
return;