@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,563 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import { PassThrough } from "node:stream";
3
- import { embeddedAgentLog, KLAW_VERSION } from "klaw/plugin-sdk/agent-harness-runtime";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import {
6
- testing,
7
- CodexAppServerClient,
8
- MIN_CODEX_APP_SERVER_VERSION,
9
- isCodexAppServerApprovalRequest,
10
- readCodexVersionFromUserAgent,
11
- } from "./client.js";
12
- import { resetSharedCodexAppServerClientForTests } from "./shared-client.js";
13
- import { createClientHarness } from "./test-support.js";
14
-
15
- describe("CodexAppServerClient", () => {
16
- const clients: CodexAppServerClient[] = [];
17
-
18
- function startInitialize() {
19
- const harness = createClientHarness();
20
- clients.push(harness.client);
21
- const initializing = harness.client.initialize();
22
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as {
23
- id?: number;
24
- method?: string;
25
- params?: { clientInfo?: { name?: string; title?: string; version?: string } };
26
- };
27
- return { harness, initializing, outbound };
28
- }
29
-
30
- afterEach(() => {
31
- resetSharedCodexAppServerClientForTests();
32
- vi.useRealTimers();
33
- vi.restoreAllMocks();
34
- for (const client of clients) {
35
- client.close();
36
- }
37
- clients.length = 0;
38
- });
39
-
40
- it("routes request responses by id", async () => {
41
- const harness = createClientHarness();
42
- clients.push(harness.client);
43
-
44
- const request = harness.client.request("model/list", {});
45
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number; method?: string };
46
- harness.send({ id: outbound.id, result: { models: [] } });
47
-
48
- await expect(request).resolves.toEqual({ models: [] });
49
- expect(outbound.method).toBe("model/list");
50
- });
51
-
52
- it("removes unpaired surrogate code units from outbound JSON-RPC strings", async () => {
53
- const harness = createClientHarness();
54
- clients.push(harness.client);
55
- const high = String.fromCharCode(0xd83d);
56
- const low = String.fromCharCode(0xdc00);
57
-
58
- const request = harness.client.request("thread/start", {
59
- prompt: `left${high}right`,
60
- nested: [`low${low}end`, "emoji 🙈 ok"],
61
- });
62
-
63
- expect(harness.writes[0]).not.toContain("\\ud83d");
64
- expect(harness.writes[0]).not.toContain("\\udc00");
65
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as {
66
- params?: { prompt?: string; nested?: string[] };
67
- };
68
- expect(outbound.params?.prompt).toBe("leftright");
69
- expect(outbound.params?.nested).toEqual(["lowend", "emoji 🙈 ok"]);
70
- harness.send({
71
- id: JSON.parse(harness.writes[0] ?? "{}").id,
72
- result: { threadId: "thread-1" },
73
- });
74
- await expect(request).resolves.toEqual({ threadId: "thread-1" });
75
- });
76
-
77
- it("logs a redacted preview for malformed app-server messages", async () => {
78
- const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
79
- const harness = createClientHarness();
80
- clients.push(harness.client);
81
-
82
- harness.process.stdout.write('{"token":"secret-value"} trailing\n');
83
-
84
- await vi.waitFor(() => expect(warn).toHaveBeenCalledTimes(1));
85
- const [message, rawMetadata] = warn.mock.calls[0] ?? [];
86
- expect(message).toBe("failed to parse codex app-server message");
87
- const metadata = rawMetadata as
88
- | {
89
- error?: unknown;
90
- errorMessage?: string;
91
- fragmentCount?: number;
92
- linePreview?: string;
93
- consoleMessage?: string;
94
- }
95
- | undefined;
96
- expect(metadata?.error).toBeInstanceOf(SyntaxError);
97
- expect(metadata?.errorMessage).toBe(
98
- "Unexpected non-whitespace character after JSON at position 25 (line 1 column 26)",
99
- );
100
- expect(metadata?.fragmentCount).toBe(1);
101
- expect(metadata?.linePreview).toBe('{"token":"<redacted>"} trailing');
102
- expect(metadata?.consoleMessage).toBe(
103
- 'failed to parse codex app-server message: preview="{\\"token\\":\\"<redacted>\\"} trailing"',
104
- );
105
- expect(JSON.stringify(warn.mock.calls)).not.toContain("secret-value");
106
- });
107
-
108
- it("redacts prefixed env credential names from app-server previews", () => {
109
- expect(
110
- testing.redactCodexAppServerLinePreview(
111
- "fatal OPENAI_API_KEY=sk-live ANTHROPIC_API_KEY='anthropic-secret' OTHER=value",
112
- ),
113
- ).toBe("fatal OPENAI_API_KEY=<redacted> ANTHROPIC_API_KEY='<redacted>' OTHER=value");
114
- });
115
-
116
- it("recovers app-server messages split by raw newlines inside JSON strings", async () => {
117
- const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
118
- const harness = createClientHarness();
119
- clients.push(harness.client);
120
- const notifications: unknown[] = [];
121
- harness.client.addNotificationHandler((notification) => {
122
- notifications.push(notification);
123
- });
124
-
125
- harness.process.stdout.write(
126
- '{"method":"item/commandExecution/outputDelta","params":{"delta":"first' +
127
- "\n" +
128
- 'second"}}\n',
129
- );
130
-
131
- await vi.waitFor(() =>
132
- expect(notifications).toEqual([
133
- {
134
- method: "item/commandExecution/outputDelta",
135
- params: { delta: "first\nsecond" },
136
- },
137
- ]),
138
- );
139
- expect(warn).not.toHaveBeenCalled();
140
- });
141
-
142
- it("preserves JSON-RPC error codes", async () => {
143
- const harness = createClientHarness();
144
- clients.push(harness.client);
145
-
146
- const request = harness.client.request("future/method", {});
147
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
148
- harness.send({ id: outbound.id, error: { code: -32601, message: "Method not found" } });
149
-
150
- await expect(request).rejects.toHaveProperty("name", "CodexAppServerRpcError");
151
- await expect(request).rejects.toHaveProperty("code", -32601);
152
- await expect(request).rejects.toHaveProperty("message", "Method not found");
153
- });
154
-
155
- it("surfaces relogin details from Codex app-server RPC errors", async () => {
156
- const harness = createClientHarness();
157
- clients.push(harness.client);
158
-
159
- const request = harness.client.request("thread/start", {});
160
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
161
- harness.send({
162
- id: outbound.id,
163
- error: {
164
- code: -32602,
165
- message: "failed to load configuration",
166
- data: {
167
- reason: "cloudRequirements",
168
- errorCode: "Auth",
169
- action: "relogin",
170
- statusCode: 401,
171
- detail:
172
- "Your authentication session could not be refreshed automatically. Please log out and sign in again.",
173
- },
174
- },
175
- });
176
-
177
- await expect(request).rejects.toHaveProperty(
178
- "message",
179
- "failed to load configuration: Your authentication session could not be refreshed automatically. Please log out and sign in again.",
180
- );
181
- await expect(request).rejects.toHaveProperty("data", {
182
- reason: "cloudRequirements",
183
- errorCode: "Auth",
184
- action: "relogin",
185
- statusCode: 401,
186
- detail:
187
- "Your authentication session could not be refreshed automatically. Please log out and sign in again.",
188
- });
189
- });
190
-
191
- it("rejects timed-out requests and ignores late responses", async () => {
192
- vi.useFakeTimers();
193
- const harness = createClientHarness();
194
- clients.push(harness.client);
195
-
196
- const request = harness.client.request("model/list", {}, { timeoutMs: 1 });
197
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
198
- const assertion = expect(request).rejects.toThrow("model/list timed out");
199
-
200
- await vi.advanceTimersByTimeAsync(100);
201
- await assertion;
202
-
203
- harness.send({ id: outbound.id, result: { data: [] } });
204
- expect(harness.writes).toHaveLength(1);
205
- });
206
-
207
- it("rejects aborted requests and ignores late responses", async () => {
208
- const harness = createClientHarness();
209
- clients.push(harness.client);
210
- const controller = new AbortController();
211
-
212
- const request = harness.client.request("model/list", {}, { signal: controller.signal });
213
- const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
214
- const assertion = expect(request).rejects.toThrow("model/list aborted");
215
- controller.abort();
216
-
217
- await assertion;
218
- harness.send({ id: outbound.id, result: { data: [] } });
219
- expect(harness.writes).toHaveLength(1);
220
- });
221
-
222
- it("initializes with the required client version", async () => {
223
- const { harness, initializing, outbound } = startInitialize();
224
- harness.send({
225
- id: outbound.id,
226
- result: { userAgent: "klaw/0.125.0 (macOS; test)" },
227
- });
228
-
229
- await expect(initializing).resolves.toBeUndefined();
230
- expect(outbound).toStrictEqual({
231
- id: outbound.id,
232
- method: "initialize",
233
- params: {
234
- clientInfo: {
235
- name: "klaw",
236
- title: "Klaw",
237
- version: KLAW_VERSION,
238
- },
239
- capabilities: {
240
- experimentalApi: true,
241
- },
242
- },
243
- });
244
- expect(outbound.params?.clientInfo?.version).not.toBe("");
245
- expect(JSON.parse(harness.writes[1] ?? "{}")).toEqual({ method: "initialized" });
246
- });
247
-
248
- it("blocks unsupported app-server versions during initialize", async () => {
249
- const { harness, initializing, outbound } = startInitialize();
250
- harness.send({
251
- id: outbound.id,
252
- result: { userAgent: "klaw/0.124.9 (macOS; test)" },
253
- });
254
-
255
- await expect(initializing).rejects.toThrow(
256
- `Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected 0.124.9`,
257
- );
258
- expect(harness.writes).toHaveLength(1);
259
- });
260
-
261
- it("blocks same-version Codex app-server prereleases below the stable floor", async () => {
262
- const { harness, initializing, outbound } = startInitialize();
263
- harness.send({
264
- id: outbound.id,
265
- result: { userAgent: "klaw/0.125.0-alpha.2 (macOS; test)" },
266
- });
267
-
268
- await expect(initializing).rejects.toThrow(
269
- `Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected 0.125.0-alpha.2`,
270
- );
271
- expect(harness.writes).toHaveLength(1);
272
- });
273
-
274
- it("blocks same-version Codex app-server build metadata below the stable floor", async () => {
275
- const { harness, initializing, outbound } = startInitialize();
276
- harness.send({
277
- id: outbound.id,
278
- result: { userAgent: "klaw/0.125.0+alpha.2 (macOS; test)" },
279
- });
280
-
281
- await expect(initializing).rejects.toThrow(
282
- `Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected 0.125.0+alpha.2`,
283
- );
284
- expect(harness.writes).toHaveLength(1);
285
- });
286
-
287
- it("accepts newer Codex app-server prereleases", async () => {
288
- const { harness, initializing, outbound } = startInitialize();
289
- harness.send({
290
- id: outbound.id,
291
- result: { userAgent: "klaw/0.126.0-alpha.1 (macOS; test)" },
292
- });
293
-
294
- await expect(initializing).resolves.toBeUndefined();
295
- expect(JSON.parse(harness.writes[1] ?? "{}")).toEqual({ method: "initialized" });
296
- });
297
-
298
- it("accepts newer Codex app-server builds", async () => {
299
- const { harness, initializing, outbound } = startInitialize();
300
- harness.send({
301
- id: outbound.id,
302
- result: { userAgent: "klaw/0.126.0+custom (macOS; test)" },
303
- });
304
-
305
- await expect(initializing).resolves.toBeUndefined();
306
- expect(JSON.parse(harness.writes[1] ?? "{}")).toEqual({ method: "initialized" });
307
- });
308
-
309
- it("blocks app-server initialize responses without a version", async () => {
310
- const { harness, initializing, outbound } = startInitialize();
311
- harness.send({ id: outbound.id, result: {} });
312
-
313
- await expect(initializing).rejects.toThrow(
314
- `Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required`,
315
- );
316
- expect(harness.writes).toHaveLength(1);
317
- });
318
-
319
- it("waits for app-server transports to exit after closing stdin before force-stopping", async () => {
320
- vi.useFakeTimers();
321
- const process = Object.assign(new EventEmitter(), {
322
- stdin: {
323
- write: vi.fn(),
324
- end: vi.fn(),
325
- destroy: vi.fn(),
326
- unref: vi.fn(),
327
- },
328
- stdout: Object.assign(new PassThrough(), { unref: vi.fn() }),
329
- stderr: Object.assign(new PassThrough(), { unref: vi.fn() }),
330
- exitCode: null,
331
- signalCode: null,
332
- kill: vi.fn(),
333
- unref: vi.fn(),
334
- });
335
-
336
- testing.closeCodexAppServerTransport(process, { forceKillDelayMs: 25 });
337
-
338
- expect(process.stdin.end).toHaveBeenCalledTimes(1);
339
- expect(process.kill).not.toHaveBeenCalled();
340
- await vi.advanceTimersByTimeAsync(25);
341
- expect(process.kill).toHaveBeenCalledWith("SIGKILL");
342
- expect(process.unref).toHaveBeenCalledTimes(1);
343
- });
344
-
345
- it("waits for app-server transport exit during async shutdown", async () => {
346
- vi.useFakeTimers();
347
- const process = Object.assign(new EventEmitter(), {
348
- stdin: {
349
- write: vi.fn(),
350
- end: vi.fn(),
351
- destroy: vi.fn(),
352
- unref: vi.fn(),
353
- },
354
- stdout: Object.assign(new PassThrough(), { unref: vi.fn() }),
355
- stderr: Object.assign(new PassThrough(), { unref: vi.fn() }),
356
- exitCode: null as number | null,
357
- signalCode: null as string | null,
358
- kill: vi.fn(),
359
- unref: vi.fn(),
360
- });
361
-
362
- const closed = testing.closeCodexAppServerTransportAndWait(process, {
363
- exitTimeoutMs: 100,
364
- forceKillDelayMs: 25,
365
- });
366
-
367
- expect(process.stdin.end).toHaveBeenCalledTimes(1);
368
- expect(process.kill).not.toHaveBeenCalled();
369
- await vi.advanceTimersByTimeAsync(25);
370
- expect(process.kill).toHaveBeenCalledWith("SIGKILL");
371
- process.signalCode = "SIGKILL";
372
- process.emit("exit");
373
-
374
- await expect(closed).resolves.toBe(true);
375
- });
376
-
377
- it("keeps async shutdown alive until the exit timeout resolves", async () => {
378
- vi.useFakeTimers();
379
- const process = Object.assign(new EventEmitter(), {
380
- stdin: {
381
- write: vi.fn(),
382
- end: vi.fn(),
383
- destroy: vi.fn(),
384
- unref: vi.fn(),
385
- },
386
- stdout: Object.assign(new PassThrough(), { unref: vi.fn() }),
387
- stderr: Object.assign(new PassThrough(), { unref: vi.fn() }),
388
- exitCode: null as number | null,
389
- signalCode: null as string | null,
390
- kill: vi.fn(),
391
- unref: vi.fn(),
392
- });
393
-
394
- const closed = testing.closeCodexAppServerTransportAndWait(process, {
395
- exitTimeoutMs: 100,
396
- forceKillDelayMs: 25,
397
- });
398
-
399
- await vi.advanceTimersByTimeAsync(100);
400
-
401
- await expect(closed).resolves.toBe(false);
402
- });
403
-
404
- it("handles stdin write errors without crashing the process", async () => {
405
- const harness = createClientHarness();
406
- clients.push(harness.client);
407
-
408
- // Start a pending request so we can verify it gets properly rejected.
409
- const pending = harness.client.request("test/method");
410
-
411
- // Simulate the child process closing its pipe — a write to the now-dead
412
- // stdin emits an asynchronous EPIPE error on the stream.
413
- harness.process.stdin.destroy(Object.assign(new Error("write EPIPE"), { code: "EPIPE" }));
414
-
415
- // The pending request must be rejected with the pipe error rather than
416
- // an unhandled exception tearing down the gateway.
417
- await expect(pending).rejects.toThrow("write EPIPE");
418
-
419
- // Subsequent requests keep the original close reason so startup logs stay actionable.
420
- await expect(harness.client.request("another/method")).rejects.toThrow("write EPIPE");
421
- });
422
-
423
- it("preserves redacted app-server stderr on exit errors", async () => {
424
- const harness = createClientHarness();
425
- clients.push(harness.client);
426
-
427
- const pending = harness.client.request("test/method");
428
- harness.process.stderr.write('fatal token="secret-value" while booting\n');
429
- harness.process.emit("exit", 1, null);
430
-
431
- await expect(pending).rejects.toThrow(
432
- 'codex app-server exited: code=1 signal=null stderr="fatal token=\\"<redacted>\\" while booting"',
433
- );
434
- await expect(harness.client.request("another/method")).rejects.toThrow(
435
- "codex app-server exited: code=1 signal=null",
436
- );
437
- });
438
-
439
- it("does not write to stdin after the child process exits", () => {
440
- const harness = createClientHarness();
441
- clients.push(harness.client);
442
-
443
- // Simulate the child process exiting.
444
- harness.process.emit("exit", 1, null);
445
-
446
- // A notification after exit must not attempt a write.
447
- harness.client.notify("late/event", { data: "ignored" });
448
- expect(harness.writes).toHaveLength(0);
449
- });
450
-
451
- it("reads the Codex version from the app-server user agent", () => {
452
- expect(readCodexVersionFromUserAgent("Codex Desktop/0.125.0")).toBe("0.125.0");
453
- expect(readCodexVersionFromUserAgent("klaw/0.125.0 (macOS; test)")).toBe("0.125.0");
454
- expect(readCodexVersionFromUserAgent("codex_cli_rs/0.125.0-dev (linux; test)")).toBe(
455
- "0.125.0-dev",
456
- );
457
- expect(readCodexVersionFromUserAgent("Codex Desktop/not-a-version")).toBeUndefined();
458
- expect(readCodexVersionFromUserAgent("Codex Desktop/0.124")).toBeUndefined();
459
- expect(readCodexVersionFromUserAgent("klaw/0.125.0abc")).toBeUndefined();
460
- expect(readCodexVersionFromUserAgent("missing-version")).toBeUndefined();
461
- });
462
-
463
- it("answers server-initiated requests with the registered handler result", async () => {
464
- const harness = createClientHarness();
465
- clients.push(harness.client);
466
- harness.client.addRequestHandler((request) => {
467
- if (request.method === "item/tool/call") {
468
- return { contentItems: [{ type: "inputText", text: "ok" }], success: true };
469
- }
470
- return undefined;
471
- });
472
-
473
- harness.send({ id: "srv-1", method: "item/tool/call", params: { tool: "message" } });
474
- await vi.waitFor(() => expect(harness.writes.length).toBe(1));
475
-
476
- expect(JSON.parse(harness.writes[0] ?? "{}")).toEqual({
477
- id: "srv-1",
478
- result: { contentItems: [{ type: "inputText", text: "ok" }], success: true },
479
- });
480
- });
481
-
482
- it("fails closed when a dynamic tool server request handler hangs", async () => {
483
- vi.useFakeTimers();
484
- const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
485
- const harness = createClientHarness();
486
- clients.push(harness.client);
487
- harness.client.addRequestHandler((request) => {
488
- if (request.method === "item/tool/call") {
489
- return new Promise<never>(() => undefined);
490
- }
491
- return undefined;
492
- });
493
-
494
- harness.send({ id: "srv-timeout", method: "item/tool/call", params: { tool: "message" } });
495
- await vi.advanceTimersByTimeAsync(testing.CODEX_DYNAMIC_TOOL_SERVER_REQUEST_TIMEOUT_MS);
496
- await vi.waitFor(() => expect(harness.writes.length).toBe(1));
497
-
498
- expect(JSON.parse(harness.writes[0] ?? "{}")).toEqual({
499
- id: "srv-timeout",
500
- result: {
501
- success: false,
502
- contentItems: [
503
- {
504
- type: "inputText",
505
- text: `Klaw dynamic tool call timed out after ${testing.CODEX_DYNAMIC_TOOL_SERVER_REQUEST_TIMEOUT_MS}ms before sending a response to Codex.`,
506
- },
507
- ],
508
- },
509
- });
510
- expect(warn).toHaveBeenCalledWith("codex app-server server request timed out", {
511
- id: "srv-timeout",
512
- method: "item/tool/call",
513
- timeoutMs: testing.CODEX_DYNAMIC_TOOL_SERVER_REQUEST_TIMEOUT_MS,
514
- });
515
- });
516
-
517
- it("fails closed for unhandled native app-server approvals", async () => {
518
- const harness = createClientHarness();
519
- clients.push(harness.client);
520
-
521
- harness.send({
522
- id: "approval-1",
523
- method: "item/commandExecution/requestApproval",
524
- params: { threadId: "thread-1", turnId: "turn-1", itemId: "cmd-1", command: "pnpm test" },
525
- });
526
- await vi.waitFor(() => expect(harness.writes.length).toBe(1));
527
-
528
- expect(JSON.parse(harness.writes[0] ?? "{}")).toEqual({
529
- id: "approval-1",
530
- result: { decision: "decline" },
531
- });
532
- });
533
-
534
- it("only treats known Codex app-server approval methods as approvals", () => {
535
- expect(isCodexAppServerApprovalRequest("item/commandExecution/requestApproval")).toBe(true);
536
- expect(isCodexAppServerApprovalRequest("item/fileChange/requestApproval")).toBe(true);
537
- expect(isCodexAppServerApprovalRequest("item/permissions/requestApproval")).toBe(true);
538
- expect(isCodexAppServerApprovalRequest("evil/Approval")).toBe(false);
539
- expect(isCodexAppServerApprovalRequest("item/tool/requestApproval")).toBe(false);
540
- });
541
-
542
- it("fails closed for unhandled request_user_input prompts", async () => {
543
- const harness = createClientHarness();
544
- clients.push(harness.client);
545
-
546
- harness.send({
547
- id: "input-1",
548
- method: "item/tool/requestUserInput",
549
- params: {
550
- threadId: "thread-1",
551
- turnId: "turn-1",
552
- itemId: "tool-1",
553
- questions: [],
554
- },
555
- });
556
- await vi.waitFor(() => expect(harness.writes.length).toBe(1));
557
-
558
- expect(JSON.parse(harness.writes[0] ?? "{}")).toEqual({
559
- id: "input-1",
560
- result: { answers: {} },
561
- });
562
- });
563
- });