@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  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 +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -0,0 +1,96 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { isEffectiveCodexDesktopAuthless } from "../loopback-target";
3
+ import { NATIVE_RESERVE_MODEL } from "./native-models";
4
+
5
+ /**
6
+ * #4811: why an opted-in install still gets no Luna Reserve row.
7
+ *
8
+ * The Reserve projection is account-qualified — its slug is `<selector>/gpt-reserve` — so it
9
+ * cannot be written without a selector that targets the main Codex account. On a fresh authless
10
+ * install `codexAccountNamespaces` is empty, the account picker is therefore off, no selectors
11
+ * exist, and `createReserveCatalogProjection` returns `undefined`. Nothing downstream can
12
+ * explain that: an omitted row has no place to carry a reason, which is exactly the shape of
13
+ * #4212 for account-gated natives, and the same answer applies — say it once, here, while the
14
+ * inputs that produced the omission are still in scope.
15
+ *
16
+ * Deliberately scoped to an operator who asked for the feature. A default install has
17
+ * `codexDesktopAuthless` unset, wants no Reserve row, and must not be told about one. An install
18
+ * that stores the flag on a bind where it cannot take effect is a different silence, already
19
+ * reported as `inertReason` by `describeCodexDesktopSwitches`, and is not repeated here.
20
+ */
21
+ export type ReserveCatalogSuppressionReason =
22
+ | "no-canonical-openai-provider"
23
+ | "account-picker-disabled"
24
+ | "no-account-namespaces"
25
+ | "no-main-account-selector";
26
+
27
+ type ReserveSuppressionConfig = Pick<
28
+ OcxConfig,
29
+ | "runtimeRole"
30
+ | "hostname"
31
+ | "unauthenticatedLoopbackListener"
32
+ | "codexDesktopAuthless"
33
+ | "codexAccountNamespaces"
34
+ | "codexAccountPickerEnabled"
35
+ >;
36
+
37
+ export interface ReserveSuppressionInput {
38
+ /** Already resolved by the caller; passed in so this module stays off the metadata graph. */
39
+ includeAccountBoundNativeOpenAi: boolean;
40
+ /** Selectors that resolved to the main Codex account. A non-empty list means no suppression. */
41
+ mainSelectors: readonly string[];
42
+ }
43
+
44
+ export function reserveCatalogSuppressionReason(
45
+ config: ReserveSuppressionConfig,
46
+ input: ReserveSuppressionInput,
47
+ ): ReserveCatalogSuppressionReason | undefined {
48
+ if (!isEffectiveCodexDesktopAuthless(config) || input.mainSelectors.length > 0) return undefined;
49
+ if (!input.includeAccountBoundNativeOpenAi) return "no-canonical-openai-provider";
50
+ // Checked before the empty-map case: when an operator has explicitly turned the picker off,
51
+ // naming the empty map would send them to hand-author selectors that the opt-in generates.
52
+ if (config.codexAccountPickerEnabled === false) return "account-picker-disabled";
53
+ if (Object.keys(config.codexAccountNamespaces ?? {}).length === 0) return "no-account-namespaces";
54
+ return "no-main-account-selector";
55
+ }
56
+
57
+ const SUPPRESSION_TEXT: Readonly<Record<ReserveCatalogSuppressionReason, {
58
+ cause: string;
59
+ action: string;
60
+ }>> = {
61
+ "no-canonical-openai-provider": {
62
+ cause: "no enabled canonical OpenAI provider can serve account-qualified native routes",
63
+ action: "Enable the built-in \"openai\" provider with forwarded ChatGPT auth.",
64
+ },
65
+ "account-picker-disabled": {
66
+ cause: "the Codex account picker is turned off (codexAccountPickerEnabled is false)",
67
+ action: "Set codexAccountPickerEnabled to true to generate the default account selectors.",
68
+ },
69
+ "no-account-namespaces": {
70
+ cause: "no Codex account selectors are configured (codexAccountNamespaces is empty)",
71
+ action: "Enable the Codex account picker once to generate the default account selectors.",
72
+ },
73
+ "no-main-account-selector": {
74
+ cause: "no configured selector in codexAccountNamespaces targets the main Codex account",
75
+ action: "Map one selector to the main Codex account.",
76
+ },
77
+ };
78
+
79
+ const warnedReserveSuppression = new Set<string>();
80
+
81
+ /** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
82
+ export function resetReserveSuppressionWarningsForTests(): void {
83
+ warnedReserveSuppression.clear();
84
+ }
85
+
86
+ export function warnReserveSuppressedOnce(reason: ReserveCatalogSuppressionReason): void {
87
+ if (warnedReserveSuppression.has(reason)) return;
88
+ warnedReserveSuppression.add(reason);
89
+ const text = SUPPRESSION_TEXT[reason];
90
+ console.warn(
91
+ `[opencodex] catalog sync: authless Codex Desktop routing is on, but the Luna Reserve row `
92
+ + `(<selector>/${NATIVE_RESERVE_MODEL}) is not being written because ${text.cause}. `
93
+ + `The Reserve projection is account-qualified and needs a selector for the main Codex `
94
+ + `account. ${text.action}`,
95
+ );
96
+ }
@@ -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 {
@@ -57,10 +59,12 @@ import {
57
59
  type CatalogWritePermit,
58
60
  } from "../catalog-write-serialization";
59
61
  import {
62
+ preparedBytesDifferFromDisk,
60
63
  publishHashedCodexCatalogBackup,
61
64
  publishLegacyCodexCatalogBackup,
62
65
  replaceActiveCodexCatalog,
63
66
  replaceCodexModelsCache,
67
+ type PreparedCatalogFileWrite,
64
68
  } from "../internal/catalog-writer";
65
69
  import { visibleCodexAccountSelectors } from "./account-models";
66
70
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
@@ -75,6 +79,7 @@ import {
75
79
  import { finishUpstreamNativeEntry } from "./derive-entry";
76
80
  import { finalizeAutoReviewModelOverride } from "./auto-review";
77
81
  import { gatedNativeAccountLabel, gatedNativeReauthSuppressionReason, warnGatedNativeSuppressedOnce } from "./gated-native-warn";
82
+ import { reserveCatalogSuppressionReason, warnReserveSuppressedOnce } from "./reserve-warn";
78
83
 
79
84
  interface RetainedCatalogSyncRead {
80
85
  readonly catalogPath: string;
@@ -231,24 +236,6 @@ function revalidateRetainedCatalogSync(
231
236
  };
232
237
  }
233
238
 
234
- /**
235
- * Exact bytes currently on disk at `path`, or null when unreadable/absent.
236
- *
237
- * Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
238
- * substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
239
- * equal to prepared content holding a legitimately encoded U+FFFD. Comparing the
240
- * decoded strings would then classify a malformed catalog as identical, skip the
241
- * atomic repair write, and leave the corruption on disk while reporting
242
- * `catalogWritten: false`.
243
- */
244
- function currentCatalogFileContent(path: string): Buffer | null {
245
- try {
246
- return readFileSync(path);
247
- } catch {
248
- return null;
249
- }
250
- }
251
-
252
239
  function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
253
240
  if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
254
241
  try {
@@ -320,6 +307,11 @@ function writeRetainedCatalogSync({
320
307
  const enabledGo = filterCatalogVisibleModels(goModels, config);
321
308
  const featured = config.subagentModels ?? [];
322
309
  const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
310
+ const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(
311
+ config,
312
+ orderedGoModels,
313
+ catalogModelsForMerge,
314
+ );
323
315
  const modelPickerOrder = config.modelPickerOrder ?? [];
324
316
  const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
325
317
  const exactComboSlugs = exactComboCatalogSlugs(config);
@@ -376,6 +368,14 @@ function writeRetainedCatalogSync({
376
368
  const accountTargets = new Map(codexAccountNamespaceEntries(config));
377
369
  const reserveMainSelectors = accountSelectors.filter(selector =>
378
370
  isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
371
+ // #4811: an omitted Reserve row carries no reason, so the explanation has to be emitted here,
372
+ // where the selector inputs that produced the omission are still in scope. Silent for every
373
+ // install that did not opt into authless Codex Desktop routing.
374
+ const reserveSuppression = reserveCatalogSuppressionReason(config, {
375
+ includeAccountBoundNativeOpenAi,
376
+ mainSelectors: reserveMainSelectors,
377
+ });
378
+ if (reserveSuppression) warnReserveSuppressedOnce(reserveSuppression);
379
379
  // The active file can own a bare source even when the bundled catalog is the build base.
380
380
  // A previously clamped qualified projection must not shorten a retained genuine ladder.
381
381
  const reserveObservations = [
@@ -443,6 +443,7 @@ function writeRetainedCatalogSync({
443
443
  // like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
444
444
  const baselineCatalog = readCatalogBackup(catalogPath);
445
445
  const baseline = readNativeBaseline(catalogPath);
446
+ const nativePinBaseline = nativeMultiAgentDefaults(baselineCatalog?.models);
446
447
  const gatheredProviderNames = new Set(
447
448
  Object.entries(config.providers ?? {})
448
449
  .filter(([, prov]) => prov.disabled !== true)
@@ -512,8 +513,10 @@ function writeRetainedCatalogSync({
512
513
  includeNativeOpenAi,
513
514
  accountBoundEntries,
514
515
  suppressedBareNativeSlugs,
516
+ suppressedSyntheticMaxSlugs,
515
517
  openaiContextCap,
516
518
  nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
519
+ nativeMultiAgentDefaults: nativePinBaseline,
517
520
  policy: {
518
521
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
519
522
  nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
@@ -539,15 +542,12 @@ function writeRetainedCatalogSync({
539
542
  // nothing about the catalog changed. Skipping the no-op write keeps both the mtime
540
543
  // and `catalogWritten` honest; `added` still reports the routed rows the catalog
541
544
  // carries, because they are on disk either way.
542
- const onDiskBytes = currentCatalogFileContent(catalogPath);
543
- if (onDiskBytes !== null && onDiskBytes.equals(Buffer.from(content, "utf8"))) {
545
+ const preparedCatalog: PreparedCatalogFileWrite = { path: catalogPath, content };
546
+ if (!preparedBytesDifferFromDisk(preparedCatalog)) {
544
547
  return { added, path: catalogPath, catalogWritten: false, comboOmissions };
545
548
  }
546
549
 
547
- replaceActiveCodexCatalog(permit, owningCodexHome, {
548
- path: catalogPath,
549
- content,
550
- });
550
+ replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
551
551
  return {
552
552
  added,
553
553
  path: catalogPath,
@@ -694,10 +694,25 @@ export function invalidateCodexModelsCacheWithPermit(
694
694
  client_version: "0.0.0",
695
695
  models: [...models, ...observedAccountModels],
696
696
  };
697
- replaceCodexModelsCache(permit, owningCodexHome, {
697
+ const preparedCache: PreparedCatalogFileWrite = {
698
698
  path: cachePath,
699
699
  content: `${JSON.stringify(wrapper, null, 2)}\n`,
700
- });
700
+ };
701
+ // The same no-op rule the active catalog already applies (#1459), for the same
702
+ // reason and at the second writer that has to obey it.
703
+ //
704
+ // This function is what `refreshCodexModelCatalog` reports as `cacheSynced`, and
705
+ // `handleStart` ORs that into the stale-app-server warning. Rewriting identical
706
+ // bytes bumped this file's mtime and returned `true`, so on a start where the
707
+ // catalog reproduced byte-identically — the settled case — the warning still
708
+ // claimed "Disk catalog/cache were updated" and told the operator their Codex
709
+ // model list might be stale, when nothing on disk had changed and Codex held the
710
+ // same model set the file already described. Returning `false` here makes
711
+ // `cacheSynced` mean what its name and its consumers already assume, and what
712
+ // `pullRemoteCatalog` and the early returns in `refreshCodexModelCatalog`
713
+ // already assert: a write happened.
714
+ if (!preparedBytesDifferFromDisk(preparedCache)) return false;
715
+ replaceCodexModelsCache(permit, owningCodexHome, preparedCache);
701
716
  return true;
702
717
  } catch {
703
718
  return false;
@@ -113,6 +113,15 @@ export interface GatherRoutedModelsOptions {
113
113
  providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
114
114
  /** Internal convergence sink for the immutable policy that produced the returned rows. */
115
115
  discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
116
+ /**
117
+ * Each provider's cache content revision as of the moment its rows were chosen.
118
+ *
119
+ * A caller that derives something from these rows and wants to know later whether the rows are
120
+ * still current must use this, not a revision sampled after the gather returns: another flight
121
+ * can publish between the choice and the sample, and the derived value would then carry that
122
+ * flight's identity while holding these rows.
123
+ */
124
+ providerContentRevisions?: Map<string, string>;
116
125
  }
117
126
 
118
127
  interface GatherFlightResult {
@@ -121,6 +130,7 @@ interface GatherFlightResult {
121
130
  providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
122
131
  providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
123
132
  discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
133
+ providerContentRevisions: ReadonlyMap<string, string>;
124
134
  }
125
135
  interface GatherInflightEntry {
126
136
  readonly discoveryPolicyIdentity: string;
@@ -256,6 +266,7 @@ async function gatherRoutedModelsWithAuth(
256
266
  providerAuthOutcomes,
257
267
  providerModelOutcomes,
258
268
  discoveryPolicySnapshots,
269
+ providerContentRevisions,
259
270
  } = await entry.promise;
260
271
  if (options?.comboOmissions) {
261
272
  options.comboOmissions.length = 0;
@@ -269,6 +280,12 @@ async function gatherRoutedModelsWithAuth(
269
280
  options.providerModelOutcomes.length = 0;
270
281
  options.providerModelOutcomes.push(...providerModelOutcomes);
271
282
  }
283
+ if (options?.providerContentRevisions) {
284
+ options.providerContentRevisions.clear();
285
+ for (const [provider, revision] of providerContentRevisions) {
286
+ options.providerContentRevisions.set(provider, revision);
287
+ }
288
+ }
272
289
  if (options?.discoveryPolicySnapshots) {
273
290
  options.discoveryPolicySnapshots.length = 0;
274
291
  options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
@@ -763,6 +780,8 @@ async function gatherRoutedModelsUncached(
763
780
  providerAuthOutcomes: localProviderAuthOutcomes,
764
781
  providerModelOutcomes,
765
782
  discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
783
+ // Stamped by each provider at the moment it chose its rows, not sampled here.
784
+ providerContentRevisions: new Map(providerResults.map(result => [result.outcome.provider, result.contentRevision])),
766
785
  };
767
786
  }
768
787
 
@@ -850,6 +869,26 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
850
869
  ];
851
870
  }
852
871
 
872
+ /**
873
+ * Add generated-registry rows the live provider list did not return, and backfill a
874
+ * published context window onto a live row that arrived without one.
875
+ *
876
+ * The backfill is the reason this function has a merge branch at all. It used to skip every
877
+ * id the live list returned, which is correct for a row that already knows its window and
878
+ * wrong for the normal OpenCode Go case, where discovery returns an id with no context
879
+ * field at all. The serialized Codex catalog was unaffected, because
880
+ * `applyCatalogMetadata` writes `context_window` onto the entry straight from the generated
881
+ * table; the absence was only visible to consumers that read `CatalogModel.contextWindow`
882
+ * (#4971). Those are not cosmetic: `buildClaudeContextWindows` drops a routed row with no
883
+ * window from the map that decides the `[1m]` marker, so a published 1M model could not be
884
+ * recognized as one, and the Grok config writer and several client exporters omit the field
885
+ * entirely rather than emit a known value.
886
+ *
887
+ * It is a missing-value fill, never an override: a live positive window still wins, because
888
+ * the upstream is the authority on its own model. The seeded row is re-hinted so operator
889
+ * precedence is unchanged — a configured window still lowers it and `providerContextCaps`
890
+ * still caps it, exactly as for an appended row.
891
+ */
853
892
  export function augmentRoutedModelsWithMetadata(
854
893
  models: CatalogModel[],
855
894
  providerNames: string[],
@@ -858,7 +897,7 @@ export function augmentRoutedModelsWithMetadata(
858
897
  metadataModelIdCaseFoldByProvider?: ReadonlyMap<string, boolean>,
859
898
  ): CatalogModel[] {
860
899
  const out = [...models];
861
- const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
900
+ const indexByKey = new Map(out.map((model, index) => [`${model.provider}/${model.id}`, index]));
862
901
  for (const provider of providerNames) {
863
902
  if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
864
903
  if (providers?.[provider]?.liveModels === false) continue;
@@ -866,9 +905,28 @@ export function augmentRoutedModelsWithMetadata(
866
905
  if (!jawcodeProvider) continue;
867
906
  for (const meta of listModelMetadata(jawcodeProvider)) {
868
907
  const key = `${provider}/${meta.id}`;
869
- if (seen.has(key)) continue;
870
- seen.add(key);
871
908
  const contextCap = caps ? providerContextCap(caps, provider) : undefined;
909
+ const existingIndex = indexByKey.get(key);
910
+ if (existingIndex !== undefined) {
911
+ const existing = out[existingIndex]!;
912
+ const publishedWindow = typeof meta.contextWindow === "number" && meta.contextWindow > 0
913
+ ? meta.contextWindow
914
+ : undefined;
915
+ const liveWindow = typeof existing.contextWindow === "number" && existing.contextWindow > 0;
916
+ if (liveWindow || publishedWindow === undefined) continue;
917
+ const seeded: CatalogModel = { ...existing, contextWindow: publishedWindow };
918
+ out[existingIndex] = providers?.[provider]
919
+ ? applyProviderConfigHints(
920
+ provider,
921
+ providers[provider],
922
+ seeded,
923
+ contextCap,
924
+ metadataModelIdCaseFoldByProvider?.get(provider),
925
+ )
926
+ : seeded;
927
+ continue;
928
+ }
929
+ indexByKey.set(key, out.length);
872
930
  const model: CatalogModel = {
873
931
  provider,
874
932
  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
+ }