@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  inspectCodexCliInstall,
3
+ observeCodexCliSelectedCommand,
3
4
  type CodexCliInstallProvenanceDeps,
4
5
  type CodexCliInstallReport,
5
6
  } from "../codex/cli-install-provenance";
@@ -11,20 +12,29 @@ import type {
11
12
  CodexCliInstallationSnapshot,
12
13
  CodexCliInstallationTargetDerivation,
13
14
  } from "../codex/cli-installation-targets";
15
+ import {
16
+ createCodexCliUpdatePlan,
17
+ type CodexCliUpdateChannel,
18
+ type CodexCliUpdatePlan,
19
+ type CodexCliUpdatePlanDeps,
20
+ } from "../codex/cli-update-plan";
21
+ import {
22
+ type ResolveCodexRuntimeDeps,
23
+ type ResolveCodexRuntimeResult,
24
+ } from "../codex/runtime";
14
25
  import { CliUsageError, isJsonOption, printData, runCliAction } from "./runtime-api";
15
26
  import { trustedNodeLauncherContext } from "./launcher-context";
16
27
 
17
28
  export const CODEX_CLI_UPDATE_USAGE = `Usage:
18
29
  ocx system codex-cli-update check [--json]
19
30
  ocx system codex-cli-update attest [--json]
20
- ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]`;
31
+ ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]
32
+ ocx system codex-cli-update plan [--channel latest] [--json]`;
21
33
 
22
- export type ParsedCodexCliUpdateArgs = Readonly<{
23
- json: boolean;
24
- }> | Readonly<{
25
- json: boolean;
26
- attest: CodexCliInstallationIdentityInput | "selected";
27
- }>;
34
+ export type ParsedCodexCliUpdateArgs =
35
+ | Readonly<{ action: "check"; json: boolean }>
36
+ | Readonly<{ action: "plan"; json: boolean; channel: CodexCliUpdateChannel }>
37
+ | Readonly<{ json: boolean; attest: CodexCliInstallationIdentityInput | "selected" }>;
28
38
 
29
39
  export interface CodexCliUpdateCommandDeps {
30
40
  readonly inspectInstall?: (deps: CodexCliInstallProvenanceDeps) => Promise<CodexCliInstallReport>;
@@ -32,6 +42,8 @@ export interface CodexCliUpdateCommandDeps {
32
42
  readonly deriveInstallationInput?: (
33
43
  snapshot: CodexCliInstallationSnapshot,
34
44
  ) => Promise<CodexCliInstallationTargetDerivation>;
45
+ readonly createPlan?: (deps: CodexCliUpdatePlanDeps) => Promise<CodexCliUpdatePlan>;
46
+ readonly resolveSelectedRuntime?: (deps: ResolveCodexRuntimeDeps) => ResolveCodexRuntimeResult;
35
47
  }
36
48
 
37
49
  function identitySummary(report: CodexCliInstallationIdentityReport): string[] {
@@ -68,24 +80,60 @@ function installSummary(report: CodexCliInstallReport): string[] {
68
80
  ];
69
81
  }
70
82
 
83
+ function planSummary(plan: CodexCliUpdatePlan): string[] {
84
+ const lines = [
85
+ `applicable: ${plan.applicable ? "yes" : "no"}`,
86
+ `reason: ${plan.refusal ?? "none"}`,
87
+ `provenance: ${plan.provenance}`,
88
+ `installed-version: ${plan.installedVersion ?? "unavailable"}`,
89
+ `version-evidence: ${plan.versionEvidence}`,
90
+ `channel: ${plan.channel}`,
91
+ `target-version: ${plan.targetVersion ?? "unresolved"}`,
92
+ `target-integrity: ${plan.targetIntegrity ?? "unresolved"}`,
93
+ `session: ${plan.session.state}${plan.session.matches === null ? "" : ` (${plan.session.matches})`}`,
94
+ ];
95
+ if (plan.planId) lines.push(`plan: ${plan.planId}`);
96
+ if (plan.command) lines.push(`command: ${plan.command.join(" ")} (indicative — a future apply phase verifies the packed tarball and installs the verified file)`);
97
+ return lines;
98
+ }
99
+
100
+ /**
101
+ * Read ``--name value`` and ``--name=value`` alike; both spellings reach this CLI. */
102
+ function optionValue(tokens: readonly string[], index: number, name: string): { value: string; next: number } {
103
+ const token = tokens[index]!;
104
+ const inline = `--${name}=`;
105
+ if (token.startsWith(inline)) {
106
+ const value = token.slice(inline.length);
107
+ if (!value) throw new CliUsageError(`--${name} requires a value`, CODEX_CLI_UPDATE_USAGE);
108
+ return { value, next: index + 1 };
109
+ }
110
+ const value = tokens[index + 1];
111
+ if (value === undefined) throw new CliUsageError(`--${name} requires a value`, CODEX_CLI_UPDATE_USAGE);
112
+ return { value, next: index + 2 };
113
+ }
114
+
115
+ function isOption(token: string, name: string): boolean {
116
+ return token === `--${name}` || token.startsWith(`--${name}=`);
117
+ }
118
+
71
119
  export function parseCodexCliUpdateArgs(argv: readonly string[]): ParsedCodexCliUpdateArgs {
72
120
  // `--json` is accepted in any argv position CLI-wide, so remove it before positional
73
- // validation. Requiring `check` at index 0 first would reject `--json check`, which
121
+ // validation. Requiring the action at index 0 first would reject `--json check`, which
74
122
  // automation that puts output flags ahead of the subcommand legitimately produces.
75
123
  let json = false;
76
- const positional: string[] = [];
124
+ const rest: string[] = [];
77
125
  for (const token of argv) {
78
126
  if (isJsonOption(token)) {
79
127
  if (json) throw new CliUsageError("--json may be specified only once", CODEX_CLI_UPDATE_USAGE);
80
128
  json = true;
81
129
  continue;
82
130
  }
83
- positional.push(token);
131
+ rest.push(token);
84
132
  }
85
- if (positional[0] === "attest") {
133
+ if (rest[0] === "attest") {
86
134
  // No options: attest the selected candidate identified from the proof-bound
87
135
  // launcher snapshot. The four explicit paths remain all-or-none.
88
- if (positional.length === 1) {
136
+ if (rest.length === 1) {
89
137
  return Object.freeze({ json, attest: "selected" as const });
90
138
  }
91
139
  const options = new Map<string, keyof CodexCliInstallationIdentityInput>([
@@ -93,12 +141,12 @@ export function parseCodexCliUpdateArgs(argv: readonly string[]): ParsedCodexCli
93
141
  ["--npm-cli", "npmCli"], ["--node", "node"],
94
142
  ]);
95
143
  const input: Partial<Record<keyof CodexCliInstallationIdentityInput, string>> = {};
96
- for (let index = 1; index < positional.length; index += 2) {
97
- const key = options.get(positional[index]!);
144
+ for (let index = 1; index < rest.length; index += 2) {
145
+ const key = options.get(rest[index]!);
98
146
  if (!key || input[key] !== undefined) {
99
147
  throw new CliUsageError("unsupported or duplicate attest option", CODEX_CLI_UPDATE_USAGE);
100
148
  }
101
- const value = positional[index + 1];
149
+ const value = rest[index + 1];
102
150
  if (!value || !value.trim() || /[\0\r\n]/.test(value)
103
151
  || !(value.startsWith("/") || /^[a-z]:[\\/]/i.test(value))) {
104
152
  throw new CliUsageError("attest options require explicit absolute paths", CODEX_CLI_UPDATE_USAGE);
@@ -112,13 +160,78 @@ export function parseCodexCliUpdateArgs(argv: readonly string[]): ParsedCodexCli
112
160
  candidate: input.candidate, npmPrefix: input.npmPrefix, npmCli: input.npmCli, node: input.node,
113
161
  }) });
114
162
  }
115
- if (positional[0] !== "check") {
116
- throw new CliUsageError("codex-cli-update action must be check or attest", CODEX_CLI_UPDATE_USAGE);
163
+ const action = rest[0];
164
+ if (action !== "check" && action !== "plan") {
165
+ throw new CliUsageError("codex-cli-update action must be check, attest or plan", CODEX_CLI_UPDATE_USAGE);
166
+ }
167
+
168
+ if (action === "check") {
169
+ if (rest.length > 1) throw new CliUsageError("unsupported codex-cli-update argument", CODEX_CLI_UPDATE_USAGE);
170
+ return Object.freeze({ action, json });
117
171
  }
118
- if (positional.length > 1) {
119
- throw new CliUsageError("unsupported codex-cli-update argument", CODEX_CLI_UPDATE_USAGE);
172
+
173
+ if (action === "plan") {
174
+ let channel: CodexCliUpdateChannel = "latest";
175
+ let seen = false;
176
+ let index = 1;
177
+ while (index < rest.length) {
178
+ const token = rest[index]!;
179
+ if (!isOption(token, "channel")) {
180
+ throw new CliUsageError("unsupported codex-cli-update argument", CODEX_CLI_UPDATE_USAGE);
181
+ }
182
+ if (seen) throw new CliUsageError("--channel may be specified only once", CODEX_CLI_UPDATE_USAGE);
183
+ const read = optionValue(rest, index, "channel");
184
+ // Only the stable channel is offered. A preview channel would need its own
185
+ // provenance story before it may install anything on the operator's behalf.
186
+ if (read.value !== "latest") throw new CliUsageError("--channel must be latest", CODEX_CLI_UPDATE_USAGE);
187
+ channel = read.value;
188
+ seen = true;
189
+ index = read.next;
190
+ }
191
+ return Object.freeze({ action, json, channel });
120
192
  }
121
- return Object.freeze({ json });
193
+
194
+ // Unreachable: the action check above already rejected every other verb, but the
195
+ // fallthrough keeps the parser total for the type checker.
196
+ throw new CliUsageError("unsupported codex-cli-update action", CODEX_CLI_UPDATE_USAGE);
197
+ }
198
+
199
+ /**
200
+ * Inspection inputs for this one-shot CLI process.
201
+ *
202
+ * The published Node launcher supplies a proof-bound snapshot of configured candidate
203
+ * evidence, not selected-runtime admission. A direct Bun or source launch has no such
204
+ * proof, so nothing ambient or persisted is inspected at all.
205
+ */
206
+ function inspectionDeps(commandDeps: CodexCliUpdateCommandDeps): CodexCliInstallProvenanceDeps {
207
+ const trusted = trustedNodeLauncherContext()?.codexCliInspectionEnv;
208
+ if (!trusted || trusted.managerRoots === null) return { env: { PATH: "" }, configDir: "." };
209
+ // Attestation also requires the runtime resolver to pick the snapshot's
210
+ // candidate. probeVersion: false keeps this read-only — no codex --version
211
+ // executes on a check/plan path — while still refusing attestation when the
212
+ // resolver would settle on a different executable.
213
+ const selectedCommand = observeCodexCliSelectedCommand({
214
+ env: {
215
+ CODEX_CLI_PATH: trusted.codexCliPath ?? undefined,
216
+ PATH: trusted.path ?? undefined,
217
+ PATHEXT: trusted.pathExt ?? undefined,
218
+ },
219
+ configDir: trusted.configDir,
220
+ }, commandDeps.resolveSelectedRuntime);
221
+ return {
222
+ env: {
223
+ ...trusted.managerRoots,
224
+ CODEX_CLI_PATH: trusted.codexCliPath ?? undefined,
225
+ PATH: trusted.path ?? undefined,
226
+ PATHEXT: trusted.pathExt ?? undefined,
227
+ },
228
+ configDir: trusted.configDir,
229
+ // The snapshot is proof-bound to the launcher, but the report may only attest
230
+ // the environment candidate when the resolver picks the same canonical path —
231
+ // managed requires both.
232
+ selectionAttested: true,
233
+ selectedCommand,
234
+ };
122
235
  }
123
236
 
124
237
  export async function handleCodexCliUpdateCommand(
@@ -136,7 +249,9 @@ export async function handleCodexCliUpdateCommand(
136
249
  }
137
250
  throw error;
138
251
  }
139
- return runCliAction(async () => {
252
+ // A refusal or an unapplied update is a legitimate answer rather than a crash, so the
253
+ // outcome exit code is decided here and only a thrown error is left to runCliAction.
254
+ const code = await runCliAction(async () => {
140
255
  if ("attest" in parsed) {
141
256
  let report: CodexCliInstallationIdentityReport;
142
257
  try {
@@ -167,25 +282,20 @@ export async function handleCodexCliUpdateCommand(
167
282
  printData(report, parsed.json, identitySummary(report));
168
283
  return;
169
284
  }
170
- const trustedInspectionEnv = trustedNodeLauncherContext()?.codexCliInspectionEnv;
171
- const inspectionDeps: CodexCliInstallProvenanceDeps = trustedInspectionEnv
172
- && trustedInspectionEnv.managerRoots !== null ? {
173
- env: {
174
- ...trustedInspectionEnv.managerRoots,
175
- CODEX_CLI_PATH: trustedInspectionEnv.codexCliPath ?? undefined,
176
- PATH: trustedInspectionEnv.path ?? undefined,
177
- PATHEXT: trustedInspectionEnv.pathExt ?? undefined,
178
- },
179
- configDir: trustedInspectionEnv.configDir,
180
- // This is a fresh one-shot CLI process. Its proof-bound launcher snapshot
181
- // supplies configured candidate evidence, not selected-runtime admission.
182
- } : {
183
- // Direct Bun/source launches have no pre-dotenv proof. Do not inspect
184
- // ambient or persisted candidate state at all.
185
- env: { PATH: "" },
186
- configDir: ".",
187
- };
188
- const report = await (deps.inspectInstall ?? inspectCodexCliInstall)(inspectionDeps);
189
- printData(report, parsed.json, installSummary(report));
285
+ if (parsed.action === "check") {
286
+ const report = await (deps.inspectInstall ?? inspectCodexCliInstall)(inspectionDeps(deps));
287
+ printData(report, parsed.json, installSummary(report));
288
+ return;
289
+ }
290
+ if (parsed.action === "plan") {
291
+ const plan = await (deps.createPlan ?? createCodexCliUpdatePlan)({
292
+ channel: parsed.channel,
293
+ inspectionDeps: inspectionDeps(deps),
294
+ inspect: deps.inspectInstall,
295
+ });
296
+ printData(plan, parsed.json, planSummary(plan));
297
+ return;
298
+ }
190
299
  });
300
+ return code;
191
301
  }
@@ -0,0 +1,44 @@
1
+ import { allowCodexProviderRouting, approvedCodexProviders, previewCodexProviderConsent, restoreApprovedCodexProviders } from "../codex/provider-routing";
2
+ import { interactiveConfirm } from "./interactive-confirm";
3
+ import { terminalSafeError, terminalSafeText } from "./runtime-api";
4
+
5
+ export const CODEX_PROVIDER_USAGE = "Usage: ocx codex-provider <status|allow <id> [--yes]|revoke <id>>";
6
+
7
+ export async function handleCodexProviderCommand(args: string[]): Promise<number> {
8
+ const [verb, id, flag] = args;
9
+ if (verb === "status" && args.length === 1) {
10
+ console.log(JSON.stringify({ approvedProviders: approvedCodexProviders() }));
11
+ return 0;
12
+ }
13
+ if ((verb !== "allow" && verb !== "revoke") || !id || args.length > 3
14
+ || (flag !== undefined && (verb !== "allow" || flag !== "--yes"))) {
15
+ console.error(CODEX_PROVIDER_USAGE);
16
+ return 1;
17
+ }
18
+ try {
19
+ if (verb === "revoke") {
20
+ restoreApprovedCodexProviders(id);
21
+ console.log(`Routing consent revoked for ${terminalSafeText(id)}; any unchanged managed URL was restored.`);
22
+ return 0;
23
+ }
24
+ const expected = previewCodexProviderConsent(id);
25
+ console.log(`OpenCodex will manage only base_url for Codex provider ${terminalSafeText(id)} in the selected CODEX_HOME.`);
26
+ console.log("Start/sync will route it to the local proxy; stop/restore will restore its original URL. Provider identity, transport settings and history stay unchanged.");
27
+ if (flag !== "--yes") {
28
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
29
+ console.error("Explicit consent required: use an interactive terminal or pass --yes with the exact provider ID.");
30
+ return 1;
31
+ }
32
+ if (!await interactiveConfirm({ question: "Allow this provider's routing to be managed?", defaultYes: false })) {
33
+ console.log("Cancelled; no consent or routing changes were saved.");
34
+ return 0;
35
+ }
36
+ }
37
+ allowCodexProviderRouting(id, expected);
38
+ console.log("Consent saved. Run ocx start or ocx sync to apply routing; use ocx codex-provider revoke <id> to undo consent.");
39
+ return 0;
40
+ } catch (error) {
41
+ console.error(terminalSafeError(error));
42
+ return 1;
43
+ }
44
+ }
@@ -8,8 +8,9 @@ type ComboInput = {
8
8
  imageInput?: "auto" | "disabled";
9
9
  reasoningEffortMode?: "strict" | "adaptive";
10
10
  nativeAlias?: boolean;
11
+ allowMeteredTargets?: boolean;
11
12
  };
12
- const TARGET_KEYS = new Set(["provider", "model", "weight", "reasoningEfforts", "modelProfile", "lastResort"]);
13
+ const TARGET_KEYS = new Set(["provider", "model", "weight", "reasoningEfforts", "modelProfile", "lastResort", "metered"]);
13
14
 
14
15
  export function parseComboTargets(value: unknown): Array<Record<string, unknown>> {
15
16
  if (!Array.isArray(value) || value.length === 0) throw new CliUsageError("Targets input must be a nonempty array");
@@ -41,6 +42,9 @@ export function parseComboTargets(value: unknown): Array<Record<string, unknown>
41
42
  if (Object.hasOwn(target, "lastResort") && typeof target.lastResort !== "boolean") {
42
43
  throw new CliUsageError("Target lastResort must be a boolean");
43
44
  }
45
+ if (Object.hasOwn(target, "metered") && typeof target.metered !== "boolean") {
46
+ throw new CliUsageError("Target metered must be a boolean");
47
+ }
44
48
  // Preserve raw slashes, target order, explicit false and omitted metadata.
45
49
  return { ...target };
46
50
  });
@@ -70,6 +74,10 @@ export async function prepareComboInput(args: string[], deps: RuntimeApiDeps = {
70
74
  const image = takeOptionWithSyntax(args, "--image-input")?.value;
71
75
  const reasoning = takeOptionWithSyntax(args, "--reasoning-effort-mode")?.value;
72
76
  const nativeAlias = takeNativeAlias(args);
77
+ const metered = takeOptionWithSyntax(args, "--allow-metered-targets")?.value;
78
+ if (metered !== undefined && metered !== "on" && metered !== "off") {
79
+ throw new CliUsageError("--allow-metered-targets must be on or off");
80
+ }
73
81
  if (file !== undefined && args.some(arg => arg === "--targets" || arg.startsWith("--targets="))) {
74
82
  throw new CliUsageError("--targets-file cannot be combined with --targets");
75
83
  }
@@ -84,5 +92,6 @@ export async function prepareComboInput(args: string[], deps: RuntimeApiDeps = {
84
92
  ...(image === undefined ? {} : { imageInput: image }),
85
93
  ...(reasoning === undefined ? {} : { reasoningEffortMode: reasoning }),
86
94
  ...(nativeAlias === undefined ? {} : { nativeAlias }),
95
+ ...(metered === undefined ? {} : { allowMeteredTargets: metered === "on" }),
87
96
  };
88
97
  }
@@ -33,7 +33,7 @@ function publicCombo(value: unknown): Record<string, unknown> {
33
33
  } else if (Object.hasOwn(RANGES, key)) {
34
34
  const [min, max] = RANGES[key]!;
35
35
  if (typeof child !== "number" || !Number.isSafeInteger(child) || child < min || child > max) invalid();
36
- } else if (key === "nativeAlias") {
36
+ } else if (key === "nativeAlias" || key === "allowMeteredTargets") {
37
37
  if (typeof child !== "boolean") invalid();
38
38
  } else invalid();
39
39
  result[key] = child;
package/src/cli/combo.ts CHANGED
@@ -23,7 +23,7 @@ const USAGE = `Usage:
23
23
  [--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->]
24
24
  [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->]
25
25
  (force overrides valid client effort and can increase cost/latency) [--alias <name|->]
26
- [--native-alias [on|off]] [--display-name <label|->]
26
+ [--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->]
27
27
  [--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>]
28
28
  [--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->]
29
29
  (jev only; the provider must be a configured jev-decision row)
@@ -160,6 +160,7 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
160
160
  if (effortMode !== undefined) combo.defaultEffortMode = effortMode === "-" ? "fallback" : effortMode;
161
161
  if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
162
162
  if (additional.nativeAlias !== undefined) combo.nativeAlias = additional.nativeAlias;
163
+ if (additional.allowMeteredTargets !== undefined) combo.allowMeteredTargets = additional.allowMeteredTargets;
163
164
  if (additional.imageInput !== undefined) combo.imageInput = additional.imageInput;
164
165
  if (additional.reasoningEffortMode !== undefined) combo.reasoningEffortMode = additional.reasoningEffortMode;
165
166
  if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
@@ -14,6 +14,7 @@ import { readClientConnectionState } from "../client/state";
14
14
  import { findManagedRegion, resolveGrokHome } from "../grok/inject";
15
15
  import { providerTableString } from "../codex/injected-marker";
16
16
  import { isLocalAttestationSecret } from "../lib/local-management-attestation";
17
+ import { isProtectedHomeUnderTest } from "../lib/test-home-guard";
17
18
  import { readOwnerRegistry } from "../config/owner-registry";
18
19
  import {
19
20
  classifyHealthz,
@@ -25,6 +26,7 @@ import {
25
26
  } from "../server/proxy-liveness";
26
27
  import { directLocalHttpFetch } from "../server/direct-local-http";
27
28
  import type { RuntimePortState } from "../config/process-state";
29
+ import { loopbackFamilyAvailability } from "./loopback-family";
28
30
 
29
31
  const MAX_HINT_BYTES = 256 * 1024;
30
32
  // Far above any real Grok config; discovery must not stall startup when Grok sync is off.
@@ -127,13 +129,19 @@ function loopbackPort(raw: string | null): number | null {
127
129
  }
128
130
  }
129
131
 
132
+ type CrossHomeOwnerOptions = {
133
+ homeDir?: string;
134
+ io?: LivenessIo;
135
+ loopbackAvailabilityFn?: typeof loopbackFamilyAvailability;
136
+ };
137
+
130
138
  /** Returns only a different process with an identity-checked /healthz response. */
131
- export async function findCrossHomeOwner(options: { homeDir?: string; io?: LivenessIo } = {}): Promise<number | null> {
139
+ export async function findCrossHomeOwner(options: CrossHomeOwnerOptions = {}): Promise<number | null> {
132
140
  const verdict = await findCrossHomeOwnerDetailed(options);
133
141
  return verdict.kind === "owner" ? verdict.port : null;
134
142
  }
135
143
 
136
- export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io?: LivenessIo } = {}): Promise<CrossHomeOwnerVerdict> {
144
+ export async function findCrossHomeOwnerDetailed(options: CrossHomeOwnerOptions = {}): Promise<CrossHomeOwnerVerdict> {
137
145
  const io = options.io ?? {};
138
146
  const nowFn = io.nowFn ?? Date.now;
139
147
  const timeoutMs = io.timeoutMs ?? START_OWNERSHIP_LIVENESS.timeoutMs ?? 1_500;
@@ -160,7 +168,7 @@ export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io
160
168
  };
161
169
 
162
170
  const defaultHome = join(options.homeDir ?? homedir(), ".opencodex");
163
- recordHome(defaultHome);
171
+ if (options.homeDir !== undefined || !isProtectedHomeUnderTest(defaultHome)) recordHome(defaultHome);
164
172
  // The shared registry is what lets one custom home find another: every runtime
165
173
  // that published a runtime record registered its home beside the shared clients.
166
174
  const registry = readOwnerRegistry();
@@ -204,6 +212,8 @@ export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io
204
212
  }
205
213
 
206
214
  const queue = [...candidates];
215
+ const availability = new Map<string, ReturnType<typeof loopbackFamilyAvailability>>();
216
+ const checkAvailability = options.loopbackAvailabilityFn ?? loopbackFamilyAvailability;
207
217
  const probed = new Set<number>();
208
218
  for (const port of queue) {
209
219
  if (probed.has(port)) continue;
@@ -224,6 +234,11 @@ export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io
224
234
  noteIndeterminate(port, "ownership discovery ran out of its shared time budget");
225
235
  break;
226
236
  }
237
+ // Disabled loopback IPv6 can route ::1 off-host and time out. An ephemeral
238
+ // local bind proves address absence without treating an available family's
239
+ // timeout as empty. Unknown bind failures retain the existing probes.
240
+ if (!availability.has(hostname)) availability.set(hostname, checkAvailability(hostname));
241
+ if (availability.get(hostname) === "unavailable") continue;
227
242
  const identity = await proxyIdentityAt(port, { hostname }, probeIo);
228
243
  if (identity === null) {
229
244
  // A null identity is not "free": distinguish a unanimous connect refusal
@@ -291,33 +306,35 @@ export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io
291
306
  }
292
307
 
293
308
  /** Mark this process before any shared-client write when another home owns the clients. */
294
- export async function markCrossHomeSibling(): Promise<boolean> {
295
- const verdict = await findCrossHomeOwnerDetailed();
309
+ export async function markCrossHomeSibling(
310
+ options: Parameters<typeof findCrossHomeOwnerDetailed>[0] = {},
311
+ ): Promise<boolean> {
312
+ const verdict = await findCrossHomeOwnerDetailed(options);
296
313
  if (verdict.kind === "none") return false;
297
- if (verdict.port === null) throw new Error("Shared-client owner could not be located; refusing startup before any shared-client write.");
298
314
  if (verdict.kind === "indeterminate") {
299
- // Fail closed: an owner the reader could not verify still owns the shared
300
- // writes. Marking the best port hint keeps every sibling gate engaged even
301
- // while the answer stays unproven (#6198).
302
- console.warn(
303
- "A shared-client owner could not be verified (" + verdict.reason + "); "
304
- + "treating this instance as a sibling so Codex, Grok and Claude configs are left alone.",
315
+ // A missed probe is not proof of a sibling topology. Refuse before publication
316
+ // so the service can retry discovery without freezing uncertainty for its lifetime.
317
+ throw new Error(
318
+ `Shared-client owner could not be verified (${verdict.reason}); `
319
+ + "refusing startup before any shared-client write. Retry startup after discovery recovers.",
305
320
  );
306
321
  }
307
- const port = verdict.port;
308
- markSiblingStart(port);
322
+ markSiblingStart(verdict.port);
309
323
  return true;
310
324
  }
311
325
 
312
326
  /** A live proxy in this home can publish its sibling owner even while that owner is down. */
313
- export async function markLiveHomeSibling(live: { pid: number | null; port: number }): Promise<boolean> {
327
+ export async function markLiveHomeSibling(
328
+ live: { pid: number | null; port: number },
329
+ options: Parameters<typeof findCrossHomeOwnerDetailed>[0] = {},
330
+ ): Promise<boolean> {
314
331
  const runtime = readRuntimePort();
315
332
  if (live.pid !== null && runtime?.pid === live.pid && runtime.port === live.port
316
333
  && runtime.siblingOfPort !== undefined && runtime.siblingOfPort !== live.port) {
317
334
  markSiblingStart(runtime.siblingOfPort);
318
335
  return true;
319
336
  }
320
- const verdict = await findCrossHomeOwnerDetailed();
337
+ const verdict = await findCrossHomeOwnerDetailed(options);
321
338
  if (verdict.kind === "none" || verdict.port === live.port) return false;
322
339
  if (verdict.port === null) throw new Error("Shared-client owner could not be located; refusing startup before any shared-client write.");
323
340
  markSiblingStart(verdict.port);
@@ -370,19 +370,32 @@ const commandRunners: Record<string, CommandRunner> = {
370
370
  },
371
371
  login: async deps => {
372
372
  const loginArgs = deps.args.slice(1);
373
+ // `--headless` is the only flag this command takes; the provider is the first bare word, so
374
+ // `ocx login --headless anthropic` and `ocx login anthropic --headless` mean the same thing.
375
+ // It is deliberately not `--code`: that name carries a secret value everywhere else in the
376
+ // CLI (`ocx account login <provider> --code <value>`) and its operand is scrubbed from stderr.
377
+ const codeMode = loginArgs.includes("--headless");
378
+ const positional = loginArgs.filter(arg => !arg.startsWith("--"));
373
379
  // 'ocx login codex' is the command people type first, and until now it answered with
374
380
  // the full provider wall because the Codex pool lives behind 'ocx account login'.
375
381
  // Route the three Codex spellings to that flow instead of making the user discover
376
382
  // a second noun. Everything else stays on the local OAuth/API-key path.
377
383
  const { isCodexAccountLoginName, handleAccountAuthCommand } = await import("./account-auth");
378
- if (isCodexAccountLoginName(loginArgs[0] ?? "")) {
384
+ if (isCodexAccountLoginName(positional[0] ?? "")) {
385
+ // `--headless` is the OAuth code-display mode, which Codex does not have; account-auth
386
+ // would reject the flag as an unexpected argument. Codex's headless path is the device-code
387
+ // login, so point there instead.
388
+ if (codeMode) {
389
+ console.error("Codex has no code-display login. For a headless host use: ocx account login codex --device");
390
+ return 1;
391
+ }
379
392
  // null means "unknown subcommand", which "login" never is; the coalesce exists because
380
393
  // the shared signature serves callers that do pass an unknown one.
381
394
  const code = await handleAccountAuthCommand("login", loginArgs, { findLiveProxy: deps.findLiveProxy });
382
395
  return code ?? 1;
383
396
  }
384
397
  const { handleLogin } = await import("../oauth/login-cli");
385
- await handleLogin(loginArgs[0]);
398
+ await handleLogin(positional[0], {}, { codeMode });
386
399
  return 0;
387
400
  },
388
401
  logout: async deps => {
@@ -461,11 +474,15 @@ const commandRunners: Record<string, CommandRunner> = {
461
474
  code = 1;
462
475
  console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
463
476
  }
464
- // Only warn/restart when a catalog or models_cache write actually happened. This is
477
+ // Catalog/model-cache writes retain their warning/restart handling. This is
465
478
  // deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
466
479
  // so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
467
480
  // exactly when a long-lived app-server is holding the stale list.
468
- if (synced.catalogWritten || synced.cacheSynced) {
481
+ // A configuration-only change also needs a reload, but only when explicitly
482
+ // requested. A successful no-op or refused write is never restart authority.
483
+ const requestedConfigRestart = (restartScope.appServers || restartScope.desktopApp)
484
+ && synced.ok && synced.configChanged === true;
485
+ if (synced.catalogWritten || synced.cacheSynced || requestedConfigRestart) {
469
486
  await handleRestartScopeAfterWrite(restartScope, console);
470
487
  }
471
488
  // `ocx sync` is a direct CLI path; it does not call the management
@@ -945,6 +962,10 @@ const commandRunners: Record<string, CommandRunner> = {
945
962
  const { handleLabCommand } = await import("./lab");
946
963
  return await handleLabCommand(deps.args.slice(1));
947
964
  },
965
+ "codex-provider": async deps => {
966
+ const { handleCodexProviderCommand } = await import("./codex-provider");
967
+ return await handleCodexProviderCommand(deps.args.slice(1));
968
+ },
948
969
  chatgpt: async deps => {
949
970
  const { handleChatgptCommand } = await import("./chatgpt-command");
950
971
  return await handleChatgptCommand(deps.args.slice(1));
package/src/cli/doctor.ts CHANGED
@@ -1160,6 +1160,11 @@ async function fetchExposedModelIds(live: LiveProxy, fetchFn: ExposedModelsFetch
1160
1160
  if (id.length === 0) continue;
1161
1161
  if (id.length > EXPOSED_MODEL_ID_MAX_LENGTH) return null;
1162
1162
  ids.add(id);
1163
+ const nativeId = (row as { native_id?: unknown }).native_id;
1164
+ if (nativeId !== undefined) {
1165
+ if (typeof nativeId !== "string" || nativeId.length > EXPOSED_MODEL_ID_MAX_LENGTH) return null;
1166
+ if (nativeId.length > 0) ids.add(nativeId);
1167
+ }
1163
1168
  }
1164
1169
  return ids;
1165
1170
  } catch {
@@ -1467,7 +1472,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1467
1472
 
1468
1473
  // Observe-only and warning-level: a bypassed Desktop first-party install still serves the
1469
1474
  // CLI and every non-Desktop client, so this never records a doctor failure.
1470
- const desktopSystemProxyLines = formatDesktopSystemProxyLines(collectDesktopSystemProxy(doctorConfig));
1475
+ const desktopSystemProxyLines = formatDesktopSystemProxyLines(await collectDesktopSystemProxy(doctorConfig));
1471
1476
  if (desktopSystemProxyLines.length > 0) {
1472
1477
  console.log("\nClaude Desktop first-party vs Windows system proxy");
1473
1478
  for (const line of desktopSystemProxyLines) console.log(line);