@jskit-ai/assistant-core 0.1.200 → 0.1.202

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 (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,1183 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { open, rename, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { isPlainObject as isRecord, normalizeText } from "./normalize.js";
7
+
8
+ const execute = promisify(execFile);
9
+ const CODEX_THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
10
+
11
+ export function normalizeCodexThreadId(value) {
12
+ const threadId = String(value || "").trim();
13
+ if (!CODEX_THREAD_ID_PATTERN.test(threadId)) {
14
+ return "";
15
+ }
16
+ return threadId.toLowerCase();
17
+ }
18
+
19
+ /** Format native requests from the host's already-resolved model and permission policy. */
20
+ export function codexAppServerThreadSettings({
21
+ effectiveSettings,
22
+ config = null,
23
+ cwd = "",
24
+ systemPrompt = null,
25
+ hostContext = null,
26
+ model = "",
27
+ approvalPolicy,
28
+ reasoningSummary,
29
+ sandbox
30
+ } = {}) {
31
+ return {
32
+ approvalPolicy,
33
+ config: {
34
+ ...(effectiveSettings.request.reasoning !== false
35
+ ? { model_reasoning_effort: effectiveSettings.thinking }
36
+ : {}),
37
+ ...(effectiveSettings.request.summary !== false
38
+ ? { model_reasoning_summary: reasoningSummary }
39
+ : {}),
40
+ ...(config && typeof config === "object" && !Array.isArray(config) ? config : {})
41
+ },
42
+ cwd,
43
+ ...(typeof systemPrompt === "string" && systemPrompt.trim() ? { systemPrompt: normalizeText(systemPrompt) } : {}),
44
+ ...(hostContext ? { hostContext } : {}),
45
+ model: normalizeText(model) || effectiveSettings.model,
46
+ ...(effectiveSettings.modelProviderId ? { modelProvider: effectiveSettings.modelProviderId } : {}),
47
+ sandbox
48
+ };
49
+ }
50
+
51
+ /** Overlay native fields after the host selects a restricted, read-only scope. */
52
+ export function codexAppServerReadOnlyThreadSettings(settings) {
53
+ return {
54
+ ...settings,
55
+ dynamicTools: [],
56
+ environments: [],
57
+ runtimeWorkspaceRoots: [],
58
+ sandbox: "read-only",
59
+ selectedCapabilityRoots: []
60
+ };
61
+ }
62
+
63
+ export function codexAppServerTurnSettings({
64
+ effectiveSettings,
65
+ cwd = "",
66
+ effort = "",
67
+ model = "",
68
+ approvalPolicy,
69
+ reasoningSummary,
70
+ sandboxPolicy,
71
+ externalSandbox = false
72
+ } = {}) {
73
+ const settings = {
74
+ approvalPolicy,
75
+ cwd,
76
+ model: normalizeText(model) || effectiveSettings.model,
77
+ sandboxPolicy: externalSandbox ? { networkAccess: "enabled", type: "externalSandbox" } : sandboxPolicy
78
+ };
79
+ if (effectiveSettings.request.reasoning !== false) {
80
+ settings.effort = normalizeText(effort) || effectiveSettings.thinking;
81
+ }
82
+ if (effectiveSettings.request.summary !== false) {
83
+ settings.summary = reasoningSummary;
84
+ }
85
+ return settings;
86
+ }
87
+
88
+ /** Interactive CLI spelling; executable selection and shell setup belong to the host. */
89
+ export function codexInteractiveArguments({
90
+ command,
91
+ threadId = "",
92
+ remoteEndpoint = "",
93
+ model,
94
+ effort,
95
+ disableStartupUpdates = false,
96
+ bypassApprovalsAndSandbox = false,
97
+ bypassHookTrust = false
98
+ } = {}) {
99
+ const codexReasoningConfig = `model_reasoning_effort="${effort}"`;
100
+ return [
101
+ command,
102
+ ...(disableStartupUpdates ? ["-c", "check_for_update_on_startup=false"] : []),
103
+ ...(remoteEndpoint ? ["--remote", remoteEndpoint] : []),
104
+ "--model",
105
+ model,
106
+ "-c",
107
+ codexReasoningConfig,
108
+ // Remote resume inherits saved server permissions and rejects CLI overrides.
109
+ ...(bypassApprovalsAndSandbox && !(remoteEndpoint && threadId) ? ["--dangerously-bypass-approvals-and-sandbox"] : []),
110
+ ...(bypassHookTrust ? ["--dangerously-bypass-hook-trust"] : []),
111
+ ...(threadId ? ["resume", threadId] : [])
112
+ ];
113
+ }
114
+
115
+ export async function codexAppServerProjectHookTrustConfig(provider, cwd = "", {
116
+ persist = false
117
+ } = {}) {
118
+ const normalizedCwd = normalizeText(cwd);
119
+ if (!normalizedCwd || typeof provider?.listHooks !== "function") {
120
+ return null;
121
+ }
122
+ await provider.trustProject(normalizedCwd);
123
+ const result = await provider.listHooks([normalizedCwd]);
124
+ const record = (Array.isArray(result?.data) ? result.data : [])
125
+ .find((item) => normalizeText(item?.cwd) === normalizedCwd);
126
+ const trustedHooks = (Array.isArray(record?.hooks) ? record.hooks : [])
127
+ // Session flags contain the host's command hook. Trust its exact hash
128
+ // alongside project hooks; unrelated account and plugin hooks stay separate.
129
+ .filter((hook) => hook?.enabled === true && ["project", "sessionFlags"].includes(hook?.source))
130
+ .map((hook) => ({
131
+ currentHash: normalizeText(hook?.currentHash),
132
+ key: normalizeText(hook?.key),
133
+ trustStatus: normalizeText(hook?.trustStatus)
134
+ }))
135
+ .filter(({ currentHash, key }) => key && currentHash);
136
+ if (trustedHooks.length === 0) {
137
+ return null;
138
+ }
139
+ const state = Object.fromEntries(trustedHooks.map(({ currentHash, key }) => [
140
+ key,
141
+ {
142
+ trusted_hash: currentHash
143
+ }
144
+ ]));
145
+ if (
146
+ persist &&
147
+ trustedHooks.some(({ trustStatus }) => !["managed", "trusted"].includes(trustStatus)) &&
148
+ typeof provider?.writeHookTrustState === "function"
149
+ ) {
150
+ await provider.writeHookTrustState(state);
151
+ }
152
+ // Native overrides replace the value at each key. Replacing the whole hooks
153
+ // table would erase the PreToolUse hook installed through session flags.
154
+ return { "hooks.state": state };
155
+ }
156
+
157
+ // Codex's model manager reads model_catalog_json once, before any threads
158
+ // exist. A thread override changes routing, but cannot supply model metadata.
159
+ // Export from the installed CLI so native model definitions stay CLI-owned.
160
+ export async function prepareCodexModelCatalog({ command, runtimeDir, bundled = false, signal, additionalModels = [] }) {
161
+ let catalog;
162
+ const models = new Map();
163
+ try {
164
+ const { stdout } = await execute(command, [
165
+ "debug", "models", ...(bundled ? ["--bundled"] : []),
166
+ "-c", "check_for_update_on_startup=false"
167
+ ], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 20_000, signal });
168
+ catalog = JSON.parse(stdout);
169
+ if (!Array.isArray(catalog?.models) || !catalog.models.length) {
170
+ throw new Error("Invalid native model catalogue");
171
+ }
172
+ for (const model of catalog.models) {
173
+ if (!model || typeof model.slug !== "string" || !model.slug.trim() || models.has(model.slug)) {
174
+ throw new Error("Invalid native model catalogue");
175
+ }
176
+ models.set(model.slug, model);
177
+ }
178
+ } catch {
179
+ // CLI output can contain provider configuration. Never include it here.
180
+ throw new Error("Codex could not load its model catalogue. Restart the assistant service after checking the Codex installation and connection.");
181
+ }
182
+ // Adding foreign metadata must not change the native default model.
183
+ let priority = catalog.models.reduce((maximum, model) => Math.max(maximum, model.priority || 0), 0);
184
+ for (const model of additionalModels) {
185
+ models.set(model.slug, { ...model, priority: ++priority });
186
+ }
187
+ const catalogPath = path.join(runtimeDir, "models.json");
188
+ const temporary = `${catalogPath}.tmp`;
189
+ try {
190
+ signal?.throwIfAborted();
191
+ await writeFile(temporary, JSON.stringify({ ...catalog, models: [...models.values()] }), { mode: 0o600 });
192
+ await rename(temporary, catalogPath);
193
+ } finally {
194
+ await rm(temporary, { force: true });
195
+ }
196
+ return catalogPath;
197
+ }
198
+
199
+ const DISABLED_FEATURES = Object.freeze([
200
+ "apps",
201
+ "artifact",
202
+ "browser_use",
203
+ "browser_use_external",
204
+ "browser_use_full_cdp_access",
205
+ "code_mode",
206
+ "code_mode_host",
207
+ "computer_use",
208
+ "default_mode_request_user_input",
209
+ "deferred_executor",
210
+ "goals",
211
+ "hooks",
212
+ "image_generation",
213
+ "in_app_browser",
214
+ "memories",
215
+ "multi_agent",
216
+ "multi_agent_v2",
217
+ "plugins",
218
+ "psp",
219
+ "recommended_plugins",
220
+ "request_permissions_tool",
221
+ "shell_tool",
222
+ "skill_mcp_dependency_install",
223
+ "skill_search",
224
+ "tool_call_mcp_elicitation",
225
+ "tool_suggest",
226
+ "unified_exec",
227
+ "unified_exec_zsh_fork",
228
+ "view_image"
229
+ ]);
230
+
231
+ export function codexAppServerTextHasControlCharacters(value = "") {
232
+ return Array.from(String(value)).some((character) => {
233
+ const codePoint = character.codePointAt(0);
234
+ return codePoint <= 31 || codePoint === 127;
235
+ });
236
+ }
237
+
238
+ function isPlainRecord(value) {
239
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
240
+ return false;
241
+ }
242
+ const prototype = Object.getPrototypeOf(value);
243
+ return prototype === Object.prototype || prototype === null;
244
+ }
245
+
246
+ function deepFreezeCodexAppServerHelperConfig(value) {
247
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) {
248
+ return value;
249
+ }
250
+ for (const nested of Object.values(value)) {
251
+ deepFreezeCodexAppServerHelperConfig(nested);
252
+ }
253
+ return Object.freeze(value);
254
+ }
255
+
256
+ function codexAppServerIsolationPolicyError(message = "", details = {}) {
257
+ const error = new Error(normalizeText(message) || "Codex cannot prove the helper execution policy.");
258
+ error.code = "codex_policy_unenforceable";
259
+ error.details = Object.freeze({ ...details });
260
+ for (const [name, value] of Object.entries(error.details)) {
261
+ if (!["code", "details", "message", "name"].includes(name)) {
262
+ error[name] = value;
263
+ }
264
+ }
265
+ return error;
266
+ }
267
+
268
+ /** Original native helper policy verification; hosts supply their profile policy. */
269
+ export function createCodexAppServerIsolation({
270
+ clientName = "jskit",
271
+ minimumVersion = "0.151.0",
272
+ disabledFeatures = DISABLED_FEATURES,
273
+ limits: {
274
+ userAgentMaxLength = 512,
275
+ mcpServerMaxCount = 128,
276
+ mcpServerNameMaxLength = 256,
277
+ configResponseMaxBytes = 256 * 1024,
278
+ hookMaxCount = 256,
279
+ hookErrorMaxCount = 256,
280
+ hookFieldMaxLength = 2048,
281
+ hookFingerprintMaxLength = 256 * 1024,
282
+ hookResponseMaxBytes = 512 * 1024
283
+ } = {},
284
+ createError = codexAppServerIsolationPolicyError
285
+ } = {}) {
286
+ const codexAppServerHelperIsolationConfigs = new WeakSet();
287
+
288
+ function assertCodexAppServerHelperResponseBounded(value, maxBytes, label) {
289
+ let bytes = 0;
290
+ try {
291
+ bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
292
+ } catch {
293
+ throw createError(
294
+ `Codex helper execution received an invalid ${label}.`
295
+ );
296
+ }
297
+ if (bytes > maxBytes) {
298
+ throw createError(
299
+ `Codex helper execution received an oversized ${label}.`
300
+ );
301
+ }
302
+ }
303
+
304
+ function codexAppServerHelperConnectionGeneration(provider) {
305
+ if (typeof provider?.currentConnectionGeneration !== "function") {
306
+ throw createError(
307
+ "Codex helper execution cannot verify the app-server connection generation."
308
+ );
309
+ }
310
+ const generation = provider.currentConnectionGeneration();
311
+ if (!Number.isSafeInteger(generation) || generation <= 0) {
312
+ throw createError(
313
+ "Codex helper execution found no active app-server connection."
314
+ );
315
+ }
316
+ return generation;
317
+ }
318
+
319
+ async function codexAppServerHelperExecutionContext(provider) {
320
+ if (typeof provider?.currentHelperExecutionContext !== "function") {
321
+ throw createError(
322
+ "Codex helper execution requires its dedicated isolated provider."
323
+ );
324
+ }
325
+ const context = await provider.currentHelperExecutionContext();
326
+ const cwd = normalizeText(context?.cwd);
327
+ const accountIdentitySignature = normalizeText(context?.accountIdentitySignature);
328
+ if (
329
+ context?.executionMode !== "helper" ||
330
+ !cwd ||
331
+ !path.isAbsolute(cwd) ||
332
+ !/^sha256:[a-f0-9]{64}$/u.test(accountIdentitySignature)
333
+ ) {
334
+ throw createError(
335
+ "Codex helper runtime isolation could not be verified."
336
+ );
337
+ }
338
+ return Object.freeze({
339
+ accountIdentitySignature,
340
+ cwd,
341
+ executionMode: "helper"
342
+ });
343
+ }
344
+
345
+ function codexAppServerSemanticVersionParts(value = "") {
346
+ const match = normalizeText(value).match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u);
347
+ if (!match) {
348
+ return null;
349
+ }
350
+ const parts = match.slice(1).map((part) => Number.parseInt(part, 10));
351
+ return parts.every(Number.isSafeInteger) ? parts : null;
352
+ }
353
+
354
+ function codexAppServerUserAgentVersionParts(value = "") {
355
+ const userAgent = normalizeText(value);
356
+ if (
357
+ !userAgent ||
358
+ userAgent.length > userAgentMaxLength ||
359
+ codexAppServerTextHasControlCharacters(userAgent)
360
+ ) {
361
+ return null;
362
+ }
363
+ const prefix = `${clientName}/`;
364
+ if (!userAgent.startsWith(prefix)) {
365
+ return null;
366
+ }
367
+ const match = userAgent.slice(prefix.length).match(/^((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))(?:$|[ \t].*$)/u);
368
+ return match ? codexAppServerSemanticVersionParts(match[1]) : null;
369
+ }
370
+
371
+ function assertCodexAppServerHelperCompatibility(provider) {
372
+ if (typeof provider?.currentServerInfo !== "function") {
373
+ throw createError(
374
+ "Codex helper execution cannot verify the app-server version. Update Codex and retry.",
375
+ { minimumVersion }
376
+ );
377
+ }
378
+ const userAgent = normalizeText(provider.currentServerInfo()?.userAgent);
379
+ const actualParts = codexAppServerUserAgentVersionParts(userAgent);
380
+ if (!actualParts) {
381
+ throw createError(
382
+ "Codex helper execution received an unrecognised app-server version. Update Codex and retry.",
383
+ {
384
+ minimumVersion
385
+ }
386
+ );
387
+ }
388
+ const actualVersion = actualParts.join(".");
389
+ const minimumParts = codexAppServerSemanticVersionParts(minimumVersion);
390
+ const differingPart = actualParts.findIndex((part, index) => part !== minimumParts[index]);
391
+ if (differingPart !== -1 && actualParts[differingPart] < minimumParts[differingPart]) {
392
+ throw createError(
393
+ `Codex helper execution requires app-server ${minimumVersion} or newer; current version is ${actualVersion}. Update Codex and retry.`,
394
+ {
395
+ actualVersion,
396
+ minimumVersion
397
+ }
398
+ );
399
+ }
400
+ return Object.freeze({
401
+ minimumVersion,
402
+ version: actualVersion
403
+ });
404
+ }
405
+
406
+ function codexAppServerHelperMcpServerNames(configResult = null) {
407
+ if (!isPlainRecord(configResult?.config)) {
408
+ throw createError(
409
+ "Codex helper execution could not read the effective app-server configuration."
410
+ );
411
+ }
412
+ const servers = configResult.config.mcp_servers;
413
+ if (servers === undefined || servers === null) {
414
+ return [];
415
+ }
416
+ // Only the MCP inventory participates in this isolation check. The native
417
+ // response can also contain large model catalogues and configuration layers.
418
+ assertCodexAppServerHelperResponseBounded(
419
+ servers,
420
+ configResponseMaxBytes,
421
+ "MCP configuration"
422
+ );
423
+ if (!isPlainRecord(servers)) {
424
+ throw createError(
425
+ "Codex helper execution received an invalid MCP server configuration."
426
+ );
427
+ }
428
+ const names = Object.keys(servers);
429
+ if (
430
+ names.length > mcpServerMaxCount ||
431
+ names.some((name) => (
432
+ !name ||
433
+ name.length > mcpServerNameMaxLength ||
434
+ codexAppServerTextHasControlCharacters(name)
435
+ ))
436
+ ) {
437
+ throw createError(
438
+ "Codex helper execution received an oversized or invalid MCP server inventory."
439
+ );
440
+ }
441
+ return names.sort();
442
+ }
443
+
444
+ function codexAppServerHelperHookState(result = null, cwd = "") {
445
+ assertCodexAppServerHelperResponseBounded(
446
+ result,
447
+ hookResponseMaxBytes,
448
+ "hook inventory"
449
+ );
450
+ if (!Array.isArray(result?.data)) {
451
+ throw createError(
452
+ "Codex helper execution could not enumerate app-server hooks."
453
+ );
454
+ }
455
+ if (result.data.length !== 1) {
456
+ throw createError(
457
+ "Codex helper execution received an unexpected hook inventory."
458
+ );
459
+ }
460
+ const record = result.data[0];
461
+ if (
462
+ normalizeText(record?.cwd) !== cwd ||
463
+ normalizeText(record?.cwd).length > hookFieldMaxLength ||
464
+ !Array.isArray(record.hooks) ||
465
+ !Array.isArray(record.errors) ||
466
+ record.hooks.length > hookMaxCount ||
467
+ record.errors.length > hookErrorMaxCount
468
+ ) {
469
+ throw createError(
470
+ "Codex helper execution received an incomplete hook inventory."
471
+ );
472
+ }
473
+ if (record.errors.length > 0) {
474
+ throw createError(
475
+ "Codex helper execution cannot continue while hook discovery has errors.",
476
+ { hookErrorCount: record.errors.length }
477
+ );
478
+ }
479
+ const hooks = record.hooks.map((hook) => {
480
+ const key = normalizeText(hook?.key);
481
+ const currentHash = normalizeText(hook?.currentHash);
482
+ const handlerType = normalizeText(hook?.handlerType);
483
+ const sourcePath = normalizeText(hook?.sourcePath);
484
+ if (
485
+ !key ||
486
+ [key, currentHash, handlerType, sourcePath].some((field) => (
487
+ field.length > hookFieldMaxLength ||
488
+ codexAppServerTextHasControlCharacters(field)
489
+ ))
490
+ ) {
491
+ throw createError(
492
+ "Codex helper execution found an invalid hook inventory entry."
493
+ );
494
+ }
495
+ if (hook?.isManaged === true && hook?.enabled === true) {
496
+ throw createError(
497
+ "Codex helper execution cannot disable a managed hook."
498
+ );
499
+ }
500
+ return {
501
+ currentHash,
502
+ enabled: hook?.enabled === true,
503
+ handlerType,
504
+ isManaged: hook?.isManaged === true,
505
+ key,
506
+ sourcePath
507
+ };
508
+ }).sort((left, right) => left.key.localeCompare(right.key));
509
+ const fingerprint = JSON.stringify(hooks);
510
+ if (fingerprint.length > hookFingerprintMaxLength) {
511
+ throw createError(
512
+ "Codex helper execution received an oversized hook inventory."
513
+ );
514
+ }
515
+ return {
516
+ fingerprint,
517
+ hookKeys: hooks.filter((hook) => !hook.isManaged).map((hook) => hook.key)
518
+ };
519
+ }
520
+
521
+ function codexAppServerHelperIsolationConfig({
522
+ effort = "",
523
+ summary = "",
524
+ hookKeys = [],
525
+ mcpServerNames = []
526
+ } = {}) {
527
+ const config = {
528
+ features: Object.fromEntries(
529
+ disabledFeatures.map((feature) => [feature, false])
530
+ ),
531
+ hooks: {
532
+ state: Object.fromEntries(hookKeys.map((key) => [key, { enabled: false }]))
533
+ },
534
+ include_apps_instructions: false,
535
+ include_collaboration_mode_instructions: false,
536
+ include_environment_context: false,
537
+ include_permissions_instructions: false,
538
+ ...(effort ? { model_reasoning_effort: effort } : {}),
539
+ ...(summary ? { model_reasoning_summary: summary } : {}),
540
+ mcp_servers: Object.fromEntries(
541
+ mcpServerNames.map((name) => [name, { enabled: false }])
542
+ ),
543
+ memories: {
544
+ dedicated_tools: false,
545
+ generate_memories: false,
546
+ use_memories: false
547
+ },
548
+ notify: [],
549
+ orchestrator: {
550
+ mcp: {
551
+ enabled: false
552
+ },
553
+ skills: {
554
+ enabled: false
555
+ }
556
+ },
557
+ project_doc_max_bytes: 0,
558
+ shell_environment_policy: {
559
+ inherit: "none",
560
+ set: {}
561
+ },
562
+ skills: {
563
+ include_instructions: false
564
+ },
565
+ tools: {
566
+ experimental_request_user_input: {
567
+ enabled: false
568
+ },
569
+ update_plan: {
570
+ enabled: false
571
+ }
572
+ },
573
+ web_search: "disabled"
574
+ };
575
+ const frozenConfig = deepFreezeCodexAppServerHelperConfig(config);
576
+ codexAppServerHelperIsolationConfigs.add(frozenConfig);
577
+ return frozenConfig;
578
+ }
579
+
580
+ async function codexAppServerHelperIsolationState(provider, { effort = "", summary = "" } = {}) {
581
+ assertCodexAppServerHelperCompatibility(provider);
582
+ if (
583
+ typeof provider?.readConfig !== "function" ||
584
+ typeof provider?.listHooks !== "function"
585
+ ) {
586
+ throw createError(
587
+ "Codex helper execution cannot inventory configuration and hooks."
588
+ );
589
+ }
590
+ const executionContext = await codexAppServerHelperExecutionContext(provider);
591
+ const generation = codexAppServerHelperConnectionGeneration(provider);
592
+ const configResult = await provider.readConfig({
593
+ cwd: executionContext.cwd,
594
+ includeLayers: false
595
+ });
596
+ if (codexAppServerHelperConnectionGeneration(provider) !== generation) {
597
+ throw createError(
598
+ "Codex app-server reconnected during helper policy verification."
599
+ );
600
+ }
601
+ const hookResult = await provider.listHooks([executionContext.cwd]);
602
+ if (codexAppServerHelperConnectionGeneration(provider) !== generation) {
603
+ throw createError(
604
+ "Codex app-server reconnected during helper policy verification."
605
+ );
606
+ }
607
+ const mcpServerNames = codexAppServerHelperMcpServerNames(configResult);
608
+ const hookState = codexAppServerHelperHookState(hookResult, executionContext.cwd);
609
+ return Object.freeze({
610
+ accountIdentitySignature: executionContext.accountIdentitySignature,
611
+ config: codexAppServerHelperIsolationConfig({
612
+ effort,
613
+ summary,
614
+ hookKeys: hookState.hookKeys,
615
+ mcpServerNames
616
+ }),
617
+ connectionGeneration: generation,
618
+ executionCwd: executionContext.cwd,
619
+ hookFingerprint: hookState.fingerprint,
620
+ hookKeys: Object.freeze([...hookState.hookKeys]),
621
+ mcpServerNames: Object.freeze([...mcpServerNames])
622
+ });
623
+ }
624
+
625
+ function codexAppServerHelperIsolationMatches(left = null, right = null) {
626
+ return left?.accountIdentitySignature === right?.accountIdentitySignature &&
627
+ left?.connectionGeneration === right?.connectionGeneration &&
628
+ left?.executionCwd === right?.executionCwd &&
629
+ left?.hookFingerprint === right?.hookFingerprint &&
630
+ JSON.stringify(left?.mcpServerNames || []) === JSON.stringify(right?.mcpServerNames || []);
631
+ }
632
+
633
+ function codexAppServerHelperVerificationFailure(error, cleanupError, threadId = "") {
634
+ const normalizedThreadId = normalizeText(threadId);
635
+ if (!cleanupError) {
636
+ error.codexAppServerHelperThreadId = normalizedThreadId;
637
+ error.codexAppServerHelperThreadRetired = true;
638
+ return error;
639
+ }
640
+ const failure = createError(
641
+ "Codex could not retire a helper thread after policy verification failed.",
642
+ {
643
+ cleanupFailed: true,
644
+ threadId: normalizedThreadId
645
+ }
646
+ );
647
+ failure.codexAppServerHelperThreadCleanupRequired = true;
648
+ failure.codexAppServerHelperThreadId = normalizedThreadId;
649
+ return failure;
650
+ }
651
+
652
+ async function throwAfterCodexAppServerHelperVerificationFailure({
653
+ error = null,
654
+ provider = null,
655
+ threadId = ""
656
+ } = {}) {
657
+ let cleanupError = null;
658
+ try {
659
+ await provider.deleteThread(threadId);
660
+ } catch (caught) {
661
+ cleanupError = caught;
662
+ }
663
+ throw codexAppServerHelperVerificationFailure(error, cleanupError, threadId);
664
+ }
665
+
666
+ async function startCodexAppServerHelperThread(provider, prepare) {
667
+ if (
668
+ typeof provider?.startThread !== "function" ||
669
+ typeof provider?.deleteThread !== "function"
670
+ ) {
671
+ throw createError(
672
+ "Codex helper execution cannot own and clean up its app-server thread."
673
+ );
674
+ }
675
+ const prepared = await prepare();
676
+ const thread = await provider.startThread(prepared.settings);
677
+ const threadId = normalizeText(thread?.id);
678
+ if (!threadId) {
679
+ throw createError(
680
+ "Codex app-server did not return a helper thread id."
681
+ );
682
+ }
683
+ try {
684
+ const verified = await codexAppServerHelperIsolationState(
685
+ provider,
686
+ {
687
+ effort: prepared.enforcement.config.model_reasoning_effort,
688
+ summary: prepared.enforcement.config.model_reasoning_summary
689
+ }
690
+ );
691
+ if (!codexAppServerHelperIsolationMatches(prepared.enforcement, verified)) {
692
+ throw createError(
693
+ "Codex execution surfaces changed while the helper thread was starting."
694
+ );
695
+ }
696
+ return Object.freeze({
697
+ enforcement: verified,
698
+ thread,
699
+ threadId
700
+ });
701
+ } catch (error) {
702
+ await throwAfterCodexAppServerHelperVerificationFailure({
703
+ error,
704
+ provider,
705
+ threadId
706
+ });
707
+ }
708
+ }
709
+
710
+ async function resumeCodexAppServerHelperThread(provider, threadId, prepare) {
711
+ if (
712
+ typeof provider?.resumeThread !== "function" ||
713
+ typeof provider?.deleteThread !== "function"
714
+ ) {
715
+ throw createError(
716
+ "Codex helper execution cannot safely resume and clean up its app-server thread."
717
+ );
718
+ }
719
+ const normalizedThreadId = normalizeText(threadId);
720
+ if (!normalizedThreadId) {
721
+ throw createError(
722
+ "Codex helper resume requires a controller-owned thread id."
723
+ );
724
+ }
725
+ const prepared = await prepare();
726
+ const enforcement = prepared.enforcement;
727
+ const thread = await provider.resumeThread(
728
+ normalizedThreadId,
729
+ prepared.settings
730
+ );
731
+ try {
732
+ const verified = await codexAppServerHelperIsolationState(
733
+ provider,
734
+ {
735
+ effort: enforcement.config.model_reasoning_effort,
736
+ summary: enforcement.config.model_reasoning_summary
737
+ }
738
+ );
739
+ if (!codexAppServerHelperIsolationMatches(enforcement, verified)) {
740
+ throw createError(
741
+ "Codex execution surfaces changed while the helper thread was resuming."
742
+ );
743
+ }
744
+ return Object.freeze({
745
+ enforcement: verified,
746
+ thread,
747
+ threadId: normalizedThreadId
748
+ });
749
+ } catch (error) {
750
+ await throwAfterCodexAppServerHelperVerificationFailure({
751
+ error,
752
+ provider,
753
+ threadId: normalizedThreadId
754
+ });
755
+ }
756
+ }
757
+
758
+ function configuration({ configResult, hookResult, workdir, effort = "", summary = "" }) {
759
+ const mcpServerNames = codexAppServerHelperMcpServerNames(configResult);
760
+ const hookState = codexAppServerHelperHookState(hookResult, workdir);
761
+ return codexAppServerHelperIsolationConfig({
762
+ effort,
763
+ summary,
764
+ hookKeys: hookState.hookKeys,
765
+ mcpServerNames
766
+ });
767
+ }
768
+
769
+ // Native Helper requests retain their verified configuration object and the
770
+ // distinct Start/Resume field sets. Profile policy is validated by the host.
771
+ function codexAppServerHelperThreadSettings({
772
+ approvalPolicy,
773
+ baseInstructions,
774
+ config,
775
+ cwd,
776
+ effectiveSettings,
777
+ sandbox,
778
+ systemPrompt
779
+ }) {
780
+ return {
781
+ allowProviderModelFallback: false,
782
+ approvalPolicy,
783
+ baseInstructions,
784
+ config,
785
+ cwd,
786
+ systemPrompt: systemPrompt || null,
787
+ dynamicTools: [],
788
+ environments: [],
789
+ model: effectiveSettings.model,
790
+ runtimeWorkspaceRoots: [],
791
+ sandbox,
792
+ selectedCapabilityRoots: []
793
+ };
794
+ }
795
+
796
+ function codexAppServerHelperThreadStartSettings(settings, threadSource) {
797
+ return {
798
+ ...settings,
799
+ sessionStartSource: "startup",
800
+ threadSource
801
+ };
802
+ }
803
+
804
+ function codexAppServerHelperThreadResumeSettings(settings) {
805
+ return {
806
+ approvalPolicy: settings.approvalPolicy,
807
+ baseInstructions: settings.baseInstructions,
808
+ config: settings.config,
809
+ cwd: settings.cwd,
810
+ systemPrompt: settings.systemPrompt,
811
+ model: settings.model,
812
+ runtimeWorkspaceRoots: settings.runtimeWorkspaceRoots,
813
+ sandbox: settings.sandbox
814
+ };
815
+ }
816
+
817
+ function codexAppServerHelperTurnSettings({
818
+ approvalPolicy,
819
+ cwd,
820
+ effectiveSettings,
821
+ outputSchema
822
+ }) {
823
+ const settings = {
824
+ approvalPolicy,
825
+ cwd,
826
+ environments: [],
827
+ model: effectiveSettings.model,
828
+ outputSchema,
829
+ runtimeWorkspaceRoots: [],
830
+ sandboxPolicy: {
831
+ networkAccess: false,
832
+ type: "readOnly"
833
+ },
834
+ summary: "none"
835
+ };
836
+ if (effectiveSettings.request.reasoning) {
837
+ settings.effort = effectiveSettings.thinking;
838
+ }
839
+ return settings;
840
+ }
841
+
842
+ return Object.freeze({
843
+ assertCompatibility: assertCodexAppServerHelperCompatibility,
844
+ executionContext: codexAppServerHelperExecutionContext,
845
+ inspect: codexAppServerHelperIsolationState,
846
+ configuration,
847
+ hasConfiguration: (config) => codexAppServerHelperIsolationConfigs.has(config),
848
+ threadSettings: codexAppServerHelperThreadSettings,
849
+ threadStartSettings: codexAppServerHelperThreadStartSettings,
850
+ threadResumeSettings: codexAppServerHelperThreadResumeSettings,
851
+ turnSettings: codexAppServerHelperTurnSettings,
852
+ start: startCodexAppServerHelperThread,
853
+ resume: resumeCodexAppServerHelperThread
854
+ });
855
+ }
856
+
857
+ const codexConversationIsolation = createCodexAppServerIsolation();
858
+
859
+ /** Disable ambient tools while allowing the caller to add its authorized tools. */
860
+ export function codexToolFreeConfiguration(options) {
861
+ return structuredClone(codexConversationIsolation.configuration(options));
862
+ }
863
+
864
+ export async function readCodexToolFreeConfiguration(
865
+ provider = null,
866
+ workdir = "",
867
+ assertCompatibility = codexConversationIsolation.assertCompatibility
868
+ ) {
869
+ assertCompatibility(provider);
870
+ if (
871
+ typeof provider?.readConfig !== "function" ||
872
+ typeof provider?.listHooks !== "function" ||
873
+ typeof provider?.currentConnectionGeneration !== "function"
874
+ ) {
875
+ throw new Error("Codex cannot verify tool isolation for this ephemeral conversation.");
876
+ }
877
+ const connectionGeneration = provider.currentConnectionGeneration();
878
+ if (!Number.isSafeInteger(connectionGeneration) || connectionGeneration <= 0) {
879
+ throw new Error("Codex has no active connection for this ephemeral conversation.");
880
+ }
881
+ const [configResult, hookResult] = await Promise.all([
882
+ provider.readConfig({ cwd: workdir, includeLayers: false }),
883
+ provider.listHooks([workdir])
884
+ ]);
885
+ if (provider.currentConnectionGeneration() !== connectionGeneration) {
886
+ throw new Error("Codex reconnected while verifying this ephemeral conversation.");
887
+ }
888
+ return codexToolFreeConfiguration({ configResult, hookResult, workdir });
889
+ }
890
+
891
+ /** Interpret native catalogue responses without choosing application offerings. */
892
+ export function codexCatalogRows(value = null, createError = message => new Error(message)) {
893
+ const rows = Array.isArray(value) ? value : value?.data;
894
+ if (!Array.isArray(rows)) {
895
+ throw createError("Codex did not return a usable live model catalog.");
896
+ }
897
+ return rows;
898
+ }
899
+
900
+ export function codexCatalogReasoningEfforts(model = {}) {
901
+ return new Set((Array.isArray(model?.supportedReasoningEfforts)
902
+ ? model.supportedReasoningEfforts
903
+ : [])
904
+ .map((option) => normalizeText(option?.reasoningEffort))
905
+ .filter(Boolean));
906
+ }
907
+
908
+ export function codexCatalogModels(rows) {
909
+ return rows.map((model) => ({
910
+ id: normalizeText(model.model),
911
+ label: normalizeText(model.displayName) || normalizeText(model.model),
912
+ status: "available",
913
+ variants: [...codexCatalogReasoningEfforts(model)]
914
+ .map((variantId) => ({
915
+ id: normalizeText(variantId),
916
+ label: normalizeText(variantId).replace(/^./u, (value) => value.toUpperCase())
917
+ }))
918
+ }));
919
+ }
920
+
921
+ export function codexConfiguredModelCatalog(includeModel, model, reasoningEffort) {
922
+ return { data: includeModel ? [{
923
+ model,
924
+ defaultReasoningEffort: reasoningEffort,
925
+ supportedReasoningEfforts: [{ reasoningEffort }]
926
+ }] : [] };
927
+ }
928
+
929
+ /** Native catalogue fields for the exact models selected by the application. */
930
+ export function codexProviderModelCatalog({ models }) {
931
+ return {
932
+ models: models.map((model, priority) => ({
933
+ slug: model.id,
934
+ display_name: model.label,
935
+ description: model.label,
936
+ default_reasoning_level: model.defaultThinking,
937
+ supported_reasoning_levels: model.variants.map((effort) => ({ effort, description: effort })),
938
+ shell_type: "shell_command",
939
+ visibility: "list",
940
+ supported_in_api: true,
941
+ priority,
942
+ base_instructions: "You are a coding assistant. Complete the user's task in the workspace, use the available tools, preserve unrelated work, and verify your changes.",
943
+ supports_reasoning_summaries: false,
944
+ default_reasoning_summary: "none",
945
+ support_verbosity: false,
946
+ truncation_policy: { mode: "bytes", limit: 10000 },
947
+ context_window: model.contextWindow,
948
+ max_context_window: model.contextWindow,
949
+ effective_context_window_percent: 95,
950
+ supports_parallel_tool_calls: true,
951
+ experimental_supported_tools: [],
952
+ input_modalities: model.images ? ["text", "image"] : ["text"],
953
+ prefer_websockets: false,
954
+ ...(model.freeformPatch ? { apply_patch_tool_type: "freeform" } : {})
955
+ }))
956
+ };
957
+ }
958
+
959
+ /** Private native configuration; credentials must never enter browser responses. */
960
+ export function codexProviderConfiguration(provider, apiKey) {
961
+ return {
962
+ model_reasoning_summary: "none",
963
+ web_search: provider.webSearch ? "live" : "disabled",
964
+ [`model_providers.${provider.id}`]: {
965
+ name: provider.label || provider.id, base_url: provider.baseUrl, wire_api: "responses",
966
+ requires_openai_auth: false, experimental_bearer_token: apiKey
967
+ }
968
+ };
969
+ }
970
+
971
+ /** Format a private native config file from an already-authorized provider and key. */
972
+ export function codexProviderFileConfiguration(provider, key, codexHome) {
973
+ return [
974
+ `model = ${JSON.stringify(provider.models[0].id)}`,
975
+ `model_provider = ${JSON.stringify(provider.id)}`,
976
+ `model_catalog_json = ${JSON.stringify(path.join(codexHome, "models.json"))}`,
977
+ `model_reasoning_effort = ${JSON.stringify(provider.models[0].defaultThinking)}`,
978
+ 'model_reasoning_summary = "none"',
979
+ `web_search = "${provider.webSearch ? "live" : "disabled"}"`,
980
+ `[model_providers.${provider.id}]`,
981
+ `name = ${JSON.stringify(provider.label)}`,
982
+ `base_url = ${JSON.stringify(provider.baseUrl)}`,
983
+ 'wire_api = "responses"',
984
+ 'requires_openai_auth = false',
985
+ `experimental_bearer_token = ${JSON.stringify(key)}`,
986
+ ""
987
+ ].join("\n");
988
+ }
989
+
990
+ export async function verifyCodexProviderKey(provider, apiKey, fetchImpl = fetch) {
991
+ let response;
992
+ try {
993
+ response = await fetchImpl(`${provider.baseUrl.replace(/\/$/u, "")}/responses`, {
994
+ method: "POST",
995
+ redirect: "error",
996
+ signal: AbortSignal.timeout(30000),
997
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
998
+ body: JSON.stringify({
999
+ model: provider.models[0].id,
1000
+ input: "Reply with OK.",
1001
+ reasoning: { effort: "low" },
1002
+ max_output_tokens: 64,
1003
+ store: false
1004
+ })
1005
+ });
1006
+ } catch {
1007
+ throw new Error(`${provider.label} could not be reached. Your previous connection is unchanged; try again.`);
1008
+ }
1009
+ // Never expose a provider's raw response, which can contain request details.
1010
+ if (!response.ok) {
1011
+ await response.body?.cancel?.();
1012
+ throw new Error(response.status === 401 || response.status === 403
1013
+ ? `${provider.label} rejected this key. Check the key and the required account plan.`
1014
+ : `${provider.label} could not complete a test request (HTTP ${response.status}). Check your plan or API credit.`);
1015
+ }
1016
+ const result = await response.json().catch(() => null);
1017
+ if (!result?.id || result.error || !Array.isArray(result.output) ||
1018
+ !["completed", "incomplete"].includes(result.status)) {
1019
+ throw new Error(`${provider.label} did not return a usable Responses API result. The key was not saved.`);
1020
+ }
1021
+ }
1022
+
1023
+ const CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES = 1024 * 1024;
1024
+ const CODEX_APP_SERVER_CHATGPT_ACCESS_TOKEN_MAX_LENGTH = 256 * 1024;
1025
+ export const CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH = 512;
1026
+ const CODEX_APP_SERVER_PLAN_TYPE_MAX_LENGTH = 128;
1027
+ const CODEX_APP_SERVER_API_KEY_MAX_LENGTH = 16 * 1024;
1028
+
1029
+ /** Native account records and secret-change detection. Hosts select the credential home. */
1030
+ export function createCodexAccountReader({
1031
+ errorPrefix = "",
1032
+ identityNamespace = "codex-account-v1",
1033
+ secretNamespace = "codex-auth-secret-v1"
1034
+ } = {}) {
1035
+ function codexAppServerHelperAuthError(code = "", message = "") {
1036
+ const error = new Error(
1037
+ normalizeText(message) || "Codex helper authentication is unavailable. Reconnect Codex and retry."
1038
+ );
1039
+ error.code = normalizeText(code) || `${errorPrefix}codex_helper_auth_unavailable`;
1040
+ return error;
1041
+ }
1042
+
1043
+ function boundedCodexAuthText(value, {
1044
+ label = "Codex authentication value",
1045
+ maxLength = 0
1046
+ } = {}) {
1047
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
1048
+ throw codexAppServerHelperAuthError(
1049
+ `${errorPrefix}codex_helper_auth_invalid`,
1050
+ `${label} is missing or invalid. Reconnect Codex and retry.`
1051
+ );
1052
+ }
1053
+ return value;
1054
+ }
1055
+
1056
+ function codexAccountIdentitySignature(authMode = "", identity = "") {
1057
+ return `sha256:${createHash("sha256")
1058
+ .update(`${identityNamespace}\0`, "utf8")
1059
+ .update(normalizeText(authMode), "utf8")
1060
+ .update("\0", "utf8")
1061
+ .update(String(identity || ""), "utf8")
1062
+ .digest("hex")}`;
1063
+ }
1064
+
1065
+ function codexAuthSecretSignature(secret = "") {
1066
+ return createHash("sha256")
1067
+ .update(`${secretNamespace}\0`, "utf8")
1068
+ .update(String(secret || ""), "utf8")
1069
+ .digest("hex");
1070
+ }
1071
+
1072
+ async function readBoundedCodexAuthJson(filePath = "") {
1073
+ let handle = null;
1074
+ try {
1075
+ handle = await open(filePath, "r");
1076
+ const fileStat = await handle.stat();
1077
+ if (!fileStat.isFile() || fileStat.size > CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES) {
1078
+ throw codexAppServerHelperAuthError(
1079
+ `${errorPrefix}codex_helper_auth_invalid`,
1080
+ "The selected Codex authentication record is invalid. Reconnect Codex and retry."
1081
+ );
1082
+ }
1083
+ const buffer = Buffer.alloc(CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES + 1);
1084
+ let offset = 0;
1085
+ while (offset < buffer.length) {
1086
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, null);
1087
+ if (bytesRead === 0) {
1088
+ break;
1089
+ }
1090
+ offset += bytesRead;
1091
+ }
1092
+ if (offset > CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES) {
1093
+ throw codexAppServerHelperAuthError(
1094
+ `${errorPrefix}codex_helper_auth_invalid`,
1095
+ "The selected Codex authentication record is too large. Reconnect Codex and retry."
1096
+ );
1097
+ }
1098
+ const parsed = JSON.parse(buffer.subarray(0, offset).toString("utf8"));
1099
+ if (!isRecord(parsed)) {
1100
+ throw new Error("invalid authentication record");
1101
+ }
1102
+ return parsed;
1103
+ } catch (error) {
1104
+ if (error?.code?.startsWith?.(`${errorPrefix}codex_helper_auth_`)) {
1105
+ throw error;
1106
+ }
1107
+ throw codexAppServerHelperAuthError(
1108
+ `${errorPrefix}codex_helper_auth_unavailable`,
1109
+ "The selected Codex authentication record could not be read. Reconnect Codex and retry."
1110
+ );
1111
+ } finally {
1112
+ await handle?.close?.().catch(() => null);
1113
+ }
1114
+ }
1115
+
1116
+ async function readCodexSelectedAccountAuth(options = {}) {
1117
+ const toolHomeSource = normalizeText(options.toolHomeSource);
1118
+ if (!toolHomeSource || !path.isAbsolute(toolHomeSource)) {
1119
+ throw codexAppServerHelperAuthError(
1120
+ `${errorPrefix}codex_helper_auth_unavailable`,
1121
+ "The selected Codex account is unavailable. Reconnect Codex and retry."
1122
+ );
1123
+ }
1124
+ const auth = await readBoundedCodexAuthJson(path.join(toolHomeSource, ".codex", "auth.json"));
1125
+ const storedMode = normalizeText(auth.auth_mode).toLowerCase();
1126
+ if (storedMode === "chatgpt") {
1127
+ const tokens = isRecord(auth.tokens) ? auth.tokens : {};
1128
+ const accountId = boundedCodexAuthText(tokens.account_id, {
1129
+ label: "Codex account identity",
1130
+ maxLength: CODEX_APP_SERVER_ACCOUNT_ID_MAX_LENGTH
1131
+ });
1132
+ const accessToken = boundedCodexAuthText(tokens.access_token, {
1133
+ label: "Codex access token",
1134
+ maxLength: CODEX_APP_SERVER_CHATGPT_ACCESS_TOKEN_MAX_LENGTH
1135
+ });
1136
+ const planTypeValue = tokens.chatgpt_plan_type ?? tokens.plan_type;
1137
+ const planType = planTypeValue === undefined || planTypeValue === null || planTypeValue === ""
1138
+ ? ""
1139
+ : boundedCodexAuthText(planTypeValue, {
1140
+ label: "Codex plan type",
1141
+ maxLength: CODEX_APP_SERVER_PLAN_TYPE_MAX_LENGTH
1142
+ });
1143
+ return Object.freeze({
1144
+ accessToken,
1145
+ accountId,
1146
+ authMode: "chatgpt",
1147
+ identitySignature: codexAccountIdentitySignature("chatgpt", accountId),
1148
+ planType,
1149
+ secretSignature: codexAuthSecretSignature(accessToken)
1150
+ });
1151
+ }
1152
+ if (["apikey", "api_key"].includes(storedMode)) {
1153
+ const apiKey = boundedCodexAuthText(auth.OPENAI_API_KEY, {
1154
+ label: "Codex API key",
1155
+ maxLength: CODEX_APP_SERVER_API_KEY_MAX_LENGTH
1156
+ });
1157
+ return Object.freeze({
1158
+ apiKey,
1159
+ authMode: "apiKey",
1160
+ identitySignature: codexAccountIdentitySignature("apiKey", apiKey),
1161
+ secretSignature: codexAuthSecretSignature(apiKey)
1162
+ });
1163
+ }
1164
+ throw codexAppServerHelperAuthError(
1165
+ `${errorPrefix}codex_helper_auth_invalid`,
1166
+ "The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
1167
+ );
1168
+ }
1169
+
1170
+ return {
1171
+ error: codexAppServerHelperAuthError,
1172
+ identitySignature: codexAccountIdentitySignature,
1173
+ readRecord: readBoundedCodexAuthJson,
1174
+ readSelected: readCodexSelectedAccountAuth
1175
+ };
1176
+ }
1177
+
1178
+ const CODEX_AUTH_INVALIDATED_PATTERN =
1179
+ /\b(?:token_invalidated|refresh_token_invalidated)\b|authentication token has been invalidated|HTTP error:\s*401 Unauthorized|401 Unauthorized/iu;
1180
+
1181
+ export function codexAuthOutputRequiresReconnect(output = "") {
1182
+ return CODEX_AUTH_INVALIDATED_PATTERN.test(String(output || ""));
1183
+ }