@kodelyth/codex 2026.5.40 → 2026.5.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/client-ChMX13_o.js +642 -0
  2. package/dist/client-factory-D3dIsp4Y.js +9 -0
  3. package/dist/command-formatters-BRW7_Nu7.js +519 -0
  4. package/dist/command-handlers-P2IqtXaZ.js +1462 -0
  5. package/dist/compact-baos5flR.js +329 -0
  6. package/dist/computer-use-VfLvTMaa.js +367 -0
  7. package/dist/config-CezENx_E.js +510 -0
  8. package/dist/doctor-contract-api.js +53 -0
  9. package/dist/harness.js +51 -0
  10. package/dist/index.js +1133 -0
  11. package/dist/media-understanding-provider.js +335 -0
  12. package/dist/models-B9DhrIwD.js +110 -0
  13. package/dist/node-cli-sessions-De4_DuFw.js +1216 -0
  14. package/dist/plugin-activation-BlMuJeXz.js +452 -0
  15. package/dist/prompt-overlay.js +12 -0
  16. package/dist/protocol-C9UWI98H.js +9 -0
  17. package/dist/protocol-validators-BGBspNmF.js +5988 -0
  18. package/dist/provider-catalog.js +84 -0
  19. package/dist/provider-discovery.js +33 -0
  20. package/dist/provider.js +150 -0
  21. package/dist/rate-limit-cache-CHuacE27.js +24 -0
  22. package/dist/request-CTQKUxaa.js +89 -0
  23. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  24. package/dist/run-attempt-DqV2OU1R.js +5366 -0
  25. package/dist/session-binding-3PzU7ZTW.js +222 -0
  26. package/dist/shared-client-Cnyr9dyT.js +631 -0
  27. package/dist/side-question-CP5XlA0U.js +667 -0
  28. package/dist/test-api.js +45 -0
  29. package/dist/thread-lifecycle-DBJetBuV.js +1561 -0
  30. package/dist/vision-tools-Cl_5a93K.js +1379 -0
  31. package/doctor-contract-api.test.ts +44 -0
  32. package/doctor-contract-api.ts +68 -0
  33. package/harness.ts +72 -0
  34. package/index.test.ts +230 -0
  35. package/index.ts +66 -0
  36. package/klaw.plugin.json +24 -85
  37. package/media-understanding-provider.test.ts +486 -0
  38. package/media-understanding-provider.ts +521 -0
  39. package/package.json +3 -3
  40. package/prompt-overlay-runtime-contract.test.ts +48 -0
  41. package/prompt-overlay.ts +21 -0
  42. package/provider-catalog.ts +83 -0
  43. package/provider-discovery.ts +45 -0
  44. package/provider.test.ts +384 -0
  45. package/provider.ts +243 -0
  46. package/src/app-server/app-inventory-cache.test.ts +176 -0
  47. package/src/app-server/app-inventory-cache.ts +324 -0
  48. package/src/app-server/approval-bridge.test.ts +1471 -0
  49. package/src/app-server/approval-bridge.ts +1211 -0
  50. package/src/app-server/auth-bridge.test.ts +1449 -0
  51. package/src/app-server/auth-bridge.ts +614 -0
  52. package/src/app-server/auth-profile-runtime-contract.test.ts +239 -0
  53. package/src/app-server/capabilities.ts +27 -0
  54. package/src/app-server/client-factory.ts +24 -0
  55. package/src/app-server/client.test.ts +563 -0
  56. package/src/app-server/client.ts +715 -0
  57. package/src/app-server/compact.test.ts +710 -0
  58. package/src/app-server/compact.ts +500 -0
  59. package/src/app-server/computer-use.test.ts +788 -0
  60. package/src/app-server/computer-use.ts +683 -0
  61. package/src/app-server/config.test.ts +879 -0
  62. package/src/app-server/config.ts +1038 -0
  63. package/src/app-server/context-engine-projection.test.ts +252 -0
  64. package/src/app-server/context-engine-projection.ts +403 -0
  65. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +80 -0
  66. package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  67. package/src/app-server/dynamic-tool-profile.ts +69 -0
  68. package/src/app-server/dynamic-tools.test.ts +1302 -0
  69. package/src/app-server/dynamic-tools.ts +623 -0
  70. package/src/app-server/elicitation-bridge.test.ts +1056 -0
  71. package/src/app-server/elicitation-bridge.ts +783 -0
  72. package/src/app-server/event-projector.test.ts +2668 -0
  73. package/src/app-server/event-projector.ts +2057 -0
  74. package/src/app-server/image-payload-sanitizer.test.ts +49 -0
  75. package/src/app-server/image-payload-sanitizer.ts +167 -0
  76. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +456 -0
  77. package/src/app-server/local-runtime-attribution.ts +39 -0
  78. package/src/app-server/managed-binary.test.ts +139 -0
  79. package/src/app-server/managed-binary.ts +193 -0
  80. package/src/app-server/models.test.ts +246 -0
  81. package/src/app-server/models.ts +172 -0
  82. package/src/app-server/native-hook-relay.test.ts +271 -0
  83. package/src/app-server/native-hook-relay.ts +150 -0
  84. package/src/app-server/native-subagent-task-mirror.test.ts +573 -0
  85. package/src/app-server/native-subagent-task-mirror.ts +497 -0
  86. package/src/app-server/outcome-fallback-runtime-contract.test.ts +404 -0
  87. package/src/app-server/plugin-activation.test.ts +336 -0
  88. package/src/app-server/plugin-activation.ts +283 -0
  89. package/src/app-server/plugin-app-cache-key.ts +74 -0
  90. package/src/app-server/plugin-approval-roundtrip.ts +122 -0
  91. package/src/app-server/plugin-inventory.test.ts +355 -0
  92. package/src/app-server/plugin-inventory.ts +357 -0
  93. package/src/app-server/plugin-thread-config.test.ts +865 -0
  94. package/src/app-server/plugin-thread-config.ts +455 -0
  95. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  96. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  97. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  98. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  99. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  100. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  101. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  102. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  103. package/src/app-server/protocol-validators.test.ts +75 -0
  104. package/src/app-server/protocol-validators.ts +203 -0
  105. package/src/app-server/protocol.ts +520 -0
  106. package/src/app-server/rate-limit-cache.ts +48 -0
  107. package/src/app-server/rate-limits.test.ts +202 -0
  108. package/src/app-server/rate-limits.ts +583 -0
  109. package/src/app-server/request.ts +73 -0
  110. package/src/app-server/run-attempt.context-engine.test.ts +1004 -0
  111. package/src/app-server/run-attempt.test.ts +9477 -0
  112. package/src/app-server/run-attempt.ts +4683 -0
  113. package/src/app-server/run-attempt.vision-tools.test.ts +35 -0
  114. package/src/app-server/schema-normalization-runtime-contract.test.ts +206 -0
  115. package/src/app-server/session-binding.test.ts +303 -0
  116. package/src/app-server/session-binding.ts +398 -0
  117. package/src/app-server/session-history.ts +44 -0
  118. package/src/app-server/shared-client.test.ts +589 -0
  119. package/src/app-server/shared-client.ts +289 -0
  120. package/src/app-server/side-question.test.ts +1175 -0
  121. package/src/app-server/side-question.ts +1007 -0
  122. package/src/app-server/test-support.ts +48 -0
  123. package/src/app-server/thread-lifecycle.test.ts +447 -0
  124. package/src/app-server/thread-lifecycle.ts +939 -0
  125. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +442 -0
  126. package/src/app-server/timeout.ts +9 -0
  127. package/src/app-server/tool-progress-normalization.ts +77 -0
  128. package/src/app-server/trajectory.test.ts +205 -0
  129. package/src/app-server/trajectory.ts +365 -0
  130. package/src/app-server/transcript-mirror.test.ts +524 -0
  131. package/src/app-server/transcript-mirror.ts +208 -0
  132. package/src/app-server/transcript-repair-runtime-contract.test.ts +44 -0
  133. package/src/app-server/transport-stdio.test.ts +171 -0
  134. package/src/app-server/transport-stdio.ts +107 -0
  135. package/src/app-server/transport-websocket.test.ts +69 -0
  136. package/src/app-server/transport-websocket.ts +90 -0
  137. package/src/app-server/transport.ts +117 -0
  138. package/src/app-server/user-input-bridge.test.ts +249 -0
  139. package/src/app-server/user-input-bridge.ts +316 -0
  140. package/src/app-server/version.ts +4 -0
  141. package/src/app-server/vision-tools.ts +12 -0
  142. package/src/command-account.ts +544 -0
  143. package/src/command-formatters.ts +425 -0
  144. package/src/command-handlers.ts +2004 -0
  145. package/src/command-rpc.test.ts +16 -0
  146. package/src/command-rpc.ts +142 -0
  147. package/src/commands.test.ts +3312 -0
  148. package/src/commands.ts +65 -0
  149. package/src/conversation-binding-data.ts +124 -0
  150. package/src/conversation-binding.test.ts +599 -0
  151. package/src/conversation-binding.ts +561 -0
  152. package/src/conversation-control.test.ts +126 -0
  153. package/src/conversation-control.ts +303 -0
  154. package/src/conversation-turn-collector.test.ts +191 -0
  155. package/src/conversation-turn-collector.ts +186 -0
  156. package/src/conversation-turn-input.test.ts +141 -0
  157. package/src/conversation-turn-input.ts +106 -0
  158. package/src/manifest.test.ts +20 -0
  159. package/src/migration/apply.ts +501 -0
  160. package/src/migration/helpers.ts +55 -0
  161. package/src/migration/plan.ts +461 -0
  162. package/src/migration/provider.test.ts +1741 -0
  163. package/src/migration/provider.ts +41 -0
  164. package/src/migration/source.ts +643 -0
  165. package/src/migration/targets.ts +25 -0
  166. package/src/node-cli-sessions.test.ts +180 -0
  167. package/src/node-cli-sessions.ts +711 -0
  168. package/test-api.ts +82 -0
  169. package/tsconfig.json +16 -0
  170. package/doctor-contract-api.js +0 -7
  171. package/harness.js +0 -7
  172. package/index.js +0 -7
  173. package/media-understanding-provider.js +0 -7
  174. package/prompt-overlay.js +0 -7
  175. package/provider-catalog.js +0 -7
  176. package/provider-discovery.js +0 -7
  177. package/provider.js +0 -7
  178. package/test-api.js +0 -7
@@ -0,0 +1,172 @@
1
+ import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
2
+ import type { CodexAppServerClient } from "./client.js";
3
+ import type { CodexAppServerStartOptions } from "./config.js";
4
+ import { readCodexModelListResponse } from "./protocol-validators.js";
5
+ import type { CodexModel, CodexReasoningEffortOption } from "./protocol.js";
6
+
7
+ export type CodexAppServerModel = {
8
+ id: string;
9
+ model: string;
10
+ displayName?: string;
11
+ description?: string;
12
+ hidden?: boolean;
13
+ isDefault?: boolean;
14
+ inputModalities: string[];
15
+ supportedReasoningEfforts: string[];
16
+ defaultReasoningEffort?: string;
17
+ };
18
+
19
+ export type CodexAppServerModelListResult = {
20
+ models: CodexAppServerModel[];
21
+ nextCursor?: string;
22
+ truncated?: boolean;
23
+ };
24
+
25
+ export type CodexAppServerListModelsOptions = {
26
+ limit?: number;
27
+ cursor?: string;
28
+ includeHidden?: boolean;
29
+ timeoutMs?: number;
30
+ startOptions?: CodexAppServerStartOptions;
31
+ authProfileId?: string;
32
+ agentDir?: string;
33
+ config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
34
+ sharedClient?: boolean;
35
+ };
36
+
37
+ export async function listCodexAppServerModels(
38
+ options: CodexAppServerListModelsOptions = {},
39
+ ): Promise<CodexAppServerModelListResult> {
40
+ return await withCodexAppServerModelClient(options, async ({ client, timeoutMs }) =>
41
+ requestModelListPage(client, { ...options, timeoutMs }),
42
+ );
43
+ }
44
+
45
+ export async function listAllCodexAppServerModels(
46
+ options: CodexAppServerListModelsOptions & { maxPages?: number } = {},
47
+ ): Promise<CodexAppServerModelListResult> {
48
+ const maxPages = normalizeMaxPages(options.maxPages);
49
+ return await withCodexAppServerModelClient(options, async ({ client, timeoutMs }) => {
50
+ const models: CodexAppServerModel[] = [];
51
+ let cursor = options.cursor;
52
+ let nextCursor: string | undefined;
53
+ for (let page = 0; page < maxPages; page += 1) {
54
+ const result = await requestModelListPage(client, {
55
+ ...options,
56
+ timeoutMs,
57
+ cursor,
58
+ });
59
+ models.push(...result.models);
60
+ nextCursor = result.nextCursor;
61
+ if (!nextCursor) {
62
+ return { models };
63
+ }
64
+ cursor = nextCursor;
65
+ }
66
+ return { models, nextCursor, truncated: true };
67
+ });
68
+ }
69
+
70
+ async function withCodexAppServerModelClient<T>(
71
+ options: CodexAppServerListModelsOptions,
72
+ run: (params: { client: CodexAppServerClient; timeoutMs: number }) => Promise<T>,
73
+ ): Promise<T> {
74
+ const timeoutMs = options.timeoutMs ?? 2500;
75
+ const useSharedClient = options.sharedClient !== false;
76
+ const { createIsolatedCodexAppServerClient, getSharedCodexAppServerClient } =
77
+ await import("./shared-client.js");
78
+ const client = useSharedClient
79
+ ? await getSharedCodexAppServerClient({
80
+ startOptions: options.startOptions,
81
+ timeoutMs,
82
+ authProfileId: options.authProfileId,
83
+ agentDir: options.agentDir,
84
+ config: options.config,
85
+ })
86
+ : await createIsolatedCodexAppServerClient({
87
+ startOptions: options.startOptions,
88
+ timeoutMs,
89
+ authProfileId: options.authProfileId,
90
+ agentDir: options.agentDir,
91
+ config: options.config,
92
+ });
93
+ try {
94
+ return await run({ client, timeoutMs });
95
+ } finally {
96
+ if (!useSharedClient) {
97
+ client.close();
98
+ }
99
+ }
100
+ }
101
+
102
+ async function requestModelListPage(
103
+ client: CodexAppServerClient,
104
+ options: CodexAppServerListModelsOptions & { timeoutMs: number },
105
+ ): Promise<CodexAppServerModelListResult> {
106
+ const response = await client.request(
107
+ "model/list",
108
+ {
109
+ limit: options.limit ?? null,
110
+ cursor: options.cursor ?? null,
111
+ includeHidden: options.includeHidden ?? null,
112
+ },
113
+ { timeoutMs: options.timeoutMs },
114
+ );
115
+ return readModelListResult(response);
116
+ }
117
+
118
+ export function readModelListResult(value: unknown): CodexAppServerModelListResult {
119
+ const response = readCodexModelListResponse(value);
120
+ if (!response) {
121
+ return { models: [] };
122
+ }
123
+ const models = response.data
124
+ .map((entry) => readCodexModel(entry))
125
+ .filter((entry): entry is CodexAppServerModel => entry !== undefined);
126
+ const nextCursor = response.nextCursor ?? undefined;
127
+ return { models, ...(nextCursor ? { nextCursor } : {}) };
128
+ }
129
+
130
+ function readCodexModel(value: CodexModel): CodexAppServerModel | undefined {
131
+ const id = readNonEmptyString(value.id);
132
+ const model = readNonEmptyString(value.model) ?? id;
133
+ if (!id || !model) {
134
+ return undefined;
135
+ }
136
+ return {
137
+ id,
138
+ model,
139
+ ...(readNonEmptyString(value.displayName)
140
+ ? { displayName: readNonEmptyString(value.displayName) }
141
+ : {}),
142
+ ...(readNonEmptyString(value.description)
143
+ ? { description: readNonEmptyString(value.description) }
144
+ : {}),
145
+ hidden: value.hidden,
146
+ isDefault: value.isDefault,
147
+ inputModalities: value.inputModalities,
148
+ supportedReasoningEfforts: readReasoningEfforts(value.supportedReasoningEfforts),
149
+ ...(readNonEmptyString(value.defaultReasoningEffort)
150
+ ? { defaultReasoningEffort: readNonEmptyString(value.defaultReasoningEffort) }
151
+ : {}),
152
+ };
153
+ }
154
+
155
+ function readReasoningEfforts(value: CodexReasoningEffortOption[]): string[] {
156
+ const efforts = value
157
+ .map((entry) => readNonEmptyString(entry.reasoningEffort))
158
+ .filter((entry): entry is string => entry !== undefined);
159
+ return [...new Set(efforts)];
160
+ }
161
+
162
+ function readNonEmptyString(value: unknown): string | undefined {
163
+ if (typeof value !== "string") {
164
+ return undefined;
165
+ }
166
+ const trimmed = value.trim();
167
+ return trimmed || undefined;
168
+ }
169
+
170
+ function normalizeMaxPages(value: unknown): number {
171
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 20;
172
+ }
@@ -0,0 +1,271 @@
1
+ import type { NativeHookRelayRegistrationHandle } from "klaw/plugin-sdk/agent-harness-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ buildCodexNativeHookRelayConfig,
5
+ buildCodexNativeHookRelayDisabledConfig,
6
+ } from "./native-hook-relay.js";
7
+
8
+ describe("Codex native hook relay config", () => {
9
+ it("builds deterministic Codex config overrides with command hooks", () => {
10
+ const config = buildCodexNativeHookRelayConfig({
11
+ relay: createRelay(),
12
+ hookTimeoutSec: 7,
13
+ });
14
+
15
+ expect(config).toEqual({
16
+ "features.hooks": true,
17
+ "hooks.PreToolUse": [
18
+ {
19
+ hooks: [
20
+ {
21
+ type: "command",
22
+ command: "klaw hooks relay --provider codex --relay-id relay-1 --event pre_tool_use",
23
+ timeout: 7,
24
+ async: false,
25
+ statusMessage: "Klaw native hook relay",
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ "hooks.PostToolUse": [
31
+ {
32
+ hooks: [
33
+ {
34
+ type: "command",
35
+ command: "klaw hooks relay --provider codex --relay-id relay-1 --event post_tool_use",
36
+ timeout: 7,
37
+ async: false,
38
+ statusMessage: "Klaw native hook relay",
39
+ },
40
+ ],
41
+ },
42
+ ],
43
+ "hooks.PermissionRequest": [
44
+ {
45
+ hooks: [
46
+ {
47
+ type: "command",
48
+ command:
49
+ "klaw hooks relay --provider codex --relay-id relay-1 --event permission_request",
50
+ timeout: 7,
51
+ async: false,
52
+ statusMessage: "Klaw native hook relay",
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ "hooks.Stop": [
58
+ {
59
+ hooks: [
60
+ {
61
+ type: "command",
62
+ command:
63
+ "klaw hooks relay --provider codex --relay-id relay-1 --event before_agent_finalize",
64
+ timeout: 7,
65
+ async: false,
66
+ statusMessage: "Klaw native hook relay",
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ "hooks.state": {
72
+ "/<session-flags>/config.toml:pre_tool_use:0:0": {
73
+ enabled: true,
74
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
75
+ },
76
+ "<session-flags>/config.toml:pre_tool_use:0:0": {
77
+ enabled: true,
78
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
79
+ },
80
+ "/<session-flags>/config.toml:post_tool_use:0:0": {
81
+ enabled: true,
82
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
83
+ },
84
+ "<session-flags>/config.toml:post_tool_use:0:0": {
85
+ enabled: true,
86
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
87
+ },
88
+ "/<session-flags>/config.toml:permission_request:0:0": {
89
+ enabled: true,
90
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
91
+ },
92
+ "<session-flags>/config.toml:permission_request:0:0": {
93
+ enabled: true,
94
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
95
+ },
96
+ "/<session-flags>/config.toml:stop:0:0": {
97
+ enabled: true,
98
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
99
+ },
100
+ "<session-flags>/config.toml:stop:0:0": {
101
+ enabled: true,
102
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
103
+ },
104
+ },
105
+ });
106
+ expect(JSON.stringify(config)).not.toContain("timeoutSec");
107
+ expect(JSON.stringify(config)).not.toContain('"matcher":null');
108
+ expect(config).not.toHaveProperty("hooks.SessionStart");
109
+ expect(config).not.toHaveProperty("hooks.UserPromptSubmit");
110
+ });
111
+
112
+ it("includes only requested hook events", () => {
113
+ expect(
114
+ buildCodexNativeHookRelayConfig({
115
+ relay: createRelay(),
116
+ events: ["permission_request"],
117
+ }),
118
+ ).toEqual({
119
+ "features.hooks": true,
120
+ "hooks.PermissionRequest": [
121
+ {
122
+ hooks: [
123
+ {
124
+ type: "command",
125
+ command:
126
+ "klaw hooks relay --provider codex --relay-id relay-1 --event permission_request",
127
+ timeout: 5,
128
+ async: false,
129
+ statusMessage: "Klaw native hook relay",
130
+ },
131
+ ],
132
+ },
133
+ ],
134
+ "hooks.state": {
135
+ "/<session-flags>/config.toml:permission_request:0:0": {
136
+ enabled: true,
137
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
138
+ },
139
+ "<session-flags>/config.toml:permission_request:0:0": {
140
+ enabled: true,
141
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
142
+ },
143
+ },
144
+ });
145
+ });
146
+
147
+ it("clears requested hook events when the relay reports no local work", () => {
148
+ expect(
149
+ buildCodexNativeHookRelayConfig({
150
+ relay: createRelay({ inactiveEvents: ["post_tool_use", "before_agent_finalize"] }),
151
+ events: ["pre_tool_use", "post_tool_use", "before_agent_finalize"],
152
+ }),
153
+ ).toEqual({
154
+ "features.hooks": true,
155
+ "hooks.PreToolUse": [
156
+ {
157
+ hooks: [
158
+ {
159
+ type: "command",
160
+ command: "klaw hooks relay --provider codex --relay-id relay-1 --event pre_tool_use",
161
+ timeout: 5,
162
+ async: false,
163
+ statusMessage: "Klaw native hook relay",
164
+ },
165
+ ],
166
+ },
167
+ ],
168
+ "hooks.PostToolUse": [],
169
+ "hooks.Stop": [],
170
+ "hooks.state": {
171
+ "/<session-flags>/config.toml:pre_tool_use:0:0": {
172
+ enabled: true,
173
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
174
+ },
175
+ "<session-flags>/config.toml:pre_tool_use:0:0": {
176
+ enabled: true,
177
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
178
+ },
179
+ },
180
+ });
181
+ });
182
+
183
+ it("clears omitted hook events when requested", () => {
184
+ expect(
185
+ buildCodexNativeHookRelayConfig({
186
+ relay: createRelay(),
187
+ events: ["permission_request"],
188
+ clearOmittedEvents: true,
189
+ }),
190
+ ).toEqual({
191
+ "features.hooks": true,
192
+ "hooks.PreToolUse": [],
193
+ "hooks.PostToolUse": [],
194
+ "hooks.PermissionRequest": [
195
+ {
196
+ hooks: [
197
+ {
198
+ type: "command",
199
+ command:
200
+ "klaw hooks relay --provider codex --relay-id relay-1 --event permission_request",
201
+ timeout: 5,
202
+ async: false,
203
+ statusMessage: "Klaw native hook relay",
204
+ },
205
+ ],
206
+ },
207
+ ],
208
+ "hooks.Stop": [],
209
+ "hooks.state": {
210
+ "/<session-flags>/config.toml:pre_tool_use:0:0": { enabled: false },
211
+ "<session-flags>/config.toml:pre_tool_use:0:0": { enabled: false },
212
+ "/<session-flags>/config.toml:post_tool_use:0:0": { enabled: false },
213
+ "<session-flags>/config.toml:post_tool_use:0:0": { enabled: false },
214
+ "/<session-flags>/config.toml:permission_request:0:0": {
215
+ enabled: true,
216
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
217
+ },
218
+ "<session-flags>/config.toml:permission_request:0:0": {
219
+ enabled: true,
220
+ trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
221
+ },
222
+ "/<session-flags>/config.toml:stop:0:0": { enabled: false },
223
+ "<session-flags>/config.toml:stop:0:0": { enabled: false },
224
+ },
225
+ });
226
+ });
227
+
228
+ it("omits matchers so Codex MCP tool names reach the relay with a stable trust hash", () => {
229
+ const config = buildCodexNativeHookRelayConfig({
230
+ relay: createRelay(),
231
+ events: ["pre_tool_use", "post_tool_use"],
232
+ });
233
+
234
+ expect((config["hooks.PreToolUse"] as Array<{ matcher?: unknown }>)[0]).not.toHaveProperty(
235
+ "matcher",
236
+ );
237
+ expect((config["hooks.PostToolUse"] as Array<{ matcher?: unknown }>)[0]).not.toHaveProperty(
238
+ "matcher",
239
+ );
240
+ });
241
+
242
+ it("builds deterministic clearing config when the relay is disabled", () => {
243
+ expect(buildCodexNativeHookRelayDisabledConfig()).toEqual({
244
+ "features.hooks": false,
245
+ "hooks.PreToolUse": [],
246
+ "hooks.PostToolUse": [],
247
+ "hooks.PermissionRequest": [],
248
+ "hooks.Stop": [],
249
+ });
250
+ });
251
+ });
252
+
253
+ function createRelay(options?: {
254
+ inactiveEvents?: readonly NativeHookRelayRegistrationHandle["allowedEvents"][number][];
255
+ }): NativeHookRelayRegistrationHandle {
256
+ const inactiveEvents = new Set(options?.inactiveEvents ?? []);
257
+ return {
258
+ relayId: "relay-1",
259
+ provider: "codex",
260
+ sessionId: "session-1",
261
+ sessionKey: "agent:main:session-1",
262
+ runId: "run-1",
263
+ allowedEvents: ["pre_tool_use", "post_tool_use", "permission_request", "before_agent_finalize"],
264
+ expiresAtMs: Date.now() + 1000,
265
+ shouldRelayEvent: (event) => !inactiveEvents.has(event),
266
+ commandForEvent: (event) =>
267
+ `klaw hooks relay --provider codex --relay-id relay-1 --event ${event}`,
268
+ renew: () => undefined,
269
+ unregister: () => undefined,
270
+ };
271
+ }
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+ import type {
3
+ NativeHookRelayEvent,
4
+ NativeHookRelayRegistrationHandle,
5
+ } from "klaw/plugin-sdk/agent-harness-runtime";
6
+ import type { JsonObject, JsonValue } from "./protocol.js";
7
+
8
+ export const CODEX_NATIVE_HOOK_RELAY_EVENTS: readonly NativeHookRelayEvent[] = [
9
+ "pre_tool_use",
10
+ "post_tool_use",
11
+ "permission_request",
12
+ "before_agent_finalize",
13
+ ] as const;
14
+
15
+ type CodexHookEventName = "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop";
16
+
17
+ const CODEX_HOOK_EVENT_BY_NATIVE_EVENT: Record<NativeHookRelayEvent, CodexHookEventName> = {
18
+ pre_tool_use: "PreToolUse",
19
+ post_tool_use: "PostToolUse",
20
+ permission_request: "PermissionRequest",
21
+ before_agent_finalize: "Stop",
22
+ };
23
+
24
+ const CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT: Record<NativeHookRelayEvent, string> = {
25
+ pre_tool_use: "pre_tool_use",
26
+ post_tool_use: "post_tool_use",
27
+ permission_request: "permission_request",
28
+ before_agent_finalize: "stop",
29
+ };
30
+
31
+ const CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS = [
32
+ "/<session-flags>/config.toml",
33
+ "<session-flags>/config.toml",
34
+ ] as const;
35
+
36
+ export function buildCodexNativeHookRelayConfig(params: {
37
+ relay: NativeHookRelayRegistrationHandle;
38
+ events?: readonly NativeHookRelayEvent[];
39
+ hookTimeoutSec?: number;
40
+ clearOmittedEvents?: boolean;
41
+ }): JsonObject {
42
+ const events = params.events?.length ? params.events : CODEX_NATIVE_HOOK_RELAY_EVENTS;
43
+ const selectedEvents = new Set<NativeHookRelayEvent>(events);
44
+ const config: JsonObject = {
45
+ "features.hooks": true,
46
+ };
47
+ const hookState: JsonObject = {};
48
+ for (const event of CODEX_NATIVE_HOOK_RELAY_EVENTS) {
49
+ const codexEvent = CODEX_HOOK_EVENT_BY_NATIVE_EVENT[event];
50
+ const selected = selectedEvents.has(event);
51
+ if (!selected || !params.relay.shouldRelayEvent(event)) {
52
+ if (selected || params.clearOmittedEvents) {
53
+ config[`hooks.${codexEvent}`] = [] satisfies JsonValue;
54
+ }
55
+ if (params.clearOmittedEvents) {
56
+ for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) {
57
+ hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] = {
58
+ enabled: false,
59
+ } satisfies JsonValue;
60
+ }
61
+ }
62
+ continue;
63
+ }
64
+ const command = params.relay.commandForEvent(event);
65
+ const timeout = normalizeHookTimeoutSec(params.hookTimeoutSec);
66
+ config[`hooks.${codexEvent}`] = [
67
+ {
68
+ hooks: [
69
+ {
70
+ type: "command",
71
+ command,
72
+ timeout,
73
+ async: false,
74
+ statusMessage: "Klaw native hook relay",
75
+ },
76
+ ],
77
+ },
78
+ ] satisfies JsonValue;
79
+ const state = {
80
+ enabled: true,
81
+ trusted_hash: codexCommandHookTrustedHash({
82
+ event,
83
+ command,
84
+ timeout,
85
+ statusMessage: "Klaw native hook relay",
86
+ }),
87
+ };
88
+ for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) {
89
+ hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] =
90
+ state satisfies JsonValue;
91
+ }
92
+ }
93
+ config["hooks.state"] = hookState;
94
+ return config;
95
+ }
96
+
97
+ export function buildCodexNativeHookRelayDisabledConfig(): JsonObject {
98
+ return {
99
+ "features.hooks": false,
100
+ "hooks.PreToolUse": [],
101
+ "hooks.PostToolUse": [],
102
+ "hooks.PermissionRequest": [],
103
+ "hooks.Stop": [],
104
+ };
105
+ }
106
+
107
+ function normalizeHookTimeoutSec(value: number | undefined): number {
108
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.ceil(value) : 5;
109
+ }
110
+
111
+ function codexCommandHookTrustedHash(params: {
112
+ event: NativeHookRelayEvent;
113
+ command: string;
114
+ timeout: number;
115
+ statusMessage: string;
116
+ }): string {
117
+ // Keep the match-all matcher omitted rather than null. Codex app-server
118
+ // converts JSON null to an empty TOML string before hashing, which changes the
119
+ // trust identity even though both forms match all tools.
120
+ const identity = {
121
+ event_name: CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[params.event],
122
+ hooks: [
123
+ {
124
+ async: false,
125
+ command: params.command,
126
+ statusMessage: params.statusMessage,
127
+ timeout: params.timeout,
128
+ type: "command",
129
+ },
130
+ ],
131
+ };
132
+ const hash = createHash("sha256")
133
+ .update(JSON.stringify(sortJsonValue(identity)))
134
+ .digest("hex");
135
+ return `sha256:${hash}`;
136
+ }
137
+
138
+ function sortJsonValue(value: JsonValue): JsonValue {
139
+ if (!value || typeof value !== "object") {
140
+ return value;
141
+ }
142
+ if (Array.isArray(value)) {
143
+ return value.map(sortJsonValue);
144
+ }
145
+ const sorted: JsonObject = {};
146
+ for (const key of Object.keys(value).toSorted()) {
147
+ sorted[key] = sortJsonValue(value[key]);
148
+ }
149
+ return sorted;
150
+ }