@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.
- package/dist/index.js +14 -0
- package/dist/register.runtime.js +108 -0
- package/dist/runtime-B3LWev_t.js +357 -0
- package/dist/runtime-api.js +4 -0
- package/dist/service-YhcC786_.js +665 -0
- package/dist/setup-api.js +16 -0
- package/package.json +33 -3
- package/AGENTS.md +0 -54
- package/index.test.ts +0 -119
- package/index.ts +0 -19
- package/register.runtime.ts +0 -154
- package/runtime-api.ts +0 -46
- package/setup-api.ts +0 -18
- package/src/acpx-runtime-compat.d.ts +0 -62
- package/src/claude-agent-acp-completion.test.ts +0 -129
- package/src/codex-auth-bridge.test.ts +0 -448
- package/src/codex-auth-bridge.ts +0 -385
- package/src/config-schema.ts +0 -117
- package/src/config.test.ts +0 -144
- package/src/config.ts +0 -273
- package/src/manifest.test.ts +0 -20
- package/src/runtime-internals/mcp-command-line.test.ts +0 -59
- package/src/runtime-internals/mcp-proxy.test.ts +0 -114
- package/src/runtime.test.ts +0 -816
- package/src/runtime.ts +0 -613
- package/src/service.test.ts +0 -401
- package/src/service.ts +0 -278
- package/tsconfig.json +0 -16
- /package/{src/runtime-internals → dist}/error-format.mjs +0 -0
- /package/{src/runtime-internals → dist}/mcp-command-line.mjs +0 -0
- /package/{src/runtime-internals → dist}/mcp-proxy.mjs +0 -0
package/src/service.test.ts
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
const { runtimeRegistry } = vi.hoisted(() => ({
|
|
7
|
-
runtimeRegistry: new Map<string, { runtime: unknown; healthy?: () => boolean }>(),
|
|
8
|
-
}));
|
|
9
|
-
const { prepareAcpxCodexAuthConfigMock } = vi.hoisted(() => ({
|
|
10
|
-
prepareAcpxCodexAuthConfigMock: vi.fn(
|
|
11
|
-
async ({ pluginConfig }: { pluginConfig: unknown }) => pluginConfig,
|
|
12
|
-
),
|
|
13
|
-
}));
|
|
14
|
-
const { acpxRuntimeConstructorMock, createAgentRegistryMock, createFileSessionStoreMock } =
|
|
15
|
-
vi.hoisted(() => ({
|
|
16
|
-
acpxRuntimeConstructorMock: vi.fn(function AcpxRuntime(options: unknown) {
|
|
17
|
-
return {
|
|
18
|
-
cancel: vi.fn(async () => {}),
|
|
19
|
-
close: vi.fn(async () => {}),
|
|
20
|
-
doctor: vi.fn(async () => ({ ok: true, message: "ok" })),
|
|
21
|
-
ensureSession: vi.fn(async () => ({
|
|
22
|
-
backend: "acpx",
|
|
23
|
-
runtimeSessionName: "agent:codex:acp:test",
|
|
24
|
-
sessionKey: "agent:codex:acp:test",
|
|
25
|
-
})),
|
|
26
|
-
getCapabilities: vi.fn(async () => ({ controls: [] })),
|
|
27
|
-
getStatus: vi.fn(async () => ({ summary: "ready" })),
|
|
28
|
-
isHealthy: vi.fn(() => true),
|
|
29
|
-
prepareFreshSession: vi.fn(async () => {}),
|
|
30
|
-
probeAvailability: vi.fn(async () => {}),
|
|
31
|
-
runTurn: vi.fn(async function* () {}),
|
|
32
|
-
setConfigOption: vi.fn(async () => {}),
|
|
33
|
-
setMode: vi.fn(async () => {}),
|
|
34
|
-
__options: options,
|
|
35
|
-
};
|
|
36
|
-
}),
|
|
37
|
-
createAgentRegistryMock: vi.fn(() => ({})),
|
|
38
|
-
createFileSessionStoreMock: vi.fn(() => ({})),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
vi.mock("../runtime-api.js", () => ({
|
|
42
|
-
getAcpRuntimeBackend: (id: string) => runtimeRegistry.get(id),
|
|
43
|
-
registerAcpRuntimeBackend: (entry: { id: string; runtime: unknown; healthy?: () => boolean }) => {
|
|
44
|
-
runtimeRegistry.set(entry.id, entry);
|
|
45
|
-
},
|
|
46
|
-
unregisterAcpRuntimeBackend: (id: string) => {
|
|
47
|
-
runtimeRegistry.delete(id);
|
|
48
|
-
},
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
vi.mock("./runtime.js", () => ({
|
|
52
|
-
ACPX_BACKEND_ID: "acpx",
|
|
53
|
-
AcpxRuntime: acpxRuntimeConstructorMock,
|
|
54
|
-
createAgentRegistry: createAgentRegistryMock,
|
|
55
|
-
createFileSessionStore: createFileSessionStoreMock,
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
vi.mock("./codex-auth-bridge.js", () => ({
|
|
59
|
-
prepareAcpxCodexAuthConfig: prepareAcpxCodexAuthConfigMock,
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
import { getAcpRuntimeBackend } from "../runtime-api.js";
|
|
63
|
-
import { createAcpxRuntimeService } from "./service.js";
|
|
64
|
-
|
|
65
|
-
const tempDirs: string[] = [];
|
|
66
|
-
|
|
67
|
-
async function makeTempDir(): Promise<string> {
|
|
68
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-acpx-service-"));
|
|
69
|
-
tempDirs.push(dir);
|
|
70
|
-
return dir;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
afterEach(async () => {
|
|
74
|
-
runtimeRegistry.clear();
|
|
75
|
-
prepareAcpxCodexAuthConfigMock.mockClear();
|
|
76
|
-
acpxRuntimeConstructorMock.mockClear();
|
|
77
|
-
createAgentRegistryMock.mockClear();
|
|
78
|
-
createFileSessionStoreMock.mockClear();
|
|
79
|
-
delete process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE;
|
|
80
|
-
delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME;
|
|
81
|
-
delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE;
|
|
82
|
-
for (const dir of tempDirs.splice(0)) {
|
|
83
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
function createServiceContext(workspaceDir: string) {
|
|
88
|
-
return {
|
|
89
|
-
workspaceDir,
|
|
90
|
-
stateDir: path.join(workspaceDir, ".openclaw-plugin-state"),
|
|
91
|
-
config: {},
|
|
92
|
-
logger: {
|
|
93
|
-
info: vi.fn(),
|
|
94
|
-
warn: vi.fn(),
|
|
95
|
-
error: vi.fn(),
|
|
96
|
-
debug: vi.fn(),
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function createMockRuntime(overrides: Record<string, unknown> = {}) {
|
|
102
|
-
return {
|
|
103
|
-
ensureSession: vi.fn(),
|
|
104
|
-
runTurn: vi.fn(),
|
|
105
|
-
cancel: vi.fn(),
|
|
106
|
-
close: vi.fn(),
|
|
107
|
-
probeAvailability: vi.fn(async () => {}),
|
|
108
|
-
isHealthy: vi.fn(() => true),
|
|
109
|
-
doctor: vi.fn(async () => ({ ok: true, message: "ok" })),
|
|
110
|
-
...overrides,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
describe("createAcpxRuntimeService", () => {
|
|
115
|
-
it("registers and unregisters the embedded backend", async () => {
|
|
116
|
-
const workspaceDir = await makeTempDir();
|
|
117
|
-
const ctx = createServiceContext(workspaceDir);
|
|
118
|
-
const runtime = createMockRuntime();
|
|
119
|
-
const service = createAcpxRuntimeService({
|
|
120
|
-
runtimeFactory: () => runtime as never,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
await service.start(ctx);
|
|
124
|
-
|
|
125
|
-
expect(getAcpRuntimeBackend("acpx")?.runtime).toBe(runtime);
|
|
126
|
-
|
|
127
|
-
await service.stop?.(ctx);
|
|
128
|
-
|
|
129
|
-
expect(getAcpRuntimeBackend("acpx")).toBeUndefined();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("creates the embedded runtime state directory without probing at startup by default", async () => {
|
|
133
|
-
const workspaceDir = await makeTempDir();
|
|
134
|
-
const stateDir = path.join(workspaceDir, "custom-state");
|
|
135
|
-
const ctx = createServiceContext(workspaceDir);
|
|
136
|
-
const probeAvailability = vi.fn(async () => {
|
|
137
|
-
await fs.access(stateDir);
|
|
138
|
-
});
|
|
139
|
-
const runtime = createMockRuntime({
|
|
140
|
-
doctor: async () => ({ ok: true, message: "ok" }),
|
|
141
|
-
isHealthy: () => true,
|
|
142
|
-
probeAvailability,
|
|
143
|
-
});
|
|
144
|
-
const service = createAcpxRuntimeService({
|
|
145
|
-
pluginConfig: { stateDir },
|
|
146
|
-
runtimeFactory: () => runtime as never,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
await service.start(ctx);
|
|
150
|
-
|
|
151
|
-
await fs.access(stateDir);
|
|
152
|
-
expect(probeAvailability).not.toHaveBeenCalled();
|
|
153
|
-
expect(getAcpRuntimeBackend("acpx")?.healthy).toBeUndefined();
|
|
154
|
-
|
|
155
|
-
await service.stop?.(ctx);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("registers the default backend without importing ACPX runtime until first use", async () => {
|
|
159
|
-
const workspaceDir = await makeTempDir();
|
|
160
|
-
const ctx = createServiceContext(workspaceDir);
|
|
161
|
-
const service = createAcpxRuntimeService();
|
|
162
|
-
|
|
163
|
-
await service.start(ctx);
|
|
164
|
-
|
|
165
|
-
const backend = getAcpRuntimeBackend("acpx");
|
|
166
|
-
expect(backend?.runtime).toBeDefined();
|
|
167
|
-
expect(acpxRuntimeConstructorMock).not.toHaveBeenCalled();
|
|
168
|
-
|
|
169
|
-
await backend?.runtime.ensureSession({
|
|
170
|
-
agent: "codex",
|
|
171
|
-
mode: "oneshot",
|
|
172
|
-
sessionKey: "agent:codex:acp:test",
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
expect(acpxRuntimeConstructorMock).toHaveBeenCalledOnce();
|
|
176
|
-
|
|
177
|
-
await service.stop?.(ctx);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("can run the embedded runtime probe at startup when explicitly enabled", async () => {
|
|
181
|
-
process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1";
|
|
182
|
-
const workspaceDir = await makeTempDir();
|
|
183
|
-
const ctx = createServiceContext(workspaceDir);
|
|
184
|
-
const probeAvailability = vi.fn(async () => {});
|
|
185
|
-
const runtime = createMockRuntime({
|
|
186
|
-
probeAvailability,
|
|
187
|
-
isHealthy: () => true,
|
|
188
|
-
});
|
|
189
|
-
const service = createAcpxRuntimeService({
|
|
190
|
-
runtimeFactory: () => runtime as never,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await service.start(ctx);
|
|
194
|
-
|
|
195
|
-
expect(probeAvailability).toHaveBeenCalledOnce();
|
|
196
|
-
expect(getAcpRuntimeBackend("acpx")?.healthy?.()).toBe(true);
|
|
197
|
-
|
|
198
|
-
await service.stop?.(ctx);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("passes the default runtime timeout to the embedded runtime factory", async () => {
|
|
202
|
-
const workspaceDir = await makeTempDir();
|
|
203
|
-
const ctx = createServiceContext(workspaceDir);
|
|
204
|
-
const runtime = createMockRuntime();
|
|
205
|
-
const runtimeFactory = vi.fn(() => runtime as never);
|
|
206
|
-
const service = createAcpxRuntimeService({
|
|
207
|
-
runtimeFactory,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
await service.start(ctx);
|
|
211
|
-
|
|
212
|
-
expect(runtimeFactory).toHaveBeenCalledWith(
|
|
213
|
-
expect.objectContaining({
|
|
214
|
-
pluginConfig: expect.objectContaining({
|
|
215
|
-
timeoutSeconds: 120,
|
|
216
|
-
}),
|
|
217
|
-
}),
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
await service.stop?.(ctx);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("forwards a configured probeAgent to the runtime factory so the probe does not hardcode the default", async () => {
|
|
224
|
-
const workspaceDir = await makeTempDir();
|
|
225
|
-
const ctx = createServiceContext(workspaceDir);
|
|
226
|
-
const runtime = {
|
|
227
|
-
ensureSession: vi.fn(),
|
|
228
|
-
runTurn: vi.fn(),
|
|
229
|
-
cancel: vi.fn(),
|
|
230
|
-
close: vi.fn(),
|
|
231
|
-
probeAvailability: vi.fn(async () => {}),
|
|
232
|
-
isHealthy: vi.fn(() => true),
|
|
233
|
-
doctor: vi.fn(async () => ({ ok: true, message: "ok" })),
|
|
234
|
-
};
|
|
235
|
-
const runtimeFactory = vi.fn(() => runtime as never);
|
|
236
|
-
const service = createAcpxRuntimeService({
|
|
237
|
-
pluginConfig: { probeAgent: "opencode" },
|
|
238
|
-
runtimeFactory,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await service.start(ctx);
|
|
242
|
-
|
|
243
|
-
expect(runtimeFactory).toHaveBeenCalledWith(
|
|
244
|
-
expect.objectContaining({
|
|
245
|
-
pluginConfig: expect.objectContaining({
|
|
246
|
-
probeAgent: "opencode",
|
|
247
|
-
}),
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
await service.stop?.(ctx);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("uses the first allowed ACP agent as the default probe agent", async () => {
|
|
255
|
-
const workspaceDir = await makeTempDir();
|
|
256
|
-
const ctx = createServiceContext(workspaceDir);
|
|
257
|
-
ctx.config = {
|
|
258
|
-
acp: {
|
|
259
|
-
allowedAgents: [" OpenCode ", "codex"],
|
|
260
|
-
},
|
|
261
|
-
};
|
|
262
|
-
const runtime = createMockRuntime();
|
|
263
|
-
const runtimeFactory = vi.fn(() => runtime as never);
|
|
264
|
-
const service = createAcpxRuntimeService({
|
|
265
|
-
runtimeFactory,
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
await service.start(ctx);
|
|
269
|
-
|
|
270
|
-
expect(runtimeFactory).toHaveBeenCalledWith(
|
|
271
|
-
expect.objectContaining({
|
|
272
|
-
pluginConfig: expect.objectContaining({
|
|
273
|
-
probeAgent: "opencode",
|
|
274
|
-
}),
|
|
275
|
-
}),
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
await service.stop?.(ctx);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("keeps explicit probeAgent ahead of acp.allowedAgents", async () => {
|
|
282
|
-
const workspaceDir = await makeTempDir();
|
|
283
|
-
const ctx = createServiceContext(workspaceDir);
|
|
284
|
-
ctx.config = {
|
|
285
|
-
acp: {
|
|
286
|
-
allowedAgents: ["opencode"],
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
const runtime = createMockRuntime();
|
|
290
|
-
const runtimeFactory = vi.fn(() => runtime as never);
|
|
291
|
-
const service = createAcpxRuntimeService({
|
|
292
|
-
pluginConfig: { probeAgent: "codex" },
|
|
293
|
-
runtimeFactory,
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
await service.start(ctx);
|
|
297
|
-
|
|
298
|
-
expect(runtimeFactory).toHaveBeenCalledWith(
|
|
299
|
-
expect.objectContaining({
|
|
300
|
-
pluginConfig: expect.objectContaining({
|
|
301
|
-
probeAgent: "codex",
|
|
302
|
-
}),
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
await service.stop?.(ctx);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("warns when legacy compatibility config is explicitly ignored", async () => {
|
|
310
|
-
const workspaceDir = await makeTempDir();
|
|
311
|
-
const ctx = createServiceContext(workspaceDir);
|
|
312
|
-
const runtime = createMockRuntime();
|
|
313
|
-
const service = createAcpxRuntimeService({
|
|
314
|
-
pluginConfig: {
|
|
315
|
-
queueOwnerTtlSeconds: 30,
|
|
316
|
-
strictWindowsCmdWrapper: false,
|
|
317
|
-
},
|
|
318
|
-
runtimeFactory: () => runtime as never,
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
await service.start(ctx);
|
|
322
|
-
|
|
323
|
-
expect(ctx.logger.warn).toHaveBeenCalledWith(
|
|
324
|
-
expect.stringContaining(
|
|
325
|
-
"embedded acpx runtime ignores legacy compatibility config: queueOwnerTtlSeconds, strictWindowsCmdWrapper=false",
|
|
326
|
-
),
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
await service.stop?.(ctx);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("can skip the embedded runtime probe via env", async () => {
|
|
333
|
-
process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1";
|
|
334
|
-
process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE = "1";
|
|
335
|
-
const workspaceDir = await makeTempDir();
|
|
336
|
-
const ctx = createServiceContext(workspaceDir);
|
|
337
|
-
const probeAvailability = vi.fn(async () => {});
|
|
338
|
-
const runtime = createMockRuntime({
|
|
339
|
-
doctor: async () => ({ ok: false, message: "nope" }),
|
|
340
|
-
isHealthy: () => false,
|
|
341
|
-
probeAvailability,
|
|
342
|
-
});
|
|
343
|
-
const service = createAcpxRuntimeService({
|
|
344
|
-
runtimeFactory: () => runtime as never,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
await service.start(ctx);
|
|
348
|
-
|
|
349
|
-
expect(probeAvailability).not.toHaveBeenCalled();
|
|
350
|
-
expect(getAcpRuntimeBackend("acpx")).toBeTruthy();
|
|
351
|
-
|
|
352
|
-
await service.stop?.(ctx);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("formats non-string doctor details without losing object payloads", async () => {
|
|
356
|
-
process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1";
|
|
357
|
-
const workspaceDir = await makeTempDir();
|
|
358
|
-
const ctx = createServiceContext(workspaceDir);
|
|
359
|
-
const runtime = createMockRuntime({
|
|
360
|
-
doctor: async () => ({
|
|
361
|
-
ok: false,
|
|
362
|
-
message: "probe failed",
|
|
363
|
-
details: [{ code: "ACP_CLOSED", agent: "codex" }, new Error("stdin closed")],
|
|
364
|
-
}),
|
|
365
|
-
isHealthy: () => false,
|
|
366
|
-
});
|
|
367
|
-
const service = createAcpxRuntimeService({
|
|
368
|
-
runtimeFactory: () => runtime as never,
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
await service.start(ctx);
|
|
372
|
-
|
|
373
|
-
await vi.waitFor(() => {
|
|
374
|
-
expect(ctx.logger.warn).toHaveBeenCalledWith(
|
|
375
|
-
'embedded acpx runtime backend probe failed: probe failed ({"code":"ACP_CLOSED","agent":"codex"}; stdin closed)',
|
|
376
|
-
);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
await service.stop?.(ctx);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("can skip the embedded runtime backend via env", async () => {
|
|
383
|
-
process.env.OPENCLAW_SKIP_ACPX_RUNTIME = "1";
|
|
384
|
-
const workspaceDir = await makeTempDir();
|
|
385
|
-
const ctx = createServiceContext(workspaceDir);
|
|
386
|
-
const runtimeFactory = vi.fn(() => {
|
|
387
|
-
throw new Error("runtime factory should not run when ACPX is skipped");
|
|
388
|
-
});
|
|
389
|
-
const service = createAcpxRuntimeService({
|
|
390
|
-
runtimeFactory: runtimeFactory as never,
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
await service.start(ctx);
|
|
394
|
-
|
|
395
|
-
expect(runtimeFactory).not.toHaveBeenCalled();
|
|
396
|
-
expect(getAcpRuntimeBackend("acpx")).toBeUndefined();
|
|
397
|
-
expect(ctx.logger.info).toHaveBeenCalledWith(
|
|
398
|
-
"skipping embedded acpx runtime backend (OPENCLAW_SKIP_ACPX_RUNTIME=1)",
|
|
399
|
-
);
|
|
400
|
-
});
|
|
401
|
-
});
|
package/src/service.ts
DELETED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import { inspect } from "node:util";
|
|
3
|
-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
|
-
import type {
|
|
5
|
-
AcpRuntime,
|
|
6
|
-
OpenClawPluginService,
|
|
7
|
-
OpenClawPluginServiceContext,
|
|
8
|
-
PluginLogger,
|
|
9
|
-
} from "../runtime-api.js";
|
|
10
|
-
import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "../runtime-api.js";
|
|
11
|
-
import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js";
|
|
12
|
-
import {
|
|
13
|
-
resolveAcpxPluginConfig,
|
|
14
|
-
toAcpMcpServers,
|
|
15
|
-
type ResolvedAcpxPluginConfig,
|
|
16
|
-
} from "./config.js";
|
|
17
|
-
|
|
18
|
-
type AcpxRuntimeLike = AcpRuntime & {
|
|
19
|
-
probeAvailability(): Promise<void>;
|
|
20
|
-
isHealthy(): boolean;
|
|
21
|
-
doctor?(): Promise<{
|
|
22
|
-
ok: boolean;
|
|
23
|
-
message: string;
|
|
24
|
-
details?: string[];
|
|
25
|
-
}>;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE";
|
|
29
|
-
const ACPX_BACKEND_ID = "acpx";
|
|
30
|
-
|
|
31
|
-
type AcpxRuntimeModule = typeof import("./runtime.js");
|
|
32
|
-
let runtimeModulePromise: Promise<AcpxRuntimeModule> | null = null;
|
|
33
|
-
|
|
34
|
-
type AcpxRuntimeFactoryParams = {
|
|
35
|
-
pluginConfig: ResolvedAcpxPluginConfig;
|
|
36
|
-
logger?: PluginLogger;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type CreateAcpxRuntimeServiceParams = {
|
|
40
|
-
pluginConfig?: unknown;
|
|
41
|
-
runtimeFactory?: (params: AcpxRuntimeFactoryParams) => AcpxRuntimeLike | Promise<AcpxRuntimeLike>;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function loadRuntimeModule(): Promise<AcpxRuntimeModule> {
|
|
45
|
-
runtimeModulePromise ??= import("./runtime.js");
|
|
46
|
-
return runtimeModulePromise;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function createLazyDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntimeLike {
|
|
50
|
-
let runtime: AcpxRuntimeLike | null = null;
|
|
51
|
-
let runtimePromise: Promise<AcpxRuntimeLike> | null = null;
|
|
52
|
-
|
|
53
|
-
async function resolveRuntime(): Promise<AcpxRuntimeLike> {
|
|
54
|
-
if (runtime) {
|
|
55
|
-
return runtime;
|
|
56
|
-
}
|
|
57
|
-
runtimePromise ??= loadRuntimeModule().then((module) => {
|
|
58
|
-
runtime = new module.AcpxRuntime({
|
|
59
|
-
cwd: params.pluginConfig.cwd,
|
|
60
|
-
sessionStore: module.createFileSessionStore({
|
|
61
|
-
stateDir: params.pluginConfig.stateDir,
|
|
62
|
-
}),
|
|
63
|
-
agentRegistry: module.createAgentRegistry({
|
|
64
|
-
overrides: params.pluginConfig.agents,
|
|
65
|
-
}),
|
|
66
|
-
probeAgent: params.pluginConfig.probeAgent,
|
|
67
|
-
mcpServers: toAcpMcpServers(params.pluginConfig.mcpServers),
|
|
68
|
-
permissionMode: params.pluginConfig.permissionMode,
|
|
69
|
-
nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions,
|
|
70
|
-
timeoutMs:
|
|
71
|
-
params.pluginConfig.timeoutSeconds != null
|
|
72
|
-
? params.pluginConfig.timeoutSeconds * 1_000
|
|
73
|
-
: undefined,
|
|
74
|
-
}) as AcpxRuntimeLike;
|
|
75
|
-
return runtime;
|
|
76
|
-
});
|
|
77
|
-
return await runtimePromise;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
async ensureSession(input) {
|
|
82
|
-
return await (await resolveRuntime()).ensureSession(input);
|
|
83
|
-
},
|
|
84
|
-
async *runTurn(input) {
|
|
85
|
-
yield* (await resolveRuntime()).runTurn(input);
|
|
86
|
-
},
|
|
87
|
-
async getCapabilities(input) {
|
|
88
|
-
return (await (await resolveRuntime()).getCapabilities?.(input)) ?? { controls: [] };
|
|
89
|
-
},
|
|
90
|
-
async getStatus(input) {
|
|
91
|
-
return (await (await resolveRuntime()).getStatus?.(input)) ?? {};
|
|
92
|
-
},
|
|
93
|
-
async setMode(input) {
|
|
94
|
-
await (await resolveRuntime()).setMode?.(input);
|
|
95
|
-
},
|
|
96
|
-
async setConfigOption(input) {
|
|
97
|
-
await (await resolveRuntime()).setConfigOption?.(input);
|
|
98
|
-
},
|
|
99
|
-
async doctor() {
|
|
100
|
-
return (await (await resolveRuntime()).doctor?.()) ?? { ok: true, message: "ok" };
|
|
101
|
-
},
|
|
102
|
-
async prepareFreshSession(input) {
|
|
103
|
-
await (await resolveRuntime()).prepareFreshSession?.(input);
|
|
104
|
-
},
|
|
105
|
-
async cancel(input) {
|
|
106
|
-
await (await resolveRuntime()).cancel(input);
|
|
107
|
-
},
|
|
108
|
-
async close(input) {
|
|
109
|
-
await (await resolveRuntime()).close(input);
|
|
110
|
-
},
|
|
111
|
-
async probeAvailability() {
|
|
112
|
-
await (await resolveRuntime()).probeAvailability();
|
|
113
|
-
},
|
|
114
|
-
isHealthy() {
|
|
115
|
-
return runtime?.isHealthy() ?? false;
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function warnOnIgnoredLegacyCompatibilityConfig(params: {
|
|
121
|
-
pluginConfig: ResolvedAcpxPluginConfig;
|
|
122
|
-
logger?: PluginLogger;
|
|
123
|
-
}): void {
|
|
124
|
-
const ignoredFields: string[] = [];
|
|
125
|
-
if (params.pluginConfig.legacyCompatibilityConfig.queueOwnerTtlSeconds != null) {
|
|
126
|
-
ignoredFields.push("queueOwnerTtlSeconds");
|
|
127
|
-
}
|
|
128
|
-
if (params.pluginConfig.legacyCompatibilityConfig.strictWindowsCmdWrapper === false) {
|
|
129
|
-
ignoredFields.push("strictWindowsCmdWrapper=false");
|
|
130
|
-
}
|
|
131
|
-
if (ignoredFields.length === 0) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
params.logger?.warn(
|
|
135
|
-
`embedded acpx runtime ignores legacy compatibility config: ${ignoredFields.join(", ")}`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function formatDoctorDetail(detail: unknown): string | null {
|
|
140
|
-
if (!detail) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
if (typeof detail === "string") {
|
|
144
|
-
return detail.trim() || null;
|
|
145
|
-
}
|
|
146
|
-
if (detail instanceof Error) {
|
|
147
|
-
return formatErrorMessage(detail);
|
|
148
|
-
}
|
|
149
|
-
if (typeof detail === "object") {
|
|
150
|
-
try {
|
|
151
|
-
return JSON.stringify(detail) ?? inspect(detail, { breakLength: Infinity, depth: 3 });
|
|
152
|
-
} catch {
|
|
153
|
-
return inspect(detail, { breakLength: Infinity, depth: 3 });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (
|
|
157
|
-
typeof detail === "number" ||
|
|
158
|
-
typeof detail === "boolean" ||
|
|
159
|
-
typeof detail === "bigint" ||
|
|
160
|
-
typeof detail === "symbol"
|
|
161
|
-
) {
|
|
162
|
-
return detail.toString();
|
|
163
|
-
}
|
|
164
|
-
return inspect(detail, { breakLength: Infinity, depth: 3 });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function formatDoctorFailureMessage(report: { message: string; details?: unknown[] }): string {
|
|
168
|
-
const detailText = report.details?.map(formatDoctorDetail).filter(Boolean).join("; ").trim();
|
|
169
|
-
return detailText ? `${report.message} (${detailText})` : report.message;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function normalizeProbeAgent(value: string | undefined): string | undefined {
|
|
173
|
-
const normalized = value?.trim().toLowerCase();
|
|
174
|
-
return normalized ? normalized : undefined;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function resolveAllowedAgentsProbeAgent(ctx: OpenClawPluginServiceContext): string | undefined {
|
|
178
|
-
for (const agent of ctx.config.acp?.allowedAgents ?? []) {
|
|
179
|
-
const normalized = normalizeProbeAgent(agent);
|
|
180
|
-
if (normalized) {
|
|
181
|
-
return normalized;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function shouldRunStartupProbe(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
188
|
-
return env[ENABLE_STARTUP_PROBE_ENV] === "1";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function createAcpxRuntimeService(
|
|
192
|
-
params: CreateAcpxRuntimeServiceParams = {},
|
|
193
|
-
): OpenClawPluginService {
|
|
194
|
-
let runtime: AcpxRuntimeLike | null = null;
|
|
195
|
-
let lifecycleRevision = 0;
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
id: "acpx-runtime",
|
|
199
|
-
async start(ctx: OpenClawPluginServiceContext): Promise<void> {
|
|
200
|
-
if (process.env.OPENCLAW_SKIP_ACPX_RUNTIME === "1") {
|
|
201
|
-
ctx.logger.info("skipping embedded acpx runtime backend (OPENCLAW_SKIP_ACPX_RUNTIME=1)");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const basePluginConfig = resolveAcpxPluginConfig({
|
|
206
|
-
rawConfig: params.pluginConfig,
|
|
207
|
-
workspaceDir: ctx.workspaceDir,
|
|
208
|
-
});
|
|
209
|
-
const effectiveBasePluginConfig: ResolvedAcpxPluginConfig = {
|
|
210
|
-
...basePluginConfig,
|
|
211
|
-
probeAgent: basePluginConfig.probeAgent ?? resolveAllowedAgentsProbeAgent(ctx),
|
|
212
|
-
};
|
|
213
|
-
const pluginConfig = await prepareAcpxCodexAuthConfig({
|
|
214
|
-
pluginConfig: effectiveBasePluginConfig,
|
|
215
|
-
stateDir: ctx.stateDir,
|
|
216
|
-
logger: ctx.logger,
|
|
217
|
-
});
|
|
218
|
-
await fs.mkdir(pluginConfig.stateDir, { recursive: true });
|
|
219
|
-
warnOnIgnoredLegacyCompatibilityConfig({
|
|
220
|
-
pluginConfig,
|
|
221
|
-
logger: ctx.logger,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
runtime = params.runtimeFactory
|
|
225
|
-
? await params.runtimeFactory({
|
|
226
|
-
pluginConfig,
|
|
227
|
-
logger: ctx.logger,
|
|
228
|
-
})
|
|
229
|
-
: createLazyDefaultRuntime({
|
|
230
|
-
pluginConfig,
|
|
231
|
-
logger: ctx.logger,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
registerAcpRuntimeBackend({
|
|
235
|
-
id: ACPX_BACKEND_ID,
|
|
236
|
-
runtime,
|
|
237
|
-
...(shouldRunStartupProbe() ? { healthy: () => runtime?.isHealthy() ?? false } : {}),
|
|
238
|
-
});
|
|
239
|
-
ctx.logger.info(`embedded acpx runtime backend registered (cwd: ${pluginConfig.cwd})`);
|
|
240
|
-
|
|
241
|
-
if (!shouldRunStartupProbe() || process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE === "1") {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
lifecycleRevision += 1;
|
|
246
|
-
const currentRevision = lifecycleRevision;
|
|
247
|
-
void (async () => {
|
|
248
|
-
try {
|
|
249
|
-
await runtime?.probeAvailability();
|
|
250
|
-
if (currentRevision !== lifecycleRevision) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (runtime?.isHealthy()) {
|
|
254
|
-
ctx.logger.info("embedded acpx runtime backend ready");
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
const doctorReport = await runtime?.doctor?.();
|
|
258
|
-
if (currentRevision !== lifecycleRevision) {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
ctx.logger.warn(
|
|
262
|
-
`embedded acpx runtime backend probe failed: ${doctorReport ? formatDoctorFailureMessage(doctorReport) : "backend remained unhealthy after probe"}`,
|
|
263
|
-
);
|
|
264
|
-
} catch (err) {
|
|
265
|
-
if (currentRevision !== lifecycleRevision) {
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
ctx.logger.warn(`embedded acpx runtime setup failed: ${formatErrorMessage(err)}`);
|
|
269
|
-
}
|
|
270
|
-
})();
|
|
271
|
-
},
|
|
272
|
-
async stop(_ctx: OpenClawPluginServiceContext): Promise<void> {
|
|
273
|
-
lifecycleRevision += 1;
|
|
274
|
-
unregisterAcpRuntimeBackend(ACPX_BACKEND_ID);
|
|
275
|
-
runtime = null;
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../tsconfig.package-boundary.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "."
|
|
5
|
-
},
|
|
6
|
-
"include": ["./*.ts", "./src/**/*.ts"],
|
|
7
|
-
"exclude": [
|
|
8
|
-
"./**/*.test.ts",
|
|
9
|
-
"./dist/**",
|
|
10
|
-
"./node_modules/**",
|
|
11
|
-
"./src/test-support/**",
|
|
12
|
-
"./src/**/*test-helpers.ts",
|
|
13
|
-
"./src/**/*test-harness.ts",
|
|
14
|
-
"./src/**/*test-support.ts"
|
|
15
|
-
]
|
|
16
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|