mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
ACPX plugin: allow configurable command and expected version
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user