@bitkyc08/opencodex 2.86.0 → 2.87.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 (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.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.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. 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,
@@ -160,7 +161,7 @@ export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io
160
161
  };
161
162
 
162
163
  const defaultHome = join(options.homeDir ?? homedir(), ".opencodex");
163
- recordHome(defaultHome);
164
+ if (options.homeDir !== undefined || !isProtectedHomeUnderTest(defaultHome)) recordHome(defaultHome);
164
165
  // The shared registry is what lets one custom home find another: every runtime
165
166
  // that published a runtime record registered its home beside the shared clients.
166
167
  const registry = readOwnerRegistry();
@@ -291,33 +292,35 @@ export async function findCrossHomeOwnerDetailed(options: { homeDir?: string; io
291
292
  }
292
293
 
293
294
  /** 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();
295
+ export async function markCrossHomeSibling(
296
+ options: Parameters<typeof findCrossHomeOwnerDetailed>[0] = {},
297
+ ): Promise<boolean> {
298
+ const verdict = await findCrossHomeOwnerDetailed(options);
296
299
  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
300
  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.",
301
+ // A missed probe is not proof of a sibling topology. Refuse before publication
302
+ // so the service can retry discovery without freezing uncertainty for its lifetime.
303
+ throw new Error(
304
+ `Shared-client owner could not be verified (${verdict.reason}); `
305
+ + "refusing startup before any shared-client write. Retry startup after discovery recovers.",
305
306
  );
306
307
  }
307
- const port = verdict.port;
308
- markSiblingStart(port);
308
+ markSiblingStart(verdict.port);
309
309
  return true;
310
310
  }
311
311
 
312
312
  /** 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> {
313
+ export async function markLiveHomeSibling(
314
+ live: { pid: number | null; port: number },
315
+ options: Parameters<typeof findCrossHomeOwnerDetailed>[0] = {},
316
+ ): Promise<boolean> {
314
317
  const runtime = readRuntimePort();
315
318
  if (live.pid !== null && runtime?.pid === live.pid && runtime.port === live.port
316
319
  && runtime.siblingOfPort !== undefined && runtime.siblingOfPort !== live.port) {
317
320
  markSiblingStart(runtime.siblingOfPort);
318
321
  return true;
319
322
  }
320
- const verdict = await findCrossHomeOwnerDetailed();
323
+ const verdict = await findCrossHomeOwnerDetailed(options);
321
324
  if (verdict.kind === "none" || verdict.port === live.port) return false;
322
325
  if (verdict.port === null) throw new Error("Shared-client owner could not be located; refusing startup before any shared-client write.");
323
326
  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);
@@ -10,8 +10,9 @@
10
10
  *
11
11
  * Rules, in order:
12
12
  * - A running Desktop app is the supervisor. Wait for its proxy; never start a competing one.
13
- * - A durable Desktop claim with no running app: start Desktop (hidden, as a login launch would)
14
- * where that is possible, then wait. Elsewhere, fail with guidance. A missing Desktop process
13
+ * - A durable Desktop claim with no running app: start Desktop (hidden, by the path its own
14
+ * login registration recorded — LaunchServices never picks the bundle) where that is
15
+ * verifiable, then wait. Elsewhere, fail with guidance. A missing Desktop process
15
16
  * never authorizes a CLI proxy by itself; handing ownership to the CLI stays an explicit
16
17
  * `ocx service install` consent step.
17
18
  * - Ownership or the process list that cannot be read is not evidence of absence: refuse.
@@ -22,9 +23,9 @@
22
23
  */
23
24
  import { execFileSync } from "node:child_process";
24
25
  import { randomUUID } from "node:crypto";
25
- import { existsSync, linkSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
26
+ import { accessSync, constants, existsSync, linkSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
26
27
  import { homedir } from "node:os";
27
- import { basename, dirname, join, win32 } from "node:path";
28
+ import { basename, dirname, join, posix, win32 } from "node:path";
28
29
  import { getConfigDir } from "../config/paths";
29
30
  import { resolveServiceOwnership } from "../service/state";
30
31
 
@@ -220,11 +221,63 @@ export function inspectDesktopProcess(options: DesktopProcessOptions = {}): Desk
220
221
  }
221
222
  }
222
223
 
223
- /** Start the macOS app hidden, the same way its login item does. */
224
- export function launchDesktopApp(options: { platform?: NodeJS.Platform; run?: Execute } = {}): boolean {
224
+ const DESKTOP_BUNDLE_ID = "com.opencodex.desktop";
225
+ const DESKTOP_PLIST_LABEL = "OpenCodex";
226
+
227
+ /**
228
+ * The app bundle the consented installation itself registered as its Start-at-Login program —
229
+ * not whichever bundle LaunchServices would resolve for our identifier. `open -b` answers a
230
+ * mutable per-user registry: any planted bundle claiming {@link DESKTOP_BUNDLE_ID} could be
231
+ * selected and executed in the real app's place. The recorded claim, this app's own install-id
232
+ * store, and the login registration it wrote are the durable evidence of which installation
233
+ * that is; a claim whose registration is absent, malformed, stale or points off a Desktop
234
+ * executable refuses the launch instead of guessing.
235
+ */
236
+ function consentedDesktopBundle(home: string, installId: string, run: Execute): string | null {
237
+ const localId = readFileSync(
238
+ join(home, "Library", "Application Support", DESKTOP_BUNDLE_ID, "install-id"), "utf8",
239
+ ).trim();
240
+ if (!localId || localId !== installId) return null;
241
+ const plistPath = realpathSync(join(home, "Library", "LaunchAgents", `${DESKTOP_PLIST_LABEL}.plist`));
242
+ const registration = JSON.parse(
243
+ run("/usr/bin/plutil", ["-convert", "json", "-o", "-", plistPath], 3_000),
244
+ ) as { Label?: unknown; ProgramArguments?: unknown; Program?: unknown };
245
+ const args = registration.ProgramArguments;
246
+ // The shapes auto-launch writes: [program] or [program, "--autostart"]; see the migration
247
+ // rules in desktop/src-tauri/src/login_registration.rs, which classify anything else Leave.
248
+ // This is a macOS plist path, regardless of the host running an injected launch probe.
249
+ if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args)
250
+ || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" || !posix.isAbsolute(args[0])
251
+ || (args.length === 2 && args[1] !== "--autostart")
252
+ || (registration.Program !== undefined && registration.Program !== args[0])) return null;
253
+ const executable = realpathSync(args[0]);
254
+ if (!isDesktopExecutable(executable, "darwin") || !statSync(executable).isFile()) return null;
255
+ accessSync(executable, constants.X_OK);
256
+ const bundle = dirname(dirname(dirname(executable)));
257
+ if (!bundle.endsWith(".app")) return null;
258
+ const info = JSON.parse(
259
+ run("/usr/bin/plutil", ["-convert", "json", "-o", "-", join(bundle, "Contents", "Info.plist")], 3_000),
260
+ ) as { CFBundleIdentifier?: unknown; CFBundleExecutable?: unknown };
261
+ // `open` uses this selector, so checking only the registration and identifier would
262
+ // validate one binary while launching another. The registered path is already canonical.
263
+ if (info.CFBundleIdentifier !== DESKTOP_BUNDLE_ID || info.CFBundleExecutable !== "opencodex-desktop") return null;
264
+ return realpathSync(join(bundle, "Contents", "MacOS", info.CFBundleExecutable)) === executable ? bundle : null;
265
+ }
266
+
267
+ /** Start the consented macOS app hidden, by the path its own login registration recorded. */
268
+ export function launchDesktopApp(options: {
269
+ platform?: NodeJS.Platform;
270
+ run?: Execute;
271
+ home?: string;
272
+ ownership?: () => ReturnType<typeof resolveServiceOwnership>;
273
+ } = {}): boolean {
225
274
  if ((options.platform ?? process.platform) !== "darwin") return false;
226
275
  try {
227
- (options.run ?? execute)("/usr/bin/open", ["-g", "-b", "com.opencodex.desktop", "--args", "--autostart"], 10_000);
276
+ const owner = (options.ownership ?? resolveServiceOwnership)();
277
+ if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return false;
278
+ const bundle = consentedDesktopBundle(options.home ?? homedir(), owner.ownership.installId, options.run ?? execute);
279
+ if (bundle === null) return false;
280
+ (options.run ?? execute)("/usr/bin/open", ["-g", bundle, "--args", "--autostart"], 10_000);
228
281
  return true;
229
282
  } catch { return false; }
230
283
  }