@openclaw/acpx 2026.5.2-beta.2 → 2026.5.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,385 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
- import type { ResolvedAcpxPluginConfig } from "./config.js";
5
-
6
- const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
7
- const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
8
- const CODEX_ACP_BIN = "codex-acp";
9
- const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
10
- const CLAUDE_ACP_PACKAGE_VERSION = "0.31.4";
11
- const CLAUDE_ACP_BIN = "claude-agent-acp";
12
- const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
13
- const requireFromHere = createRequire(import.meta.url);
14
-
15
- type PackageManifest = {
16
- name?: unknown;
17
- bin?: unknown;
18
- };
19
-
20
- function quoteCommandPart(value: string): string {
21
- return JSON.stringify(value);
22
- }
23
-
24
- function splitCommandParts(value: string): string[] {
25
- const parts: string[] = [];
26
- let current = "";
27
- let quote: "'" | '"' | null = null;
28
- let escaping = false;
29
-
30
- for (const ch of value) {
31
- if (escaping) {
32
- current += ch;
33
- escaping = false;
34
- continue;
35
- }
36
- if (ch === "\\" && quote !== "'") {
37
- escaping = true;
38
- continue;
39
- }
40
- if (quote) {
41
- if (ch === quote) {
42
- quote = null;
43
- } else {
44
- current += ch;
45
- }
46
- continue;
47
- }
48
- if (ch === "'" || ch === '"') {
49
- quote = ch;
50
- continue;
51
- }
52
- if (/\s/.test(ch)) {
53
- if (current) {
54
- parts.push(current);
55
- current = "";
56
- }
57
- continue;
58
- }
59
- current += ch;
60
- }
61
-
62
- if (escaping) {
63
- current += "\\";
64
- }
65
- if (current) {
66
- parts.push(current);
67
- }
68
- return parts;
69
- }
70
-
71
- function basename(value: string): string {
72
- return value.split(/[\\/]/).pop() ?? value;
73
- }
74
-
75
- function resolvePackageBinPath(
76
- packageJsonPath: string,
77
- manifest: PackageManifest,
78
- binName: string,
79
- ): string | undefined {
80
- const { bin } = manifest;
81
- const relativeBinPath =
82
- typeof bin === "string"
83
- ? bin
84
- : bin && typeof bin === "object"
85
- ? (bin as Record<string, unknown>)[binName]
86
- : undefined;
87
- if (typeof relativeBinPath !== "string" || relativeBinPath.trim() === "") {
88
- return undefined;
89
- }
90
- return path.resolve(path.dirname(packageJsonPath), relativeBinPath);
91
- }
92
-
93
- async function resolveInstalledAcpPackageBinPath(
94
- packageName: string,
95
- binName: string,
96
- ): Promise<string | undefined> {
97
- try {
98
- const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
99
- const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as PackageManifest;
100
- if (manifest.name !== packageName) {
101
- return undefined;
102
- }
103
- const binPath = resolvePackageBinPath(packageJsonPath, manifest, binName);
104
- if (!binPath) {
105
- return undefined;
106
- }
107
- await fs.access(binPath);
108
- return binPath;
109
- } catch {
110
- return undefined;
111
- }
112
- }
113
-
114
- async function resolveInstalledCodexAcpBinPath(): Promise<string | undefined> {
115
- // Keep OpenClaw's isolated CODEX_HOME wrapper, but launch the plugin-local
116
- // Codex ACP adapter when the package dependency is available.
117
- return await resolveInstalledAcpPackageBinPath(CODEX_ACP_PACKAGE, CODEX_ACP_BIN);
118
- }
119
-
120
- async function resolveInstalledClaudeAcpBinPath(): Promise<string | undefined> {
121
- return await resolveInstalledAcpPackageBinPath(CLAUDE_ACP_PACKAGE, CLAUDE_ACP_BIN);
122
- }
123
-
124
- function buildAdapterWrapperScript(params: {
125
- displayName: string;
126
- packageSpec: string;
127
- binName: string;
128
- installedBinPath?: string;
129
- envSetup: string;
130
- }): string {
131
- return `#!/usr/bin/env node
132
- import { existsSync } from "node:fs";
133
- import path from "node:path";
134
- import { spawn } from "node:child_process";
135
- import { fileURLToPath } from "node:url";
136
-
137
- ${params.envSetup}
138
- const configuredArgs = process.argv.slice(2);
139
-
140
- function resolveNpmCliPath() {
141
- const candidate = path.resolve(
142
- path.dirname(process.execPath),
143
- "..",
144
- "lib",
145
- "node_modules",
146
- "npm",
147
- "bin",
148
- "npm-cli.js",
149
- );
150
- return existsSync(candidate) ? candidate : undefined;
151
- }
152
-
153
- const npmCliPath = resolveNpmCliPath();
154
- const installedBinPath = ${params.installedBinPath ? quoteCommandPart(params.installedBinPath) : "undefined"};
155
- let defaultCommand;
156
- let defaultArgs;
157
- if (installedBinPath) {
158
- defaultCommand = process.execPath;
159
- defaultArgs = [installedBinPath];
160
- } else if (npmCliPath) {
161
- defaultCommand = process.execPath;
162
- defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
163
- } else {
164
- defaultCommand = process.platform === "win32" ? "npx.cmd" : "npx";
165
- defaultArgs = ["--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
166
- }
167
- const command =
168
- configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}" ? configuredArgs[1] : defaultCommand;
169
- const args =
170
- configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}"
171
- ? configuredArgs.slice(2)
172
- : [...defaultArgs, ...configuredArgs];
173
-
174
- if (!command) {
175
- console.error("[openclaw] missing configured ${params.displayName} ACP command");
176
- process.exit(1);
177
- }
178
-
179
- const child = spawn(command, args, {
180
- env,
181
- stdio: "inherit",
182
- windowsHide: true,
183
- });
184
-
185
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
186
- process.once(signal, () => {
187
- child.kill(signal);
188
- });
189
- }
190
-
191
- child.on("error", (error) => {
192
- console.error(\`[openclaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`);
193
- process.exit(1);
194
- });
195
-
196
- child.on("exit", (code, signal) => {
197
- if (code !== null) {
198
- process.exit(code);
199
- }
200
- process.exit(signal ? 1 : 0);
201
- });
202
- `;
203
- }
204
-
205
- function buildCodexAcpWrapperScript(installedBinPath?: string): string {
206
- return buildAdapterWrapperScript({
207
- displayName: "Codex",
208
- packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_RANGE}`,
209
- binName: CODEX_ACP_BIN,
210
- installedBinPath,
211
- envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
212
- const env = {
213
- ...process.env,
214
- CODEX_HOME: codexHome,
215
- };`,
216
- });
217
- }
218
-
219
- function buildClaudeAcpWrapperScript(installedBinPath?: string): string {
220
- return buildAdapterWrapperScript({
221
- displayName: "Claude",
222
- // This package is patched in OpenClaw; fallback must not float to an unpatched newer release.
223
- packageSpec: `${CLAUDE_ACP_PACKAGE}@${CLAUDE_ACP_PACKAGE_VERSION}`,
224
- binName: CLAUDE_ACP_BIN,
225
- installedBinPath,
226
- envSetup: `const env = {
227
- ...process.env,
228
- };`,
229
- });
230
- }
231
-
232
- async function prepareIsolatedCodexHome(baseDir: string): Promise<string> {
233
- const codexHome = path.join(baseDir, "codex-home");
234
- await fs.mkdir(codexHome, { recursive: true });
235
- await fs.writeFile(
236
- path.join(codexHome, "config.toml"),
237
- "# Generated by OpenClaw for Codex ACP sessions.\n",
238
- "utf8",
239
- );
240
- return codexHome;
241
- }
242
-
243
- async function makeGeneratedWrapperExecutableIfPossible(wrapperPath: string): Promise<void> {
244
- try {
245
- await fs.chmod(wrapperPath, 0o755);
246
- } catch {
247
- // The wrapper is invoked via `node wrapper.mjs`; executable mode is only a convenience.
248
- }
249
- }
250
-
251
- async function writeCodexAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
252
- await fs.mkdir(baseDir, { recursive: true });
253
- const wrapperPath = path.join(baseDir, "codex-acp-wrapper.mjs");
254
- await fs.writeFile(wrapperPath, buildCodexAcpWrapperScript(installedBinPath), {
255
- encoding: "utf8",
256
- });
257
- await makeGeneratedWrapperExecutableIfPossible(wrapperPath);
258
- return wrapperPath;
259
- }
260
-
261
- async function writeClaudeAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
262
- await fs.mkdir(baseDir, { recursive: true });
263
- const wrapperPath = path.join(baseDir, "claude-agent-acp-wrapper.mjs");
264
- await fs.writeFile(wrapperPath, buildClaudeAcpWrapperScript(installedBinPath), {
265
- encoding: "utf8",
266
- });
267
- await makeGeneratedWrapperExecutableIfPossible(wrapperPath);
268
- return wrapperPath;
269
- }
270
-
271
- function buildWrapperCommand(wrapperPath: string, args: string[] = []): string {
272
- return [process.execPath, wrapperPath, ...args].map(quoteCommandPart).join(" ");
273
- }
274
-
275
- function isAcpPackageSpec(value: string, packageName: string): boolean {
276
- const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
277
- return new RegExp(`^${escapedPackageName}(?:@.+)?$`, "i").test(value.trim());
278
- }
279
-
280
- function isAcpBinName(value: string, binName: string): boolean {
281
- const commandName = basename(value);
282
- const escapedBinName = binName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
283
- return new RegExp(`^${escapedBinName}(?:\\.exe|\\.[cm]?js)?$`, "i").test(commandName);
284
- }
285
-
286
- function isPackageRunnerCommand(value: string): boolean {
287
- return /^(?:npx|npm|pnpm|bunx)(?:\.cmd|\.exe)?$/i.test(basename(value));
288
- }
289
-
290
- function extractConfiguredAdapterArgs(params: {
291
- configuredCommand?: string;
292
- packageName: string;
293
- binName: string;
294
- }): string[] | undefined {
295
- const trimmedConfiguredCommand = params.configuredCommand?.trim();
296
- if (!trimmedConfiguredCommand) {
297
- return [];
298
- }
299
- const parts = splitCommandParts(trimmedConfiguredCommand);
300
- if (!parts.length) {
301
- return [];
302
- }
303
-
304
- const packageIndex = parts.findIndex((part) => isAcpPackageSpec(part, params.packageName));
305
- if (packageIndex >= 0) {
306
- if (!isPackageRunnerCommand(parts[0] ?? "")) {
307
- return undefined;
308
- }
309
- const afterPackage = parts.slice(packageIndex + 1);
310
- if (afterPackage[0] === "--" && isAcpBinName(afterPackage[1] ?? "", params.binName)) {
311
- return afterPackage.slice(2);
312
- }
313
- if (isAcpBinName(afterPackage[0] ?? "", params.binName)) {
314
- return afterPackage.slice(1);
315
- }
316
- return afterPackage[0] === "--" ? afterPackage.slice(1) : afterPackage;
317
- }
318
-
319
- if (isAcpBinName(parts[0] ?? "", params.binName)) {
320
- return parts.slice(1);
321
- }
322
- if (basename(parts[0] ?? "") === "node" && isAcpBinName(parts[1] ?? "", params.binName)) {
323
- return parts.slice(2);
324
- }
325
-
326
- return undefined;
327
- }
328
-
329
- function buildCodexAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
330
- const configuredAdapterArgs = extractConfiguredAdapterArgs({
331
- configuredCommand,
332
- packageName: CODEX_ACP_PACKAGE,
333
- binName: CODEX_ACP_BIN,
334
- });
335
- if (configuredAdapterArgs) {
336
- return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
337
- }
338
- return buildWrapperCommand(wrapperPath, [
339
- RUN_CONFIGURED_COMMAND_SENTINEL,
340
- ...splitCommandParts(configuredCommand?.trim() ?? ""),
341
- ]);
342
- }
343
-
344
- function buildClaudeAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
345
- const configuredAdapterArgs = extractConfiguredAdapterArgs({
346
- configuredCommand,
347
- packageName: CLAUDE_ACP_PACKAGE,
348
- binName: CLAUDE_ACP_BIN,
349
- });
350
- if (configuredAdapterArgs) {
351
- return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
352
- }
353
- return configuredCommand?.trim() || buildWrapperCommand(wrapperPath);
354
- }
355
-
356
- export async function prepareAcpxCodexAuthConfig(params: {
357
- pluginConfig: ResolvedAcpxPluginConfig;
358
- stateDir: string;
359
- logger?: unknown;
360
- resolveInstalledCodexAcpBinPath?: () => Promise<string | undefined>;
361
- resolveInstalledClaudeAcpBinPath?: () => Promise<string | undefined>;
362
- }): Promise<ResolvedAcpxPluginConfig> {
363
- void params.logger;
364
- const codexBaseDir = path.join(params.stateDir, "acpx");
365
- await prepareIsolatedCodexHome(codexBaseDir);
366
- const installedCodexBinPath = await (
367
- params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath
368
- )();
369
- const installedClaudeBinPath = await (
370
- params.resolveInstalledClaudeAcpBinPath ?? resolveInstalledClaudeAcpBinPath
371
- )();
372
- const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedCodexBinPath);
373
- const claudeWrapperPath = await writeClaudeAcpWrapper(codexBaseDir, installedClaudeBinPath);
374
- const configuredCodexCommand = params.pluginConfig.agents.codex;
375
- const configuredClaudeCommand = params.pluginConfig.agents.claude;
376
-
377
- return {
378
- ...params.pluginConfig,
379
- agents: {
380
- ...params.pluginConfig.agents,
381
- codex: buildCodexAcpWrapperCommand(wrapperPath, configuredCodexCommand),
382
- claude: buildClaudeAcpWrapperCommand(claudeWrapperPath, configuredClaudeCommand),
383
- },
384
- };
385
- }
@@ -1,117 +0,0 @@
1
- import { z } from "openclaw/plugin-sdk/zod";
2
-
3
- const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const;
4
- export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
5
-
6
- const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
7
- export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
8
-
9
- export const DEFAULT_ACPX_TIMEOUT_SECONDS = 120;
10
-
11
- export type McpServerConfig = {
12
- command: string;
13
- args?: string[];
14
- env?: Record<string, string>;
15
- };
16
-
17
- export type AcpxMcpServer = {
18
- name: string;
19
- command: string;
20
- args: string[];
21
- env: Array<{ name: string; value: string }>;
22
- };
23
-
24
- export type AcpxPluginConfig = {
25
- cwd?: string;
26
- stateDir?: string;
27
- probeAgent?: string;
28
- permissionMode?: AcpxPermissionMode;
29
- nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy;
30
- pluginToolsMcpBridge?: boolean;
31
- openClawToolsMcpBridge?: boolean;
32
- strictWindowsCmdWrapper?: boolean;
33
- timeoutSeconds?: number;
34
- queueOwnerTtlSeconds?: number;
35
- mcpServers?: Record<string, McpServerConfig>;
36
- agents?: Record<string, { command: string }>;
37
- };
38
-
39
- export type ResolvedAcpxPluginConfig = {
40
- cwd: string;
41
- stateDir: string;
42
- probeAgent?: string;
43
- permissionMode: AcpxPermissionMode;
44
- nonInteractivePermissions: AcpxNonInteractivePermissionPolicy;
45
- pluginToolsMcpBridge: boolean;
46
- openClawToolsMcpBridge: boolean;
47
- strictWindowsCmdWrapper: boolean;
48
- timeoutSeconds?: number;
49
- queueOwnerTtlSeconds: number;
50
- legacyCompatibilityConfig: {
51
- strictWindowsCmdWrapper?: boolean;
52
- queueOwnerTtlSeconds?: number;
53
- };
54
- mcpServers: Record<string, McpServerConfig>;
55
- agents: Record<string, string>;
56
- };
57
-
58
- const nonEmptyTrimmedString = (message: string) =>
59
- z.string({ error: message }).trim().min(1, { error: message });
60
-
61
- const McpServerConfigSchema = z.object({
62
- command: nonEmptyTrimmedString("command must be a non-empty string").describe(
63
- "Command to run the MCP server",
64
- ),
65
- args: z
66
- .array(z.string({ error: "args must be an array of strings" }), {
67
- error: "args must be an array of strings",
68
- })
69
- .optional()
70
- .describe("Arguments to pass to the command"),
71
- env: z
72
- .record(z.string(), z.string({ error: "env values must be strings" }), {
73
- error: "env must be an object of strings",
74
- })
75
- .optional()
76
- .describe("Environment variables for the MCP server"),
77
- });
78
-
79
- export const AcpxPluginConfigSchema = z.strictObject({
80
- cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(),
81
- stateDir: nonEmptyTrimmedString("stateDir must be a non-empty string").optional(),
82
- probeAgent: nonEmptyTrimmedString("probeAgent must be a non-empty string").optional(),
83
- permissionMode: z
84
- .enum(ACPX_PERMISSION_MODES, {
85
- error: `permissionMode must be one of: ${ACPX_PERMISSION_MODES.join(", ")}`,
86
- })
87
- .optional(),
88
- nonInteractivePermissions: z
89
- .enum(ACPX_NON_INTERACTIVE_POLICIES, {
90
- error: `nonInteractivePermissions must be one of: ${ACPX_NON_INTERACTIVE_POLICIES.join(", ")}`,
91
- })
92
- .optional(),
93
- pluginToolsMcpBridge: z.boolean({ error: "pluginToolsMcpBridge must be a boolean" }).optional(),
94
- openClawToolsMcpBridge: z
95
- .boolean({ error: "openClawToolsMcpBridge must be a boolean" })
96
- .optional(),
97
- strictWindowsCmdWrapper: z
98
- .boolean({ error: "strictWindowsCmdWrapper must be a boolean" })
99
- .optional(),
100
- timeoutSeconds: z
101
- .number({ error: "timeoutSeconds must be a number >= 0.001" })
102
- .min(0.001, { error: "timeoutSeconds must be a number >= 0.001" })
103
- .default(DEFAULT_ACPX_TIMEOUT_SECONDS),
104
- queueOwnerTtlSeconds: z
105
- .number({ error: "queueOwnerTtlSeconds must be a number >= 0" })
106
- .min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" })
107
- .optional(),
108
- mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
109
- agents: z
110
- .record(
111
- z.string(),
112
- z.strictObject({
113
- command: nonEmptyTrimmedString("agents.<id>.command must be a non-empty string"),
114
- }),
115
- )
116
- .optional(),
117
- });
@@ -1,144 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { describe, expect, it } from "vitest";
4
- import { resolveAcpxPluginConfig, resolveAcpxPluginRoot } from "./config.js";
5
-
6
- describe("embedded acpx plugin config", () => {
7
- it("resolves workspace stateDir and cwd by default", () => {
8
- const workspaceDir = "/tmp/openclaw-acpx";
9
- const resolved = resolveAcpxPluginConfig({
10
- rawConfig: undefined,
11
- workspaceDir,
12
- });
13
-
14
- expect(resolved.cwd).toBe(workspaceDir);
15
- expect(resolved.stateDir).toBe(path.join(workspaceDir, "state"));
16
- expect(resolved.permissionMode).toBe("approve-reads");
17
- expect(resolved.nonInteractivePermissions).toBe("fail");
18
- expect(resolved.timeoutSeconds).toBe(120);
19
- expect(resolved.agents).toEqual({});
20
- });
21
-
22
- it("keeps explicit timeoutSeconds config", () => {
23
- const resolved = resolveAcpxPluginConfig({
24
- rawConfig: {
25
- timeoutSeconds: 300,
26
- },
27
- workspaceDir: "/tmp/openclaw-acpx",
28
- });
29
-
30
- expect(resolved.timeoutSeconds).toBe(300);
31
- });
32
-
33
- it("keeps explicit probeAgent config", () => {
34
- const resolved = resolveAcpxPluginConfig({
35
- rawConfig: {
36
- probeAgent: "claude",
37
- },
38
- workspaceDir: "/tmp/openclaw-acpx",
39
- });
40
-
41
- expect(resolved.probeAgent).toBe("claude");
42
- });
43
-
44
- it("accepts agent command overrides", () => {
45
- const resolved = resolveAcpxPluginConfig({
46
- rawConfig: {
47
- agents: {
48
- claude: { command: "claude --acp" },
49
- codex: { command: "codex custom-acp" },
50
- },
51
- },
52
- workspaceDir: "/tmp/openclaw-acpx",
53
- });
54
-
55
- expect(resolved.agents).toEqual({
56
- claude: "claude --acp",
57
- codex: "codex custom-acp",
58
- });
59
- });
60
-
61
- it("leaves probeAgent undefined by default so the runtime picks its built-in probe agent", () => {
62
- const resolved = resolveAcpxPluginConfig({
63
- rawConfig: undefined,
64
- workspaceDir: "/tmp/openclaw-acpx",
65
- });
66
-
67
- expect(resolved.probeAgent).toBeUndefined();
68
- });
69
-
70
- it("carries an explicit probeAgent through to the resolved plugin config, trimmed and lowercased", () => {
71
- const resolved = resolveAcpxPluginConfig({
72
- rawConfig: {
73
- probeAgent: " OpenCode ",
74
- },
75
- workspaceDir: "/tmp/openclaw-acpx",
76
- });
77
-
78
- expect(resolved.probeAgent).toBe("opencode");
79
- });
80
-
81
- it("rejects an empty probeAgent string", () => {
82
- expect(() =>
83
- resolveAcpxPluginConfig({
84
- rawConfig: {
85
- probeAgent: "",
86
- },
87
- workspaceDir: "/tmp/openclaw-acpx",
88
- }),
89
- ).toThrow(/probeAgent must be a non-empty string/);
90
- });
91
-
92
- it("injects the built-in plugin-tools MCP server only when explicitly enabled", () => {
93
- const resolved = resolveAcpxPluginConfig({
94
- rawConfig: {
95
- pluginToolsMcpBridge: true,
96
- },
97
- workspaceDir: "/tmp/openclaw-acpx",
98
- });
99
-
100
- const server = resolved.mcpServers["openclaw-plugin-tools"];
101
- expect(server).toBeDefined();
102
- expect(server.command).toBe(process.execPath);
103
- expect(Array.isArray(server.args)).toBe(true);
104
- expect(server.args?.length).toBeGreaterThan(0);
105
- });
106
-
107
- it("injects the built-in OpenClaw tools MCP server only when explicitly enabled", () => {
108
- const resolved = resolveAcpxPluginConfig({
109
- rawConfig: {
110
- openClawToolsMcpBridge: true,
111
- },
112
- workspaceDir: "/tmp/openclaw-acpx",
113
- });
114
-
115
- const server = resolved.mcpServers["openclaw-tools"];
116
- expect(server).toBeDefined();
117
- expect(server.command).toBe(process.execPath);
118
- expect(Array.isArray(server.args)).toBe(true);
119
- expect(server.args?.length).toBeGreaterThan(0);
120
- });
121
-
122
- it("keeps the runtime json schema in sync with the manifest config schema", () => {
123
- const pluginRoot = resolveAcpxPluginRoot();
124
- const manifest = JSON.parse(
125
- fs.readFileSync(path.join(pluginRoot, "openclaw.plugin.json"), "utf8"),
126
- ) as { configSchema?: unknown };
127
-
128
- expect(manifest.configSchema).toMatchObject({
129
- type: "object",
130
- additionalProperties: false,
131
- properties: expect.objectContaining({
132
- cwd: expect.any(Object),
133
- stateDir: expect.any(Object),
134
- probeAgent: expect.any(Object),
135
- timeoutSeconds: expect.objectContaining({
136
- default: 120,
137
- }),
138
- agents: expect.any(Object),
139
- mcpServers: expect.any(Object),
140
- openClawToolsMcpBridge: expect.any(Object),
141
- }),
142
- });
143
- });
144
- });