@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.0

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 (155) hide show
  1. package/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
  2. package/gui/dist/assets/index-DO8liQVL.js +112 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +2 -1
  5. package/src/AGENTS.md +2 -1
  6. package/src/adapters/agentrouter.ts +50 -0
  7. package/src/adapters/anthropic.ts +1 -51
  8. package/src/adapters/cursor/call-id.ts +76 -8
  9. package/src/adapters/cursor/checkpoint-store.ts +6 -1
  10. package/src/adapters/cursor/cursor-errors.ts +44 -0
  11. package/src/adapters/cursor/native-exec.ts +13 -0
  12. package/src/adapters/cursor/protobuf-request.ts +651 -29
  13. package/src/adapters/cursor/tool-result-normalize.ts +3 -3
  14. package/src/adapters/cursor/transport-retry.ts +5 -1
  15. package/src/adapters/cursor.ts +15 -1
  16. package/src/adapters/empty-tool-output-annotation.ts +43 -0
  17. package/src/adapters/exec-tool-result-normalize.ts +70 -5
  18. package/src/adapters/google.ts +22 -2
  19. package/src/adapters/kiro.ts +26 -2
  20. package/src/adapters/ollama-native-url.ts +111 -0
  21. package/src/adapters/ollama-native.ts +1131 -0
  22. package/src/adapters/openai-chat.ts +30 -7
  23. package/src/adapters/openai-responses.ts +72 -4
  24. package/src/adapters/registry.ts +7 -0
  25. package/src/adapters/xai-web-search.ts +58 -0
  26. package/src/claude/desktop-3p.ts +21 -1
  27. package/src/claude/desktop-policy.ts +149 -0
  28. package/src/cli/account.ts +16 -2
  29. package/src/cli/claude-desktop.ts +13 -3
  30. package/src/cli/combo.ts +8 -5
  31. package/src/cli/doctor.ts +77 -11
  32. package/src/cli/help.ts +1 -1
  33. package/src/cli/index.ts +16 -0
  34. package/src/cli/models.ts +20 -3
  35. package/src/cli/registry.ts +2 -1
  36. package/src/cli/status.ts +140 -2
  37. package/src/cli/storage.ts +10 -1
  38. package/src/codex/account-runtime-state.ts +39 -5
  39. package/src/codex/account-store.ts +393 -13
  40. package/src/codex/account-usability.ts +11 -4
  41. package/src/codex/app-server-processes.ts +46 -5
  42. package/src/codex/auth-context.ts +160 -32
  43. package/src/codex/catalog/bundled.ts +7 -5
  44. package/src/codex/catalog/metadata.ts +1 -1
  45. package/src/codex/catalog/parsing.ts +57 -1
  46. package/src/codex/catalog/provider-fetch.ts +61 -4
  47. package/src/codex/catalog/sync.ts +4 -3
  48. package/src/codex/convergence.ts +3 -2
  49. package/src/codex/data/upstream-models.json +40 -8
  50. package/src/codex/inject-coordination.ts +111 -14
  51. package/src/codex/integration-record.ts +12 -2
  52. package/src/codex/main-account.ts +225 -1
  53. package/src/codex/model-entitlements.ts +339 -27
  54. package/src/codex/prompt-layers.ts +346 -7
  55. package/src/codex/prompt-text-probe.ts +272 -21
  56. package/src/codex/routing.ts +693 -132
  57. package/src/codex/runtime.ts +12 -0
  58. package/src/codex/subagent-model-fallback.ts +62 -24
  59. package/src/codex/user-identity.ts +33 -25
  60. package/src/combos/index.ts +1 -0
  61. package/src/combos/reset-window.ts +46 -0
  62. package/src/combos/resolve.ts +84 -2
  63. package/src/combos/types.ts +5 -2
  64. package/src/config/atomic-write.ts +104 -22
  65. package/src/config/provider-validation.ts +11 -0
  66. package/src/config.ts +75 -3
  67. package/src/generated/compatibility-version.json +207 -131
  68. package/src/generated/model-metadata.ts +1 -1
  69. package/src/grok/catalog.ts +71 -0
  70. package/src/grok/effort.ts +83 -0
  71. package/src/grok/inject.ts +952 -127
  72. package/src/grok/models.ts +56 -0
  73. package/src/grok/status.ts +21 -8
  74. package/src/grok/sync.ts +10 -18
  75. package/src/images/loop.ts +6 -3
  76. package/src/integrations/native/ownership-preflight.ts +4 -1
  77. package/src/lab/fabric/producer-isolate.ts +36 -3
  78. package/src/lib/destination-policy.ts +93 -7
  79. package/src/lib/redact.ts +6 -1
  80. package/src/lib/shadow-call.ts +38 -3
  81. package/src/lib/test-home-guard.ts +18 -3
  82. package/src/lib/upstream-retry.ts +43 -6
  83. package/src/lib/windows-secret-acl.ts +66 -0
  84. package/src/lib/windows-text.ts +28 -2
  85. package/src/lib/windows-user-principal.ts +35 -23
  86. package/src/oauth/account-quota-rank.ts +107 -0
  87. package/src/oauth/anthropic-routing.ts +125 -30
  88. package/src/oauth/chatgpt.ts +5 -1
  89. package/src/oauth/generic-account-failover.ts +114 -7
  90. package/src/oauth/index.ts +15 -8
  91. package/src/oauth/store.ts +16 -0
  92. package/src/providers/account-quota-disk.ts +79 -0
  93. package/src/providers/command-code-efforts.ts +24 -0
  94. package/src/providers/derive.ts +6 -0
  95. package/src/providers/key-failover.ts +33 -1
  96. package/src/providers/kiro-usage.ts +272 -0
  97. package/src/providers/ollama-show.ts +311 -0
  98. package/src/providers/openai-sidecar.ts +5 -0
  99. package/src/providers/quota-routing-cache.ts +32 -0
  100. package/src/providers/quota-types.ts +36 -0
  101. package/src/providers/quota-wire.ts +102 -0
  102. package/src/providers/quota.ts +208 -147
  103. package/src/providers/registry.ts +68 -8
  104. package/src/providers/slug-codec.ts +12 -4
  105. package/src/providers/vercel-gateway-routing.ts +108 -0
  106. package/src/router.ts +22 -12
  107. package/src/server/auth-cors.ts +26 -0
  108. package/src/server/catalog-download.ts +73 -0
  109. package/src/server/chat-native.ts +12 -2
  110. package/src/server/gui-static.ts +4 -1
  111. package/src/server/index.ts +132 -9
  112. package/src/server/management/agent-settings-routes.ts +38 -5
  113. package/src/server/management/codex-prompt-routes.ts +7 -1
  114. package/src/server/management/combo-routes.ts +10 -1
  115. package/src/server/management/config-routes.ts +9 -1
  116. package/src/server/management/context.ts +5 -0
  117. package/src/server/management/model-routes.ts +16 -6
  118. package/src/server/management/native-integration-routes.ts +12 -17
  119. package/src/server/management/oauth-account-routes.ts +13 -0
  120. package/src/server/management/provider-routes.ts +32 -5
  121. package/src/server/management/routing-profile-routes.ts +15 -0
  122. package/src/server/management/shadow-call-validation.ts +29 -0
  123. package/src/server/management-api.ts +7 -3
  124. package/src/server/request-log.ts +3 -5
  125. package/src/server/responses/agent-task-recovery-cache.ts +8 -0
  126. package/src/server/responses/agent-task-recovery.ts +52 -20
  127. package/src/server/responses/codex-auth-error.ts +26 -0
  128. package/src/server/responses/compact.ts +345 -10
  129. package/src/server/responses/core.ts +736 -108
  130. package/src/server/responses/empty-completion-guard.ts +16 -0
  131. package/src/server/responses/fetch-helpers.ts +42 -0
  132. package/src/server/responses/policy-fallback.ts +11 -6
  133. package/src/server/responses-undeclared-tool-guard.ts +16 -3
  134. package/src/server/startup-health-cache.ts +59 -13
  135. package/src/service-manager-probe.ts +115 -9
  136. package/src/service.ts +139 -40
  137. package/src/storage/cleanup.ts +10 -0
  138. package/src/storage/storage-mutation-coordinator.ts +14 -3
  139. package/src/tray/windows-tray.ps1 +10 -4
  140. package/src/tray/windows.ts +30 -2
  141. package/src/types/config.ts +27 -14
  142. package/src/types/provider.ts +54 -0
  143. package/src/types/tools.ts +13 -3
  144. package/src/types.ts +4 -0
  145. package/src/usage/summary.ts +421 -177
  146. package/src/vision/anthropic-describe.ts +3 -3
  147. package/src/vision/describe.ts +5 -3
  148. package/src/web-search/anthropic-executor.ts +9 -2
  149. package/src/web-search/exa-executor.ts +3 -3
  150. package/src/web-search/executor.ts +8 -3
  151. package/src/web-search/gemini-executor.ts +3 -3
  152. package/src/web-search/loop.ts +11 -3
  153. package/src/web-search/xai-executor.ts +3 -3
  154. package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
  155. package/gui/dist/assets/index-DX0RGj3e.js +0 -112
@@ -21,6 +21,10 @@
21
21
  "multi_agent_version": "v2",
22
22
  "use_responses_lite": true,
23
23
  "include_skills_usage_instructions": false,
24
+ "node_repl_disabled": false,
25
+ "node_repl_auto_review_required": false,
26
+ "include_plugin_usage_instructions": true,
27
+ "include_apps_usage_instructions": true,
24
28
  "auto_review_model_override": null,
25
29
  "context_window": 372000,
26
30
  "max_context_window": 372000,
@@ -57,7 +61,7 @@
57
61
  "description": "Maximum reasoning with automatic task delegation"
58
62
  }
59
63
  ],
60
- "shell_type": "shell_command",
64
+ "shell_type": "unified_exec",
61
65
  "visibility": "list",
62
66
  "minimal_client_version": "0.142.2",
63
67
  "supported_in_api": true,
@@ -135,6 +139,10 @@
135
139
  "multi_agent_version": "v2",
136
140
  "use_responses_lite": true,
137
141
  "include_skills_usage_instructions": false,
142
+ "node_repl_disabled": false,
143
+ "node_repl_auto_review_required": false,
144
+ "include_plugin_usage_instructions": true,
145
+ "include_apps_usage_instructions": true,
138
146
  "auto_review_model_override": null,
139
147
  "context_window": 372000,
140
148
  "max_context_window": 372000,
@@ -171,7 +179,7 @@
171
179
  "description": "Maximum reasoning with automatic task delegation"
172
180
  }
173
181
  ],
174
- "shell_type": "shell_command",
182
+ "shell_type": "unified_exec",
175
183
  "visibility": "list",
176
184
  "minimal_client_version": "0.142.2",
177
185
  "supported_in_api": true,
@@ -247,6 +255,10 @@
247
255
  "multi_agent_version": "v1",
248
256
  "use_responses_lite": true,
249
257
  "include_skills_usage_instructions": false,
258
+ "node_repl_disabled": false,
259
+ "node_repl_auto_review_required": false,
260
+ "include_plugin_usage_instructions": true,
261
+ "include_apps_usage_instructions": true,
250
262
  "auto_review_model_override": null,
251
263
  "context_window": 372000,
252
264
  "max_context_window": 372000,
@@ -279,7 +291,7 @@
279
291
  "description": "Maximum reasoning depth for the hardest problems"
280
292
  }
281
293
  ],
282
- "shell_type": "shell_command",
294
+ "shell_type": "unified_exec",
283
295
  "visibility": "list",
284
296
  "minimal_client_version": "0.142.2",
285
297
  "supported_in_api": true,
@@ -355,6 +367,10 @@
355
367
  "multi_agent_version": null,
356
368
  "use_responses_lite": false,
357
369
  "include_skills_usage_instructions": true,
370
+ "node_repl_disabled": false,
371
+ "node_repl_auto_review_required": false,
372
+ "include_plugin_usage_instructions": true,
373
+ "include_apps_usage_instructions": true,
358
374
  "auto_review_model_override": null,
359
375
  "context_window": 272000,
360
376
  "max_context_window": 272000,
@@ -383,7 +399,7 @@
383
399
  "description": "Extra high reasoning depth for complex problems"
384
400
  }
385
401
  ],
386
- "shell_type": "shell_command",
402
+ "shell_type": "unified_exec",
387
403
  "visibility": "list",
388
404
  "minimal_client_version": "0.124.0",
389
405
  "supported_in_api": true,
@@ -461,6 +477,10 @@
461
477
  "multi_agent_version": null,
462
478
  "use_responses_lite": false,
463
479
  "include_skills_usage_instructions": false,
480
+ "node_repl_disabled": false,
481
+ "node_repl_auto_review_required": false,
482
+ "include_plugin_usage_instructions": true,
483
+ "include_apps_usage_instructions": true,
464
484
  "auto_review_model_override": null,
465
485
  "context_window": 272000,
466
486
  "max_context_window": 1000000,
@@ -489,7 +509,7 @@
489
509
  "description": "Extra high reasoning depth for complex problems"
490
510
  }
491
511
  ],
492
- "shell_type": "shell_command",
512
+ "shell_type": "unified_exec",
493
513
  "visibility": "list",
494
514
  "minimal_client_version": "0.98.0",
495
515
  "supported_in_api": true,
@@ -562,6 +582,10 @@
562
582
  "multi_agent_version": null,
563
583
  "use_responses_lite": false,
564
584
  "include_skills_usage_instructions": false,
585
+ "node_repl_disabled": false,
586
+ "node_repl_auto_review_required": false,
587
+ "include_plugin_usage_instructions": true,
588
+ "include_apps_usage_instructions": true,
565
589
  "auto_review_model_override": null,
566
590
  "context_window": 272000,
567
591
  "max_context_window": 272000,
@@ -590,7 +614,7 @@
590
614
  "description": "Extra high reasoning depth for complex problems"
591
615
  }
592
616
  ],
593
- "shell_type": "shell_command",
617
+ "shell_type": "unified_exec",
594
618
  "visibility": "list",
595
619
  "minimal_client_version": "0.98.0",
596
620
  "supported_in_api": true,
@@ -658,6 +682,10 @@
658
682
  "multi_agent_version": null,
659
683
  "use_responses_lite": false,
660
684
  "include_skills_usage_instructions": false,
685
+ "node_repl_disabled": false,
686
+ "node_repl_auto_review_required": false,
687
+ "include_plugin_usage_instructions": true,
688
+ "include_apps_usage_instructions": true,
661
689
  "auto_review_model_override": null,
662
690
  "context_window": 272000,
663
691
  "max_context_window": 272000,
@@ -686,7 +714,7 @@
686
714
  "description": "Extra high reasoning for complex problems"
687
715
  }
688
716
  ],
689
- "shell_type": "shell_command",
717
+ "shell_type": "unified_exec",
690
718
  "visibility": "list",
691
719
  "minimal_client_version": "0.0.1",
692
720
  "supported_in_api": true,
@@ -754,6 +782,10 @@
754
782
  "multi_agent_version": null,
755
783
  "use_responses_lite": false,
756
784
  "include_skills_usage_instructions": false,
785
+ "node_repl_disabled": false,
786
+ "node_repl_auto_review_required": false,
787
+ "include_plugin_usage_instructions": true,
788
+ "include_apps_usage_instructions": true,
757
789
  "auto_review_model_override": null,
758
790
  "context_window": 272000,
759
791
  "max_context_window": 1000000,
@@ -782,7 +814,7 @@
782
814
  "description": "Extra high reasoning depth for complex problems"
783
815
  }
784
816
  ],
785
- "shell_type": "shell_command",
817
+ "shell_type": "unified_exec",
786
818
  "visibility": "hide",
787
819
  "minimal_client_version": "0.98.0",
788
820
  "supported_in_api": true,
@@ -16,6 +16,7 @@ import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
16
16
  import type {
17
17
  CodexArtifactId,
18
18
  CodexProvenanceEntry,
19
+ CodexProvenanceLedger,
19
20
  } from "./convergence-types";
20
21
  import {
21
22
  codexWriteCoordination,
@@ -196,6 +197,41 @@ export function captureCodexPreImages(): CodexPreImages {
196
197
  */
197
198
  export const CODEX_PROVENANCE_MAX_TRANSACTIONS = 16;
198
199
 
200
+ /**
201
+ * How many serialized bytes of evidence the ledger keeps.
202
+ *
203
+ * The transaction window alone bounds the ledger only if each transaction is small, and a
204
+ * baseline embeds the artifact's exact bytes. Those artifacts sit outside this proxy's trust
205
+ * boundary: a native `config.toml` grown to hundreds of megabytes is copied into the record as
206
+ * base64 and re-serialized on every append, so a single oversized file turns a 16-transaction
207
+ * window into a multi-gigabyte write.
208
+ *
209
+ * The size is derived from the window above rather than picked: the 25 KB `config.toml` that
210
+ * comment describes measures 100.8 KiB per transaction, so a full 16-transaction window is
211
+ * 1.58 MiB. 4 MiB leaves that ordinary window untouched with room to spare while still refusing
212
+ * the pathological case, and a regression test pins the ordinary window so the two cannot drift
213
+ * apart silently.
214
+ */
215
+ export const CODEX_PROVENANCE_MAX_BYTES = 4 * 1024 * 1024;
216
+
217
+ /**
218
+ * Measure the representation the integration-record writer actually emits.
219
+ *
220
+ * The minimal wrapper reproduces the final indentation depth of `provenance.entries`, including
221
+ * structured unknown extension fields preserved from older records. Its fixed wrapper makes this
222
+ * slightly conservative as a ledger-only ceiling, which is preferable to admitting an entry that
223
+ * expands past the limit when `JSON.stringify(record, null, 2)` writes it.
224
+ */
225
+ function serializedProvenanceBytes(
226
+ entries: readonly CodexProvenanceEntry[],
227
+ ledger: CodexProvenanceLedger | undefined,
228
+ ): number {
229
+ return Buffer.byteLength(`${JSON.stringify({
230
+ version: 1,
231
+ provenance: { ...ledger, entries },
232
+ }, null, 2)}\n`, "utf-8");
233
+ }
234
+
199
235
  function provenanceBaseline(bytes: string | null): CodexProvenanceEntry["baseline"] {
200
236
  if (bytes === null) return { kind: "absent" };
201
237
  return {
@@ -214,21 +250,66 @@ function provenancePostImage(path: string): string | null {
214
250
  }
215
251
 
216
252
  /**
217
- * Keep the newest `CODEX_PROVENANCE_MAX_TRANSACTIONS` transactions, whole.
253
+ * Keep the newest transactions that fit both the transaction window and the byte ceiling, whole.
218
254
  *
219
255
  * Trimming by ENTRY count would cut a transaction in half and leave evidence that says a
220
256
  * transaction touched two artifacts when it touched three — worse than dropping it outright,
221
- * because a partial record still reads as complete. Order is preserved; only whole leading
222
- * transactions are removed.
257
+ * because a partial record still reads as complete. Order is preserved; only whole transactions
258
+ * are removed.
259
+ *
260
+ * The byte ceiling applies the same rule: a transaction that does not fit is omitted whole,
261
+ * including the newest one — a record silently truncated to fit would be read as a faithful
262
+ * pre-image. An oversized transaction is skipped rather than ending the scan, so one pathological
263
+ * artifact cannot erase the smaller transactions that still fit and are still diagnosable.
223
264
  */
224
265
  export function boundProvenanceEntries(
225
266
  entries: readonly CodexProvenanceEntry[],
226
267
  maxTransactions = CODEX_PROVENANCE_MAX_TRANSACTIONS,
268
+ maxBytes = CODEX_PROVENANCE_MAX_BYTES,
269
+ ledger?: CodexProvenanceLedger,
227
270
  ): readonly CodexProvenanceEntry[] {
228
- const order: string[] = [];
229
- for (const entry of entries) if (!order.includes(entry.txId)) order.push(entry.txId);
230
- if (order.length <= maxTransactions) return entries;
231
- const keep = new Set(order.slice(order.length - maxTransactions));
271
+ const transactions = new Map<string, CodexProvenanceEntry[]>();
272
+ for (const entry of entries) {
273
+ const transaction = transactions.get(entry.txId);
274
+ if (transaction) transaction.push(entry);
275
+ else transactions.set(entry.txId, [entry]);
276
+ }
277
+ const order = [...transactions.keys()];
278
+ if (order.length <= maxTransactions) {
279
+ const baselineBytes = entries.reduce(
280
+ (total, entry) => total + (entry.baseline.kind === "present" ? entry.baseline.bytesBase64.length : 0),
281
+ 0,
282
+ );
283
+ if (
284
+ baselineBytes <= maxBytes
285
+ && serializedProvenanceBytes(entries, ledger) <= maxBytes
286
+ ) return entries;
287
+ }
288
+ // Newest first, so the transactions anyone diagnoses against are the ones that fit.
289
+ const keep = new Set<string>();
290
+ let baselineBytes = 0;
291
+ for (let i = order.length - 1; i >= 0 && keep.size < maxTransactions; i--) {
292
+ const txId = order[i]!;
293
+ const txEntries = transactions.get(txId)!;
294
+ // Measure the embedded pre-images first: a pathological baseline is refused without
295
+ // serializing it, so the ceiling does not itself allocate the payload it exists to reject.
296
+ const transactionBaselineBytes = txEntries.reduce(
297
+ (total, entry) => total + (entry.baseline.kind === "present" ? entry.baseline.bytesBase64.length : 0),
298
+ 0,
299
+ );
300
+ if (baselineBytes + transactionBaselineBytes > maxBytes) continue;
301
+ const candidateKeep = new Set(keep);
302
+ candidateKeep.add(txId);
303
+ // Reuse the already-grouped transactions: this avoids another full-ledger filter on each
304
+ // iteration while preserving the original transaction and entry order.
305
+ const candidateEntries = order.flatMap(id =>
306
+ candidateKeep.has(id) ? transactions.get(id)! : []
307
+ );
308
+ if (serializedProvenanceBytes(candidateEntries, ledger) > maxBytes) continue;
309
+ baselineBytes += transactionBaselineBytes;
310
+ keep.add(txId);
311
+ }
312
+ if (keep.size === order.length) return entries;
232
313
  return entries.filter(entry => keep.has(entry.txId));
233
314
  }
234
315
 
@@ -250,13 +331,29 @@ export function recordCodexNativeTransactionProvenance(
250
331
  txId,
251
332
  at,
252
333
  }));
253
- return updateIntegrationRecord(record => ({
254
- ...record,
255
- provenance: {
256
- ...record.provenance,
257
- entries: boundProvenanceEntries([...(record.provenance?.entries ?? []), ...entries]),
258
- },
259
- }));
334
+ return updateIntegrationRecord(record => {
335
+ const previousLedger = record.provenance;
336
+ // Unknown ledger extensions are forward-compatible and must be preserved. If those fixed
337
+ // fields alone exceed the ceiling, no entry selection can make the write compliant. Keep the
338
+ // existing record byte-for-byte instead of deleting all known evidence and rewriting the
339
+ // same oversized extension on every native transaction.
340
+ if (
341
+ previousLedger
342
+ && serializedProvenanceBytes([], previousLedger) > CODEX_PROVENANCE_MAX_BYTES
343
+ ) return record;
344
+ return {
345
+ ...record,
346
+ provenance: {
347
+ ...previousLedger,
348
+ entries: boundProvenanceEntries(
349
+ [...(previousLedger?.entries ?? []), ...entries],
350
+ CODEX_PROVENANCE_MAX_TRANSACTIONS,
351
+ CODEX_PROVENANCE_MAX_BYTES,
352
+ previousLedger,
353
+ ),
354
+ },
355
+ };
356
+ });
260
357
  }
261
358
 
262
359
  /**
@@ -204,12 +204,17 @@ function mergeEntry(previous: CodexProvenanceEntry, next: CodexProvenanceEntry):
204
204
  function mergeLedger(previous: CodexProvenanceLedger, next: CodexProvenanceLedger): CodexProvenanceLedger {
205
205
  const unused = new Set(previous.entries.map((_, index) => index));
206
206
  const entries = next.entries.map((entry, nextIndex) => {
207
- let previousIndex = previous.entries.findIndex((candidate, index) =>
207
+ // Extensions follow provenance identity, never position. A bounded ledger may drop an
208
+ // oversized transaction, shifting an unrelated entry into that slot, and even within one
209
+ // transaction and timestamp the artifact may differ. Since this search already requires
210
+ // txId, timestamp, and artifact identity, any remaining positional match is by definition
211
+ // a different artifact, so preserving its unknown entry/artifact/baseline fields would
212
+ // attach false evidence and could regrow the record past the measured byte ceiling.
213
+ const previousIndex = previous.entries.findIndex((candidate, index) =>
208
214
  unused.has(index)
209
215
  && candidate.txId === entry.txId
210
216
  && candidate.at === entry.at
211
217
  && knownArtifactIdentity(candidate.artifact) === knownArtifactIdentity(entry.artifact));
212
- if (previousIndex < 0 && unused.has(nextIndex)) previousIndex = nextIndex;
213
218
  if (previousIndex < 0) return entry;
214
219
  unused.delete(previousIndex);
215
220
  return mergeEntry(previous.entries[previousIndex]!, entry);
@@ -245,6 +250,11 @@ export const updateIntegrationRecord = (
245
250
  if (read.kind === "invalid") return read;
246
251
  const previous: CodexIntegrationRecord = read.kind === "ready" ? read.record : { version: 1 };
247
252
  const proposed = mutate(previous);
253
+ // The mutator can explicitly decline an update by returning the exact record it received.
254
+ // This matters when a preserved forward-compatible extension already exceeds a caller's
255
+ // write budget: rewriting the same oversized bytes would amplify I/O while deleting known
256
+ // entries cannot make that irreducible overhead fit.
257
+ if (proposed === previous) return { kind: "updated" as const, record: previous };
248
258
  if (!validateRecord(proposed)) {
249
259
  return { kind: "invalid", message: "Codex integration record update produced a malformed v1 shape" };
250
260
  }
@@ -1,7 +1,23 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
1
4
  import { readCodexTokens } from "./auth-collision";
2
- import { decodeJwtPayload } from "../oauth/chatgpt";
5
+ import {
6
+ decodeJwtPayload,
7
+ extractAccountId,
8
+ refreshChatGPTToken,
9
+ } from "../oauth/chatgpt";
10
+ import type { OAuthCredentials } from "../oauth/types";
3
11
  import { extractChatgptPlanType } from "./plan";
4
12
  import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
13
+ import {
14
+ refreshGrantFingerprintForToken,
15
+ withCodexRefreshFileLock,
16
+ } from "./account-store";
17
+ import { atomicWriteFile, resolveWriteTarget } from "../config/atomic-write";
18
+ import { resolveCodexHomeDir } from "./home";
19
+ import { assertNotRealCodexHomeUnderTest } from "../lib/test-home-guard";
20
+ import { clearAccountNeedsReauth } from "./account-runtime-state";
5
21
 
6
22
  export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
7
23
 
@@ -12,6 +28,214 @@ export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
12
28
  */
13
29
  let mainAccountPlan: string | null = null;
14
30
  let jwtPlanAttempted = false;
31
+ const MAIN_TOKEN_REFRESH_SKEW_MS = 60_000;
32
+ let beforeMainAuthJsonRenameForTests: (() => void) | null = null;
33
+
34
+ type MainAuthJsonCredential = {
35
+ path: string;
36
+ rawSha256: string;
37
+ root: Record<string, unknown>;
38
+ tokens: Record<string, unknown>;
39
+ accessToken?: string;
40
+ refreshToken?: string;
41
+ chatgptAccountId: string;
42
+ };
43
+
44
+ export interface NativeMainRefreshDependencies {
45
+ refreshToken?: (refreshToken: string, options: { signal: AbortSignal }) => Promise<OAuthCredentials>;
46
+ signal?: AbortSignal;
47
+ }
48
+
49
+ export class MainAuthJsonChangedDuringRefreshError extends Error {
50
+ constructor() {
51
+ super("Codex auth.json changed while its token was refreshing");
52
+ this.name = "MainAuthJsonChangedDuringRefreshError";
53
+ }
54
+ }
55
+
56
+ export class MainAccountTokenRefreshError extends Error {
57
+ constructor(readonly reason: "reauth" | "transient", options?: ErrorOptions) {
58
+ super(reason === "reauth"
59
+ ? "Codex main account needs reauthentication"
60
+ : "Codex main token refresh did not complete", options);
61
+ this.name = "MainAccountTokenRefreshError";
62
+ }
63
+ }
64
+
65
+ function nonEmptyString(value: unknown): string | undefined {
66
+ return typeof value === "string" && value.trim() ? value : undefined;
67
+ }
68
+
69
+ function sha256(value: string): string {
70
+ return createHash("sha256").update(value).digest("hex");
71
+ }
72
+
73
+ function readMainAuthJsonCredential(): MainAuthJsonCredential | null {
74
+ const path = resolveWriteTarget(join(resolveCodexHomeDir(), "auth.json"));
75
+ let raw: string;
76
+ try {
77
+ raw = readFileSync(path, "utf8");
78
+ } catch {
79
+ return null;
80
+ }
81
+ try {
82
+ const parsed = JSON.parse(raw) as unknown;
83
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
84
+ const root = parsed as Record<string, unknown>;
85
+ const tokenValue = root.tokens;
86
+ if (!tokenValue || typeof tokenValue !== "object" || Array.isArray(tokenValue)) return null;
87
+ const tokens = tokenValue as Record<string, unknown>;
88
+ const accessToken = nonEmptyString(tokens.access_token);
89
+ const refreshToken = nonEmptyString(tokens.refresh_token);
90
+ if (!accessToken && !refreshToken) return null;
91
+ const idToken = nonEmptyString(tokens.id_token);
92
+ const chatgptAccountId = extractAccountId(idToken, accessToken)
93
+ ?? nonEmptyString(tokens.account_id)
94
+ ?? "";
95
+ return {
96
+ path,
97
+ rawSha256: sha256(raw),
98
+ root,
99
+ tokens,
100
+ ...(accessToken ? { accessToken } : {}),
101
+ ...(refreshToken ? { refreshToken } : {}),
102
+ chatgptAccountId,
103
+ };
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function mainAccessTokenFresh(accessToken: string | undefined, now: number, skewMs: number): boolean {
110
+ if (!accessToken) return false;
111
+ const payload = decodeJwtPayload(accessToken);
112
+ const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : undefined;
113
+ return exp === undefined || exp > now + skewMs;
114
+ }
115
+
116
+ /** A refresh grant makes native main routeable even when the current access token is expired. */
117
+ export function isMainAccountCredentialUsable(now = Date.now()): boolean {
118
+ const current = readMainAuthJsonCredential();
119
+ return !!current?.refreshToken || mainAccessTokenFresh(current?.accessToken, now, 0);
120
+ }
121
+
122
+ export function hasMainAccountRefreshGrant(): boolean {
123
+ return !!readMainAuthJsonCredential()?.refreshToken;
124
+ }
125
+
126
+ function assertMainAuthJsonSnapshotUnchanged(expected: MainAuthJsonCredential): void {
127
+ const current = readMainAuthJsonCredential();
128
+ if (!current || current.path !== expected.path || current.rawSha256 !== expected.rawSha256) {
129
+ throw new MainAuthJsonChangedDuringRefreshError();
130
+ }
131
+ }
132
+
133
+ function persistRefreshedMainAuthJson(
134
+ expected: MainAuthJsonCredential,
135
+ refreshed: OAuthCredentials,
136
+ ): { accessToken: string; chatgptAccountId: string } {
137
+ assertNotRealCodexHomeUnderTest(resolveCodexHomeDir());
138
+ const accessToken = refreshed.access;
139
+ const refreshToken = refreshed.refresh || expected.refreshToken!;
140
+ const chatgptAccountId = refreshed.accountId
141
+ ?? extractAccountId(undefined, accessToken)
142
+ ?? expected.chatgptAccountId;
143
+ const tokens = {
144
+ ...expected.tokens,
145
+ access_token: accessToken,
146
+ refresh_token: refreshToken,
147
+ account_id: chatgptAccountId,
148
+ };
149
+ atomicWriteFile(
150
+ expected.path,
151
+ JSON.stringify({ ...expected.root, tokens }, null, 2) + "\n",
152
+ undefined,
153
+ {
154
+ beforeRename: () => {
155
+ assertMainAuthJsonSnapshotUnchanged(expected);
156
+ const hook = beforeMainAuthJsonRenameForTests;
157
+ beforeMainAuthJsonRenameForTests = null;
158
+ hook?.();
159
+ },
160
+ validateBeforeRename: () => assertMainAuthJsonSnapshotUnchanged(expected),
161
+ },
162
+ );
163
+ return { accessToken, chatgptAccountId };
164
+ }
165
+
166
+ export function setMainAuthJsonBeforeRenameHookForTests(hook: (() => void) | null): void {
167
+ beforeMainAuthJsonRenameForTests = hook;
168
+ }
169
+
170
+ async function resolveMainAccountToken(
171
+ dependencies: NativeMainRefreshDependencies = {},
172
+ rejectedAccessToken?: string,
173
+ ): Promise<{ accessToken: string; chatgptAccountId: string } | null> {
174
+ const initial = readMainAuthJsonCredential();
175
+ if (!initial) return null;
176
+ const now = Date.now();
177
+ if (initial.accessToken !== rejectedAccessToken
178
+ && mainAccessTokenFresh(initial.accessToken, now, MAIN_TOKEN_REFRESH_SKEW_MS)) {
179
+ return { accessToken: initial.accessToken!, chatgptAccountId: initial.chatgptAccountId };
180
+ }
181
+ if (!initial.refreshToken) {
182
+ return initial.accessToken !== rejectedAccessToken
183
+ && mainAccessTokenFresh(initial.accessToken, now, 0)
184
+ ? { accessToken: initial.accessToken!, chatgptAccountId: initial.chatgptAccountId }
185
+ : null;
186
+ }
187
+
188
+ const signal = dependencies.signal
189
+ ? AbortSignal.any([dependencies.signal, AbortSignal.timeout(30_000)])
190
+ : AbortSignal.timeout(30_000);
191
+ const lockKey = refreshGrantFingerprintForToken(initial.refreshToken);
192
+ return withCodexRefreshFileLock(lockKey, signal, async () => {
193
+ const locked = readMainAuthJsonCredential();
194
+ if (!locked) throw new MainAuthJsonChangedDuringRefreshError();
195
+ if (!locked.refreshToken
196
+ || refreshGrantFingerprintForToken(locked.refreshToken) !== lockKey) {
197
+ if (locked.accessToken !== rejectedAccessToken
198
+ && mainAccessTokenFresh(locked.accessToken, Date.now(), 0)) {
199
+ return { accessToken: locked.accessToken!, chatgptAccountId: locked.chatgptAccountId };
200
+ }
201
+ throw new MainAuthJsonChangedDuringRefreshError();
202
+ }
203
+ if (locked.accessToken !== rejectedAccessToken
204
+ && mainAccessTokenFresh(locked.accessToken, Date.now(), MAIN_TOKEN_REFRESH_SKEW_MS)) {
205
+ return { accessToken: locked.accessToken!, chatgptAccountId: locked.chatgptAccountId };
206
+ }
207
+ const refresh = dependencies.refreshToken
208
+ ?? ((refreshToken: string, options: { signal: AbortSignal }) => refreshChatGPTToken(refreshToken, options));
209
+ let refreshed: OAuthCredentials;
210
+ try {
211
+ refreshed = await refresh(locked.refreshToken, { signal });
212
+ } catch (cause) {
213
+ const message = cause instanceof Error ? cause.message.toLowerCase() : "";
214
+ const reason = /invalid_grant|invalidated|revoked|expired/.test(message)
215
+ ? "reauth" as const
216
+ : "transient" as const;
217
+ throw new MainAccountTokenRefreshError(reason, { cause });
218
+ }
219
+ const result = persistRefreshedMainAuthJson(locked, refreshed);
220
+ clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
221
+ return result;
222
+ });
223
+ }
224
+
225
+ /** Refresh the CLI-owned native credential before upstream I/O and publish it atomically. */
226
+ export function getValidMainAccountToken(
227
+ dependencies: NativeMainRefreshDependencies = {},
228
+ ): Promise<{ accessToken: string; chatgptAccountId: string } | null> {
229
+ return resolveMainAccountToken(dependencies);
230
+ }
231
+
232
+ /** Force refresh after upstream rejected this exact bearer once. */
233
+ export function forceRefreshMainAccountToken(
234
+ rejectedAccessToken: string,
235
+ dependencies: NativeMainRefreshDependencies = {},
236
+ ): Promise<{ accessToken: string; chatgptAccountId: string } | null> {
237
+ return resolveMainAccountToken(dependencies, rejectedAccessToken);
238
+ }
15
239
 
16
240
  export function setMainAccountPlan(plan: string | null): void {
17
241
  mainAccountPlan = plan;