@bitkyc08/opencodex 2.57.0 → 2.59.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 (241) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +2 -2
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  10. package/src/adapters/command-code.ts +13 -4
  11. package/src/adapters/cursor/catalog.ts +11 -0
  12. package/src/adapters/cursor/cursor-errors.ts +15 -0
  13. package/src/adapters/cursor/discovery.ts +65 -1
  14. package/src/adapters/cursor/effort-map.ts +16 -2
  15. package/src/adapters/cursor/envelope-echo.ts +55 -2
  16. package/src/adapters/cursor/live-transport.ts +5 -1
  17. package/src/adapters/cursor/message-mapper.ts +3 -2
  18. package/src/adapters/cursor/protobuf-events.ts +110 -11
  19. package/src/adapters/cursor/protobuf-request.ts +27 -6
  20. package/src/adapters/cursor/request-builder.ts +14 -3
  21. package/src/adapters/cursor/text-toolcall.ts +230 -0
  22. package/src/adapters/cursor/thread-continuity.ts +141 -0
  23. package/src/adapters/cursor/tool-guidance.ts +5 -4
  24. package/src/adapters/cursor/types.ts +5 -0
  25. package/src/adapters/cursor.ts +97 -6
  26. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  27. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  28. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  29. package/src/adapters/devin.ts +75 -13
  30. package/src/adapters/google-antigravity-wire.ts +29 -2
  31. package/src/adapters/google-http.ts +45 -13
  32. package/src/adapters/google.ts +23 -4
  33. package/src/adapters/mimo-free.ts +32 -17
  34. package/src/adapters/ollama-native.ts +42 -8
  35. package/src/adapters/openai-chat/response-events.ts +61 -0
  36. package/src/adapters/openai-chat.ts +5 -10
  37. package/src/adapters/openai-responses/passthrough.ts +40 -5
  38. package/src/adapters/openai-responses/request-strips.ts +43 -0
  39. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  40. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  41. package/src/adapters/physical-send.ts +50 -0
  42. package/src/adapters/responses-tool-schema.ts +76 -46
  43. package/src/adapters/run-turn-queue.ts +17 -4
  44. package/src/bridge/response-json.ts +2 -2
  45. package/src/bridge/sse.ts +166 -25
  46. package/src/claude/context-windows.ts +22 -0
  47. package/src/claude/outbound.ts +46 -5
  48. package/src/cli/account-api.ts +4 -3
  49. package/src/cli/account-extended.ts +22 -2
  50. package/src/cli/account-orca-import.ts +63 -0
  51. package/src/cli/account.ts +32 -4
  52. package/src/cli/capabilities.ts +40 -0
  53. package/src/cli/claude.ts +29 -1
  54. package/src/cli/codex-cli-update.ts +97 -2
  55. package/src/cli/config-command.ts +35 -18
  56. package/src/cli/dispatch.ts +71 -4
  57. package/src/cli/doctor.ts +197 -2
  58. package/src/cli/help.ts +4 -1
  59. package/src/cli/index.ts +132 -22
  60. package/src/cli/models-runtime.ts +33 -4
  61. package/src/cli/registry.ts +11 -1
  62. package/src/cli/runtime-api.ts +44 -0
  63. package/src/cli/start-args.ts +94 -0
  64. package/src/cli/system-command.ts +72 -1
  65. package/src/cli/uninstall-client-state.ts +12 -0
  66. package/src/client/machine-api.ts +4 -3
  67. package/src/client/machine-listener.ts +14 -1
  68. package/src/clients/config-export/constants.ts +2 -3
  69. package/src/clients/config-export.ts +5 -5
  70. package/src/codex/account-store.ts +81 -5
  71. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  72. package/src/codex/auth-api/routes.ts +17 -2
  73. package/src/codex/auth-context.ts +58 -20
  74. package/src/codex/catalog/build-entries.ts +25 -4
  75. package/src/codex/catalog/derive-entry.ts +8 -1
  76. package/src/codex/catalog/effort.ts +10 -6
  77. package/src/codex/catalog/gather-capture.ts +1 -0
  78. package/src/codex/catalog/model-hints.ts +37 -5
  79. package/src/codex/catalog/parsing.ts +83 -5
  80. package/src/codex/catalog/reserve-warn.ts +96 -0
  81. package/src/codex/catalog/retained-sync.ts +19 -0
  82. package/src/codex/catalog/routed-gather.ts +42 -3
  83. package/src/codex/cli-installation-identity.ts +210 -0
  84. package/src/codex/cli-installation-targets.ts +158 -0
  85. package/src/codex/convergence.ts +5 -0
  86. package/src/codex/desktop-switches.ts +145 -0
  87. package/src/codex/history-job.ts +5 -1
  88. package/src/codex/history-provider.ts +37 -5
  89. package/src/codex/history-state-open.ts +105 -0
  90. package/src/codex/history-worker.ts +14 -1
  91. package/src/codex/inject/config-toml.ts +44 -2
  92. package/src/codex/inject/remove.ts +145 -7
  93. package/src/codex/inject/restore.ts +204 -32
  94. package/src/codex/inject.ts +6 -9
  95. package/src/codex/lineage.ts +83 -32
  96. package/src/codex/loopback-target.ts +40 -0
  97. package/src/codex/main-account-hard-lock.ts +2 -1
  98. package/src/codex/main-account.ts +10 -3
  99. package/src/codex/main-device-reauth.ts +17 -9
  100. package/src/codex/model-entitlements.ts +60 -1
  101. package/src/codex/native-profile-startup.ts +64 -20
  102. package/src/codex/observed-model-denials.ts +137 -0
  103. package/src/codex/orca-auth-source.ts +94 -0
  104. package/src/codex/orca-import.ts +219 -0
  105. package/src/codex/prompt-text-probe.ts +282 -12
  106. package/src/codex/quota-401-recovery.ts +12 -0
  107. package/src/codex/quota-types.ts +65 -0
  108. package/src/codex/quota.ts +24 -19
  109. package/src/codex/routing/cooldown-math.ts +8 -47
  110. package/src/codex/routing/pin-drain.ts +57 -0
  111. package/src/codex/routing.ts +13 -15
  112. package/src/codex/subagent-model-fallback.ts +94 -0
  113. package/src/codex/windows-installation-files.ts +224 -0
  114. package/src/combos/failover.ts +122 -5
  115. package/src/config/atomic-write.ts +83 -8
  116. package/src/config/diagnostics.ts +21 -0
  117. package/src/config/load-degrade.ts +15 -0
  118. package/src/config/pending-teardown.ts +8 -0
  119. package/src/config/process-state.ts +36 -3
  120. package/src/config/provider-relative-send-path.ts +16 -0
  121. package/src/config/proxy-env.ts +23 -5
  122. package/src/config/schema/config-schema.ts +23 -0
  123. package/src/config/schema/leaf-validators.ts +65 -17
  124. package/src/generated/compatibility-version.json +337 -201
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/lib/bounded-body.ts +4 -2
  127. package/src/lib/bounded-subprocess.ts +62 -10
  128. package/src/lib/destination-policy.ts +48 -6
  129. package/src/lib/errors.ts +3 -15
  130. package/src/lib/local-destinations.ts +32 -5
  131. package/src/lib/provider-outbound.ts +3 -3
  132. package/src/lib/proxy-env.ts +70 -3
  133. package/src/lib/request-execution-budget.ts +11 -3
  134. package/src/lib/response-body-inactivity.ts +193 -0
  135. package/src/lib/retry-delay.ts +69 -0
  136. package/src/lib/socks5-fetch.ts +631 -0
  137. package/src/lib/spend-reservation-ledger.ts +115 -9
  138. package/src/lib/windows-secret-acl.ts +151 -15
  139. package/src/lib/windows-user-principal.ts +5 -1
  140. package/src/lib/workflow-budget.ts +145 -8
  141. package/src/oauth/account-quota-rank.ts +72 -15
  142. package/src/oauth/generic-account-failover.ts +40 -27
  143. package/src/oauth/orcarouter.ts +15 -2
  144. package/src/oauth/store.ts +8 -0
  145. package/src/providers/codex-capacity.ts +9 -0
  146. package/src/providers/derive.ts +6 -0
  147. package/src/providers/devin-provider-merge-migration.ts +33 -12
  148. package/src/providers/free-directory.ts +20 -2
  149. package/src/providers/key-failover.ts +261 -7
  150. package/src/providers/model-discovery.ts +19 -7
  151. package/src/providers/model-rename-migration.ts +1 -0
  152. package/src/providers/openai-sidecar.ts +4 -0
  153. package/src/providers/opencode-go-transport.ts +14 -5
  154. package/src/providers/quota/report-cache.ts +3 -0
  155. package/src/providers/registry/entries-core.ts +11 -0
  156. package/src/providers/registry/entries-extended.ts +146 -28
  157. package/src/providers/registry/model-seeds.ts +136 -29
  158. package/src/providers/registry/types.ts +9 -0
  159. package/src/responses/apply-patch-envelope.ts +44 -11
  160. package/src/responses/bridge-search-replay-cache.ts +152 -0
  161. package/src/responses/code-mode-helper-compat.ts +26 -16
  162. package/src/responses/custom-tool-compat.ts +1 -1
  163. package/src/responses/hosted-tool-policy.ts +85 -2
  164. package/src/responses/schema.ts +9 -2
  165. package/src/responses/spill-store.ts +17 -0
  166. package/src/responses/state/body-policy.ts +25 -0
  167. package/src/responses/state/spill-queue.ts +8 -6
  168. package/src/responses/state.ts +3 -22
  169. package/src/router.ts +4 -0
  170. package/src/server/auth-cors.ts +27 -0
  171. package/src/server/chat-completions.ts +9 -4
  172. package/src/server/chat-native-sse.ts +26 -9
  173. package/src/server/chat-native.ts +10 -4
  174. package/src/server/claude-messages.ts +24 -2
  175. package/src/server/gui-static.ts +36 -2
  176. package/src/server/inbound-body-admission.ts +187 -0
  177. package/src/server/index/websocket-handler.ts +48 -1
  178. package/src/server/index.ts +15 -19
  179. package/src/server/management/api-access.ts +3 -4
  180. package/src/server/management/config-routes.ts +57 -10
  181. package/src/server/management/provider-capability-config.ts +35 -7
  182. package/src/server/management/provider-routes.ts +70 -18
  183. package/src/server/models-capabilities.ts +24 -3
  184. package/src/server/proxy-liveness.ts +97 -2
  185. package/src/server/relay.ts +17 -24
  186. package/src/server/request-log.ts +25 -1
  187. package/src/server/responses/adapter-continuation.ts +71 -27
  188. package/src/server/responses/adapter-delivery.ts +39 -8
  189. package/src/server/responses/adapter-dispatch.ts +52 -24
  190. package/src/server/responses/codex-ws-exchange.ts +65 -4
  191. package/src/server/responses/combo-stream-preflight.ts +68 -5
  192. package/src/server/responses/compact.ts +60 -11
  193. package/src/server/responses/core-codex-account.ts +83 -22
  194. package/src/server/responses/core-combo.ts +26 -0
  195. package/src/server/responses/core-normalize.ts +12 -5
  196. package/src/server/responses/core-options.ts +3 -0
  197. package/src/server/responses/fetch-helpers.ts +72 -3
  198. package/src/server/responses/native-injection-protocol.ts +42 -0
  199. package/src/server/responses/native-injection-replay.ts +105 -0
  200. package/src/server/responses/native-injection.ts +242 -0
  201. package/src/server/responses/native-response-control.ts +56 -0
  202. package/src/server/responses/native-response-json.ts +14 -0
  203. package/src/server/responses/native-response-output.ts +37 -0
  204. package/src/server/responses/native-steering-log.ts +44 -0
  205. package/src/server/responses/native-steering-policy.ts +49 -0
  206. package/src/server/responses/native-steering-replay.ts +126 -0
  207. package/src/server/responses/native-steering-settings.ts +76 -0
  208. package/src/server/responses/native-steering.ts +400 -0
  209. package/src/server/responses/native-tool-results.ts +130 -0
  210. package/src/server/responses/passthrough-delivery.ts +21 -1
  211. package/src/server/responses/passthrough-dispatch.ts +146 -49
  212. package/src/server/responses/passthrough-execution.ts +11 -1
  213. package/src/server/responses/request-prepare.ts +70 -0
  214. package/src/server/responses/request-send-budget.ts +84 -7
  215. package/src/server/responses/request-sidecar-auth.ts +16 -8
  216. package/src/server/responses/request-spend.ts +38 -9
  217. package/src/server/responses/request-transport.ts +13 -10
  218. package/src/server/responses/run-turn-execution.ts +20 -5
  219. package/src/server/responses/sidecar-execution.ts +2 -0
  220. package/src/server/responses/ws-upstream.ts +23 -2
  221. package/src/server/responses-custom-tool-repair.ts +2 -2
  222. package/src/server/sse-frame-buffer.ts +12 -10
  223. package/src/server/sse-payload-rewrite.ts +36 -9
  224. package/src/server/stop-teardown.ts +8 -1
  225. package/src/server/system-env-shell.ts +5 -1
  226. package/src/server/system-env.ts +7 -1
  227. package/src/server/workflow-refusal.ts +56 -2
  228. package/src/server/ws-bridge.ts +16 -1
  229. package/src/service/cli.ts +29 -7
  230. package/src/service/guards.ts +10 -0
  231. package/src/service/health.ts +43 -0
  232. package/src/service/state.ts +7 -2
  233. package/src/types/accounts.ts +4 -0
  234. package/src/types/config.ts +104 -3
  235. package/src/types/provider.ts +32 -0
  236. package/src/types/request.ts +7 -1
  237. package/src/types/wire.ts +9 -1
  238. package/src/usage/expected-prices.ts +28 -0
  239. package/src/usage/log.ts +87 -4
  240. package/src/web-search/passthrough-bridge.ts +39 -5
  241. package/gui/dist/assets/index-Cz7CLdif.js +0 -128
@@ -33,6 +33,7 @@ import {
33
33
  readCodexCatalogPath,
34
34
  readCodexCatalogPathForHome,
35
35
  readNativeBaseline,
36
+ nativeMultiAgentDefaults,
36
37
  } from "./parsing";
37
38
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
38
39
  import {
@@ -50,6 +51,7 @@ import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
50
51
  import { bundledCatalogCacheState, loadBundledCodexCatalog } from "./bundled";
51
52
  import { isMultiAgentV2Enabled } from "../features";
52
53
  import { clampCatalogModelsToCodexSupport } from "./effort";
54
+ import { suppressedSyntheticMaxCatalogSlugs } from "./model-hints";
53
55
  import { filterCatalogVisibleModels, gatherRoutedModels, type CatalogGatherProviderModelOutcome } from "./provider-fetch";
54
56
  import { dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
55
57
  import {
@@ -75,6 +77,7 @@ import {
75
77
  import { finishUpstreamNativeEntry } from "./derive-entry";
76
78
  import { finalizeAutoReviewModelOverride } from "./auto-review";
77
79
  import { gatedNativeAccountLabel, gatedNativeReauthSuppressionReason, warnGatedNativeSuppressedOnce } from "./gated-native-warn";
80
+ import { reserveCatalogSuppressionReason, warnReserveSuppressedOnce } from "./reserve-warn";
78
81
 
79
82
  interface RetainedCatalogSyncRead {
80
83
  readonly catalogPath: string;
@@ -320,6 +323,11 @@ function writeRetainedCatalogSync({
320
323
  const enabledGo = filterCatalogVisibleModels(goModels, config);
321
324
  const featured = config.subagentModels ?? [];
322
325
  const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
326
+ const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(
327
+ config,
328
+ orderedGoModels,
329
+ catalogModelsForMerge,
330
+ );
323
331
  const modelPickerOrder = config.modelPickerOrder ?? [];
324
332
  const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
325
333
  const exactComboSlugs = exactComboCatalogSlugs(config);
@@ -376,6 +384,14 @@ function writeRetainedCatalogSync({
376
384
  const accountTargets = new Map(codexAccountNamespaceEntries(config));
377
385
  const reserveMainSelectors = accountSelectors.filter(selector =>
378
386
  isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
387
+ // #4811: an omitted Reserve row carries no reason, so the explanation has to be emitted here,
388
+ // where the selector inputs that produced the omission are still in scope. Silent for every
389
+ // install that did not opt into authless Codex Desktop routing.
390
+ const reserveSuppression = reserveCatalogSuppressionReason(config, {
391
+ includeAccountBoundNativeOpenAi,
392
+ mainSelectors: reserveMainSelectors,
393
+ });
394
+ if (reserveSuppression) warnReserveSuppressedOnce(reserveSuppression);
379
395
  // The active file can own a bare source even when the bundled catalog is the build base.
380
396
  // A previously clamped qualified projection must not shorten a retained genuine ladder.
381
397
  const reserveObservations = [
@@ -443,6 +459,7 @@ function writeRetainedCatalogSync({
443
459
  // like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
444
460
  const baselineCatalog = readCatalogBackup(catalogPath);
445
461
  const baseline = readNativeBaseline(catalogPath);
462
+ const nativePinBaseline = nativeMultiAgentDefaults(baselineCatalog?.models);
446
463
  const gatheredProviderNames = new Set(
447
464
  Object.entries(config.providers ?? {})
448
465
  .filter(([, prov]) => prov.disabled !== true)
@@ -512,8 +529,10 @@ function writeRetainedCatalogSync({
512
529
  includeNativeOpenAi,
513
530
  accountBoundEntries,
514
531
  suppressedBareNativeSlugs,
532
+ suppressedSyntheticMaxSlugs,
515
533
  openaiContextCap,
516
534
  nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
535
+ nativeMultiAgentDefaults: nativePinBaseline,
517
536
  policy: {
518
537
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
519
538
  nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
@@ -850,6 +850,26 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
850
850
  ];
851
851
  }
852
852
 
853
+ /**
854
+ * Add generated-registry rows the live provider list did not return, and backfill a
855
+ * published context window onto a live row that arrived without one.
856
+ *
857
+ * The backfill is the reason this function has a merge branch at all. It used to skip every
858
+ * id the live list returned, which is correct for a row that already knows its window and
859
+ * wrong for the normal OpenCode Go case, where discovery returns an id with no context
860
+ * field at all. The serialized Codex catalog was unaffected, because
861
+ * `applyCatalogMetadata` writes `context_window` onto the entry straight from the generated
862
+ * table; the absence was only visible to consumers that read `CatalogModel.contextWindow`
863
+ * (#4971). Those are not cosmetic: `buildClaudeContextWindows` drops a routed row with no
864
+ * window from the map that decides the `[1m]` marker, so a published 1M model could not be
865
+ * recognized as one, and the Grok config writer and several client exporters omit the field
866
+ * entirely rather than emit a known value.
867
+ *
868
+ * It is a missing-value fill, never an override: a live positive window still wins, because
869
+ * the upstream is the authority on its own model. The seeded row is re-hinted so operator
870
+ * precedence is unchanged — a configured window still lowers it and `providerContextCaps`
871
+ * still caps it, exactly as for an appended row.
872
+ */
853
873
  export function augmentRoutedModelsWithMetadata(
854
874
  models: CatalogModel[],
855
875
  providerNames: string[],
@@ -858,7 +878,7 @@ export function augmentRoutedModelsWithMetadata(
858
878
  metadataModelIdCaseFoldByProvider?: ReadonlyMap<string, boolean>,
859
879
  ): CatalogModel[] {
860
880
  const out = [...models];
861
- const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
881
+ const indexByKey = new Map(out.map((model, index) => [`${model.provider}/${model.id}`, index]));
862
882
  for (const provider of providerNames) {
863
883
  if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
864
884
  if (providers?.[provider]?.liveModels === false) continue;
@@ -866,9 +886,28 @@ export function augmentRoutedModelsWithMetadata(
866
886
  if (!jawcodeProvider) continue;
867
887
  for (const meta of listModelMetadata(jawcodeProvider)) {
868
888
  const key = `${provider}/${meta.id}`;
869
- if (seen.has(key)) continue;
870
- seen.add(key);
871
889
  const contextCap = caps ? providerContextCap(caps, provider) : undefined;
890
+ const existingIndex = indexByKey.get(key);
891
+ if (existingIndex !== undefined) {
892
+ const existing = out[existingIndex]!;
893
+ const publishedWindow = typeof meta.contextWindow === "number" && meta.contextWindow > 0
894
+ ? meta.contextWindow
895
+ : undefined;
896
+ const liveWindow = typeof existing.contextWindow === "number" && existing.contextWindow > 0;
897
+ if (liveWindow || publishedWindow === undefined) continue;
898
+ const seeded: CatalogModel = { ...existing, contextWindow: publishedWindow };
899
+ out[existingIndex] = providers?.[provider]
900
+ ? applyProviderConfigHints(
901
+ provider,
902
+ providers[provider],
903
+ seeded,
904
+ contextCap,
905
+ metadataModelIdCaseFoldByProvider?.get(provider),
906
+ )
907
+ : seeded;
908
+ continue;
909
+ }
910
+ indexByKey.set(key, out.length);
872
911
  const model: CatalogModel = {
873
912
  provider,
874
913
  id: meta.id,
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { win32 } from "node:path";
3
+ import { parseStrictSemver } from "../lib/strict-semver";
4
+
5
+ /** Explicit observation, not runtime selection or authority to run an installer. */
6
+ export interface CodexCliInstallationIdentityInput {
7
+ readonly candidate: string;
8
+ readonly npmPrefix: string;
9
+ readonly npmCli: string;
10
+ readonly node: string;
11
+ /** Where the candidate came from; defaults to an explicit CLI argument. */
12
+ readonly candidateSource?: "explicit-cli" | "selected";
13
+ }
14
+
15
+ export interface CodexCliInstallationIdentityReport {
16
+ readonly schemaVersion: 1;
17
+ readonly candidateSource: "explicit-cli" | "selected";
18
+ readonly status: "observed" | "refused";
19
+ readonly reason: string;
20
+ readonly installationIdentityObserved: boolean;
21
+ readonly selectionAttested: false;
22
+ readonly managed: false;
23
+ readonly applyAllowed: false;
24
+ readonly packageVersion: string | null;
25
+ readonly npmVersion: string | null;
26
+ readonly identityDigest: string | null;
27
+ readonly proof: "windows-handle-bound" | null;
28
+ readonly toolchain: "observed-only";
29
+ }
30
+
31
+ export interface InstallationFileRequest {
32
+ readonly path: string;
33
+ readonly maxBytes: number;
34
+ readonly hashOnly?: boolean;
35
+ }
36
+
37
+ export interface ObservedInstallationFile {
38
+ readonly path: string;
39
+ readonly identity: {
40
+ readonly volumeSerial: string;
41
+ readonly fileId: string;
42
+ readonly size: number;
43
+ readonly lastWriteTime: string;
44
+ readonly changeTime: string;
45
+ };
46
+ readonly bytes: Uint8Array;
47
+ readonly digest?: string;
48
+ }
49
+
50
+ export type InstallationFilesResult =
51
+ | { readonly kind: "observed"; readonly files: readonly ObservedInstallationFile[] }
52
+ | { readonly kind: "refused"; readonly reason: string };
53
+
54
+ export interface CodexCliInstallationIdentityDeps {
55
+ readonly platform?: NodeJS.Platform;
56
+ readonly inspectFiles?: (requests: readonly InstallationFileRequest[]) => Promise<InstallationFilesResult>;
57
+ }
58
+
59
+ const TEXT_LIMIT = 256 * 1024;
60
+ const NODE_LIMIT = 256 * 1024 * 1024;
61
+ const hash = (value: string | Uint8Array): string => createHash("sha256").update(value).digest("hex");
62
+ // NTFS can enable case-sensitive directory entries. Do not collapse different
63
+ // paths merely because ordinary Windows directories usually ignore case.
64
+ const key = (path: string): string => path[0]!.toUpperCase() + path.slice(1);
65
+
66
+ function localPath(value: string): string | null {
67
+ if (typeof value !== "string" || value.length > 30_000 || !/^[a-z]:[\\/]/i.test(value)) return null;
68
+ const path = value.replaceAll("/", "\\");
69
+ const parts = path.slice(3).split("\\");
70
+ if (!parts.length || parts.some(part => !part || part === "." || part === ".."
71
+ || /[\x00-\x1f<>:"|?*]/.test(part) || /[. ]$/.test(part)
72
+ || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) return null;
73
+ return path;
74
+ }
75
+
76
+ function supportedLayoutPath(path: string): boolean {
77
+ // This slice does not adopt package managers or packaged desktop applications.
78
+ return !/(?:^|\\)(?:windowsapps|scoop|\.volta|\.nvm|\.asdf|\.mise|\.fnm|\.nvs|\.nodenv)(?:\\|$)|\.app(?:\\|$)/i.test(path);
79
+ }
80
+
81
+ function report(
82
+ reason: string,
83
+ versions?: { codex: string; npm: string; digest: string },
84
+ candidateSource: CodexCliInstallationIdentityReport["candidateSource"] = "explicit-cli",
85
+ ): CodexCliInstallationIdentityReport {
86
+ return Object.freeze({
87
+ schemaVersion: 1, candidateSource, status: versions ? "observed" : "refused",
88
+ reason, installationIdentityObserved: !!versions, selectionAttested: false, managed: false,
89
+ applyAllowed: false, packageVersion: versions?.codex ?? null, npmVersion: versions?.npm ?? null,
90
+ identityDigest: versions?.digest ?? null, proof: versions ? "windows-handle-bound" : null,
91
+ toolchain: "observed-only",
92
+ });
93
+ }
94
+
95
+ /** A refusal before any observation ran, e.g. when no selected candidate can be identified. */
96
+ export function codexCliInstallationRefusal(
97
+ reason: string,
98
+ candidateSource: CodexCliInstallationIdentityReport["candidateSource"] = "explicit-cli",
99
+ ): CodexCliInstallationIdentityReport {
100
+ return report(reason, undefined, candidateSource);
101
+ }
102
+
103
+ function manifest(file: ObservedInstallationFile, name: string, bin: string, expected: string): string | null {
104
+ try {
105
+ const value: unknown = JSON.parse(Buffer.from(file.bytes).toString("utf8"));
106
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
107
+ const item = value as Record<string, unknown>;
108
+ const entry = typeof item.bin === "string" ? item.bin
109
+ : item.bin && typeof item.bin === "object" ? (item.bin as Record<string, unknown>)[bin] : null;
110
+ return item.name === name && typeof item.version === "string" && parseStrictSemver(item.version)
111
+ && entry === expected ? item.version : null;
112
+ } catch { return null; }
113
+ }
114
+
115
+ /** Only the standard npm cmd-shim is linked here; arbitrary wrappers remain unverified. */
116
+ function npmCodexCommandShim(bytes: Uint8Array): boolean {
117
+ const expected = `@ECHO off
118
+ GOTO start
119
+ :find_dp0
120
+ SET dp0=%~dp0
121
+ EXIT /b
122
+ :start
123
+ SETLOCAL
124
+ CALL :find_dp0
125
+
126
+ IF EXIST "%dp0%\\node.exe" (
127
+ SET "_prog=%dp0%\\node.exe"
128
+ ) ELSE (
129
+ SET "_prog=node"
130
+ SET PATHEXT=%PATHEXT:;.JS;=;%
131
+ )
132
+
133
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\@openai\\codex\\bin\\codex.js" %*`;
134
+ const normalize = (text: string) => text.replaceAll("\r\n", "\n").trimEnd();
135
+ return normalize(Buffer.from(bytes).toString("utf8")) === normalize(expected);
136
+ }
137
+
138
+ function fileDigest(file: ObservedInstallationFile): string {
139
+ return file.digest ?? hash(file.bytes);
140
+ }
141
+
142
+ function fileEvidence(file: ObservedInstallationFile): readonly (string | number)[] {
143
+ const id = file.identity;
144
+ return [key(file.path), id.volumeSerial, id.fileId, id.size, id.lastWriteTime, id.changeTime, fileDigest(file)];
145
+ }
146
+
147
+ /** No filesystem or native-library work occurs until this explicit operation is called. */
148
+ export async function inspectCodexCliInstallationIdentity(
149
+ input: CodexCliInstallationIdentityInput,
150
+ deps: CodexCliInstallationIdentityDeps = {},
151
+ ): Promise<CodexCliInstallationIdentityReport> {
152
+ const source = input.candidateSource ?? "explicit-cli";
153
+ const refused = (reason: string) => report(reason, undefined, source);
154
+ if ((deps.platform ?? process.platform) !== "win32") return refused("unsupported_platform");
155
+ const candidate = localPath(input.candidate);
156
+ const prefix = localPath(input.npmPrefix);
157
+ const npmCli = localPath(input.npmCli);
158
+ const node = localPath(input.node);
159
+ if (!candidate || !prefix || !npmCli || !node) return refused("unsafe_path");
160
+ if (![candidate, prefix, npmCli, node].every(supportedLayoutPath)) return refused("unsupported_layout");
161
+ const packageRoot = win32.join(prefix, "node_modules", "@openai", "codex");
162
+ const codexManifest = win32.join(packageRoot, "package.json");
163
+ const codexBin = win32.join(packageRoot, "bin", "codex.js");
164
+ const shim = win32.join(prefix, "codex.cmd");
165
+ // A codex.opencodex-real.cmd backup is npm's renamed launcher: same grammar, same dir.
166
+ const backingShim = win32.join(prefix, "codex.opencodex-real.cmd");
167
+ const acceptedCandidates = source === "selected"
168
+ ? [key(codexBin), key(shim), key(backingShim)]
169
+ : [key(codexBin), key(shim)];
170
+ if (!acceptedCandidates.includes(key(candidate))
171
+ || !/\\node_modules\\npm\\bin\\npm-cli\.js$/i.test(npmCli)
172
+ || win32.basename(node).toLowerCase() !== "node.exe") return refused("unsupported_layout");
173
+ const npmManifest = win32.join(win32.dirname(win32.dirname(npmCli)), "package.json");
174
+ try {
175
+ const inspect = deps.inspectFiles ?? (await import("./windows-installation-files")).inspectWindowsInstallationFiles;
176
+ // Read both manifests again with every linked file held open: first-pass bytes cannot
177
+ // authorize links after a manifest replacement between the two observations.
178
+ const first = await inspect([{ path: codexManifest, maxBytes: TEXT_LIMIT }, { path: npmManifest, maxBytes: TEXT_LIMIT }]);
179
+ if (first.kind !== "observed") return refused("native_proof_unavailable");
180
+ const initial = new Map(first.files.map(file => [key(file.path), file]));
181
+ const codex = initial.get(key(codexManifest));
182
+ const npm = initial.get(key(npmManifest));
183
+ if (!codex || !npm) return refused("read_failed");
184
+ const codexVersion = manifest(codex, "@openai/codex", "codex", "bin/codex.js");
185
+ const npmVersion = manifest(npm, "npm", "npm", "bin/npm-cli.js");
186
+ if (!codexVersion || !npmVersion) return refused("package_mismatch");
187
+ const requests: InstallationFileRequest[] = [codexManifest, npmManifest, candidate, codexBin, npmCli]
188
+ .filter((path, index, all) => all.findIndex(other => key(other) === key(path)) === index)
189
+ .map(path => ({ path, maxBytes: TEXT_LIMIT }));
190
+ requests.push({ path: node, maxBytes: NODE_LIMIT, hashOnly: true });
191
+ const second = await inspect(requests);
192
+ if (second.kind !== "observed") return refused("native_proof_unavailable");
193
+ const files = new Map(second.files.map(file => [key(file.path), file]));
194
+ if (requests.some(request => !files.has(key(request.path)))) return refused("read_failed");
195
+ for (const before of [codex, npm]) {
196
+ if (JSON.stringify(fileEvidence(before)) !== JSON.stringify(fileEvidence(files.get(key(before.path))!))) {
197
+ return refused("identity_changed");
198
+ }
199
+ }
200
+ if ([key(shim), key(backingShim)].includes(key(candidate))
201
+ && !npmCodexCommandShim(files.get(key(candidate))!.bytes)) {
202
+ return refused("launcher_mismatch");
203
+ }
204
+ const observed = [...files.values()].sort((a, b) => key(a.path) < key(b.path) ? -1 : key(a.path) > key(b.path) ? 1 : 0);
205
+ if (observed.some(file => !/^[a-f0-9]{64}$/i.test(fileDigest(file)))) return refused("read_failed");
206
+ const digest = hash(JSON.stringify(["opencodex-explicit-installation-observation-v1", key(prefix),
207
+ key(candidate), key(npmCli), key(node), ...observed.map(fileEvidence)]));
208
+ return report("identity_observed", { codex: codexVersion, npm: npmVersion, digest }, source);
209
+ } catch { return refused("read_failed"); }
210
+ }
@@ -0,0 +1,158 @@
1
+ import { closeSync, existsSync, openSync, readSync } from "node:fs";
2
+ import { win32 } from "node:path";
3
+ import { SHIM_MARKER } from "./shim-templates";
4
+ import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
5
+
6
+ /**
7
+ * The proof-captured launcher snapshot used to identify the selected candidate.
8
+ * Every field is optional because older launchers and direct Bun/source runs
9
+ * supply no snapshot at all; derivation fails closed rather than reading the
10
+ * ambient environment.
11
+ */
12
+ export interface CodexCliInstallationSnapshot {
13
+ readonly codexCliPath?: string | null;
14
+ readonly path?: string | null;
15
+ readonly pathExt?: string | null;
16
+ }
17
+
18
+ export type CodexCliInstallationTargetRefusal =
19
+ | "unsupported_platform"
20
+ | "candidate_unavailable"
21
+ | "unsupported_layout"
22
+ | "toolchain_unresolved";
23
+
24
+ export type CodexCliInstallationTargetDerivation =
25
+ | { readonly kind: "derived"; readonly input: CodexCliInstallationIdentityInput }
26
+ | { readonly kind: "unavailable"; readonly reason: CodexCliInstallationTargetRefusal };
27
+
28
+ export interface CodexCliInstallationTargetDeps {
29
+ readonly platform?: NodeJS.Platform;
30
+ readonly exists?: (path: string) => boolean;
31
+ /** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
32
+ readonly fileContains?: (path: string, marker: string) => boolean;
33
+ }
34
+
35
+ const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
36
+ const SHIM_PROBE_BYTES = 8 * 1024;
37
+ const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
38
+
39
+ function defaultFileContains(path: string, marker: string): boolean {
40
+ let descriptor: number | undefined;
41
+ let contains = false;
42
+ try {
43
+ descriptor = openSync(path, "r");
44
+ const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
45
+ const count = readSync(descriptor, bytes, 0, bytes.length, 0);
46
+ contains = bytes.subarray(0, count).toString("utf8").includes(marker);
47
+ } catch {
48
+ contains = false;
49
+ } finally {
50
+ if (descriptor !== undefined) {
51
+ try { closeSync(descriptor); } catch { contains = false; }
52
+ }
53
+ }
54
+ return contains;
55
+ }
56
+
57
+ /**
58
+ * First match wins, mirroring PATH resolution: directories in order, and within
59
+ * each directory every PATHEXT suffix in order (or the exact name when it
60
+ * already carries an extension). Skipping a hit to keep scanning would attest
61
+ * something other than the launcher that actually resolves.
62
+ */
63
+ function scanPath(
64
+ name: string,
65
+ pathValue: string | null | undefined,
66
+ pathExt: string | null | undefined,
67
+ exists: (path: string) => boolean,
68
+ ): string | null {
69
+ const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
70
+ const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
71
+ for (const entry of (pathValue ?? "").split(";")) {
72
+ const dir = entry.trim().replace(/^"+|"+$/g, "");
73
+ if (!dir) continue;
74
+ for (const candidateName of names) {
75
+ const candidate = win32.join(dir, candidateName);
76
+ if (exists(candidate)) return candidate;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Derive the four attestation inputs from the proof-captured launcher snapshot.
84
+ * Discovery only proposes candidate paths; the held-handle observation remains
85
+ * the authority, so a stale or racing probe can only produce a refusal, never a
86
+ * false identity. No ambient environment is read: without a snapshot the result
87
+ * is unavailable rather than silently trusting the child's environment.
88
+ */
89
+ export function deriveCodexCliInstallationInput(
90
+ snapshot: CodexCliInstallationSnapshot,
91
+ deps: CodexCliInstallationTargetDeps = {},
92
+ ): CodexCliInstallationTargetDerivation {
93
+ if ((deps.platform ?? process.platform) !== "win32") {
94
+ return { kind: "unavailable", reason: "unsupported_platform" };
95
+ }
96
+ const exists = deps.exists ?? existsSync;
97
+ const fileContains = deps.fileContains ?? defaultFileContains;
98
+ const configured = snapshot.codexCliPath;
99
+
100
+ let candidate: string | null;
101
+ if (configured) {
102
+ if (/^[a-z]:[\\/]/i.test(configured)) {
103
+ if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
104
+ candidate = win32.normalize(configured);
105
+ } else {
106
+ if (configured.includes("/") || configured.includes("\\")) {
107
+ return { kind: "unavailable", reason: "candidate_unavailable" };
108
+ }
109
+ candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
110
+ if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
111
+ }
112
+ } else {
113
+ candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
114
+ if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
115
+ }
116
+
117
+ // An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
118
+ // artifact. The renamed original beside it is the file npm wrote.
119
+ if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
120
+ const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
121
+ if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
122
+ candidate = backing;
123
+ }
124
+
125
+ const base = win32.basename(candidate).toLowerCase();
126
+ let prefix: string;
127
+ if (base === "codex.cmd" || base === "codex.opencodex-real.cmd") {
128
+ prefix = win32.dirname(candidate);
129
+ } else if (candidate.toLowerCase().endsWith(CODEX_PACKAGE_SUFFIX)) {
130
+ prefix = candidate.slice(0, candidate.length - CODEX_PACKAGE_SUFFIX.length);
131
+ } else {
132
+ return { kind: "unavailable", reason: "unsupported_layout" };
133
+ }
134
+ if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
135
+ return { kind: "unavailable", reason: "unsupported_layout" };
136
+ }
137
+
138
+ // The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
139
+ let node = win32.join(prefix, "node.exe");
140
+ if (!exists(node)) {
141
+ const resolved = scanPath("node.exe", snapshot.path, null, exists);
142
+ if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
143
+ node = resolved;
144
+ }
145
+ const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
146
+ if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
147
+
148
+ return {
149
+ kind: "derived",
150
+ input: Object.freeze({
151
+ candidate,
152
+ npmPrefix: prefix,
153
+ npmCli,
154
+ node,
155
+ candidateSource: "selected",
156
+ }),
157
+ };
158
+ }
@@ -36,6 +36,7 @@ import {
36
36
  catalogHasRoutedEntries,
37
37
  findSupportedNativeTemplate,
38
38
  legacyCatalogBackupPath,
39
+ nativeMultiAgentDefaults,
39
40
  parseCatalogJson,
40
41
  type RawCatalog,
41
42
  type RawEntry,
@@ -69,6 +70,7 @@ import {
69
70
  clampCatalogModelsToObservedCodexSupport,
70
71
  supportedCodexReasoningEffortsFromObservedCatalog,
71
72
  } from "./catalog/effort";
73
+ import { suppressedSyntheticMaxCatalogSlugs } from "./catalog/model-hints";
72
74
  import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
73
75
  import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
74
76
  import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
@@ -305,6 +307,7 @@ function prepareCatalog(
305
307
  [catalog.models ?? [], ...nativeRecoverySources],
306
308
  );
307
309
  const catalogModels = nativeCatalogModels;
310
+ const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(config, ordered, catalogModels);
308
311
  const routedEntries = buildCatalogEntriesFromObservedState({
309
312
  template: template ? JSON.parse(JSON.stringify(template)) : null,
310
313
  gptSlugs: [],
@@ -371,8 +374,10 @@ function prepareCatalog(
371
374
  includeNativeOpenAi,
372
375
  accountBoundEntries,
373
376
  suppressedBareNativeSlugs,
377
+ suppressedSyntheticMaxSlugs,
374
378
  openaiContextCap,
375
379
  nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
380
+ nativeMultiAgentDefaults: nativeMultiAgentDefaults(baselineCatalogModels),
376
381
  policy: {
377
382
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
378
383
  nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
@@ -0,0 +1,145 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { shouldSyncCodexOnStart } from "./desired-state";
3
+ import {
4
+ isEffectiveCodexClientCompaction,
5
+ isEffectiveCodexDesktopAuthless,
6
+ } from "./loopback-target";
7
+
8
+ export type CodexDesktopSwitchInertReason =
9
+ | "client_role"
10
+ | "non_loopback_bind_requires_admission_token";
11
+
12
+ export interface CodexDesktopSwitchState {
13
+ stored: boolean;
14
+ effective: boolean;
15
+ inertReason?: CodexDesktopSwitchInertReason;
16
+ }
17
+
18
+ export type CodexDesktopSwitchApplyReason =
19
+ | "not_requested"
20
+ | "proxy_not_running"
21
+ | "integration_disabled"
22
+ | "write_lock_busy"
23
+ | "injection_refused";
24
+
25
+ export type CodexDesktopSwitchApply =
26
+ | { applied: true }
27
+ | {
28
+ applied: false;
29
+ reason: CodexDesktopSwitchApplyReason;
30
+ retryable: boolean;
31
+ detail?: string;
32
+ };
33
+
34
+ export interface CodexDesktopSwitchReport {
35
+ codexDesktopAuthless: CodexDesktopSwitchState;
36
+ codexClientCompaction: CodexDesktopSwitchState;
37
+ apply: CodexDesktopSwitchApply;
38
+ authSource: { presentsCodexAccount: boolean; summary: string };
39
+ }
40
+
41
+ type DesktopSwitchConfig = Pick<
42
+ OcxConfig,
43
+ | "clientIntegrations"
44
+ | "runtimeRole"
45
+ | "hostname"
46
+ | "unauthenticatedLoopbackListener"
47
+ | "codexDesktopAuthless"
48
+ | "codexClientCompaction"
49
+ >;
50
+
51
+ function describeSwitch(
52
+ stored: boolean,
53
+ effective: boolean,
54
+ config: Pick<OcxConfig, "runtimeRole">,
55
+ ): CodexDesktopSwitchState {
56
+ if (!stored || effective) return { stored, effective };
57
+ return {
58
+ stored,
59
+ effective,
60
+ inertReason: config.runtimeRole === "client"
61
+ ? "client_role"
62
+ : "non_loopback_bind_requires_admission_token",
63
+ };
64
+ }
65
+
66
+ export function describeCodexDesktopSwitches(
67
+ config: DesktopSwitchConfig,
68
+ apply: CodexDesktopSwitchApply,
69
+ ): CodexDesktopSwitchReport {
70
+ const authlessStored = config.codexDesktopAuthless === true;
71
+ const authlessEffective = isEffectiveCodexDesktopAuthless(config);
72
+ const compactionStored = config.codexClientCompaction === true;
73
+ const compactionEffective = isEffectiveCodexClientCompaction(config);
74
+
75
+ return {
76
+ codexDesktopAuthless: describeSwitch(authlessStored, authlessEffective, config),
77
+ codexClientCompaction: describeSwitch(compactionStored, compactionEffective, config),
78
+ apply,
79
+ authSource: authlessEffective
80
+ ? {
81
+ presentsCodexAccount: false,
82
+ summary: "The Codex app will not require its own account sign-in.",
83
+ }
84
+ : {
85
+ presentsCodexAccount: true,
86
+ summary: "The Codex app will require its own account sign-in.",
87
+ },
88
+ };
89
+ }
90
+
91
+ export async function applyCodexDesktopSwitches(
92
+ config: OcxConfig,
93
+ ): Promise<CodexDesktopSwitchApply> {
94
+ if (!shouldSyncCodexOnStart(config)) {
95
+ return { applied: false, reason: "integration_disabled", retryable: false };
96
+ }
97
+
98
+ const { readRuntimePort } = await import("../config/process-state");
99
+ const runtime = readRuntimePort(process.pid);
100
+ if (!runtime) {
101
+ return { applied: false, reason: "proxy_not_running", retryable: true };
102
+ }
103
+
104
+ try {
105
+ // Imported at call time, not module load. The settings route reaches this module on
106
+ // every GET, and pulling the whole injection graph in just to report stored-versus-
107
+ // effective state would put it on a read path that never writes anything.
108
+ const { injectCodexConfig } = await import("./inject");
109
+ const result = await injectCodexConfig(runtime.port, config);
110
+ if (result.status === "skipped") {
111
+ return {
112
+ applied: false,
113
+ reason: "integration_disabled",
114
+ retryable: false,
115
+ detail: result.message,
116
+ };
117
+ }
118
+ if (result.success) {
119
+ // history_paginated_requires_native_writer stands down only the legacy relabel;
120
+ // apply still writes the routing and catalog half for paginated Codex homes.
121
+ return { applied: true };
122
+ }
123
+ if (result.retryable === true) {
124
+ return {
125
+ applied: false,
126
+ reason: "write_lock_busy",
127
+ retryable: true,
128
+ detail: result.message,
129
+ };
130
+ }
131
+ return {
132
+ applied: false,
133
+ reason: "injection_refused",
134
+ retryable: false,
135
+ detail: result.message,
136
+ };
137
+ } catch (error) {
138
+ return {
139
+ applied: false,
140
+ reason: "injection_refused",
141
+ retryable: false,
142
+ detail: error instanceof Error ? error.message : "Codex config injection failed.",
143
+ };
144
+ }
145
+ }