@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
@@ -1,127 +1,29 @@
1
- import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { mkdtempSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { assertSshAlias, buildExecArgv, buildFingerprintArgv, buildProbeArgv, REMOTE_COMMAND_NOT_FOUND, remoteOcxArgv } from "../../link/ssh-argv";
6
- import { parseFingerprintLine } from "../../link/fingerprint";
7
- import { awaitFirstAdmission } from "../../link/admission-wait";
8
- import { linkKnownHostsPath, linkStorePath } from "../../link/paths";
9
- import { clearCompensationFailed, compensationPath, markCompensationFailed, readCompensation } from "../../link/compensation";
1
+ import { probe, confirmHost, apply } from "./link-enrollment";
2
+ import { fail, pairedSession, auth, port, isRecord, exactBody, runnerFor, readStoreFor, writeStoreFor, knownHostsFile, issueKey, revokeKeyIdempotent, compensateNewLink, type LinkRouteState } from "./link-route-state";
3
+ export type { LinkRouteState, PendingLinkHost, ConfirmedLinkHost, LinkRouteListener } from "./link-route-state";
4
+ import { assertSshAlias, buildExecArgv, remoteOcxArgv } from "../../link/ssh-argv";
5
+ import { clearCompensationFailed, compensationPath, readCompensation } from "../../link/compensation";
10
6
  import { loadHostCandidates } from "../../link/ssh-config";
11
- import { newLinkId, readLinkStore, writeLinkStore, type LinkStore } from "../../link/store";
12
- import { boundHint, createSshRunner, sshFailureHint, sshRunnerErrorHint, type SshRunner, type SshRunResult } from "../../link/ssh-runner";
7
+ import { newLinkId, type LinkStore } from "../../link/store";
8
+ import { sshFailureHint, sshRunnerErrorHint } from "../../link/ssh-runner";
13
9
  import { projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
14
10
  import { isLinkPort } from "../../link/ports";
15
11
  import { joinHome, type ClientLinkJoinDeps } from "../../client/link-join";
12
+ import { clientLinkStatusIdentity } from "../../client/link-status";
16
13
  import { clientLinkTunnelStatus } from "../../client/link-tunnel";
17
14
  import { withConfigMutationLockSync } from "../../config/mutation-lock";
18
15
  import type { ManagementContext } from "./context";
19
16
  import { readManagementJsonBodyOr } from "./body";
20
- import { issueApiKeyInProcess, revokeApiKeyInProcess, type IssuedApiKey } from "./oauth-account-routes";
17
+ import { type IssuedApiKey } from "./oauth-account-routes";
21
18
  import { acceptSystemRestart, resolveListenPort } from "./system-restart";
22
19
 
23
- const PROBE_TTL_MS = 5 * 60_000;
24
- const APPLY_ADMISSION_TIMEOUT_MS = 15_000;
25
20
  const LINK_ID = /^lnk_[0-9a-f]{16}$/;
26
- // `ocx link` and `ocx connect --link` first shipped in 2.66.0; an older remote passes --version
27
- // and then fails at apply with remote_port_failed.
28
- const MIN_REMOTE_OCX = [2, 66, 0] as const;
29
- // A bounded semver shape: the parsed version reaches the confirm-host body and the outdated hint,
30
- // so the tail admits only a pre-release and build of semver identifier characters and must end
31
- // the token. Anything else attached to the version makes the output unrecognized.
32
- const REMOTE_OCX_VERSION = /^opencodex (\d{1,9})\.(\d{1,9})\.(\d{1,9})(-[0-9A-Za-z.-]{1,64})?(\+[0-9A-Za-z.-]{1,64})?(?=\s|$)/;
33
21
  const isLinkPath = (path: string): boolean => path === "/api/link" || path.startsWith("/api/link/");
34
22
 
35
- export interface PendingLinkHost {
36
- alias: string;
37
- fingerprint: string;
38
- keyType: string;
39
- knownHostLine: string;
40
- probedAt: number;
41
- }
42
-
43
- export interface ConfirmedLinkHost extends PendingLinkHost {
44
- ocxVersion: string;
45
- }
46
-
47
- export interface LinkRouteListener {
48
- ensureStarted(): Promise<void>;
49
- status(): { state: "off" | "listening" | "failed"; port: number | null; reason: string | null };
50
- close(): Promise<void>;
51
- onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
52
- }
53
-
54
- export interface LinkRouteState {
55
- pendingHosts: Map<string, PendingLinkHost>;
56
- confirmedHosts?: Map<string, ConfirmedLinkHost>;
57
- compensationFailures?: Map<string, { since: string; reason: "compensation_failed" }>;
58
- supervisor: import("../../link/supervisor").LinkSupervisor;
59
- listener: LinkRouteListener;
60
- }
61
-
62
23
  const states = new WeakMap<object, LinkRouteState>();
63
24
  // One process runs at most one join: a join ends by restarting this process as a client.
64
25
  let joinInProgress = false;
65
26
 
66
- /**
67
- * `hint` is one line bounded by `boundHint`: from ssh stderr (`sshFailureHint`), from the ssh
68
- * runner's own failure (`sshRunnerErrorHint`), or the parsed remote version for
69
- * `remote_ocx_outdated`. It is returned, never logged.
70
- */
71
- function fail(code: string, message: string, status: number, hint?: string): Response {
72
- return Response.json({ error: { code, message, ...(hint ? { hint } : {}) } }, { status, headers: { "cache-control": "no-store" } });
73
- }
74
-
75
- function isRecord(value: unknown): value is Record<string, unknown> {
76
- return value !== null && typeof value === "object" && !Array.isArray(value);
77
- }
78
-
79
- function exactBody(body: unknown, keys: readonly string[]): Record<string, unknown> | null {
80
- if (!isRecord(body)) return null;
81
- const actual = Object.keys(body).sort();
82
- const expected = [...keys].sort();
83
- return actual.length === expected.length && actual.every((key, index) => key === expected[index]) ? body : null;
84
- }
85
-
86
- function port(value: unknown): value is number {
87
- return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
88
- }
89
-
90
- /** A GUI session redeemed from an operator-created, one-use pairing grant. */
91
- function pairedSession(ctx: ManagementContext): boolean {
92
- return ctx.principal === "gui-session"
93
- && ctx.sessionControl?.isPaired(ctx.req, ctx.config) === true;
94
- }
95
-
96
- /**
97
- * A paired session, or on a standalone runtime the current loopback-issued session that reached
98
- * the public listener bound to a loopback hostname. The loopback bootstrap mints that session
99
- * without a credential, so this is casual-path protection like POST /api/github/star, not a
100
- * secret-backed boundary like the admin token. Hubs keep the paired-only rule. Durable operations
101
- * that require operator approval, such as joining this machine as a Child, use pairedSession.
102
- */
103
- function dashboardSession(ctx: ManagementContext): boolean {
104
- if (pairedSession(ctx)) return true;
105
- return ctx.principal === "gui-session"
106
- && (ctx.config.runtimeRole ?? "standalone") === "standalone"
107
- && ctx.guiSessionIssuance === "loopback"
108
- && ctx.trustedLoopbackIngress
109
- && ctx.sessionControl?.isCurrent(ctx.req, ctx.config) === true;
110
- }
111
-
112
- function adminLoopback(ctx: ManagementContext): boolean {
113
- return ctx.principal === "admin-token" && ctx.trustedLoopbackIngress;
114
- }
115
-
116
- function auth(ctx: ManagementContext, kind: "dashboard" | "paired" | "admin" | "either"): Response | null {
117
- if (ctx.guiSessionIssuance === "tailscale-identity") return fail("tailscale_session_refused", "Tailscale identity sessions cannot use link routes.", 403);
118
- const allowed = kind === "dashboard" ? dashboardSession(ctx)
119
- : kind === "paired" ? pairedSession(ctx)
120
- : kind === "admin" ? adminLoopback(ctx)
121
- : dashboardSession(ctx) || adminLoopback(ctx);
122
- return allowed ? null : fail("forbidden", "The required link authorization was not present.", 403);
123
- }
124
-
125
27
  /**
126
28
  * The client runtime a join restarts into binds exactly the configured port, with no fallback.
127
29
  * A standalone that runs elsewhere (`ocx start --port`, or a port fallback) would restart into a
@@ -142,18 +44,6 @@ function joinDenied(ctx: ManagementContext): LinkJoinDenied | null {
142
44
  return null;
143
45
  }
144
46
 
145
- function runnerFor(ctx: ManagementContext): SshRunner {
146
- return ctx.deps.sshRunner ?? createSshRunner();
147
- }
148
-
149
- function readStoreFor(ctx: ManagementContext): LinkStore {
150
- return ctx.deps.readLinkStore?.() ?? readLinkStore(linkStorePath());
151
- }
152
-
153
- function writeStoreFor(ctx: ManagementContext, store: LinkStore): void {
154
- (ctx.deps.writeLinkStore ?? (value => writeLinkStore(linkStorePath(), value)))(store);
155
- }
156
-
157
47
  function stateFor(ctx: ManagementContext): LinkRouteState | null {
158
48
  const supervisor = ctx.deps.linkSupervisor?.();
159
49
  const listener = ctx.deps.linkListener?.();
@@ -172,259 +62,11 @@ function stateFor(ctx: ManagementContext): LinkRouteState | null {
172
62
  return state;
173
63
  }
174
64
 
175
- function knownHostsFile(ctx: ManagementContext): string {
176
- return ctx.deps.linkKnownHostsPath?.() ?? linkKnownHostsPath();
177
- }
178
-
179
- function putKnownHost(path: string, alias: string, line: string): string {
180
- const before = (() => { try { return readFileSync(path, "utf8"); } catch { return ""; } })();
181
- const lines = before.split(/\r?\n/).filter(value => value && !value.startsWith(`${alias} `) && !value.startsWith(`${alias},`));
182
- lines.push(line.trim());
183
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
184
- writeFileSync(path, `${lines.join("\n")}\n`, { mode: 0o600 });
185
- try { chmodSync(path, 0o600); } catch { /* best effort on Windows */ }
186
- return before;
187
- }
188
-
189
- function restoreKnownHost(path: string, before: string): void {
190
- if (!before) {
191
- try { rmSync(path, { force: true }); } catch { /* best effort compensation */ }
192
- return;
193
- }
194
- writeFileSync(path, before, { mode: 0o600 });
195
- }
196
-
197
- function issueKey(ctx: ManagementContext, name: string): IssuedApiKey {
198
- return (ctx.deps.issueApiKey ?? issueApiKeyInProcess)(ctx.config, name);
199
- }
200
-
201
- function revokeKey(ctx: ManagementContext, id: string): boolean {
202
- return (ctx.deps.revokeApiKey ?? revokeApiKeyInProcess)(ctx.config, id);
203
- }
204
-
205
- /**
206
- * Revocation that a retry can repeat. A key that is no longer in the live config is already
207
- * revoked, which is exactly the state a partially completed cleanup leaves behind; without this
208
- * a retried DELETE would fail forever on the key it removed the first time.
209
- */
210
- function revokeKeyIdempotent(ctx: ManagementContext, id: string): boolean {
211
- let revoked = false;
212
- try { revoked = revokeKey(ctx, id); } catch { revoked = false; }
213
- if (revoked) return true;
214
- return !(ctx.config.apiKeys ?? []).some(entry => entry.id === id);
215
- }
216
-
217
- function compensationFailure(ctx: ManagementContext, state: LinkRouteState, record: LinkStore["links"][number], phase: string): Response {
218
- const since = new Date((ctx.deps.now ?? Date.now)()).toISOString();
219
- (state.compensationFailures ??= new Map()).set(record.id, { since, reason: "compensation_failed" });
220
- try { markCompensationFailed(record.id, since, compensationPath()); } catch (error) {
221
- console.warn(`[link] compensation marker persistence failed linkId=${record.id}: ${error instanceof Error ? error.message : String(error)}`);
222
- }
223
- console.warn(`[link] compensation_failed linkId=${record.id} phase=${phase}`);
224
- return fail("compensation_failed", "The link compensation could not be completed.", 500);
225
- }
226
-
227
- async function compensateNewLink(ctx: ManagementContext, state: LinkRouteState, record: LinkStore["links"][number]): Promise<Response | null> {
228
- const revoked = revokeKeyIdempotent(ctx, record.apiKeyId);
229
- if (!revoked) {
230
- try { await state.supervisor.stopLink(record.id); } catch { /* retain the record and failure marker */ }
231
- try {
232
- withConfigMutationLockSync(() => {
233
- const current = readStoreFor(ctx);
234
- if (!current.links.some(link => link.id === record.id)) {
235
- writeStoreFor(ctx, { ...current, links: [...current.links, record] });
236
- }
237
- });
238
- } catch {
239
- // The in-memory marker still makes the residual visible when persistence is unavailable.
240
- }
241
- return compensationFailure(ctx, state, record, "revoke");
242
- }
243
- try { await state.supervisor.stopLink(record.id); } catch { return compensationFailure(ctx, state, record, "stop"); }
244
- try {
245
- // Read→filter→write stays one section of the shared mutation lock: a sibling
246
- // route's create must not land between the read and the rename and vanish.
247
- const next = withConfigMutationLockSync(() => {
248
- const current = readStoreFor(ctx);
249
- const filtered = { ...current, links: current.links.filter(link => link.id !== record.id) };
250
- if (filtered.links.length !== current.links.length) {
251
- writeStoreFor(ctx, filtered);
252
- }
253
- return filtered;
254
- });
255
- clearCompensationFailed(record.id, compensationPath());
256
- state.compensationFailures?.delete(record.id);
257
- if (next.links.length === 0) await state.listener.close();
258
- return null;
259
- } catch {
260
- return compensationFailure(ctx, state, record, "store");
261
- }
262
- }
263
-
264
65
  async function candidates(ctx: ManagementContext): Promise<Response> {
265
66
  const values = ctx.deps.loadLinkCandidates?.() ?? loadHostCandidates();
266
67
  return Response.json({ candidates: values.map(value => ({ alias: value.alias, source: value.source })) });
267
68
  }
268
69
 
269
- async function probe(ctx: ManagementContext, state: LinkRouteState): Promise<Response> {
270
- const body = exactBody(await readManagementJsonBodyOr(ctx.req, null), ["alias"]);
271
- if (!body || typeof body.alias !== "string") return fail("invalid_body", "alias is required.", 400);
272
- try { assertSshAlias(body.alias); } catch { return fail("invalid_alias", "alias must be a valid SSH host alias.", 400); }
273
- const dir = mkdtempSync(join(tmpdir(), "ocx-link-probe-"));
274
- const tempKnownHosts = join(dir, "known_hosts");
275
- writeFileSync(tempKnownHosts, "", { mode: 0o600 });
276
- const runner = runnerFor(ctx);
277
- try {
278
- const result = await runner.run(buildProbeArgv({ alias: body.alias, tempKnownHostsFile: tempKnownHosts }), { timeoutMs: 30_000 });
279
- if (result.code !== 0) return fail("probe_failed", "SSH host probing failed.", 502, sshFailureHint(result.stderr));
280
- const fingerprintResult = await runner.run(buildFingerprintArgv(tempKnownHosts), { timeoutMs: 10_000 });
281
- if (fingerprintResult.code !== 0) return fail("fingerprint_failed", "The SSH host fingerprint could not be read.", 502, sshFailureHint(fingerprintResult.stderr));
282
- const parsed = parseFingerprintLine(fingerprintResult.stdout.trim());
283
- const knownHostLine = readFileSync(tempKnownHosts, "utf8").trim().split(/\r?\n/).filter(Boolean).at(-1);
284
- if (!knownHostLine) return fail("fingerprint_failed", "The SSH probe did not record a host key.", 502);
285
- state.pendingHosts.set(body.alias, { alias: body.alias, fingerprint: parsed.fingerprint, keyType: parsed.keyType, knownHostLine, probedAt: (ctx.deps.now ?? Date.now)() });
286
- return Response.json({ alias: body.alias, fingerprint: parsed.fingerprint, keyType: parsed.keyType });
287
- } catch (error) {
288
- return fail("probe_failed", "SSH host probing failed.", 502, sshRunnerErrorHint(error));
289
- } finally {
290
- try { rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
291
- }
292
- }
293
-
294
- async function confirmHost(ctx: ManagementContext, state: LinkRouteState): Promise<Response> {
295
- const body = exactBody(await readManagementJsonBodyOr(ctx.req, null), ["alias", "fingerprint"]);
296
- if (!body || typeof body.alias !== "string" || typeof body.fingerprint !== "string") return fail("invalid_body", "alias and fingerprint are required.", 400);
297
- const pending = state.pendingHosts.get(body.alias);
298
- const now = (ctx.deps.now ?? Date.now)();
299
- if (!pending || now - pending.probedAt > PROBE_TTL_MS) return fail("host_confirmation_expired", "The SSH host probe has expired.", 409);
300
- if (pending.fingerprint !== body.fingerprint) return fail("host_fingerprint_mismatch", "The confirmed fingerprint does not match the probe.", 409);
301
- const path = knownHostsFile(ctx);
302
- const before = putKnownHost(path, pending.alias, pending.knownHostLine);
303
- // Every refusal restores known_hosts and keeps the pending probe, so a retry within its TTL works.
304
- const refuse = (response: Response): Response => { restoreKnownHost(path, before); return response; };
305
- let result: SshRunResult;
306
- try {
307
- result = await runnerFor(ctx).run(buildExecArgv({ alias: pending.alias, argv: remoteOcxArgv(["--version"]), knownHostsFile: path }), { timeoutMs: 30_000 });
308
- } catch (error) {
309
- return refuse(fail("version_probe_failed", "The remote ocx version could not be confirmed.", 502, sshRunnerErrorHint(error)));
310
- }
311
- if (result.code === REMOTE_COMMAND_NOT_FOUND) return refuse(fail("remote_ocx_missing", "ocx was not found on the remote host.", 502, sshFailureHint(result.stderr)));
312
- if (result.code !== 0) return refuse(fail("version_probe_failed", "The remote ocx version could not be confirmed.", 502, sshFailureHint(result.stderr)));
313
- const version = parseRemoteOcxVersion(result.stdout);
314
- if (!version) return refuse(fail("remote_ocx_unrecognized", "The remote ocx did not report an OpenCodex version.", 502));
315
- if (!meetsRemoteOcxFloor(version.parts)) {
316
- return refuse(fail("remote_ocx_outdated", `The remote OpenCodex is older than ${MIN_REMOTE_OCX.join(".")}.`, 409, boundHint(`opencodex ${version.version}`)));
317
- }
318
- const confirmed = { ...pending, ocxVersion: version.version };
319
- state.confirmedHosts ??= new Map();
320
- state.confirmedHosts.set(pending.alias, confirmed);
321
- state.pendingHosts.delete(pending.alias);
322
- return Response.json({ alias: pending.alias, fingerprint: pending.fingerprint, ocxVersion: confirmed.ocxVersion });
323
- }
324
-
325
- /**
326
- * `ocx --version` prints `opencodex <version>` first; a usage banner, an unbounded version tail or
327
- * other output is refused.
328
- */
329
- function parseRemoteOcxVersion(stdout: string): { version: string; parts: [number, number, number] } | null {
330
- const match = REMOTE_OCX_VERSION.exec(stdout.trim().split(/\r?\n/)[0] ?? "");
331
- if (!match) return null;
332
- return { version: `${match[1]}.${match[2]}.${match[3]}${match[4] ?? ""}${match[5] ?? ""}`, parts: [Number(match[1]), Number(match[2]), Number(match[3])] };
333
- }
334
-
335
- function meetsRemoteOcxFloor(parts: readonly [number, number, number]): boolean {
336
- for (let index = 0; index < MIN_REMOTE_OCX.length; index += 1) {
337
- if (parts[index] !== MIN_REMOTE_OCX[index]) return parts[index]! > MIN_REMOTE_OCX[index]!;
338
- }
339
- return true;
340
- }
341
-
342
- function parsePortOutput(stdout: string): number | null {
343
- try {
344
- const value: unknown = JSON.parse(stdout.trim());
345
- if (!isRecord(value) || Object.keys(value).length !== 1 || !port(value.port)) return null;
346
- return value.port;
347
- } catch { return null; }
348
- }
349
-
350
- async function apply(ctx: ManagementContext, state: LinkRouteState): Promise<Response> {
351
- const body = exactBody(await readManagementJsonBodyOr(ctx.req, null), ["alias"]);
352
- if (!body || typeof body.alias !== "string") return fail("invalid_body", "alias is required.", 400);
353
- const confirmed = state.confirmedHosts?.get(body.alias);
354
- if (!confirmed) return fail("host_not_confirmed", "Confirm the SSH host before applying the link.", 409);
355
- const knownHosts = knownHostsFile(ctx);
356
- const runner = runnerFor(ctx);
357
- let remotePort: number;
358
- let portHint: string | undefined;
359
- try {
360
- const result = await runner.run(buildExecArgv({ alias: body.alias, argv: remoteOcxArgv(["link", "port"]), knownHostsFile: knownHosts }), { timeoutMs: 30_000 });
361
- if (result.code === REMOTE_COMMAND_NOT_FOUND) return fail("remote_ocx_missing", "ocx was not found on the remote host.", 502, sshFailureHint(result.stderr));
362
- remotePort = result.code === 0 ? parsePortOutput(result.stdout) ?? 0 : 0;
363
- if (result.code !== 0) portHint = sshFailureHint(result.stderr);
364
- } catch (error) { remotePort = 0; portHint = sshRunnerErrorHint(error); }
365
- if (!port(remotePort)) return fail("remote_port_failed", "The remote link port could not be determined.", 502, portHint);
366
- let issued: IssuedApiKey;
367
- try { issued = issueKey(ctx, `link:${body.alias}`); }
368
- catch { return fail("key_issue_failed", "The link key could not be issued.", 503); }
369
- const id = newLinkId();
370
- const record = { id, alias: body.alias, direction: "hub-initiated" as const, hostKeyFingerprint: confirmed.fingerprint, tunnelPort: remotePort, apiKeyId: issued.id, createdAt: new Date((ctx.deps.now ?? Date.now)()).toISOString() };
371
- let store: LinkStore;
372
- try {
373
- // Duplicate-check and append under one mutation-lock section: two writers
374
- // racing this check could otherwise both pass it and append, losing one link.
375
- const writeOutcome = withConfigMutationLockSync((): "dup" | "written" => {
376
- const current = readStoreFor(ctx);
377
- if (current.links.some(link => link.alias === body.alias && link.direction === "hub-initiated")) return "dup";
378
- store = { ...current, links: [...current.links, record] };
379
- writeStoreFor(ctx, store);
380
- return "written";
381
- });
382
- if (writeOutcome === "dup") {
383
- const compensation = await compensateNewLink(ctx, state, record);
384
- if (compensation) return compensation;
385
- return fail("link_exists", "A link for this alias already exists.", 409);
386
- }
387
- await state.listener.ensureStarted();
388
- if (state.listener.status().state !== "listening") {
389
- const compensation = await compensateNewLink(ctx, state, record);
390
- return compensation ?? fail("listener_unavailable", "The link listener is unavailable.", 503);
391
- }
392
- await state.supervisor.ensureStarted();
393
- await state.supervisor.reload();
394
- } catch {
395
- const compensation = await compensateNewLink(ctx, state, record);
396
- if (compensation) return compensation;
397
- return fail("link_apply_failed", "The link could not be started.", 503);
398
- }
399
- const admission = awaitFirstAdmission(issued.id, APPLY_ADMISSION_TIMEOUT_MS, state.listener.onAuthenticatedCatalog);
400
- const input = new TextEncoder().encode(JSON.stringify({ apiKeyId: issued.id, key: issued.key }));
401
- try {
402
- const result = await runner.run(buildExecArgv({ alias: body.alias, argv: remoteOcxArgv(["connect", "--link", "--key-stdin", "--tunnel-port", String(remotePort), "--link-id", id]), knownHostsFile: knownHosts }), { stdin: input, timeoutMs: APPLY_ADMISSION_TIMEOUT_MS });
403
- if (result.code !== 0) {
404
- void admission.catch(() => {});
405
- const compensation = await compensateNewLink(ctx, state, record);
406
- if (compensation) return compensation;
407
- const hint = sshFailureHint(result.stderr);
408
- return result.code === REMOTE_COMMAND_NOT_FOUND
409
- ? fail("remote_ocx_missing", "ocx was not found on the remote host.", 502, hint)
410
- : fail("remote_connect_failed", "The remote link connection failed.", 502, hint);
411
- }
412
- try {
413
- await admission;
414
- } catch {
415
- const compensation = await compensateNewLink(ctx, state, record);
416
- return compensation ?? fail("admission_timeout", "The remote link did not authenticate a catalog request in time.", 502);
417
- }
418
- return Response.json({ linkId: id }, { status: 202 });
419
- } catch (error) {
420
- void admission.catch(() => {});
421
- const compensation = await compensateNewLink(ctx, state, record);
422
- return compensation ?? fail("remote_connect_failed", "The remote link connection failed.", 502, sshRunnerErrorHint(error));
423
- } finally {
424
- input.fill(0);
425
- }
426
- }
427
-
428
70
  async function issue(ctx: ManagementContext): Promise<Response> {
429
71
  const body = exactBody(await readManagementJsonBodyOr(ctx.req, null), ["alias", "tunnelPort"]);
430
72
  if (!body || typeof body.alias !== "string" || !isLinkPort(body.tunnelPort)) return fail("invalid_body", "alias and tunnelPort are required.", 400);
@@ -525,6 +167,30 @@ async function handleJoin(ctx: ManagementContext, state: LinkRouteState): Promis
525
167
  }
526
168
  }
527
169
 
170
+ /**
171
+ * Restart a Home-initiated link's tunnel now: the dashboard's Retry and `ocx link reconnect`.
172
+ * The supervisor stops the current ssh child, if any, forgets its backoff or failed state and
173
+ * spawns a fresh attempt; status then reports it like any other attempt.
174
+ */
175
+ async function reconnect(ctx: ManagementContext, state: LinkRouteState, id: string): Promise<Response> {
176
+ if (!LINK_ID.test(id)) return fail("invalid_link_id", "The link id is invalid.", 400);
177
+ const record = readStoreFor(ctx).links.find(link => link.id === id);
178
+ if (!record) return fail("link_not_found", "The link was not found.", 404);
179
+ if (record.direction !== "hub-initiated") {
180
+ return fail("link_not_home_initiated", "Only a Home-initiated link has a tunnel this computer can restart.", 409);
181
+ }
182
+ try {
183
+ // A reverse forward may only target a port the link listener owns, as when a link is applied.
184
+ await state.listener.ensureStarted();
185
+ if (state.listener.status().state !== "listening") return fail("listener_unavailable", "The link listener is unavailable.", 503);
186
+ await state.supervisor.ensureStarted();
187
+ if (!await state.supervisor.reconnect(id)) return fail("link_unavailable", "The link tunnel could not be restarted.", 503);
188
+ } catch {
189
+ return fail("link_unavailable", "The link tunnel could not be restarted.", 503);
190
+ }
191
+ return Response.json({ linkId: id });
192
+ }
193
+
528
194
  async function remove(ctx: ManagementContext, state: LinkRouteState, id: string): Promise<Response> {
529
195
  if (!LINK_ID.test(id)) return fail("invalid_link_id", "The link id is invalid.", 400);
530
196
  const current = readStoreFor(ctx);
@@ -533,41 +199,46 @@ async function remove(ctx: ManagementContext, state: LinkRouteState, id: string)
533
199
  const body = await readManagementJsonBodyOr(ctx.req, {});
534
200
  if (!isRecord(body) || Object.keys(body).some(key => key !== "force") || (body.force !== undefined && typeof body.force !== "boolean")) return fail("invalid_body", "force must be a boolean.", 400);
535
201
  const force = body.force === true;
536
- await state.supervisor.stopLink(id);
537
- const restartTunnel = async (): Promise<void> => {
538
- try {
539
- await state.supervisor.ensureStarted();
540
- await state.supervisor.reload();
541
- } catch (error) {
542
- console.warn(`[link] tunnel restart failed linkId=${id}: ${error instanceof Error ? error.message : String(error)}`);
543
- }
544
- };
545
- if (!force && record.direction === "hub-initiated") {
546
- try {
547
- const result = await runnerFor(ctx).run(buildExecArgv({ alias: record.alias, argv: remoteOcxArgv(["disconnect"]), knownHostsFile: knownHostsFile(ctx) }), { timeoutMs: 30_000 });
548
- if (result.code !== 0) {
202
+ const release = state.supervisor.holdLink?.(id);
203
+ try {
204
+ await state.supervisor.stopLink(id);
205
+ const restartTunnel = async (): Promise<void> => {
206
+ release?.();
207
+ try {
208
+ await state.supervisor.ensureStarted();
209
+ await state.supervisor.reload();
210
+ } catch (error) {
211
+ console.warn(`[link] tunnel restart failed linkId=${id}: ${error instanceof Error ? error.message : String(error)}`);
212
+ }
213
+ };
214
+ if (!force && record.direction === "hub-initiated") {
215
+ try {
216
+ const result = await runnerFor(ctx).run(buildExecArgv({ alias: record.alias, argv: remoteOcxArgv(["disconnect"]), knownHostsFile: knownHostsFile(ctx) }), { timeoutMs: 30_000 });
217
+ if (result.code !== 0) {
218
+ await restartTunnel();
219
+ return fail("remote_disconnect_failed", "The remote client could not be disconnected.", 502, sshFailureHint(result.stderr));
220
+ }
221
+ } catch (error) {
549
222
  await restartTunnel();
550
- return fail("remote_disconnect_failed", "The remote client could not be disconnected.", 502, sshFailureHint(result.stderr));
223
+ return fail("remote_disconnect_failed", "The remote client could not be disconnected.", 502, sshRunnerErrorHint(error));
551
224
  }
552
- } catch (error) {
553
- await restartTunnel();
554
- return fail("remote_disconnect_failed", "The remote client could not be disconnected.", 502, sshRunnerErrorHint(error));
555
225
  }
556
- }
557
- if (!revokeKeyIdempotent(ctx, record.apiKeyId)) return fail("key_revoke_failed", "The link key could not be revoked.", 502);
558
- let next: LinkStore;
559
- try {
560
- next = withConfigMutationLockSync(() => {
561
- const latest = readStoreFor(ctx);
562
- const filtered = { ...latest, links: latest.links.filter(link => link.id !== id) };
563
- if (filtered.links.length !== latest.links.length) writeStoreFor(ctx, filtered);
564
- return filtered;
565
- });
566
- clearCompensationFailed(id, compensationPath());
567
- } catch { return fail("link_remove_failed", "The link record could not be removed.", 503); }
568
- state.compensationFailures?.delete(id);
569
- if (next.links.length === 0) await state.listener.close();
570
- return Response.json({ linkId: id });
226
+ if (!revokeKeyIdempotent(ctx, record.apiKeyId)) return fail("key_revoke_failed", "The link key could not be revoked.", 502);
227
+ let next: LinkStore;
228
+ try {
229
+ next = withConfigMutationLockSync(() => {
230
+ const latest = readStoreFor(ctx);
231
+ const filtered = { ...latest, links: latest.links.filter(link => link.id !== id) };
232
+ if (filtered.links.length !== latest.links.length) writeStoreFor(ctx, filtered);
233
+ return filtered;
234
+ });
235
+ clearCompensationFailed(id, compensationPath());
236
+ } catch { return fail("link_remove_failed", "The link record could not be removed.", 503); }
237
+ state.compensationFailures?.delete(id);
238
+ await state.supervisor.reload();
239
+ if (next.links.length === 0) await state.listener.close();
240
+ return Response.json({ linkId: id });
241
+ } finally { release?.(); }
571
242
  }
572
243
 
573
244
  export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: LinkRouteState): Promise<Response | null> {
@@ -596,6 +267,7 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
596
267
  const dto: LinkStatusDto = projectLinkStatus(
597
268
  store, state.supervisor.status(), listenerStatus, ctx.config, readCompensation(),
598
269
  ctx.config.runtimeRole === "client" ? clientLinkTunnelStatus() : null,
270
+ ctx.config.runtimeRole === "client" && ctx.config.client?.link ? clientLinkStatusIdentity(ctx.config.client.link.linkId) : null,
599
271
  );
600
272
  for (const link of dto.links) {
601
273
  const failure = state.compensationFailures?.get(link.id);
@@ -618,17 +290,17 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
618
290
  return candidates(ctx);
619
291
  }
620
292
  if (url.pathname === "/api/link/probe" && req.method === "POST") {
621
- const denied = auth(ctx, "dashboard");
293
+ const denied = auth(ctx, "either");
622
294
  if (denied) return denied;
623
295
  return probe(ctx, state);
624
296
  }
625
297
  if (url.pathname === "/api/link/confirm-host" && req.method === "POST") {
626
- const denied = auth(ctx, "dashboard");
298
+ const denied = auth(ctx, "either");
627
299
  if (denied) return denied;
628
300
  return confirmHost(ctx, state);
629
301
  }
630
302
  if (url.pathname === "/api/link/apply" && req.method === "POST") {
631
- const denied = auth(ctx, "dashboard");
303
+ const denied = auth(ctx, "either");
632
304
  if (denied) return denied;
633
305
  return apply(ctx, state);
634
306
  }
@@ -637,6 +309,14 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
637
309
  if (denied) return denied;
638
310
  return issue(ctx);
639
311
  }
312
+ const reconnectPath = req.method === "POST" ? /^\/api\/link\/([^/]+)\/reconnect$/.exec(path) : null;
313
+ if (reconnectPath) {
314
+ const denied = auth(ctx, "either");
315
+ if (denied) return denied;
316
+ let id: string;
317
+ try { id = decodeURIComponent(reconnectPath[1]!); } catch { return fail("invalid_link_id", "The link id is invalid.", 400); }
318
+ return reconnect(ctx, state, id);
319
+ }
640
320
  if (req.method === "DELETE" && path.startsWith("/api/link/")) {
641
321
  const denied = auth(ctx, "either");
642
322
  if (denied) return denied;
@@ -556,9 +556,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
556
556
  if (!(error instanceof ClientPathError)) throw error;
557
557
  return jsonResponse({ error: error.message }, 400, req, config);
558
558
  }
559
- if ((requested === "raycast" || requested === "droid") && shouldInjectApiAuthHeader(config)) {
559
+ if ((requested === "raycast" || requested === "droid" || requested === "qoder") && shouldInjectApiAuthHeader(config)) {
560
560
  return jsonResponse({
561
- error: `${requested === "droid" ? "Factory Droid" : "Raycast"} export requires an unauthenticated loopback destination; this listener requires an admission header the client cannot supply.`,
561
+ error: `${requested === "droid" ? "Factory Droid" : requested === "qoder" ? "Qoder" : "Raycast"} export requires an unauthenticated loopback destination; this listener requires an admission header the client cannot supply.`,
562
562
  reason: "non_loopback",
563
563
  }, 400, req, config);
564
564
  }
@@ -11,6 +11,7 @@
11
11
  import type { CatalogModel } from "../../codex/catalog";
12
12
  import { antigravityFamilyDisabled } from "../../providers/antigravity-effort-families";
13
13
  import { observeModelCacheRevision } from "../../codex/model-cache";
14
+ import { isPassiveRead } from "../../lib/passive-read";
14
15
  import {
15
16
  captureExportConfigAdmission,
16
17
  detachedConfigSnapshot,
@@ -306,8 +307,9 @@ export async function listManagementModelRows(
306
307
  * that brings its own roster still sees exactly what a writer would, and the two cannot
307
308
  * disagree about the roster for any reason except the roster itself.
308
309
  */
310
+ // A passive read (#6959) also skips the entitlement refresh, which reaches the Codex accounts.
309
311
  const models = options.models === undefined
310
- ? (await Promise.all([
312
+ ? isPassiveRead() ? [] : (await Promise.all([
311
313
  fetchAllModels(config, options.providerContentRevisions, projection => { config = projection; }),
312
314
  ensureCodexEntitlementFreshness(config, {
313
315
  waitMs: options.entitlementWaitMs ?? 3_000,
@@ -31,6 +31,7 @@ export const PROTOCOL_ROLLOUT_KEYS = [
31
31
  "managedMessagesNative",
32
32
  "managedMessagesNativeOAuth",
33
33
  "directEncoders",
34
+ "antigravityDirect",
34
35
  "shadowPlan",
35
36
  ] as const satisfies readonly (keyof ProtocolRolloutSettings)[];
36
37
  const ROLLOUT_KEYS = new Set<string>(PROTOCOL_ROLLOUT_KEYS);
@@ -50,6 +50,7 @@ const RETRY_POLICY_VALIDATORS = {
50
50
  */
51
51
  export const PROVIDER_COMPAT_CARRY_FIELDS = [
52
52
  "preserveReasoningContentModels",
53
+ "preserveResponsesReasoningContent",
53
54
  "requiresReasoningPlaceholderModels",
54
55
  "foldDeveloperRoleToSystem",
55
56
  "reasoningWireFormat",
@@ -160,6 +161,8 @@ export function providerCompatFieldConfigError(provider: Record<string, unknown>
160
161
  if (fold !== undefined && typeof fold !== "boolean") return "foldDeveloperRoleToSystem must be a boolean";
161
162
  const hideRawReasoning = provider.hideRawReasoning;
162
163
  if (hideRawReasoning !== undefined && typeof hideRawReasoning !== "boolean") return "hideRawReasoning must be a boolean";
164
+ const preserveResponses = provider.preserveResponsesReasoningContent;
165
+ if (preserveResponses !== undefined && typeof preserveResponses !== "boolean") return "preserveResponsesReasoningContent must be a boolean";
163
166
  const wire = provider.reasoningWireFormat;
164
167
  if (wire !== undefined && !PROVIDER_REASONING_WIRE_FORMATS.includes(wire as never)) {
165
168
  return `reasoningWireFormat must be one of: ${PROVIDER_REASONING_WIRE_FORMATS.join(", ")}`;
@@ -204,6 +207,13 @@ export function applyProviderCompatPatchFields(
204
207
  else return { error: "hideRawReasoning must be a boolean" };
205
208
  touched = true;
206
209
  }
210
+ if (Object.hasOwn(rawBody, "preserveResponsesReasoningContent")) {
211
+ const value = rawBody.preserveResponsesReasoningContent;
212
+ if (value === null) delete next.preserveResponsesReasoningContent;
213
+ else if (typeof value === "boolean") next.preserveResponsesReasoningContent = value;
214
+ else return { error: "preserveResponsesReasoningContent must be a boolean" };
215
+ touched = true;
216
+ }
207
217
  if (Object.hasOwn(rawBody, "reasoningWireFormat")) {
208
218
  const value = rawBody.reasoningWireFormat;
209
219
  if (value === null) {
@@ -917,6 +917,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
917
917
  // Presence only (#959 review): header names and values never leave the process.
918
918
  hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
919
919
  allowPrivateNetwork: p.allowPrivateNetwork === true,
920
+ preserveResponsesReasoningContent: p.preserveResponsesReasoningContent,
920
921
  liveModels: p.liveModels !== false,
921
922
  requestPacing: p.requestPacing,
922
923
  models: p.models ?? [],