@bitkyc08/opencodex 2.10.2 → 2.11.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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -17,18 +17,20 @@
17
17
  * Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
18
18
  * 011 (Claude Code), 012 (Grok).
19
19
  */
20
- import { readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
20
+ import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
21
21
  import { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
22
+ import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
22
23
  import { injectGrokConfig, stripGrokConfig, type GrokInjectModel } from "../../grok/inject";
23
24
  import { inspectGrokConfig } from "../../grok/inspect";
24
25
  import { grokConfigPath } from "../../grok/status";
25
26
  import { assertNativeTeardownOwned } from "../../integrations/native/ownership-preflight";
27
+ import type { CodexNativeRestoreResult } from "../../codex/inject";
26
28
  import type { OcxConfig } from "../../types";
27
29
  import { jsonResponse } from "../auth-cors";
28
30
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
29
31
  import type { ManagementContext } from "./context";
30
32
 
31
- export type NativeIntegrationClientId = "claude" | "grok" | "codex";
33
+ export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
32
34
 
33
35
  /** Every reason this module can decline, in one place (audit r3 #6). */
34
36
  export type NativeRefusalReason =
@@ -36,13 +38,17 @@ export type NativeRefusalReason =
36
38
  | "orphaned_marker"
37
39
  | "home_mismatch"
38
40
  | "config_busy"
39
- | "write_failed";
41
+ | "write_failed"
42
+ | "metadata_unreadable"
43
+ | "cleanup_incomplete"
44
+ | "desired_state_changed";
40
45
 
41
46
  export interface NativeStatus {
42
47
  clientId: NativeIntegrationClientId;
43
48
  state: "absent" | "current" | "unsafe";
44
49
  installed: boolean;
45
50
  configPath: string;
51
+ desiredEnabled: boolean;
46
52
  /**
47
53
  * Set when a disable would be refused right now. ADVISORY: the file can
48
54
  * change before the PUT, which re-checks and whose answer is authoritative.
@@ -61,8 +67,10 @@ export interface NativeToggleEnvelope {
61
67
  changed: boolean;
62
68
  state: NativeStatus["state"];
63
69
  message: string;
70
+ desiredEnabled: boolean;
64
71
  /** Present when the outcome needs more than success/failure to be honest. */
65
72
  reason?: string;
73
+ artifacts?: CodexNativeRestoreResult["artifacts"];
66
74
  }
67
75
 
68
76
  export interface NativeRefusalEnvelope {
@@ -71,21 +79,66 @@ export interface NativeRefusalEnvelope {
71
79
  clientId: NativeIntegrationClientId;
72
80
  reason: NativeRefusalReason;
73
81
  message: string;
82
+ /** Present on every post-commit refusal; absent only for pre-commit refusals. */
83
+ desiredEnabled?: boolean;
84
+ residualPaths?: string[];
74
85
  }
75
86
 
87
+ export type NativePostCommitRefusalEnvelope = Omit<NativeRefusalEnvelope, "desiredEnabled"> & {
88
+ desiredEnabled: boolean;
89
+ };
90
+
76
91
  function refusal(
77
92
  status: number,
78
93
  clientId: NativeIntegrationClientId,
79
94
  reason: NativeRefusalReason,
80
95
  message: string,
96
+ extra: Pick<NativeRefusalEnvelope, "desiredEnabled" | "residualPaths"> = {},
81
97
  ): Response {
82
98
  return jsonResponse({
83
99
  error: status >= 500 ? "native integration change failed" : "native integration change refused",
84
100
  code: status >= 500 ? "native_integration_failed" : "native_integration_refused",
85
- clientId, reason, message,
101
+ clientId, reason, message, ...extra,
86
102
  } satisfies NativeRefusalEnvelope, status);
87
103
  }
88
104
 
105
+ function postCommitRefusal(
106
+ status: number,
107
+ clientId: NativeIntegrationClientId,
108
+ reason: NativeRefusalReason,
109
+ message: string,
110
+ extra: Pick<NativePostCommitRefusalEnvelope, "desiredEnabled" | "residualPaths">,
111
+ ): Response {
112
+ return jsonResponse({
113
+ error: status >= 500 ? "native integration change failed" : "native integration change refused",
114
+ code: status >= 500 ? "native_integration_failed" : "native_integration_refused",
115
+ clientId, reason, message, ...extra,
116
+ } satisfies NativePostCommitRefusalEnvelope, status);
117
+ }
118
+
119
+ function desktopStatus(config: ManagementContext["config"]): NativeStatus {
120
+ const seen = inspectDesktop3pConfigLibrary({
121
+ appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
122
+ });
123
+ const state: NativeStatus["state"] = seen.kind === "gateway_ours"
124
+ ? "current"
125
+ : seen.kind === "unsafe" || seen.kind === "broken" ? "unsafe" : "absent";
126
+ const disableBlocked = seen.kind === "unsafe" || seen.kind === "broken" || seen.kind === "foreign"
127
+ ? {
128
+ reason: seen.kind === "unsafe" && seen.reason === "metadata_unreadable" ? "metadata_unreadable" as const : "write_failed" as const,
129
+ message: "Claude Desktop configuration cannot be changed safely.",
130
+ }
131
+ : null;
132
+ return {
133
+ clientId: "claude-desktop",
134
+ state,
135
+ installed: seen.kind !== "not_installed",
136
+ configPath: seen.libraryPath,
137
+ desiredEnabled: config.clientIntegrations?.["claude-desktop"] !== false,
138
+ disableBlocked,
139
+ };
140
+ }
141
+
89
142
  /** Absent means ON: the six read sites all treat only an explicit `false` as off. */
90
143
  export function claudeCodeEnabled(config: ManagementContext["config"]): boolean {
91
144
  return config.claudeCode?.enabled !== false;
@@ -98,17 +151,30 @@ function claudeStatus(config: ManagementContext["config"], configPath: string):
98
151
  // The surface exists wherever the proxy does; there is no separate install.
99
152
  installed: true,
100
153
  configPath,
154
+ desiredEnabled: claudeCodeEnabled(config),
101
155
  // Nothing can refuse this disable: no external file, no shared teardown.
102
156
  disableBlocked: null,
103
157
  };
104
158
  }
105
159
 
160
+ function codexStatus(config: ManagementContext["config"], configPath: string): NativeStatus {
161
+ const desiredEnabled = config.clientIntegrations?.codex !== false;
162
+ return {
163
+ clientId: "codex",
164
+ state: desiredEnabled ? "current" : "absent",
165
+ installed: true,
166
+ configPath,
167
+ desiredEnabled,
168
+ disableBlocked: null,
169
+ };
170
+ }
171
+
106
172
  /**
107
173
  * Grok's GET row (030 §field table). `disableBlocked` is ADVISORY — the file
108
174
  * can change before the PUT, which re-checks with the same inspector and whose
109
175
  * answer is authoritative.
110
176
  */
111
- function grokStatus(): NativeStatus {
177
+ function grokStatus(config: ManagementContext["config"]): NativeStatus {
112
178
  const seen = inspectGrokConfig();
113
179
  let disableBlocked: NativeStatus["disableBlocked"] = null;
114
180
  if (seen.kind === "orphaned_marker") {
@@ -137,6 +203,7 @@ function grokStatus(): NativeStatus {
137
203
  state,
138
204
  installed: seen.kind !== "not_installed",
139
205
  configPath: grokConfigPath(),
206
+ desiredEnabled: config.clientIntegrations?.grok !== false,
140
207
  disableBlocked,
141
208
  };
142
209
  }
@@ -260,9 +327,19 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
260
327
  const port = runtime?.port ?? ctx.config.port;
261
328
  const { syncModelsToCodex } = await import("../../codex/sync");
262
329
  const applied = await syncModelsToCodex(port);
330
+ if (applied.status === "skipped") {
331
+ return jsonResponse({
332
+ ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
333
+ state: "absent",
334
+ desiredEnabled: enabled,
335
+ message: "Codex integration is OFF; enable did not change Codex.",
336
+ reason: "apply_incomplete",
337
+ } satisfies NativeToggleEnvelope);
338
+ }
263
339
  return jsonResponse({
264
340
  ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
265
341
  state: applied.ok ? "current" : "absent",
342
+ desiredEnabled: enabled,
266
343
  message: applied.ok
267
344
  ? "Codex now routes through opencodex"
268
345
  : `Codex intent saved, but applying it did not complete: ${applied.message}`,
@@ -273,17 +350,28 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
273
350
  }
274
351
 
275
352
  // OFF. Restore the native path; the proxy keeps serving every other client.
353
+ if (durable && persisted.status === "unchanged") {
354
+ const { classifyNativeRoutedResidue } = await import("../../codex/native-residue");
355
+ if (classifyNativeRoutedResidue().kind === "clean") {
356
+ return jsonResponse({
357
+ ok: true, clientId: "codex", changed: false, state: "absent", desiredEnabled: false,
358
+ message: "Codex integration is already OFF and native; no Codex files changed.",
359
+ } satisfies NativeToggleEnvelope);
360
+ }
361
+ }
276
362
  const { restoreNativeCodexAsync } = await import("../../codex/inject");
277
- const restored = await restoreNativeCodexAsync();
363
+ const restored = await restoreNativeCodexAsync({ revalidateDesiredState: true });
278
364
  return jsonResponse({
279
365
  ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
280
366
  state: restored.success ? "absent" : "unsafe",
367
+ desiredEnabled: enabled,
281
368
  message: restored.success
282
369
  ? "Codex restored to its native path; the proxy is still serving other clients"
283
370
  : `Codex intent saved, but restoring the native path did not complete: ${restored.message}`,
284
371
  ...(restored.success
285
372
  ? (durable ? {} : { reason: "not_durable" })
286
373
  : { reason: "restore_incomplete" }),
374
+ artifacts: restored.artifacts,
287
375
  } satisfies NativeToggleEnvelope);
288
376
  })();
289
377
  try {
@@ -361,6 +449,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
361
449
  );
362
450
  }
363
451
  const durable = persisted.ok;
452
+ const desiredEnabled = durable ? loadConfig().clientIntegrations?.grok !== false : enabled;
364
453
 
365
454
  if (!enabled) {
366
455
  /*
@@ -369,24 +458,25 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
369
458
  * gated: writing our own fence tears nothing down.
370
459
  */
371
460
  const owned = assertNativeTeardownOwned();
372
- if (!owned.ok) return refusal(409, "grok", "home_mismatch", owned.message);
461
+ if (!owned.ok) return postCommitRefusal(409, "grok", "home_mismatch", owned.message, { desiredEnabled });
373
462
 
374
463
  const result = stripGrokConfig();
375
464
  if (result.skippedReason === "no-grok-home") {
376
- return refusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE);
465
+ return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
377
466
  }
378
467
  if (result.skippedReason === "orphaned-marker" || !result.ok) {
379
468
  // Orphaned can still arrive between the preflight and the strip; the
380
469
  // writer refuses it correctly and we map the refusal, never a retry lie.
381
470
  return result.skippedReason === "orphaned-marker"
382
- ? refusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE)
383
- : refusal(500, "grok", "write_failed", result.message);
471
+ ? postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled })
472
+ : postCommitRefusal(500, "grok", "write_failed", result.message, { desiredEnabled });
384
473
  }
385
474
  // The writer's own read IS the last read within this synchronous
386
475
  // operation (012 Rev 3 N4): strip removed the fence, so absent.
387
476
  return jsonResponse({
388
477
  ok: true, clientId: "grok", changed: result.changed,
389
478
  state: "absent",
479
+ desiredEnabled,
390
480
  message: result.changed
391
481
  ? "Grok integration disabled — the opencodex block was removed. Re-enabling regenerates it from the current model list."
392
482
  : "Grok integration is already off",
@@ -427,13 +517,13 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
427
517
  } catch (error) {
428
518
  // A catalog failure must never write an empty fence (syncGrokConfig
429
519
  // guards this; the route inherits the rule). Nothing was written.
430
- return refusal(500, "grok", "write_failed",
431
- `The model catalog is unavailable, so nothing was written (${error instanceof Error ? error.message : String(error)}). Try again once provider discovery recovers.`);
520
+ return postCommitRefusal(500, "grok", "write_failed",
521
+ `The model catalog is unavailable, so nothing was written (${error instanceof Error ? error.message : String(error)}). Try again once provider discovery recovers.`, { desiredEnabled });
432
522
  }
433
523
 
434
524
  const recheck = inspectGrokConfig();
435
- if (recheck.kind === "not_installed") return refusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE);
436
- if (recheck.kind === "orphaned_marker") return refusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE);
525
+ if (recheck.kind === "not_installed") return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
526
+ if (recheck.kind === "orphaned_marker") return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
437
527
 
438
528
  const inject = deps.injectGrokConfig ?? injectGrokConfig;
439
529
  const result = inject(port, models, {
@@ -455,7 +545,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
455
545
  const after = inspectGrokConfig();
456
546
  switch (after.kind) {
457
547
  case "orphaned_marker":
458
- return refusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE);
548
+ return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
459
549
  case "present":
460
550
  // A well-formed fence arrived from elsewhere between the strip and
461
551
  // this read (`ocx ensure`, another proxy, a hand edit). It is not
@@ -464,14 +554,16 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
464
554
  return jsonResponse({
465
555
  ok: true, clientId: "grok", changed: result.changed,
466
556
  state: "current", reason: "non_loopback_superseded",
557
+ desiredEnabled,
467
558
  message: "opencodex is bound to a non-loopback address, so this request did not write a block — but a well-formed opencodex block is present in the Grok config, written by something else. The card shows what is on disk.",
468
559
  } satisfies NativeToggleEnvelope);
469
560
  case "not_installed":
470
- return refusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE);
561
+ return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
471
562
  case "absent":
472
563
  return jsonResponse({
473
564
  ok: true, clientId: "grok", changed: result.changed,
474
565
  state: "absent", reason: "non_loopback_removed",
566
+ desiredEnabled,
475
567
  message: "opencodex is bound to a non-loopback address, so Grok cannot be auto-registered. The previously generated block was removed because it pointed at a loopback address that no longer serves.",
476
568
  } satisfies NativeToggleEnvelope);
477
569
  default: {
@@ -484,17 +576,18 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
484
576
  }
485
577
 
486
578
  if (result.skippedReason === "no-grok-home") {
487
- return refusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE);
579
+ return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
488
580
  }
489
581
  if (result.skippedReason === "orphaned-marker") {
490
- return refusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE);
582
+ return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
491
583
  }
492
584
  if (!result.ok) {
493
- return refusal(500, "grok", "write_failed", result.message);
585
+ return postCommitRefusal(500, "grok", "write_failed", result.message, { desiredEnabled });
494
586
  }
495
587
  return jsonResponse({
496
588
  ok: true, clientId: "grok", changed: result.changed,
497
589
  state: "current",
590
+ desiredEnabled,
498
591
  message: result.changed ? "Grok integration enabled — the opencodex block was regenerated from the current model list." : "Grok integration is already on",
499
592
  } satisfies NativeToggleEnvelope);
500
593
  })();
@@ -505,13 +598,93 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
505
598
  }
506
599
  }
507
600
 
601
+ let claudeDesktopToggleFlight: Promise<Response> | null = null;
602
+
603
+ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Response> {
604
+ if (claudeDesktopToggleFlight) {
605
+ return refusal(409, "claude-desktop", "config_busy",
606
+ "Another Claude Desktop change is already in flight. Nothing was written — try again in a moment.");
607
+ }
608
+ claudeDesktopToggleFlight = (async (): Promise<Response> => {
609
+ let body: { enabled?: unknown };
610
+ try {
611
+ body = await readManagementJsonBody(ctx.req);
612
+ } catch (error) {
613
+ rethrowManagementBodyTooLarge(error);
614
+ return jsonResponse({ error: "invalid JSON body" }, 400);
615
+ }
616
+ if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
617
+
618
+ const { setIntegrationEnabled } = await import("../../codex/desired-state");
619
+ const persisted = setIntegrationEnabled("claude-desktop", body.enabled);
620
+ if (!persisted.ok) {
621
+ return refusal(persisted.retryable ? 409 : 500, "claude-desktop", persisted.retryable ? "config_busy" : "write_failed", persisted.message);
622
+ }
623
+ const desiredEnabled = loadConfig().clientIntegrations?.["claude-desktop"] !== false;
624
+ const current = loadConfig();
625
+ const fingerprint = current.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
626
+
627
+ if (!body.enabled) {
628
+ const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint });
629
+ if (removed.kind === "cleanup_incomplete") {
630
+ return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
631
+ "Claude Desktop now points at standard mode, but credential cleanup is incomplete.",
632
+ { desiredEnabled, residualPaths: removed.residualPaths ?? [] });
633
+ }
634
+ if (!removed.ok) {
635
+ return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
636
+ "Claude Desktop configuration could not be changed safely.", { desiredEnabled });
637
+ }
638
+ return jsonResponse({
639
+ ok: true, clientId: "claude-desktop", changed: removed.changed, state: "absent", desiredEnabled,
640
+ message: removed.changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
641
+ } satisfies NativeToggleEnvelope);
642
+ }
643
+
644
+ const fetchModels = ctx.deps.fetchAllModels ?? defaultFetchAllModels;
645
+ try {
646
+ const fetched = await fetchModels(current);
647
+ const latest = loadConfig();
648
+ const latestDesiredEnabled = latest.clientIntegrations?.["claude-desktop"] !== false;
649
+ if (!latestDesiredEnabled) {
650
+ return postCommitRefusal(409, "claude-desktop", "desired_state_changed",
651
+ "Claude Desktop enable was cancelled because the desired state changed to off.", { desiredEnabled: latestDesiredEnabled });
652
+ }
653
+ const routed = filterCatalogVisibleModels(fetched, latest).map(model => ({
654
+ provider: model.provider, id: model.id, contextWindow: model.contextWindow,
655
+ }));
656
+ const runtime = (ctx.deps.readRuntimePort ?? readRuntimePort)(process.pid);
657
+ const result = (ctx.deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
658
+ runtime?.port ?? latest.port,
659
+ [...visibleNativeSlugs(latest)],
660
+ routed,
661
+ latest.apiKeys?.[0]?.key,
662
+ "static",
663
+ latest.claudeCode?.desktopProfile,
664
+ );
665
+ if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
666
+ return jsonResponse({
667
+ ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
668
+ message: "Claude Desktop integration enabled.",
669
+ } satisfies NativeToggleEnvelope);
670
+ } catch {
671
+ return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled });
672
+ }
673
+ })();
674
+ try {
675
+ return await claudeDesktopToggleFlight;
676
+ } finally {
677
+ claudeDesktopToggleFlight = null;
678
+ }
679
+ }
680
+
508
681
  export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
509
682
  const { req, url, config, deps } = ctx;
510
683
 
511
684
  if (url.pathname === "/api/native-integrations" && req.method === "GET") {
512
685
  const { getConfigPath } = await import("../../config");
513
686
  return jsonResponse({
514
- clients: [claudeStatus(config, getConfigPath()), grokStatus()],
687
+ clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config, getConfigPath()), desktopStatus(config)],
515
688
  } satisfies NativeStatusListEnvelope);
516
689
  }
517
690
 
@@ -532,6 +705,7 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
532
705
  return jsonResponse({
533
706
  ok: true, clientId: "claude", changed: false,
534
707
  state: enabled ? "current" : "absent",
708
+ desiredEnabled: enabled,
535
709
  message: enabled ? "Claude inbound is already on" : "Claude inbound is already off",
536
710
  } satisfies NativeToggleEnvelope);
537
711
  }
@@ -571,6 +745,7 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
571
745
  return jsonResponse({
572
746
  ok: true, clientId: "claude", changed: true,
573
747
  state: enabled ? "current" : "absent",
748
+ desiredEnabled: enabled,
574
749
  message: enabled ? "Claude inbound enabled" : "Claude inbound disabled",
575
750
  } satisfies NativeToggleEnvelope);
576
751
  }
@@ -583,5 +758,9 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
583
758
  return handleCodexToggle(ctx);
584
759
  }
585
760
 
761
+ if (url.pathname === "/api/native-integrations/claude-desktop" && req.method === "PUT") {
762
+ return handleClaudeDesktopToggle(ctx);
763
+ }
764
+
586
765
  return null;
587
766
  }
@@ -165,6 +165,45 @@ function applyProviderPatchFields(
165
165
  next.liveModels = rawBody.liveModels;
166
166
  touched = true;
167
167
  }
168
+ // The Models page edits the catalog hints in place; keep them on the existing
169
+ // provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
170
+ if (Object.hasOwn(rawBody, "contextWindow")) {
171
+ const value = rawBody.contextWindow;
172
+ if (value === null) {
173
+ delete next.contextWindow;
174
+ // `Number.isInteger(1e100)` is true, so an integer check alone admits a value that
175
+ // serializes into the catalog as an enormous number and can make Codex reject the whole
176
+ // file. Safe-integer is the real bound for something that ends up in a JSON int field.
177
+ } else if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
178
+ next.contextWindow = value;
179
+ } else {
180
+ return { error: "contextWindow must be a positive safe integer or null" };
181
+ }
182
+ touched = true;
183
+ }
184
+ if (Object.hasOwn(rawBody, "modelContextWindows")) {
185
+ const value = rawBody.modelContextWindows;
186
+ if (value === null) {
187
+ delete next.modelContextWindows;
188
+ } else {
189
+ if (!isPlainRecord(value)) return { error: "modelContextWindows must be a plain object or null" };
190
+ const windows: Record<string, number> = { ...(next.modelContextWindows ?? {}) };
191
+ for (const [model, window] of Object.entries(value)) {
192
+ if (!model.trim()) return { error: "modelContextWindows keys must be nonblank model ids" };
193
+ if (window === null) {
194
+ delete windows[model];
195
+ continue;
196
+ }
197
+ if (typeof window !== "number" || !Number.isSafeInteger(window) || window <= 0) {
198
+ return { error: "modelContextWindows values must be positive safe integers or null" };
199
+ }
200
+ windows[model] = window;
201
+ }
202
+ if (Object.keys(windows).length > 0) next.modelContextWindows = windows;
203
+ else delete next.modelContextWindows;
204
+ }
205
+ touched = true;
206
+ }
168
207
 
169
208
  // headers is the one object-valued field in the mask. PATCH semantics merge it
170
209
  // shallowly into the existing block so a single fingerprint header can be added
@@ -249,6 +288,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
249
288
  allowPrivateNetwork: p.allowPrivateNetwork === true,
250
289
  liveModels: p.liveModels !== false,
251
290
  models: p.models ?? [],
291
+ contextWindow: p.contextWindow,
292
+ modelContextWindows: p.modelContextWindows,
252
293
  authMode: p.authMode,
253
294
  apiKeyTransport: p.apiKeyTransport,
254
295
  disabled: p.disabled === true,