@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -205,7 +205,7 @@ export function setGrokApplyFlightTestHooks(
205
205
  grokApplyFlight = null;
206
206
  grokApplyHighWaterBytes = 0;
207
207
  }
208
- import type { ManagementContext } from "./context";
208
+ import { managementInferencePort, type ManagementContext } from "./context";
209
209
 
210
210
  async function injectionModelOptions(config: OcxConfig, models: readonly CatalogModel[]) {
211
211
  const disabled = new Set(config.disabledModels ?? []);
@@ -262,7 +262,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
262
262
  const writtenProfile = current.claudeCode.desktopProfile;
263
263
  const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
264
264
  const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
265
- current.port ?? 10100,
265
+ // Live bound port, not config: a CLI override or ephemeral bind must not strand Desktop (#6598).
266
+ managementInferencePort({ config: current, deps }) ?? 10100,
266
267
  [...desktopVisibleNativeSlugs(current)],
267
268
  routed,
268
269
  current.apiKeys?.[0]?.key,
@@ -761,135 +762,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
761
762
  });
762
763
  }
763
764
 
764
- // Featured roster and saved picker order are separate settings. Native Codex advertises
765
- // the first five eligible visible rows by display priority; OCX guidance uses natural ranks.
766
- if (url.pathname === "/api/subagent-models" && req.method === "GET") {
767
- const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
768
- // Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
769
- // catalog, just buried by priority. List them first so the user can feature them over routed.
770
- const [{ listCatalogNativeSlugs }, { subagentSelectableModels }] = await Promise.all([
771
- import("../../codex/catalog"),
772
- import("../../codex/subagent-selectable-models"),
773
- ]);
774
- const chosen = config.subagentModels ?? [];
775
- // A saved roster slot must stay representable even after its model is disabled
776
- // elsewhere (Models page, provider allowlist, a provider row going away). The
777
- // dashboard treats `available` as the set of rows it can render, so a chosen id
778
- // missing from it disappears from the roster UI and the next Save — which PUTs
779
- // exactly what the UI holds — silently truncates the persisted list. Losing a
780
- // deliberate 5-model roster to an unrelated visibility toggle is data loss, not a
781
- // filter. Same reasoning as `fetchGrokCandidateModels`, which deliberately lists a
782
- // model the user already excluded so its switch remains reachable.
783
- const available = subagentSelectableModels(config, models, listCatalogNativeSlugs());
784
- // #857: let CLI/GUI show when a running Codex app-server keeps an older
785
- // in-memory catalog than the one on disk. Bounded request-path read: the synchronous
786
- // collector blocked the event loop for the whole Windows CIM walk (4-7s measured).
787
- const {
788
- collectCodexAppServerCatalogStateWithin,
789
- DASHBOARD_CATALOG_STATE_DEADLINE_MS,
790
- } = await import("../../codex/app-server-processes");
791
- const catalogState = await collectCodexAppServerCatalogStateWithin(DASHBOARD_CATALOG_STATE_DEADLINE_MS);
792
- return jsonResponse({
793
- chosen, available, catalogState,
794
- pickerAvailable: [...new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")))],
795
- pickerOrder: config.modelPickerOrder ?? [],
796
- pickerOrderMode: config.modelPickerOrderMode ?? null,
797
- });
798
- }
799
- if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
800
- let rawBody: unknown;
801
- try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
802
- if (!isPlainRecord(rawBody)) return jsonResponse({ error: "JSON body must be an object" }, 400);
803
- const body = rawBody as { models?: unknown; pickerOrder?: unknown; pickerOrderMode?: unknown };
804
- const updatesRoster = body.models !== undefined;
805
- const updatesPicker = body.pickerOrder !== undefined;
806
- if (!updatesRoster && !updatesPicker) return jsonResponse({ error: "models or pickerOrder is required" }, 400);
807
- let chosen: string[] | undefined;
808
- if (updatesRoster) {
809
- if (!Array.isArray(body.models) || body.models.some(model => typeof model !== "string")) {
810
- return jsonResponse({ error: "models must be an array of strings" }, 400);
811
- }
812
- // Keep the original valid roster contract: no discovery validation, trimming or deduping.
813
- chosen = body.models.slice(0, 5);
814
- }
815
- const mode = body.pickerOrderMode;
816
- if (mode !== undefined && (!updatesPicker || (mode !== null
817
- && mode !== "alphabetical" && mode !== "provider" && mode !== "most-used"))) {
818
- return jsonResponse({ error: "pickerOrderMode requires pickerOrder and must be alphabetical, provider, most-used, or null" }, 400);
819
- }
820
- let pickerOrder: string[] | undefined;
821
- if (updatesPicker) {
822
- if (body.pickerOrder !== null && (!Array.isArray(body.pickerOrder)
823
- || body.pickerOrder.some(model => typeof model !== "string" || model.trim() === ""))) {
824
- return jsonResponse({ error: "pickerOrder must be an array of non-empty routed model ids, or null" }, 400);
825
- }
826
- pickerOrder = body.pickerOrder === null ? [] : (body.pickerOrder as string[]).map(model => model.trim());
827
- if (new Set(pickerOrder).size !== pickerOrder.length) {
828
- return jsonResponse({ error: "pickerOrder must not contain duplicate ids" }, 400);
829
- }
830
- if (pickerOrder.length > 0) {
831
- const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
832
- // Evaluate visibility AFTER discovery: a concurrent visibility write may have completed.
833
- const visible = new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")));
834
- // A bare native id orders the complete picker (docs: guides/model-ordering.md).
835
- for (const row of nativeModelRows(config)) if (!row.disabled) visible.add(row.slug);
836
- if (pickerOrder.some(model => !visible.has(model))) {
837
- return jsonResponse({ error: "pickerOrder must contain visible routed or native model ids, each at most once" }, 400);
838
- }
839
- }
840
- }
841
-
842
- // Everything above can await. From this snapshot through persistence there is no yield.
843
- // Stage deletion intent before adopting the touched fields through the canonical
844
- // live deletion owner. A failed save restores both fields and pending intent.
845
- if (updatesPicker && config.configRebaseProvenance !== undefined
846
- && parsedConfigRebaseDeletionKeys(config) === null) {
847
- // A newer provenance format must not silently discard this clear's intent on rebase.
848
- return jsonResponse({ error: "unsupported config deletion provenance" }, 409);
849
- }
850
- const draft = { ...projectConfigRebaseProvenance(config) };
851
- if (chosen !== undefined) draft.subagentModels = chosen;
852
- if (pickerOrder !== undefined) {
853
- if (pickerOrder.length === 0) {
854
- deleteConfigTopLevelKey(draft, "modelPickerOrder");
855
- deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
856
- } else {
857
- draft.modelPickerOrder = pickerOrder;
858
- if (mode === "alphabetical" || mode === "provider" || mode === "most-used") draft.modelPickerOrderMode = mode;
859
- else deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
860
- }
861
- }
862
- const projected = projectConfigRebaseProvenance(draft);
863
- const touched = [
864
- ...(updatesRoster ? ["subagentModels" as const] : []),
865
- ...(updatesPicker ? ["modelPickerOrder" as const, "modelPickerOrderMode" as const] : []),
866
- "configRebaseProvenance" as const,
867
- ];
868
- const rollback = captureConfigTopLevelRollback(config, touched);
869
- try {
870
- for (const key of touched) {
871
- if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, {
872
- value: projected[key], writable: true, enumerable: true, configurable: true,
873
- });
874
- else deleteConfigTopLevelKey(config, key);
875
- }
876
- (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
877
- } catch (error) {
878
- rollback();
879
- throw error;
880
- }
881
- // Capture the result before convergence yields to another settings mutation.
882
- const saved = {
883
- applied: [...(config.subagentModels ?? [])],
884
- pickerOrder: [...(config.modelPickerOrder ?? [])],
885
- pickerOrderMode: config.modelPickerOrderMode ?? null,
886
- };
887
- const catalogRefresh = await convergeCodexCatalog();
888
- if (updatesRoster) {
889
- await syncClaudeAgentDefsBestEffort();
890
- await autoApplyDesktopBestEffort();
891
- }
892
- return jsonResponse({ ok: true, ...saved, catalogRefresh });
765
+ if (url.pathname === "/api/subagent-models") {
766
+ const { handleSubagentModelRoutes } = await import("./subagent-model-routes");
767
+ return handleSubagentModelRoutes(ctx, autoApplyDesktopBestEffort);
893
768
  }
894
769
 
895
770
  // Priority-ordered subagent model fallback chain for quota-aware spawn routing.
@@ -1024,7 +899,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1024
899
  if (url.pathname === "/api/claude-desktop" && req.method === "GET") {
1025
900
  try {
1026
901
  const state = await buildClaudeDesktopState(config);
1027
- const runtimePort = Number(url.port) || config.port;
902
+ const runtimePort = managementInferencePort(ctx);
1028
903
  return jsonResponse({ ...state, port: runtimePort });
1029
904
  } catch (error) {
1030
905
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
@@ -1092,7 +967,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1092
967
  };
1093
968
  }
1094
969
  const saved = await buildClaudeDesktopState(config);
1095
- const runtimePort = Number(url.port) || config.port;
970
+ const runtimePort = managementInferencePort(ctx);
1096
971
  return jsonResponse({ ok: true, ...saved, port: runtimePort });
1097
972
  } catch (error) {
1098
973
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
@@ -1194,6 +1069,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1194
1069
  const picker = modeSaved.ok && pickerPreferenceOn(loadConfig())
1195
1070
  ? await ops.enableLocked({ persist: false, context: "server" })
1196
1071
  : await pickerStatusFor(loadConfig());
1072
+ if (modeSaved.ok) await syncClaudeAgentDefsBestEffort();
1197
1073
  return jsonResponse({
1198
1074
  ok: true,
1199
1075
  mode: "first-party",
@@ -1256,7 +1132,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1256
1132
  // Picker mode belongs to first-party: stop terminating claude.ai before the gateway lands.
1257
1133
  const pickerOff = await ops.disableLocked({ persist: false });
1258
1134
  const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
1259
- Number(url.port) || latest.port,
1135
+ managementInferencePort({ config: latest, deps }),
1260
1136
  [...desktopVisibleNativeSlugs(latest)],
1261
1137
  routed,
1262
1138
  latest.apiKeys?.[0]?.key,
@@ -1542,6 +1418,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1542
1418
  ? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model } }
1543
1419
  : {}),
1544
1420
  fastMode: config.fastMode,
1421
+ forceAvailable: [...visibleNativeSlugs(config), ...filterCatalogVisibleModels(models, config).map(catalogModelSlug)],
1545
1422
  contextWindows,
1546
1423
  effectiveModelEnv: effectiveModelEnv(config.claudeCode, contextWindows),
1547
1424
  available,
@@ -1678,6 +1555,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1678
1555
  const finalDesired = firstPartyDesired(config, observeClaudeDesktopMode(config));
1679
1556
  const residual = !finalDesired.desktop && !finalDesired.cli
1680
1557
  && readFirstPartyProxyStatus(config, bound?.proxyPort ?? null) !== "none";
1558
+ await syncClaudeAgentDefsBestEffort();
1681
1559
  return jsonResponse({ ok: true, enabled: config.claudeCode?.enabled !== false,
1682
1560
  cliFirstParty: body.cliFirstParty,
1683
1561
  warnings: [
@@ -276,7 +276,7 @@ export async function syncEnabledClientIntegrations(
276
276
  },
277
277
  config,
278
278
  port,
279
- }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid"]));
279
+ }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
280
280
 
281
281
  return out;
282
282
  }
@@ -160,7 +160,7 @@ export interface ManagementApiDeps {
160
160
  issueApiKey?: (config: OcxConfig, name: string) => IssuedApiKey;
161
161
  revokeApiKey?: (config: OcxConfig, id: string) => boolean;
162
162
  loadLinkCandidates?: () => Array<{ alias: string; source: "ssh_config" | "tailscale" }>;
163
- /** The port this runtime listens on; a join is refused unless it is the configured port. */
163
+ /** Bound public inference port, not the ingress receiving this management request. */
164
164
  liveListenPort?: () => number | undefined;
165
165
  now?: () => number;
166
166
  }
@@ -191,3 +191,10 @@ export interface ManagementContext {
191
191
  convergeCodexCatalog: () => Promise<CatalogDisposition>;
192
192
  syncClaudeAgentDefsBestEffort: () => Promise<void>;
193
193
  }
194
+
195
+ /** A management-only ingress cannot supply the port used by generated inference clients. */
196
+ export function managementInferencePort(ctx: Pick<ManagementContext, "config" | "deps">): number {
197
+ // Lifecycle owns the actual port (including CLI overrides/ephemeral binds); direct route
198
+ // fixtures fall back to config without consulting another runtime's state file.
199
+ return ctx.deps.liveListenPort?.() ?? ctx.config.port;
200
+ }
@@ -53,9 +53,9 @@ export async function buildCursorIntegrationStatus(
53
53
  const privateInference = pick(installs, "private-inference");
54
54
  const regular = pick(installs, "regular");
55
55
  const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
56
- // The port the browser reached is the one Cursor on the same machine will reach too; the
57
- // runtime record and config.port are fallbacks for a request that carries no port.
58
- const port = runtime?.port ?? (Number(ctx.url?.port) || config.port);
56
+ // The lifecycle-bound public port wins, then the PID-matched runtime record, then config.
57
+ // A management-only ingress port must never become the Cursor gateway (#6598).
58
+ const port = deps.liveListenPort?.() ?? runtime?.port ?? config.port;
59
59
  // Cursor runs on this machine, so the gateway URL it is told to paste is the LOCAL one: the
60
60
  // unauthenticated loopback listener when one is enabled, and otherwise the bind address on the
61
61
  // public port — 127.0.0.1 for a loopback or wildcard bind exactly as before, and the tailnet
@@ -24,7 +24,8 @@ import {
24
24
  type IntegrationClientId,
25
25
  } from "../../integrations/registry";
26
26
  import { detectRaycast, type RaycastInstall } from "../../integrations/raycast-detect";
27
- import { readIntegrationState } from "../../integrations/state";
27
+ import { readIntegrationState, readOwnedDroidReasoningDefaults } from "../../integrations/state";
28
+ import { droidReasoningModels, validateDroidReasoningDefaults } from "../../clients/config-export";
28
29
  import { createIntegrationStateStore, type IntegrationStateStore } from "../../integrations/store";
29
30
  import {
30
31
  applyIntegrationCoordinated,
@@ -45,7 +46,7 @@ import {
45
46
  } from "../../integrations/mutation-flight";
46
47
  import { jsonResponse } from "../auth-cors";
47
48
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
48
- import type { ManagementContext } from "./context";
49
+ import { managementInferencePort, type ManagementContext } from "./context";
49
50
  import { exportSnapshotIdentity, loadExportModels, previewExportSnapshot } from "./model-rows";
50
51
  import {
51
52
  previewIntegration,
@@ -74,6 +75,10 @@ export type IntegrationStateEnvelope = {
74
75
  * the shared `IntegrationStatus`, which describes the file, not the app.
75
76
  */
76
77
  raycast?: RaycastInstall;
78
+ droidReasoning?: {
79
+ models: Array<{ model: string; label: string; efforts: string[] }>;
80
+ defaults: Record<string, string>;
81
+ };
77
82
  } & IntegrationStateRecord;
78
83
 
79
84
  export interface IntegrationStateListEnvelope {
@@ -225,7 +230,7 @@ function asideOptions(ctx: ManagementContext): AsideProfileRouteOptions {
225
230
  return {
226
231
  input: () => input ??= {
227
232
  config: ctx.config,
228
- port: Number(ctx.url.port) || ctx.config.port,
233
+ port: managementInferencePort(ctx),
229
234
  models: () => loadExportModels(ctx.config),
230
235
  store: integrationStore(),
231
236
  ...pathOverrides(),
@@ -241,12 +246,14 @@ async function buildIntegrationWriteInput(
241
246
  clientId: IntegrationClientId,
242
247
  ctx: ManagementContext,
243
248
  store: IntegrationStateStore,
249
+ droidReasoningDefaults?: Record<string, string>,
244
250
  ): Promise<IntegrationWriteInput> {
245
251
  return {
246
252
  clientId,
247
253
  models: await loadExportModels(ctx.config),
248
254
  config: ctx.config,
249
- port: Number(ctx.url.port) || ctx.config.port,
255
+ port: managementInferencePort(ctx),
256
+ ...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
250
257
  store,
251
258
  io: integrationMutationTestHooks?.io,
252
259
  ...pathOverrides(),
@@ -265,6 +272,7 @@ async function buildIntegrationPreviewInput(
265
272
  clientId: IntegrationClientId,
266
273
  ctx: ManagementContext,
267
274
  store: IntegrationStateStore,
275
+ droidReasoningDefaults?: Record<string, string>,
268
276
  ): Promise<{ input: IntegrationWriteInput; identity: string } | null> {
269
277
  const snapshot = previewExportSnapshot(ctx.config);
270
278
  // No cached roster means no honest snapshot to plan against. Gathering one here would make a
@@ -276,7 +284,8 @@ async function buildIntegrationPreviewInput(
276
284
  clientId,
277
285
  models: snapshot.models,
278
286
  config: ctx.config,
279
- port: Number(ctx.url.port) || ctx.config.port,
287
+ port: managementInferencePort(ctx),
288
+ ...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
280
289
  store,
281
290
  io: integrationMutationTestHooks?.io,
282
291
  ...pathOverrides(),
@@ -309,6 +318,31 @@ function invalidClientResponse(ctx: ManagementContext): Response {
309
318
  }, 400, ctx.req, ctx.config);
310
319
  }
311
320
 
321
+ function isStringRecord(value: unknown): value is Record<string, string> {
322
+ return value !== null && typeof value === "object" && !Array.isArray(value)
323
+ && Object.values(value).every(item => typeof item === "string");
324
+ }
325
+
326
+ function invalidDroidDefaultsResponse(ctx: ManagementContext, message: string): Response {
327
+ return jsonResponse({ error: message, code: "invalid_droid_reasoning_defaults" }, 400, ctx.req, ctx.config);
328
+ }
329
+
330
+ function readDroidReasoningDefaults(
331
+ ctx: ManagementContext,
332
+ body: Record<string, unknown>,
333
+ clientId: string,
334
+ operation: IntegrationPlanOperation,
335
+ ): Record<string, string> | Response | undefined {
336
+ if (!Object.hasOwn(body, "droidReasoningDefaults")) return undefined;
337
+ if (clientId !== "droid" || (operation !== "apply" && operation !== "overwrite")) {
338
+ return invalidDroidDefaultsResponse(ctx, "droidReasoningDefaults is allowed only for Droid apply or overwrite");
339
+ }
340
+ const defaults = body.droidReasoningDefaults;
341
+ return isStringRecord(defaults)
342
+ ? defaults
343
+ : invalidDroidDefaultsResponse(ctx, "droidReasoningDefaults must map model names to effort strings");
344
+ }
345
+
312
346
  /**
313
347
  * No cached model roster, so there is nothing honest to plan against.
314
348
  *
@@ -364,6 +398,7 @@ function stalePlanGuard(
364
398
  request: PreviewRequest,
365
399
  fingerprint: string,
366
400
  capturedIdentity: string | null,
401
+ droidReasoningDefaults?: Record<string, string>,
367
402
  ): {
368
403
  revalidate: NonNullable<CoordinatedIntegrationOptions["revalidate"]>;
369
404
  response: () => Response | null;
@@ -379,7 +414,7 @@ function stalePlanGuard(
379
414
  * without ever swapping the roster this mutation is about to use.
380
415
  */
381
416
  if (exportSnapshotIdentity(ctx.config) !== capturedIdentity) {
382
- const refreshed = await buildIntegrationPreviewInput(clientId, ctx, store);
417
+ const refreshed = await buildIntegrationPreviewInput(clientId, ctx, store, droidReasoningDefaults);
383
418
  stale = refreshed === null ? "unavailable" : previewIntegration(refreshed.input, request);
384
419
  return { ok: false, reason: "conflict", state: "conflict", clientId, message: "the model roster changed while confirming" };
385
420
  }
@@ -584,7 +619,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
584
619
  if (url.pathname === "/api/client-integrations" && req.method === "GET") {
585
620
  try {
586
621
  const models = await loadExportModels(ctx.config);
587
- const port = Number(url.port) || ctx.config.port;
622
+ const port = managementInferencePort(ctx);
588
623
  // One store for the whole collection read: without it this route retried
589
624
  // maintenance and counted snapshots in the default store even when the
590
625
  // caller had bound everything else to a temp root (A-gate round 13).
@@ -723,9 +758,17 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
723
758
  code: "invalid_preview_operation",
724
759
  }, 400, req, ctx.config);
725
760
  }
761
+ const droidDefaults = readDroidReasoningDefaults(ctx, parsed, previewClient, operation);
762
+ if (droidDefaults instanceof Response) return droidDefaults;
726
763
  try {
727
- const captured = await buildIntegrationPreviewInput(previewClient as IntegrationClientId, ctx, integrationStore());
764
+ const captured = await buildIntegrationPreviewInput(
765
+ previewClient as IntegrationClientId, ctx, integrationStore(), droidDefaults,
766
+ );
728
767
  if (!captured) return previewUnavailableResponse(ctx);
768
+ if (droidDefaults !== undefined) {
769
+ const invalid = validateDroidReasoningDefaults(captured.input.models, droidDefaults);
770
+ if (invalid) return invalidDroidDefaultsResponse(ctx, invalid);
771
+ }
729
772
  return jsonResponse(previewIntegration(captured.input, { operation }), 200, req, ctx.config);
730
773
  } catch (error) {
731
774
  return internalErrorResponse(error, ctx);
@@ -907,7 +950,15 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
907
950
  // process spawn, and no other client's read should pay for it.
908
951
  const envelope: IntegrationStateEnvelope = requestedClient === "raycast"
909
952
  ? { ...state, raycast: (raycastDetectTestHook ?? detectRaycast)() }
910
- : state;
953
+ : requestedClient === "droid"
954
+ ? {
955
+ ...state,
956
+ droidReasoning: {
957
+ models: droidReasoningModels(input.models),
958
+ defaults: readOwnedDroidReasoningDefaults(input),
959
+ },
960
+ }
961
+ : state;
911
962
  return jsonResponse(envelope, 200, req, ctx.config);
912
963
  } catch (error) {
913
964
  return internalErrorResponse(error, ctx);
@@ -944,6 +995,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
944
995
  const requestedOperation: IntegrationPlanOperation = parsed.enabled
945
996
  ? (parsed.overwriteConflict === true ? "overwrite" : "apply")
946
997
  : "disable";
998
+ const droidDefaults = readDroidReasoningDefaults(ctx, parsed, requestedClient, requestedOperation);
999
+ if (droidDefaults instanceof Response) return droidDefaults;
947
1000
  const binding = planBindingOf(parsed);
948
1001
  if (binding === "half") return halfBoundResponse(ctx);
949
1002
  if (binding === "unknown-operation" || (binding !== "none" && binding.operation !== requestedOperation)) {
@@ -964,11 +1017,15 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
964
1017
  */
965
1018
  const boundToggle = binding === "none"
966
1019
  ? null
967
- : await buildIntegrationPreviewInput(requestedClient, ctx, integrationStore());
1020
+ : await buildIntegrationPreviewInput(requestedClient, ctx, integrationStore(), droidDefaults);
968
1021
  if (binding !== "none" && boundToggle === null) return previewUnavailableResponse(ctx);
969
1022
  const input = boundToggle
970
1023
  ? boundToggle.input
971
- : await buildIntegrationWriteInput(requestedClient, ctx, integrationStore());
1024
+ : await buildIntegrationWriteInput(requestedClient, ctx, integrationStore(), droidDefaults);
1025
+ if (droidDefaults !== undefined) {
1026
+ const invalid = validateDroidReasoningDefaults(input.models, droidDefaults);
1027
+ if (invalid) return invalidDroidDefaultsResponse(ctx, invalid);
1028
+ }
972
1029
  const guard = binding === "none" ? null : stalePlanGuard(
973
1030
  requestedClient,
974
1031
  ctx,
@@ -976,6 +1033,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
976
1033
  { operation: binding.operation },
977
1034
  binding.fingerprint,
978
1035
  boundToggle === null ? null : boundToggle.identity,
1036
+ droidDefaults,
979
1037
  );
980
1038
  const result = await runIntegrationMutationFlight(
981
1039
  requestedClient,
@@ -86,7 +86,7 @@ function port(value: unknown): value is number {
86
86
  return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
87
87
  }
88
88
 
89
- /** A paired GUI session. Only hub runtimes issue these. */
89
+ /** A GUI session redeemed from an operator-created, one-use pairing grant. */
90
90
  function pairedSession(ctx: ManagementContext): boolean {
91
91
  return ctx.principal === "gui-session"
92
92
  && ctx.sessionControl?.isPaired(ctx.req, ctx.config) === true;
@@ -96,9 +96,8 @@ function pairedSession(ctx: ManagementContext): boolean {
96
96
  * A paired session, or on a standalone runtime the current loopback-issued session that reached
97
97
  * the public listener bound to a loopback hostname. The loopback bootstrap mints that session
98
98
  * without a credential, so this is casual-path protection like POST /api/github/star, not a
99
- * secret-backed boundary like the admin token. Hubs keep the paired-only rule. Join admits this
100
- * session too: turning a Child on from its own dashboard is the point of the route, and the
101
- * dashboard warns first that the restart briefly interrupts running Codex turns.
99
+ * secret-backed boundary like the admin token. Hubs keep the paired-only rule. Durable operations
100
+ * that require operator approval, such as joining this machine as a Child, use pairedSession.
102
101
  */
103
102
  function dashboardSession(ctx: ManagementContext): boolean {
104
103
  if (pairedSession(ctx)) return true;
@@ -113,9 +112,10 @@ function adminLoopback(ctx: ManagementContext): boolean {
113
112
  return ctx.principal === "admin-token" && ctx.trustedLoopbackIngress;
114
113
  }
115
114
 
116
- function auth(ctx: ManagementContext, kind: "dashboard" | "admin" | "either"): Response | null {
115
+ function auth(ctx: ManagementContext, kind: "dashboard" | "paired" | "admin" | "either"): Response | null {
117
116
  if (ctx.guiSessionIssuance === "tailscale-identity") return fail("tailscale_session_refused", "Tailscale identity sessions cannot use link routes.", 403);
118
117
  const allowed = kind === "dashboard" ? dashboardSession(ctx)
118
+ : kind === "paired" ? pairedSession(ctx)
119
119
  : kind === "admin" ? adminLoopback(ctx)
120
120
  : dashboardSession(ctx) || adminLoopback(ctx);
121
121
  return allowed ? null : fail("forbidden", "The required link authorization was not present.", 403);
@@ -131,9 +131,14 @@ function joinPortMatches(ctx: ManagementContext): boolean {
131
131
  return live !== undefined && live === ctx.config.port;
132
132
  }
133
133
 
134
- /** Whether `POST /api/link/join` would pass its admission, role and port gates for this caller. */
135
- function joinAvailable(ctx: ManagementContext): boolean {
136
- return dashboardSession(ctx) && (ctx.config.runtimeRole ?? "standalone") === "standalone" && joinPortMatches(ctx);
134
+ type LinkJoinDenied = "pairing_required" | "standalone_required" | "join_port_mismatch";
135
+
136
+ /** Read-only explanation of the join gates, in the same order as `POST /api/link/join`. */
137
+ function joinDenied(ctx: ManagementContext): LinkJoinDenied | null {
138
+ if (!pairedSession(ctx)) return "pairing_required";
139
+ if ((ctx.config.runtimeRole ?? "standalone") !== "standalone") return "standalone_required";
140
+ if (!joinPortMatches(ctx)) return "join_port_mismatch";
141
+ return null;
137
142
  }
138
143
 
139
144
  function runnerFor(ctx: ManagementContext): SshRunner {
@@ -549,9 +554,9 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
549
554
  if (!isLinkPath(path)) return null;
550
555
  if (ctx.guiSessionIssuance === "tailscale-identity") return fail("tailscale_session_refused", "Tailscale identity sessions cannot use link routes.", 403);
551
556
  if (url.pathname === "/api/link/join" && req.method === "POST") {
552
- // The same dashboard admission as the Home side. Every refusal below runs before link state
553
- // is read and before any SSH.
554
- const denied = auth(ctx, "dashboard");
557
+ // Joining durably redirects local client traffic, so a credentiallessly bootstrapped loopback
558
+ // session is insufficient. Every refusal below runs before link state and before any SSH.
559
+ const denied = auth(ctx, "paired");
555
560
  if (denied) return denied;
556
561
  if ((ctx.config.runtimeRole ?? "standalone") !== "standalone") return fail("standalone_required", "Client initiated links require standalone runtime mode.", 409);
557
562
  if (!joinPortMatches(ctx)) return fail("join_port_mismatch", "OpenCodex is not running on its configured port, so it cannot restart as a Child.", 409);
@@ -578,9 +583,11 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
578
583
  const failure = state.compensationFailures?.get(store.links[0].id);
579
584
  if (failure) Object.assign(dto.child, { state: "failed" as const, since: failure.since, reason: failure.reason });
580
585
  }
581
- // A dashboard session also learns whether it may join as a Child. The admin-token answer stays
586
+ // A dashboard session also learns why it cannot join as a Child. The admin-token answer stays
582
587
  // the exact K16 document that `ocx link status` validates key by key.
583
- const body: LinkStatusDto & { joinAvailable?: boolean } = ctx.principal === "gui-session" ? { ...dto, joinAvailable: joinAvailable(ctx) } : dto;
588
+ const denial = ctx.principal === "gui-session" ? joinDenied(ctx) : null;
589
+ const body: LinkStatusDto & { joinAvailable?: boolean; joinDenied?: LinkJoinDenied | null } = ctx.principal === "gui-session"
590
+ ? { ...dto, joinAvailable: denial === null, joinDenied: denial } : dto;
584
591
  return Response.json(body, { headers: { "cache-control": "no-store" } });
585
592
  }
586
593
  if (url.pathname === "/api/link/candidates" && req.method === "GET") {
@@ -167,7 +167,7 @@ import type {
167
167
 
168
168
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
169
169
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
170
- import type { ManagementContext } from "./context";
170
+ import { managementInferencePort, type ManagementContext } from "./context";
171
171
  import { effectiveModelReasoningEfforts, inheritedModelReasoningEfforts, listManagementModelRows, loadExportModels } from "./model-rows";
172
172
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
173
173
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -204,7 +204,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
204
204
  ?? (await import("../../integrations/catalog-refresh")).refreshOwnedCatalogIntegrations;
205
205
  const clientIntegrations = await refresh({
206
206
  config,
207
- port: Number(url.port) || config.port,
207
+ port: managementInferencePort(ctx),
208
208
  models: () => loadExportModels(config),
209
209
  });
210
210
  return { catalogRefresh, clientIntegrations };
@@ -562,7 +562,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
562
562
  reason: "non_loopback",
563
563
  }, 400, req, config);
564
564
  }
565
- const baseUrl = opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname, config);
565
+ const baseUrl = opencodeProxyBaseUrl(managementInferencePort(ctx), config.hostname, config);
566
566
  let models: ExportModel[];
567
567
  try {
568
568
  // The ONE loader every export surface uses. It carries the visibility