From c2435306a78bc219db75bfbfc4bc9a02191f36b6 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 4 Apr 2026 15:14:43 +0900 Subject: [PATCH] refactor(acpx): lazy-load runtime service entry --- extensions/acpx/index.ts | 8 +- extensions/acpx/register.runtime.ts | 1 + extensions/acpx/src/config-schema.ts | 105 ++++++++++++++++++++++ extensions/acpx/src/config.ts | 126 +++++---------------------- 4 files changed, 131 insertions(+), 109 deletions(-) create mode 100644 extensions/acpx/register.runtime.ts create mode 100644 extensions/acpx/src/config-schema.ts diff --git a/extensions/acpx/index.ts b/extensions/acpx/index.ts index 2ae578b9c3f..d991ff0eec6 100644 --- a/extensions/acpx/index.ts +++ b/extensions/acpx/index.ts @@ -1,13 +1,13 @@ import type { OpenClawPluginApi } from "./runtime-api.js"; -import { createAcpxPluginConfigSchema } from "./src/config.js"; -import { createAcpxRuntimeService } from "./src/service.js"; +import { createAcpxPluginConfigSchema } from "./src/config-schema.js"; const plugin = { id: "acpx", name: "ACPX Runtime", description: "ACP runtime backend powered by the acpx CLI.", - configSchema: createAcpxPluginConfigSchema(), - register(api: OpenClawPluginApi) { + configSchema: () => createAcpxPluginConfigSchema(), + async register(api: OpenClawPluginApi) { + const { createAcpxRuntimeService } = await import("./register.runtime.js"); api.registerService( createAcpxRuntimeService({ pluginConfig: api.pluginConfig, diff --git a/extensions/acpx/register.runtime.ts b/extensions/acpx/register.runtime.ts new file mode 100644 index 00000000000..419c0d06632 --- /dev/null +++ b/extensions/acpx/register.runtime.ts @@ -0,0 +1 @@ +export { createAcpxRuntimeService } from "./src/service.js"; diff --git a/extensions/acpx/src/config-schema.ts b/extensions/acpx/src/config-schema.ts new file mode 100644 index 00000000000..846e970309a --- /dev/null +++ b/extensions/acpx/src/config-schema.ts @@ -0,0 +1,105 @@ +import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginConfigSchema } from "../runtime-api.js"; +import { z } from "openclaw/plugin-sdk/zod"; + +export const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const; +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 type McpServerConfig = { + command: string; + args?: string[]; + env?: Record; +}; + +export type AcpxMcpServer = { + name: string; + command: string; + args: string[]; + env: Array<{ name: string; value: string }>; +}; + +export type AcpxPluginConfig = { + command?: string; + expectedVersion?: string; + cwd?: string; + permissionMode?: AcpxPermissionMode; + nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy; + pluginToolsMcpBridge?: boolean; + strictWindowsCmdWrapper?: boolean; + timeoutSeconds?: number; + queueOwnerTtlSeconds?: number; + mcpServers?: Record; +}; + +export type ResolvedAcpxPluginConfig = { + command: string; + expectedVersion?: string; + allowPluginLocalInstall: boolean; + stripProviderAuthEnvVars: boolean; + installCommand: string; + cwd: string; + permissionMode: AcpxPermissionMode; + nonInteractivePermissions: AcpxNonInteractivePermissionPolicy; + pluginToolsMcpBridge: boolean; + strictWindowsCmdWrapper: boolean; + timeoutSeconds?: number; + queueOwnerTtlSeconds: number; + mcpServers: Record; +}; + +const nonEmptyTrimmedString = (message: string) => + z.string({ error: message }).trim().min(1, { error: message }); + +const McpServerConfigSchema = z.object({ + command: nonEmptyTrimmedString("command must be a non-empty string").describe( + "Command to run the MCP server", + ), + args: z + .array(z.string({ error: "args must be an array of strings" }), { + error: "args must be an array of strings", + }) + .optional() + .describe("Arguments to pass to the command"), + env: z + .record(z.string(), z.string({ error: "env values must be strings" }), { + error: "env must be an object of strings", + }) + .optional() + .describe("Environment variables for the MCP server"), +}); + +export const AcpxPluginConfigSchema = z.strictObject({ + command: nonEmptyTrimmedString("command must be a non-empty string").optional(), + expectedVersion: nonEmptyTrimmedString("expectedVersion must be a non-empty string").optional(), + cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(), + permissionMode: z + .enum(ACPX_PERMISSION_MODES, { + error: `permissionMode must be one of: ${ACPX_PERMISSION_MODES.join(", ")}`, + }) + .optional(), + nonInteractivePermissions: z + .enum(ACPX_NON_INTERACTIVE_POLICIES, { + error: `nonInteractivePermissions must be one of: ${ACPX_NON_INTERACTIVE_POLICIES.join(", ")}`, + }) + .optional(), + pluginToolsMcpBridge: z.boolean({ error: "pluginToolsMcpBridge must be a boolean" }).optional(), + strictWindowsCmdWrapper: z + .boolean({ error: "strictWindowsCmdWrapper must be a boolean" }) + .optional(), + timeoutSeconds: z + .number({ error: "timeoutSeconds must be a number >= 0.001" }) + .min(0.001, { error: "timeoutSeconds must be a number >= 0.001" }) + .optional(), + queueOwnerTtlSeconds: z + .number({ error: "queueOwnerTtlSeconds must be a number >= 0" }) + .min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" }) + .optional(), + mcpServers: z.record(z.string(), McpServerConfigSchema).optional(), +}); + +export function createAcpxPluginConfigSchema(): OpenClawPluginConfigSchema { + return buildPluginConfigSchema(AcpxPluginConfigSchema); +} diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index 37626d9f070..19deffe7c56 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -1,15 +1,27 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core"; -import { z } from "openclaw/plugin-sdk/zod"; -import type { OpenClawPluginConfigSchema } from "../runtime-api.js"; - -export const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const; -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]; +import { AcpxPluginConfigSchema } from "./config-schema.js"; +import type { + AcpxPluginConfig, + AcpxPermissionMode, + AcpxNonInteractivePermissionPolicy, + McpServerConfig, + AcpxMcpServer, + ResolvedAcpxPluginConfig, +} from "./config-schema.js"; +import type { z } from "openclaw/plugin-sdk/zod"; +export { + ACPX_NON_INTERACTIVE_POLICIES, + ACPX_PERMISSION_MODES, + type AcpxMcpServer, + type AcpxNonInteractivePermissionPolicy, + type AcpxPermissionMode, + type AcpxPluginConfig, + type McpServerConfig, + type ResolvedAcpxPluginConfig, + createAcpxPluginConfigSchema, +} from "./config-schema.js"; export const ACPX_VERSION_ANY = "any"; export const ACPX_PLUGIN_TOOLS_MCP_SERVER_NAME = "openclaw-plugin-tools"; @@ -72,48 +84,6 @@ export function buildAcpxLocalInstallCommand(version: string = ACPX_PINNED_VERSI } export const ACPX_LOCAL_INSTALL_COMMAND = buildAcpxLocalInstallCommand(); -export type McpServerConfig = { - command: string; - args?: string[]; - env?: Record; -}; - -export type AcpxMcpServer = { - name: string; - command: string; - args: string[]; - env: Array<{ name: string; value: string }>; -}; - -export type AcpxPluginConfig = { - command?: string; - expectedVersion?: string; - cwd?: string; - permissionMode?: AcpxPermissionMode; - nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy; - pluginToolsMcpBridge?: boolean; - strictWindowsCmdWrapper?: boolean; - timeoutSeconds?: number; - queueOwnerTtlSeconds?: number; - mcpServers?: Record; -}; - -export type ResolvedAcpxPluginConfig = { - command: string; - expectedVersion?: string; - allowPluginLocalInstall: boolean; - stripProviderAuthEnvVars: boolean; - installCommand: string; - cwd: string; - permissionMode: AcpxPermissionMode; - nonInteractivePermissions: AcpxNonInteractivePermissionPolicy; - pluginToolsMcpBridge: boolean; - strictWindowsCmdWrapper: boolean; - timeoutSeconds?: number; - queueOwnerTtlSeconds: number; - mcpServers: Record; -}; - const DEFAULT_PERMISSION_MODE: AcpxPermissionMode = "approve-reads"; const DEFAULT_NON_INTERACTIVE_POLICY: AcpxNonInteractivePermissionPolicy = "fail"; const DEFAULT_QUEUE_OWNER_TTL_SECONDS = 0.1; @@ -123,56 +93,6 @@ type ParseResult = | { ok: true; value: AcpxPluginConfig | undefined } | { ok: false; message: string }; -const nonEmptyTrimmedString = (message: string) => - z.string({ error: message }).trim().min(1, { error: message }); - -const McpServerConfigSchema = z.object({ - command: nonEmptyTrimmedString("command must be a non-empty string").describe( - "Command to run the MCP server", - ), - args: z - .array(z.string({ error: "args must be an array of strings" }), { - error: "args must be an array of strings", - }) - .optional() - .describe("Arguments to pass to the command"), - env: z - .record(z.string(), z.string({ error: "env values must be strings" }), { - error: "env must be an object of strings", - }) - .optional() - .describe("Environment variables for the MCP server"), -}); - -const AcpxPluginConfigSchema = z.strictObject({ - command: nonEmptyTrimmedString("command must be a non-empty string").optional(), - expectedVersion: nonEmptyTrimmedString("expectedVersion must be a non-empty string").optional(), - cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(), - permissionMode: z - .enum(ACPX_PERMISSION_MODES, { - error: `permissionMode must be one of: ${ACPX_PERMISSION_MODES.join(", ")}`, - }) - .optional(), - nonInteractivePermissions: z - .enum(ACPX_NON_INTERACTIVE_POLICIES, { - error: `nonInteractivePermissions must be one of: ${ACPX_NON_INTERACTIVE_POLICIES.join(", ")}`, - }) - .optional(), - pluginToolsMcpBridge: z.boolean({ error: "pluginToolsMcpBridge must be a boolean" }).optional(), - strictWindowsCmdWrapper: z - .boolean({ error: "strictWindowsCmdWrapper must be a boolean" }) - .optional(), - timeoutSeconds: z - .number({ error: "timeoutSeconds must be a number >= 0.001" }) - .min(0.001, { error: "timeoutSeconds must be a number >= 0.001" }) - .optional(), - queueOwnerTtlSeconds: z - .number({ error: "queueOwnerTtlSeconds must be a number >= 0" }) - .min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" }) - .optional(), - mcpServers: z.record(z.string(), McpServerConfigSchema).optional(), -}); - function formatAcpxConfigIssue(issue: z.ZodIssue | undefined): string { if (!issue) { return "invalid config"; @@ -263,10 +183,6 @@ function resolveConfiguredMcpServers(params: { return resolved; } -export function createAcpxPluginConfigSchema(): OpenClawPluginConfigSchema { - return buildPluginConfigSchema(AcpxPluginConfigSchema); -} - export function toAcpMcpServers(mcpServers: Record): AcpxMcpServer[] { return Object.entries(mcpServers).map(([name, server]) => ({ name,