@kodelyth/codex 2026.5.42 → 2026.6.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.
Files changed (138) hide show
  1. package/package.json +17 -2
  2. package/doctor-contract-api.test.ts +0 -44
  3. package/doctor-contract-api.ts +0 -68
  4. package/harness.ts +0 -72
  5. package/index.test.ts +0 -230
  6. package/index.ts +0 -66
  7. package/media-understanding-provider.test.ts +0 -486
  8. package/media-understanding-provider.ts +0 -521
  9. package/prompt-overlay-runtime-contract.test.ts +0 -48
  10. package/prompt-overlay.ts +0 -21
  11. package/provider-catalog.ts +0 -83
  12. package/provider-discovery.ts +0 -45
  13. package/provider.test.ts +0 -384
  14. package/provider.ts +0 -243
  15. package/src/app-server/app-inventory-cache.test.ts +0 -176
  16. package/src/app-server/app-inventory-cache.ts +0 -324
  17. package/src/app-server/approval-bridge.test.ts +0 -1471
  18. package/src/app-server/approval-bridge.ts +0 -1211
  19. package/src/app-server/auth-bridge.test.ts +0 -1449
  20. package/src/app-server/auth-bridge.ts +0 -614
  21. package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
  22. package/src/app-server/capabilities.ts +0 -27
  23. package/src/app-server/client-factory.ts +0 -24
  24. package/src/app-server/client.test.ts +0 -563
  25. package/src/app-server/client.ts +0 -715
  26. package/src/app-server/compact.test.ts +0 -710
  27. package/src/app-server/compact.ts +0 -500
  28. package/src/app-server/computer-use.test.ts +0 -788
  29. package/src/app-server/computer-use.ts +0 -683
  30. package/src/app-server/config.test.ts +0 -879
  31. package/src/app-server/config.ts +0 -1038
  32. package/src/app-server/context-engine-projection.test.ts +0 -252
  33. package/src/app-server/context-engine-projection.ts +0 -403
  34. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
  35. package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
  36. package/src/app-server/dynamic-tool-profile.ts +0 -69
  37. package/src/app-server/dynamic-tools.test.ts +0 -1302
  38. package/src/app-server/dynamic-tools.ts +0 -623
  39. package/src/app-server/elicitation-bridge.test.ts +0 -1056
  40. package/src/app-server/elicitation-bridge.ts +0 -783
  41. package/src/app-server/event-projector.test.ts +0 -2668
  42. package/src/app-server/event-projector.ts +0 -2057
  43. package/src/app-server/image-payload-sanitizer.test.ts +0 -49
  44. package/src/app-server/image-payload-sanitizer.ts +0 -167
  45. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
  46. package/src/app-server/local-runtime-attribution.ts +0 -39
  47. package/src/app-server/managed-binary.test.ts +0 -139
  48. package/src/app-server/managed-binary.ts +0 -193
  49. package/src/app-server/models.test.ts +0 -246
  50. package/src/app-server/models.ts +0 -172
  51. package/src/app-server/native-hook-relay.test.ts +0 -271
  52. package/src/app-server/native-hook-relay.ts +0 -150
  53. package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
  54. package/src/app-server/native-subagent-task-mirror.ts +0 -497
  55. package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
  56. package/src/app-server/plugin-activation.test.ts +0 -336
  57. package/src/app-server/plugin-activation.ts +0 -283
  58. package/src/app-server/plugin-app-cache-key.ts +0 -74
  59. package/src/app-server/plugin-approval-roundtrip.ts +0 -122
  60. package/src/app-server/plugin-inventory.test.ts +0 -355
  61. package/src/app-server/plugin-inventory.ts +0 -357
  62. package/src/app-server/plugin-thread-config.test.ts +0 -865
  63. package/src/app-server/plugin-thread-config.ts +0 -455
  64. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
  65. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
  66. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
  67. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
  68. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
  69. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
  70. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
  71. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
  72. package/src/app-server/protocol-validators.test.ts +0 -75
  73. package/src/app-server/protocol-validators.ts +0 -203
  74. package/src/app-server/protocol.ts +0 -520
  75. package/src/app-server/rate-limit-cache.ts +0 -48
  76. package/src/app-server/rate-limits.test.ts +0 -202
  77. package/src/app-server/rate-limits.ts +0 -583
  78. package/src/app-server/request.ts +0 -73
  79. package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
  80. package/src/app-server/run-attempt.test.ts +0 -9477
  81. package/src/app-server/run-attempt.ts +0 -4683
  82. package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
  83. package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
  84. package/src/app-server/session-binding.test.ts +0 -303
  85. package/src/app-server/session-binding.ts +0 -398
  86. package/src/app-server/session-history.ts +0 -44
  87. package/src/app-server/shared-client.test.ts +0 -589
  88. package/src/app-server/shared-client.ts +0 -289
  89. package/src/app-server/side-question.test.ts +0 -1175
  90. package/src/app-server/side-question.ts +0 -1007
  91. package/src/app-server/test-support.ts +0 -48
  92. package/src/app-server/thread-lifecycle.test.ts +0 -447
  93. package/src/app-server/thread-lifecycle.ts +0 -939
  94. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
  95. package/src/app-server/timeout.ts +0 -9
  96. package/src/app-server/tool-progress-normalization.ts +0 -77
  97. package/src/app-server/trajectory.test.ts +0 -205
  98. package/src/app-server/trajectory.ts +0 -365
  99. package/src/app-server/transcript-mirror.test.ts +0 -524
  100. package/src/app-server/transcript-mirror.ts +0 -208
  101. package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
  102. package/src/app-server/transport-stdio.test.ts +0 -171
  103. package/src/app-server/transport-stdio.ts +0 -107
  104. package/src/app-server/transport-websocket.test.ts +0 -69
  105. package/src/app-server/transport-websocket.ts +0 -90
  106. package/src/app-server/transport.ts +0 -117
  107. package/src/app-server/user-input-bridge.test.ts +0 -249
  108. package/src/app-server/user-input-bridge.ts +0 -316
  109. package/src/app-server/version.ts +0 -4
  110. package/src/app-server/vision-tools.ts +0 -12
  111. package/src/command-account.ts +0 -544
  112. package/src/command-formatters.ts +0 -425
  113. package/src/command-handlers.ts +0 -2004
  114. package/src/command-rpc.test.ts +0 -16
  115. package/src/command-rpc.ts +0 -142
  116. package/src/commands.test.ts +0 -3312
  117. package/src/commands.ts +0 -65
  118. package/src/conversation-binding-data.ts +0 -124
  119. package/src/conversation-binding.test.ts +0 -599
  120. package/src/conversation-binding.ts +0 -561
  121. package/src/conversation-control.test.ts +0 -126
  122. package/src/conversation-control.ts +0 -303
  123. package/src/conversation-turn-collector.test.ts +0 -191
  124. package/src/conversation-turn-collector.ts +0 -186
  125. package/src/conversation-turn-input.test.ts +0 -141
  126. package/src/conversation-turn-input.ts +0 -106
  127. package/src/manifest.test.ts +0 -20
  128. package/src/migration/apply.ts +0 -501
  129. package/src/migration/helpers.ts +0 -55
  130. package/src/migration/plan.ts +0 -461
  131. package/src/migration/provider.test.ts +0 -1741
  132. package/src/migration/provider.ts +0 -41
  133. package/src/migration/source.ts +0 -643
  134. package/src/migration/targets.ts +0 -25
  135. package/src/node-cli-sessions.test.ts +0 -180
  136. package/src/node-cli-sessions.ts +0 -711
  137. package/test-api.ts +0 -82
  138. package/tsconfig.json +0 -16
@@ -1,589 +0,0 @@
1
- import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
2
- import { WebSocketServer, type RawData } from "ws";
3
- import { CodexAppServerClient, MIN_CODEX_APP_SERVER_VERSION } from "./client.js";
4
- import { codexAppServerStartOptionsKey } from "./config.js";
5
- import { createClientHarness } from "./test-support.js";
6
-
7
- const mocks = vi.hoisted(() => ({
8
- bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions),
9
- applyCodexAppServerAuthProfile: vi.fn(
10
- async (_params?: { agentDir?: string; authProfileId?: string; config?: unknown }) => undefined,
11
- ),
12
- resolveCodexAppServerAuthProfileIdForAgent: vi.fn(
13
- (params?: { authProfileId?: string }) => params?.authProfileId,
14
- ),
15
- resolveManagedCodexAppServerStartOptions: vi.fn(async (startOptions) => startOptions),
16
- embeddedAgentLog: { debug: vi.fn(), warn: vi.fn() },
17
- resolveDefaultAgentDir: vi.fn(() => "/tmp/klaw-agent"),
18
- }));
19
-
20
- vi.mock("./auth-bridge.js", () => ({
21
- applyCodexAppServerAuthProfile: mocks.applyCodexAppServerAuthProfile,
22
- bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions,
23
- resolveCodexAppServerAuthProfileIdForAgent: mocks.resolveCodexAppServerAuthProfileIdForAgent,
24
- }));
25
-
26
- vi.mock("./managed-binary.js", () => ({
27
- resolveManagedCodexAppServerStartOptions: mocks.resolveManagedCodexAppServerStartOptions,
28
- }));
29
-
30
- vi.mock("klaw/plugin-sdk/agent-harness-runtime", () => ({
31
- embeddedAgentLog: mocks.embeddedAgentLog,
32
- KLAW_VERSION: "test",
33
- }));
34
-
35
- vi.mock("klaw/plugin-sdk/agent-runtime", () => ({
36
- resolveDefaultAgentDir: mocks.resolveDefaultAgentDir,
37
- }));
38
-
39
- let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
40
- let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
41
- let clearSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrent;
42
- let clearSharedCodexAppServerClientIfCurrentAndWait: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrentAndWait;
43
- let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient;
44
- let getSharedCodexAppServerClient: typeof import("./shared-client.js").getSharedCodexAppServerClient;
45
- let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
46
-
47
- async function sendInitializeResult(
48
- harness: ReturnType<typeof createClientHarness>,
49
- userAgent: string,
50
- ): Promise<void> {
51
- await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1));
52
- const initialize = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
53
- harness.send({ id: initialize.id, result: { userAgent } });
54
- }
55
-
56
- async function sendEmptyModelList(harness: ReturnType<typeof createClientHarness>): Promise<void> {
57
- await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(3));
58
- const modelList = JSON.parse(harness.writes[2] ?? "{}") as { id?: number };
59
- harness.send({ id: modelList.id, result: { data: [] } });
60
- }
61
-
62
- function firstMockArg(mock: unknown, label: string): unknown {
63
- const call = (mock as { mock?: { calls?: unknown[][] } }).mock?.calls?.at(0);
64
- if (!call) {
65
- throw new Error(`Expected ${label} first call`);
66
- }
67
- return call[0];
68
- }
69
-
70
- function bridgeStartOptionsCall() {
71
- return firstMockArg(mocks.bridgeCodexAppServerStartOptions, "bridge start options") as {
72
- agentDir?: string;
73
- authProfileId?: string;
74
- config?: unknown;
75
- startOptions: { command?: string; commandSource?: string };
76
- };
77
- }
78
-
79
- function applyAuthProfileCall() {
80
- return firstMockArg(mocks.applyCodexAppServerAuthProfile, "apply auth profile") as {
81
- agentDir?: string;
82
- authProfileId?: string;
83
- config?: unknown;
84
- };
85
- }
86
-
87
- function resolveAuthProfileCall() {
88
- return firstMockArg(mocks.resolveCodexAppServerAuthProfileIdForAgent, "resolve auth profile") as {
89
- agentDir?: string;
90
- authProfileId?: string;
91
- config?: unknown;
92
- };
93
- }
94
-
95
- function managedStartOptionsCall() {
96
- return firstMockArg(mocks.resolveManagedCodexAppServerStartOptions, "managed start options") as {
97
- command?: string;
98
- commandSource?: string;
99
- };
100
- }
101
-
102
- function clientStartCall(startSpy: unknown) {
103
- return firstMockArg(startSpy, "CodexAppServerClient.start") as {
104
- command?: string;
105
- commandSource?: string;
106
- };
107
- }
108
-
109
- describe("shared Codex app-server client", () => {
110
- beforeAll(async () => {
111
- ({ listCodexAppServerModels } = await import("./models.js"));
112
- ({
113
- clearSharedCodexAppServerClient,
114
- clearSharedCodexAppServerClientIfCurrent,
115
- clearSharedCodexAppServerClientIfCurrentAndWait,
116
- createIsolatedCodexAppServerClient,
117
- getSharedCodexAppServerClient,
118
- resetSharedCodexAppServerClientForTests,
119
- } = await import("./shared-client.js"));
120
- });
121
-
122
- afterEach(() => {
123
- resetSharedCodexAppServerClientForTests();
124
- vi.useRealTimers();
125
- vi.restoreAllMocks();
126
- mocks.bridgeCodexAppServerStartOptions.mockClear();
127
- mocks.applyCodexAppServerAuthProfile.mockClear();
128
- mocks.resolveCodexAppServerAuthProfileIdForAgent.mockClear();
129
- mocks.resolveCodexAppServerAuthProfileIdForAgent.mockImplementation(
130
- (params?: { authProfileId?: string }) => params?.authProfileId,
131
- );
132
- mocks.resolveManagedCodexAppServerStartOptions.mockClear();
133
- mocks.resolveManagedCodexAppServerStartOptions.mockImplementation(
134
- async (startOptions) => startOptions,
135
- );
136
- mocks.embeddedAgentLog.debug.mockClear();
137
- mocks.embeddedAgentLog.warn.mockClear();
138
- mocks.resolveDefaultAgentDir.mockClear();
139
- });
140
-
141
- it("closes the shared app-server when the version gate fails", async () => {
142
- const harness = createClientHarness();
143
- const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
144
-
145
- // Model discovery uses the shared-client path, which owns child teardown
146
- // when initialize discovers an unsupported app-server.
147
- const listPromise = listCodexAppServerModels({ timeoutMs: 1000 });
148
- await sendInitializeResult(harness, "klaw/0.117.9 (macOS; test)");
149
-
150
- await expect(listPromise).rejects.toThrow(
151
- `Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required`,
152
- );
153
- expect(harness.process.stdin.destroyed).toBe(true);
154
- startSpy.mockRestore();
155
- });
156
-
157
- it("closes and clears a shared app-server when initialize times out", async () => {
158
- const first = createClientHarness();
159
- const second = createClientHarness();
160
- const startSpy = vi
161
- .spyOn(CodexAppServerClient, "start")
162
- .mockReturnValueOnce(first.client)
163
- .mockReturnValueOnce(second.client);
164
-
165
- await expect(listCodexAppServerModels({ timeoutMs: 5 })).rejects.toThrow(
166
- "codex app-server initialize timed out",
167
- );
168
- expect(first.process.stdin.destroyed).toBe(true);
169
-
170
- const secondList = listCodexAppServerModels({ timeoutMs: 1000 });
171
- await sendInitializeResult(second, "klaw/0.125.0 (macOS; test)");
172
- await sendEmptyModelList(second);
173
-
174
- await expect(secondList).resolves.toEqual({ models: [] });
175
- expect(startSpy).toHaveBeenCalledTimes(2);
176
- });
177
-
178
- it("does not wait for isolated initialize after a timeout closes the client", async () => {
179
- const harness = createClientHarness();
180
- vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
181
-
182
- await expect(createIsolatedCodexAppServerClient({ timeoutMs: 5 })).rejects.toThrow(
183
- "codex app-server initialize timed out",
184
- );
185
- expect(harness.process.stdin.destroyed).toBe(true);
186
- });
187
-
188
- it("passes the selected auth profile through the bridge helper", async () => {
189
- const harness = createClientHarness();
190
- vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
191
-
192
- const listPromise = listCodexAppServerModels({
193
- timeoutMs: 1000,
194
- authProfileId: "openai-codex:work",
195
- });
196
- await sendInitializeResult(harness, "klaw/0.125.0 (macOS; test)");
197
- await sendEmptyModelList(harness);
198
-
199
- await expect(listPromise).resolves.toEqual({ models: [] });
200
- const bridgeCall = bridgeStartOptionsCall();
201
- expect(bridgeCall?.authProfileId).toBe("openai-codex:work");
202
- const applyCall = applyAuthProfileCall();
203
- expect(applyCall?.authProfileId).toBe("openai-codex:work");
204
- });
205
-
206
- it("skips target auth resolution when native source auth is requested", async () => {
207
- const harness = createClientHarness();
208
- vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
209
- const config = { auth: { order: { "openai-codex": ["openai-codex:target"] } } };
210
-
211
- const clientPromise = getSharedCodexAppServerClient({
212
- timeoutMs: 1000,
213
- authProfileId: null,
214
- agentDir: "/tmp/klaw-target-agent",
215
- config,
216
- });
217
- await sendInitializeResult(harness, "klaw/0.125.0 (macOS; test)");
218
-
219
- await expect(clientPromise).resolves.toBe(harness.client);
220
- expect(mocks.resolveCodexAppServerAuthProfileIdForAgent).not.toHaveBeenCalled();
221
- const bridgeCall = bridgeStartOptionsCall();
222
- expect(bridgeCall.agentDir).toBe("/tmp/klaw-target-agent");
223
- expect(bridgeCall.authProfileId).toBeNull();
224
- expect(bridgeCall.config).toBe(config);
225
- const applyCall = applyAuthProfileCall();
226
- expect(applyCall.agentDir).toBe("/tmp/klaw-target-agent");
227
- expect(applyCall.authProfileId).toBeNull();
228
- expect(applyCall.config).toBe(config);
229
- });
230
-
231
- it("resolves the configured implicit auth profile before sharing a client", async () => {
232
- const harness = createClientHarness();
233
- vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
234
- const config = { auth: { order: { "openai-codex": ["openai-codex:work"] } } };
235
- mocks.resolveCodexAppServerAuthProfileIdForAgent.mockReturnValue("openai-codex:work");
236
-
237
- const listPromise = listCodexAppServerModels({
238
- timeoutMs: 1000,
239
- config,
240
- });
241
- await sendInitializeResult(harness, "klaw/0.125.0 (macOS; test)");
242
- await sendEmptyModelList(harness);
243
-
244
- await expect(listPromise).resolves.toEqual({ models: [] });
245
- const resolveCall = resolveAuthProfileCall();
246
- expect(resolveCall).toStrictEqual({
247
- authProfileId: undefined,
248
- agentDir: "/tmp/klaw-agent",
249
- config,
250
- });
251
- const bridgeCall = bridgeStartOptionsCall();
252
- expect(bridgeCall?.authProfileId).toBe("openai-codex:work");
253
- expect(bridgeCall?.config).toBe(config);
254
- const applyCall = applyAuthProfileCall();
255
- expect(applyCall?.authProfileId).toBe("openai-codex:work");
256
- expect(applyCall?.config).toBe(config);
257
- });
258
-
259
- it("uses the selected agent dir for shared app-server auth bridging", async () => {
260
- const harness = createClientHarness();
261
- vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
262
-
263
- const listPromise = listCodexAppServerModels({
264
- timeoutMs: 1000,
265
- authProfileId: "openai-codex:work",
266
- agentDir: "/tmp/klaw-agent-nova",
267
- });
268
- await sendInitializeResult(harness, "klaw/0.125.0 (macOS; test)");
269
- await sendEmptyModelList(harness);
270
-
271
- await expect(listPromise).resolves.toEqual({ models: [] });
272
- const bridgeCall = bridgeStartOptionsCall();
273
- expect(bridgeCall?.agentDir).toBe("/tmp/klaw-agent-nova");
274
- expect(bridgeCall?.authProfileId).toBe("openai-codex:work");
275
- const applyCall = applyAuthProfileCall();
276
- expect(applyCall?.agentDir).toBe("/tmp/klaw-agent-nova");
277
- expect(applyCall?.authProfileId).toBe("openai-codex:work");
278
- });
279
-
280
- it("migrates legacy singleton global state into the keyed registry", async () => {
281
- const legacy = createClientHarness();
282
- const next = createClientHarness();
283
- const startOptions = {
284
- transport: "websocket" as const,
285
- command: "codex",
286
- args: [],
287
- url: "ws://127.0.0.1:39175",
288
- authToken: "tok-legacy",
289
- headers: {},
290
- };
291
- const key = codexAppServerStartOptionsKey(startOptions, {
292
- agentDir: "/tmp/klaw-agent",
293
- });
294
- const globalState = globalThis as typeof globalThis & {
295
- [key: symbol]: unknown;
296
- };
297
- globalState[Symbol.for("klaw.codexAppServerClientState")] = {
298
- key,
299
- client: legacy.client,
300
- promise: Promise.resolve(legacy.client),
301
- };
302
-
303
- await expect(getSharedCodexAppServerClient({ startOptions })).resolves.toBe(legacy.client);
304
-
305
- legacy.client.close();
306
- const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(next.client);
307
- const list = listCodexAppServerModels({ timeoutMs: 1000, startOptions });
308
- await sendInitializeResult(next, "klaw/0.125.0 (macOS; test)");
309
- await sendEmptyModelList(next);
310
-
311
- await expect(list).resolves.toEqual({ models: [] });
312
- expect(startSpy).toHaveBeenCalledTimes(1);
313
- });
314
-
315
- it("keeps an active shared client alive when another agent dir uses a different key", async () => {
316
- const first = createClientHarness();
317
- const second = createClientHarness();
318
- const startSpy = vi
319
- .spyOn(CodexAppServerClient, "start")
320
- .mockReturnValueOnce(first.client)
321
- .mockReturnValueOnce(second.client);
322
-
323
- const firstList = listCodexAppServerModels({
324
- timeoutMs: 1000,
325
- agentDir: "/tmp/klaw-agent-one",
326
- });
327
- await sendInitializeResult(first, "klaw/0.125.0 (macOS; test)");
328
- await sendEmptyModelList(first);
329
- await expect(firstList).resolves.toEqual({ models: [] });
330
-
331
- const secondList = listCodexAppServerModels({
332
- timeoutMs: 1000,
333
- agentDir: "/tmp/klaw-agent-two",
334
- });
335
- await sendInitializeResult(second, "klaw/0.125.0 (macOS; test)");
336
- await sendEmptyModelList(second);
337
- await expect(secondList).resolves.toEqual({ models: [] });
338
-
339
- expect(startSpy).toHaveBeenCalledTimes(2);
340
- expect(first.process.stdin.destroyed).toBe(false);
341
- expect(second.process.stdin.destroyed).toBe(false);
342
- });
343
-
344
- it("resolves the managed binary before bridging and spawning the shared client", async () => {
345
- const harness = createClientHarness();
346
- const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
347
- mocks.resolveManagedCodexAppServerStartOptions.mockImplementationOnce(async (startOptions) => ({
348
- ...startOptions,
349
- command: "/cache/klaw/codex",
350
- commandSource: "resolved-managed",
351
- }));
352
-
353
- const listPromise = listCodexAppServerModels({ timeoutMs: 1000 });
354
- await sendInitializeResult(harness, "klaw/0.125.0 (macOS; test)");
355
- await sendEmptyModelList(harness);
356
-
357
- await expect(listPromise).resolves.toEqual({ models: [] });
358
- const managedCall = managedStartOptionsCall();
359
- expect(managedCall?.command).toBe("codex");
360
- expect(managedCall?.commandSource).toBe("managed");
361
- const bridgeCall = bridgeStartOptionsCall();
362
- expect(bridgeCall?.startOptions.command).toBe("/cache/klaw/codex");
363
- expect(bridgeCall?.startOptions.commandSource).toBe("resolved-managed");
364
- const startCall = clientStartCall(startSpy);
365
- expect(startCall?.command).toBe("/cache/klaw/codex");
366
- expect(startCall?.commandSource).toBe("resolved-managed");
367
- });
368
-
369
- it("starts an independent shared client when the bridged auth token changes", async () => {
370
- const first = createClientHarness();
371
- const second = createClientHarness();
372
- const startSpy = vi
373
- .spyOn(CodexAppServerClient, "start")
374
- .mockReturnValueOnce(first.client)
375
- .mockReturnValueOnce(second.client);
376
-
377
- const firstList = listCodexAppServerModels({
378
- timeoutMs: 1000,
379
- startOptions: {
380
- transport: "websocket",
381
- command: "codex",
382
- args: [],
383
- url: "ws://127.0.0.1:39175",
384
- authToken: "tok-first",
385
- headers: {},
386
- },
387
- });
388
- await sendInitializeResult(first, "klaw/0.125.0 (macOS; test)");
389
- await sendEmptyModelList(first);
390
- await expect(firstList).resolves.toEqual({ models: [] });
391
-
392
- const secondList = listCodexAppServerModels({
393
- timeoutMs: 1000,
394
- startOptions: {
395
- transport: "websocket",
396
- command: "codex",
397
- args: [],
398
- url: "ws://127.0.0.1:39175",
399
- authToken: "tok-second",
400
- headers: {},
401
- },
402
- });
403
- await sendInitializeResult(second, "klaw/0.125.0 (macOS; test)");
404
- await sendEmptyModelList(second);
405
- await expect(secondList).resolves.toEqual({ models: [] });
406
-
407
- expect(startSpy).toHaveBeenCalledTimes(2);
408
- expect(first.process.stdin.destroyed).toBe(false);
409
- });
410
-
411
- it("does not let one shared-client failure tear down another keyed client", async () => {
412
- const first = createClientHarness();
413
- const second = createClientHarness();
414
- vi.spyOn(CodexAppServerClient, "start")
415
- .mockReturnValueOnce(first.client)
416
- .mockReturnValueOnce(second.client);
417
-
418
- const firstList = listCodexAppServerModels({
419
- timeoutMs: 1000,
420
- startOptions: {
421
- transport: "websocket",
422
- command: "codex",
423
- args: [],
424
- url: "ws://127.0.0.1:39175",
425
- authToken: "tok-first",
426
- headers: {},
427
- },
428
- });
429
- const firstFailure = firstList.catch((error: unknown) => error);
430
- await vi.waitFor(() => expect(first.writes.length).toBeGreaterThanOrEqual(1));
431
-
432
- const secondList = listCodexAppServerModels({
433
- timeoutMs: 1000,
434
- startOptions: {
435
- transport: "websocket",
436
- command: "codex",
437
- args: [],
438
- url: "ws://127.0.0.1:39175",
439
- authToken: "tok-second",
440
- headers: {},
441
- },
442
- });
443
- await vi.waitFor(() => expect(second.writes.length).toBeGreaterThanOrEqual(1));
444
-
445
- await sendInitializeResult(second, "klaw/0.125.0 (macOS; test)");
446
- await sendEmptyModelList(second);
447
- await expect(secondList).resolves.toEqual({ models: [] });
448
-
449
- first.client.close();
450
- await expect(firstFailure).resolves.toBeInstanceOf(Error);
451
-
452
- expect(second.process.kill).not.toHaveBeenCalled();
453
- });
454
-
455
- it("only clears the shared client that is still current", async () => {
456
- const first = createClientHarness();
457
- const second = createClientHarness();
458
- vi.spyOn(CodexAppServerClient, "start")
459
- .mockReturnValueOnce(first.client)
460
- .mockReturnValueOnce(second.client);
461
-
462
- const firstList = listCodexAppServerModels({ timeoutMs: 1000 });
463
- await sendInitializeResult(first, "klaw/0.125.0 (macOS; test)");
464
- await sendEmptyModelList(first);
465
- await expect(firstList).resolves.toEqual({ models: [] });
466
-
467
- expect(clearSharedCodexAppServerClientIfCurrent(first.client)).toBe(true);
468
- expect(first.process.stdin.destroyed).toBe(true);
469
-
470
- const secondList = listCodexAppServerModels({ timeoutMs: 1000 });
471
- await sendInitializeResult(second, "klaw/0.125.0 (macOS; test)");
472
- await sendEmptyModelList(second);
473
- await expect(secondList).resolves.toEqual({ models: [] });
474
-
475
- expect(clearSharedCodexAppServerClientIfCurrent(first.client)).toBe(false);
476
- expect(second.process.kill).not.toHaveBeenCalled();
477
- expect(clearSharedCodexAppServerClientIfCurrent(second.client)).toBe(true);
478
- expect(second.process.stdin.destroyed).toBe(true);
479
- });
480
-
481
- it("waits only for the shared client that is still current", async () => {
482
- const first = createClientHarness();
483
- const second = createClientHarness();
484
- vi.spyOn(CodexAppServerClient, "start")
485
- .mockReturnValueOnce(first.client)
486
- .mockReturnValueOnce(second.client);
487
- const firstCloseAndWait = vi.spyOn(first.client, "closeAndWait");
488
- const secondCloseAndWait = vi.spyOn(second.client, "closeAndWait");
489
-
490
- const firstList = listCodexAppServerModels({
491
- timeoutMs: 1000,
492
- agentDir: "/tmp/klaw-agent-one",
493
- });
494
- await sendInitializeResult(first, "klaw/0.125.0 (macOS; test)");
495
- await sendEmptyModelList(first);
496
- await expect(firstList).resolves.toEqual({ models: [] });
497
-
498
- const secondList = listCodexAppServerModels({
499
- timeoutMs: 1000,
500
- agentDir: "/tmp/klaw-agent-two",
501
- });
502
- await sendInitializeResult(second, "klaw/0.125.0 (macOS; test)");
503
- await sendEmptyModelList(second);
504
- await expect(secondList).resolves.toEqual({ models: [] });
505
-
506
- await expect(
507
- clearSharedCodexAppServerClientIfCurrentAndWait(first.client, {
508
- exitTimeoutMs: 25,
509
- forceKillDelayMs: 5,
510
- }),
511
- ).resolves.toBe(true);
512
-
513
- expect(firstCloseAndWait).toHaveBeenCalledTimes(1);
514
- expect(secondCloseAndWait).not.toHaveBeenCalled();
515
- expect(first.process.stdin.destroyed).toBe(true);
516
- expect(second.process.stdin.destroyed).toBe(false);
517
- });
518
-
519
- it("uses a fresh websocket Authorization header after shared-client token rotation", async () => {
520
- const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
521
- const authHeaders: Array<string | undefined> = [];
522
- server.on("connection", (socket, request) => {
523
- authHeaders.push(request.headers.authorization);
524
- socket.on("message", (data) => {
525
- const message = JSON.parse(rawDataToText(data)) as { id?: number; method?: string };
526
- if (message.method === "initialize") {
527
- socket.send(JSON.stringify({ id: message.id, result: { userAgent: "klaw/0.125.0" } }));
528
- return;
529
- }
530
- if (message.method === "model/list") {
531
- socket.send(JSON.stringify({ id: message.id, result: { data: [] } }));
532
- }
533
- });
534
- });
535
-
536
- try {
537
- await new Promise<void>((resolve) => server.once("listening", resolve));
538
- const address = server.address();
539
- if (!address || typeof address === "string") {
540
- throw new Error("expected websocket test server port");
541
- }
542
- const url = `ws://127.0.0.1:${address.port}`;
543
-
544
- await expect(
545
- listCodexAppServerModels({
546
- timeoutMs: 1000,
547
- startOptions: {
548
- transport: "websocket",
549
- command: "codex",
550
- args: [],
551
- url,
552
- authToken: "tok-first",
553
- headers: {},
554
- },
555
- }),
556
- ).resolves.toEqual({ models: [] });
557
- await expect(
558
- listCodexAppServerModels({
559
- timeoutMs: 1000,
560
- startOptions: {
561
- transport: "websocket",
562
- command: "codex",
563
- args: [],
564
- url,
565
- authToken: "tok-second",
566
- headers: {},
567
- },
568
- }),
569
- ).resolves.toEqual({ models: [] });
570
-
571
- expect(authHeaders).toEqual(["Bearer tok-first", "Bearer tok-second"]);
572
- } finally {
573
- clearSharedCodexAppServerClient();
574
- await new Promise<void>((resolve, reject) =>
575
- server.close((error) => (error ? reject(error) : resolve())),
576
- );
577
- }
578
- });
579
- });
580
-
581
- function rawDataToText(data: RawData): string {
582
- if (Array.isArray(data)) {
583
- return Buffer.concat(data).toString("utf8");
584
- }
585
- if (data instanceof ArrayBuffer) {
586
- return Buffer.from(new Uint8Array(data)).toString("utf8");
587
- }
588
- return Buffer.from(data).toString("utf8");
589
- }