@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { collapseAntigravityPublicModels } from "../../providers/antigravity-effort-families";
2
3
  import { createHash } from "node:crypto";
3
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
4
5
  import { delimiter, dirname, join, resolve } from "node:path";
@@ -504,6 +505,7 @@ export function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<Catalo
504
505
  }
505
506
 
506
507
  export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): CatalogModel[] {
508
+ goModels = collapseAntigravityPublicModels(goModels);
507
509
  const collisionSkipped = resolveSlugAliasCollisions(goModels);
508
510
  const comboPublicSlugs = new Set(goModels
509
511
  .filter(model => model.provider === COMBO_NAMESPACE)
@@ -525,6 +527,7 @@ export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): Cata
525
527
  }
526
528
 
527
529
  export function uniqueCatalogModelsForRawPublicList(goModels: CatalogModel[]): CatalogModel[] {
530
+ goModels = collapseAntigravityPublicModels(goModels);
528
531
  const publicId = (model: CatalogModel): string => model.alias ?? `${model.provider}/${model.id}`;
529
532
  const comboPublicIds = new Set(goModels
530
533
  .filter(model => model.provider === COMBO_NAMESPACE)
@@ -372,7 +372,7 @@ export function orderForModelPicker(
372
372
  * user tooling do not. `owned_by` describes upstream ownership, and `comp_hash`
373
373
  * describes history compatibility; neither is an authorship signal.
374
374
  */
375
- function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
375
+ export function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
376
376
  if (isNativeAliasCatalogEntry(entry)) return true;
377
377
  const desc = typeof entry.description === "string" ? entry.description : "";
378
378
  const slug = typeof entry.slug === "string" ? entry.slug : "";
@@ -29,6 +29,7 @@ import type { NormalizedComboConfig } from "../../combos/types";
29
29
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
30
30
  import { redactSecretString } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
+ import { decodeOverlayState } from "../shim-state-file";
32
33
 
33
34
 
34
35
  import { activeCodexModelsCachePath, catalogBackupPathFor, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, legacyCatalogBackupPath, parseCatalogJson, readCatalog, readCatalogBackup, readCodexCatalogPath } from "./parsing";
@@ -196,14 +197,20 @@ export function codexCommandCandidates(): string[] {
196
197
 
197
198
  export function codexShimCommandCandidates(): string[] {
198
199
  try {
199
- const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
200
+ const configDir = getConfigDir();
201
+ const state = JSON.parse(readFileSync(join(configDir, "codex-shim.json"), "utf8")) as {
202
+ schema?: unknown;
203
+ mode?: unknown;
200
204
  wrapperPath?: unknown;
201
205
  originalPath?: unknown;
202
206
  backupPath?: unknown;
203
207
  wrappers?: Array<{ wrapperPath?: unknown; originalPath?: unknown; backupPath?: unknown }>;
204
208
  };
209
+ const overlay = decodeOverlayState(state, configDir);
210
+ if ((state.schema !== undefined || state.mode !== undefined) && !overlay) return [];
205
211
  const files = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
206
- const out: string[] = [];
212
+ const out: string[] = overlay && isSpawnableCodexCandidate(overlay.launcherPath)
213
+ ? [overlay.launcherPath] : [];
207
214
  for (const file of files) {
208
215
  for (const value of [file.backupPath, file.originalPath, file.wrapperPath]) {
209
216
  if (typeof value !== "string" || value.length === 0) continue;
@@ -0,0 +1,66 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, type Stats } from "node:fs";
2
+ import { readCodexHomeJournal } from "../codex-home-owner";
3
+ import { samePath, type RawCatalog } from "./parsing";
4
+
5
+ export const CATALOG_HEAL_MAX_BYTES = 64 * 1024 * 1024;
6
+
7
+ export interface CatalogObservation {
8
+ readonly signature: string;
9
+ readonly catalog: RawCatalog | null;
10
+ }
11
+
12
+ /** Canonical path equality when available; unavailable paths retain lexical equality. */
13
+ export function sameCatalogHealPath(left: string, right: string): boolean {
14
+ try { return samePath(realpathSync.native(left), realpathSync.native(right)); }
15
+ catch { return samePath(left, right); }
16
+ }
17
+
18
+ /** Resolve authority without reading catalog bytes or cleaning journal evidence. */
19
+ export function selectCatalogHealPath(
20
+ journalPath: string,
21
+ defaultPath: string,
22
+ resolvePath: (path: string) => string,
23
+ ): string | null {
24
+ const inspected = readCodexHomeJournal(journalPath);
25
+ if (inspected.kind === "unknown") return null;
26
+ if (inspected.kind === "missing") return defaultPath;
27
+ const selected = inspected.journal.injectedCatalogPath;
28
+ if (selected === undefined || selected === null) return defaultPath;
29
+ // A selected custom catalog remains selected even when missing or corrupt.
30
+ if (typeof selected !== "string" || !selected.trim() || selected.includes("\0")) return null;
31
+ try { return resolvePath(selected); }
32
+ catch { return null; }
33
+ }
34
+
35
+ function signature(stat: Stats): string {
36
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
37
+ }
38
+
39
+ /** Bounded regular descriptor reads; never open a FIFO/device or follow a replaced symlink. */
40
+ export function observeCatalogHealFile(path: string, readContent: boolean): CatalogObservation | null {
41
+ let fd: number | undefined;
42
+ try {
43
+ const before = lstatSync(path);
44
+ if (!before.isFile() || before.size > CATALOG_HEAL_MAX_BYTES) return null;
45
+ if (!readContent) return { signature: signature(before), catalog: null };
46
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
47
+ const opened = fstatSync(fd);
48
+ if (!opened.isFile() || opened.size > CATALOG_HEAL_MAX_BYTES || signature(before) !== signature(opened)) return null;
49
+ const bytes = Buffer.alloc(opened.size + 1);
50
+ let length = 0;
51
+ while (length < bytes.length) {
52
+ const count = readSync(fd, bytes, length, bytes.length - length, null);
53
+ if (count === 0) break;
54
+ length += count;
55
+ }
56
+ if (length !== opened.size || signature(fstatSync(fd)) !== signature(opened)
57
+ || signature(lstatSync(path)) !== signature(opened)) return null;
58
+ let catalog: RawCatalog | null = null;
59
+ try {
60
+ const value: unknown = JSON.parse(bytes.subarray(0, length).toString("utf8"));
61
+ if (value && typeof value === "object" && Array.isArray((value as RawCatalog).models)) catalog = value as RawCatalog;
62
+ } catch { /* Invalid catalog bytes remain unavailable until their stat changes. */ }
63
+ return { signature: signature(opened), catalog };
64
+ } catch { return null; }
65
+ finally { if (fd !== undefined) closeSync(fd); }
66
+ }
@@ -295,7 +295,8 @@ export function applyProviderConfigHints(
295
295
  inputModalities = base.includes("image") ? [...base] : [...base, "image"];
296
296
  }
297
297
  const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
298
- const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
298
+ const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id)
299
+ ?? (model.antigravityEffortWireModelIds !== undefined);
299
300
  const defaultReasoningEffort = staticPolicy.model.defaultReasoningEffort ?? model.defaultReasoningEffort;
300
301
  const supportsReasoningSummaries = staticPolicy.model.supportsReasoningSummaries;
301
302
  const supportsVerbosity = staticPolicy.model.supportsVerbosity;
@@ -1,4 +1,5 @@
1
1
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
+ import { antigravityFamilyDisabled, collapseAntigravityPublicModels, projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
2
3
  import { XAI_OAUTH_FAST_VARIANT_IDS } from "../../providers/xai-fast-model";
3
4
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
4
5
  import { execFileSync } from "node:child_process";
@@ -267,7 +268,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
267
268
 
268
269
  export function filterCatalogVisibleModels(
269
270
  models: CatalogModel[],
270
- config: Pick<OcxConfig, "disabledModels" | "providers">,
271
+ config: Pick<OcxConfig, "disabledModels" | "providers" | "modelDiscovery">,
271
272
  ): CatalogModel[] {
272
273
  const disabled = new Set(config.disabledModels ?? []);
273
274
  const allowByProvider = new Map<string, Set<string>>();
@@ -289,12 +290,14 @@ export function filterCatalogVisibleModels(
289
290
  // Two catalog stages with different equivalence relations is the exact bug class
290
291
  // this change exists to remove.
291
292
  if (Array.isArray(sel) && sel.length > 0) {
292
- allowByProvider.set(name, new Set(sel.map(model => slugEquivalenceKey(routedSlug(name, model)))));
293
+ allowByProvider.set(name, new Set(projectAntigravitySelectedModels(name, sel, models)
294
+ .map(model => slugEquivalenceKey(routedSlug(name, model)))));
293
295
  }
294
296
  }
295
- return models.filter(m => {
297
+ return collapseAntigravityPublicModels(models).filter(m => {
296
298
  if (initialModelSelectionPending(config.providers[m.provider])) return false;
297
299
  if (config.providers[m.provider]?.disabled === true) return false;
300
+ if (antigravityFamilyDisabled(config, m, models)) return false;
298
301
  const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
299
302
  // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
300
303
  for (const stored of disabled) {
@@ -113,6 +113,8 @@ export interface CatalogModel {
113
113
  displayName?: string;
114
114
  owned_by?: string;
115
115
  reasoningEfforts?: string[];
116
+ /** Exact CCA family evidence, retained through the in-memory discovery cache. */
117
+ antigravityEffortWireModelIds?: Partial<Record<"low" | "medium" | "high", string>>;
116
118
  /** Suppress only catalog synthesis of a missing max rung; provider-declared max survives. */
117
119
  suppressSyntheticMax?: boolean;
118
120
  defaultReasoningEffort?: string;
@@ -751,20 +751,29 @@ export async function fetchProviderModelsWithAuth(
751
751
  const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
752
752
  id: model.id,
753
753
  provider: name,
754
- // CCA only exposes a numeric thinking budget. Until the adapter owns an exact Codex
755
- // effort-to-wire mapping for a newly discovered model, do not advertise a false ladder.
756
- reasoningEfforts: [],
754
+ // Only an exact discovered wire map proves a new model's selectable effort ladder.
755
+ reasoningEfforts: Object.keys(model.effortWireModelIds ?? {}),
756
+ ...(model.effortWireModelIds ? {
757
+ antigravityEffortWireModelIds: model.effortWireModelIds,
758
+ suppressSyntheticMax: true,
759
+ defaultReasoningEffort: model.effortWireModelIds.medium ? "medium" : "high",
760
+ } : {}),
757
761
  ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
758
762
  ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
759
763
  }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
760
764
  const forCache = withConfiguredRetention(live, { retainComboTargets: false });
761
- if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
765
+ if (!isCurrentCacheGeneration()) {
762
766
  return observed(withConfiguredRetention(configured), "degraded");
763
767
  }
768
+ // Save exact family routes before publishing synthetic IDs. These synchronous writes
769
+ // cannot interleave with a credential-generation change; failure keeps the prior cache.
764
770
  registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
765
771
  provider: name,
766
772
  cacheGeneration,
767
773
  });
774
+ if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
775
+ return observed(withConfiguredRetention(configured), "degraded");
776
+ }
768
777
  markProviderDiscoveryOk(name, live.length);
769
778
  return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
770
779
  }
@@ -0,0 +1,19 @@
1
+ import type { CatalogWriteIntent } from "./write-audit-contract";
2
+
3
+ export type CatalogPublicationEvent =
4
+ | { readonly kind: "published"; readonly path: string; readonly intent: CatalogWriteIntent }
5
+ | { readonly kind: "native-released"; readonly path: string | null };
6
+
7
+ const listeners = new Set<(event: CatalogPublicationEvent) => void>();
8
+
9
+ /** Process-local notifications; subscribers never own publication success. */
10
+ export function subscribeCatalogPublication(listener: (event: CatalogPublicationEvent) => void): () => void {
11
+ listeners.add(listener);
12
+ return () => { listeners.delete(listener); };
13
+ }
14
+
15
+ export function notifyCatalogPublication(event: CatalogPublicationEvent): void {
16
+ for (const listener of [...listeners]) {
17
+ try { listener(event); } catch { /* A subscriber cannot fail or undo publication. */ }
18
+ }
19
+ }
@@ -7,7 +7,7 @@ import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
7
7
  import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
8
8
  import { getCodexHome } from "../paths";
9
9
  import { readCodexCatalogPathForHome } from "./parsing";
10
- import { invalidateCodexModelsCacheWithPermit } from "./sync";
10
+ import { invalidateCodexModelsCacheWithPermitOutcome } from "./sync";
11
11
 
12
12
  const DEFAULT_TIMEOUT_MS = 15_000;
13
13
  const MAX_MODELS = 2_000;
@@ -17,7 +17,7 @@ const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
17
17
  export type RemoteCatalogFailureCode =
18
18
  | "url_invalid" | "insecure_http_refused" | "credential_invalid" | "request_failed"
19
19
  | "redirect_refused" | "http_error" | "body_too_large" | "body_invalid"
20
- | "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path";
20
+ | "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path" | "foreign_owner" | "owner_unknown";
21
21
 
22
22
  export class RemoteCatalogError extends Error {
23
23
  constructor(readonly code: RemoteCatalogFailureCode, message: string, readonly status?: number) {
@@ -202,7 +202,10 @@ export async function fetchRemoteCatalog(
202
202
 
203
203
  function mapSerializationFailure<T>(outcome: CatalogSerializationOutcome<T>): never {
204
204
  if (outcome.kind === "completed") throw new RemoteCatalogError("write_failed", "Remote catalog installation failed");
205
- const code = outcome.reason === "busy" ? "lock_busy" : outcome.reason === "database" ? "lock_database" : "unsafe_path";
205
+ const code = outcome.reason === "busy" ? "lock_busy"
206
+ : outcome.reason === "database" ? "lock_database"
207
+ : outcome.reason === "foreign-owner" ? "foreign_owner"
208
+ : outcome.reason === "owner-unknown" ? "owner_unknown" : "unsafe_path";
206
209
  throw new RemoteCatalogError(code, `Remote catalog installation unavailable (${outcome.reason})`);
207
210
  }
208
211
 
@@ -235,23 +238,18 @@ export async function pullRemoteCatalog(input: string, options: PullRemoteCatalo
235
238
  const fetched = await fetchRemoteCatalog(input, options);
236
239
  const codexHome = options.codexHome ?? getCodexHome();
237
240
  const catalogPath = readCodexCatalogPathForHome(codexHome);
238
- const current = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
239
- const candidate = Buffer.from(fetched.content, "utf8");
240
- if (current?.equals(candidate)) {
241
- return { status: "unchanged", catalogWritten: false, cacheSynced: false, codexHome, catalogPath, modelCount: fetched.document.models.length };
242
- }
243
241
  const outcome = withCatalogWriteSerialization(codexHome, permit => {
244
242
  // Re-check under K: another writer may have installed these bytes while the request was in flight.
245
243
  const lockedCurrent = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
246
- if (lockedCurrent?.equals(candidate)) return { catalogWritten: false, cacheSynced: false };
247
- replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: fetched.content });
248
- const cacheSynced = invalidateCodexModelsCacheWithPermit(permit, codexHome, { allowWhenDesiredDisabled: true });
249
- if (!cacheSynced) {
250
- restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
244
+ const catalog = replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: fetched.content });
245
+ if (catalog.kind === "refused") throw new RemoteCatalogError("write_failed", "Remote catalog publication refused");
246
+ const cache = invalidateCodexModelsCacheWithPermitOutcome(permit, codexHome, { allowWhenDesiredDisabled: true });
247
+ if (cache !== "written" && cache !== "unchanged") {
248
+ if (catalog.kind === "written") restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
251
249
  throw new RemoteCatalogError("write_failed", "Remote catalog cache synchronization failed");
252
250
  }
253
- return { catalogWritten: true, cacheSynced: true };
254
- });
251
+ return { catalogWritten: catalog.kind === "written", cacheSynced: cache === "written" };
252
+ }, { intent: "pull", writer: "catalog-pull" });
255
253
  if (outcome.kind !== "completed") return mapSerializationFailure(outcome);
256
254
  return {
257
255
  status: outcome.value.catalogWritten ? "updated" : "unchanged",
@@ -6,9 +6,12 @@ import { RETIRED_NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./m
6
6
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
7
7
  import {
8
8
  withCatalogWriteSerialization,
9
+ CatalogWritePermitRefusal,
9
10
  type CatalogWritePermit,
10
11
  } from "../catalog-write-serialization";
11
12
  import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
13
+ import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE } from "./routed-removal";
14
+ import { notifyCatalogPublication } from "./publication-observer";
12
15
 
13
16
  function visibleAccountReplacementNatives(
14
17
  models: readonly RawEntry[],
@@ -72,7 +75,10 @@ export function restoreCodexCatalogWithPermit(
72
75
  ): { removed: number; kept: number; path: string } {
73
76
  const catalogPath = injectedCatalogPath ?? readCodexCatalogPath();
74
77
  const catalog = readCatalog(catalogPath);
75
- if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
78
+ if (!catalog || !Array.isArray(catalog.models)) {
79
+ notifyCatalogPublication({ kind: "published", path: catalogPath, intent: "restore" });
80
+ return { removed: 0, kept: 0, path: catalogPath };
81
+ }
76
82
  const disabledModels = currentDisabledModelsForRestore();
77
83
  const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
78
84
  const backup = readCatalogBackup(catalogPath);
@@ -94,10 +100,11 @@ export function restoreCodexCatalogWithPermit(
94
100
  models: [...backup.models.filter(m => typeof m.slug !== "string"
95
101
  || !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions],
96
102
  };
97
- replaceActiveCodexCatalog(permit, owningCodexHome, {
103
+ const replacement = replaceActiveCodexCatalog(permit, owningCodexHome, {
98
104
  path: catalogPath,
99
105
  content: `${JSON.stringify(restored, null, 2)}\n`,
100
106
  });
107
+ if (replacement.kind === "refused") throw new CatalogWritePermitRefusal("Catalog restoration was refused.");
101
108
  return { removed, kept: restored.models.length, path: catalogPath };
102
109
  }
103
110
  const before = catalog.models.length;
@@ -110,11 +117,13 @@ export function restoreCodexCatalogWithPermit(
110
117
  const removed = before - native.length;
111
118
  if (removed > 0) {
112
119
  catalog.models = native;
113
- replaceActiveCodexCatalog(permit, owningCodexHome, {
120
+ const replacement = replaceActiveCodexCatalog(permit, owningCodexHome, {
114
121
  path: catalogPath,
115
122
  content: `${JSON.stringify(catalog, null, 2)}\n`,
116
123
  });
124
+ if (replacement.kind === "refused") throw new CatalogWritePermitRefusal("Catalog restoration was refused.");
117
125
  }
126
+ if (removed === 0) notifyCatalogPublication({ kind: "published", path: catalogPath, intent: "restore" });
118
127
  return { removed, kept: native.length, path: catalogPath };
119
128
  }
120
129
 
@@ -123,10 +132,14 @@ export function restoreCodexCatalog(): { removed: number; kept: number; path: st
123
132
  const outcome = withCatalogWriteSerialization(
124
133
  owningCodexHome,
125
134
  permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
135
+ { intent: "restore", writer: "catalog-restore" },
126
136
  );
127
- return outcome.kind === "completed"
128
- ? outcome.value
129
- : { removed: 0, kept: 0, path: readCodexCatalogPath() };
137
+ if (outcome.kind === "unavailable") {
138
+ throw new CatalogWritePermitRefusal(outcome.reason === "foreign-owner"
139
+ ? FOREIGN_CODEX_HOME_OWNER_MESSAGE : outcome.reason === "owner-unknown"
140
+ ? UNKNOWN_CODEX_HOME_OWNER_MESSAGE : `Catalog restoration unavailable (${outcome.reason}).`);
141
+ }
142
+ return outcome.value;
130
143
  }
131
144
 
132
145
  /** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
@@ -1,7 +1,8 @@
1
+ import { projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { initializeConfigOwnership } from "../../lib/config-ownership";
4
- import { getConfigDir, loadConfig, websocketsEnabled } from "../../config";
5
+ import { getConfigDir, loadConfig, websocketsEnabled, withConfigMutationLockSync } from "../../config";
5
6
  import { shouldSyncCodexOnStart } from "../desired-state";
6
7
  import { legacyCustomModelCatalogSlugs } from "../custom-model-catalog-migration";
7
8
  import { getCodexHome } from "../paths";
@@ -39,6 +40,8 @@ import {
39
40
  nativeMultiAgentDefaults,
40
41
  } from "./parsing";
41
42
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
43
+ import { ocxRoutedNamespaceCounts, routedRemovalBackedByConfigFile, unconfiguredRoutedRemoval } from "./routed-removal";
44
+ import { ConfigMutationLockError } from "../../config/mutation-lock";
42
45
  import {
43
46
  accountBoundNativeOpenAiSlugsBySelector,
44
47
  desktopAllowlistSuppressedNativeSlugs,
@@ -63,11 +66,13 @@ import {
63
66
  } from "../catalog-write-serialization";
64
67
  import {
65
68
  preparedBytesDifferFromDisk,
69
+ auditRefusedCatalogReplacement,
66
70
  publishHashedCodexCatalogBackup,
67
71
  publishLegacyCodexCatalogBackup,
68
72
  replaceActiveCodexCatalog,
69
73
  replaceCodexModelsCache,
70
74
  type PreparedCatalogFileWrite,
75
+ type CatalogFileReplacement,
71
76
  } from "../internal/catalog-writer";
72
77
  import { visibleCodexAccountSelectors } from "./account-models";
73
78
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
@@ -104,8 +109,15 @@ interface RetainedCatalogSyncResult {
104
109
  comboOmissions: ComboCatalogOmission[];
105
110
  /** Validated catalog commit (including identical bytes), or a refused refresh. */
106
111
  refreshOutcome?: "committed" | "refused";
107
- /** `desired_disabled` observed under K after the provider await; nothing was written. */
108
- skippedReason?: "desired_disabled";
112
+ /**
113
+ * `desired_disabled`: observed under K after the provider await. `unbacked_routed_removal`: the
114
+ * catalog would lose routed namespaces config.json still enables, or config.json is missing or
115
+ * unreadable (#6529). `foreign_owner`: this Codex home is bound to another OPENCODEX_HOME. Nothing
116
+ * was written in any of these cases.
117
+ */
118
+ skippedReason?: "desired_disabled" | "unbacked_routed_removal" | "foreign_owner" | "owner_unknown";
119
+ /** With `unbacked_routed_removal`: how many routed namespaces the refused write would have emptied. */
120
+ protectedRoutedNamespaces?: number;
109
121
  }
110
122
 
111
123
  /**
@@ -465,7 +477,7 @@ function writeRetainedCatalogSync({
465
477
  provider.disabled !== true
466
478
  && Array.isArray(provider.selectedModels)
467
479
  && provider.selectedModels.length > 0
468
- ? [[name, new Set(provider.selectedModels)] as const]
480
+ ? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, goModels))] as const]
469
481
  : []
470
482
  )),
471
483
  );
@@ -553,12 +565,46 @@ function writeRetainedCatalogSync({
553
565
  if (!preparedBytesDifferFromDisk(preparedCatalog)) {
554
566
  return { added, path: catalogPath, catalogWritten: false, comboOmissions };
555
567
  }
556
-
557
- replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
568
+ // A refresh may drop a provider's rows only when config.json on disk agrees the provider is gone.
569
+ // Without that, a config that is not the user's (missing or unreadable file read as defaults,
570
+ // another OPENCODEX_HOME) would publish a native-only catalog and exit cleanly (#6529).
571
+ const refused = (protectedRoutedNamespaces: number): RetainedCatalogSyncResult => ({
572
+ added: 0,
573
+ path: catalogPath,
574
+ catalogWritten: false,
575
+ comboOmissions,
576
+ skippedReason: "unbacked_routed_removal",
577
+ protectedRoutedNamespaces,
578
+ });
579
+ let replacement: CatalogFileReplacement | undefined;
580
+ const removal = unconfiguredRoutedRemoval(onDiskCatalog, catalog, config);
581
+ if (removal !== null) {
582
+ // Hold C (K -> C) from the config read through the replacement, so a save that enables one of
583
+ // these providers cannot land between the check and the write. A held C is a refusal too.
584
+ let backed = false;
585
+ try {
586
+ backed = withConfigMutationLockSync(() => {
587
+ if (!routedRemovalBackedByConfigFile(removal)) return false;
588
+ replacement = replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
589
+ return replacement.kind !== "refused";
590
+ });
591
+ } catch (error) {
592
+ if (!(error instanceof ConfigMutationLockError)) throw error;
593
+ }
594
+ if (!backed) {
595
+ if (replacement?.kind !== "refused") {
596
+ auditRefusedCatalogReplacement(permit, owningCodexHome, preparedCatalog, "unbacked-routed-removal");
597
+ }
598
+ return refused(removal.namespaces.length);
599
+ }
600
+ } else {
601
+ replacement = replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
602
+ if (replacement.kind === "refused") return refused(ocxRoutedNamespaceCounts(onDiskCatalog).size);
603
+ }
558
604
  return {
559
605
  added,
560
606
  path: catalogPath,
561
- catalogWritten: true,
607
+ catalogWritten: replacement?.kind === "written",
562
608
  comboOmissions,
563
609
  };
564
610
  }
@@ -648,7 +694,7 @@ export async function syncCatalogModels(
648
694
  owningCodexHome,
649
695
  modelEntitlements,
650
696
  });
651
- });
697
+ }, { intent: "refresh", writer: "retained-sync" });
652
698
  if (committed.kind === "completed" && committed.value !== null) {
653
699
  return {
654
700
  ...committed.value,
@@ -661,6 +707,10 @@ export async function syncCatalogModels(
661
707
  catalogWritten: false,
662
708
  comboOmissions,
663
709
  refreshOutcome: "refused",
710
+ ...(committed.kind === "unavailable" && committed.reason === "foreign-owner"
711
+ ? { skippedReason: "foreign_owner" as const } : {}),
712
+ ...(committed.kind === "unavailable" && committed.reason === "owner-unknown"
713
+ ? { skippedReason: "owner_unknown" as const } : {}),
664
714
  };
665
715
  }
666
716
 
@@ -728,9 +778,7 @@ export function invalidateCodexModelsCacheWithPermitOutcome(
728
778
  // `cacheSynced` mean what its name and its consumers already assume, and what
729
779
  // `pullRemoteCatalog` and the early returns in `refreshCodexModelCatalog`
730
780
  // already assert: a write happened.
731
- if (!preparedBytesDifferFromDisk(preparedCache)) return "unchanged";
732
- replaceCodexModelsCache(permit, owningCodexHome, preparedCache);
733
- return "written";
781
+ return replaceCodexModelsCache(permit, owningCodexHome, preparedCache).kind;
734
782
  } catch {
735
783
  return "failed";
736
784
  }
@@ -749,6 +797,7 @@ export function invalidateCodexModelsCache(options?: CodexCatalogSyncOptions): b
749
797
  const outcome = withCatalogWriteSerialization(
750
798
  owningCodexHome,
751
799
  permit => invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, options),
800
+ { intent: "cache", writer: "cache-invalidate" },
752
801
  );
753
802
  return outcome.kind === "completed" && outcome.value;
754
803
  }
@@ -0,0 +1,116 @@
1
+ import { COMBO_NAMESPACE } from "../../combos";
2
+ import { readConfigAdmissionSnapshot, type ConfigAdmissionSnapshot } from "../../config/diagnostics";
3
+ import type { OcxConfig } from "../../types";
4
+ import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
5
+ import { isOcxAuthoredRoutedEntry } from "./build-entries";
6
+ import { isNativeAliasCatalogEntry } from "./metadata";
7
+ import type { RawCatalog, RawEntry } from "./parsing";
8
+
9
+ /**
10
+ * Routed namespaces a candidate catalog empties of OpenCodex-authored rows only because the config
11
+ * that produced it does not enable them: the "deleted provider" rule in build-entries. That rule is
12
+ * the one way a refresh removes a provider's rows wholesale, so it is the one a config that is not
13
+ * the user's (a missing or unreadable config.json read as defaults, another OPENCODEX_HOME) turns
14
+ * into a silent native-only catalog (#6529).
15
+ */
16
+ export interface UnconfiguredRoutedRemoval {
17
+ readonly namespaces: readonly string[];
18
+ }
19
+
20
+ type RoutedNamespaceConfig = Pick<OcxConfig, "providers" | "combos">;
21
+
22
+ function routedNamespace(entry: RawEntry): string | null {
23
+ if (!entry || typeof entry !== "object" || typeof entry.slug !== "string") return null;
24
+ // Account-bound native rows follow the account pool, not provider config.
25
+ if (trustedAccountBoundNativeCatalogSlug(entry) !== undefined) return null;
26
+ if (isNativeAliasCatalogEntry(entry)) return COMBO_NAMESPACE;
27
+ // Combo aliases may be bare or use an unrelated provider namespace. Ownership selects the
28
+ // combo namespace only with the same generated-description authorship signal as routed rows.
29
+ if (entry.owned_by === COMBO_NAMESPACE
30
+ && typeof entry.description === "string"
31
+ && entry.description.startsWith("Routed via opencodex → ")) return COMBO_NAMESPACE;
32
+ // Foreign rows (Cursor, user tooling) are never removed by the provider rule.
33
+ if (!isOcxAuthoredRoutedEntry(entry)) return null;
34
+ return entry.slug.slice(0, entry.slug.indexOf("/"));
35
+ }
36
+
37
+ /** OpenCodex-authored routed rows per namespace; foreign and account-bound rows never count. */
38
+ export function ocxRoutedNamespaceCounts(catalog: RawCatalog | null): Map<string, number> {
39
+ const counts = new Map<string, number>();
40
+ for (const entry of catalog?.models ?? []) {
41
+ const namespace = routedNamespace(entry);
42
+ if (namespace !== null) counts.set(namespace, (counts.get(namespace) ?? 0) + 1);
43
+ }
44
+ return counts;
45
+ }
46
+
47
+ /** Total OpenCodex-authored routed rows, or null for a value that is not a catalog. */
48
+ export function ocxRoutedRowCount(catalog: unknown): number | null {
49
+ if (catalog === null || typeof catalog !== "object" || !Array.isArray((catalog as RawCatalog).models)) return null;
50
+ let total = 0;
51
+ for (const count of ocxRoutedNamespaceCounts(catalog as RawCatalog).values()) total += count;
52
+ return total;
53
+ }
54
+
55
+ /** Whether a config still produces routed rows for a namespace: an enabled provider, or any combo. */
56
+ export function configEnablesRoutedNamespace(config: RoutedNamespaceConfig, namespace: string): boolean {
57
+ if (namespace === COMBO_NAMESPACE && Object.keys(config.combos ?? {}).length > 0) return true;
58
+ const provider = config.providers?.[namespace];
59
+ return provider !== undefined && provider.disabled !== true;
60
+ }
61
+
62
+ /**
63
+ * The namespaces whose OpenCodex-authored routed rows are on disk now, absent from the candidate,
64
+ * and not enabled by the driving config; null when the candidate removes no such namespace.
65
+ * Removals inside a namespace the driving config still enables (discovery, selection, a deleted
66
+ * custom model) are the provider's own business and are not reported here.
67
+ */
68
+ export function unconfiguredRoutedRemoval(
69
+ active: RawCatalog | null,
70
+ candidate: RawCatalog,
71
+ driving: RoutedNamespaceConfig,
72
+ ): UnconfiguredRoutedRemoval | null {
73
+ const activeModels = active?.models ?? [];
74
+ if (activeModels.length === 0) return null;
75
+ const kept = new Set((candidate.models ?? []).flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []));
76
+ const namespaces = new Set<string>();
77
+ for (const entry of activeModels) {
78
+ if (typeof entry.slug === "string" && kept.has(entry.slug)) continue;
79
+ const namespace = routedNamespace(entry);
80
+ if (namespace !== null && !configEnablesRoutedNamespace(driving, namespace)) namespaces.add(namespace);
81
+ }
82
+ return namespaces.size > 0 ? { namespaces: [...namespaces].sort() } : null;
83
+ }
84
+
85
+ /**
86
+ * True when config.json on disk backs every removal: it is a readable, parseable file, and it enables
87
+ * none of the removed namespaces. A missing, unreadable or salvaged file backs nothing, and a file
88
+ * that still enables a namespace shows the driving config was not the one on disk. Read once, at
89
+ * write time, so a config that fell back to defaults while the file was briefly unreadable cannot
90
+ * pass on a later successful read.
91
+ */
92
+ export function routedRemovalBackedByConfigFile(
93
+ removal: UnconfiguredRoutedRemoval,
94
+ read: () => ConfigAdmissionSnapshot = readConfigAdmissionSnapshot,
95
+ ): boolean {
96
+ const snapshot = read();
97
+ if (snapshot.kind !== "read" || snapshot.diagnostics.source !== "file") return false;
98
+ return removal.namespaces.every(namespace => !configEnablesRoutedNamespace(snapshot.diagnostics.config, namespace));
99
+ }
100
+
101
+ /** One line for logs when K refused a writer from another OPENCODEX_HOME; names no path. */
102
+ export const FOREIGN_CODEX_HOME_OWNER_MESSAGE = "Codex catalog left unchanged: this Codex home was set up by "
103
+ + "another OpenCodex home (OPENCODEX_HOME), recorded in its opencodex-journal.json. Run opencodex "
104
+ + "from that home, or run `ocx restore` there first.";
105
+
106
+ /** Unavailable evidence is not proof of a foreign owner. No paths or journal content are exposed. */
107
+ export const UNKNOWN_CODEX_HOME_OWNER_MESSAGE = "Codex catalog left unchanged: ownership of this Codex home "
108
+ + "could not be checked. Restore readable ownership evidence and retry.";
109
+
110
+ /** One line for logs: counts only, never provider names, model ids or paths. */
111
+ export function unbackedRoutedRemovalMessage(count: number): string {
112
+ return `Codex catalog left unchanged: this refresh would remove the routed models of ${count} `
113
+ + `provider namespace${count === 1 ? "" : "s"} that config.json still enables, or config.json `
114
+ + "is missing or unreadable. Run `ocx sync` again once config.json is readable; use `ocx restore` "
115
+ + "to remove routed models on purpose.";
116
+ }