@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Cold start for `ocx ensure` when OpenCodex Desktop owns or runs the proxy (#6971).
3
+ *
4
+ * The Codex launcher shim runs `ocx ensure` before every Codex launch. Without this policy,
5
+ * ensure spawned a detached CLI proxy whenever nothing answered on the port. That is wrong in two
6
+ * Desktop situations: during the Desktop supervisor's restart backoff, and after Desktop quit
7
+ * while its durable ownership claim remains. In both, the CLI proxy took the port and Desktop came
8
+ * back as a guest without supervision. That is why the dashboard used to disable the shim under
9
+ * Desktop.
10
+ *
11
+ * Rules, in order:
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
15
+ * never authorizes a CLI proxy by itself; handing ownership to the CLI stays an explicit
16
+ * `ocx service install` consent step.
17
+ * - Ownership or the process list that cannot be read is not evidence of absence: refuse.
18
+ * - No claim and no running app: the ordinary CLI cold start.
19
+ *
20
+ * Concurrent shim launches share one Desktop launch through a lease file held by a live process;
21
+ * every caller then waits on the same readiness probe.
22
+ */
23
+ import { execFileSync } from "node:child_process";
24
+ import { randomUUID } from "node:crypto";
25
+ import { existsSync, linkSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { basename, dirname, join, win32 } from "node:path";
28
+ import { getConfigDir } from "../config/paths";
29
+ import { resolveServiceOwnership } from "../service/state";
30
+
31
+ export type DesktopProcessState = "running" | "absent" | "unknown";
32
+ export type DesktopOwnershipState = "desktop" | "other" | "unknown";
33
+
34
+ export type EnsureColdStartPlan =
35
+ | { kind: "spawn-cli" }
36
+ | { kind: "wait-desktop" }
37
+ | { kind: "launch-desktop" }
38
+ | { kind: "refuse"; reason: "desktop-unlaunchable" | "unverified" };
39
+
40
+ export function planEnsureColdStart(facts: {
41
+ ownership: DesktopOwnershipState;
42
+ desktopProcess: DesktopProcessState;
43
+ platform: NodeJS.Platform;
44
+ }): EnsureColdStartPlan {
45
+ if (facts.desktopProcess === "running") return { kind: "wait-desktop" };
46
+ if (facts.ownership === "unknown" || facts.desktopProcess === "unknown") return { kind: "refuse", reason: "unverified" };
47
+ if (facts.ownership === "other") return { kind: "spawn-cli" };
48
+ return facts.platform === "darwin" ? { kind: "launch-desktop" } : { kind: "refuse", reason: "desktop-unlaunchable" };
49
+ }
50
+
51
+ /** Desktop restart backoff tops out at 30 s; the budget covers it plus a cold sidecar start. */
52
+ export const DESKTOP_COLD_START_TIMEOUT_MS = 40_000;
53
+
54
+ export type DesktopColdStartResult<T> =
55
+ | { kind: "cli" }
56
+ | { kind: "live"; live: T }
57
+ | { kind: "failed"; message: string };
58
+
59
+ export interface DesktopColdStartDeps<T> {
60
+ platform: NodeJS.Platform;
61
+ ownership: () => DesktopOwnershipState;
62
+ desktopProcess: () => DesktopProcessState;
63
+ /** Start the Desktop app hidden. Returns false when the request could not be made. */
64
+ launchDesktop: () => boolean;
65
+ /** Returns a release function when this caller should launch, or null when another caller already did. */
66
+ acquireLaunchLease: () => (() => void) | null;
67
+ waitForProxy: (timeoutMs: number) => Promise<T | null>;
68
+ info?: (line: string) => void;
69
+ }
70
+
71
+ const QUIT_HINT = "To run the proxy from this CLI instead, quit OpenCodex and run `ocx service install` or `ocx start`.";
72
+
73
+ export async function runDesktopColdStart<T>(deps: DesktopColdStartDeps<T>): Promise<DesktopColdStartResult<T>> {
74
+ const plan = planEnsureColdStart({
75
+ ownership: deps.ownership(),
76
+ desktopProcess: deps.desktopProcess(),
77
+ platform: deps.platform,
78
+ });
79
+ if (plan.kind === "spawn-cli") return { kind: "cli" };
80
+ if (plan.kind === "refuse") {
81
+ return { kind: "failed", message: plan.reason === "unverified"
82
+ ? "Could not verify whether OpenCodex Desktop manages this proxy, so no CLI proxy was started. Run `ocx doctor`, or start the proxy explicitly with `ocx start`."
83
+ : `OpenCodex Desktop manages this proxy but is not running. Open OpenCodex to start it. ${QUIT_HINT}` };
84
+ }
85
+ if (plan.kind === "launch-desktop") {
86
+ const release = deps.acquireLaunchLease();
87
+ try {
88
+ if (release) {
89
+ deps.info?.("Starting OpenCodex Desktop, which manages this proxy...");
90
+ if (!deps.launchDesktop()) {
91
+ return { kind: "failed", message: `OpenCodex Desktop manages this proxy, but it could not be started. Open OpenCodex manually. ${QUIT_HINT}` };
92
+ }
93
+ }
94
+ const live = await deps.waitForProxy(DESKTOP_COLD_START_TIMEOUT_MS);
95
+ if (live) return { kind: "live", live };
96
+ return { kind: "failed", message: `OpenCodex Desktop was started, but its proxy did not answer in time. Check the OpenCodex menu. ${QUIT_HINT}` };
97
+ } finally { release?.(); }
98
+ }
99
+ deps.info?.("Waiting for OpenCodex Desktop to restart its proxy...");
100
+ const live = await deps.waitForProxy(DESKTOP_COLD_START_TIMEOUT_MS);
101
+ if (live) return { kind: "live", live };
102
+ return {
103
+ kind: "failed",
104
+ message: `OpenCodex Desktop is running but its proxy is not answering. Use Start Proxy in the OpenCodex menu. ${QUIT_HINT}`,
105
+ };
106
+ }
107
+
108
+ type Execute = (command: string, args: string[], timeoutMs: number) => string;
109
+
110
+ function execute(command: string, args: string[], timeoutMs: number): string {
111
+ return execFileSync(command, args, {
112
+ encoding: "utf8", timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"], windowsHide: true,
113
+ });
114
+ }
115
+
116
+ /** A Desktop app executable: `opencodex-desktop`, inside an app bundle's Contents/MacOS on macOS. */
117
+ export function isDesktopExecutable(path: string, platform: NodeJS.Platform): boolean {
118
+ if (basename(path) !== "opencodex-desktop") return false;
119
+ return platform !== "darwin" || (basename(dirname(path)) === "MacOS" && basename(dirname(dirname(path))) === "Contents");
120
+ }
121
+
122
+ export interface DesktopProcessOptions {
123
+ platform?: NodeJS.Platform;
124
+ uid?: number;
125
+ env?: NodeJS.ProcessEnv;
126
+ run?: Execute;
127
+ /** Linux: executable paths of this user's processes, or \`unverified\`. */
128
+ linuxExecutables?: (uid: number) => string[] | "unverified";
129
+ /** Linux: whether procfs hides processes (hidepid=2/4). */
130
+ procfsHidesProcesses?: () => boolean;
131
+ /** Linux: whether this machine shows evidence of an OpenCodex Desktop install. */
132
+ desktopInstalled?: () => boolean;
133
+ }
134
+
135
+ /**
136
+ * Linux: hidepid=2 (invisible) or 4 (ptraceable) can hide a same-uid process whose credentials
137
+ * differ from the caller's (for example after newgrp), so an empty scan proves nothing there.
138
+ * An unreadable mount table is treated as filtering.
139
+ */
140
+ export function procfsHidesProcesses(mountinfo = "/proc/self/mountinfo"): boolean {
141
+ let text: string;
142
+ try { text = readFileSync(mountinfo, "utf8"); } catch { return true; }
143
+ return text.split("\n").some(line => {
144
+ const [mount, fs] = line.split(" - ");
145
+ if (mount?.split(" ")[4] !== "/proc" || !fs) return false;
146
+ const [type, , options = ""] = fs.split(" ");
147
+ return type === "proc" && options.split(",").some(option => /^hidepid=(2|4|invisible|ptraceable)$/.test(option));
148
+ });
149
+ }
150
+
151
+ function linuxDesktopInstalled(): boolean {
152
+ return existsSync(join(homedir(), ".opencodex-desktop")) || existsSync("/usr/bin/opencodex-desktop");
153
+ }
154
+
155
+ /** Linux truncates comm to 15 bytes. */
156
+ const LINUX_DESKTOP_COMM = "opencodex-desktop".slice(0, 15);
157
+
158
+ /**
159
+ * Linux: executables of this user's processes. A process that exited mid-scan or belongs to someone
160
+ * else is skipped. A non-dumpable process (ssh-agent, gpg-agent) hides its executable but not its
161
+ * comm, so it is ruled out by name; one whose comm could be Desktop makes the answer unverifiable.
162
+ * The scan must also see this process, or procfs is filtered and absence proves nothing.
163
+ */
164
+ export function scanLinuxExecutables(uid: number, selfPid: number = process.pid, proc = "/proc"): string[] | "unverified" {
165
+ const paths: string[] = [];
166
+ let sawSelf = false;
167
+ const code = (error: unknown) => (error as { code?: unknown } | null)?.code;
168
+ for (const entry of readdirSync(proc)) {
169
+ if (!/^\d+$/.test(entry)) continue;
170
+ if (Number(entry) === selfPid) sawSelf = true;
171
+ try {
172
+ if (statSync(`${proc}/${entry}`).uid !== uid) continue;
173
+ paths.push(readlinkSync(`${proc}/${entry}/exe`).replace(/ \(deleted\)$/, ""));
174
+ } catch (error) {
175
+ if (code(error) === "ENOENT" || code(error) === "ESRCH") continue;
176
+ try {
177
+ if (readFileSync(`${proc}/${entry}/comm`, "utf8").trim() !== LINUX_DESKTOP_COMM) continue;
178
+ } catch (commError) {
179
+ if (code(commError) === "ENOENT" || code(commError) === "ESRCH") continue;
180
+ }
181
+ return "unverified";
182
+ }
183
+ }
184
+ return sawSelf ? paths : "unverified";
185
+ }
186
+
187
+ /** Whether this user runs an OpenCodex Desktop app, judged by executable identity, never arguments. */
188
+ export function inspectDesktopProcess(options: DesktopProcessOptions = {}): DesktopProcessState {
189
+ const platform = options.platform ?? process.platform;
190
+ const run = options.run ?? execute;
191
+ try {
192
+ const uid = options.uid ?? process.getuid?.();
193
+ if (platform === "linux") {
194
+ if (uid === undefined) return "unknown";
195
+ const executables = (options.linuxExecutables ?? scanLinuxExecutables)(uid);
196
+ if (executables === "unverified") return "unknown";
197
+ if (executables.some(path => isDesktopExecutable(path, platform))) return "running";
198
+ // A filtered procfs cannot prove absence where Desktop may be installed. Without an install
199
+ // there is nothing to hide, so hardened hosts keep the plain CLI cold start.
200
+ return (options.procfsHidesProcesses ?? procfsHidesProcesses)() && (options.desktopInstalled ?? linuxDesktopInstalled)()
201
+ ? "unknown" : "absent";
202
+ }
203
+ if (platform === "darwin") {
204
+ if (uid === undefined) return "unknown";
205
+ // macOS reports the full executable path as comm.
206
+ const output = run("/bin/ps", ["-U", String(uid), "-o", "comm="], 3_000);
207
+ return output.split("\n").some(line => isDesktopExecutable(line.trim(), platform)) ? "running" : "absent";
208
+ }
209
+ if (platform === "win32") {
210
+ const env = options.env ?? process.env;
211
+ const root = env.SystemRoot ?? env.windir ?? "C:\\Windows";
212
+ const user = env.USERNAME?.trim();
213
+ const args = ["/FI", "IMAGENAME eq opencodex-desktop.exe", ...(user ? ["/FI", `USERNAME eq ${user}`] : []), "/FO", "CSV", "/NH"];
214
+ const output = run(win32.join(root, "System32", "tasklist.exe"), args, 3_000);
215
+ return /^"opencodex-desktop\.exe",/im.test(output) ? "running" : "absent";
216
+ }
217
+ return "absent";
218
+ } catch {
219
+ return "unknown";
220
+ }
221
+ }
222
+
223
+ /** Start the macOS app hidden, the same way its login item does. */
224
+ export function launchDesktopApp(options: { platform?: NodeJS.Platform; run?: Execute } = {}): boolean {
225
+ if ((options.platform ?? process.platform) !== "darwin") return false;
226
+ try {
227
+ (options.run ?? execute)("/usr/bin/open", ["-g", "-b", "com.opencodex.desktop", "--args", "--autostart"], 10_000);
228
+ return true;
229
+ } catch { return false; }
230
+ }
231
+
232
+ function processAlive(pid: number): boolean {
233
+ try { process.kill(pid, 0); return true; }
234
+ catch (error) { return (error as NodeJS.ErrnoException | null)?.code === "EPERM"; }
235
+ }
236
+
237
+ function readHolder(path: string): number | null {
238
+ const pid = Number(readFileSync(path, "utf8").trim());
239
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
240
+ }
241
+
242
+ const LEASE_FILE = /^desktop-launch\.(\d+)\.lease$/;
243
+
244
+ /**
245
+ * One Desktop launch per cold start. Leases are numbered generations, each published whole with
246
+ * link(2), which fails if the name exists, so exactly one contender can create generation N+1. A
247
+ * caller launches only after creating the next generation while the newest one belongs to a dead
248
+ * process (or none exists). A lease stays valid while its holder lives, so a slow launch never
249
+ * expires under its owner. Nothing ever moves or overwrites another holder's lease.
250
+ */
251
+ export function acquireDesktopLaunchLease(
252
+ configDir: string,
253
+ io: { pid?: number; alive?: (pid: number) => boolean } = {},
254
+ ): (() => void) | null {
255
+ const pid = io.pid ?? process.pid;
256
+ const alive = io.alive ?? processAlive;
257
+ const leasePath = (generation: number) => join(configDir, `desktop-launch.${generation}.lease`);
258
+ for (let attempt = 0; attempt < 4; attempt++) {
259
+ let generations: number[];
260
+ try {
261
+ generations = readdirSync(configDir).flatMap(name => {
262
+ const match = LEASE_FILE.exec(name);
263
+ return match ? [Number(match[1])] : [];
264
+ }).sort((a, b) => b - a);
265
+ } catch {
266
+ // An unreadable lease directory must not suppress the launch; it only loses deduplication.
267
+ return () => {};
268
+ }
269
+ const newest = generations[0];
270
+ if (newest !== undefined) {
271
+ try {
272
+ const holder = readHolder(leasePath(newest));
273
+ if (holder !== null && alive(holder)) return null;
274
+ } catch (error) {
275
+ if ((error as { code?: unknown })?.code === "ENOENT") continue;
276
+ return null;
277
+ }
278
+ }
279
+ const next = (newest ?? 0) + 1;
280
+ const staged = join(configDir, `desktop-launch.${randomUUID()}.tmp`);
281
+ try {
282
+ writeFileSync(staged, String(pid), { flag: "wx", mode: 0o600 });
283
+ linkSync(staged, leasePath(next));
284
+ } catch (error) {
285
+ if ((error as { code?: unknown })?.code === "EEXIST") continue;
286
+ return () => {};
287
+ } finally {
288
+ try { unlinkSync(staged); } catch { /* not created */ }
289
+ }
290
+ const withdraw = () => {
291
+ try { if (readHolder(leasePath(next)) === pid) unlinkSync(leasePath(next)); } catch { /* already gone */ }
292
+ };
293
+ // A contender with a stale directory read can publish a generation a winner already cleaned
294
+ // up. The highest generation is the one launcher; anyone below it stands down.
295
+ try {
296
+ if (readdirSync(configDir).some(name => Number(LEASE_FILE.exec(name)?.[1] ?? 0) > next)) {
297
+ withdraw();
298
+ return null;
299
+ }
300
+ } catch { /* keep the lease: the directory was readable a moment ago */ }
301
+ // Older generations belong to dead holders: the newest was checked before publishing above it.
302
+ for (const generation of generations) {
303
+ try { unlinkSync(leasePath(generation)); } catch { /* already gone */ }
304
+ }
305
+ // The newest generation is never deleted on release: removing it would let numbering restart
306
+ // below a stale contender. It stops counting once this process exits.
307
+ return () => {};
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /** Production wiring; `waitForProxy` is ensure's identity-checked readiness probe. */
313
+ export function productionDesktopColdStartDeps<T>(waitForProxy: (timeoutMs: number) => Promise<T | null>): DesktopColdStartDeps<T> {
314
+ return {
315
+ platform: process.platform,
316
+ ownership: () => {
317
+ try {
318
+ const owner = resolveServiceOwnership();
319
+ if (owner.kind === "unknown") return "unknown";
320
+ return owner.kind === "owned" && owner.ownership.owner === "desktop" ? "desktop" : "other";
321
+ } catch { return "unknown"; }
322
+ },
323
+ desktopProcess: () => inspectDesktopProcess(),
324
+ launchDesktop: () => launchDesktopApp(),
325
+ acquireLaunchLease: () => acquireDesktopLaunchLease(getConfigDir()),
326
+ waitForProxy,
327
+ info: line => console.log(line),
328
+ };
329
+ }
@@ -29,6 +29,7 @@
29
29
  import { writeFileSync } from "node:fs";
30
30
  import { loadConfig } from "../config";
31
31
  import {
32
+ ClientPathError,
32
33
  EXPORT_CLIENTS,
33
34
  EXPORT_CLIENT_IDS,
34
35
  buildClientConfigText,
@@ -168,7 +169,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
168
169
  const spec = EXPORT_CLIENTS[client];
169
170
  const root = await runtimeBaseUrl(deps);
170
171
  let built: { document: unknown; text: string };
171
- if (client === "raycast" || client === "droid") {
172
+ if (client === "raycast" || client === "droid" || client === "qoder") {
172
173
  // The dial address alone cannot distinguish a wildcard authenticated bind
173
174
  // from loopback. Let the live server resolve its admission/listener policy;
174
175
  // saved config can differ from the process serving this request.
@@ -193,6 +194,18 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
193
194
  const clientConfig = built.document;
194
195
  const text = built.text;
195
196
 
197
+ // Resolve the human-facing destination BEFORE writing anything. A client whose
198
+ // config location cannot be determined (a relative override, or ZCode bootstrap
199
+ // settings naming an unknown data root) still exports the document: the export
200
+ // writes nothing into the client and the destination is advisory text.
201
+ let destination: string;
202
+ try {
203
+ destination = spec.destination(process.env);
204
+ } catch (error) {
205
+ if (!(error instanceof ClientPathError)) throw error;
206
+ destination = `unknown (${error.message})`;
207
+ }
208
+
196
209
  // Every serializer already ends with exactly one newline.
197
210
  if (out !== undefined) writeExport(out, text, force);
198
211
  // stderr, so `--json` stdout stays a standalone JSON document.
@@ -208,7 +221,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
208
221
  ...text.trimEnd().split("\n"),
209
222
  "",
210
223
  ...(out !== undefined ? [`Wrote ${out}`] : []),
211
- `Destination: ${spec.destination(process.env)}`,
224
+ `Destination: ${destination}`,
212
225
  "Merge this generated configuration into that file; do not replace it.",
213
226
  `Before launching: ${spec.exportHint}`,
214
227
  `${modelCount} model${modelCount === 1 ? "" : "s"}; ${modelsWithoutLimits} omit context limits (the client applies its own defaults).`,
package/src/cli/help.ts CHANGED
@@ -92,11 +92,12 @@ Usage:
92
92
  ocx api-key <sub> Alias of ocx access key
93
93
  ocx access <sub> External API keys and endpoint information
94
94
  ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
95
- ocx export --client <id> Print a client config wired to the running proxy (18 clients)
95
+ ocx export --client <id> Print a client config wired to the running proxy (19 clients)
96
96
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
97
97
  ocx grok <sub> Grok Build model selection and apply
98
98
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
99
99
  ocx config [sub] Validated configuration show/get/set/import/export
100
+ ocx advisor <status|on|off|set|consent> Advisor sidecar settings and context-sharing consent
100
101
  ocx companion <show|set|reset> Menu-bar and widget companion usage settings
101
102
  ocx lab <sub> Inspect Lab evidence and control local automation
102
103
  ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
@@ -0,0 +1,99 @@
1
+ import { mutatePersistedConfig, validateConfigCandidate } from "../config";
2
+ import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
3
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
4
+ import { probeServiceInstallation } from "../service/cli";
5
+ import { redactSecretString } from "../lib/redact";
6
+ import { CliUsageError, printData, runCliAction } from "./runtime-api";
7
+ import type { OcxConfig } from "../types";
8
+
9
+ export const HUB_DISABLE_USAGE = "ocx hub disable [--revoke-clients --client-key <id> ...] [--json]";
10
+ const HUB_KEYS = ["hub", "unauthenticatedLoopbackListener", "runtimeRole", "hostname"] as const;
11
+ const PUBLISHING_NOTE = "External publishing is unchanged. Check tailscale serve status and any other frontend for mappings to the old hub ports.";
12
+
13
+ export interface HubDisableOptions { json: boolean; revokeClients: boolean; clientKeys: string[] }
14
+ export interface HubDisableDeps {
15
+ mutateConfig?: typeof mutatePersistedConfig;
16
+ restartHint?: () => string;
17
+ }
18
+ interface HubDisableResult {
19
+ removed: string[];
20
+ keys: { id: string; name: string; createdAt: string; revoked: boolean }[];
21
+ revoked: string[];
22
+ }
23
+
24
+ export function parseHubDisableArgs(args: string[]): HubDisableOptions {
25
+ const options: HubDisableOptions = { json: false, revokeClients: false, clientKeys: [] };
26
+ for (let index = 0; index < args.length; index++) {
27
+ const arg = args[index];
28
+ if (arg === "--json" && !options.json) options.json = true;
29
+ else if (arg === "--revoke-clients" && !options.revokeClients) options.revokeClients = true;
30
+ else if (arg === "--client-key") {
31
+ const id = args[++index];
32
+ if (!id || id.startsWith("-") || options.clientKeys.includes(id)) throw new CliUsageError("Each --client-key requires a unique exact key ID.", HUB_DISABLE_USAGE);
33
+ options.clientKeys.push(id);
34
+ } else throw new CliUsageError("Unknown or repeated hub disable argument.", HUB_DISABLE_USAGE);
35
+ }
36
+ if (options.clientKeys.length && !options.revokeClients) throw new CliUsageError("--client-key requires --revoke-clients.", HUB_DISABLE_USAGE);
37
+ return options;
38
+ }
39
+
40
+ /** Selection is explicit: legacy keys have editable names but no trustworthy issuance marker. */
41
+ export function disableHubConfig(config: OcxConfig, options: HubDisableOptions): HubDisableResult {
42
+ if (config.runtimeRole !== "hub") throw new CliUsageError("This machine is not a hub; no settings or keys were changed.");
43
+ const keys = config.apiKeys ?? [];
44
+ const secrets = keys.flatMap(key => [key.key, key.pendingRotation?.key]).filter((secret): secret is string => Boolean(secret));
45
+ const safeMetadata = (value: string) => redactSecretString(secrets.reduce((text, secret) => text.split(secret).join("[REDACTED]"), value));
46
+ if (options.revokeClients && keys.length && !options.clientKeys.length) {
47
+ throw new CliUsageError("Keys have no verified client origin. List IDs with 'ocx access key list --json', then repeat --client-key <id> for each client key to revoke.", HUB_DISABLE_USAGE);
48
+ }
49
+ const selected = new Set(options.clientKeys);
50
+ if (options.clientKeys.some(id => !keys.some(key => key.id === id))) {
51
+ throw new CliUsageError("A selected client key ID is not in the current config; no settings or keys were changed.");
52
+ }
53
+ const candidate = structuredClone(config);
54
+ const removed = HUB_KEYS.filter(key => Object.hasOwn(candidate, key));
55
+ for (const key of HUB_KEYS) delete candidate[key];
56
+ if (options.revokeClients) candidate.apiKeys = keys.filter(key => !selected.has(key.id));
57
+ const validated = validateConfigCandidate(candidate);
58
+ if (!validated.ok) throw new CliUsageError(validated.error);
59
+ // Keep deletion intent so a later live save cannot resurrect the hub's old bindings.
60
+ for (const key of HUB_KEYS) deleteConfigTopLevelKey(config, key);
61
+ Object.assign(config, validated.config);
62
+ return {
63
+ removed,
64
+ keys: keys.map(key => ({ id: safeMetadata(key.id), name: safeMetadata(key.name), createdAt: safeMetadata(key.createdAt), revoked: selected.has(key.id) })),
65
+ revoked: options.revokeClients ? [...selected].map(safeMetadata) : [],
66
+ };
67
+ }
68
+
69
+ export function hubDisableRestartHint(): string {
70
+ try {
71
+ if (inspectDesktopSupervision().kind === "desktop") return "Restart to apply: ocx restart (OpenCodex Desktop supervises the replacement).";
72
+ const service = probeServiceInstallation();
73
+ if (service.state === "installed") return "Restart to apply: ocx service restart.";
74
+ } catch { /* An unavailable probe must not prevent an already validated config change. */ }
75
+ return "Restart to apply: use your current proxy supervisor; for a manually started proxy, ocx restart.";
76
+ }
77
+
78
+ export async function runHubDisable(args: string[], deps: HubDisableDeps = {}): Promise<number> {
79
+ return runCliAction(async () => {
80
+ const options = parseHubDisableArgs(args);
81
+ const outcome = (deps.mutateConfig ?? mutatePersistedConfig)(config => ({
82
+ changed: true, value: disableHubConfig(config, options),
83
+ }));
84
+ if (outcome.status === "unavailable") throw new Error(`Hub disable could not read or commit the config (${outcome.reason}); retry after fixing the config.`);
85
+ let restart: string;
86
+ try { restart = (deps.restartHint ?? hubDisableRestartHint)(); }
87
+ catch { restart = "Restart to apply: use your current proxy supervisor."; }
88
+ const result = { ok: true, ...outcome.value, restart, note: PUBLISHING_NOTE, keyOrigin: "unverified", effectiveAfterRestart: true };
89
+ // Explicit field projection excludes secrets and pending rotations; names are arbitrary input.
90
+ const safe = { ...result, keys: result.keys.map(key => ({ ...key, name: redactSecretString(key.name) })) };
91
+ printData(safe, options.json, [
92
+ `Removed hub settings: ${safe.removed.join(", ")}.`,
93
+ ...safe.keys.map(key => `${key.revoked ? "Revoked" : "Retained"} saved key: ${key.id} (${key.name})`),
94
+ "Saved keys have no verified client origin; revocation uses only explicitly selected IDs.",
95
+ restart, PUBLISHING_NOTE,
96
+ "Restart is required to apply all saved settings; this command does not terminate existing sessions.",
97
+ ]);
98
+ });
99
+ }
package/src/cli/hub.ts CHANGED
@@ -7,9 +7,10 @@ import {
7
7
  type GuiPairRequestResult,
8
8
  } from "./gui-pair-client";
9
9
  import type { RuntimeApiDeps } from "./runtime-api";
10
+ import { HUB_DISABLE_USAGE, runHubDisable, type HubDisableDeps } from "./hub-disable";
10
11
 
11
12
  export const HUB_USAGE =
12
- "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]";
13
+ "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]\n " + HUB_DISABLE_USAGE;
13
14
 
14
15
  const PAIRING_WARNING = "Pairing codes are secret, single-use, and expire quickly. Do not save them.";
15
16
 
@@ -23,7 +24,7 @@ const PAIRING_WARNING = "Pairing codes are secret, single-use, and expire quickl
23
24
  */
24
25
  const DEFAULT_CLIENT_BROWSER_ORIGIN = "http://localhost:10100";
25
26
 
26
- export interface HubCommandDeps extends RuntimeApiDeps {
27
+ export interface HubCommandDeps extends RuntimeApiDeps, HubDisableDeps {
27
28
  loadConfig: () => OcxConfig;
28
29
  findLiveProxy?: () => Promise<LiveProxy | null>;
29
30
  requestPairingGrant?: (
@@ -361,6 +362,7 @@ async function runInvite(args: string[], deps: HubCommandDeps): Promise<number>
361
362
  }
362
363
 
363
364
  export async function runHubCommand(args: string[], deps: HubCommandDeps): Promise<number> {
365
+ if (args[0] === "disable") return runHubDisable(args.slice(1), deps);
364
366
  if (args[0] !== "invite") {
365
367
  console.error(`Usage: ${HUB_USAGE}`);
366
368
  return 1;
package/src/cli/index.ts CHANGED
@@ -11,11 +11,11 @@ import { packageVersion } from "../lib/package-version";
11
11
  import { codexHomeIsAbsent } from "../codex/codex-home-owner";
12
12
  import { getCodexHome } from "../codex/paths";
13
13
  import { admitUpdateRestartChild } from "./update-restart-child";
14
+ import { HEALTH_SENTINEL_STANDALONE_ARG, startServiceHealthSentinel } from "../service/health-sentinel-launch";
14
15
  import { UpdateRestartRequired } from "./update-restart-candidate";
15
16
  import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
16
17
  import { findGuiDist } from "../server/gui-static";
17
18
  import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
18
-
19
19
  // Best-effort recovery for runtime execution and spawned children if launched
20
20
  // from an unlinked/deleted working directory (runs after hoisted ESM module imports).
21
21
  try {
@@ -155,9 +155,9 @@ import {
155
155
  reconcileEnsureDesiredIntegrations,
156
156
  } from "./ensure-desired-integrations";
157
157
  import { ENSURE_READY_TIMEOUT_MS, ensureKeepWaiting, waitForLiveProxy } from "./ensure-readiness";
158
+ import { productionDesktopColdStartDeps, runDesktopColdStart } from "./ensure-desktop-cold-start";
158
159
  import { refreshOwnedCatalogIntegrations } from "../integrations/catalog-refresh";
159
160
  import { loadExportModels } from "../server/management/model-rows";
160
-
161
161
  import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
162
162
  import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
163
163
  import { startArgv } from "../lib/self-launch-argv";
@@ -168,6 +168,7 @@ import { recoverStartStateUnderOwnershipLease } from "./start-owner-fence";
168
168
  import { handledSignalExitCode } from "../lib/handled-signal-exit";
169
169
  import { createLocalAttestationSecret } from "../lib/local-management-attestation";
170
170
  import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
171
+ import { startServiceLogging } from "../service/logging";
171
172
 
172
173
  /**
173
174
  * A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
@@ -207,11 +208,13 @@ async function refreshOwnedRaycastCatalog(
207
208
  }
208
209
  }
209
210
 
211
+ if (process.argv[2] === "start" && process.env.OCX_SERVICE) startServiceLogging(join(getConfigDir(), "service.log"));
210
212
  const updateRestartChild = admitUpdateRestartChild(process.argv.slice(2));
211
213
  initializeNodeLauncherContext();
212
214
  // The compiled executable is also the capture-only MCP server's launcher.
213
215
  // Handle this private entrypoint before CLI preflight or command dispatch.
214
216
  if (process.argv[2] === "__keyring-load-check") { console.log(JSON.stringify((await import("../lib/keyring-native")).inspectKeyringBinding())); process.exit(0); }
217
+ if (process.argv[2] === HEALTH_SENTINEL_STANDALONE_ARG) process.exit(await (await import("../service/health-sentinel-main")).runHealthSentinelProcess());
215
218
 
216
219
  // The compiled executable also launches the isolated MCP servers.
217
220
  // Handle these private entrypoints before CLI preflight or command dispatch.
@@ -280,6 +283,7 @@ async function chooseListenPort(
280
283
  `Port ${preferred} is reserved for unauthenticatedLoopbackListener; choose a different proxy port.`,
281
284
  );
282
285
  }
286
+ await (await import("./auxiliary-listener-preflight")).assertAuxiliaryListenerPortsAvailable(config);
283
287
  // Soft start: brief prefer-retry then ephemeral hop.
284
288
  // Explicit `--port` (service wrappers / update restart): wait for the pinned port
285
289
  // to free without killing any listener (healthy ocx / foreign). Never hop.
@@ -623,6 +627,7 @@ async function handleStart(options: { block?: boolean } = {}) {
623
627
  // Records are visible now; background work may observe this runtime without a gap.
624
628
  scheduleCatalogPrewarm();
625
629
  installCrashGuards();
630
+ let healthSentinel: ReturnType<typeof startServiceHealthSentinel> = null; // opt-in #6671, started once ready
626
631
  // No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
627
632
  // is about to transform; snapshotting earlier only captured a baseline that could
628
633
  // already be stale by the time injection ran (#477).
@@ -646,6 +651,7 @@ async function handleStart(options: { block?: boolean } = {}) {
646
651
  try { routingHealer?.stop(); } catch { /* best-effort */ }
647
652
  try { guardian.stop(); } catch { /* best-effort */ }
648
653
  try { historyGuardian?.stop(); } catch { /* best-effort */ }
654
+ try { healthSentinel?.stop(); } catch { /* best-effort */ }
649
655
  // Dashboard drain-and-restart (#563) must not tear down injection: the replacement
650
656
  // process expects Codex/Grok/env fences to still be in place. A sibling owns none of them.
651
657
  const teardown = decideStartExitTeardown({ sibling: siblingStart, recycling: isRecyclingForExit(), ocxService: process.env.OCX_SERVICE });
@@ -746,6 +752,7 @@ async function handleStart(options: { block?: boolean } = {}) {
746
752
  );
747
753
  if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config), server.port ?? port));
748
754
  await refreshOwnedRaycastCatalog(config, port);
755
+ if (!cleaned && !shuttingDown) healthSentinel = startServiceHealthSentinel(config); // after startup reconciliation; never mid-boot or mid-drain
749
756
  // #1046: one warning per startup, after BOTH writes. The server's cache
750
757
  // invalidation happens first and the catalog sync second, so the mtime is only
751
758
  // final here — and neither write site warns on its own, or a boot that hits
@@ -811,9 +818,16 @@ async function handleEnsure(options: { existingIsSuccess?: boolean; forceStart?:
811
818
  console.log("Codex autostart is disabled.");
812
819
  return false;
813
820
  }
814
- const live = owner.live;
821
+ let live = owner.live;
822
+ if (!live) {
823
+ // OpenCodex Desktop owns or runs this proxy: wait for it or start it, never a competing CLI proxy (#6971).
824
+ const desktopStart = await runDesktopColdStart(productionDesktopColdStartDeps(timeoutMs => waitForProxy(timeoutMs)));
825
+ if (desktopStart.kind === "failed") { console.error(`❌ ${desktopStart.message}`); process.exitCode = 1; return false; }
826
+ // Also for restart: the proxy was absent and Desktop started it, so the live tail below applies.
827
+ if (desktopStart.kind === "live") live = desktopStart.live;
828
+ }
815
829
  if (live) {
816
- if (options.existingIsSuccess === false) {
830
+ if (options.existingIsSuccess === false && live === owner.live) {
817
831
  console.error("Proxy appeared while restart was confirming absence; no start was attempted.");
818
832
  return false;
819
833
  }
@@ -1717,7 +1731,7 @@ async function handleStatus() {
1717
1731
  // are tagged `(local)`. A reader who sees neither draws the wrong conclusion from a correct
1718
1732
  // report — an agent read `xai ✗ not logged in` off a client and decided the hub could not
1719
1733
  // serve grok.
1720
- const remoteHubBanner = remoteHubBannerLine(status.json.remoteHub);
1734
+ const remoteHubBanner = remoteHubBannerLine(status.json.remoteHub, status.json.connection.transport);
1721
1735
  if (remoteHubBanner) console.log(remoteHubBanner);
1722
1736
  // `(local)` only while connected: on a standalone install every line is local, and tagging
1723
1737
  // them all would be noise that trains the reader to skip the tag.
@@ -1798,7 +1812,9 @@ async function handleStatus() {
1798
1812
  if (status.json.hub) {
1799
1813
  for (const line of hubStatusLines(status.json.hub)) console.log(` ${line}`);
1800
1814
  }
1801
- console.log(` Remote hub: ${status.json.connection.state}${status.json.connection.serverUrl ? ` (${status.json.connection.serverUrl})` : ""}`);
1815
+ console.log(status.json.remoteHub.link
1816
+ ? ` Remote Link Home: ${status.json.connection.state} (${status.json.remoteHub.link.homePhrase}, ${status.json.remoteHub.link.linkId})`
1817
+ : ` ${status.json.connection.transport === "link" ? "Remote Link Home" : "Remote hub"}: ${status.json.connection.state}${status.json.connection.serverUrl ? ` (${status.json.connection.serverUrl})` : ""}`);
1802
1818
  if (status.json.connection.state === "invalid" || status.json.connection.state === "mismatched") {
1803
1819
  console.log(` ⚠️ ${status.json.connection.reason}`);
1804
1820
  }
@@ -1854,10 +1870,12 @@ async function handleStatus() {
1854
1870
  // that decide what a request can route to. The local block still prints — an operator debugging
1855
1871
  // a half-migrated machine needs to see it — but under a heading that says it is not in use, and
1856
1872
  // below the hub's, so the hub's is what a reader (or an agent) encounters first (#4236).
1857
- for (const line of remoteHubStatusLines(status.json.remoteHub)) console.log(` ${line}`);
1873
+ for (const line of remoteHubStatusLines(status.json.remoteHub, status.json.connection.transport)) console.log(` ${line}`);
1858
1874
  if (status.json.remoteHub.connected) {
1859
1875
  console.log(status.json.remoteHub.stateSource === "unavailable"
1860
- ? " Local-only credential state (this is NOT the hub's; the hub's state could not be read):"
1876
+ ? status.json.connection.transport === "link"
1877
+ ? " Local-only credential state (Home state is unavailable over Remote Link):"
1878
+ : " Local-only credential state (this is NOT the hub's; the hub's state could not be read):"
1861
1879
  : " Local-only (not used for routing while connected):");
1862
1880
  }
1863
1881
  console.log(` OAuth logins${local}:`);