@bitkyc08/opencodex 2.64.0 → 2.65.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.
- package/README.md +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-DGLte6IR.js +50 -0
- package/gui/dist/assets/Tray-D4bA1vff.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-SggB6t3z.js → index-Bi1K37Wl.js} +12 -12
- package/gui/dist/assets/tray-data-BZXhB3qz.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-CpuDF3ci.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
|
@@ -19,7 +19,8 @@ import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-sta
|
|
|
19
19
|
* 011 (Claude Code), 012 (Grok).
|
|
20
20
|
*/
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
-
import {
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { getConfigPath, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
23
24
|
import { readRuntimePort } from "../../config/process-state";
|
|
24
25
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
|
|
25
26
|
import { getCodexHome } from "../../codex/paths";
|
|
@@ -30,11 +31,15 @@ import {
|
|
|
30
31
|
applyDesktopFirstParty,
|
|
31
32
|
captureDesktopFirstPartyRollback,
|
|
32
33
|
inspectDesktopFirstParty,
|
|
34
|
+
observeClaudeDesktopMode,
|
|
33
35
|
recordClaudeDesktopMode,
|
|
34
36
|
removeDesktopFirstParty,
|
|
35
37
|
resolveClaudeDesktopApplyMode,
|
|
36
38
|
type ClaudeDesktopMode,
|
|
37
39
|
} from "../../claude/desktop-first-party";
|
|
40
|
+
import { FIRST_PARTY_ACCOUNT_RISK } from "../../claude/desktop-risk";
|
|
41
|
+
import type { DesktopPickerStatus } from "../../claude/desktop-picker";
|
|
42
|
+
import { pickerPreferenceOn, runPickerTransition } from "./claude-desktop-picker-routes";
|
|
38
43
|
import { projectGrokCatalog } from "../../grok/catalog";
|
|
39
44
|
import { injectGrokConfig, stripGrokConfig } from "../../grok/inject";
|
|
40
45
|
import { inspectGrokConfig } from "../../grok/inspect";
|
|
@@ -149,7 +154,7 @@ function desktopStatus(config: ManagementContext["config"]): NativeStatus {
|
|
|
149
154
|
: null;
|
|
150
155
|
// First-party mode lives in Claude Code's settings.json, not in Desktop's library. A leftover
|
|
151
156
|
// gateway profile still counts as current (it is what Desktop is actually running).
|
|
152
|
-
const firstParty = resolveClaudeDesktopApplyMode(config) === "first-party" && gatewayState === "absent"
|
|
157
|
+
const firstParty = resolveClaudeDesktopApplyMode(config, observeClaudeDesktopMode(config)) === "first-party" && gatewayState === "absent"
|
|
153
158
|
? inspectDesktopFirstParty(config)
|
|
154
159
|
: null;
|
|
155
160
|
const state: NativeStatus["state"] = firstParty
|
|
@@ -183,11 +188,26 @@ function claudeStatus(config: ManagementContext["config"], configPath: string):
|
|
|
183
188
|
};
|
|
184
189
|
}
|
|
185
190
|
|
|
191
|
+
/**
|
|
192
|
+
* The state the latest Codex toggle in this process reported, keyed by the intent it
|
|
193
|
+
* applied. Intent alone cannot describe an apply or restore that did not complete: the
|
|
194
|
+
* PUT reports `absent` for a skipped or failed enable and `unsafe` for an incomplete
|
|
195
|
+
* restore, and the next GET must not turn those into `current` or `absent`. It applies
|
|
196
|
+
* only while the persisted intent still matches; a restart re-runs startup convergence.
|
|
197
|
+
*/
|
|
198
|
+
let codexLastToggle: { desiredEnabled: boolean; state: NativeStatus["state"] } | null = null;
|
|
199
|
+
|
|
200
|
+
function rememberCodexToggle(desiredEnabled: boolean, state: NativeStatus["state"]): NativeStatus["state"] {
|
|
201
|
+
codexLastToggle = { desiredEnabled, state };
|
|
202
|
+
return state;
|
|
203
|
+
}
|
|
204
|
+
|
|
186
205
|
function codexStatus(config: ManagementContext["config"], configPath: string): NativeStatus {
|
|
187
206
|
const desiredEnabled = config.clientIntegrations?.codex !== false;
|
|
207
|
+
const reported = codexLastToggle?.desiredEnabled === desiredEnabled ? codexLastToggle.state : null;
|
|
188
208
|
return {
|
|
189
209
|
clientId: "codex",
|
|
190
|
-
state: desiredEnabled ? "current" : "absent",
|
|
210
|
+
state: reported ?? (desiredEnabled ? "current" : "absent"),
|
|
191
211
|
installed: true,
|
|
192
212
|
configPath,
|
|
193
213
|
desiredEnabled,
|
|
@@ -356,7 +376,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
356
376
|
if (applied.status === "skipped") {
|
|
357
377
|
return jsonResponse({
|
|
358
378
|
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
359
|
-
state: "absent",
|
|
379
|
+
state: rememberCodexToggle(true, "absent"),
|
|
360
380
|
desiredEnabled: enabled,
|
|
361
381
|
message: "Codex integration is OFF; enable did not change Codex.",
|
|
362
382
|
reason: "apply_incomplete",
|
|
@@ -364,7 +384,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
364
384
|
}
|
|
365
385
|
return jsonResponse({
|
|
366
386
|
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
367
|
-
state: applied.ok ? "current" : "absent",
|
|
387
|
+
state: rememberCodexToggle(true, applied.ok ? "current" : "absent"),
|
|
368
388
|
desiredEnabled: enabled,
|
|
369
389
|
message: applied.ok
|
|
370
390
|
? "Codex now routes through opencodex"
|
|
@@ -379,6 +399,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
379
399
|
if (durable && persisted.status === "unchanged") {
|
|
380
400
|
const { classifyNativeRoutedResidue } = await import("../../codex/native-residue");
|
|
381
401
|
if (classifyNativeRoutedResidue().kind === "clean") {
|
|
402
|
+
rememberCodexToggle(false, "absent");
|
|
382
403
|
return jsonResponse({
|
|
383
404
|
ok: true, clientId: "codex", changed: false, state: "absent", desiredEnabled: false,
|
|
384
405
|
message: "Codex integration is already OFF and native; no Codex files changed.",
|
|
@@ -390,7 +411,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
390
411
|
const restored = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
391
412
|
return jsonResponse({
|
|
392
413
|
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
393
|
-
state: restored.success ? "absent" : "unsafe",
|
|
414
|
+
state: rememberCodexToggle(false, restored.success ? "absent" : "unsafe"),
|
|
394
415
|
desiredEnabled: enabled,
|
|
395
416
|
message: restored.success
|
|
396
417
|
? `Codex restored to its native path; the proxy is still serving other clients. ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`
|
|
@@ -635,6 +656,24 @@ export function firstPartyRefusalMessage(
|
|
|
635
656
|
}
|
|
636
657
|
}
|
|
637
658
|
|
|
659
|
+
/** One line for a toggle message about picker mode after a first-party enable. */
|
|
660
|
+
function pickerStateNote(picker: DesktopPickerStatus): string {
|
|
661
|
+
if (picker.reason === "active" || picker.reason === "restart_required") {
|
|
662
|
+
return "Picker mode is on: fully quit and reopen Claude Desktop to see OpenCodex models in the Code tab.";
|
|
663
|
+
}
|
|
664
|
+
if (picker.reason === "trust_pending" || picker.reason === "trust_declined") {
|
|
665
|
+
return `Picker mode is waiting for the keychain step: run ${picker.hint ?? "ocx claude desktop picker trust"}.`;
|
|
666
|
+
}
|
|
667
|
+
return `Picker mode is off (${picker.reason}).`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Empty unless turning picker mode off left something behind. */
|
|
671
|
+
function pickerCleanupNote(picker: DesktopPickerStatus): string {
|
|
672
|
+
return picker.residual?.length
|
|
673
|
+
? `Picker mode cleanup is incomplete (${picker.residual.join(", ")}); run ocx claude desktop picker off.`
|
|
674
|
+
: "";
|
|
675
|
+
}
|
|
676
|
+
|
|
638
677
|
/** Record which Desktop mode is applied; `false` when the config file could not be updated. */
|
|
639
678
|
function persistDesktopModeMarker(desktopMode: ClaudeDesktopMode): boolean {
|
|
640
679
|
const outcome = mutatePersistedConfig(persisted => recordClaudeDesktopMode(persisted, desktopMode));
|
|
@@ -668,104 +707,125 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
668
707
|
const fingerprint = current.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
669
708
|
|
|
670
709
|
if (!body.enabled) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
|
|
679
|
-
"Claude Desktop now points at standard mode, but credential cleanup is incomplete.",
|
|
680
|
-
{ desiredEnabled, residualPaths: removed.residualPaths ?? [] });
|
|
681
|
-
}
|
|
682
|
-
if (!removed.ok) {
|
|
683
|
-
return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
|
|
684
|
-
"Claude Desktop configuration could not be changed safely.", { desiredEnabled });
|
|
685
|
-
}
|
|
686
|
-
const changed = removed.changed || firstPartyRemoved.changed;
|
|
687
|
-
return jsonResponse({
|
|
688
|
-
ok: true, clientId: "claude-desktop", changed, state: "absent", desiredEnabled,
|
|
689
|
-
message: changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
|
|
690
|
-
} satisfies NativeToggleEnvelope);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (resolveClaudeDesktopApplyMode(current) === "first-party") {
|
|
694
|
-
const rollback = captureDesktopFirstPartyRollback(current);
|
|
695
|
-
const applied = applyDesktopFirstParty(current);
|
|
696
|
-
if (!applied.ok) {
|
|
697
|
-
const reason = applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? applied.reason : "write_failed";
|
|
698
|
-
return postCommitRefusal(applied.reason === "unreadable" || applied.reason === "ca_unavailable" ? 500 : 409, "claude-desktop", reason,
|
|
699
|
-
firstPartyRefusalMessage(applied.reason, applied.path), { desiredEnabled });
|
|
700
|
-
}
|
|
701
|
-
const library = inspectDesktop3pConfigLibrary({ appliedFingerprint: fingerprint });
|
|
702
|
-
let gatewayRemoved = false;
|
|
703
|
-
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
|
|
704
|
-
const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint, replaceWhileEnabled: true });
|
|
705
|
-
if (!removed.ok && !removed.changed && applied.changed && !rollback()) {
|
|
706
|
-
return postCommitRefusal(500, "claude-desktop", "write_failed",
|
|
707
|
-
"Gateway cleanup and first-party settings rollback did not complete.", { desiredEnabled });
|
|
710
|
+
// Picker mode goes first: stop terminating claude.ai, drop its profile and trust.
|
|
711
|
+
return await runPickerTransition(current, async ops => {
|
|
712
|
+
const pickerOff = await ops.disableLocked({ persist: false });
|
|
713
|
+
const firstPartyRemoved = removeDesktopFirstParty();
|
|
714
|
+
if (!firstPartyRemoved.ok) {
|
|
715
|
+
return postCommitRefusal(409, "claude-desktop", "write_failed",
|
|
716
|
+
`Claude Code settings could not be read (${firstPartyRemoved.path}); the first-party proxy env was left in place.`, { desiredEnabled });
|
|
708
717
|
}
|
|
709
|
-
const
|
|
710
|
-
? " First-party is active but its mode marker was not saved." : "";
|
|
718
|
+
const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint });
|
|
711
719
|
if (removed.kind === "cleanup_incomplete") {
|
|
712
720
|
return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
|
|
713
|
-
"Claude Desktop now points at standard mode, but
|
|
721
|
+
"Claude Desktop now points at standard mode, but credential cleanup is incomplete.",
|
|
714
722
|
{ desiredEnabled, residualPaths: removed.residualPaths ?? [] });
|
|
715
723
|
}
|
|
716
724
|
if (!removed.ok) {
|
|
717
725
|
return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
|
|
718
|
-
"
|
|
726
|
+
"Claude Desktop configuration could not be changed safely.", { desiredEnabled });
|
|
719
727
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
728
|
+
const changed = removed.changed || firstPartyRemoved.changed;
|
|
729
|
+
return jsonResponse({
|
|
730
|
+
ok: true, clientId: "claude-desktop", changed, state: "absent", desiredEnabled,
|
|
731
|
+
message: [
|
|
732
|
+
changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
|
|
733
|
+
pickerCleanupNote(pickerOff),
|
|
734
|
+
].filter(Boolean).join(" "),
|
|
735
|
+
} satisfies NativeToggleEnvelope);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (resolveClaudeDesktopApplyMode(current, observeClaudeDesktopMode(current)) === "first-party") {
|
|
740
|
+
// The whole switch runs under the picker lock, in today's order: env first, then gateway cleanup.
|
|
741
|
+
return await runPickerTransition(current, async ops => {
|
|
742
|
+
const rollback = captureDesktopFirstPartyRollback(current);
|
|
743
|
+
const applied = applyDesktopFirstParty(current);
|
|
744
|
+
if (!applied.ok) {
|
|
745
|
+
const reason = applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? applied.reason : "write_failed";
|
|
746
|
+
return postCommitRefusal(applied.reason === "unreadable" || applied.reason === "ca_unavailable" ? 500 : 409, "claude-desktop", reason,
|
|
747
|
+
firstPartyRefusalMessage(applied.reason, applied.path), { desiredEnabled });
|
|
748
|
+
}
|
|
749
|
+
const library = inspectDesktop3pConfigLibrary({ appliedFingerprint: fingerprint });
|
|
750
|
+
let gatewayRemoved = false;
|
|
751
|
+
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
|
|
752
|
+
const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint, replaceWhileEnabled: true });
|
|
753
|
+
if (!removed.ok && !removed.changed && applied.changed && !rollback()) {
|
|
754
|
+
return postCommitRefusal(500, "claude-desktop", "write_failed",
|
|
755
|
+
"Gateway cleanup and first-party settings rollback did not complete.", { desiredEnabled });
|
|
756
|
+
}
|
|
757
|
+
const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker("first-party")
|
|
758
|
+
? " First-party is active but its mode marker was not saved." : "";
|
|
759
|
+
if (removed.kind === "cleanup_incomplete") {
|
|
760
|
+
return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
|
|
761
|
+
"Claude Desktop now points at standard mode, but gateway credential cleanup is incomplete; the first-party connection remains active." + partialModeWarning,
|
|
762
|
+
{ desiredEnabled, residualPaths: removed.residualPaths ?? [] });
|
|
763
|
+
}
|
|
764
|
+
if (!removed.ok) {
|
|
765
|
+
return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
|
|
766
|
+
"The gateway profile could not be removed safely; the mode switch is incomplete." + partialModeWarning, { desiredEnabled });
|
|
767
|
+
}
|
|
768
|
+
gatewayRemoved = removed.changed;
|
|
769
|
+
}
|
|
770
|
+
const modeSaved = persistDesktopModeMarker("first-party");
|
|
771
|
+
const changed = applied.changed || gatewayRemoved;
|
|
772
|
+
// Picker mode is on by default in first-party; only a committed mode turns it on.
|
|
773
|
+
const picker = modeSaved && pickerPreferenceOn(loadConfig())
|
|
774
|
+
? await ops.enableLocked({ persist: false, context: "server" })
|
|
775
|
+
: null;
|
|
776
|
+
return jsonResponse({
|
|
777
|
+
ok: true, clientId: "claude-desktop", changed, state: "current", desiredEnabled,
|
|
778
|
+
message: [
|
|
779
|
+
changed
|
|
780
|
+
? "Claude Desktop integration enabled (first-party). Fully quit and reopen Claude Desktop."
|
|
781
|
+
: "Claude Desktop integration is already on.",
|
|
782
|
+
FIRST_PARTY_ACCOUNT_RISK.message,
|
|
783
|
+
picker ? pickerStateNote(picker) : "",
|
|
784
|
+
modeSaved ? "" : "The first-party mode marker could not be saved to config; status may report the mode as unsaved.",
|
|
785
|
+
].filter(Boolean).join(" "),
|
|
786
|
+
} satisfies NativeToggleEnvelope);
|
|
787
|
+
});
|
|
733
788
|
}
|
|
734
789
|
|
|
735
790
|
const fetchModels = ctx.deps.fetchAllModels ?? defaultFetchAllModels;
|
|
736
791
|
try {
|
|
737
792
|
const fetched = await fetchModels(current);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
793
|
+
return await runPickerTransition(current, async ops => {
|
|
794
|
+
const latest = loadConfig();
|
|
795
|
+
const latestDesiredEnabled = latest.clientIntegrations?.["claude-desktop"] !== false;
|
|
796
|
+
if (!latestDesiredEnabled) {
|
|
797
|
+
return postCommitRefusal(409, "claude-desktop", "desired_state_changed",
|
|
798
|
+
"Claude Desktop enable was cancelled because the desired state changed to off.", { desiredEnabled: latestDesiredEnabled });
|
|
799
|
+
}
|
|
800
|
+
// Picker mode belongs to first-party: stop terminating claude.ai before the gateway lands.
|
|
801
|
+
const pickerOff = await ops.disableLocked({ persist: false });
|
|
802
|
+
const routed = filterCatalogVisibleModels(fetched, latest).map(model => ({
|
|
803
|
+
provider: model.provider, id: model.id, contextWindow: model.contextWindow,
|
|
804
|
+
}));
|
|
805
|
+
const runtime = (ctx.deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
806
|
+
const result = (ctx.deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
807
|
+
runtime?.port ?? latest.port,
|
|
808
|
+
[...desktopVisibleNativeSlugs(latest)],
|
|
809
|
+
routed,
|
|
810
|
+
latest.apiKeys?.[0]?.key,
|
|
811
|
+
"static",
|
|
812
|
+
latest.claudeCode?.desktopProfile,
|
|
813
|
+
nativeContextLimits(latest),
|
|
814
|
+
);
|
|
815
|
+
if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
|
|
816
|
+
const committed = persistCommittedDesktopGateway(ctx.config, latest.claudeCode?.desktopProfile, result.fingerprint);
|
|
817
|
+
const stateWarning = committed.ok ? "" : " The committed gateway mode/profile state was not saved.";
|
|
818
|
+
const removed = removeDesktopFirstParty();
|
|
819
|
+
if (!removed.ok) return postCommitRefusal(500, "claude-desktop", "write_failed", "Gateway applied, but first-party settings cleanup did not complete." + stateWarning, { desiredEnabled: latestDesiredEnabled });
|
|
820
|
+
return jsonResponse({
|
|
821
|
+
ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
|
|
822
|
+
message: [
|
|
823
|
+
"Claude Desktop integration enabled.",
|
|
824
|
+
stateWarning,
|
|
825
|
+
pickerCleanupNote(pickerOff),
|
|
826
|
+
].filter(Boolean).join(" "),
|
|
827
|
+
} satisfies NativeToggleEnvelope);
|
|
828
|
+
});
|
|
769
829
|
} catch {
|
|
770
830
|
return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled });
|
|
771
831
|
}
|
|
@@ -777,14 +837,30 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
777
837
|
}
|
|
778
838
|
}
|
|
779
839
|
|
|
840
|
+
function persistedIntentConfig(snapshot: ManagementContext["config"]): ManagementContext["config"] {
|
|
841
|
+
// Only the per-client intent is refreshed; every other field keeps the snapshot the
|
|
842
|
+
// rest of this request already reasons about.
|
|
843
|
+
try {
|
|
844
|
+
// No config file means no persisted intent: loadConfig would return defaults, which
|
|
845
|
+
// must not override the in-memory intent this request carries.
|
|
846
|
+
if (!existsSync(getConfigPath())) return snapshot;
|
|
847
|
+
return { ...snapshot, clientIntegrations: loadConfig().clientIntegrations };
|
|
848
|
+
} catch {
|
|
849
|
+
return snapshot;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
780
853
|
export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
781
854
|
const { req, url, config, deps } = ctx;
|
|
782
855
|
|
|
783
856
|
if (url.pathname === "/api/native-integrations" && req.method === "GET") {
|
|
784
|
-
const { getConfigPath } = await import("../../config");
|
|
785
857
|
const codexConfigPath = join(getCodexHome(), "config.toml");
|
|
858
|
+
// The Codex, Grok and Claude Desktop toggles persist intent independently of the
|
|
859
|
+
// server's startup config snapshot. Read that intent once so the next dashboard
|
|
860
|
+
// refresh reflects a completed PUT; fall back to the snapshot if the file is unreadable.
|
|
861
|
+
const persisted = persistedIntentConfig(config);
|
|
786
862
|
return jsonResponse({
|
|
787
|
-
clients: [claudeStatus(config, getConfigPath()), grokStatus(
|
|
863
|
+
clients: [claudeStatus(config, getConfigPath()), grokStatus(persisted), codexStatus(persisted, codexConfigPath), desktopStatus(persisted)],
|
|
788
864
|
} satisfies NativeStatusListEnvelope);
|
|
789
865
|
}
|
|
790
866
|
|
|
@@ -43,6 +43,8 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
|
43
43
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
44
44
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
45
45
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
46
|
+
import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
|
|
47
|
+
import { resolveDevinApiBaseUrl } from "../../oauth/devin/api-base";
|
|
46
48
|
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
47
49
|
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
48
50
|
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
@@ -448,6 +450,13 @@ function applyProviderPatchFields(
|
|
|
448
450
|
}
|
|
449
451
|
touched = true;
|
|
450
452
|
}
|
|
453
|
+
if (Object.hasOwn(rawBody, "fastEnabled")) {
|
|
454
|
+
const value = rawBody.fastEnabled;
|
|
455
|
+
if (value === null) delete next.fastEnabled;
|
|
456
|
+
else if (typeof value === "boolean") next.fastEnabled = value;
|
|
457
|
+
else return { error: "fastEnabled must be a boolean or null" };
|
|
458
|
+
touched = true;
|
|
459
|
+
}
|
|
451
460
|
if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
|
|
452
461
|
if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
|
|
453
462
|
if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
|
|
@@ -934,12 +943,15 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
934
943
|
retainModels: p.retainModels,
|
|
935
944
|
omitReasoningEffortWithToolsModels: p.omitReasoningEffortWithToolsModels,
|
|
936
945
|
upstreamHttpVersion: p.upstreamHttpVersion,
|
|
937
|
-
|
|
946
|
+
// As configured: unset on canonical `openai` means upstream WebSocket, so never coerce to false.
|
|
947
|
+
upstreamWebsocket: p.upstreamWebsocket,
|
|
938
948
|
authMode: p.authMode,
|
|
939
949
|
apiKeyTransport: p.apiKeyTransport,
|
|
940
950
|
disabled: p.disabled === true,
|
|
941
951
|
codexAccountMode: providerCodexAccountMode(name, p),
|
|
942
952
|
...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
|
|
953
|
+
// Only opt-in Fast lanes (Anthropic fast mode bills usage credits) get a dashboard switch.
|
|
954
|
+
...(getProviderRegistryEntry(name)?.fastOptIn === true ? { fastOptIn: { enabled: p.fastEnabled === true } } : {}),
|
|
943
955
|
discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
|
|
944
956
|
...(name === "openai" && isCanonicalOpenAiForwardProvider(p)
|
|
945
957
|
? { entitlement: getCodexModelEntitlementStatus(config) }
|
|
@@ -1203,6 +1215,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1203
1215
|
const submittedModelDisplayNames = Object.hasOwn(prov, "modelDisplayNames");
|
|
1204
1216
|
const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
|
|
1205
1217
|
const submittedUpstreamWebsocket = Object.hasOwn(prov, "upstreamWebsocket");
|
|
1218
|
+
const submittedFastEnabled = Object.hasOwn(prov, "fastEnabled");
|
|
1206
1219
|
// Same trap, one more field: DeepSeek carries a registry default of `true` for
|
|
1207
1220
|
// annotateEmptyToolOutputs, so enrichment cannot distinguish "the client omitted it"
|
|
1208
1221
|
// from "the registry supplied it" either. Without this sample, an unrelated edit that
|
|
@@ -1277,6 +1290,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1277
1290
|
if (!submittedUpstreamWebsocket && existing?.upstreamWebsocket !== undefined) {
|
|
1278
1291
|
prov.upstreamWebsocket = existing.upstreamWebsocket;
|
|
1279
1292
|
}
|
|
1293
|
+
// The Models-page Fast switch is PATCH-owned and the provider form never sends it, so an
|
|
1294
|
+
// unrelated full save must not silently turn an opted-in Anthropic Fast lane back off.
|
|
1295
|
+
const liveFastEnabled = config.providers[name]?.fastEnabled;
|
|
1296
|
+
if (!submittedFastEnabled && liveFastEnabled !== undefined) prov.fastEnabled = liveFastEnabled;
|
|
1280
1297
|
// The form sends none of the compatibility settings either (#5563). Read the live row rather
|
|
1281
1298
|
// than `existing`, like the alias overlays below: a PATCH that saved one of them while DNS
|
|
1282
1299
|
// validation awaited must not be undone. Nothing is carried to a new destination.
|
|
@@ -1602,10 +1619,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1602
1619
|
}
|
|
1603
1620
|
const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
|
|
1604
1621
|
const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
1605
|
-
const snapshot =
|
|
1622
|
+
const snapshot = prov.authMode === "oauth"
|
|
1606
1623
|
? await getValidAccessTokenSnapshot(name).catch(() => undefined)
|
|
1607
1624
|
: undefined;
|
|
1608
|
-
const apiKey = snapshot?.accessToken
|
|
1625
|
+
const apiKey = prov.authMode === "oauth" ? snapshot?.accessToken : await resolveModelsAuthToken(name, prov);
|
|
1609
1626
|
if (prov.authMode === "oauth" && !apiKey) {
|
|
1610
1627
|
return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
|
|
1611
1628
|
}
|
|
@@ -1630,6 +1647,29 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1630
1647
|
message: `Connected. ${live.models.length} models.`,
|
|
1631
1648
|
});
|
|
1632
1649
|
}
|
|
1650
|
+
if (prov.adapter === "devin") {
|
|
1651
|
+
const started = Date.now();
|
|
1652
|
+
const configuredBase = name === "devin" ? getProviderRegistryEntry(name)?.baseUrl ?? prov.baseUrl : prov.baseUrl;
|
|
1653
|
+
const destination = resolveDevinApiBaseUrl(snapshot?.apiBaseUrl ?? configuredBase);
|
|
1654
|
+
const liveResult = await fetchDevinUsableModels({
|
|
1655
|
+
apiKey: apiKey ?? "",
|
|
1656
|
+
baseUrl: destination,
|
|
1657
|
+
});
|
|
1658
|
+
const latencyMs = Date.now() - started;
|
|
1659
|
+
if (!liveResult.ok) {
|
|
1660
|
+
return jsonResponse({
|
|
1661
|
+
ok: false,
|
|
1662
|
+
latencyMs,
|
|
1663
|
+
error: `devin discovery ${liveResult.error}${liveResult.detail ? `: ${liveResult.detail}` : ""}`,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
return jsonResponse({
|
|
1667
|
+
ok: true,
|
|
1668
|
+
latencyMs,
|
|
1669
|
+
models: liveResult.models.length,
|
|
1670
|
+
message: `Connected. ${liveResult.models.length} models.`,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1633
1673
|
if (prov.adapter === "qoder") {
|
|
1634
1674
|
const started = Date.now();
|
|
1635
1675
|
const profile = resolveQoderProfile(prov.baseUrl);
|
|
@@ -1656,7 +1696,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1656
1696
|
if (antigravity && !project) {
|
|
1657
1697
|
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
1658
1698
|
}
|
|
1659
|
-
const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name
|
|
1699
|
+
const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name, {
|
|
1700
|
+
oauthApiBaseUrl: snapshot?.apiBaseUrl,
|
|
1701
|
+
});
|
|
1660
1702
|
const discovery = resolveProviderModelDiscovery(name, prov);
|
|
1661
1703
|
const started = Date.now();
|
|
1662
1704
|
try {
|
|
@@ -51,6 +51,8 @@ export type ExemptionReason =
|
|
|
51
51
|
| "interactive-preview"
|
|
52
52
|
/** Unreachable in the live dispatch order; delete rather than expose. */
|
|
53
53
|
| "dead"
|
|
54
|
+
/** Desktop shell internal display-state POST; the CLI has no app updater state to publish. */
|
|
55
|
+
| "desktop-internal"
|
|
54
56
|
/**
|
|
55
57
|
* A verb is owed but belongs to a later work-phase. BOUNDED: requires `owner` and
|
|
56
58
|
* `ownerDoc`, and the parity test asserts that tracked doc exists and names the route.
|
|
@@ -152,6 +154,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
152
154
|
{ method: "POST", path: "/api/anthropic/reset-grants/consume", module: "server/management/anthropic-reset-grant-routes", mutates: true, exempt: { reason: "session-only", why: "Spending a Claude reset grant requires the gui-session principal (anthropic-reset-grant-routes.ts handleConsume); the admin token is refused." } },
|
|
153
155
|
{ method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true },
|
|
154
156
|
{ method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true },
|
|
157
|
+
{ method: "PUT", path: "/api/claude-desktop/first-party-bindings", module: "server/management/agent-settings-routes", mutates: true },
|
|
158
|
+
// server/management/claude-desktop-picker-routes
|
|
159
|
+
{ method: "GET", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: false },
|
|
160
|
+
{ method: "PUT", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: true },
|
|
155
161
|
{ method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
|
|
156
162
|
{ method: "PUT", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: true },
|
|
157
163
|
{ method: "PUT", path: "/api/grok/selection", module: "server/management/agent-settings-routes", mutates: true },
|
|
@@ -346,6 +352,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
346
352
|
// server/management/sidebar-routes
|
|
347
353
|
{ method: "GET", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: false },
|
|
348
354
|
{ method: "GET", path: "/api/update/badge", module: "server/management/sidebar-routes", mutates: false },
|
|
355
|
+
{ method: "POST", path: "/api/update/desktop-snapshot",
|
|
356
|
+
module: "server/management/sidebar-routes", mutates: true,
|
|
357
|
+
exempt: { reason: "desktop-internal",
|
|
358
|
+
why: "Only the Tauri shell has signed-updater state to publish; a CLI verb could only forge that state and would not create an operator action." } },
|
|
349
359
|
{ method: "POST", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: true, exempt: { reason: "session-only", why: "User-consent boundary in AGENTS_INSTALL.md: starring spends the user's identity. Must never gain a CLI verb." } },
|
|
350
360
|
// server/management/storage-log-guard-routes
|
|
351
361
|
{ method: "GET", path: "/api/storage/codex-logs", module: "server/management/storage-log-guard-routes", mutates: false },
|
|
@@ -207,6 +207,15 @@ function migrateProfileModelReferences(
|
|
|
207
207
|
]),
|
|
208
208
|
);
|
|
209
209
|
}
|
|
210
|
+
if (claudeCode.intercept?.modelMap) {
|
|
211
|
+
// First-party picker bindings: keys are Anthropic picker ids, only the route follows the rename.
|
|
212
|
+
claudeCode.intercept = {
|
|
213
|
+
...claudeCode.intercept,
|
|
214
|
+
modelMap: Object.fromEntries(
|
|
215
|
+
Object.entries(claudeCode.intercept.modelMap).map(([pickerId, route]) => [pickerId, migrateAgentReference(route)]),
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
210
219
|
config.claudeCode = claudeCode;
|
|
211
220
|
}
|
|
212
221
|
return shouldSyncClaudeAgentDefs;
|
|
@@ -97,6 +97,58 @@ export async function handleSidebarRoutes(ctx: ManagementContext): Promise<Respo
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
if (url.pathname === "/api/update/desktop-snapshot" && req.method === "POST") {
|
|
101
|
+
// Only the native ProxyClient publishes this state; browsers always send Origin.
|
|
102
|
+
if (req.headers.has("origin")) {
|
|
103
|
+
return Response.json({ error: "desktop snapshot does not accept browser-origin requests" }, { status: 403 });
|
|
104
|
+
}
|
|
105
|
+
if (ctx.principal !== "admin-token") {
|
|
106
|
+
return jsonResponse({ error: "desktop snapshot requires admin token" }, 403, req, ctx.config);
|
|
107
|
+
}
|
|
108
|
+
if (req.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
|
|
109
|
+
return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
|
|
110
|
+
}
|
|
111
|
+
const declared = Number(req.headers.get("content-length") ?? "0");
|
|
112
|
+
if (Number.isFinite(declared) && declared > 1024) {
|
|
113
|
+
return jsonResponse({ error: "desktop snapshot too large" }, 413, req, ctx.config);
|
|
114
|
+
}
|
|
115
|
+
const reader = req.body?.getReader();
|
|
116
|
+
if (!reader) return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
|
|
117
|
+
const bytes = new Uint8Array(1024);
|
|
118
|
+
let used = 0;
|
|
119
|
+
try {
|
|
120
|
+
while (true) {
|
|
121
|
+
const part = await reader.read();
|
|
122
|
+
if (part.done) break;
|
|
123
|
+
if (used + part.value.length > bytes.length) {
|
|
124
|
+
await reader.cancel();
|
|
125
|
+
return jsonResponse({ error: "desktop snapshot too large" }, 413, req, ctx.config);
|
|
126
|
+
}
|
|
127
|
+
bytes.set(part.value, used);
|
|
128
|
+
used += part.value.length;
|
|
129
|
+
}
|
|
130
|
+
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used));
|
|
131
|
+
const { desktopBadgeStore } = await import("../../update/desktop-badge");
|
|
132
|
+
if (!desktopBadgeStore.put(JSON.parse(decoded))) {
|
|
133
|
+
return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
|
|
137
|
+
}
|
|
138
|
+
return jsonResponse({ ok: true }, 200, req, ctx.config);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (url.pathname === "/api/update/badge" && req.method === "GET"
|
|
142
|
+
&& url.searchParams.get("surface") === "desktop") {
|
|
143
|
+
const { desktopBadgeStore } = await import("../../update/desktop-badge");
|
|
144
|
+
return jsonResponse(desktopBadgeStore.read(url.searchParams.get("session")), 200, req, ctx.config);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (url.pathname === "/api/update/badge" && req.method === "GET"
|
|
148
|
+
&& url.searchParams.has("surface") && url.searchParams.get("surface") !== "desktop") {
|
|
149
|
+
return jsonResponse({ error: "invalid badge surface" }, 400, req, ctx.config);
|
|
150
|
+
}
|
|
151
|
+
|
|
100
152
|
if (url.pathname === "/api/update/badge" && req.method === "GET") {
|
|
101
153
|
const { readUpdateBadge } = await import("../../update/badge");
|
|
102
154
|
return jsonResponse(readUpdateBadge());
|
|
@@ -76,6 +76,7 @@ import { handleCompanionRoutes } from "./management/companion-routes";
|
|
|
76
76
|
import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
|
|
77
77
|
import { handleIntegrationRoutes } from "./management/integration-routes";
|
|
78
78
|
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
|
|
79
|
+
import { handleClaudeDesktopPickerRoutes } from "./management/claude-desktop-picker-routes";
|
|
79
80
|
import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
|
|
80
81
|
import type { ManagementContext } from "./management/context";
|
|
81
82
|
import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
|
|
@@ -293,6 +294,7 @@ export async function handleManagementAPI(
|
|
|
293
294
|
?? (await handleIntegrationRoutes(ctx))
|
|
294
295
|
?? (await handleNativeIntegrationRoutes(ctx))
|
|
295
296
|
?? (await handleCursorIntegrationRoutes(ctx))
|
|
297
|
+
?? (await handleClaudeDesktopPickerRoutes(ctx))
|
|
296
298
|
?? (await handleAgentSettingsRoutes(ctx))
|
|
297
299
|
?? (await handleCodexPromptRoutes(ctx))
|
|
298
300
|
?? (await handleOauthAccountRoutes(ctx))
|