@bitkyc08/opencodex 2.64.0 → 2.65.0-preview.20260925

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 (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-SggB6t3z.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-CpuDF3ci.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
@@ -52,7 +52,7 @@ import {
52
52
  setDebugSettings,
53
53
  type DebugFlag,
54
54
  } from "../../lib/debug-settings";
55
- import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
55
+ import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig, OcxComboCooldownWaitPolicy } from "../../types";
56
56
  import { drainAndShutdown } from "../lifecycle";
57
57
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
58
58
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
@@ -78,12 +78,14 @@ import { COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS } from "../../combos";
78
78
  function sparseComboConfig<T extends {
79
79
  cooldownMs?: number;
80
80
  waitForCooldownMs?: number;
81
+ cooldownWaitPolicy?: OcxComboCooldownWaitPolicy | null;
81
82
  imageInput?: "auto" | "disabled";
82
83
  reasoningEffortMode?: "strict" | "adaptive";
83
84
  defaultEffortMode?: "fallback" | "force";
84
- }>(combo: T): Omit<T, "cooldownMs" | "waitForCooldownMs" | "imageInput" | "reasoningEffortMode" | "defaultEffortMode"> & {
85
+ }>(combo: T): Omit<T, "cooldownMs" | "waitForCooldownMs" | "cooldownWaitPolicy" | "imageInput" | "reasoningEffortMode" | "defaultEffortMode"> & {
85
86
  cooldownMs?: number;
86
87
  waitForCooldownMs?: number;
88
+ cooldownWaitPolicy?: OcxComboCooldownWaitPolicy;
87
89
  imageInput?: "disabled";
88
90
  reasoningEffortMode?: "adaptive";
89
91
  defaultEffortMode?: "force";
@@ -91,6 +93,7 @@ function sparseComboConfig<T extends {
91
93
  const {
92
94
  cooldownMs,
93
95
  waitForCooldownMs,
96
+ cooldownWaitPolicy,
94
97
  imageInput,
95
98
  reasoningEffortMode,
96
99
  defaultEffortMode,
@@ -99,6 +102,9 @@ function sparseComboConfig<T extends {
99
102
  return {
100
103
  ...rest,
101
104
  ...(cooldownMs !== undefined ? { cooldownMs } : {}),
105
+ // #5691: the normalizer yields null for "unset"; persisting that would put a
106
+ // meaningless key in every stored combo. Only the opt-in value is written.
107
+ ...(cooldownWaitPolicy ? { cooldownWaitPolicy } : {}),
102
108
  ...(waitForCooldownMs !== undefined && waitForCooldownMs !== COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS
103
109
  ? { waitForCooldownMs }
104
110
  : {}),
@@ -174,6 +180,42 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
174
180
  ...(!Object.hasOwn(requestedCombo, "waitForCooldownMs") && previous?.waitForCooldownMs !== undefined
175
181
  ? { waitForCooldownMs: previous.waitForCooldownMs }
176
182
  : {}),
183
+ // Same reason as defaultEffortMode below: the dashboard does not expose the
184
+ // last-resort policy, so a GUI round-trip that omits it must not delete it (#5736).
185
+ ...(!Object.hasOwn(requestedCombo, "cooldownWaitPolicy") && previous?.cooldownWaitPolicy !== undefined
186
+ ? { cooldownWaitPolicy: previous.cooldownWaitPolicy }
187
+ : {}),
188
+ // #5687: an API or CLI client that omits these must not reset them. The dashboard
189
+ // sends both explicitly, so switching back to auto/strict there still replaces them.
190
+ ...(!Object.hasOwn(requestedCombo, "reasoningEffortMode") && previous?.reasoningEffortMode !== undefined
191
+ ? { reasoningEffortMode: previous.reasoningEffortMode }
192
+ : {}),
193
+ ...(!Object.hasOwn(requestedCombo, "imageInput") && previous?.imageInput !== undefined
194
+ ? { imageInput: previous.imageInput }
195
+ : {}),
196
+ // `lastResort` rides on each target, so a GUI that re-sends the target list without the
197
+ // flag would strip it. Carry it over per target, matched on provider+model.
198
+ ...(Array.isArray(requestedCombo.targets) && Array.isArray(previous?.targets)
199
+ ? {
200
+ targets: requestedCombo.targets.map(target => {
201
+ // A non-record entry stays as the client sent it so comboConfigError reports it.
202
+ // Reading `lastResort` off it here would throw in place of that structured 400.
203
+ if (!isPlainRecord(target)) return target;
204
+ if (Object.hasOwn(target, "lastResort")) return target;
205
+ const rawProvider = target.provider;
206
+ const rawModel = target.model;
207
+ if (typeof rawProvider !== "string" || typeof rawModel !== "string") return target;
208
+ // Identity is trimmed on both sides: the normalizer trims too, so a re-sent
209
+ // " b " must still match the stored "b" instead of losing the flag.
210
+ const provider = rawProvider.trim();
211
+ const model = rawModel.trim();
212
+ const before = previous.targets.find(
213
+ candidate => candidate.provider === provider && candidate.model === model,
214
+ );
215
+ return before?.lastResort ? { ...target, lastResort: true } : target;
216
+ }),
217
+ }
218
+ : {}),
177
219
  // The dashboard does not expose this advanced CLI/API policy. Preserve it when
178
220
  // a GUI round-trip omits the field instead of silently downgrading to fallback.
179
221
  ...(!Object.hasOwn(requestedCombo, "defaultEffortMode") && previous?.defaultEffortMode !== undefined
@@ -198,6 +240,13 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
198
240
  } = sparseComboConfig(normalized);
199
241
  const stored: OcxComboConfig = {
200
242
  ...normalizedBase,
243
+ // The normalizer gives every target an explicit `lastResort: false`; persisting that
244
+ // would add a noise key to every target of every combo, including ones that never use
245
+ // the policy (#5736). Only the opt-in value is stored, matching how the combo-level
246
+ // policy is handled in sparseComboConfig.
247
+ targets: normalizedBase.targets.map(({ lastResort, ...target }) =>
248
+ lastResort ? { ...target, lastResort: true } : target,
249
+ ),
201
250
  ...(normalizedAlias ? { alias: normalizedAlias } : {}),
202
251
  ...(normalizedNativeAlias ? { nativeAlias: true } : {}),
203
252
  ...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
@@ -275,6 +324,14 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
275
324
  Object.entries(claudeCode.modelMap).map(([source, model]) => [source, migrateAgentReference(model)]),
276
325
  );
277
326
  }
327
+ if (claudeCode.intercept?.modelMap) {
328
+ claudeCode.intercept = {
329
+ ...claudeCode.intercept,
330
+ modelMap: Object.fromEntries(
331
+ Object.entries(claudeCode.intercept.modelMap).map(([pickerId, route]) => [pickerId, migrateAgentReference(route)]),
332
+ ),
333
+ };
334
+ }
278
335
  config.claudeCode = claudeCode;
279
336
  }
280
337
  }
@@ -6,7 +6,7 @@ import { readFileSync } from "node:fs";
6
6
  import type { CatalogModel } from "../../codex/catalog";
7
7
  import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
8
8
  import {
9
- applyCodexDesktopSwitches,
9
+ applyCodexConfigInjection,
10
10
  describeCodexDesktopSwitches,
11
11
  type CodexDesktopSwitchApply,
12
12
  } from "../../codex/desktop-switches";
@@ -59,7 +59,10 @@ import {
59
59
  codexQuotaAutoRefreshStatus,
60
60
  runCodexQuotaAutoRefresh,
61
61
  } from "../../codex/quota-auto-refresh";
62
- import { getMainAccountHardLockStatus } from "../../codex/main-account-hard-lock";
62
+ import {
63
+ getMainAccountHardLockStatus,
64
+ isMainAccountHardLockEnabled,
65
+ } from "../../codex/main-account-hard-lock";
63
66
  import {
64
67
  codexAccountPickerEnabled,
65
68
  initializeDefaultCodexAccountNamespaces,
@@ -208,40 +211,48 @@ export async function syncEnabledClientIntegrations(
208
211
  }
209
212
  }
210
213
 
211
- if (claudeDesktopIntegrationEnabled(config)) {
214
+ const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
215
+ // A first-party Desktop must never get a gateway profile written and selected by a sync.
216
+ if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") {
212
217
  try {
213
218
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
214
219
  const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
215
220
  const { fetchAllModels } = await import("../management-api");
216
221
  const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
217
- // Discovery admits a concurrent OFF or settings edit. Re-read outside C:
218
- // the writer facade owns L and its final desired-state check under L→C.
219
- const latest = loadConfig();
220
- if (claudeDesktopIntegrationEnabled(latest)) {
221
- const routed = filterCatalogVisibleModels(models, latest)
222
- .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
223
- const writtenProfile = latest.claudeCode?.desktopProfile;
224
- const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
225
- const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
226
- port,
227
- [...desktopVisibleNativeSlugs(latest)],
228
- routed,
229
- latest.apiKeys?.[0]?.key,
230
- "static",
231
- writtenProfile,
232
- nativeContextLimits(latest),
233
- );
234
- if (!r.written || !r.fingerprint) {
235
- out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
236
- } else {
237
- const marked = commitDesktopAppliedMarker(markerBaseline, r.fingerprint);
238
- out.push(marked.status === "unavailable"
239
- ? { client: "claude-desktop", ok: false, reason: "Claude Desktop applied marker was not saved (" + marked.reason + ")" }
240
- : marked.value === false
241
- ? { client: "claude-desktop", ok: false, reason: "Claude Desktop desired profile changed during sync; applied marker skipped" }
242
- : { client: "claude-desktop", ok: true, changed: true });
222
+ // Serialized with Desktop mode transitions (picker lock): a first-party switch cannot interleave.
223
+ const { runPickerTransition } = await import("./claude-desktop-picker-routes");
224
+ await runPickerTransition(config, async () => {
225
+ // Discovery admits a concurrent OFF or settings edit. Re-read outside C:
226
+ // the writer facade owns L and its final desired-state check under L→C.
227
+ const latest = loadConfig();
228
+ // Discovery awaited: the mode may have changed meanwhile. Re-resolve on the fresh read,
229
+ // immediately before the writer, so a first-party switch during fetchAllModels still wins.
230
+ if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party") {
231
+ const routed = filterCatalogVisibleModels(models, latest)
232
+ .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
233
+ const writtenProfile = latest.claudeCode?.desktopProfile;
234
+ const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
235
+ const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
236
+ port,
237
+ [...desktopVisibleNativeSlugs(latest)],
238
+ routed,
239
+ latest.apiKeys?.[0]?.key,
240
+ "static",
241
+ writtenProfile,
242
+ nativeContextLimits(latest),
243
+ );
244
+ if (!r.written || !r.fingerprint) {
245
+ out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
246
+ } else {
247
+ const marked = commitDesktopAppliedMarker(markerBaseline, r.fingerprint);
248
+ out.push(marked.status === "unavailable"
249
+ ? { client: "claude-desktop", ok: false, reason: "Claude Desktop applied marker was not saved (" + marked.reason + ")" }
250
+ : marked.value === false
251
+ ? { client: "claude-desktop", ok: false, reason: "Claude Desktop desired profile changed during sync; applied marker skipped" }
252
+ : { client: "claude-desktop", ok: true, changed: true });
253
+ }
243
254
  }
244
- }
255
+ });
245
256
  } catch (error) {
246
257
  out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) });
247
258
  }
@@ -340,7 +351,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
340
351
  ultraFastTier: config.ultraFastTier === true,
341
352
  // Absent means on by default: the GUI renders a switch enabled unless explicit false.
342
353
  fastRows: config.fastRows !== false,
343
- codexMainAccountHardLock: config.codexMainAccountHardLock === true,
354
+ codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
344
355
  mainAccountHardLock: getMainAccountHardLockStatus(config),
345
356
  // Absent means the historical auto-open, so the GUI can render the toggle
346
357
  // without having to know that `undefined` and `true` mean the same thing.
@@ -586,8 +597,10 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
586
597
  else if (body.ultraFastTier === false) deleteConfigTopLevelKey(config, "ultraFastTier");
587
598
  if (body.fastRows === false) config.fastRows = false;
588
599
  else if (body.fastRows === true) deleteConfigTopLevelKey(config, "fastRows");
589
- if (body.codexMainAccountHardLock === true) config.codexMainAccountHardLock = true;
590
- else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
600
+ // Inverted from the pair above because the default is on (#5694): off is the persisted
601
+ // decision, so it writes `false`, while on deletes the key and returns to the default.
602
+ if (body.codexMainAccountHardLock === false) config.codexMainAccountHardLock = false;
603
+ else if (body.codexMainAccountHardLock === true) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
591
604
  if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
592
605
  else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
593
606
  if (body.codexClientCompaction === true) config.codexClientCompaction = true;
@@ -673,7 +686,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
673
686
  // because coordinated Codex writes acquire the Codex write lock N before the config mutation
674
687
  // lock C — awaiting N while still holding C would invert that order.
675
688
  const desktopSwitchApply: CodexDesktopSwitchApply = desktopSwitchesChanged
676
- ? await applyCodexDesktopSwitches(config)
689
+ ? await applyCodexConfigInjection(config)
677
690
  : { applied: false, reason: "not_requested", retryable: false };
678
691
  const codexDesktopSwitches = describeCodexDesktopSwitches(config, desktopSwitchApply);
679
692
  const catalogRefreshPending = catalogRefresh
@@ -695,7 +708,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
695
708
  codexClientCompaction: clientCompactionIsEnabled,
696
709
  codexDesktopSwitches,
697
710
  compactionRouting: config.compactionRouting ?? null,
698
- codexMainAccountHardLock: config.codexMainAccountHardLock === true,
711
+ codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
699
712
  mainAccountHardLock: getMainAccountHardLockStatus(config),
700
713
  startupHealth: await readStartupHealth(config),
701
714
  });
@@ -735,12 +748,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
735
748
  }
736
749
 
737
750
  if (url.pathname === "/api/update/check" && req.method === "GET") {
738
- const { checkForUpdate, normalizeUpdateChannel } = await import("../../update/job");
751
+ const { normalizeUpdateChannel } = await import("../../update/job");
752
+ const { packageRefresh } = await import("../../update/refresh-scheduler");
739
753
  const rawTag = url.searchParams.get("tag");
740
754
  if (rawTag && rawTag !== "latest" && rawTag !== "preview") {
741
755
  return jsonResponse({ error: "tag must be latest or preview" }, 400);
742
756
  }
743
- return jsonResponse(checkForUpdate(normalizeUpdateChannel(rawTag)));
757
+ return jsonResponse(await (deps.checkPackageUpdate ?? packageRefresh.check)(normalizeUpdateChannel(rawTag)));
744
758
  }
745
759
 
746
760
  if (url.pathname === "/api/update/run" && req.method === "POST") {
@@ -754,7 +768,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
754
768
  return jsonResponse({ error: "restart boolean is required" }, 400);
755
769
  }
756
770
  try {
757
- return jsonResponse({ ok: true, job: startUpdateJob(normalizeUpdateChannel(body.tag as string | undefined), body.restart !== false) });
771
+ const channel = normalizeUpdateChannel(body.tag as string | undefined);
772
+ const { packageRefresh } = await import("../../update/refresh-scheduler");
773
+ const checked = await (deps.checkPackageUpdate ?? packageRefresh.check)(channel);
774
+ return jsonResponse({ ok: true, job: startUpdateJob(channel, body.restart !== false, {
775
+ checkForUpdateFn: () => checked,
776
+ }) });
758
777
  } catch (err) {
759
778
  if (err instanceof UpdateJobError) {
760
779
  return jsonResponse({ error: err.message, code: err.code }, err.status);
@@ -799,7 +818,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
799
818
  if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
800
819
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
801
820
  const body = raw as {
802
- webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
821
+ webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
803
822
  vision?: {
804
823
  model?: unknown;
805
824
  backend?: unknown;
@@ -821,6 +840,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
821
840
  && typeof body.webSearch.streamRoutedModelOutput !== "boolean") {
822
841
  return jsonResponse({ error: "webSearch.streamRoutedModelOutput must be a boolean" }, 400);
823
842
  }
843
+ if (body.webSearch && body.webSearch.enabled !== undefined && typeof body.webSearch.enabled !== "boolean") {
844
+ return jsonResponse({ error: "webSearch.enabled must be a boolean" }, 400);
845
+ }
824
846
  if (body.vision && body.vision.backend !== undefined
825
847
  && body.vision.backend !== null && body.vision.backend !== "openai" && body.vision.backend !== "anthropic"
826
848
  && body.vision.backend !== "routed") {
@@ -884,6 +906,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
884
906
  : normalizeVisionReasoningForModel(model, sourceReasoning);
885
907
  }
886
908
 
909
+ // Read BEFORE the mutation below: the Codex-side key follows the switch, and a flip is the
910
+ // only case that owes a config.toml rewrite.
911
+ const webSearchEnabledBefore = config.webSearchSidecar?.enabled !== false;
887
912
  if (body.webSearch) {
888
913
  const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
889
914
  // Validate against the backend the caller SUBMITTED, across the whole
@@ -979,6 +1004,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
979
1004
  if (body.webSearch.streamRoutedModelOutput) webSearchCandidate.streamRoutedModelOutput = true;
980
1005
  else delete webSearchCandidate.streamRoutedModelOutput;
981
1006
  }
1007
+ if (typeof body.webSearch.enabled === "boolean") {
1008
+ // `true` is the default — drop the key so a disable/re-enable cycle does not rewrite the
1009
+ // file, exactly like the Vision master switch.
1010
+ if (body.webSearch.enabled) delete webSearchCandidate.enabled;
1011
+ else webSearchCandidate.enabled = false;
1012
+ }
982
1013
  config.webSearchSidecar = webSearchCandidate;
983
1014
  }
984
1015
  if (body.vision) {
@@ -1009,6 +1040,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1009
1040
  }
1010
1041
  }
1011
1042
  saveConfigPreservingClaudeCode(config);
1043
+ // The sidecar's own switch is only half of what "off" means: Codex keeps declaring its native
1044
+ // hosted `web_search` tool until its own mode says otherwise, and the tool a client advertises
1045
+ // is the one the model reaches for — which is why an MCP search server needs the client-side
1046
+ // key off too. The injection owns that root key, and a write that cannot happen is reported
1047
+ // (the Desktop switches' contract) instead of being stored as if it had.
1048
+ const webSearchEnabledChanged = typeof body.webSearch?.enabled === "boolean"
1049
+ && webSearchEnabledBefore !== (config.webSearchSidecar?.enabled !== false);
1050
+ const codexWebSearch: CodexDesktopSwitchApply = webSearchEnabledChanged
1051
+ ? await applyCodexConfigInjection(config)
1052
+ : { applied: false, reason: "not_requested", retryable: false };
1012
1053
  const ws = config.webSearchSidecar ?? {};
1013
1054
  const vision = await sidecarVisionResponseSettings(config);
1014
1055
  const savedWebSearchCandidates = await webSearchCandidateRows(config);
@@ -1021,6 +1062,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1021
1062
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
1022
1063
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
1023
1064
  },
1065
+ codexWebSearch,
1024
1066
  vision: publicVisionSidecarSettings(config, vision),
1025
1067
  visionModels: vision.models,
1026
1068
  // Echoed for the same reason GET always carries it: the dashboard rebuilds
@@ -1,4 +1,6 @@
1
1
  import type { OcxConfig } from "../../types";
2
+ import type { Channel } from "../../update/index";
3
+ import type { UpdateCheckResult } from "../../update/job";
2
4
  import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
3
5
  import type { CodexLogGuardProtectionDeps } from "../../codex/log-guard/protection";
4
6
  import type { CodexLogGuardMaintenanceDeps } from "../../codex/log-guard/maintenance";
@@ -34,6 +36,7 @@ export type RemoteWorkspaceSessionsApi = Pick<RemoteWorkspaceSessionService,
34
36
  export interface ManagementApiDeps {
35
37
  /** Read-only process-local aggregate metrics; absent keeps the scrape route unavailable. */
36
38
  requestMetrics?: RequestMetricsSnapshotter;
39
+ checkPackageUpdate?: (channel: Channel) => Promise<UpdateCheckResult>;
37
40
  remoteWorkspaceHub?: RemoteWorkspaceHubApi;
38
41
  remoteWorkspaceSessions?: RemoteWorkspaceSessionsApi;
39
42
  /** The listener retains and awaits teardown only after this optional subsystem activates. */