@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/server/index.ts
CHANGED
|
@@ -69,11 +69,6 @@ import { runDevinProviderMergeStartupMigration } from "../providers/devin-provid
|
|
|
69
69
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
70
70
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
71
71
|
import type { StorageCleanupPolicy } from "../types";
|
|
72
|
-
import {
|
|
73
|
-
MAX_CONFIGURABLE_INBOUND_BODY_BYTES,
|
|
74
|
-
MIN_CONFIGURABLE_INBOUND_BODY_BYTES,
|
|
75
|
-
resolveInboundBodyLimitBytes,
|
|
76
|
-
} from "./request-decompress";
|
|
77
72
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
78
73
|
export {
|
|
79
74
|
clearThreadAccountMap,
|
|
@@ -116,7 +111,7 @@ import {
|
|
|
116
111
|
type RequestLogEntry,
|
|
117
112
|
} from "./request-log";
|
|
118
113
|
import { sessionLaneIdFromRequest } from "./request-log-conversation";
|
|
119
|
-
import {
|
|
114
|
+
import { setUsageLedgerRetention } from "./usage-ledger-retention";
|
|
120
115
|
import { admitHttpWorkflowTurn, workflowDecisionRefusalResponse, type WorkflowRefusalLog } from "./workflow-refusal";
|
|
121
116
|
export {
|
|
122
117
|
addFinalRequestLog,
|
|
@@ -204,9 +199,18 @@ import {
|
|
|
204
199
|
} from "../lib/package-tree-integrity";
|
|
205
200
|
import { detectInstall } from "../update/index";
|
|
206
201
|
import { createServeOptions, type ServerIngress } from "./index/serve-options";
|
|
207
|
-
import {
|
|
202
|
+
import { createClaudeInterceptLifecycle } from "./index/claude-intercept-lifecycle";
|
|
203
|
+
import { inspectStartupOwnership, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
|
|
204
|
+
import { acquireSpendLedgerServerLifecycle, recordFailedStartRollback, type SpendLedgerServerLifecycle } from "./index/spend-ledger-lifecycle";
|
|
205
|
+
export { waitForFailedStartRollback } from "./index/spend-ledger-lifecycle";
|
|
208
206
|
|
|
209
207
|
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
208
|
+
const spendLedgerLifecycle = acquireSpendLedgerServerLifecycle(getConfigDir());
|
|
209
|
+
try { return startServerWithSpendLedgerOwner(port, deps, spendLedgerLifecycle); }
|
|
210
|
+
catch (error) { recordFailedStartRollback(error, spendLedgerLifecycle.releaseAfterFailedStart()); throw error; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartServerDeps, spendLedgerLifecycle: SpendLedgerServerLifecycle): Server<WsData> {
|
|
210
214
|
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
211
215
|
// Captured before loadConfig() starts the optional ACL flight so stop() drains the same dir
|
|
212
216
|
// even if OPENCODEX_HOME changes underneath a long-lived process.
|
|
@@ -299,12 +303,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
299
303
|
registerAppOwnedMemorySweepFallback();
|
|
300
304
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
301
305
|
enforceAppOwnedMemoryBudget();
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
configureSharedSpendLedger(spendPolicyFromConfig(config.spend));
|
|
306
|
+
// Observe-only mode still journals physical sends, so every server owns before configuring.
|
|
307
|
+
spendLedgerLifecycle.configure(config.spend);
|
|
308
|
+
// After ownership: a second server on the same home is refused above, so the process running
|
|
309
|
+
// this line is the only one appending to usage.jsonl and the only one that may compact it.
|
|
310
|
+
setUsageLedgerRetention(config.usageLedgerMaxBytes);
|
|
308
311
|
registerCodexCooldownRecoveryProbeWorker(config);
|
|
309
312
|
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
310
313
|
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
@@ -625,26 +628,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
625
628
|
let server: Server<WsData>;
|
|
626
629
|
let loopbackServer: Server<WsData> | null = null;
|
|
627
630
|
let managementIngressServer: Server<WsData> | null = null;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// stays pure and per-request cheap; the operator is told here instead, once, because a
|
|
631
|
-
// config value that was quietly reduced is exactly the thing they would otherwise debug
|
|
632
|
-
// against the wrong limit.
|
|
633
|
-
const inboundBodyLimitBytes = resolveInboundBodyLimitBytes(config.maxInboundBodyBytes);
|
|
634
|
-
const requestedInboundBodyLimit = config.maxInboundBodyBytes;
|
|
635
|
-
if (requestedInboundBodyLimit !== undefined
|
|
636
|
-
&& requestedInboundBodyLimit > 0
|
|
637
|
-
&& requestedInboundBodyLimit !== inboundBodyLimitBytes) {
|
|
638
|
-
console.warn(
|
|
639
|
-
`[server] maxInboundBodyBytes=${requestedInboundBodyLimit} is outside the supported range `
|
|
640
|
-
+ `[${MIN_CONFIGURABLE_INBOUND_BODY_BYTES}, ${MAX_CONFIGURABLE_INBOUND_BODY_BYTES}]; `
|
|
641
|
-
+ `using ${inboundBodyLimitBytes} bytes.`,
|
|
642
|
-
);
|
|
643
|
-
}
|
|
631
|
+
const claudeIntercept = createClaudeInterceptLifecycle<WsData>();
|
|
632
|
+
const inboundBodyLimitBytes = resolveInboundBodyLimitWithWarning(config);
|
|
644
633
|
|
|
645
634
|
function ingressForServer(requestServer: Server<WsData>): ServerIngress {
|
|
646
635
|
if (requestServer === loopbackServer) return "unauthenticated-loopback";
|
|
647
636
|
if (requestServer === managementIngressServer) return "hub-management";
|
|
637
|
+
if (claudeIntercept.ownsListener(requestServer)) return "claude-intercept";
|
|
648
638
|
return "public";
|
|
649
639
|
}
|
|
650
640
|
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
@@ -703,7 +693,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
703
693
|
get remoteWorkspaceStopping() { return remoteWorkspaceStopping; },
|
|
704
694
|
});
|
|
705
695
|
|
|
706
|
-
server = Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost });
|
|
696
|
+
server = spendLedgerLifecycle.track(Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost }));
|
|
707
697
|
|
|
708
698
|
// Both binds are one startup transaction (#1102). If the loopback bind fails after the
|
|
709
699
|
// public one succeeded, leaving the public listener up would strand it: the CLI's port
|
|
@@ -711,11 +701,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
711
701
|
// accumulating listeners. Roll back and rethrow the original error instead.
|
|
712
702
|
if (loopbackListenerPort !== null) {
|
|
713
703
|
try {
|
|
714
|
-
loopbackServer = Bun.serve<WsData>({
|
|
704
|
+
loopbackServer = spendLedgerLifecycle.track(Bun.serve<WsData>({
|
|
715
705
|
...serveOptions,
|
|
716
706
|
port: loopbackListenerPort,
|
|
717
707
|
hostname: "127.0.0.1",
|
|
718
|
-
});
|
|
708
|
+
}));
|
|
719
709
|
} catch (error) {
|
|
720
710
|
try {
|
|
721
711
|
// startServer is synchronous, so this rollback cannot await. Bun begins closing the
|
|
@@ -731,11 +721,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
731
721
|
}
|
|
732
722
|
if (managementIngressPort !== null) {
|
|
733
723
|
try {
|
|
734
|
-
managementIngressServer = Bun.serve<WsData>({
|
|
724
|
+
managementIngressServer = spendLedgerLifecycle.track(Bun.serve<WsData>({
|
|
735
725
|
...serveOptions,
|
|
736
726
|
port: managementIngressPort,
|
|
737
727
|
hostname: "127.0.0.1",
|
|
738
|
-
});
|
|
728
|
+
}));
|
|
739
729
|
} catch (error) {
|
|
740
730
|
// Preserve the management bind failure while synchronously initiating rollback of every
|
|
741
731
|
// listener already opened in this startup transaction. startServer must not become async.
|
|
@@ -746,6 +736,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
746
736
|
throw new AuxiliaryListenerBindError("hub.managementIngress", managementIngressPort, "127.0.0.1", error);
|
|
747
737
|
}
|
|
748
738
|
}
|
|
739
|
+
claudeIntercept.start({
|
|
740
|
+
config, publicPort: server.port ?? listenPort, requestedPort: listenPort, maxRequestBodySize: inboundBodyLimitBytes,
|
|
741
|
+
dispatch: (req, requestServer) => serveOptions.fetch(req, requestServer),
|
|
742
|
+
});
|
|
749
743
|
} catch (error) {
|
|
750
744
|
unregisterQuotaAutoRefresh?.();
|
|
751
745
|
userCostOverlayReconciler?.stop();
|
|
@@ -774,6 +768,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
774
768
|
...(managementIngressRef
|
|
775
769
|
? [() => managementIngressRef.stop(closeActiveConnections)]
|
|
776
770
|
: []),
|
|
771
|
+
() => claudeIntercept.stop(),
|
|
777
772
|
async () => { await remoteWorkspaceShutdown?.(); },
|
|
778
773
|
async () => {
|
|
779
774
|
try {
|
|
@@ -783,16 +778,17 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
783
778
|
}
|
|
784
779
|
},
|
|
785
780
|
],
|
|
786
|
-
async
|
|
781
|
+
async listenersStopped => {
|
|
787
782
|
try {
|
|
788
783
|
await backgroundLifecycle.release();
|
|
789
784
|
await releaseNativeMainStartupLifecycle(server);
|
|
790
785
|
} finally {
|
|
791
786
|
// icacls.exe from hardenConfigDir() holds the config dir open; a caller that
|
|
792
787
|
// removes the dir right after stop() settles would hit EPERM/EBUSY on Windows
|
|
793
|
-
// otherwise.
|
|
794
|
-
//
|
|
795
|
-
|
|
788
|
+
// otherwise. Config hardening still flushes when an earlier release rejects. The
|
|
789
|
+
// spend owner is retained when a listener stop failed because the socket may live.
|
|
790
|
+
try { if (listenersStopped) spendLedgerLifecycle.release(); }
|
|
791
|
+
finally { await flushConfigDirHardening(startupConfigDir); }
|
|
796
792
|
}
|
|
797
793
|
},
|
|
798
794
|
);
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -357,12 +357,12 @@ export function getServerListenPort(): number | undefined {
|
|
|
357
357
|
* caller sees the same result before a replacement binds the port. Swallowing it would let
|
|
358
358
|
* `drainAndShutdown` report success while a socket is still held.
|
|
359
359
|
*
|
|
360
|
-
* `always` runs after the listeners regardless of their outcome
|
|
361
|
-
* reported set rather than replacing it.
|
|
360
|
+
* `always` runs after the listeners regardless of their outcome and receives whether every
|
|
361
|
+
* listener stop succeeded. Its own failure joins the reported set rather than replacing it.
|
|
362
362
|
*/
|
|
363
363
|
export async function runListenerShutdown(
|
|
364
364
|
steps: Array<() => Promise<void>>,
|
|
365
|
-
always: () => Promise<void>,
|
|
365
|
+
always: (listenersStopped: boolean) => Promise<void>,
|
|
366
366
|
): Promise<void> {
|
|
367
367
|
const failures: unknown[] = [];
|
|
368
368
|
// Close admission and start connection-owner cleanup before waiting for any drain.
|
|
@@ -372,7 +372,7 @@ export async function runListenerShutdown(
|
|
|
372
372
|
if (result.status === "rejected") failures.push(result.reason);
|
|
373
373
|
}
|
|
374
374
|
try {
|
|
375
|
-
await always();
|
|
375
|
+
await always(failures.length === 0);
|
|
376
376
|
} catch (error) {
|
|
377
377
|
failures.push(error);
|
|
378
378
|
}
|
|
@@ -9,6 +9,12 @@ export interface LiveCallBinding {
|
|
|
9
9
|
upstreamCallId: string;
|
|
10
10
|
joinStyle: "frameless-path" | "realtime-query";
|
|
11
11
|
providerName: string;
|
|
12
|
+
/**
|
|
13
|
+
* The model this call settled on when it was created. A join names no model
|
|
14
|
+
* of its own, so this is what a per-key scope is judged against on reconnect
|
|
15
|
+
* — without it, a call created for one model could be rejoined as another.
|
|
16
|
+
*/
|
|
17
|
+
model?: string;
|
|
12
18
|
accountId?: string;
|
|
13
19
|
chatgptAccountId?: string;
|
|
14
20
|
keyedCredentialDigest?: string;
|
package/src/server/live.ts
CHANGED
|
@@ -44,7 +44,8 @@ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
|
|
|
44
44
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
45
45
|
import type { OcxConfig } from "../types";
|
|
46
46
|
import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
|
|
47
|
-
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
47
|
+
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "./auth-cors";
|
|
48
|
+
import { admissionScopeDenial } from "./admission-model-scope";
|
|
48
49
|
import type { RequestLogContext } from "./request-log";
|
|
49
50
|
import { codexLogAccountId } from "./responses";
|
|
50
51
|
import type { AdmissionLease } from "../lib/admission";
|
|
@@ -550,6 +551,62 @@ async function readRequestBodyCapped(req: Request, maxBytes: number): Promise<Ar
|
|
|
550
551
|
}
|
|
551
552
|
}
|
|
552
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Who is asking and for which live model, as far as a per-key scope is concerned.
|
|
556
|
+
*
|
|
557
|
+
* This path has no router to resolve a destination, so the model is read where
|
|
558
|
+
* the client states it — the call-create session, or a standalone socket's own
|
|
559
|
+
* query — and the provider is whichever OpenAI upstream this relay settles on.
|
|
560
|
+
* `model` is undefined when nobody stated one: a call-create that sends no
|
|
561
|
+
* session model, or a join onto a call this compatibility path never recorded.
|
|
562
|
+
*/
|
|
563
|
+
export interface LiveScopeDestination {
|
|
564
|
+
admission?: DataPlaneAdmission;
|
|
565
|
+
model: string | undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The live model a call-create body names, or undefined when it names none.
|
|
570
|
+
*
|
|
571
|
+
* Both inbound shapes carry it at `session.model` — JSON directly, multipart in
|
|
572
|
+
* the `session` field — and this path relays the body upstream unchanged, so
|
|
573
|
+
* the string is the destination rather than a selector the proxy rewrites. A
|
|
574
|
+
* body that states nothing readable leaves the model to the upstream, which is
|
|
575
|
+
* a destination no model list can describe.
|
|
576
|
+
*/
|
|
577
|
+
export async function liveCallCreateModel(body: ArrayBuffer, contentType: string): Promise<string | undefined> {
|
|
578
|
+
try {
|
|
579
|
+
let session: unknown;
|
|
580
|
+
if (contentType.toLowerCase().includes("multipart/form-data")) {
|
|
581
|
+
const form = await new Response(body, { headers: { "content-type": contentType } }).formData();
|
|
582
|
+
const raw = form.get("session");
|
|
583
|
+
session = typeof raw === "string" ? JSON.parse(raw) : undefined;
|
|
584
|
+
} else {
|
|
585
|
+
session = (JSON.parse(new TextDecoder().decode(body)) as { session?: unknown } | null)?.session;
|
|
586
|
+
}
|
|
587
|
+
const model = (session as { model?: unknown } | null | undefined)?.model;
|
|
588
|
+
return typeof model === "string" && model.trim() ? model.trim() : undefined;
|
|
589
|
+
} catch {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The model a sideband upgrade is for, or undefined when the request names none.
|
|
596
|
+
*
|
|
597
|
+
* A standalone session states it in the query it forwards. A join names nothing
|
|
598
|
+
* of its own: the call it attaches to chose a model at create time, and only a
|
|
599
|
+
* recorded binding can say which. This compatibility path keeps no such record,
|
|
600
|
+
* so a native join is undefined here and the external path in `audio-live.ts`
|
|
601
|
+
* supplies the model its binding stored.
|
|
602
|
+
*/
|
|
603
|
+
export function liveSidebandModel(target: LiveSidebandTarget): string | undefined {
|
|
604
|
+
if (target.style === "realtime-standalone" || target.style === "frameless-standalone") {
|
|
605
|
+
return new URLSearchParams(target.query).get("model")?.trim() || undefined;
|
|
606
|
+
}
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
|
|
553
610
|
/**
|
|
554
611
|
* Resolve OpenAI/ChatGPT auth + headers for live HTTP or sideband WebSocket relays.
|
|
555
612
|
* Shared by call-create and sideband so pool token override stays consistent.
|
|
@@ -559,6 +616,7 @@ export async function resolveLiveRelay(
|
|
|
559
616
|
config: OcxConfig,
|
|
560
617
|
logCtx: RequestLogContext,
|
|
561
618
|
turnAdmissionLease?: AdmissionLease,
|
|
619
|
+
destination?: LiveScopeDestination,
|
|
562
620
|
): Promise<LiveRelayTarget | Response> {
|
|
563
621
|
try {
|
|
564
622
|
validateForwardAdmissionCredential(req.headers, config);
|
|
@@ -624,8 +682,22 @@ export async function resolveLiveRelay(
|
|
|
624
682
|
|
|
625
683
|
// Client protocol headers first so provider/auth headers below always win on conflict.
|
|
626
684
|
const headers: Record<string, string> = clientProtocolHeaders(req.headers);
|
|
685
|
+
const scopedModel = destination?.model;
|
|
627
686
|
if (forward) {
|
|
628
687
|
const { provider } = forward;
|
|
688
|
+
// The upstream is settled here and voice bills it for whatever model this
|
|
689
|
+
// request carries. Refuse before any of it is sent, and give back the probe
|
|
690
|
+
// lease the resolution took. A join states no model and this path keeps no
|
|
691
|
+
// record of the call it attaches to, so a key with a model list is refused
|
|
692
|
+
// there rather than admitted against an assumed default.
|
|
693
|
+
const denial = admissionScopeDenial(config, destination?.admission, scopedModel, {
|
|
694
|
+
providerName: forward.providerName,
|
|
695
|
+
modelId: scopedModel,
|
|
696
|
+
});
|
|
697
|
+
if (denial) {
|
|
698
|
+
forward.releaseProbeLease?.();
|
|
699
|
+
return denial;
|
|
700
|
+
}
|
|
629
701
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
630
702
|
for (const [name, value] of forward.headers) headers[name] = value;
|
|
631
703
|
logCtx.model = "gpt-live";
|
|
@@ -640,6 +712,11 @@ export async function resolveLiveRelay(
|
|
|
640
712
|
if (forwardAuthError) return forwardAuthError;
|
|
641
713
|
if (candidates.keyed) {
|
|
642
714
|
const { provider, apiKey, providerName } = candidates.keyed;
|
|
715
|
+
const denial = admissionScopeDenial(config, destination?.admission, scopedModel, {
|
|
716
|
+
providerName,
|
|
717
|
+
modelId: scopedModel,
|
|
718
|
+
});
|
|
719
|
+
if (denial) return denial;
|
|
643
720
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
644
721
|
headers.authorization = `Bearer ${apiKey}`;
|
|
645
722
|
logCtx.provider = providerName;
|
|
@@ -663,13 +740,17 @@ export async function handleLive(
|
|
|
663
740
|
config: OcxConfig,
|
|
664
741
|
logCtx: RequestLogContext,
|
|
665
742
|
turnAdmissionLease?: AdmissionLease,
|
|
743
|
+
admission?: DataPlaneAdmission,
|
|
666
744
|
): Promise<Response> {
|
|
667
745
|
const inboundContentType = req.headers.get("content-type") ?? "application/octet-stream";
|
|
668
746
|
const inboundBodyOrError = await readRequestBodyCapped(req, LIVE_REQUEST_MAX_BYTES);
|
|
669
747
|
if (inboundBodyOrError instanceof Response) return inboundBodyOrError;
|
|
670
748
|
const inboundBody = inboundBodyOrError;
|
|
671
749
|
|
|
672
|
-
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease
|
|
750
|
+
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease, {
|
|
751
|
+
admission,
|
|
752
|
+
model: await liveCallCreateModel(inboundBody, inboundContentType),
|
|
753
|
+
});
|
|
673
754
|
if (relay instanceof Response) return relay;
|
|
674
755
|
|
|
675
756
|
const headers: Record<string, string> = { ...relay.headers };
|
|
@@ -766,8 +847,12 @@ export async function resolveLiveSidebandUpgrade(
|
|
|
766
847
|
logCtx: RequestLogContext,
|
|
767
848
|
target: LiveSidebandTarget,
|
|
768
849
|
turnAdmissionLease?: AdmissionLease,
|
|
850
|
+
admission?: DataPlaneAdmission,
|
|
769
851
|
): Promise<{ headers: Record<string, string>; upstreamWsUrl: string; recordOutcome?: LiveRelayTarget["recordOutcome"] } | Response> {
|
|
770
|
-
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease
|
|
852
|
+
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease, {
|
|
853
|
+
admission,
|
|
854
|
+
model: liveSidebandModel(target),
|
|
855
|
+
});
|
|
771
856
|
if (relay instanceof Response) return relay;
|
|
772
857
|
return {
|
|
773
858
|
headers: relay.headers,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import type { CatalogModel } from "../../codex/catalog";
|
|
@@ -131,6 +132,17 @@ function persistDesktopProfileField(
|
|
|
131
132
|
return { ok: true };
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
async function persistDesktopModeField(
|
|
136
|
+
config: OcxConfig,
|
|
137
|
+
desktopMode: "first-party" | "gateway",
|
|
138
|
+
): Promise<{ ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" }> {
|
|
139
|
+
const { recordClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
140
|
+
const outcome = mutatePersistedConfig(persisted => recordClaudeDesktopMode(persisted, desktopMode));
|
|
141
|
+
if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
|
|
142
|
+
recordClaudeDesktopMode(config, desktopMode);
|
|
143
|
+
return { ok: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
export function grokApplyFlightSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
|
|
135
147
|
return {
|
|
136
148
|
currentBytes: grokApplyFlight?.bytes ?? 0,
|
|
@@ -1002,7 +1014,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1002
1014
|
if (url.pathname === "/api/claude-desktop/apply" && req.method === "POST") {
|
|
1003
1015
|
try {
|
|
1004
1016
|
// #859: the CLI delegates here so the registry is built in the serving
|
|
1005
|
-
// process.
|
|
1017
|
+
// process. `mode` selects the Desktop mode: `first-party` (default when the
|
|
1018
|
+
// install has no gateway profile) or `gateway`; the legacy gateway shapes
|
|
1019
|
+
// (static/hybrid/discovery) still select gateway with that library layout.
|
|
1006
1020
|
let mode: "static" | "hybrid" | "discovery" = "static";
|
|
1007
1021
|
let parsed: unknown;
|
|
1008
1022
|
try {
|
|
@@ -1011,12 +1025,17 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1011
1025
|
rethrowManagementBodyTooLarge(error);
|
|
1012
1026
|
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
1013
1027
|
}
|
|
1028
|
+
const { resolveClaudeDesktopApplyMode, applyDesktopFirstParty, captureDesktopFirstPartyRollback, removeDesktopFirstParty } = await import("../../claude/desktop-first-party");
|
|
1014
1029
|
const requested = (parsed as { mode?: unknown } | null)?.mode;
|
|
1030
|
+
let desktopMode: "first-party" | "gateway" = resolveClaudeDesktopApplyMode(config);
|
|
1015
1031
|
if (requested !== undefined) {
|
|
1016
1032
|
if (requested === "static" || requested === "hybrid" || requested === "discovery") {
|
|
1017
1033
|
mode = requested;
|
|
1034
|
+
desktopMode = "gateway";
|
|
1035
|
+
} else if (requested === "gateway" || requested === "first-party") {
|
|
1036
|
+
desktopMode = requested;
|
|
1018
1037
|
} else {
|
|
1019
|
-
return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
|
|
1038
|
+
return jsonResponse({ error: "mode must be first-party, gateway, static, hybrid, or discovery" }, 400);
|
|
1020
1039
|
}
|
|
1021
1040
|
}
|
|
1022
1041
|
// #859: a delegated CLI apply carries the profile it just saved — the
|
|
@@ -1032,6 +1051,60 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1032
1051
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1033
1052
|
}
|
|
1034
1053
|
}
|
|
1054
|
+
if (desktopMode === "first-party") {
|
|
1055
|
+
const { setIntegrationEnabled } = await import("../../codex/desired-state");
|
|
1056
|
+
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
1057
|
+
if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
|
|
1058
|
+
mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
|
|
1059
|
+
const { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot } = await import("../../claude/desktop-3p");
|
|
1060
|
+
const rollback = captureDesktopFirstPartyRollback(config);
|
|
1061
|
+
const applied = applyDesktopFirstParty(config);
|
|
1062
|
+
if (!applied.ok) {
|
|
1063
|
+
const { firstPartyRefusalMessage } = await import("./native-integration-routes");
|
|
1064
|
+
return jsonResponse({
|
|
1065
|
+
error: firstPartyRefusalMessage(applied.reason, applied.path),
|
|
1066
|
+
code: "claude_desktop_first_party_refused",
|
|
1067
|
+
reason: applied.reason,
|
|
1068
|
+
path: applied.path,
|
|
1069
|
+
}, applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? 409 : 500);
|
|
1070
|
+
}
|
|
1071
|
+
// Remove the gateway only after the replacement env was written.
|
|
1072
|
+
const appliedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
1073
|
+
const library = inspectDesktop3pConfigLibrary({ appliedFingerprint });
|
|
1074
|
+
let gatewayRemoved = false;
|
|
1075
|
+
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
|
|
1076
|
+
const removed = (deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint, replaceWhileEnabled: true });
|
|
1077
|
+
if (!removed.ok) {
|
|
1078
|
+
const restored = removed.changed || !applied.changed || rollback();
|
|
1079
|
+
const modeSaved = !removed.changed || (await persistDesktopModeField(config, "first-party")).ok;
|
|
1080
|
+
const warning = [restored ? "" : "first-party settings rollback did not complete",
|
|
1081
|
+
modeSaved ? "" : "first-party is active but its mode marker was not saved"].filter(Boolean).join("; ");
|
|
1082
|
+
return jsonResponse({
|
|
1083
|
+
error: removed.kind === "cleanup_incomplete"
|
|
1084
|
+
? "Claude Desktop now points at standard mode, but gateway credential cleanup is incomplete; the first-party connection remains active."
|
|
1085
|
+
: "The gateway profile could not be removed safely, so first-party mode was not applied.",
|
|
1086
|
+
code: "claude_desktop_gateway_removal_failed",
|
|
1087
|
+
...(warning ? { warning } : {}),
|
|
1088
|
+
reason: removed.reason ?? removed.kind,
|
|
1089
|
+
...(removed.residualPaths ? { residualPaths: removed.residualPaths } : {}),
|
|
1090
|
+
}, removed.kind === "cleanup_incomplete" ? 500 : 409);
|
|
1091
|
+
}
|
|
1092
|
+
gatewayRemoved = removed.changed;
|
|
1093
|
+
}
|
|
1094
|
+
const modeSaved = await persistDesktopModeField(config, "first-party");
|
|
1095
|
+
return jsonResponse({
|
|
1096
|
+
ok: true,
|
|
1097
|
+
mode: "first-party",
|
|
1098
|
+
saved: modeSaved.ok,
|
|
1099
|
+
applied: true,
|
|
1100
|
+
changed: applied.changed || gatewayRemoved,
|
|
1101
|
+
gatewayRemoved,
|
|
1102
|
+
path: applied.path,
|
|
1103
|
+
proxyPort: applied.proxyPort,
|
|
1104
|
+
caCertPath: applied.env.NODE_EXTRA_CA_CERTS,
|
|
1105
|
+
...(modeSaved.ok ? {} : { warning: `First-party env was applied, but the mode marker was not saved (${modeSaved.reason}).` }),
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1035
1108
|
const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
1036
1109
|
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
1037
1110
|
if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
|
|
@@ -1083,42 +1156,31 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1083
1156
|
nativeContextLimits(latest),
|
|
1084
1157
|
);
|
|
1085
1158
|
if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
|
|
1159
|
+
const committed = persistCommittedDesktopGateway(config, state.profile, result.fingerprint);
|
|
1160
|
+
const modeWarning = committed.ok ? undefined : `Gateway applied, but its mode/profile state was not saved (${committed.reason}).`;
|
|
1161
|
+
// The durable marker describes the committed gateway even if old-mode cleanup fails.
|
|
1162
|
+
const firstPartyRemoved = removeDesktopFirstParty();
|
|
1163
|
+
if (!firstPartyRemoved.ok) {
|
|
1164
|
+
return jsonResponse({
|
|
1165
|
+
error: `Claude Code settings could not be parsed (${firstPartyRemoved.path}); first-party cleanup remains incomplete after gateway apply.`,
|
|
1166
|
+
code: "claude_desktop_first_party_refused", reason: firstPartyRemoved.reason,
|
|
1167
|
+
applied: true, saved: committed.ok, mode: "gateway", path: result.path,
|
|
1168
|
+
...(modeWarning ? { warning: modeWarning } : {}),
|
|
1169
|
+
}, 500);
|
|
1170
|
+
}
|
|
1086
1171
|
const { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
1087
|
-
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
|
|
1088
|
-
platform: deps.platform ?? process.platform,
|
|
1089
|
-
});
|
|
1172
|
+
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({ platform: deps.platform ?? process.platform });
|
|
1090
1173
|
const policyWarning = claudeDesktopPolicyWarning(policyState);
|
|
1091
|
-
|
|
1092
|
-
if (result.fingerprint) {
|
|
1093
|
-
// The Desktop write already landed, so a failed bookkeeping save is not
|
|
1094
|
-
// an apply failure: report the miss instead of claiming a clean apply.
|
|
1095
|
-
const marked = persistDesktopProfileField(config, {
|
|
1096
|
-
...state.profile,
|
|
1097
|
-
appliedFingerprint: result.fingerprint,
|
|
1098
|
-
appliedAt: new Date().toISOString(),
|
|
1099
|
-
});
|
|
1100
|
-
if (!marked.ok) {
|
|
1101
|
-
return jsonResponse({
|
|
1102
|
-
ok: true,
|
|
1103
|
-
applied: true,
|
|
1104
|
-
saved: false,
|
|
1105
|
-
path: result.path,
|
|
1106
|
-
fingerprint: result.fingerprint,
|
|
1107
|
-
warning: [
|
|
1108
|
-
`Claude Desktop was applied, but the applied marker was not saved (${marked.reason}).`,
|
|
1109
|
-
policyWarning,
|
|
1110
|
-
].filter(Boolean).join(" "),
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1174
|
+
const warning = [modeWarning, policyWarning].filter(Boolean).join(" ");
|
|
1114
1175
|
return jsonResponse({
|
|
1115
1176
|
ok: true,
|
|
1116
|
-
|
|
1177
|
+
mode: "gateway",
|
|
1178
|
+
saved: committed.ok,
|
|
1117
1179
|
applied: true,
|
|
1118
1180
|
path: result.path,
|
|
1119
1181
|
fingerprint: result.fingerprint,
|
|
1120
1182
|
policyState,
|
|
1121
|
-
...(
|
|
1183
|
+
...(warning ? { warning } : {}),
|
|
1122
1184
|
});
|
|
1123
1185
|
} catch (error) {
|
|
1124
1186
|
rethrowManagementBodyTooLarge(error);
|
|
@@ -1131,21 +1193,39 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1131
1193
|
try {
|
|
1132
1194
|
const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
1133
1195
|
const { inspectDesktop3pConfigLibrary } = await import("../../claude/desktop-3p");
|
|
1196
|
+
const { resolveClaudeDesktopApplyMode, inspectDesktopFirstParty } = await import("../../claude/desktop-first-party");
|
|
1197
|
+
const { getClaudeInterceptState } = await import("../../claude/intercept/runtime");
|
|
1134
1198
|
const persisted = loadConfig();
|
|
1135
1199
|
const savedFingerprint = persisted.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
1136
1200
|
const observed = inspectDesktop3pConfigLibrary({ appliedFingerprint: savedFingerprint });
|
|
1137
1201
|
const desiredEnabled = claudeDesktopIntegrationEnabled(persisted);
|
|
1138
|
-
const
|
|
1202
|
+
const gatewayApplied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
|
|
1203
|
+
// A gateway profile on disk is what Desktop actually runs, whatever the saved mode says.
|
|
1204
|
+
const mode = gatewayApplied ? "gateway" : resolveClaudeDesktopApplyMode(persisted);
|
|
1205
|
+
const firstPartySeen = inspectDesktopFirstParty(persisted);
|
|
1206
|
+
const intercept = getClaudeInterceptState();
|
|
1207
|
+
const firstParty = {
|
|
1208
|
+
settingsState: firstPartySeen.settings.kind,
|
|
1209
|
+
applied: firstPartySeen.applied,
|
|
1210
|
+
stale: firstPartySeen.stale,
|
|
1211
|
+
interceptEnabled: firstPartySeen.interceptEnabled,
|
|
1212
|
+
interceptRunning: intercept !== null,
|
|
1213
|
+
proxyPort: intercept?.proxyPort ?? firstPartySeen.proxyPort,
|
|
1214
|
+
caCertPath: firstPartySeen.caCertPath,
|
|
1215
|
+
};
|
|
1216
|
+
const applied = mode === "first-party" ? firstPartySeen.applied : gatewayApplied;
|
|
1139
1217
|
// "Needs update" is only meaningful while the integration is wanted. When the
|
|
1140
1218
|
// durable switch is OFF, a leftover drifted profile is residue to clear — not
|
|
1141
1219
|
// a stale apply the operator should refresh.
|
|
1142
|
-
const stale = desiredEnabled && observed.kind === "gateway_drifted";
|
|
1220
|
+
const stale = desiredEnabled && (mode === "first-party" ? firstPartySeen.stale : observed.kind === "gateway_drifted");
|
|
1143
1221
|
const { getDesktopHealth } = await import("../../claude/desktop-health");
|
|
1144
1222
|
const { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
1145
1223
|
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
|
|
1146
1224
|
platform: deps.platform ?? process.platform,
|
|
1147
1225
|
});
|
|
1148
|
-
|
|
1226
|
+
// Managed-policy conflicts only matter for the gateway profile; first-party mode never
|
|
1227
|
+
// touches Desktop's own configuration.
|
|
1228
|
+
const policy = claudeDesktopPolicyHealth(mode === "first-party" ? "absent" : policyState);
|
|
1149
1229
|
const health = {
|
|
1150
1230
|
...getDesktopHealth(),
|
|
1151
1231
|
ok: policy.ok,
|
|
@@ -1153,14 +1233,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1153
1233
|
policy,
|
|
1154
1234
|
};
|
|
1155
1235
|
const policyConflict = desiredEnabled && !policy.ok;
|
|
1156
|
-
const
|
|
1236
|
+
const firstPartyResidue = mode === "gateway" && (firstPartySeen.applied || firstPartySeen.stale);
|
|
1237
|
+
const baseDrift = desiredEnabled
|
|
1238
|
+
? !applied || stale || firstPartyResidue
|
|
1239
|
+
: applied || gatewayApplied || firstPartySeen.applied || firstPartySeen.stale || observed.kind === "unsafe";
|
|
1157
1240
|
const driftReason = policyConflict
|
|
1158
1241
|
? policy.state === "present" ? "managed_policy_present" : "managed_policy_unknown"
|
|
1159
1242
|
: desiredEnabled
|
|
1160
|
-
? !applied ? "desired_on_not_current" : stale ? "profile_drift" : null
|
|
1161
|
-
:
|
|
1243
|
+
? !applied ? "desired_on_not_current" : stale ? "profile_drift" : firstPartyResidue ? "first_party_residue" : null
|
|
1244
|
+
: gatewayApplied ? "desired_off_gateway_selected" : firstPartySeen.applied || firstPartySeen.stale ? "desired_off_first_party_env" : null;
|
|
1162
1245
|
return jsonResponse({
|
|
1163
1246
|
desiredEnabled,
|
|
1247
|
+
mode,
|
|
1248
|
+
firstParty,
|
|
1164
1249
|
installed: observed.kind !== "not_installed",
|
|
1165
1250
|
observedKind: observed.kind,
|
|
1166
1251
|
applied,
|