@kodelyth/acpx 2026.5.39 → 2026.5.42

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.
Files changed (47) hide show
  1. package/AGENTS.md +54 -0
  2. package/CLAUDE.md +54 -0
  3. package/dist/index.js +14 -0
  4. package/dist/process-reaper-DdVqzAA_.js +370 -0
  5. package/dist/register.runtime.js +53 -0
  6. package/dist/runtime-D9qhNKmy.js +741 -0
  7. package/dist/runtime-api.js +4 -0
  8. package/dist/service-CXeUME_-.js +1483 -0
  9. package/dist/setup-api.js +16 -0
  10. package/index.test.ts +119 -0
  11. package/index.ts +19 -0
  12. package/klaw.plugin.json +12 -27
  13. package/package.json +2 -2
  14. package/register.runtime.test.ts +104 -0
  15. package/register.runtime.ts +86 -0
  16. package/runtime-api.ts +49 -0
  17. package/setup-api.ts +18 -0
  18. package/src/acpx-runtime-compat.d.ts +65 -0
  19. package/src/claude-agent-acp-completion.test.ts +187 -0
  20. package/src/codex-auth-bridge.test.ts +688 -0
  21. package/src/codex-auth-bridge.ts +780 -0
  22. package/src/codex-trust-config.ts +297 -0
  23. package/src/config-schema.ts +118 -0
  24. package/src/config.test.ts +285 -0
  25. package/src/config.ts +281 -0
  26. package/src/manifest.test.ts +21 -0
  27. package/src/process-lease.test.ts +89 -0
  28. package/src/process-lease.ts +179 -0
  29. package/src/process-reaper.test.ts +330 -0
  30. package/src/process-reaper.ts +434 -0
  31. package/src/runtime-internals/error-format.mjs +6 -0
  32. package/src/runtime-internals/mcp-command-line.mjs +123 -0
  33. package/src/runtime-internals/mcp-command-line.test.ts +59 -0
  34. package/src/runtime-internals/mcp-proxy.mjs +121 -0
  35. package/src/runtime-internals/mcp-proxy.test.ts +130 -0
  36. package/src/runtime.test.ts +1817 -0
  37. package/src/runtime.ts +1261 -0
  38. package/src/service.test.ts +802 -0
  39. package/src/service.ts +630 -0
  40. package/tsconfig.json +16 -0
  41. package/index.js +0 -7
  42. package/register.runtime.js +0 -7
  43. package/runtime-api.js +0 -7
  44. package/setup-api.js +0 -7
  45. /package/{error-format.mjs → dist/error-format.mjs} +0 -0
  46. /package/{mcp-command-line.mjs → dist/mcp-command-line.mjs} +0 -0
  47. /package/{mcp-proxy.mjs → dist/mcp-proxy.mjs} +0 -0
@@ -0,0 +1,688 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js";
8
+ import { resolveAcpxPluginConfig } from "./config.js";
9
+ import { KLAW_ACPX_LEASE_ID_ARG, KLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const tempDirs: string[] = [];
13
+ const previousEnv = {
14
+ CODEX_HOME: process.env.CODEX_HOME,
15
+ KLAW_AGENT_DIR: process.env.KLAW_AGENT_DIR,
16
+ PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR,
17
+ };
18
+
19
+ async function makeTempDir(): Promise<string> {
20
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "klaw-acpx-codex-auth-"));
21
+ tempDirs.push(dir);
22
+ return dir;
23
+ }
24
+
25
+ function quoteArg(value: string): string {
26
+ return JSON.stringify(value);
27
+ }
28
+
29
+ function restoreEnv(name: keyof typeof previousEnv): void {
30
+ const value = previousEnv[name];
31
+ if (value === undefined) {
32
+ delete process.env[name];
33
+ } else {
34
+ process.env[name] = value;
35
+ }
36
+ }
37
+
38
+ function generatedCodexPaths(stateDir: string): {
39
+ configPath: string;
40
+ wrapperPath: string;
41
+ } {
42
+ const baseDir = path.join(stateDir, "acpx");
43
+ const codexHome = path.join(baseDir, "codex-home");
44
+ return {
45
+ configPath: path.join(codexHome, "config.toml"),
46
+ wrapperPath: path.join(baseDir, "codex-acp-wrapper.mjs"),
47
+ };
48
+ }
49
+
50
+ function generatedClaudePaths(stateDir: string): {
51
+ wrapperPath: string;
52
+ } {
53
+ const baseDir = path.join(stateDir, "acpx");
54
+ return {
55
+ wrapperPath: path.join(baseDir, "claude-agent-acp-wrapper.mjs"),
56
+ };
57
+ }
58
+
59
+ function expectCodexWrapperCommand(command: string | undefined, wrapperPath: string): void {
60
+ expect(command).toContain(quoteArg(process.execPath));
61
+ expect(command).toContain(quoteArg(wrapperPath));
62
+ }
63
+
64
+ function expectClaudeWrapperCommand(command: string | undefined, wrapperPath: string): void {
65
+ expect(command).toContain(quoteArg(process.execPath));
66
+ expect(command).toContain(quoteArg(wrapperPath));
67
+ }
68
+
69
+ function expectWrapperToContainPathSuffix(wrapper: string, pathSuffix: string[]): void {
70
+ const nativeSuffix = pathSuffix.join(path.sep);
71
+ const escapedNativeSuffix = JSON.stringify(nativeSuffix).slice(1, -1);
72
+ const posixSuffix = pathSuffix.join("/");
73
+ if (wrapper.includes(escapedNativeSuffix)) {
74
+ expect(wrapper).toContain(escapedNativeSuffix);
75
+ } else {
76
+ expect(wrapper).toContain(posixSuffix);
77
+ }
78
+ }
79
+
80
+ async function expectPathMissing(targetPath: string): Promise<void> {
81
+ let error: unknown;
82
+ try {
83
+ await fs.access(targetPath);
84
+ } catch (caught) {
85
+ error = caught;
86
+ }
87
+ expect(error).toBeInstanceOf(Error);
88
+ expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
89
+ }
90
+
91
+ afterEach(async () => {
92
+ vi.restoreAllMocks();
93
+ restoreEnv("CODEX_HOME");
94
+ restoreEnv("KLAW_AGENT_DIR");
95
+ restoreEnv("PI_CODING_AGENT_DIR");
96
+ for (const dir of tempDirs.splice(0)) {
97
+ await fs.rm(dir, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ describe("prepareAcpxCodexAuthConfig", () => {
102
+ it("installs an isolated Codex ACP wrapper without synthesizing auth from canonical Klaw OAuth", async () => {
103
+ const root = await makeTempDir();
104
+ const agentDir = path.join(root, "agent");
105
+ const stateDir = path.join(root, "state");
106
+ const generated = generatedCodexPaths(stateDir);
107
+ const generatedClaude = generatedClaudePaths(stateDir);
108
+ const installedBinPath = path.join(
109
+ root,
110
+ "node_modules",
111
+ "@zed-industries",
112
+ "codex-acp",
113
+ "bin",
114
+ "codex-acp.js",
115
+ );
116
+ process.env.KLAW_AGENT_DIR = agentDir;
117
+ delete process.env.PI_CODING_AGENT_DIR;
118
+
119
+ const pluginConfig = resolveAcpxPluginConfig({
120
+ rawConfig: {},
121
+ workspaceDir: root,
122
+ });
123
+ const resolved = await prepareAcpxCodexAuthConfig({
124
+ pluginConfig,
125
+ stateDir,
126
+ resolveInstalledCodexAcpBinPath: async () => installedBinPath,
127
+ });
128
+
129
+ expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
130
+ expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath);
131
+ await expect(fs.access(generated.wrapperPath)).resolves.toBeUndefined();
132
+ await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined();
133
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
134
+ expect(wrapper).toContain(JSON.stringify(installedBinPath));
135
+ expect(wrapper).toContain("defaultArgs = [installedBinPath]");
136
+ await expectPathMissing(path.join(agentDir, "acp-auth", "codex", "auth.json"));
137
+ });
138
+
139
+ it("keeps generated wrappers usable when chmod is rejected by the state filesystem", async () => {
140
+ const root = await makeTempDir();
141
+ const stateDir = path.join(root, "state");
142
+ const generatedCodex = generatedCodexPaths(stateDir);
143
+ const generatedClaude = generatedClaudePaths(stateDir);
144
+ const chmodError = Object.assign(new Error("operation not permitted"), { code: "EPERM" });
145
+ const chmodSpy = vi.spyOn(fs, "chmod").mockRejectedValue(chmodError);
146
+ const pluginConfig = resolveAcpxPluginConfig({
147
+ rawConfig: {},
148
+ workspaceDir: root,
149
+ });
150
+
151
+ const resolved = await prepareAcpxCodexAuthConfig({
152
+ pluginConfig,
153
+ stateDir,
154
+ });
155
+
156
+ expect(chmodSpy).toHaveBeenCalledWith(generatedCodex.wrapperPath, 0o755);
157
+ expect(chmodSpy).toHaveBeenCalledWith(generatedClaude.wrapperPath, 0o755);
158
+ expectCodexWrapperCommand(resolved.agents.codex, generatedCodex.wrapperPath);
159
+ expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath);
160
+ await expect(fs.access(generatedCodex.wrapperPath)).resolves.toBeUndefined();
161
+ await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined();
162
+ });
163
+
164
+ it("falls back to the current Codex ACP package range when the local adapter is unavailable", async () => {
165
+ const root = await makeTempDir();
166
+ const stateDir = path.join(root, "state");
167
+ const generated = generatedCodexPaths(stateDir);
168
+ const pluginConfig = resolveAcpxPluginConfig({
169
+ rawConfig: {},
170
+ workspaceDir: root,
171
+ });
172
+
173
+ await prepareAcpxCodexAuthConfig({
174
+ pluginConfig,
175
+ stateDir,
176
+ resolveInstalledCodexAcpBinPath: async () => undefined,
177
+ });
178
+
179
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
180
+ expect(wrapper).toContain('"@zed-industries/codex-acp@0.14.0"');
181
+ expect(wrapper).toContain('"--", "codex-acp"');
182
+ expect(wrapper).not.toContain("@zed-industries/codex-acp@^0.11.1");
183
+ });
184
+
185
+ it("falls back to the patched Claude ACP package when the local adapter is unavailable", async () => {
186
+ const root = await makeTempDir();
187
+ const stateDir = path.join(root, "state");
188
+ const generated = generatedClaudePaths(stateDir);
189
+ const pluginConfig = resolveAcpxPluginConfig({
190
+ rawConfig: {},
191
+ workspaceDir: root,
192
+ });
193
+
194
+ await prepareAcpxCodexAuthConfig({
195
+ pluginConfig,
196
+ stateDir,
197
+ resolveInstalledClaudeAcpBinPath: async () => undefined,
198
+ });
199
+
200
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
201
+ expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.33.1"');
202
+ expect(wrapper).toContain('"--", "claude-agent-acp"');
203
+ expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
204
+ expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");
205
+ });
206
+
207
+ it("uses the bundled Codex ACP dependency by default when it is installed", async () => {
208
+ const root = await makeTempDir();
209
+ const stateDir = path.join(root, "state");
210
+ const generated = generatedCodexPaths(stateDir);
211
+ const pluginConfig = resolveAcpxPluginConfig({
212
+ rawConfig: {},
213
+ workspaceDir: root,
214
+ });
215
+
216
+ await prepareAcpxCodexAuthConfig({
217
+ pluginConfig,
218
+ stateDir,
219
+ });
220
+
221
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
222
+ expect(wrapper).toContain("@zed-industries/codex-acp");
223
+ expectWrapperToContainPathSuffix(wrapper, ["bin", "codex-acp.js"]);
224
+ expect(wrapper).toContain("defaultArgs = [installedBinPath]");
225
+ });
226
+
227
+ it("keeps the orphaned wrapper alive long enough to force-kill the child process group", async () => {
228
+ const root = await makeTempDir();
229
+ const stateDir = path.join(root, "state");
230
+ const generated = generatedCodexPaths(stateDir);
231
+ const pluginConfig = resolveAcpxPluginConfig({
232
+ rawConfig: {},
233
+ workspaceDir: root,
234
+ });
235
+
236
+ await prepareAcpxCodexAuthConfig({
237
+ pluginConfig,
238
+ stateDir,
239
+ });
240
+
241
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
242
+ expect(wrapper).toContain('killChildTree("SIGTERM")');
243
+ expect(wrapper).toContain('killChildTree("SIGKILL", { force: true })');
244
+ expect(wrapper).toMatch(
245
+ /forceKillTimer = setTimeout\(\(\) => \{\s*killChildTree\("SIGKILL", \{ force: true \}\);\s*childExitCode = 1;/s,
246
+ );
247
+ expect(wrapper).toMatch(
248
+ /child\.on\("exit", \(code, signal\) => \{\s*if \(parentWatcher\) \{\s*clearInterval\(parentWatcher\);\s*\}\s*if \(orphanCleanupStarted\) \{\s*return;\s*\}/s,
249
+ );
250
+ expect(wrapper).toMatch(
251
+ /child\.on\("close", \(\) => \{\s*finishStderrLog\(\);\s*process\.exit\(childExitCode\);/s,
252
+ );
253
+ expect(wrapper).not.toMatch(
254
+ /forceKillTimer = setTimeout\(\(\) => killChildTree\("SIGKILL"\), 1_500\);\s*forceKillTimer\.unref\?\.\(\);\s*process\.exit\(1\);/s,
255
+ );
256
+ });
257
+
258
+ it("uses the bundled Claude ACP dependency by default when it is installed", async () => {
259
+ const root = await makeTempDir();
260
+ const stateDir = path.join(root, "state");
261
+ const generated = generatedClaudePaths(stateDir);
262
+ const pluginConfig = resolveAcpxPluginConfig({
263
+ rawConfig: {},
264
+ workspaceDir: root,
265
+ });
266
+
267
+ await prepareAcpxCodexAuthConfig({
268
+ pluginConfig,
269
+ stateDir,
270
+ });
271
+
272
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
273
+ expect(wrapper).toContain("@agentclientprotocol/claude-agent-acp");
274
+ expectWrapperToContainPathSuffix(wrapper, ["dist", "index.js"]);
275
+ expect(wrapper).toContain("defaultArgs = [installedBinPath]");
276
+ });
277
+
278
+ it("launches the locally installed Codex ACP bin with isolated CODEX_HOME", async () => {
279
+ const root = await makeTempDir();
280
+ const stateDir = path.join(root, "state");
281
+ const generated = generatedCodexPaths(stateDir);
282
+ const installedBinPath = path.join(root, "codex-acp-bin.js");
283
+ await fs.writeFile(
284
+ installedBinPath,
285
+ "console.log(JSON.stringify({ argv: process.argv.slice(2), codexHome: process.env.CODEX_HOME }));\n",
286
+ "utf8",
287
+ );
288
+ const pluginConfig = resolveAcpxPluginConfig({
289
+ rawConfig: {},
290
+ workspaceDir: root,
291
+ });
292
+
293
+ await prepareAcpxCodexAuthConfig({
294
+ pluginConfig,
295
+ stateDir,
296
+ resolveInstalledCodexAcpBinPath: async () => installedBinPath,
297
+ });
298
+
299
+ const { stdout } = await execFileAsync(
300
+ process.execPath,
301
+ [
302
+ generated.wrapperPath,
303
+ "--klaw-acpx-lease-id",
304
+ "lease-1",
305
+ "--klaw-gateway-instance-id",
306
+ "gateway-1",
307
+ ],
308
+ {
309
+ cwd: root,
310
+ },
311
+ );
312
+ const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown };
313
+ expect(launched.argv).toStrictEqual([]);
314
+ const expectedCodexHome = await fs.realpath(path.join(stateDir, "acpx", "codex-home"));
315
+ expect(path.resolve(String(launched.codexHome))).toBe(expectedCodexHome);
316
+ });
317
+
318
+ it("launches the locally installed Claude ACP bin without going through npm", async () => {
319
+ const root = await makeTempDir();
320
+ const stateDir = path.join(root, "state");
321
+ const generated = generatedClaudePaths(stateDir);
322
+ const installedBinPath = path.join(root, "claude-agent-acp-bin.js");
323
+ await fs.writeFile(
324
+ installedBinPath,
325
+ "console.log(JSON.stringify({ argv: process.argv.slice(2), codexHome: process.env.CODEX_HOME ?? null }));\n",
326
+ "utf8",
327
+ );
328
+ const pluginConfig = resolveAcpxPluginConfig({
329
+ rawConfig: {},
330
+ workspaceDir: root,
331
+ });
332
+
333
+ await prepareAcpxCodexAuthConfig({
334
+ pluginConfig,
335
+ stateDir,
336
+ resolveInstalledClaudeAcpBinPath: async () => installedBinPath,
337
+ });
338
+
339
+ const { stdout } = await execFileAsync(
340
+ process.execPath,
341
+ [generated.wrapperPath, "--permission-mode", "bypass"],
342
+ {
343
+ cwd: root,
344
+ },
345
+ );
346
+ const launched = JSON.parse(stdout.trim()) as { argv?: unknown; codexHome?: unknown };
347
+ expect(launched.argv).toEqual(["--permission-mode", "bypass"]);
348
+ expect(launched.codexHome).toBeNull();
349
+ });
350
+
351
+ it("does not copy source Codex auth", async () => {
352
+ const root = await makeTempDir();
353
+ const sourceCodexHome = path.join(root, "source-codex");
354
+ const agentDir = path.join(root, "agent");
355
+ const stateDir = path.join(root, "state");
356
+ const generated = generatedCodexPaths(stateDir);
357
+ await fs.mkdir(sourceCodexHome, { recursive: true });
358
+ await fs.writeFile(
359
+ path.join(sourceCodexHome, "auth.json"),
360
+ `${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: "test-api-key" }, null, 2)}\n`,
361
+ );
362
+ await fs.writeFile(
363
+ path.join(sourceCodexHome, "config.toml"),
364
+ [
365
+ 'model = "gpt-5.5-1"',
366
+ 'model_provider = "azure_foundry"',
367
+ 'model_reasoning_effort = "high"',
368
+ 'sandbox_mode = "workspace-write"',
369
+ 'notify = ["SkyComputerUseClient", "turn-ended"]',
370
+ "",
371
+ "[model_providers.azure_foundry]",
372
+ 'name = "Azure Foundry"',
373
+ 'base_url = "https://example.azure.com/openai/v1"',
374
+ 'wire_api = "responses"',
375
+ 'env_key = "AZURE_OPENAI_API_KEY"',
376
+ 'http_headers = { "api-key" = "inline-secret-key" }',
377
+ 'query_params = { "api-version" = "2026-01-01", "secret" = "inline-secret-param" }',
378
+ 'experimental_bearer_token = "inline-secret-bearer"',
379
+ "",
380
+ "[model_providers.azure_foundry.auth]",
381
+ 'command = "bash"',
382
+ 'args = ["-lc", "printf %s test-key"]',
383
+ "",
384
+ "[model_providers.secret_only]",
385
+ 'experimental_bearer_token = "secret-only-token"',
386
+ "",
387
+ `[projects.${JSON.stringify(path.join(root, "project-with-model-key"))}]`,
388
+ 'model = "nested-project-model"',
389
+ "",
390
+ ].join("\n"),
391
+ );
392
+ process.env.CODEX_HOME = sourceCodexHome;
393
+ process.env.KLAW_AGENT_DIR = agentDir;
394
+ delete process.env.PI_CODING_AGENT_DIR;
395
+
396
+ const pluginConfig = resolveAcpxPluginConfig({
397
+ rawConfig: {},
398
+ workspaceDir: root,
399
+ });
400
+ const resolved = await prepareAcpxCodexAuthConfig({
401
+ pluginConfig,
402
+ stateDir,
403
+ resolveInstalledCodexAcpBinPath: async () => undefined,
404
+ });
405
+
406
+ expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
407
+ const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
408
+ expect(isolatedConfig).toContain('model = "gpt-5.5-1"');
409
+ expect(isolatedConfig).toContain('model_provider = "azure_foundry"');
410
+ expect(isolatedConfig).toContain('model_reasoning_effort = "high"');
411
+ expect(isolatedConfig).toContain('sandbox_mode = "workspace-write"');
412
+ expect(isolatedConfig).toContain("[model_providers.azure_foundry]");
413
+ expect(isolatedConfig).toContain('base_url = "https://example.azure.com/openai/v1"');
414
+ expect(isolatedConfig).toContain('env_key = "AZURE_OPENAI_API_KEY"');
415
+ expect(isolatedConfig).not.toContain("http_headers");
416
+ expect(isolatedConfig).not.toContain("query_params");
417
+ expect(isolatedConfig).not.toContain("experimental_bearer_token");
418
+ expect(isolatedConfig).not.toContain("[model_providers.azure_foundry.auth]");
419
+ expect(isolatedConfig).not.toContain("[model_providers.secret_only]");
420
+ expect(isolatedConfig).not.toContain("nested-project-model");
421
+ expect(isolatedConfig).not.toContain("inline-secret");
422
+ expect(isolatedConfig).not.toContain('args = ["-lc", "printf %s test-key"]');
423
+ expect(isolatedConfig).not.toContain("notify");
424
+ expect(isolatedConfig).not.toContain("SkyComputerUseClient");
425
+ expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`);
426
+ expect(isolatedConfig).toContain('trust_level = "trusted"');
427
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
428
+ expect(wrapper).toContain("CODEX_HOME: codexHome");
429
+ expect(wrapper).not.toContain(sourceCodexHome);
430
+ await expectPathMissing(path.join(agentDir, "acp-auth", "codex-source", "auth.json"));
431
+ await expectPathMissing(path.join(agentDir, "acp-auth", "codex", "auth.json"));
432
+ });
433
+
434
+ it("copies only trusted Codex project declarations into the isolated Codex home", async () => {
435
+ const root = await makeTempDir();
436
+ const sourceCodexHome = path.join(root, "source-codex");
437
+ const stateDir = path.join(root, "state");
438
+ const explicitProject = path.join(root, "explicit project");
439
+ const inlineProject = path.join(root, "inline-project");
440
+ const mapProject = path.join(root, "map-project");
441
+ const untrustedProject = path.join(root, "untrusted-project");
442
+ const generated = generatedCodexPaths(stateDir);
443
+ await fs.mkdir(sourceCodexHome, { recursive: true });
444
+ await fs.writeFile(
445
+ path.join(sourceCodexHome, "config.toml"),
446
+ [
447
+ 'notify = ["SkyComputerUseClient", "turn-ended"]',
448
+ `projects = { ${JSON.stringify(mapProject)} = { trust_level = "trusted" }, ${JSON.stringify(untrustedProject)} = { trust_level = "untrusted" } }`,
449
+ "[projects]",
450
+ `${JSON.stringify(inlineProject)} = { trust_level = "trusted" }`,
451
+ `[projects.${JSON.stringify(explicitProject)}]`,
452
+ 'trust_level = "trusted"',
453
+ "",
454
+ ].join("\n"),
455
+ );
456
+ process.env.CODEX_HOME = sourceCodexHome;
457
+ const pluginConfig = resolveAcpxPluginConfig({
458
+ rawConfig: {},
459
+ workspaceDir: root,
460
+ });
461
+
462
+ await prepareAcpxCodexAuthConfig({
463
+ pluginConfig,
464
+ stateDir,
465
+ resolveInstalledCodexAcpBinPath: async () => undefined,
466
+ });
467
+
468
+ const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
469
+ expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`);
470
+ expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(explicitProject))}]`);
471
+ expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(inlineProject))}]`);
472
+ expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(mapProject))}]`);
473
+ expect(isolatedConfig).not.toContain(untrustedProject);
474
+ expect(isolatedConfig).not.toContain("notify");
475
+ expect(isolatedConfig).not.toContain("SkyComputerUseClient");
476
+ });
477
+
478
+ it("normalizes an explicitly configured Codex ACP command to the local wrapper", async () => {
479
+ const root = await makeTempDir();
480
+ const sourceCodexHome = path.join(root, "source-codex");
481
+ const stateDir = path.join(root, "state");
482
+ const generated = generatedCodexPaths(stateDir);
483
+ await fs.mkdir(sourceCodexHome, { recursive: true });
484
+ await fs.writeFile(
485
+ path.join(sourceCodexHome, "config.toml"),
486
+ 'notify = ["SkyComputerUseClient", "turn-ended"]\n',
487
+ );
488
+ process.env.CODEX_HOME = sourceCodexHome;
489
+ const pluginConfig = resolveAcpxPluginConfig({
490
+ rawConfig: {
491
+ agents: {
492
+ codex: {
493
+ command: "npx @zed-industries/codex-acp@0.12.0 -c 'model=\"gpt-5.4\"'",
494
+ },
495
+ },
496
+ },
497
+ workspaceDir: root,
498
+ });
499
+
500
+ const resolved = await prepareAcpxCodexAuthConfig({
501
+ pluginConfig,
502
+ stateDir,
503
+ resolveInstalledCodexAcpBinPath: async () => path.join(root, "codex-acp.js"),
504
+ });
505
+
506
+ expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
507
+ expect(resolved.agents.codex).not.toContain("npx @zed-industries/codex-acp@0.12.0");
508
+ expect(resolved.agents.codex).toContain(quoteArg("-c"));
509
+ expect(resolved.agents.codex).toContain(quoteArg('model="gpt-5.4"'));
510
+ const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
511
+ expect(isolatedConfig).not.toContain("notify");
512
+ expect(isolatedConfig).not.toContain("SkyComputerUseClient");
513
+ const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
514
+ expect(wrapper).toContain("process.argv.slice(2)");
515
+ expect(wrapper).toContain("CODEX_HOME: codexHome");
516
+ expect(wrapper).not.toContain(sourceCodexHome);
517
+ });
518
+
519
+ it("normalizes an explicitly configured Claude ACP npx command to the local wrapper", async () => {
520
+ const root = await makeTempDir();
521
+ const stateDir = path.join(root, "state");
522
+ const generated = generatedClaudePaths(stateDir);
523
+ const pluginConfig = resolveAcpxPluginConfig({
524
+ rawConfig: {
525
+ agents: {
526
+ claude: {
527
+ command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.4 --permission-mode bypass",
528
+ },
529
+ },
530
+ },
531
+ workspaceDir: root,
532
+ });
533
+
534
+ const resolved = await prepareAcpxCodexAuthConfig({
535
+ pluginConfig,
536
+ stateDir,
537
+ resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
538
+ });
539
+
540
+ expectClaudeWrapperCommand(resolved.agents.claude, generated.wrapperPath);
541
+ expect(resolved.agents.claude).not.toContain("npx -y @agentclientprotocol/claude-agent-acp");
542
+ expect(resolved.agents.claude).toContain("--permission-mode");
543
+ expect(resolved.agents.claude).toContain("bypass");
544
+ });
545
+
546
+ it("captures Codex wrapper stderr in a stream-aware redacted per-lease log", async () => {
547
+ const root = await makeTempDir();
548
+ const stateDir = path.join(root, "state");
549
+ const generated = generatedCodexPaths(stateDir);
550
+ const stderrScript = path.join(root, "emit-stderr.mjs");
551
+ await fs.writeFile(
552
+ stderrScript,
553
+ `const chunks = [
554
+ "token=sk-test",
555
+ "secret1234567890\\n",
556
+ "Authorization: Bearer bearer-secret",
557
+ "-token-1234567890\\n",
558
+ '{"client_secret":"json-secret-1234567890","api_key":"json-api-key-1234567890"}\\n',
559
+ "client-secret: kebab-secret-1234567890\\n",
560
+ "standalone sk-live-secret",
561
+ "1234567890\\n",
562
+ "url=https://example.test/callback?token=query-secret",
563
+ "-1234567890\\n",
564
+ "github_pat_1234567890",
565
+ "abcdefghijklmnopqrstuvwxyz\\n",
566
+ "-----BEGIN PRIVATE KEY-----\\nprivate-secret-body\\n",
567
+ "-----END PRIVATE KEY-----\\n",
568
+ "tail-token=tail-secret-1234567890",
569
+ "\\n-----BEGIN PRIVATE KEY-----\\ntruncated-private-secret",
570
+ ];
571
+ let index = 0;
572
+ function writeNext() {
573
+ if (index >= chunks.length) {
574
+ process.exit(1);
575
+ return;
576
+ }
577
+ process.stderr.write(chunks[index]);
578
+ index += 1;
579
+ setTimeout(writeNext, 5);
580
+ }
581
+ writeNext();`,
582
+ "utf8",
583
+ );
584
+ const pluginConfig = resolveAcpxPluginConfig({
585
+ rawConfig: {
586
+ agents: {
587
+ codex: {
588
+ command: `${process.execPath} ${stderrScript}`,
589
+ },
590
+ },
591
+ },
592
+ workspaceDir: root,
593
+ });
594
+
595
+ await prepareAcpxCodexAuthConfig({
596
+ pluginConfig,
597
+ stateDir,
598
+ resolveInstalledCodexAcpBinPath: async () => path.join(root, "codex-acp.js"),
599
+ });
600
+
601
+ await expect(
602
+ execFileAsync(process.execPath, [
603
+ generated.wrapperPath,
604
+ "--klaw-run-configured",
605
+ process.execPath,
606
+ stderrScript,
607
+ KLAW_ACPX_LEASE_ID_ARG,
608
+ "lease-secret",
609
+ KLAW_GATEWAY_INSTANCE_ID_ARG,
610
+ "gateway-test",
611
+ ]),
612
+ ).rejects.toMatchObject({ code: 1 });
613
+
614
+ const log = await fs.readFile(
615
+ path.join(stateDir, "acpx", "codex-acp-wrapper.stderr.lease-secret.log"),
616
+ "utf8",
617
+ );
618
+ expect(log).toContain("token=[REDACTED]");
619
+ expect(log).toContain("Authorization: Bearer [REDACTED]");
620
+ expect(log).toContain('"client_secret":"[REDACTED]"');
621
+ expect(log).toContain('"api_key":"[REDACTED]"');
622
+ expect(log).toContain("client-secret: [REDACTED]");
623
+ expect(log).toContain("standalone [REDACTED_OPENAI_KEY]");
624
+ expect(log).toContain("?token=[REDACTED]");
625
+ expect(log).toContain("[REDACTED_GITHUB_TOKEN]");
626
+ expect(log).toContain("[REDACTED_PRIVATE_KEY]");
627
+ expect(log).toContain("tail-token=[REDACTED]");
628
+ expect(log).not.toContain("sk-testsecret1234567890");
629
+ expect(log).not.toContain("bearer-secret-token-1234567890");
630
+ expect(log).not.toContain("json-secret-1234567890");
631
+ expect(log).not.toContain("json-api-key-1234567890");
632
+ expect(log).not.toContain("kebab-secret-1234567890");
633
+ expect(log).not.toContain("query-secret-1234567890");
634
+ expect(log).not.toContain("github_pat_1234567890abcdefghijklmnopqrstuvwxyz");
635
+ expect(log).not.toContain("private-secret-body");
636
+ expect(log).not.toContain("truncated-private-secret");
637
+ expect(log).not.toContain("tail-secret-1234567890");
638
+ await expectPathMissing(path.join(stateDir, "acpx", "codex-acp-wrapper.stderr.log"));
639
+ });
640
+
641
+ it("leaves a custom Claude agent command alone", async () => {
642
+ const root = await makeTempDir();
643
+ const stateDir = path.join(root, "state");
644
+ const pluginConfig = resolveAcpxPluginConfig({
645
+ rawConfig: {
646
+ agents: {
647
+ claude: {
648
+ command: "node ./custom-claude-wrapper.mjs --flag",
649
+ },
650
+ },
651
+ },
652
+ workspaceDir: root,
653
+ });
654
+
655
+ const resolved = await prepareAcpxCodexAuthConfig({
656
+ pluginConfig,
657
+ stateDir,
658
+ resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
659
+ });
660
+
661
+ expect(resolved.agents.claude).toBe("node ./custom-claude-wrapper.mjs --flag");
662
+ });
663
+
664
+ it("does not normalize custom Claude commands that only mention the package name", async () => {
665
+ const root = await makeTempDir();
666
+ const stateDir = path.join(root, "state");
667
+ const command =
668
+ "node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.4 --flag";
669
+ const pluginConfig = resolveAcpxPluginConfig({
670
+ rawConfig: {
671
+ agents: {
672
+ claude: {
673
+ command,
674
+ },
675
+ },
676
+ },
677
+ workspaceDir: root,
678
+ });
679
+
680
+ const resolved = await prepareAcpxCodexAuthConfig({
681
+ pluginConfig,
682
+ stateDir,
683
+ resolveInstalledClaudeAcpBinPath: async () => path.join(root, "claude-agent-acp.js"),
684
+ });
685
+
686
+ expect(resolved.agents.claude).toBe(command);
687
+ });
688
+ });