@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,248 @@
1
+ /**
2
+ * A Remote Link Child's view of its Home (#6957): the Home's name for refusal and status text,
3
+ * the Home's OpenCodex-owned Codex settings, and authenticated read-only management reads.
4
+ *
5
+ * Every read goes through the connection-bound link relay (`fetchBoundLinkRelay`): the link key
6
+ * is sent only after the listener on the tunnel port proves it holds the same link identity, so a
7
+ * process that took the tunnel port never sees the key.
8
+ */
9
+ import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { atomicWriteFile, isMissingPathError } from "../config/atomic-write";
12
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
13
+ import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
14
+ import { hardenSecretDir } from "../lib/windows-secret-acl";
15
+ import {
16
+ parseHomeCodexSettingsDocument,
17
+ type HomeCodexSettingsDocument,
18
+ } from "../link/codex-settings";
19
+ import {
20
+ LINK_CODEX_SETTINGS_PATH,
21
+ LINK_MANAGEMENT_PREFIX,
22
+ linkManagementReadAllowed,
23
+ } from "../link/management-routes";
24
+ import { linkDir } from "../link/paths";
25
+ import type { OcxClientConnectionConfig } from "../types";
26
+ import { fetchBoundLinkRelay } from "./link-relay-transport";
27
+ import { readClientLinkState, type ClientLinkState } from "./link-state";
28
+
29
+ const RECORD_MAX_BYTES = 64 * 1024;
30
+ const SETTINGS_MAX_BYTES = 64 * 1024;
31
+ const READ_TIMEOUT_MS = 10_000;
32
+
33
+ export interface LinkHomeDeps {
34
+ /** Trusted complete-send test seam; production uses the connection-bound transport. */
35
+ fetchImpl?: typeof fetch;
36
+ timeoutMs?: number;
37
+ }
38
+
39
+ /** The connection fields a Home read needs; connect calls these before its state is committed. */
40
+ export type LinkHomeConnection = Pick<OcxClientConnectionConfig, "transport" | "link" | "apiKeyId">;
41
+
42
+ export interface HomeCodexSettingsRecord {
43
+ linkId: string;
44
+ fetchedAt: string;
45
+ document: HomeCodexSettingsDocument;
46
+ }
47
+
48
+ export function homeCodexSettingsPath(): string {
49
+ return join(linkDir(), "home-codex-settings.json");
50
+ }
51
+
52
+ function linkTarget(connection: LinkHomeConnection, admissionKey: string) {
53
+ if (connection.transport !== "link" || !connection.link) throw new Error("not a Remote Link Child");
54
+ return {
55
+ tunnelPort: connection.link.tunnelPort,
56
+ linkId: connection.link.linkId,
57
+ apiKeyId: connection.apiKeyId,
58
+ admissionKey,
59
+ };
60
+ }
61
+
62
+ /** The last applied Home settings for this link, or null when absent, malformed or another link's. */
63
+ export function readHomeCodexSettingsRecord(linkId: string, path = homeCodexSettingsPath()): HomeCodexSettingsRecord | null {
64
+ try {
65
+ const stat = lstatSync(path);
66
+ if (!stat.isFile() || stat.size > RECORD_MAX_BYTES) return null;
67
+ const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
68
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
69
+ const record = raw as Record<string, unknown>;
70
+ if (record.linkId !== linkId || typeof record.fetchedAt !== "string" || Number.isNaN(Date.parse(record.fetchedAt))) return null;
71
+ const document = parseHomeCodexSettingsDocument(record.document);
72
+ return document ? { linkId, fetchedAt: record.fetchedAt, document } : null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ export function writeHomeCodexSettingsRecord(record: HomeCodexSettingsRecord, path = homeCodexSettingsPath()): void {
79
+ const dir = linkDir();
80
+ assertNotRealHomeUnderTest(join(dir, ".."));
81
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
82
+ if (process.platform === "win32") hardenSecretDir(dir, { required: true });
83
+ else chmodSync(dir, 0o700);
84
+ atomicWriteFile(path, `${JSON.stringify(record, null, 2)}\n`);
85
+ if (process.platform !== "win32") chmodSync(path, 0o600);
86
+ }
87
+
88
+ export function clearHomeCodexSettingsRecord(path = homeCodexSettingsPath()): void {
89
+ try { unlinkSync(path); } catch (error) { if (!isMissingPathError(error)) throw error; }
90
+ }
91
+
92
+ /**
93
+ * The Home's name as this Child knows it: the SSH alias of a Child-initiated link, else the
94
+ * host name the Home published with its Codex settings, else null ("its Home").
95
+ */
96
+ export function linkHomeName(
97
+ connection: OcxClientConnectionConfig,
98
+ readSidecar: () => ClientLinkState | null = readClientLinkState,
99
+ ): string | null {
100
+ const linkId = connection.link?.linkId;
101
+ if (!linkId) return null;
102
+ try {
103
+ const sidecar = readSidecar();
104
+ if (sidecar && sidecar.linkId === linkId) return sidecar.alias;
105
+ } catch { /* an unreadable sidecar names nothing */ }
106
+ return readHomeCodexSettingsRecord(linkId)?.document.home.name ?? null;
107
+ }
108
+
109
+ /** "Home home-box" or "its Home": the phrase every link Child message uses. */
110
+ export function linkHomePhrase(
111
+ connection: OcxClientConnectionConfig,
112
+ readSidecar?: () => ClientLinkState | null,
113
+ ): string {
114
+ const name = linkHomeName(connection, readSidecar);
115
+ return name ? `Home ${name}` : "its Home";
116
+ }
117
+
118
+ /**
119
+ * A `fetch` for the existing data-plane reads (`/v1/hub-state`, `/v1/usage`) that sends each
120
+ * request through the connection-bound link relay, so the link key reaches only the Home.
121
+ */
122
+ export function linkBoundFetch(connection: LinkHomeConnection, admissionKey: string): typeof fetch {
123
+ const target = linkTarget(connection, admissionKey);
124
+ return ((input: RequestInfo | URL, init?: RequestInit) => {
125
+ // A Request keeps its method, headers and signal unless init overrides them.
126
+ const request = input instanceof Request ? input : null;
127
+ const url = request ? request.url : String(input);
128
+ return fetchBoundLinkRelay(target, url, {
129
+ ...(request ? { method: request.method, headers: request.headers, signal: request.signal } : {}),
130
+ ...(init ?? {}),
131
+ redirect: "manual",
132
+ });
133
+ }) as typeof fetch;
134
+ }
135
+
136
+ /**
137
+ * One authenticated, read-only management read on the Home. `path` is a Home management path
138
+ * with an optional query (`/api/providers`); anything outside the shared allowlist throws before
139
+ * a request is built.
140
+ */
141
+ export async function fetchLinkHomeManagement(
142
+ connection: LinkHomeConnection,
143
+ admissionKey: string,
144
+ path: string,
145
+ init: { method?: "GET" | "HEAD"; signal?: AbortSignal } = {},
146
+ deps: LinkHomeDeps = {},
147
+ ): Promise<Response> {
148
+ const method = init.method ?? "GET";
149
+ const local = new URL(path, "http://link.invalid");
150
+ if (local.origin !== "http://link.invalid" || !linkManagementReadAllowed(method, local.pathname, local.searchParams)) {
151
+ throw new Error("path is not readable over the Remote Link");
152
+ }
153
+ const target = linkTarget(connection, admissionKey);
154
+ const destination = `http://127.0.0.1:${target.tunnelPort}${LINK_MANAGEMENT_PREFIX}${local.pathname}${local.search}`;
155
+ const signal = init.signal
156
+ ? AbortSignal.any([init.signal, AbortSignal.timeout(deps.timeoutMs ?? READ_TIMEOUT_MS)])
157
+ : AbortSignal.timeout(deps.timeoutMs ?? READ_TIMEOUT_MS);
158
+ const request: RequestInit = {
159
+ method,
160
+ headers: { accept: "application/json", authorization: `Bearer ${admissionKey}` },
161
+ redirect: "manual",
162
+ signal,
163
+ };
164
+ return deps.fetchImpl ? deps.fetchImpl(destination, request) : fetchBoundLinkRelay(target, destination, request);
165
+ }
166
+
167
+ export type HomeCodexSettingsFetch =
168
+ | { kind: "fresh"; document: HomeCodexSettingsDocument }
169
+ | { kind: "unsupported" }
170
+ | { kind: "failed"; reason: string };
171
+
172
+ export async function fetchHomeCodexSettings(
173
+ connection: LinkHomeConnection,
174
+ admissionKey: string,
175
+ deps: LinkHomeDeps = {},
176
+ ): Promise<HomeCodexSettingsFetch> {
177
+ let response: Response;
178
+ try {
179
+ const target = linkTarget(connection, admissionKey);
180
+ const destination = `http://127.0.0.1:${target.tunnelPort}${LINK_CODEX_SETTINGS_PATH}`;
181
+ const request: RequestInit = {
182
+ method: "GET",
183
+ headers: { accept: "application/json", authorization: `Bearer ${admissionKey}` },
184
+ redirect: "manual",
185
+ signal: AbortSignal.timeout(deps.timeoutMs ?? READ_TIMEOUT_MS),
186
+ };
187
+ response = deps.fetchImpl ? await deps.fetchImpl(destination, request) : await fetchBoundLinkRelay(target, destination, request);
188
+ } catch (error) {
189
+ return { kind: "failed", reason: error instanceof Error && error.name === "LinkRelayAuthenticationError" ? error.message : "the Home is unreachable" };
190
+ }
191
+ if (response.status === 404) {
192
+ try { await response.body?.cancel(); } catch { /* best effort */ }
193
+ return { kind: "unsupported" };
194
+ }
195
+ if (!response.ok) {
196
+ try { await response.body?.cancel(); } catch { /* best effort */ }
197
+ return { kind: "failed", reason: `the Home answered HTTP ${response.status}` };
198
+ }
199
+ try {
200
+ const result = await readBoundedResponseBytes(response, { maxBytes: SETTINGS_MAX_BYTES, signal: AbortSignal.timeout(deps.timeoutMs ?? READ_TIMEOUT_MS) });
201
+ if (result.oversized) return { kind: "failed", reason: "the Home's settings exceeded the allowed size" };
202
+ const document = parseHomeCodexSettingsDocument(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(result.bytes)));
203
+ return document ? { kind: "fresh", document } : { kind: "failed", reason: "the Home returned an unreadable settings document" };
204
+ } catch {
205
+ return { kind: "failed", reason: "the Home returned an unreadable settings document" };
206
+ }
207
+ }
208
+
209
+ export type HomeCodexSettingsSource =
210
+ | { source: "home"; document: HomeCodexSettingsDocument; fetchedAt: string }
211
+ | { source: "cached"; document: HomeCodexSettingsDocument; fetchedAt: string; reason: string }
212
+ | { source: "local"; reason: string };
213
+
214
+ /**
215
+ * The settings a sync applies. A failed read reuses the last record for this link so a brief
216
+ * outage never flips the Child back to its local values; a Home that predates the route leaves
217
+ * the Child's local values in place and says so. A fresh read is NOT recorded here: the caller
218
+ * records it with `recordHomeCodexSettings` only after the injection that applies it succeeds,
219
+ * because `ocx status` reads the record as "applied".
220
+ */
221
+ export async function resolveHomeCodexSettings(
222
+ connection: LinkHomeConnection,
223
+ admissionKey: string,
224
+ deps: LinkHomeDeps & { now?: () => Date } = {},
225
+ ): Promise<HomeCodexSettingsSource> {
226
+ const linkId = connection.link?.linkId;
227
+ if (!linkId) return { source: "local", reason: "not a Remote Link Child" };
228
+ const fetched = await fetchHomeCodexSettings(connection, admissionKey, deps);
229
+ if (fetched.kind === "fresh") {
230
+ return { source: "home", document: fetched.document, fetchedAt: (deps.now ?? (() => new Date()))().toISOString() };
231
+ }
232
+ if (fetched.kind === "unsupported") {
233
+ clearHomeCodexSettingsRecord();
234
+ return { source: "local", reason: "the Home does not publish Codex settings; upgrade the Home" };
235
+ }
236
+ const cached = readHomeCodexSettingsRecord(linkId);
237
+ return cached
238
+ ? { source: "cached", document: cached.document, fetchedAt: cached.fetchedAt, reason: fetched.reason }
239
+ : { source: "local", reason: fetched.reason };
240
+ }
241
+
242
+ /** Record settings a successful injection applied. Best effort: the config is already written. */
243
+ export function recordHomeCodexSettings(linkId: string, settings: HomeCodexSettingsSource): void {
244
+ if (settings.source !== "home") return;
245
+ try {
246
+ writeHomeCodexSettingsRecord({ linkId, fetchedAt: settings.fetchedAt, document: settings.document });
247
+ } catch { /* the next sync records it */ }
248
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The Child end of the Remote Link management relay (#6957).
3
+ *
4
+ * The Child's dashboard reaches its Home's management reads at `/api/machine/hub-relay/api/*`,
5
+ * the same base a hub client's dashboard uses. The browser authenticates to the Child with the
6
+ * Child's own dashboard session; this relay drops every browser header, builds a fresh GET for
7
+ * the Home's `/v1/link-management` prefix and sends it through the connection-bound link relay,
8
+ * which attaches the link key only after the tunnel listener proves the link identity. Anything
9
+ * outside the shared read allowlist is refused here, before a request exists.
10
+ */
11
+ import { HUB_RELAY_RESPONSE_BODY_MAX_BYTES } from "./hub-relay";
12
+ import type { LinkIngress } from "./link-ingress";
13
+ import { relayLinkDataRequest } from "./link-relay";
14
+ import {
15
+ LINK_MANAGEMENT_PREFIX,
16
+ linkChildManagementRefusal,
17
+ linkManagementReadAllowed,
18
+ linkManagementRouteAllowed,
19
+ } from "../link/management-routes";
20
+
21
+ export const LINK_CHILD_READ_ONLY = "link_child_read_only";
22
+
23
+ export function linkChildRefusalResponse(homePhrase: string, linkId: string, operation: string): Response {
24
+ return Response.json({
25
+ error: linkChildManagementRefusal(homePhrase, linkId, operation),
26
+ code: LINK_CHILD_READ_ONLY,
27
+ linkId,
28
+ }, { status: 403, headers: { "cache-control": "no-store" } });
29
+ }
30
+
31
+ export async function relayLinkManagementRead(
32
+ req: Request,
33
+ suffix: string,
34
+ ingress: LinkIngress,
35
+ homePhrase: string,
36
+ ): Promise<Response> {
37
+ const linkId = ingress.linkId ?? "unknown";
38
+ const query = suffix.indexOf("?");
39
+ const pathname = query === -1 ? suffix : suffix.slice(0, query);
40
+ const search = query === -1 ? "" : suffix.slice(query);
41
+ if (!linkManagementReadAllowed(req.method, pathname, new URLSearchParams(search.slice(1)))) {
42
+ return linkChildRefusalResponse(homePhrase, linkId, `${req.method.toUpperCase()} ${pathname}`);
43
+ }
44
+ const admissionKey = ingress.linkKey();
45
+ if (!admissionKey) return Response.json({ error: "link_credential_unavailable" }, { status: 503 });
46
+ const outbound = new Request(`http://127.0.0.1${LINK_MANAGEMENT_PREFIX}${pathname}${search}`, {
47
+ method: req.method,
48
+ headers: { accept: "application/json" },
49
+ signal: req.signal,
50
+ });
51
+ return relayLinkDataRequest(outbound, {
52
+ tunnelPort: ingress.tunnelPort,
53
+ admissionKey,
54
+ linkId: ingress.linkId,
55
+ apiKeyId: ingress.apiKeyId,
56
+ }, {
57
+ ...ingress.relay,
58
+ bodyLimitBytes: HUB_RELAY_RESPONSE_BODY_MAX_BYTES,
59
+ routeAllowed: linkManagementRouteAllowed,
60
+ });
61
+ }
@@ -69,6 +69,11 @@ export interface LinkRelayDeps {
69
69
  /** The longest a request waits for a reconnecting tunnel, from its first wait. */
70
70
  holdMs?: number;
71
71
  now?: () => number;
72
+ /**
73
+ * The route table this relay enforces. Defaults to the data-plane table; only the Child's
74
+ * authenticated management relay passes the read-only management table (#6957).
75
+ */
76
+ routeAllowed?: (url: URL, req: Request) => boolean;
72
77
  }
73
78
 
74
79
  export const LINK_RELAY_RETRY_AFTER_SECONDS = 1;
@@ -305,7 +310,7 @@ export async function relayLinkDataRequest(
305
310
  deps: LinkRelayDeps = {},
306
311
  ): Promise<Response> {
307
312
  const url = new URL(req.url);
308
- if (!linkRouteAllowed(url, req)) return jsonError(404, "not_found");
313
+ if (!(deps.routeAllowed ?? linkRouteAllowed)(url, req)) return jsonError(404, "not_found");
309
314
  let destination: string;
310
315
  try { destination = linkRelayDestination(url, target); } catch { return jsonError(404, "not_found"); }
311
316
  const validation = validateHubRelayRequestHeaders(linkFramingHeaders(req.headers));
@@ -1,3 +1,7 @@
1
+ import { lstatSync } from "node:fs";
2
+ import { isMissingPathError } from "../config/atomic-write";
3
+ import { readClientLinkState, childLinkMarkerPath } from "./link-state";
4
+ import type { ClientLinkIdentity } from "../link/status-projection";
1
5
  import type { LinkStatusDto } from "../link/status-projection";
2
6
  import type { ClientLinkState } from "./link-state";
3
7
  import type { ClientLinkSupervisorStatus } from "./link-tunnel";
@@ -37,3 +41,13 @@ function childState(sidecar: ClientLinkSidecarRead, supervisor: ClientLinkSuperv
37
41
  reason: supervisor.probe ?? (tunnel.kind === "failed" ? tunnel.reason : null),
38
42
  };
39
43
  }
44
+
45
+ /** Only client-owned evidence can establish who owns the tunnel. Unreadable evidence stays unknown. */
46
+ export function clientLinkStatusIdentity(linkId: string): ClientLinkIdentity | null {
47
+ try {
48
+ const sidecar = readClientLinkState();
49
+ if (sidecar) return sidecar.linkId === linkId ? { direction: "client-initiated", alias: sidecar.alias } : null;
50
+ try { lstatSync(childLinkMarkerPath()); return null; }
51
+ catch (error) { return isMissingPathError(error) ? { direction: "hub-initiated" } : null; }
52
+ } catch { return null; }
53
+ }
@@ -3,6 +3,7 @@ import { diagnoseCodexShim, installCodexShim, uninstallCodexShim } from "../code
3
3
  import { readManagementJsonBody } from "../server/management/body";
4
4
  import type { OcxClientConnectionConfig } from "../types";
5
5
  import { disconnectClient, syncConnectedClient } from "./connect";
6
+ import { linkHomeName } from "./link-home";
6
7
  import { isLinkConnection } from "./state";
7
8
 
8
9
  export type HubReachability = "unknown" | "online" | "offline" | "unauthorized";
@@ -19,6 +20,8 @@ export interface MachineStatusV1 {
19
20
  connectedAt: string;
20
21
  catalogSyncedAt?: string;
21
22
  hubReachability: HubReachability;
23
+ /** Present only on a Remote Link Child: the dashboard shows "managed by Home <name>". */
24
+ link?: { linkId: string; home: string | null; readOnly: true };
22
25
  }
23
26
 
24
27
  export interface MachineApiDeps {
@@ -27,6 +30,8 @@ export interface MachineApiDeps {
27
30
  scheduleStandaloneRecycle: (disconnectedTokenFingerprint: string) => void;
28
31
  hubReachability?: () => HubReachability;
29
32
  setHubReachability?: (value: HubReachability) => void;
33
+ /** Link mode: the Home's name as this listener knows it (sidecar alias or recorded host name). */
34
+ linkHomeName?: () => string | null;
30
35
  }
31
36
 
32
37
  const defaultDeps: MachineApiDeps = {
@@ -52,24 +57,26 @@ async function jsonBody(req: Request): Promise<unknown | Response> {
52
57
  function statusPayload(req: Request, state: OcxClientConnectionConfig, deps: MachineApiDeps): MachineStatusV1 {
53
58
  const machineBase = new URL(req.url).origin;
54
59
  // A link Child's management URL is the tunnel, whose hub-link ingress serves no /api/* and no
55
- // session bootstrap. Its dashboard reads the Child's own listener instead (`/api/link/status`).
60
+ // session bootstrap. Its dashboard therefore uses this listener's relay base: the browser holds
61
+ // only this listener's session, and the listener relays allowlisted reads to the Home with the
62
+ // link key (#6957, src/client/link-management-relay.ts).
56
63
  const linkMode = isLinkConnection(state);
64
+ const relay = linkMode || state.managementTransport === "relay";
57
65
  return {
58
66
  mode: "client",
59
67
  connected: true,
60
68
  machineBase,
61
- sharedBase: linkMode
62
- ? machineBase
63
- : state.managementTransport === "relay"
64
- ? `${machineBase}/api/machine/hub-relay`
65
- : state.managementUrl,
69
+ sharedBase: relay ? `${machineBase}/api/machine/hub-relay` : state.managementUrl,
66
70
  sharedServerOrigin: linkMode ? machineBase : state.managementUrl,
67
- managementTransport: state.managementTransport,
71
+ managementTransport: relay ? "relay" : state.managementTransport,
68
72
  apiKeyId: state.apiKeyId,
69
73
  protocolVersion: state.protocolVersion,
70
74
  connectedAt: state.connectedAt,
71
75
  ...(state.catalogSyncedAt ? { catalogSyncedAt: state.catalogSyncedAt } : {}),
72
76
  hubReachability: deps.hubReachability?.() ?? "unknown",
77
+ ...(linkMode && state.link
78
+ ? { link: { linkId: state.link.linkId, home: deps.linkHomeName ? deps.linkHomeName() : linkHomeName(state), readOnly: true as const } }
79
+ : {}),
73
80
  };
74
81
  }
75
82
 
@@ -23,6 +23,8 @@ import { projectClientLinkChild, type ClientLinkSidecarRead } from "./link-statu
23
23
  import type { ClientLinkSupervisorStatus } from "./link-tunnel";
24
24
  import { packageVersion } from "../lib/package-version";
25
25
  import { linkRouteAllowed } from "../link/routes";
26
+ import { linkHomeName, linkHomePhrase } from "./link-home";
27
+ import { relayLinkManagementRead } from "./link-management-relay";
26
28
 
27
29
  const VERSION = packageVersion("0.0.0");
28
30
  const GUI_SPA_PATHS = new Set([
@@ -100,6 +102,8 @@ export function startMachineListener(
100
102
  if (linkMode && !connection.link) throw new Error("link machine listener requires link transport metadata");
101
103
  const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
102
104
  let hubReachability: HubReachability = "unknown";
105
+ const readSidecar = deps.readSidecar ?? (() => readClientLinkState());
106
+ const sidecarOrNull = () => { const read = readSidecarSafely(readSidecar); return read === "invalid" ? null : read; };
103
107
  const machineApiDeps: MachineApiDeps = {
104
108
  sync: deps.machineApi?.sync ?? syncConnectedClient,
105
109
  disconnect: deps.machineApi?.disconnect ?? disconnectClient,
@@ -108,8 +112,11 @@ export function startMachineListener(
108
112
  }),
109
113
  hubReachability: deps.machineApi?.hubReachability ?? (() => hubReachability),
110
114
  setHubReachability: deps.machineApi?.setHubReachability ?? (value => { hubReachability = value; }),
115
+ ...(linkMode ? { linkHomeName: () => linkHomeName(connection, sidecarOrNull) } : {}),
111
116
  };
112
- const relayEnabled = !linkMode && connection.managementTransport === "relay";
117
+ // A link Child relays its dashboard's management reads to the Home over the link (#6957);
118
+ // a hub client relays over its management transport when that is `relay`.
119
+ const relayEnabled = linkMode || connection.managementTransport === "relay";
113
120
  // Link mode only. Everything is resolved once here, so a relayed request reads no file and
114
121
  // builds no policy: the key is cached, the loopback policy is fixed at bind like the listener.
115
122
  const inboundBodyLimit = resolveInboundBodyLimitBytes(config.maxInboundBodyBytes);
@@ -123,8 +130,6 @@ export function startMachineListener(
123
130
  relay: { fetchImpl: deps.fetchImpl, bodyLimitBytes: inboundBodyLimit, tunnel: deps.linkTunnel, ...deps.linkRelay },
124
131
  }
125
132
  : null;
126
- const readSidecar = deps.readSidecar ?? (() => readClientLinkState());
127
-
128
133
  return (deps.serve ?? (options => Bun.serve(options)))({
129
134
  port: port ?? config.port ?? 10100,
130
135
  hostname: "127.0.0.1",
@@ -147,9 +152,29 @@ export function startMachineListener(
147
152
  }
148
153
  if (url.pathname.startsWith("/api/machine/hub-relay/")) {
149
154
  if (!relayEnabled) return json404(req);
155
+ const prefix = "/api/machine/hub-relay";
156
+ if (linkIngress) {
157
+ const suffix = `${url.pathname.slice(prefix.length)}${url.search}`;
158
+ // The dashboard's shared plane bootstraps here. On a link Child that plane is this
159
+ // listener, so it receives this listener's own session; no Home credential exists.
160
+ if (suffix === "/opencodex-session") {
161
+ if (req.method !== "GET") return json404(req);
162
+ const session = issueGuiSession(req, config, managementAuth, { trustedTailscaleIngress: false });
163
+ return session ? serveSessionBootstrap(session) : Response.json({ error: "session unavailable" }, { status: 503 });
164
+ }
165
+ const authError = requireManagementAuth(req, managementAuth, config);
166
+ if (authError) return authError;
167
+ if (managementPrincipal(req, managementAuth, config) !== "gui-session") {
168
+ return Response.json({ error: "opencodex machine GUI session required" }, { status: 401 });
169
+ }
170
+ const response = await relayLinkManagementRead(req, suffix, linkIngress, linkHomePhrase(connection, sidecarOrNull));
171
+ if (response.status === 401) hubReachability = "unauthorized";
172
+ else if (response.status >= 500) hubReachability = "offline";
173
+ else if (response.status !== 403) hubReachability = "online";
174
+ return response;
175
+ }
150
176
  const authError = requireMachineAuth(req, managementAuth, config);
151
177
  if (authError) return authError;
152
- const prefix = "/api/machine/hub-relay";
153
178
  const suffix = `${url.pathname.slice(prefix.length)}${url.search}`;
154
179
  const response = await relayHubManagementRequest(req, suffix, {
155
180
  managementUrl: connection.managementUrl,
@@ -202,6 +202,7 @@ export interface ExportContext {
202
202
  export type DroidReasoningDefaults = Record<string, string>;
203
203
 
204
204
  export type ExportClientId =
205
+ | "qoder"
205
206
  | "opencode"
206
207
  | "pi"
207
208
  | "omp"
@@ -9,8 +9,9 @@ import { shouldInjectApiAuthHeader } from "../../codex/inject";
9
9
 
10
10
 
11
11
  /**
12
- * Authoritative context window, or undefined. Never guesses: a missing, non-finite, or
13
- * non-positive value means the serializer omits every context-derived field.
12
+ * Validate the supplied catalog window (which may be a labelled vendor estimate).
13
+ * A missing, non-finite, or non-positive value omits every context-derived field;
14
+ * this serializer does not independently infer a window.
14
15
  */
15
16
  export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
16
17
  if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
@@ -0,0 +1,93 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { isLoopbackHostname, shouldInjectApiAuthHeader } from "../../codex/loopback-target";
4
+ import { formatSelectorConjunction } from "../../integrations/merge";
5
+ import { ClientPathError } from "../config-export";
6
+ import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
7
+ import { LOOPBACK_API_KEY_PLACEHOLDER, OPENCODE_PROVIDER_ID } from "./constants";
8
+ import { authoritativeContextWindow, normalizeExportModels } from "./model-metadata";
9
+
10
+ /**
11
+ * Qoder's configuration directory: `QODER_CONFIG_DIR` when set, otherwise `~/.qoder`
12
+ * (https://docs.qoder.com/cli/settings-reference). Export and detection both resolve it here.
13
+ * A relative override is refused because opencodex and Qoder would anchor it to different cwds.
14
+ */
15
+ export function qoderHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
16
+ const override = env.QODER_CONFIG_DIR?.trim();
17
+ if (!override) return join(home, ".qoder");
18
+ if (override === "~") return home;
19
+ if (override.startsWith("~/") || override.startsWith("~\\")) return join(home, override.slice(2));
20
+ if (!isAbsolute(override)) {
21
+ throw new ClientPathError(
22
+ `QODER_CONFIG_DIR must be an absolute path or start with ~; "${override}" depends on the working directory, `
23
+ + "so opencodex and Qoder would disagree about which file it names.",
24
+ );
25
+ }
26
+ return override;
27
+ }
28
+
29
+ export function qoderConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
30
+ return join(qoderHomeDir(env, home), "settings.json");
31
+ }
32
+
33
+ /** Qoder IDE and qodercli share this provider and custom-model catalog. */
34
+ export function buildQoderClientConfig(ctx: ExportContext) {
35
+ if (shouldInjectApiAuthHeader(ctx.config) || !isLoopbackHostname(new URL(ctx.baseUrl).hostname)) {
36
+ throw new Error("Qoder's placeholder API key requires a loopback-only listener.");
37
+ }
38
+ return qoderDocument(ctx);
39
+ }
40
+
41
+ function qoderDocument(ctx: ExportContext) {
42
+ const models = normalizeExportModels(ctx.models);
43
+ return {
44
+ providers: {
45
+ [OPENCODE_PROVIDER_ID]: {
46
+ type: "openai-compatible",
47
+ protocol: "openai",
48
+ displayName: "opencodex",
49
+ baseUrl: ctx.baseUrl,
50
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
51
+ ...(models[0] ? { defaultModel: models[0].namespaced } : {}),
52
+ models: models.map(model => ({ model: model.namespaced })),
53
+ },
54
+ },
55
+ modelConfigs: {
56
+ customModels: models.map(model => ({
57
+ provider: OPENCODE_PROVIDER_ID,
58
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
59
+ model: model.namespaced,
60
+ baseURL: ctx.baseUrl,
61
+ displayName: model.namespaced,
62
+ maxInputTokens: authoritativeContextWindow(model.contextWindow) ?? 128000,
63
+ })),
64
+ },
65
+ };
66
+ }
67
+
68
+ export type QoderGeneratedConfig = ReturnType<typeof buildQoderClientConfig>;
69
+
70
+ export function summarizeQoder(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
71
+ const models = (document as QoderGeneratedConfig | undefined)?.modelConfigs?.customModels ?? [];
72
+ return { modelCount: models.length, modelsWithoutLimits: 0 };
73
+ }
74
+
75
+ export function buildQoderContribution(ctx: ExportContext): ManagedContribution {
76
+ // The writer gates admission before applying. Classification and disable must
77
+ // still work after the operator changes the proxy to a non-loopback bind.
78
+ const doc = qoderDocument(ctx);
79
+ return {
80
+ clientId: "qoder",
81
+ fragments: [
82
+ { path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
83
+ ...doc.modelConfigs.customModels.map(model => {
84
+ const selector = formatSelectorConjunction([
85
+ { field: "provider", value: OPENCODE_PROVIDER_ID },
86
+ { field: "model", value: model.model },
87
+ ]);
88
+ if (selector === null) throw new Error("Qoder model ID cannot be represented by a managed array selector.");
89
+ return { path: ["modelConfigs", "customModels", selector], value: model };
90
+ }),
91
+ ],
92
+ };
93
+ }