@kodelyth/codex 2026.5.42 → 2026.6.1

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 +16 -1
  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,1471 +0,0 @@
1
- import {
2
- callGatewayTool,
3
- hasNativeHookRelayInvocation,
4
- invokeNativeHookRelay,
5
- runBeforeToolCallHook,
6
- type EmbeddedRunAttemptParams,
7
- } from "klaw/plugin-sdk/agent-harness-runtime";
8
- import { beforeEach, describe, expect, it, vi } from "vitest";
9
- import { buildApprovalResponse, handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
10
-
11
- vi.mock("klaw/plugin-sdk/agent-harness-runtime", async (importOriginal) => ({
12
- ...(await importOriginal<typeof import("klaw/plugin-sdk/agent-harness-runtime")>()),
13
- callGatewayTool: vi.fn(),
14
- hasNativeHookRelayInvocation: vi.fn(() => false),
15
- invokeNativeHookRelay: vi.fn(),
16
- runBeforeToolCallHook: vi.fn(async ({ params }: { params: unknown }) => ({
17
- blocked: false,
18
- params,
19
- })),
20
- }));
21
-
22
- const mockCallGatewayTool = vi.mocked(callGatewayTool);
23
- const mockHasNativeHookRelayInvocation = vi.mocked(hasNativeHookRelayInvocation);
24
- const mockInvokeNativeHookRelay = vi.mocked(invokeNativeHookRelay);
25
- const mockRunBeforeToolCallHook = vi.mocked(runBeforeToolCallHook);
26
-
27
- function requireRecord(value: unknown, label: string): Record<string, unknown> {
28
- if (!value || typeof value !== "object" || Array.isArray(value)) {
29
- throw new Error(`Expected ${label}`);
30
- }
31
- return value as Record<string, unknown>;
32
- }
33
-
34
- function gatewayCallAt(callIndex = 0) {
35
- const call = mockCallGatewayTool.mock.calls[callIndex];
36
- if (!call) {
37
- throw new Error(`Expected gateway call ${callIndex + 1}`);
38
- }
39
- return call;
40
- }
41
-
42
- function gatewayRequestPayload(callIndex = 0) {
43
- return requireRecord(gatewayCallAt(callIndex)[2], `gateway request payload ${callIndex + 1}`);
44
- }
45
-
46
- function gatewayCallOptions(callIndex = 0) {
47
- return gatewayCallAt(callIndex)[3];
48
- }
49
-
50
- function gatewayCallMethod(callIndex = 0) {
51
- return gatewayCallAt(callIndex)[0];
52
- }
53
-
54
- function findApprovalEvent(
55
- params: EmbeddedRunAttemptParams,
56
- fields: {
57
- status?: string;
58
- approvalId?: string;
59
- command?: string;
60
- reason?: string;
61
- message?: string;
62
- },
63
- ) {
64
- const onAgentEvent = params.onAgentEvent as unknown as { mock?: { calls?: unknown[][] } };
65
- const calls = onAgentEvent.mock?.calls;
66
- if (!Array.isArray(calls)) {
67
- throw new Error("Expected onAgentEvent mock calls");
68
- }
69
- for (const call of calls) {
70
- const event = requireRecord(call[0], "agent event");
71
- if (event.stream !== "approval") {
72
- continue;
73
- }
74
- const data = requireRecord(event.data, "approval event data");
75
- if (
76
- (!fields.status || data.status === fields.status) &&
77
- (!fields.approvalId || data.approvalId === fields.approvalId) &&
78
- (!fields.command || data.command === fields.command) &&
79
- (!fields.reason || data.reason === fields.reason) &&
80
- (!fields.message || data.message === fields.message)
81
- ) {
82
- return data;
83
- }
84
- }
85
- throw new Error(`Expected approval event ${JSON.stringify(fields)}`);
86
- }
87
-
88
- function createParams(): EmbeddedRunAttemptParams {
89
- return {
90
- sessionKey: "agent:main:session-1",
91
- agentId: "main",
92
- messageChannel: "telegram",
93
- currentChannelId: "chat-1",
94
- agentAccountId: "default",
95
- currentThreadTs: "thread-ts",
96
- onAgentEvent: vi.fn(),
97
- } as unknown as EmbeddedRunAttemptParams;
98
- }
99
-
100
- describe("Codex app-server approval bridge", () => {
101
- beforeEach(() => {
102
- mockCallGatewayTool.mockReset();
103
- mockHasNativeHookRelayInvocation.mockReset();
104
- mockHasNativeHookRelayInvocation.mockReturnValue(false);
105
- mockInvokeNativeHookRelay.mockReset();
106
- mockRunBeforeToolCallHook.mockReset();
107
- mockRunBeforeToolCallHook.mockImplementation(async ({ params }) => ({
108
- blocked: false,
109
- params,
110
- }));
111
- });
112
-
113
- it("routes command approvals through plugin approvals and accepts allowed commands", async () => {
114
- const params = createParams();
115
- mockCallGatewayTool
116
- .mockResolvedValueOnce({ id: "plugin:approval-1", status: "accepted" })
117
- .mockResolvedValueOnce({ id: "plugin:approval-1", decision: "allow-once" });
118
-
119
- const result = await handleCodexAppServerApprovalRequest({
120
- method: "item/commandExecution/requestApproval",
121
- requestParams: {
122
- threadId: "thread-1",
123
- turnId: "turn-1",
124
- itemId: "cmd-1",
125
- command: "pnpm test extensions/codex/src/app-server",
126
- },
127
- paramsForRun: params,
128
- threadId: "thread-1",
129
- turnId: "turn-1",
130
- });
131
-
132
- expect(result).toEqual({ decision: "accept" });
133
- expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
134
- "plugin.approval.request",
135
- "plugin.approval.waitDecision",
136
- ]);
137
- expect(gatewayCallMethod()).toBe("plugin.approval.request");
138
- expect(typeof gatewayCallAt(0)[1]).toBe("object");
139
- const requestPayload = gatewayRequestPayload();
140
- expect(requestPayload.pluginId).toBe("klaw-codex-app-server");
141
- expect(requestPayload.title).toBe("Codex app-server command approval");
142
- expect(requestPayload.twoPhase).toBe(true);
143
- expect(requestPayload.turnSourceChannel).toBe("telegram");
144
- expect(requestPayload.turnSourceTo).toBe("chat-1");
145
- expect(gatewayCallOptions()).toEqual({ expectFinal: false });
146
- expect(mockRunBeforeToolCallHook).toHaveBeenCalledWith({
147
- toolName: "exec",
148
- params: {
149
- command: "pnpm test extensions/codex/src/app-server",
150
- approval: {
151
- threadId: "thread-1",
152
- turnId: "turn-1",
153
- itemId: "cmd-1",
154
- command: "pnpm test extensions/codex/src/app-server",
155
- },
156
- },
157
- toolCallId: "cmd-1",
158
- approvalMode: "report",
159
- signal: undefined,
160
- ctx: {
161
- agentId: "main",
162
- sessionKey: "agent:main:session-1",
163
- channelId: "chat-1",
164
- },
165
- });
166
- findApprovalEvent(params, { status: "pending", approvalId: "plugin:approval-1" });
167
- findApprovalEvent(params, { status: "approved", approvalId: "plugin:approval-1" });
168
- });
169
-
170
- it("normalizes prefixed channel targets for Klaw tool policy context", async () => {
171
- const params = createParams();
172
- params.messageChannel = "telegram";
173
- params.messageProvider = "telegram";
174
- params.currentChannelId = "telegram:-100123";
175
- mockCallGatewayTool
176
- .mockResolvedValueOnce({ id: "plugin:approval-prefixed", status: "accepted" })
177
- .mockResolvedValueOnce({ id: "plugin:approval-prefixed", decision: "allow-once" });
178
-
179
- await handleCodexAppServerApprovalRequest({
180
- method: "item/commandExecution/requestApproval",
181
- requestParams: {
182
- threadId: "thread-1",
183
- turnId: "turn-1",
184
- itemId: "cmd-prefixed",
185
- command: "pnpm test extensions/codex/src/app-server",
186
- },
187
- paramsForRun: params,
188
- threadId: "thread-1",
189
- turnId: "turn-1",
190
- });
191
-
192
- expect(mockRunBeforeToolCallHook).toHaveBeenCalledWith(
193
- expect.objectContaining({
194
- ctx: expect.objectContaining({
195
- channelId: "-100123",
196
- }),
197
- }),
198
- );
199
- expect(gatewayRequestPayload().turnSourceTo).toBe("telegram:-100123");
200
- });
201
-
202
- it("denies command approvals before prompting when Klaw tool policy blocks", async () => {
203
- const params = createParams();
204
- mockRunBeforeToolCallHook.mockResolvedValueOnce({
205
- blocked: true,
206
- kind: "veto",
207
- deniedReason: "plugin-before-tool-call",
208
- reason: "blocked by policy",
209
- });
210
-
211
- const result = await handleCodexAppServerApprovalRequest({
212
- method: "item/commandExecution/requestApproval",
213
- requestParams: {
214
- threadId: "thread-1",
215
- turnId: "turn-1",
216
- itemId: "cmd-blocked",
217
- command: "cat /tmp/private_key",
218
- },
219
- paramsForRun: params,
220
- threadId: "thread-1",
221
- turnId: "turn-1",
222
- });
223
-
224
- expect(result).toEqual({ decision: "decline" });
225
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
226
- findApprovalEvent(params, { status: "denied" });
227
- });
228
-
229
- it("routes command approvals through the active native hook relay before prompting", async () => {
230
- const params = createParams();
231
- mockInvokeNativeHookRelay.mockResolvedValueOnce({
232
- stdout: `${JSON.stringify({
233
- hookSpecificOutput: {
234
- hookEventName: "PreToolUse",
235
- permissionDecision: "deny",
236
- permissionDecisionReason: "blocked by native relay",
237
- },
238
- })}\n`,
239
- stderr: "",
240
- exitCode: 0,
241
- });
242
-
243
- const result = await handleCodexAppServerApprovalRequest({
244
- method: "item/commandExecution/requestApproval",
245
- requestParams: {
246
- threadId: "thread-1",
247
- turnId: "turn-1",
248
- itemId: "cmd-native-relay",
249
- command: "cat /tmp/private_key",
250
- cwd: "/workspace",
251
- },
252
- paramsForRun: params,
253
- threadId: "thread-1",
254
- turnId: "turn-1",
255
- nativeHookRelay: {
256
- relayId: "relay-1",
257
- allowedEvents: ["pre_tool_use"],
258
- },
259
- });
260
-
261
- expect(result).toEqual({ decision: "decline" });
262
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
263
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
264
- expect(mockInvokeNativeHookRelay).toHaveBeenCalledWith({
265
- provider: "codex",
266
- relayId: "relay-1",
267
- event: "pre_tool_use",
268
- rawPayload: {
269
- hook_event_name: "PreToolUse",
270
- klaw_approval_mode: "report",
271
- tool_name: "exec_command",
272
- tool_use_id: "cmd-native-relay",
273
- cwd: "/workspace",
274
- turn_id: "turn-1",
275
- tool_input: {
276
- command: "cat /tmp/private_key",
277
- cwd: "/workspace",
278
- approval: {
279
- threadId: "thread-1",
280
- turnId: "turn-1",
281
- itemId: "cmd-native-relay",
282
- command: "cat /tmp/private_key",
283
- cwd: "/workspace",
284
- },
285
- cmd: "cat /tmp/private_key",
286
- },
287
- },
288
- });
289
- findApprovalEvent(params, {
290
- status: "denied",
291
- message: "blocked by native relay",
292
- });
293
- });
294
-
295
- it("falls through to plugin approval when the native hook relay has no decision", async () => {
296
- const params = createParams();
297
- mockInvokeNativeHookRelay.mockResolvedValueOnce({
298
- stdout: "",
299
- stderr: "",
300
- exitCode: 0,
301
- });
302
- mockCallGatewayTool
303
- .mockResolvedValueOnce({ id: "plugin:approval-native-noop", status: "accepted" })
304
- .mockResolvedValueOnce({ id: "plugin:approval-native-noop", decision: "allow-once" });
305
-
306
- const result = await handleCodexAppServerApprovalRequest({
307
- method: "item/commandExecution/requestApproval",
308
- requestParams: {
309
- threadId: "thread-1",
310
- turnId: "turn-1",
311
- itemId: "cmd-native-relay-noop",
312
- command: "pnpm test extensions/codex/src/app-server",
313
- cwd: "/workspace",
314
- },
315
- paramsForRun: params,
316
- threadId: "thread-1",
317
- turnId: "turn-1",
318
- nativeHookRelay: {
319
- relayId: "relay-1",
320
- allowedEvents: ["pre_tool_use"],
321
- },
322
- });
323
-
324
- expect(result).toEqual({ decision: "accept" });
325
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
326
- expect(mockInvokeNativeHookRelay).toHaveBeenCalledTimes(1);
327
- expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
328
- "plugin.approval.request",
329
- "plugin.approval.waitDecision",
330
- ]);
331
- findApprovalEvent(params, {
332
- status: "pending",
333
- approvalId: "plugin:approval-native-noop",
334
- });
335
- findApprovalEvent(params, {
336
- status: "approved",
337
- approvalId: "plugin:approval-native-noop",
338
- });
339
- });
340
-
341
- it("does not invoke the app-server relay when native PreToolUse already ran", async () => {
342
- const params = createParams();
343
- mockHasNativeHookRelayInvocation.mockReturnValueOnce(true);
344
- mockCallGatewayTool
345
- .mockResolvedValueOnce({ id: "plugin:approval-native-observed", status: "accepted" })
346
- .mockResolvedValueOnce({ id: "plugin:approval-native-observed", decision: "allow-once" });
347
-
348
- const result = await handleCodexAppServerApprovalRequest({
349
- method: "item/commandExecution/requestApproval",
350
- requestParams: {
351
- threadId: "thread-1",
352
- turnId: "turn-1",
353
- itemId: "cmd-native-relay-observed",
354
- command: "pnpm test extensions/codex/src/app-server",
355
- cwd: "/workspace",
356
- },
357
- paramsForRun: params,
358
- threadId: "thread-1",
359
- turnId: "turn-1",
360
- nativeHookRelay: {
361
- relayId: "relay-1",
362
- allowedEvents: ["pre_tool_use"],
363
- },
364
- });
365
-
366
- expect(result).toEqual({ decision: "accept" });
367
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
368
- expect(mockHasNativeHookRelayInvocation).toHaveBeenCalledWith({
369
- relayId: "relay-1",
370
- event: "pre_tool_use",
371
- toolUseId: "cmd-native-relay-observed",
372
- });
373
- expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
374
- "plugin.approval.request",
375
- "plugin.approval.waitDecision",
376
- ]);
377
- });
378
-
379
- it("fails closed when the native hook relay returns unreadable approval output", async () => {
380
- const params = createParams();
381
- mockInvokeNativeHookRelay.mockResolvedValueOnce({
382
- stdout: "not-json",
383
- stderr: "",
384
- exitCode: 0,
385
- });
386
-
387
- const result = await handleCodexAppServerApprovalRequest({
388
- method: "item/commandExecution/requestApproval",
389
- requestParams: {
390
- threadId: "thread-1",
391
- turnId: "turn-1",
392
- itemId: "cmd-native-relay-unreadable",
393
- command: "pnpm test extensions/codex/src/app-server",
394
- cwd: "/workspace",
395
- },
396
- paramsForRun: params,
397
- threadId: "thread-1",
398
- turnId: "turn-1",
399
- nativeHookRelay: {
400
- relayId: "relay-1",
401
- allowedEvents: ["pre_tool_use"],
402
- },
403
- });
404
-
405
- expect(result).toEqual({ decision: "decline" });
406
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
407
- expect(mockInvokeNativeHookRelay).toHaveBeenCalledTimes(1);
408
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
409
- findApprovalEvent(params, {
410
- status: "denied",
411
- message: "Klaw native hook relay returned an unreadable Codex app-server approval result.",
412
- });
413
- });
414
-
415
- it("fails closed when the native hook relay returns a non-deny decision", async () => {
416
- const params = createParams();
417
- mockInvokeNativeHookRelay.mockResolvedValueOnce({
418
- stdout:
419
- JSON.stringify({
420
- hookSpecificOutput: {
421
- hookEventName: "PreToolUse",
422
- permissionDecision: "allow",
423
- },
424
- }) + "\n",
425
- stderr: "",
426
- exitCode: 0,
427
- });
428
-
429
- const result = await handleCodexAppServerApprovalRequest({
430
- method: "item/commandExecution/requestApproval",
431
- requestParams: {
432
- threadId: "thread-1",
433
- turnId: "turn-1",
434
- itemId: "cmd-native-relay-allow",
435
- command: "pnpm test extensions/codex/src/app-server",
436
- cwd: "/workspace",
437
- },
438
- paramsForRun: params,
439
- threadId: "thread-1",
440
- turnId: "turn-1",
441
- nativeHookRelay: {
442
- relayId: "relay-1",
443
- allowedEvents: ["pre_tool_use"],
444
- },
445
- });
446
-
447
- expect(result).toEqual({ decision: "decline" });
448
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
449
- expect(mockInvokeNativeHookRelay).toHaveBeenCalledTimes(1);
450
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
451
- findApprovalEvent(params, {
452
- status: "denied",
453
- message: "Klaw native hook relay returned a non-deny Codex app-server approval decision.",
454
- });
455
- });
456
-
457
- it("fails closed when the native hook relay exits non-zero", async () => {
458
- const params = createParams();
459
- mockInvokeNativeHookRelay.mockResolvedValueOnce({
460
- stdout: "ignored stdout",
461
- stderr: "blocked from stderr",
462
- exitCode: 1,
463
- });
464
-
465
- const result = await handleCodexAppServerApprovalRequest({
466
- method: "item/commandExecution/requestApproval",
467
- requestParams: {
468
- threadId: "thread-1",
469
- turnId: "turn-1",
470
- itemId: "cmd-native-relay-exit",
471
- command: "pnpm test extensions/codex/src/app-server",
472
- cwd: "/workspace",
473
- },
474
- paramsForRun: params,
475
- threadId: "thread-1",
476
- turnId: "turn-1",
477
- nativeHookRelay: {
478
- relayId: "relay-1",
479
- allowedEvents: ["pre_tool_use"],
480
- },
481
- });
482
-
483
- expect(result).toEqual({ decision: "decline" });
484
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
485
- expect(mockInvokeNativeHookRelay).toHaveBeenCalledTimes(1);
486
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
487
- findApprovalEvent(params, {
488
- status: "denied",
489
- message: "blocked from stderr",
490
- });
491
- });
492
-
493
- it("fails closed when the expected native hook relay cannot be invoked", async () => {
494
- const params = createParams();
495
- mockInvokeNativeHookRelay.mockRejectedValueOnce(new Error("native hook relay not found"));
496
-
497
- const result = await handleCodexAppServerApprovalRequest({
498
- method: "item/commandExecution/requestApproval",
499
- requestParams: {
500
- threadId: "thread-1",
501
- turnId: "turn-1",
502
- itemId: "cmd-native-relay-missing",
503
- command: "cat /tmp/private_key",
504
- },
505
- paramsForRun: params,
506
- threadId: "thread-1",
507
- turnId: "turn-1",
508
- nativeHookRelay: {
509
- relayId: "relay-missing",
510
- allowedEvents: ["pre_tool_use"],
511
- },
512
- });
513
-
514
- expect(result).toEqual({ decision: "decline" });
515
- expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
516
- expect(mockInvokeNativeHookRelay).toHaveBeenCalledTimes(1);
517
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
518
- findApprovalEvent(params, {
519
- status: "denied",
520
- message:
521
- "Klaw native hook relay unavailable for Codex app-server approval: native hook relay not found",
522
- });
523
- });
524
-
525
- it("keeps non-command approvals on the app-server approval route when a native relay is registered", async () => {
526
- const params = createParams();
527
- mockCallGatewayTool
528
- .mockResolvedValueOnce({ id: "plugin:file-approval", status: "accepted" })
529
- .mockResolvedValueOnce({ id: "plugin:file-approval", decision: "allow-once" })
530
- .mockResolvedValueOnce({ id: "plugin:permission-approval", status: "accepted" })
531
- .mockResolvedValueOnce({ id: "plugin:permission-approval", decision: "deny" });
532
- const nativeHookRelay = {
533
- relayId: "relay-1",
534
- allowedEvents: ["pre_tool_use" as const],
535
- };
536
-
537
- await handleCodexAppServerApprovalRequest({
538
- method: "item/fileChange/requestApproval",
539
- requestParams: {
540
- threadId: "thread-1",
541
- turnId: "turn-1",
542
- itemId: "patch-native-relay-registered",
543
- reason: "needs write access",
544
- },
545
- paramsForRun: params,
546
- threadId: "thread-1",
547
- turnId: "turn-1",
548
- nativeHookRelay,
549
- });
550
- await handleCodexAppServerApprovalRequest({
551
- method: "item/permissions/requestApproval",
552
- requestParams: {
553
- threadId: "thread-1",
554
- turnId: "turn-1",
555
- itemId: "permission-native-relay-registered",
556
- permissions: {
557
- network: { allowHosts: ["example.com"] },
558
- },
559
- },
560
- paramsForRun: params,
561
- threadId: "thread-1",
562
- turnId: "turn-1",
563
- nativeHookRelay,
564
- });
565
-
566
- expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
567
- "plugin.approval.request",
568
- "plugin.approval.waitDecision",
569
- "plugin.approval.request",
570
- "plugin.approval.waitDecision",
571
- ]);
572
- });
573
-
574
- it("denies command approvals when Klaw tool policy rewrites params", async () => {
575
- const params = createParams();
576
- mockRunBeforeToolCallHook.mockResolvedValueOnce({
577
- blocked: false,
578
- params: {
579
- command: "echo rewritten",
580
- approval: {
581
- threadId: "thread-1",
582
- turnId: "turn-1",
583
- itemId: "cmd-rewritten",
584
- command: "echo rewritten",
585
- },
586
- },
587
- });
588
-
589
- const result = await handleCodexAppServerApprovalRequest({
590
- method: "item/commandExecution/requestApproval",
591
- requestParams: {
592
- threadId: "thread-1",
593
- turnId: "turn-1",
594
- itemId: "cmd-rewritten",
595
- command: "cat /tmp/private_key",
596
- },
597
- paramsForRun: params,
598
- threadId: "thread-1",
599
- turnId: "turn-1",
600
- });
601
-
602
- expect(result).toEqual({ decision: "decline" });
603
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
604
- findApprovalEvent(params, {
605
- status: "denied",
606
- message:
607
- "Klaw tool policy rewrote Codex app-server approval params; refusing original request.",
608
- });
609
- });
610
-
611
- it("denies command approvals when Klaw tool policy requires approval", async () => {
612
- const params = createParams();
613
- mockRunBeforeToolCallHook.mockResolvedValueOnce({
614
- blocked: true,
615
- kind: "failure",
616
- deniedReason: "plugin-approval",
617
- reason: "Plugin approval required",
618
- });
619
- const result = await handleCodexAppServerApprovalRequest({
620
- method: "item/commandExecution/requestApproval",
621
- requestParams: {
622
- threadId: "thread-1",
623
- turnId: "turn-1",
624
- itemId: "cmd-needs-approval",
625
- command: "pnpm test",
626
- },
627
- paramsForRun: params,
628
- threadId: "thread-1",
629
- turnId: "turn-1",
630
- });
631
-
632
- expect(result).toEqual({ decision: "decline" });
633
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
634
- findApprovalEvent(params, {
635
- status: "denied",
636
- message: "Plugin approval required",
637
- });
638
- });
639
-
640
- it("describes command approvals from parsed command actions when available", async () => {
641
- const params = createParams();
642
- mockCallGatewayTool
643
- .mockResolvedValueOnce({ id: "plugin:approval-actions", status: "accepted" })
644
- .mockResolvedValueOnce({ id: "plugin:approval-actions", decision: "allow-once" });
645
-
646
- await handleCodexAppServerApprovalRequest({
647
- method: "item/commandExecution/requestApproval",
648
- requestParams: {
649
- threadId: "thread-1",
650
- turnId: "turn-1",
651
- itemId: "cmd-actions",
652
- command: "bash -lc 'pnpm test extensions/codex'",
653
- commandActions: [{ command: "pnpm test extensions/codex" }],
654
- },
655
- paramsForRun: params,
656
- threadId: "thread-1",
657
- turnId: "turn-1",
658
- });
659
-
660
- const requestPayload = gatewayRequestPayload();
661
- expect(String(requestPayload.description)).toContain("Command: pnpm test extensions/codex");
662
- expect(String(requestPayload.description)).not.toContain("bash -lc");
663
- expect(mockRunBeforeToolCallHook.mock.calls.at(0)?.[0]).toMatchObject({
664
- toolName: "exec",
665
- params: {
666
- command: "bash -lc 'pnpm test extensions/codex'",
667
- },
668
- });
669
- findApprovalEvent(params, { command: "pnpm test extensions/codex" });
670
- });
671
-
672
- it("describes command approval permission and policy amendments", async () => {
673
- const params = createParams();
674
- mockCallGatewayTool
675
- .mockResolvedValueOnce({ id: "plugin:approval-command-permissions", status: "accepted" })
676
- .mockResolvedValueOnce({
677
- id: "plugin:approval-command-permissions",
678
- decision: "allow-always",
679
- });
680
-
681
- const result = await handleCodexAppServerApprovalRequest({
682
- method: "item/commandExecution/requestApproval",
683
- requestParams: {
684
- threadId: "thread-1",
685
- turnId: "turn-1",
686
- itemId: "cmd-permissions",
687
- command: "npm install",
688
- additionalPermissions: {
689
- network: { enabled: true },
690
- fileSystem: {
691
- write: ["/"],
692
- },
693
- },
694
- proposedExecpolicyAmendment: ["npm install"],
695
- proposedNetworkPolicyAmendments: [{ host: "registry.npmjs.org", action: "allow" }],
696
- },
697
- paramsForRun: params,
698
- threadId: "thread-1",
699
- turnId: "turn-1",
700
- });
701
-
702
- expect(result).toEqual({ decision: "acceptForSession" });
703
- const description = String(gatewayRequestPayload().description);
704
- expect(description).toContain("Command: npm install");
705
- expect(description).toContain("Additional permissions: network, fileSystem");
706
- expect(description).toContain("High-risk targets: network access, filesystem root");
707
- expect(description).toContain("Network enabled: true");
708
- expect(description).toContain("File system write: /");
709
- expect(description).toContain("Proposed exec policy: npm install");
710
- expect(description).toContain("Proposed network policy: allow registry.npmjs.org");
711
- });
712
-
713
- it("keeps command approval permission details visible after long command previews", async () => {
714
- const params = createParams();
715
- mockCallGatewayTool
716
- .mockResolvedValueOnce({ id: "plugin:approval-long-command-permissions", status: "accepted" })
717
- .mockResolvedValueOnce({
718
- id: "plugin:approval-long-command-permissions",
719
- decision: "allow-always",
720
- });
721
-
722
- await handleCodexAppServerApprovalRequest({
723
- method: "item/commandExecution/requestApproval",
724
- requestParams: {
725
- threadId: "thread-1",
726
- turnId: "turn-1",
727
- itemId: "cmd-long-permissions",
728
- command: `${"npm install ".repeat(500)} --unsafe-perm`,
729
- additionalPermissions: {
730
- network: { enabled: true },
731
- fileSystem: {
732
- write: ["/"],
733
- },
734
- },
735
- },
736
- paramsForRun: params,
737
- threadId: "thread-1",
738
- turnId: "turn-1",
739
- });
740
-
741
- const description = String(gatewayRequestPayload().description);
742
- expect(description).toContain("[preview truncated or unsafe content omitted]");
743
- expect(description).toContain("Additional permissions: network, fileSystem");
744
- expect(description).toContain("High-risk targets: network access, filesystem root");
745
- });
746
-
747
- it("sanitizes command previews before forwarding approval text and events", async () => {
748
- const params = createParams();
749
- mockCallGatewayTool
750
- .mockResolvedValueOnce({ id: "plugin:approval-sanitized-command", status: "accepted" })
751
- .mockResolvedValueOnce({ id: "plugin:approval-sanitized-command", decision: "allow-once" });
752
-
753
- await handleCodexAppServerApprovalRequest({
754
- method: "item/commandExecution/requestApproval",
755
- requestParams: {
756
- threadId: "thread-1",
757
- turnId: "turn-1",
758
- itemId: "cmd-sanitized",
759
- command: ["pnpm", "test\n--watch", "\u001b[31mextensions/codex/src/app-server\u001b[0m"],
760
- },
761
- paramsForRun: params,
762
- threadId: "thread-1",
763
- turnId: "turn-1",
764
- });
765
-
766
- expect(gatewayRequestPayload().description).toBe(
767
- "Command: pnpm test --watch extensions/codex/src/app-server\nSession: agent:main:session-1",
768
- );
769
- findApprovalEvent(params, {
770
- status: "pending",
771
- command: "pnpm test --watch extensions/codex/src/app-server",
772
- });
773
- });
774
-
775
- it("escapes command approval previews before forwarding approval text and events", async () => {
776
- const params = createParams();
777
- mockCallGatewayTool
778
- .mockResolvedValueOnce({ id: "plugin:approval-escaped-command", status: "accepted" })
779
- .mockResolvedValueOnce({ id: "plugin:approval-escaped-command", decision: "allow-once" });
780
-
781
- await handleCodexAppServerApprovalRequest({
782
- method: "item/commandExecution/requestApproval",
783
- requestParams: {
784
- threadId: "thread-1",
785
- turnId: "turn-1",
786
- itemId: "cmd-escaped",
787
- command: "printf '<@U123> [trusted](https://evil) @here'",
788
- },
789
- paramsForRun: params,
790
- threadId: "thread-1",
791
- turnId: "turn-1",
792
- });
793
-
794
- const description = String(gatewayRequestPayload().description);
795
- expect(description).toContain(
796
- "printf '&lt;\uff20U123&gt; \uff3btrusted\uff3d\uff08https://evil\uff09 \uff20here'",
797
- );
798
- expect(description).not.toContain("<@U123>");
799
- expect(description).not.toContain("[trusted](https://evil)");
800
- expect(description).not.toContain("@here");
801
- findApprovalEvent(params, {
802
- command: "printf '&lt;\uff20U123&gt; \uff3btrusted\uff3d\uff08https://evil\uff09 \uff20here'",
803
- });
804
- });
805
-
806
- it("preserves visible OSC-8 link labels in command previews", async () => {
807
- const params = createParams();
808
- mockCallGatewayTool
809
- .mockResolvedValueOnce({ id: "plugin:approval-osc", status: "accepted" })
810
- .mockResolvedValueOnce({ id: "plugin:approval-osc", decision: "allow-once" });
811
- const esc = "\u001b";
812
-
813
- await handleCodexAppServerApprovalRequest({
814
- method: "item/commandExecution/requestApproval",
815
- requestParams: {
816
- threadId: "thread-1",
817
- turnId: "turn-1",
818
- itemId: "cmd-osc",
819
- command: `prefix ${esc}]8;;https://example.com${esc}\\VISIBLE${esc}]8;;${esc}\\ suffix`,
820
- },
821
- paramsForRun: params,
822
- threadId: "thread-1",
823
- turnId: "turn-1",
824
- });
825
-
826
- expect(gatewayRequestPayload().description).toBe(
827
- "Command: prefix VISIBLE suffix\nSession: agent:main:session-1",
828
- );
829
- findApprovalEvent(params, { command: "prefix VISIBLE suffix" });
830
- });
831
-
832
- it("strips bidi and invisible formatting controls from command previews", async () => {
833
- const params = createParams();
834
- mockCallGatewayTool
835
- .mockResolvedValueOnce({ id: "plugin:approval-bidi", status: "accepted" })
836
- .mockResolvedValueOnce({ id: "plugin:approval-bidi", decision: "allow-once" });
837
-
838
- await handleCodexAppServerApprovalRequest({
839
- method: "item/commandExecution/requestApproval",
840
- requestParams: {
841
- threadId: "thread-1",
842
- turnId: "turn-1",
843
- itemId: "cmd-bidi",
844
- command: "echo safe\u202e cod.exe\u2066 hidden\u2069 \ufeffdone\u{e0100}",
845
- },
846
- paramsForRun: params,
847
- threadId: "thread-1",
848
- turnId: "turn-1",
849
- });
850
-
851
- expect(gatewayRequestPayload().description).toBe(
852
- "Command: echo safe cod.exe hidden done\nSession: agent:main:session-1",
853
- );
854
- findApprovalEvent(params, { command: "echo safe cod.exe hidden done" });
855
- });
856
-
857
- it("marks oversized unsafe command previews as omitted", async () => {
858
- const params = createParams();
859
- mockCallGatewayTool
860
- .mockResolvedValueOnce({ id: "plugin:approval-omitted-command", status: "accepted" })
861
- .mockResolvedValueOnce({ id: "plugin:approval-omitted-command", decision: "allow-once" });
862
- const esc = "\u001b";
863
- const oversizedPrefix = `${esc}]8;;https://example.com${esc}\\`.repeat(300);
864
-
865
- await handleCodexAppServerApprovalRequest({
866
- method: "item/commandExecution/requestApproval",
867
- requestParams: {
868
- threadId: "thread-1",
869
- turnId: "turn-1",
870
- itemId: "cmd-omitted",
871
- command: [oversizedPrefix, "TAIL"],
872
- },
873
- paramsForRun: params,
874
- threadId: "thread-1",
875
- turnId: "turn-1",
876
- });
877
-
878
- expect(gatewayRequestPayload().description).toBe(
879
- "Command: [preview truncated or unsafe content omitted]\nSession: agent:main:session-1",
880
- );
881
- const omittedEvent = findApprovalEvent(params, {});
882
- expect(omittedEvent.commandPreviewOmitted).toBe(true);
883
- });
884
-
885
- it("marks clipped command previews even when a safe prefix remains", async () => {
886
- const params = createParams();
887
- mockCallGatewayTool
888
- .mockResolvedValueOnce({ id: "plugin:approval-clipped-command", status: "accepted" })
889
- .mockResolvedValueOnce({ id: "plugin:approval-clipped-command", decision: "allow-once" });
890
-
891
- await handleCodexAppServerApprovalRequest({
892
- method: "item/commandExecution/requestApproval",
893
- requestParams: {
894
- threadId: "thread-1",
895
- turnId: "turn-1",
896
- itemId: "cmd-clipped",
897
- command: `${"a".repeat(5000)} tail`,
898
- },
899
- paramsForRun: params,
900
- threadId: "thread-1",
901
- turnId: "turn-1",
902
- });
903
-
904
- const description = String(gatewayRequestPayload().description);
905
- expect(description).toContain("[preview truncated or unsafe content omitted]");
906
- const omittedEvent = findApprovalEvent(params, {});
907
- expect(omittedEvent.commandPreviewOmitted).toBe(true);
908
- });
909
-
910
- it("does not trust request-time decisions for two-phase command approvals", async () => {
911
- const params = createParams();
912
- mockCallGatewayTool
913
- .mockResolvedValueOnce({
914
- id: "plugin:approval-untrusted",
915
- status: "accepted",
916
- decision: "allow-always",
917
- })
918
- .mockResolvedValueOnce({ id: "plugin:approval-untrusted", decision: "deny" });
919
-
920
- const result = await handleCodexAppServerApprovalRequest({
921
- method: "item/commandExecution/requestApproval",
922
- requestParams: {
923
- threadId: "thread-1",
924
- turnId: "turn-1",
925
- itemId: "cmd-untrusted",
926
- command: "pnpm test",
927
- },
928
- paramsForRun: params,
929
- threadId: "thread-1",
930
- turnId: "turn-1",
931
- });
932
-
933
- expect(result).toEqual({ decision: "decline" });
934
- expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
935
- "plugin.approval.request",
936
- "plugin.approval.waitDecision",
937
- ]);
938
- findApprovalEvent(params, {
939
- status: "denied",
940
- approvalId: "plugin:approval-untrusted",
941
- });
942
- });
943
-
944
- it("only treats own null data-property request decisions as no-route", async () => {
945
- const params = createParams();
946
- const inheritedDecisionResult = Object.assign(Object.create({ decision: null }), {
947
- id: "plugin:approval-inherited",
948
- status: "accepted",
949
- });
950
- mockCallGatewayTool
951
- .mockResolvedValueOnce(inheritedDecisionResult)
952
- .mockResolvedValueOnce({ id: "plugin:approval-inherited", decision: "allow-once" });
953
-
954
- const result = await handleCodexAppServerApprovalRequest({
955
- method: "item/commandExecution/requestApproval",
956
- requestParams: {
957
- threadId: "thread-1",
958
- turnId: "turn-1",
959
- itemId: "cmd-inherited",
960
- command: "pnpm test",
961
- },
962
- paramsForRun: params,
963
- threadId: "thread-1",
964
- turnId: "turn-1",
965
- });
966
-
967
- expect(result).toEqual({ decision: "accept" });
968
- expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
969
- "plugin.approval.request",
970
- "plugin.approval.waitDecision",
971
- ]);
972
- });
973
-
974
- it("does not invoke request-time decision accessors", async () => {
975
- const params = createParams();
976
- const requestResult = {
977
- id: "plugin:approval-accessor",
978
- status: "accepted",
979
- get decision() {
980
- throw new Error("decision getter must not run");
981
- },
982
- };
983
- mockCallGatewayTool
984
- .mockResolvedValueOnce(requestResult)
985
- .mockResolvedValueOnce({ id: "plugin:approval-accessor", decision: "allow-once" });
986
-
987
- const result = await handleCodexAppServerApprovalRequest({
988
- method: "item/commandExecution/requestApproval",
989
- requestParams: {
990
- threadId: "thread-1",
991
- turnId: "turn-1",
992
- itemId: "cmd-accessor",
993
- command: "pnpm test",
994
- },
995
- paramsForRun: params,
996
- threadId: "thread-1",
997
- turnId: "turn-1",
998
- });
999
-
1000
- expect(result).toEqual({ decision: "accept" });
1001
- });
1002
-
1003
- it("does not fail when request-time decision descriptors throw", async () => {
1004
- const params = createParams();
1005
- const requestResult = new Proxy(
1006
- { id: "plugin:approval-proxy", status: "accepted" },
1007
- {
1008
- getOwnPropertyDescriptor(target, property) {
1009
- if (property === "decision") {
1010
- throw new Error("descriptor trap must not fail approval");
1011
- }
1012
- return Reflect.getOwnPropertyDescriptor(target, property);
1013
- },
1014
- },
1015
- );
1016
- mockCallGatewayTool
1017
- .mockResolvedValueOnce(requestResult)
1018
- .mockResolvedValueOnce({ id: "plugin:approval-proxy", decision: "allow-once" });
1019
-
1020
- const result = await handleCodexAppServerApprovalRequest({
1021
- method: "item/commandExecution/requestApproval",
1022
- requestParams: {
1023
- threadId: "thread-1",
1024
- turnId: "turn-1",
1025
- itemId: "cmd-proxy",
1026
- command: "pnpm test",
1027
- },
1028
- paramsForRun: params,
1029
- threadId: "thread-1",
1030
- turnId: "turn-1",
1031
- });
1032
-
1033
- expect(result).toEqual({ decision: "accept" });
1034
- });
1035
-
1036
- it("fails closed when no approval route is available", async () => {
1037
- const params = createParams();
1038
- mockCallGatewayTool.mockResolvedValueOnce({
1039
- id: "plugin:approval-2",
1040
- decision: null,
1041
- });
1042
-
1043
- const result = await handleCodexAppServerApprovalRequest({
1044
- method: "item/fileChange/requestApproval",
1045
- requestParams: {
1046
- threadId: "thread-1",
1047
- turnId: "turn-1",
1048
- itemId: "patch-1",
1049
- reason: "needs write access",
1050
- },
1051
- paramsForRun: params,
1052
- threadId: "thread-1",
1053
- turnId: "turn-1",
1054
- });
1055
-
1056
- expect(result).toEqual({ decision: "decline" });
1057
- expect(mockCallGatewayTool).toHaveBeenCalledTimes(1);
1058
- findApprovalEvent(params, { status: "unavailable", reason: "needs write access" });
1059
- });
1060
-
1061
- it("sanitizes reason previews before forwarding approval text and events", async () => {
1062
- const params = createParams();
1063
- mockCallGatewayTool.mockResolvedValueOnce({
1064
- id: "plugin:approval-sanitized-reason",
1065
- decision: null,
1066
- });
1067
-
1068
- await handleCodexAppServerApprovalRequest({
1069
- method: "item/fileChange/requestApproval",
1070
- requestParams: {
1071
- threadId: "thread-1",
1072
- turnId: "turn-1",
1073
- itemId: "patch-sanitized",
1074
- reason: "needs write access\nfor \u001b[31m/tmp\u001b[0m\tplease",
1075
- },
1076
- paramsForRun: params,
1077
- threadId: "thread-1",
1078
- turnId: "turn-1",
1079
- });
1080
-
1081
- expect(gatewayRequestPayload().description).toBe(
1082
- "Reason: needs write access for /tmp please\nSession: agent:main:session-1",
1083
- );
1084
- findApprovalEvent(params, {
1085
- status: "unavailable",
1086
- reason: "needs write access for /tmp please",
1087
- });
1088
- });
1089
-
1090
- it("fails closed for unsupported native approval methods without requesting plugin approval", async () => {
1091
- const params = createParams();
1092
-
1093
- const result = await handleCodexAppServerApprovalRequest({
1094
- method: "future/requestApproval",
1095
- requestParams: {
1096
- threadId: "thread-1",
1097
- turnId: "turn-1",
1098
- itemId: "future-1",
1099
- },
1100
- paramsForRun: params,
1101
- threadId: "thread-1",
1102
- turnId: "turn-1",
1103
- });
1104
-
1105
- expect(result).toEqual({
1106
- decision: "decline",
1107
- reason: "Klaw codex app-server bridge does not grant native approvals yet.",
1108
- });
1109
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
1110
- expect(params.onAgentEvent).not.toHaveBeenCalled();
1111
- });
1112
- it("labels permission approvals explicitly with permission detail", async () => {
1113
- const params = createParams();
1114
- mockCallGatewayTool
1115
- .mockResolvedValueOnce({ id: "plugin:approval-3", status: "accepted" })
1116
- .mockResolvedValueOnce({ id: "plugin:approval-3", decision: "allow-once" });
1117
-
1118
- const result = await handleCodexAppServerApprovalRequest({
1119
- method: "item/permissions/requestApproval",
1120
- requestParams: {
1121
- threadId: "thread-1",
1122
- turnId: "turn-1",
1123
- itemId: "perm-1",
1124
- permissions: {
1125
- network: { allowHosts: ["example.com", "*.internal"] },
1126
- fileSystem: { roots: ["/"], writePaths: ["/home/simone"] },
1127
- },
1128
- },
1129
- paramsForRun: params,
1130
- threadId: "thread-1",
1131
- turnId: "turn-1",
1132
- });
1133
-
1134
- expect(result).toEqual({
1135
- permissions: {
1136
- network: { allowHosts: ["example.com", "*.internal"] },
1137
- fileSystem: { roots: ["/"], writePaths: ["/home/simone"] },
1138
- },
1139
- scope: "turn",
1140
- });
1141
- expect(gatewayCallMethod()).toBe("plugin.approval.request");
1142
- expect(typeof gatewayCallAt(0)[1]).toBe("object");
1143
- const requestPayload = gatewayRequestPayload();
1144
- expect(requestPayload.title).toBe("Codex app-server permission approval");
1145
- expect(requestPayload.toolName).toBe("codex_permission_approval");
1146
- const description = String(requestPayload.description);
1147
- expect(description).toContain("Permissions: network, fileSystem");
1148
- expect(gatewayCallOptions()).toEqual({ expectFinal: false });
1149
- expect(description).toContain("Network allowHosts: example.com, *.internal");
1150
- expect(description).toContain("File system roots: /; writePaths: ~");
1151
- expect(description).toContain(
1152
- "High-risk targets: wildcard hosts, private-network wildcards, filesystem root",
1153
- );
1154
- expect(description).not.toContain("agent:main:session-1");
1155
- });
1156
-
1157
- it("keeps permission detail bounded with truncated and compacted target samples", async () => {
1158
- const params = createParams();
1159
- mockCallGatewayTool
1160
- .mockResolvedValueOnce({ id: "plugin:approval-4", status: "accepted" })
1161
- .mockResolvedValueOnce({ id: "plugin:approval-4", decision: "allow-once" });
1162
-
1163
- await handleCodexAppServerApprovalRequest({
1164
- method: "item/permissions/requestApproval",
1165
- requestParams: {
1166
- threadId: "thread-1",
1167
- turnId: "turn-1",
1168
- itemId: "perm-2",
1169
- permissions: {
1170
- network: {
1171
- allowHosts: [
1172
- "https://secret-token@example.com/private",
1173
- "*.internal",
1174
- "very-long-service-name.example.corp",
1175
- "third.example.com",
1176
- ],
1177
- },
1178
- fileSystem: {
1179
- roots: ["/", "/workspace/project", "/Users/simone/Documents"],
1180
- readPaths: ["/Users/simone/.ssh/id_rsa", "/etc/hosts", "/var/log/system.log"],
1181
- writePaths: ["/tmp/output", "/var/log/app", "/home/simone/private"],
1182
- },
1183
- },
1184
- },
1185
- paramsForRun: params,
1186
- threadId: "thread-1",
1187
- turnId: "turn-1",
1188
- });
1189
-
1190
- const description = String(gatewayRequestPayload().description);
1191
- expect(description.length).toBeLessThanOrEqual(700);
1192
- expect(description).toContain("example.com");
1193
- expect(description).not.toContain("secret-token");
1194
- expect(description).not.toContain("simone");
1195
- expect(description).toContain("*.internal");
1196
- expect(description).toContain("/workspace/project");
1197
- expect(description).toContain("High-risk targets:");
1198
- expect(description).toContain("readPaths: ~/.ssh/id_rsa, /etc/hosts");
1199
- });
1200
-
1201
- it("describes current protocol network and filesystem permission grants", async () => {
1202
- const params = createParams();
1203
- mockCallGatewayTool
1204
- .mockResolvedValueOnce({ id: "plugin:approval-current-permissions", status: "accepted" })
1205
- .mockResolvedValueOnce({ id: "plugin:approval-current-permissions", decision: "allow-once" });
1206
-
1207
- const result = await handleCodexAppServerApprovalRequest({
1208
- method: "item/permissions/requestApproval",
1209
- requestParams: {
1210
- threadId: "thread-1",
1211
- turnId: "turn-1",
1212
- itemId: "perm-current",
1213
- permissions: {
1214
- network: { enabled: true },
1215
- fileSystem: {
1216
- read: ["/Users/simone/.ssh/id_rsa"],
1217
- write: ["/"],
1218
- entries: [
1219
- { path: "/workspace/project", access: "read" },
1220
- { path: "/tmp/output", access: "write" },
1221
- { path: "/ignored", access: "none" },
1222
- ],
1223
- },
1224
- },
1225
- },
1226
- paramsForRun: params,
1227
- threadId: "thread-1",
1228
- turnId: "turn-1",
1229
- });
1230
-
1231
- expect(result).toEqual({
1232
- permissions: {
1233
- network: { enabled: true },
1234
- fileSystem: {
1235
- read: ["/Users/simone/.ssh/id_rsa"],
1236
- write: ["/"],
1237
- entries: [
1238
- { path: "/workspace/project", access: "read" },
1239
- { path: "/tmp/output", access: "write" },
1240
- { path: "/ignored", access: "none" },
1241
- ],
1242
- },
1243
- },
1244
- scope: "turn",
1245
- });
1246
- const description = String(gatewayRequestPayload().description);
1247
- expect(description).toContain("Network enabled: true");
1248
- expect(description).toContain("File system read: ~/.ssh/id_rsa; write: /");
1249
- expect(description).toContain("entries: read /workspace/project, write /tmp/output (+1 more)");
1250
- expect(description).toContain("High-risk targets: network access, filesystem root");
1251
- });
1252
-
1253
- it("compacts Windows home paths in permission descriptions", async () => {
1254
- const params = createParams();
1255
- mockCallGatewayTool
1256
- .mockResolvedValueOnce({ id: "plugin:approval-windows-home", status: "accepted" })
1257
- .mockResolvedValueOnce({ id: "plugin:approval-windows-home", decision: "allow-once" });
1258
-
1259
- await handleCodexAppServerApprovalRequest({
1260
- method: "item/permissions/requestApproval",
1261
- requestParams: {
1262
- threadId: "thread-1",
1263
- turnId: "turn-1",
1264
- itemId: "perm-windows-home",
1265
- permissions: {
1266
- fileSystem: {
1267
- roots: ["C:/Users/alice"],
1268
- readPaths: ["C:\\Users\\alice\\.ssh\\id_rsa", "c:/users/bob/project"],
1269
- },
1270
- },
1271
- },
1272
- paramsForRun: params,
1273
- threadId: "thread-1",
1274
- turnId: "turn-1",
1275
- });
1276
-
1277
- const description = String(gatewayRequestPayload().description);
1278
- expect(description).toContain("File system roots: ~; readPaths: ~/.ssh/id_rsa, ~/project");
1279
- expect(description).not.toContain("High-risk targets");
1280
- });
1281
-
1282
- it("strips terminal and invisible controls from permission descriptions", async () => {
1283
- const params = createParams();
1284
- mockCallGatewayTool
1285
- .mockResolvedValueOnce({ id: "plugin:approval-permission-controls", status: "accepted" })
1286
- .mockResolvedValueOnce({ id: "plugin:approval-permission-controls", decision: "allow-once" });
1287
-
1288
- await handleCodexAppServerApprovalRequest({
1289
- method: "item/permissions/requestApproval",
1290
- requestParams: {
1291
- threadId: "thread-1",
1292
- turnId: "turn-1",
1293
- itemId: "perm-controls",
1294
- permissions: {
1295
- network: { allowHosts: ["exa\u009b31mmple.com", "safe\u202e.example.com"] },
1296
- fileSystem: { roots: ["/tmp/\u001b[31mproject\u001b[0m"] },
1297
- },
1298
- },
1299
- paramsForRun: params,
1300
- threadId: "thread-1",
1301
- turnId: "turn-1",
1302
- });
1303
-
1304
- const description = String(gatewayRequestPayload().description);
1305
- expect(description).toContain("example.com");
1306
- expect(description).toContain("safe .example.com");
1307
- expect(description).toContain("/tmp/project");
1308
- expect(description).not.toContain("\u009b");
1309
- expect(description).not.toContain("\u202e");
1310
- expect(description).not.toContain("\u001b");
1311
- });
1312
-
1313
- it("ignores approval requests that are missing explicit thread or turn ids", async () => {
1314
- const params = createParams();
1315
-
1316
- const result = await handleCodexAppServerApprovalRequest({
1317
- method: "item/commandExecution/requestApproval",
1318
- requestParams: {
1319
- itemId: "cmd-2",
1320
- command: "pnpm test",
1321
- },
1322
- paramsForRun: params,
1323
- threadId: "thread-1",
1324
- turnId: "turn-1",
1325
- });
1326
-
1327
- expect(result).toBeUndefined();
1328
- expect(mockCallGatewayTool).not.toHaveBeenCalled();
1329
- expect(params.onAgentEvent).not.toHaveBeenCalled();
1330
- });
1331
-
1332
- it("maps app-server approval response families separately", () => {
1333
- expect(
1334
- buildApprovalResponse(
1335
- "item/commandExecution/requestApproval",
1336
- { availableDecisions: ["accept"] },
1337
- "approved-session",
1338
- ),
1339
- ).toEqual({
1340
- decision: "accept",
1341
- });
1342
- expect(
1343
- buildApprovalResponse(
1344
- "item/commandExecution/requestApproval",
1345
- {
1346
- availableDecisions: [
1347
- "accept",
1348
- {
1349
- acceptWithExecpolicyAmendment: {
1350
- execpolicy_amendment: {
1351
- permissions: [{ permission: "allow", command: ["pnpm", "test"] }],
1352
- },
1353
- },
1354
- },
1355
- ],
1356
- },
1357
- "approved-session",
1358
- ),
1359
- ).toEqual({
1360
- decision: {
1361
- acceptWithExecpolicyAmendment: {
1362
- execpolicy_amendment: {
1363
- permissions: [{ permission: "allow", command: ["pnpm", "test"] }],
1364
- },
1365
- },
1366
- },
1367
- });
1368
- expect(
1369
- buildApprovalResponse(
1370
- "item/commandExecution/requestApproval",
1371
- {
1372
- availableDecisions: [
1373
- {
1374
- applyNetworkPolicyAmendment: {
1375
- network_policy_amendment: {
1376
- domain: "registry.npmjs.org",
1377
- },
1378
- },
1379
- },
1380
- ],
1381
- },
1382
- "approved-session",
1383
- ),
1384
- ).toEqual({
1385
- decision: {
1386
- applyNetworkPolicyAmendment: {
1387
- network_policy_amendment: {
1388
- domain: "registry.npmjs.org",
1389
- },
1390
- },
1391
- },
1392
- });
1393
- expect(
1394
- buildApprovalResponse(
1395
- "item/commandExecution/requestApproval",
1396
- { availableDecisions: ["decline"] },
1397
- "approved-once",
1398
- ),
1399
- ).toEqual({
1400
- decision: "decline",
1401
- });
1402
- expect(
1403
- buildApprovalResponse(
1404
- "item/commandExecution/requestApproval",
1405
- { availableDecisions: ["decline"] },
1406
- "approved-session",
1407
- ),
1408
- ).toEqual({
1409
- decision: "decline",
1410
- });
1411
- expect(
1412
- buildApprovalResponse("item/commandExecution/requestApproval", undefined, "approved-once"),
1413
- ).toEqual({
1414
- decision: "accept",
1415
- });
1416
- expect(
1417
- buildApprovalResponse("item/commandExecution/requestApproval", undefined, "approved-session"),
1418
- ).toEqual({
1419
- decision: "acceptForSession",
1420
- });
1421
- expect(
1422
- buildApprovalResponse(
1423
- "item/commandExecution/requestApproval",
1424
- { availableDecisions: ["cancel"] },
1425
- "approved-once",
1426
- ),
1427
- ).toEqual({
1428
- decision: "cancel",
1429
- });
1430
- expect(
1431
- buildApprovalResponse(
1432
- "item/commandExecution/requestApproval",
1433
- { availableDecisions: ["accept", "cancel"] },
1434
- "denied",
1435
- ),
1436
- ).toEqual({
1437
- decision: "cancel",
1438
- });
1439
- expect(
1440
- buildApprovalResponse(
1441
- "item/commandExecution/requestApproval",
1442
- { availableDecisions: ["decline"] },
1443
- "cancelled",
1444
- ),
1445
- ).toEqual({
1446
- decision: "decline",
1447
- });
1448
- expect(buildApprovalResponse("item/fileChange/requestApproval", undefined, "denied")).toEqual({
1449
- decision: "decline",
1450
- });
1451
- expect(
1452
- buildApprovalResponse(
1453
- "item/permissions/requestApproval",
1454
- {
1455
- permissions: {
1456
- network: { allowHosts: ["example.com"] },
1457
- fileSystem: null,
1458
- },
1459
- },
1460
- "approved-once",
1461
- ),
1462
- ).toEqual({
1463
- permissions: { network: { allowHosts: ["example.com"] } },
1464
- scope: "turn",
1465
- });
1466
- expect(buildApprovalResponse("future/requestApproval", undefined, "approved-once")).toEqual({
1467
- decision: "decline",
1468
- reason: "Klaw codex app-server bridge does not grant native approvals yet.",
1469
- });
1470
- });
1471
- });