@yansigit/opencodex 2.31.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/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import type { CatalogModel } from "../../codex/catalog";
|
|
4
|
+
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SUBAGENT_MODELS,
|
|
7
|
+
codexAutoStartEnabled,
|
|
8
|
+
hasOwnProvider,
|
|
9
|
+
isValidProviderName,
|
|
10
|
+
loadConfig,
|
|
11
|
+
multiAgentGuidanceEnabled,
|
|
12
|
+
mutatePersistedConfig,
|
|
13
|
+
providerBaseUrlConfigError,
|
|
14
|
+
providerHeadersConfigError,
|
|
15
|
+
saveConfigPreservingClaudeCode,
|
|
16
|
+
subagentDefaultSyncEffective,
|
|
17
|
+
} from "../../config";
|
|
18
|
+
import {
|
|
19
|
+
clearLoginState,
|
|
20
|
+
getLoginStatus,
|
|
21
|
+
isPublicOAuthProvider,
|
|
22
|
+
listOAuthProviders,
|
|
23
|
+
startLoginFlow,
|
|
24
|
+
submitManualLoginCode,
|
|
25
|
+
upsertOAuthProvider,
|
|
26
|
+
} from "../../oauth";
|
|
27
|
+
import { removeCredential } from "../../oauth/store";
|
|
28
|
+
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
29
|
+
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
30
|
+
import { deriveProviderPresets } from "../../providers/derive";
|
|
31
|
+
import { providerCodexAccountMode } from "../../providers/registry";
|
|
32
|
+
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
33
|
+
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
34
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
35
|
+
import { clearThreadAccountMap } from "../../codex/routing";
|
|
36
|
+
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
37
|
+
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
38
|
+
import { resolveCodexHomeDir } from "../../codex/home";
|
|
39
|
+
import { readUsageEntries } from "../../usage/log";
|
|
40
|
+
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
41
|
+
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
42
|
+
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
43
|
+
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
44
|
+
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
45
|
+
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
|
|
46
|
+
import {
|
|
47
|
+
clearDebugSettings,
|
|
48
|
+
clearDebugSetting,
|
|
49
|
+
getDebugSettings,
|
|
50
|
+
setDebugSettings,
|
|
51
|
+
type DebugFlag,
|
|
52
|
+
} from "../../lib/debug-settings";
|
|
53
|
+
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
54
|
+
import {
|
|
55
|
+
visionCandidateRows,
|
|
56
|
+
visionDescriberIsProvablyBlind,
|
|
57
|
+
visionDescriberRejection,
|
|
58
|
+
} from "./vision-sidecar-options";
|
|
59
|
+
import {
|
|
60
|
+
webSearchCandidateRows,
|
|
61
|
+
webSearchModelIsRejected,
|
|
62
|
+
webSearchModelRejection,
|
|
63
|
+
} from "./web-search-sidecar-options";
|
|
64
|
+
import { drainAndShutdown } from "../lifecycle";
|
|
65
|
+
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
66
|
+
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
67
|
+
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
68
|
+
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
69
|
+
import { applySystemEnvToggle } from "../system-env";
|
|
70
|
+
|
|
71
|
+
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
|
|
72
|
+
import {
|
|
73
|
+
agentRolesSyncEffective,
|
|
74
|
+
parseSubagentRoles,
|
|
75
|
+
routedOnV2Warnings,
|
|
76
|
+
unionRoleModelsIntoRoster,
|
|
77
|
+
} from "../../codex/agent-roles";
|
|
78
|
+
import { syncCodexAgentRoles } from "../../codex/agent-roles-sync";
|
|
79
|
+
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
80
|
+
import { readManagementJsonBody, readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
81
|
+
|
|
82
|
+
const GROK_APPLY_JOIN_MS = 120_000;
|
|
83
|
+
export const GROK_APPLY_TERMINAL_MS = 10 * 60_000;
|
|
84
|
+
const grokApplyEncoder = new TextEncoder();
|
|
85
|
+
let grokApplyFlight: { startedAt: number; promise: Promise<unknown>; bytes: number } | null = null;
|
|
86
|
+
let grokApplyHighWaterBytes = 0;
|
|
87
|
+
let grokApplyTestHooks: { now?: () => number; run?: () => Promise<unknown> } | null = null;
|
|
88
|
+
|
|
89
|
+
class GrokApplyBusyError extends Error {}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mirror a durable desired-state transition onto the long-lived server snapshot.
|
|
93
|
+
*
|
|
94
|
+
* `setIntegrationEnabled` writes DISK only. The server reuses one `config` object
|
|
95
|
+
* for every request, so leaving it stale makes the native GET report the opposite
|
|
96
|
+
* of what was just persisted, and makes any later whole-snapshot save (the Desktop
|
|
97
|
+
* profile PUT does exactly that) write the stale value back over the transition.
|
|
98
|
+
* ON is the ABSENCE of the key, matching `setIntegrationEnabled`'s on-disk shape.
|
|
99
|
+
*/
|
|
100
|
+
function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-desktop", enabled: boolean): void {
|
|
101
|
+
const integrations = { ...(config.clientIntegrations ?? {}) };
|
|
102
|
+
if (enabled) delete integrations[client];
|
|
103
|
+
else integrations[client] = false;
|
|
104
|
+
if (Object.keys(integrations).length === 0) delete config.clientIntegrations;
|
|
105
|
+
else config.clientIntegrations = integrations;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Persist ONLY `claudeCode.desktopProfile`, field-scoped, against the CURRENT
|
|
110
|
+
* on-disk config.
|
|
111
|
+
*
|
|
112
|
+
* `saveConfigPreservingClaudeCode(ctx.config)` writes the whole long-lived server
|
|
113
|
+
* snapshot. On the apply path that snapshot still carries the `clientIntegrations`
|
|
114
|
+
* it was loaded with, so a save right after `setIntegrationEnabled("claude-desktop",
|
|
115
|
+
* true)` carried the stale OFF back over the enable and made the route cancel its
|
|
116
|
+
* own apply. Mutating one field under the config-mutation lock cannot regress an
|
|
117
|
+
* unrelated key another writer just committed.
|
|
118
|
+
*/
|
|
119
|
+
function persistDesktopProfileField(
|
|
120
|
+
config: OcxConfig,
|
|
121
|
+
desktopProfile: NonNullable<OcxConfig["claudeCode"]>["desktopProfile"],
|
|
122
|
+
): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" } {
|
|
123
|
+
const outcome = mutatePersistedConfig(persisted => {
|
|
124
|
+
persisted.claudeCode = { ...(persisted.claudeCode ?? {}), desktopProfile };
|
|
125
|
+
return { changed: true, value: true };
|
|
126
|
+
});
|
|
127
|
+
// Only mirror into memory once the durable write actually landed; an
|
|
128
|
+
// `unavailable` outcome must not leave the snapshot claiming a saved profile.
|
|
129
|
+
if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
|
|
130
|
+
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile };
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function grokApplyFlightSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
|
|
135
|
+
return {
|
|
136
|
+
currentBytes: grokApplyFlight?.bytes ?? 0,
|
|
137
|
+
highWaterBytes: grokApplyHighWaterBytes,
|
|
138
|
+
active: grokApplyFlight ? 1 : 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function runGrokApplyFlight(): Promise<unknown> {
|
|
143
|
+
const at = grokApplyTestHooks?.now?.() ?? Date.now();
|
|
144
|
+
const current = grokApplyFlight;
|
|
145
|
+
if (current) {
|
|
146
|
+
const age = at - current.startedAt;
|
|
147
|
+
if (age < GROK_APPLY_JOIN_MS) return current.promise;
|
|
148
|
+
if (age <= GROK_APPLY_TERMINAL_MS) return Promise.reject(new GrokApplyBusyError("grok_apply_busy"));
|
|
149
|
+
// A permanently hung operation must not monopolize the singleton forever. Its
|
|
150
|
+
// eventual finally is identity-checked, so it cannot clear a replacement flight.
|
|
151
|
+
if (grokApplyFlight === current) grokApplyFlight = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const flight = { startedAt: at, promise: Promise.resolve() as Promise<unknown>, bytes: 0 };
|
|
155
|
+
flight.promise = (grokApplyTestHooks?.run ?? (async () => {
|
|
156
|
+
const [{ syncGrokConfig }, { readRuntimePort }] = await Promise.all([
|
|
157
|
+
import("../../grok/sync"),
|
|
158
|
+
import("../../config"),
|
|
159
|
+
]);
|
|
160
|
+
const currentConfig = loadConfig();
|
|
161
|
+
const runtime = readRuntimePort(process.pid);
|
|
162
|
+
const port = runtime?.port ?? currentConfig.port;
|
|
163
|
+
const hostname = runtime?.hostname ?? currentConfig.hostname;
|
|
164
|
+
flight.bytes = grokApplyEncoder.encode(JSON.stringify(currentConfig)).byteLength
|
|
165
|
+
+ grokApplyEncoder.encode(hostname ?? "").byteLength;
|
|
166
|
+
grokApplyHighWaterBytes = Math.max(grokApplyHighWaterBytes, flight.bytes);
|
|
167
|
+
return syncGrokConfig(port, currentConfig, hostname !== undefined ? { hostname } : {});
|
|
168
|
+
}))().finally(() => {
|
|
169
|
+
if (grokApplyFlight === flight) grokApplyFlight = null;
|
|
170
|
+
});
|
|
171
|
+
grokApplyFlight = flight;
|
|
172
|
+
return flight.promise;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function runGrokApplyFlightForTests(): Promise<unknown> {
|
|
176
|
+
return runGrokApplyFlight();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function setGrokApplyFlightTestHooks(
|
|
180
|
+
hooks: { now?: () => number; run?: () => Promise<unknown> } | null,
|
|
181
|
+
): void {
|
|
182
|
+
grokApplyTestHooks = hooks;
|
|
183
|
+
grokApplyFlight = null;
|
|
184
|
+
grokApplyHighWaterBytes = 0;
|
|
185
|
+
}
|
|
186
|
+
import type { ManagementContext } from "./context";
|
|
187
|
+
|
|
188
|
+
export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
189
|
+
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
190
|
+
|
|
191
|
+
/** Best-effort Desktop 3P config auto-reconcile when providers change. */
|
|
192
|
+
async function autoApplyDesktopBestEffort(): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
195
|
+
const admitted = loadConfig();
|
|
196
|
+
if (!claudeDesktopIntegrationEnabled(admitted)) return;
|
|
197
|
+
if (admitted.claudeCode?.desktopAutoApply === false) return;
|
|
198
|
+
if (!admitted.claudeCode?.desktopProfile) return;
|
|
199
|
+
const { inspectDesktop3pConfigLibrary, writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
200
|
+
const beforeKind = inspectDesktop3pConfigLibrary({
|
|
201
|
+
appliedFingerprint: admitted.claudeCode.desktopProfile.appliedFingerprint ?? null,
|
|
202
|
+
}).kind;
|
|
203
|
+
if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(beforeKind)) return;
|
|
204
|
+
const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
205
|
+
const allModels = await (deps.fetchAllModels ?? fetchAllModels)(admitted);
|
|
206
|
+
const current = loadConfig();
|
|
207
|
+
// This is the real guard: the catalog await admits a concurrent explicit OFF.
|
|
208
|
+
if (!claudeDesktopIntegrationEnabled(current)) return;
|
|
209
|
+
if (current.claudeCode?.desktopAutoApply === false || !current.claudeCode?.desktopProfile) return;
|
|
210
|
+
const afterKind = inspectDesktop3pConfigLibrary({
|
|
211
|
+
appliedFingerprint: current.claudeCode.desktopProfile.appliedFingerprint ?? null,
|
|
212
|
+
}).kind;
|
|
213
|
+
if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(afterKind)) return;
|
|
214
|
+
const routed = filterCatalogVisibleModels(allModels, current).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
|
|
215
|
+
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
216
|
+
current.port ?? 10100,
|
|
217
|
+
[...desktopVisibleNativeSlugs(current)],
|
|
218
|
+
routed,
|
|
219
|
+
current.apiKeys?.[0]?.key,
|
|
220
|
+
"static",
|
|
221
|
+
current.claudeCode.desktopProfile,
|
|
222
|
+
nativeContextLimits(current),
|
|
223
|
+
);
|
|
224
|
+
if (result.written && result.fingerprint) {
|
|
225
|
+
current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
|
|
226
|
+
saveConfigPreservingClaudeCode(current);
|
|
227
|
+
}
|
|
228
|
+
} catch { /* best-effort */ }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// multi_agent_v2 surface toggle. GET reports the flag + the agents.max_threads
|
|
232
|
+
// boot conflict; PUT flips it via the official `codex features` CLI and RESYNCS
|
|
233
|
+
// the catalog so multi-agent surface metadata stays fresh. The catalog build
|
|
234
|
+
// itself never writes config — this endpoint is the only server-side mutation
|
|
235
|
+
// surface for the flag.
|
|
236
|
+
if (url.pathname === "/api/v2" && req.method === "GET") {
|
|
237
|
+
const {
|
|
238
|
+
isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads,
|
|
239
|
+
getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
|
|
240
|
+
getMultiAgentModeHintText,
|
|
241
|
+
} = await import("../../codex/features");
|
|
242
|
+
const enabled = isMultiAgentV2Enabled();
|
|
243
|
+
return jsonResponse({
|
|
244
|
+
enabled,
|
|
245
|
+
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
246
|
+
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
247
|
+
multiAgentMode: config.multiAgentMode ?? "default",
|
|
248
|
+
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
249
|
+
agentsEnabled: getAgentsEnabled(),
|
|
250
|
+
agentsMaxDepth: getAgentsMaxDepth(),
|
|
251
|
+
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
252
|
+
multiAgentModeHintText: getMultiAgentModeHintText(),
|
|
253
|
+
// max_depth is V1-only upstream; this is the global-flag statement, derived
|
|
254
|
+
// server-side so no client can present it as an effective V2 limit.
|
|
255
|
+
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (url.pathname === "/api/v2" && req.method === "PUT") {
|
|
259
|
+
let body: {
|
|
260
|
+
enabled?: unknown;
|
|
261
|
+
maxConcurrentThreadsPerSession?: unknown;
|
|
262
|
+
multiAgentMode?: unknown;
|
|
263
|
+
keepNativeChatGptOnV1?: unknown;
|
|
264
|
+
agentsEnabled?: unknown;
|
|
265
|
+
agentsMaxDepth?: unknown;
|
|
266
|
+
subagentDeveloperInstructions?: unknown;
|
|
267
|
+
multiAgentModeHintText?: unknown;
|
|
268
|
+
};
|
|
269
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
270
|
+
const wantsFlag = body.enabled !== undefined;
|
|
271
|
+
const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
|
|
272
|
+
const wantsMode = body.multiAgentMode !== undefined;
|
|
273
|
+
const wantsKeepNative = body.keepNativeChatGptOnV1 !== undefined;
|
|
274
|
+
const wantsAgentsEnabled = body.agentsEnabled !== undefined;
|
|
275
|
+
const wantsMaxDepth = body.agentsMaxDepth !== undefined;
|
|
276
|
+
const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
|
|
277
|
+
const wantsModeHintText = body.multiAgentModeHintText !== undefined;
|
|
278
|
+
if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
|
|
279
|
+
return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
|
|
280
|
+
}
|
|
281
|
+
if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
282
|
+
if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
|
|
283
|
+
return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
|
|
284
|
+
}
|
|
285
|
+
if (wantsKeepNative && typeof body.keepNativeChatGptOnV1 !== "boolean") {
|
|
286
|
+
return jsonResponse({ error: "body.keepNativeChatGptOnV1 must be a boolean" }, 400);
|
|
287
|
+
}
|
|
288
|
+
if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
|
|
289
|
+
return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
|
|
290
|
+
}
|
|
291
|
+
// Validate every new field BEFORE any write, so each 400 leaves config untouched.
|
|
292
|
+
// null unsets the key; "" is a meaningful value for instructions and must not be
|
|
293
|
+
// collapsed by a falsy check. The i32 preflight mirrors the upstream Option<i32>
|
|
294
|
+
// contract — out-of-range would otherwise surface as a mid-sequence write failure.
|
|
295
|
+
if (wantsAgentsEnabled && body.agentsEnabled !== null && typeof body.agentsEnabled !== "boolean") {
|
|
296
|
+
return jsonResponse({ error: "body.agentsEnabled must be a boolean or null" }, 400);
|
|
297
|
+
}
|
|
298
|
+
if (wantsMaxDepth && body.agentsMaxDepth !== null
|
|
299
|
+
&& (typeof body.agentsMaxDepth !== "number" || !Number.isInteger(body.agentsMaxDepth)
|
|
300
|
+
|| body.agentsMaxDepth < -2_147_483_648 || body.agentsMaxDepth > 2_147_483_647)) {
|
|
301
|
+
return jsonResponse({ error: "body.agentsMaxDepth must be an integer within signed i32 range, or null" }, 400);
|
|
302
|
+
}
|
|
303
|
+
if (wantsSubagentInstructions && body.subagentDeveloperInstructions !== null && typeof body.subagentDeveloperInstructions !== "string") {
|
|
304
|
+
return jsonResponse({ error: "body.subagentDeveloperInstructions must be a string or null" }, 400);
|
|
305
|
+
}
|
|
306
|
+
// null unsets the upstream key (effort-derived policy resumes); an empty/whitespace
|
|
307
|
+
// string is rejected because codex-rs treats any present hint as an override that
|
|
308
|
+
// suppresses even the Ultra-derived Proactive message (Option<String>, no blank
|
|
309
|
+
// special-case in effective_multi_agent_mode).
|
|
310
|
+
if (wantsModeHintText && body.multiAgentModeHintText !== null
|
|
311
|
+
&& (typeof body.multiAgentModeHintText !== "string" || body.multiAgentModeHintText.trim().length === 0)) {
|
|
312
|
+
return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
|
|
313
|
+
}
|
|
314
|
+
const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
|
|
315
|
+
const modeFlag = mode === "v2" ? true : mode === "v1" ? false : undefined;
|
|
316
|
+
if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
|
|
317
|
+
return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
|
|
318
|
+
}
|
|
319
|
+
const {
|
|
320
|
+
isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
|
|
321
|
+
getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
|
|
322
|
+
getMultiAgentModeHintText, probeCodexSupportsModeHint, setAgentsEnabled, setAgentsMaxDepth,
|
|
323
|
+
setSubagentDeveloperInstructions, setMultiAgentModeHintText, MODE_HINT_UNSUPPORTED_ERROR,
|
|
324
|
+
} = await import("../../codex/features");
|
|
325
|
+
// Probe the capability before any combined-request mutation. The scalar writer
|
|
326
|
+
// repeats this check, but doing it here prevents an earlier flag/mode/agents
|
|
327
|
+
// write from landing before an unsupported runtime returns 502.
|
|
328
|
+
if (wantsModeHintText && body.multiAgentModeHintText !== null
|
|
329
|
+
&& probeCodexSupportsModeHint() === false) {
|
|
330
|
+
return jsonResponse({
|
|
331
|
+
error: `writing multiAgentModeHintText failed: ${MODE_HINT_UNSUPPORTED_ERROR}`,
|
|
332
|
+
}, 502);
|
|
333
|
+
}
|
|
334
|
+
const warnings: string[] = [];
|
|
335
|
+
const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
|
|
336
|
+
if (requestedFlag !== undefined || wantsThreads) {
|
|
337
|
+
const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
|
|
338
|
+
let toggle = deps.toggleCodexMultiAgentV2;
|
|
339
|
+
if (!toggle) {
|
|
340
|
+
const { runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
341
|
+
toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable");
|
|
342
|
+
}
|
|
343
|
+
const result = transitionMultiAgentV2(targetFlag, toggle, {
|
|
344
|
+
...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
|
|
345
|
+
});
|
|
346
|
+
if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}` }, 502);
|
|
347
|
+
if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
|
|
348
|
+
}
|
|
349
|
+
if (wantsMode) {
|
|
350
|
+
if (mode === "default") delete config.multiAgentMode;
|
|
351
|
+
else config.multiAgentMode = mode;
|
|
352
|
+
saveConfigPreservingClaudeCode(config);
|
|
353
|
+
warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
|
|
354
|
+
}
|
|
355
|
+
if (wantsKeepNative) {
|
|
356
|
+
if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
|
|
357
|
+
else delete config.keepNativeChatGptOnV1;
|
|
358
|
+
saveConfigPreservingClaudeCode(config);
|
|
359
|
+
const effectiveMode = mode ?? config.multiAgentMode ?? "default";
|
|
360
|
+
warnings.push(body.keepNativeChatGptOnV1 === true
|
|
361
|
+
? (effectiveMode === "v2"
|
|
362
|
+
? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
|
|
363
|
+
: "keepNativeChatGptOnV1 is stored but inactive until multi-agent mode is v2. Applies to new sessions.")
|
|
364
|
+
: "ChatGPT-native models follow the selected v1/v2/base surface. Applies to new sessions.");
|
|
365
|
+
}
|
|
366
|
+
// New-key scalar writes: each writer is individually atomic, so apply them in
|
|
367
|
+
// sequence after the transition. A failure here is a persistence failure (the
|
|
368
|
+
// writers' ok:false result or a throw from the underlying atomic write helper),
|
|
369
|
+
// reported as 502 naming the failed key plus the writes that already landed.
|
|
370
|
+
// NOTE: do not name that helper literally here — tests/grok-writer-boundary.test.ts
|
|
371
|
+
// asserts this route file contains no direct write primitive, and matches on the
|
|
372
|
+
// symbol name even inside a comment.
|
|
373
|
+
const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
|
|
374
|
+
if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
|
|
375
|
+
if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
|
|
376
|
+
if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
|
|
377
|
+
if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
|
|
378
|
+
const landed: string[] = [];
|
|
379
|
+
for (const write of scalarWrites) {
|
|
380
|
+
try {
|
|
381
|
+
const result = write.run();
|
|
382
|
+
if (!result.ok) {
|
|
383
|
+
return jsonResponse({ error: `writing ${write.field} failed: ${result.error}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
384
|
+
}
|
|
385
|
+
landed.push(write.field);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
388
|
+
return jsonResponse({ error: `writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Derived from fresh post-write readers (readConfigText is uncached): upstream
|
|
392
|
+
// lets an enabled multi_agent_v2 feature override [agents].enabled = false, so
|
|
393
|
+
// warn rather than reject — silently accepting would imply multi-agent is off.
|
|
394
|
+
if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
|
|
395
|
+
warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
|
|
396
|
+
}
|
|
397
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
398
|
+
if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
|
|
399
|
+
const enabled = isMultiAgentV2Enabled();
|
|
400
|
+
return jsonResponse({
|
|
401
|
+
ok: true,
|
|
402
|
+
enabled,
|
|
403
|
+
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
404
|
+
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
405
|
+
multiAgentMode: config.multiAgentMode ?? "default",
|
|
406
|
+
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
407
|
+
agentsEnabled: getAgentsEnabled(),
|
|
408
|
+
agentsMaxDepth: getAgentsMaxDepth(),
|
|
409
|
+
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
410
|
+
multiAgentModeHintText: getMultiAgentModeHintText(),
|
|
411
|
+
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
412
|
+
warnings,
|
|
413
|
+
catalogRefresh,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// default_mode_request_user_input feature toggle (Codex Auth page). GET reads the
|
|
418
|
+
// flag from $CODEX_HOME/config.toml; PUT flips it via the official `codex features`
|
|
419
|
+
// CLI so the TOML edit stays upstream-owned and format-preserving.
|
|
420
|
+
if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "GET") {
|
|
421
|
+
const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
|
|
422
|
+
return jsonResponse({
|
|
423
|
+
enabled: isDefaultModeRequestUserInputEnabled(),
|
|
424
|
+
key: DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "PUT") {
|
|
428
|
+
let parsedBody: unknown;
|
|
429
|
+
try {
|
|
430
|
+
parsedBody = await readManagementJsonBody(req);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
rethrowManagementBodyTooLarge(error);
|
|
433
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
434
|
+
}
|
|
435
|
+
if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
|
|
436
|
+
return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
437
|
+
}
|
|
438
|
+
const body = parsedBody as { enabled?: unknown };
|
|
439
|
+
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
440
|
+
const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
|
|
441
|
+
const before = isDefaultModeRequestUserInputEnabled();
|
|
442
|
+
let toggle = deps.toggleDefaultModeRequestUserInput;
|
|
443
|
+
if (!toggle) {
|
|
444
|
+
const { runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
445
|
+
toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable", DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY);
|
|
446
|
+
}
|
|
447
|
+
let toggleError: string | null = null;
|
|
448
|
+
try {
|
|
449
|
+
toggle(body.enabled);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const err = error as { stderr?: unknown; message?: string };
|
|
452
|
+
const raw = err.stderr;
|
|
453
|
+
const stderrText = typeof raw === "string"
|
|
454
|
+
? raw.trim()
|
|
455
|
+
: raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
|
|
456
|
+
toggleError = stderrText || (err.message ?? String(error));
|
|
457
|
+
}
|
|
458
|
+
const enabled = isDefaultModeRequestUserInputEnabled();
|
|
459
|
+
if (toggleError !== null || enabled !== body.enabled) {
|
|
460
|
+
const reason = toggleError
|
|
461
|
+
?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
|
|
462
|
+
return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
|
|
463
|
+
}
|
|
464
|
+
const warnings: string[] = [];
|
|
465
|
+
if (enabled !== before) {
|
|
466
|
+
warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
|
|
467
|
+
}
|
|
468
|
+
return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Subagent prompt injection model: single native or routed model whose info is
|
|
472
|
+
// dynamically injected into the v1 proactive prompt, plus an optional reasoning
|
|
473
|
+
// effort the prompt tells the agent to pass to spawn_agent. GET returns the current
|
|
474
|
+
// picks + available models/efforts; PUT sets or clears them.
|
|
475
|
+
if (url.pathname === "/api/injection-model" && req.method === "GET") {
|
|
476
|
+
const models = await fetchAllModels(config);
|
|
477
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
478
|
+
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
479
|
+
const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
|
|
480
|
+
const nativeModels = listCatalogNativeSlugs()
|
|
481
|
+
.filter(slug => !disabled.has(slug))
|
|
482
|
+
.map(slug => ({ provider: "openai", model: slug, namespaced: slug }));
|
|
483
|
+
const routedModels = uniqueCatalogModelsForPublicList(models)
|
|
484
|
+
.map(m => ({ provider: m.provider, model: m.id, namespaced: catalogModelSlug(m) }))
|
|
485
|
+
.filter(m => ![...disabled].some(stored => (
|
|
486
|
+
stored === m.namespaced || slugEquals(stored, m.provider, m.model)
|
|
487
|
+
)));
|
|
488
|
+
return jsonResponse({
|
|
489
|
+
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
490
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
491
|
+
model: config.injectionModel ?? null,
|
|
492
|
+
effort: config.injectionEffort ?? null,
|
|
493
|
+
prompt: config.injectionPrompt ?? null,
|
|
494
|
+
efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
|
|
495
|
+
available: [...nativeModels, ...routedModels],
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (url.pathname === "/api/injection-model" && req.method === "PUT") {
|
|
499
|
+
let parsedBody: unknown;
|
|
500
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) {
|
|
501
|
+
rethrowManagementBodyTooLarge(error);
|
|
502
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
503
|
+
}
|
|
504
|
+
if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
|
|
505
|
+
return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
506
|
+
}
|
|
507
|
+
const body = parsedBody as {
|
|
508
|
+
multiAgentGuidanceEnabled?: unknown;
|
|
509
|
+
syncCodexSubagentDefaults?: unknown;
|
|
510
|
+
model?: unknown;
|
|
511
|
+
effort?: unknown;
|
|
512
|
+
prompt?: unknown;
|
|
513
|
+
};
|
|
514
|
+
const { isCodexReasoningEffort } = await import("../../reasoning-effort");
|
|
515
|
+
|
|
516
|
+
let nextEnabled = config.multiAgentGuidanceEnabled;
|
|
517
|
+
// Start from the effective state reported by GET. A stale hand-edited
|
|
518
|
+
// `true` without a model must not spring back on during a model-only PUT.
|
|
519
|
+
let nextSyncCodexSubagentDefaults = subagentDefaultSyncEffective(config);
|
|
520
|
+
let nextModel = config.injectionModel;
|
|
521
|
+
let nextEffort = config.injectionEffort;
|
|
522
|
+
let nextPrompt = config.injectionPrompt;
|
|
523
|
+
|
|
524
|
+
if ("multiAgentGuidanceEnabled" in body) {
|
|
525
|
+
if (typeof body.multiAgentGuidanceEnabled !== "boolean") {
|
|
526
|
+
return jsonResponse({ error: "multiAgentGuidanceEnabled must be a boolean" }, 400);
|
|
527
|
+
}
|
|
528
|
+
nextEnabled = body.multiAgentGuidanceEnabled;
|
|
529
|
+
}
|
|
530
|
+
if ("syncCodexSubagentDefaults" in body) {
|
|
531
|
+
if (typeof body.syncCodexSubagentDefaults !== "boolean") {
|
|
532
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults must be a boolean" }, 400);
|
|
533
|
+
}
|
|
534
|
+
nextSyncCodexSubagentDefaults = body.syncCodexSubagentDefaults;
|
|
535
|
+
}
|
|
536
|
+
if ("model" in body) {
|
|
537
|
+
if (body.model === null || body.model === "") nextModel = undefined;
|
|
538
|
+
else if (typeof body.model === "string" && body.model.trim().length > 0) nextModel = body.model;
|
|
539
|
+
else return jsonResponse({ error: "model must be a nonblank string or null" }, 400);
|
|
540
|
+
}
|
|
541
|
+
if ("effort" in body) {
|
|
542
|
+
if (body.effort === null || body.effort === "") nextEffort = undefined;
|
|
543
|
+
else if (typeof body.effort === "string" && isCodexReasoningEffort(body.effort)) {
|
|
544
|
+
nextEffort = body.effort;
|
|
545
|
+
} else {
|
|
546
|
+
return jsonResponse({ error: `unknown reasoning effort "${String(body.effort)}"` }, 400);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if ("prompt" in body) {
|
|
550
|
+
if (typeof body.prompt === "string" && body.prompt.trim().length > 0) nextPrompt = body.prompt;
|
|
551
|
+
else if (body.prompt === null || body.prompt === "") nextPrompt = undefined;
|
|
552
|
+
else return jsonResponse({ error: "prompt must be a string or null" }, 400);
|
|
553
|
+
}
|
|
554
|
+
// Clearing the model always clears model-dependent settings before sync/effort gates.
|
|
555
|
+
if (!nextModel) {
|
|
556
|
+
nextEffort = undefined;
|
|
557
|
+
nextSyncCodexSubagentDefaults = false;
|
|
558
|
+
}
|
|
559
|
+
if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
|
|
560
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
|
|
561
|
+
}
|
|
562
|
+
if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
|
|
563
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
config.multiAgentGuidanceEnabled = nextEnabled;
|
|
567
|
+
if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
|
|
568
|
+
else delete config.syncCodexSubagentDefaults;
|
|
569
|
+
if (nextModel) config.injectionModel = nextModel;
|
|
570
|
+
else delete config.injectionModel;
|
|
571
|
+
if (nextEffort) config.injectionEffort = nextEffort;
|
|
572
|
+
else delete config.injectionEffort;
|
|
573
|
+
if (nextPrompt) config.injectionPrompt = nextPrompt;
|
|
574
|
+
else delete config.injectionPrompt;
|
|
575
|
+
|
|
576
|
+
saveConfigPreservingClaudeCode(config);
|
|
577
|
+
return jsonResponse({
|
|
578
|
+
ok: true,
|
|
579
|
+
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
580
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
581
|
+
model: config.injectionModel ?? null,
|
|
582
|
+
effort: config.injectionEffort ?? null,
|
|
583
|
+
prompt: config.injectionPrompt ?? null,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Hard reasoning-effort caps (devlog/260710_subagent_effort_intercept): a global ceiling and a
|
|
588
|
+
// sub-agent-only ceiling, enforced per-request in handleResponses (src/server/effort-policy.ts).
|
|
589
|
+
// Key semantics per field: absent -> unchanged; null/"" -> clear; ladder value -> set; else 400.
|
|
590
|
+
if (url.pathname === "/api/effort-caps" && req.method === "GET") {
|
|
591
|
+
const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
|
|
592
|
+
return jsonResponse({
|
|
593
|
+
effortCap: config.effortCap ?? null,
|
|
594
|
+
subagentEffortCap: config.subagentEffortCap ?? null,
|
|
595
|
+
efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (url.pathname === "/api/effort-caps" && req.method === "PUT") {
|
|
599
|
+
let body: { effortCap?: unknown; subagentEffortCap?: unknown };
|
|
600
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
601
|
+
const { isCodexReasoningEffort } = await import("../../reasoning-effort");
|
|
602
|
+
for (const key of ["effortCap", "subagentEffortCap"] as const) {
|
|
603
|
+
if (!(key in body)) continue;
|
|
604
|
+
const value = body[key];
|
|
605
|
+
if (value === null || value === "") { delete config[key]; continue; }
|
|
606
|
+
if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
|
|
607
|
+
return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
|
|
608
|
+
}
|
|
609
|
+
config[key] = value;
|
|
610
|
+
}
|
|
611
|
+
saveConfigPreservingClaudeCode(config);
|
|
612
|
+
return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the
|
|
616
|
+
// first 5 routed catalog entries). PUT reorders the injected catalog so the chosen ones lead.
|
|
617
|
+
if (url.pathname === "/api/subagent-models" && req.method === "GET") {
|
|
618
|
+
const models = await fetchAllModels(config);
|
|
619
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
620
|
+
// Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
|
|
621
|
+
// catalog, just buried by priority. List them first so the user can feature them over routed.
|
|
622
|
+
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
623
|
+
const visibleRouted = [...new Set(models
|
|
624
|
+
.filter(m => ![...disabled].some(stored =>
|
|
625
|
+
stored === catalogModelSlug(m) || slugEquals(stored, m.provider, m.id)
|
|
626
|
+
))
|
|
627
|
+
.map(catalogModelSlug))];
|
|
628
|
+
const chosen = config.subagentModels ?? [];
|
|
629
|
+
const selectable = [
|
|
630
|
+
...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
|
|
631
|
+
...visibleRouted,
|
|
632
|
+
];
|
|
633
|
+
// A saved roster slot must stay representable even after its model is disabled
|
|
634
|
+
// elsewhere (Models page, provider allowlist, a provider row going away). The
|
|
635
|
+
// dashboard treats `available` as the set of rows it can render, so a chosen id
|
|
636
|
+
// missing from it disappears from the roster UI and the next Save — which PUTs
|
|
637
|
+
// exactly what the UI holds — silently truncates the persisted list. Losing a
|
|
638
|
+
// deliberate 5-model roster to an unrelated visibility toggle is data loss, not a
|
|
639
|
+
// filter. Same reasoning as `fetchGrokCandidateModels`, which deliberately lists a
|
|
640
|
+
// model the user already excluded so its switch remains reachable.
|
|
641
|
+
const selectableSet = new Set(selectable);
|
|
642
|
+
const available = [
|
|
643
|
+
...selectable,
|
|
644
|
+
...[...new Set(chosen)].filter(model => !selectableSet.has(model)),
|
|
645
|
+
];
|
|
646
|
+
// #857: let CLI/GUI show when a running Codex app-server keeps an older
|
|
647
|
+
// in-memory catalog than the one on disk.
|
|
648
|
+
const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
|
|
649
|
+
const catalogState = collectCodexAppServerCatalogState();
|
|
650
|
+
return jsonResponse({ chosen, available, catalogState });
|
|
651
|
+
}
|
|
652
|
+
if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
|
|
653
|
+
let body: { models?: unknown };
|
|
654
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
655
|
+
const chosen = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string").slice(0, 5) : [];
|
|
656
|
+
config.subagentModels = chosen;
|
|
657
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
658
|
+
save(config);
|
|
659
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
660
|
+
await syncClaudeAgentDefsBestEffort();
|
|
661
|
+
await autoApplyDesktopBestEffort();
|
|
662
|
+
return jsonResponse({ ok: true, applied: chosen, catalogRefresh });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (url.pathname === "/api/subagent-roles" && req.method === "GET") {
|
|
666
|
+
const models = await fetchAllModels(config);
|
|
667
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
668
|
+
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
669
|
+
const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
|
|
670
|
+
const nativeModels = listCatalogNativeSlugs()
|
|
671
|
+
.filter(slug => !disabled.has(slug))
|
|
672
|
+
.map(slug => ({ provider: "openai", model: slug, namespaced: slug }));
|
|
673
|
+
const routedModels = uniqueCatalogModelsForPublicList(models)
|
|
674
|
+
.map(m => ({ provider: m.provider, model: m.id, namespaced: catalogModelSlug(m) }))
|
|
675
|
+
.filter(m => ![...disabled].some(stored => (
|
|
676
|
+
stored === m.namespaced || slugEquals(stored, m.provider, m.model)
|
|
677
|
+
)));
|
|
678
|
+
return jsonResponse({
|
|
679
|
+
roles: config.subagentRoles ?? [],
|
|
680
|
+
...(config.syncCodexAgentRoles === undefined ? {} : { syncCodexAgentRoles: config.syncCodexAgentRoles }),
|
|
681
|
+
syncCodexAgentRolesEffective: agentRolesSyncEffective(config),
|
|
682
|
+
efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
|
|
683
|
+
available: [...nativeModels, ...routedModels],
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
if (url.pathname === "/api/subagent-roles" && req.method === "PUT") {
|
|
687
|
+
let parsedBody: unknown;
|
|
688
|
+
try {
|
|
689
|
+
parsedBody = await readManagementJsonBody(req);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
rethrowManagementBodyTooLarge(error);
|
|
692
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
693
|
+
}
|
|
694
|
+
if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
|
|
695
|
+
return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
696
|
+
}
|
|
697
|
+
const body = parsedBody as { roles?: unknown; remove?: unknown; syncCodexAgentRoles?: unknown };
|
|
698
|
+
if ("remove" in body) {
|
|
699
|
+
if ("roles" in body) return jsonResponse({ error: "body.remove cannot be combined with body.roles" }, 400);
|
|
700
|
+
if (typeof body.remove !== "string" || body.remove.trim().length === 0) {
|
|
701
|
+
return jsonResponse({ error: "body.remove must be a non-empty role id" }, 400);
|
|
702
|
+
}
|
|
703
|
+
const id = body.remove.trim();
|
|
704
|
+
config.subagentRoles = (config.subagentRoles ?? []).filter(role => role.id !== id);
|
|
705
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
706
|
+
save(config);
|
|
707
|
+
const warnings = [...syncCodexAgentRoles(config).warnings];
|
|
708
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
709
|
+
await syncClaudeAgentDefsBestEffort();
|
|
710
|
+
await autoApplyDesktopBestEffort();
|
|
711
|
+
return jsonResponse({
|
|
712
|
+
ok: true,
|
|
713
|
+
roles: config.subagentRoles,
|
|
714
|
+
...(config.syncCodexAgentRoles === undefined ? {} : { syncCodexAgentRoles: config.syncCodexAgentRoles }),
|
|
715
|
+
syncCodexAgentRolesEffective: agentRolesSyncEffective(config),
|
|
716
|
+
warnings,
|
|
717
|
+
catalogRefresh,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (!("roles" in body)) return jsonResponse({ error: "body.roles is required" }, 400);
|
|
721
|
+
const parsed = parseSubagentRoles(body.roles);
|
|
722
|
+
if (!parsed.ok) return jsonResponse({ error: parsed.error, index: parsed.index }, 400);
|
|
723
|
+
if ("syncCodexAgentRoles" in body && body.syncCodexAgentRoles !== null) {
|
|
724
|
+
if (typeof body.syncCodexAgentRoles !== "boolean") {
|
|
725
|
+
return jsonResponse({ error: "syncCodexAgentRoles must be a boolean" }, 400);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const warnings: string[] = [];
|
|
730
|
+
const union = unionRoleModelsIntoRoster(config.subagentModels, parsed.roles);
|
|
731
|
+
if (union.droppedRoleIds.length > 0) {
|
|
732
|
+
warnings.push(`Featured roster truncated to 5 models; dropped role id(s): ${union.droppedRoleIds.join(", ")}`);
|
|
733
|
+
}
|
|
734
|
+
warnings.push(...routedOnV2Warnings(parsed.roles, config));
|
|
735
|
+
|
|
736
|
+
config.subagentRoles = parsed.roles;
|
|
737
|
+
config.subagentModels = union.models;
|
|
738
|
+
if ("syncCodexAgentRoles" in body && typeof body.syncCodexAgentRoles === "boolean") {
|
|
739
|
+
config.syncCodexAgentRoles = body.syncCodexAgentRoles;
|
|
740
|
+
}
|
|
741
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
742
|
+
save(config);
|
|
743
|
+
warnings.push(...syncCodexAgentRoles(config).warnings);
|
|
744
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
745
|
+
await syncClaudeAgentDefsBestEffort();
|
|
746
|
+
await autoApplyDesktopBestEffort();
|
|
747
|
+
return jsonResponse({
|
|
748
|
+
ok: true,
|
|
749
|
+
roles: config.subagentRoles,
|
|
750
|
+
...(config.syncCodexAgentRoles === undefined ? {} : { syncCodexAgentRoles: config.syncCodexAgentRoles }),
|
|
751
|
+
syncCodexAgentRolesEffective: agentRolesSyncEffective(config),
|
|
752
|
+
warnings,
|
|
753
|
+
catalogRefresh,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Priority-ordered subagent model fallback chain for quota-aware spawn routing.
|
|
758
|
+
if (url.pathname === "/api/subagent-model-fallback" && req.method === "GET") {
|
|
759
|
+
const models = await fetchAllModels(config);
|
|
760
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
761
|
+
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
762
|
+
const visibleRouted = [...new Set(models
|
|
763
|
+
.filter(m => ![...disabled].some(stored =>
|
|
764
|
+
stored === catalogModelSlug(m) || slugEquals(stored, m.provider, m.id)
|
|
765
|
+
))
|
|
766
|
+
.map(catalogModelSlug))];
|
|
767
|
+
const available = [
|
|
768
|
+
...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
|
|
769
|
+
...visibleRouted,
|
|
770
|
+
];
|
|
771
|
+
return jsonResponse({
|
|
772
|
+
models: config.subagentModelFallback ?? [],
|
|
773
|
+
pollMs: config.subagentModelFallbackPollMs ?? 60_000,
|
|
774
|
+
available,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (url.pathname === "/api/subagent-model-fallback" && req.method === "PUT") {
|
|
778
|
+
let body: { models?: unknown; pollMs?: unknown };
|
|
779
|
+
try {
|
|
780
|
+
body = await readManagementJsonBody(req);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
rethrowManagementBodyTooLarge(error);
|
|
783
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
784
|
+
}
|
|
785
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
786
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
787
|
+
}
|
|
788
|
+
let nextModels = config.subagentModelFallback;
|
|
789
|
+
let nextPollMs = config.subagentModelFallbackPollMs;
|
|
790
|
+
if ("models" in body) {
|
|
791
|
+
if (!Array.isArray(body.models)) return jsonResponse({ error: "models must be an array" }, 400);
|
|
792
|
+
const models: string[] = [];
|
|
793
|
+
for (let i = 0; i < body.models.length; i++) {
|
|
794
|
+
const entry = body.models[i];
|
|
795
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
796
|
+
return jsonResponse({
|
|
797
|
+
error: `models[${i}] must be a non-empty string`,
|
|
798
|
+
index: i,
|
|
799
|
+
value: entry,
|
|
800
|
+
}, 400);
|
|
801
|
+
}
|
|
802
|
+
models.push(entry.trim());
|
|
803
|
+
}
|
|
804
|
+
nextModels = models.length > 0 ? models : undefined;
|
|
805
|
+
}
|
|
806
|
+
if ("pollMs" in body) {
|
|
807
|
+
const pollMs = body.pollMs;
|
|
808
|
+
if (pollMs === null || pollMs === "") nextPollMs = undefined;
|
|
809
|
+
else if (typeof pollMs === "number" && Number.isInteger(pollMs) && pollMs >= 5_000 && pollMs <= 600_000) {
|
|
810
|
+
nextPollMs = pollMs;
|
|
811
|
+
} else {
|
|
812
|
+
return jsonResponse({ error: "pollMs must be an integer between 5000 and 600000" }, 400);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (nextModels !== undefined) config.subagentModelFallback = nextModels;
|
|
816
|
+
else delete config.subagentModelFallback;
|
|
817
|
+
if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
|
|
818
|
+
else delete config.subagentModelFallbackPollMs;
|
|
819
|
+
saveConfigPreservingClaudeCode(config);
|
|
820
|
+
return jsonResponse({
|
|
821
|
+
ok: true,
|
|
822
|
+
models: config.subagentModelFallback ?? [],
|
|
823
|
+
pollMs: config.subagentModelFallbackPollMs ?? 60_000,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Grok Build: view of the managed fence in ~/.grok/config.toml plus the candidate
|
|
828
|
+
// catalog and the user's selection. The fence itself is still written ONLY by
|
|
829
|
+
// injectGrokConfig — the write routes below carry no path/host/port/body input.
|
|
830
|
+
if (url.pathname === "/api/grok" && req.method === "GET") {
|
|
831
|
+
try {
|
|
832
|
+
const { readGrokStatus } = await import("../../grok/status");
|
|
833
|
+
// `candidates` is the full visible catalog the fence WOULD carry, so the page can
|
|
834
|
+
// show a switch for a model the user has already excluded. Aliases come from
|
|
835
|
+
// `status.models` — the writer's output — never computed client-side.
|
|
836
|
+
return jsonResponse({
|
|
837
|
+
...readGrokStatus(),
|
|
838
|
+
candidates: await fetchGrokCandidateModels(config),
|
|
839
|
+
excluded: config.grokExcludedModels ?? [],
|
|
840
|
+
});
|
|
841
|
+
} catch (error) {
|
|
842
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Writes CONFIG only. ~/.grok/config.toml is still written exclusively by
|
|
847
|
+
// injectGrokConfig, through the apply route below — this route cannot touch that file.
|
|
848
|
+
if (url.pathname === "/api/grok/selection" && req.method === "PUT") {
|
|
849
|
+
let body: { excluded?: unknown };
|
|
850
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
851
|
+
const raw = body.excluded;
|
|
852
|
+
if (!Array.isArray(raw) || raw.some(entry => typeof entry !== "string" || entry.length === 0)) {
|
|
853
|
+
return jsonResponse({ error: "excluded must be an array of model ids" }, 400);
|
|
854
|
+
}
|
|
855
|
+
// Dedupe + sort so the stored list is stable, and cap it so a hostile client
|
|
856
|
+
// cannot grow config.json without bound.
|
|
857
|
+
const excluded = [...new Set(raw as string[])].sort();
|
|
858
|
+
if (excluded.length > 2000) return jsonResponse({ error: "excluded list is too large" }, 400);
|
|
859
|
+
if (excluded.length === 0) delete config.grokExcludedModels;
|
|
860
|
+
else config.grokExcludedModels = excluded;
|
|
861
|
+
saveConfigPreservingClaudeCode(config);
|
|
862
|
+
return jsonResponse({ ok: true, excluded });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Re-runs the SAME sync the CLI runs. All guards (no-grok-home, non-loopback refusal,
|
|
866
|
+
// orphaned marker, backup, alias reservation) live in injectGrokConfig and are not
|
|
867
|
+
// duplicated here. Accepts no body: every input comes from persisted state.
|
|
868
|
+
if (url.pathname === "/api/grok/apply" && req.method === "POST") {
|
|
869
|
+
try {
|
|
870
|
+
const result = await runGrokApplyFlight() as Awaited<ReturnType<typeof import("../../grok/sync")["syncGrokConfig"]>>;
|
|
871
|
+
// A policy skip (non-loopback, no ~/.grok) is not a server error: report it as a
|
|
872
|
+
// result the page can explain rather than a 500 the user cannot act on.
|
|
873
|
+
return jsonResponse({
|
|
874
|
+
ok: result.ok,
|
|
875
|
+
changed: result.changed,
|
|
876
|
+
message: result.message,
|
|
877
|
+
...(result.skippedReason ? { skippedReason: result.skippedReason } : {}),
|
|
878
|
+
}, result.ok ? 200 : 500);
|
|
879
|
+
} catch (error) {
|
|
880
|
+
if (error instanceof GrokApplyBusyError) return jsonResponse({ error: "grok_apply_busy" }, 409);
|
|
881
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Claude Desktop profile: routed/native model assignments for the Desktop 3P config.
|
|
886
|
+
if (url.pathname === "/api/claude-desktop" && req.method === "GET") {
|
|
887
|
+
try {
|
|
888
|
+
const state = await buildClaudeDesktopState(config);
|
|
889
|
+
const runtimePort = Number(url.port) || config.port;
|
|
890
|
+
return jsonResponse({ ...state, port: runtimePort });
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (url.pathname === "/api/claude-desktop" && req.method === "PUT") {
|
|
896
|
+
let body: { profile?: unknown };
|
|
897
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
898
|
+
try {
|
|
899
|
+
const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile");
|
|
900
|
+
const parsed = parseDesktopProfile(body.profile);
|
|
901
|
+
const current = await buildClaudeDesktopState(config);
|
|
902
|
+
for (const model of current.models.filter(item => !item.available)) {
|
|
903
|
+
const before = current.profile.assignments[model.route];
|
|
904
|
+
const after = parsed.assignments[model.route];
|
|
905
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
906
|
+
throw new Error(`현재 사용할 수 없는 모델은 옮길 수 없습니다: ${model.route}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
for (const family of ["opus", "fable", "sonnet", "haiku"] as const) {
|
|
910
|
+
const nextDefault = parsed.defaults[family];
|
|
911
|
+
const target = nextDefault ? current.models.find(model => model.route === nextDefault) : undefined;
|
|
912
|
+
if (target && !target.available && current.profile.defaults[family] !== nextDefault) {
|
|
913
|
+
throw new Error(`현재 사용할 수 없는 모델은 기본값으로 지정할 수 없습니다: ${nextDefault}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const state = await buildClaudeDesktopState(config, parsed);
|
|
917
|
+
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: reconcileDesktopProfile(state.profile, state.models) };
|
|
918
|
+
saveConfigPreservingClaudeCode(config);
|
|
919
|
+
const saved = await buildClaudeDesktopState(config);
|
|
920
|
+
const runtimePort = Number(url.port) || config.port;
|
|
921
|
+
return jsonResponse({ ok: true, ...saved, port: runtimePort });
|
|
922
|
+
} catch (error) {
|
|
923
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (url.pathname === "/api/claude-desktop/apply" && req.method === "POST") {
|
|
927
|
+
try {
|
|
928
|
+
// #859: the CLI delegates here so the registry is built in the serving
|
|
929
|
+
// process. Accept an optional mode; default stays static for back-compat.
|
|
930
|
+
let mode: "static" | "hybrid" | "discovery" = "static";
|
|
931
|
+
let parsed: unknown;
|
|
932
|
+
try {
|
|
933
|
+
parsed = await readOptionalManagementJsonBody(req);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
rethrowManagementBodyTooLarge(error);
|
|
936
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
937
|
+
}
|
|
938
|
+
const requested = (parsed as { mode?: unknown } | null)?.mode;
|
|
939
|
+
if (requested !== undefined) {
|
|
940
|
+
if (requested === "static" || requested === "hybrid" || requested === "discovery") {
|
|
941
|
+
mode = requested;
|
|
942
|
+
} else {
|
|
943
|
+
return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// #859: a delegated CLI apply carries the profile it just saved — the
|
|
947
|
+
// daemon's own config can be older, and building state from it would
|
|
948
|
+
// apply (and persist) the stale profile over the newer one.
|
|
949
|
+
const bodyProfile = (parsed as { profile?: unknown } | null)?.profile;
|
|
950
|
+
let profileOverride: Parameters<typeof buildClaudeDesktopState>[1];
|
|
951
|
+
if (bodyProfile !== undefined) {
|
|
952
|
+
const { parseDesktopProfile } = await import("../../claude/desktop-profile");
|
|
953
|
+
try {
|
|
954
|
+
profileOverride = parseDesktopProfile(bodyProfile);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
960
|
+
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
961
|
+
if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
|
|
962
|
+
// Disk now says ON; the reused server snapshot must agree, or the native
|
|
963
|
+
// GET reports OFF and a later whole-snapshot save undoes this transition.
|
|
964
|
+
mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
|
|
965
|
+
const state = await buildClaudeDesktopState(config, profileOverride);
|
|
966
|
+
// `setIntegrationEnabled` above wrote desired ON to DISK; it does not touch
|
|
967
|
+
// this long-lived server snapshot. Saving the snapshot wholesale would carry
|
|
968
|
+
// its stale `clientIntegrations` back over that write and turn the enable
|
|
969
|
+
// action into an immediate self-cancelling OFF — the guard below would then
|
|
970
|
+
// refuse the apply it was asked to perform. Persist ONLY the profile field.
|
|
971
|
+
const profileSaved = persistDesktopProfileField(config, state.profile);
|
|
972
|
+
if (!profileSaved.ok) {
|
|
973
|
+
return jsonResponse({
|
|
974
|
+
error: `Claude Desktop profile could not be saved (${profileSaved.reason}); nothing was applied.`,
|
|
975
|
+
saved: false,
|
|
976
|
+
applied: false,
|
|
977
|
+
}, profileSaved.reason === "conflict" ? 409 : 500);
|
|
978
|
+
}
|
|
979
|
+
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
980
|
+
const { desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
981
|
+
const routed = state.models
|
|
982
|
+
.filter(model => model.available && !model.route.startsWith("native/"))
|
|
983
|
+
.map(model => {
|
|
984
|
+
const slash = model.route.indexOf("/");
|
|
985
|
+
return { provider: model.route.slice(0, slash), id: model.route.slice(slash + 1), contextWindow: model.contextWindow };
|
|
986
|
+
});
|
|
987
|
+
// State construction can await catalog work; never write from the stale
|
|
988
|
+
// config captured before that await if another request turned Desktop off.
|
|
989
|
+
const latest = loadConfig();
|
|
990
|
+
if (!claudeDesktopIntegrationEnabled(latest)) {
|
|
991
|
+
return jsonResponse({
|
|
992
|
+
error: "Claude Desktop apply was cancelled because the desired state changed to off.",
|
|
993
|
+
code: "claude_desktop_apply_skipped",
|
|
994
|
+
reason: "desired_state_changed",
|
|
995
|
+
desiredEnabled: false,
|
|
996
|
+
saved: true,
|
|
997
|
+
applied: false,
|
|
998
|
+
}, 409);
|
|
999
|
+
}
|
|
1000
|
+
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
1001
|
+
Number(url.port) || latest.port,
|
|
1002
|
+
[...desktopVisibleNativeSlugs(latest)],
|
|
1003
|
+
routed,
|
|
1004
|
+
latest.apiKeys?.[0]?.key,
|
|
1005
|
+
mode,
|
|
1006
|
+
state.profile,
|
|
1007
|
+
nativeContextLimits(latest),
|
|
1008
|
+
);
|
|
1009
|
+
if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
|
|
1010
|
+
// Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
|
|
1011
|
+
if (result.fingerprint) {
|
|
1012
|
+
// The Desktop write already landed, so a failed bookkeeping save is not
|
|
1013
|
+
// an apply failure: report the miss instead of claiming a clean apply.
|
|
1014
|
+
const marked = persistDesktopProfileField(config, {
|
|
1015
|
+
...state.profile,
|
|
1016
|
+
appliedFingerprint: result.fingerprint,
|
|
1017
|
+
appliedAt: new Date().toISOString(),
|
|
1018
|
+
});
|
|
1019
|
+
if (!marked.ok) {
|
|
1020
|
+
return jsonResponse({
|
|
1021
|
+
ok: true,
|
|
1022
|
+
applied: true,
|
|
1023
|
+
saved: false,
|
|
1024
|
+
path: result.path,
|
|
1025
|
+
fingerprint: result.fingerprint,
|
|
1026
|
+
warning: `Claude Desktop was applied, but the applied marker was not saved (${marked.reason}).`,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return jsonResponse({ ok: true, saved: true, applied: true, path: result.path, fingerprint: result.fingerprint });
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
rethrowManagementBodyTooLarge(error);
|
|
1033
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Desktop applied-state + health status.
|
|
1038
|
+
if (url.pathname === "/api/claude-desktop/status" && req.method === "GET") {
|
|
1039
|
+
try {
|
|
1040
|
+
const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
1041
|
+
const { inspectDesktop3pConfigLibrary } = await import("../../claude/desktop-3p");
|
|
1042
|
+
const persisted = loadConfig();
|
|
1043
|
+
const savedFingerprint = persisted.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
1044
|
+
const observed = inspectDesktop3pConfigLibrary({ appliedFingerprint: savedFingerprint });
|
|
1045
|
+
const desiredEnabled = claudeDesktopIntegrationEnabled(persisted);
|
|
1046
|
+
const applied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
|
|
1047
|
+
// "Needs update" is only meaningful while the integration is wanted. When the
|
|
1048
|
+
// durable switch is OFF, a leftover drifted profile is residue to clear — not
|
|
1049
|
+
// a stale apply the operator should refresh.
|
|
1050
|
+
const stale = desiredEnabled && observed.kind === "gateway_drifted";
|
|
1051
|
+
const { getDesktopHealth } = await import("../../claude/desktop-health");
|
|
1052
|
+
const health = getDesktopHealth();
|
|
1053
|
+
return jsonResponse({
|
|
1054
|
+
desiredEnabled,
|
|
1055
|
+
installed: observed.kind !== "not_installed",
|
|
1056
|
+
observedKind: observed.kind,
|
|
1057
|
+
applied,
|
|
1058
|
+
appliedAt: persisted.claudeCode?.desktopProfile?.appliedAt ?? null,
|
|
1059
|
+
savedFingerprint,
|
|
1060
|
+
onDiskFingerprint: observed.fingerprint ?? null,
|
|
1061
|
+
configPath: observed.selectedProfilePath,
|
|
1062
|
+
stale,
|
|
1063
|
+
// Tri-state by ID match, independent of profile health: null =
|
|
1064
|
+
// undeterminable (no/unreadable metadata or no appliedId); a readable
|
|
1065
|
+
// appliedId with no owned entry is a KNOWN false. Predates the inspector.
|
|
1066
|
+
activeProfile: observed.ownedProfileActive,
|
|
1067
|
+
drift: desiredEnabled ? !applied || stale : applied || observed.kind === "unsafe",
|
|
1068
|
+
driftReason: desiredEnabled ? (!applied ? "desired_on_not_current" : stale ? "profile_drift" : null) : (applied ? "desired_off_gateway_selected" : null),
|
|
1069
|
+
health,
|
|
1070
|
+
});
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Claude Code inbound settings (GUI "Claude ON" toggle + Claude page).
|
|
1077
|
+
if (url.pathname === "/api/claude-code" && req.method === "GET") {
|
|
1078
|
+
const models = await fetchAllModels(config);
|
|
1079
|
+
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
1080
|
+
const { claudeCodeAlias, claudeCodeNativeAlias } = await import("../../claude/alias");
|
|
1081
|
+
const { buildClaudeContextWindows, effectiveModelEnv } = await import("../../claude/context-windows");
|
|
1082
|
+
const { visibleNativeSlugs } = await import("../../codex/catalog");
|
|
1083
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
1084
|
+
const isDisabled = (provider: string, id: string) =>
|
|
1085
|
+
[...disabled].some(stored => slugEquals(stored, provider, id));
|
|
1086
|
+
const available = [
|
|
1087
|
+
...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
|
|
1088
|
+
// Claude-facing values stay RAW native selectors (resolved inbound via routeModel,
|
|
1089
|
+
// which accepts the raw full-slash form); only the disabled check goes tolerant.
|
|
1090
|
+
...models.filter(m => !isDisabled(m.provider, m.id)).map(m => `${m.provider}/${m.id}`),
|
|
1091
|
+
];
|
|
1092
|
+
const aliases: { id: string; display_name: string }[] = [];
|
|
1093
|
+
for (const slug of listCatalogNativeSlugs()) {
|
|
1094
|
+
// Readable CLI-surface alias with hash fallback (devlog 050 / audit 051 #2) —
|
|
1095
|
+
// the same shared helper the /v1/models ?ids=cli path uses.
|
|
1096
|
+
if (!disabled.has(slug)) aliases.push({ id: claudeCodeNativeAlias(slug), display_name: `${slug} (native)` });
|
|
1097
|
+
}
|
|
1098
|
+
for (const m of models) {
|
|
1099
|
+
if (isDisabled(m.provider, m.id)) continue;
|
|
1100
|
+
aliases.push({ id: claudeCodeAlias(m.provider, m.id), display_name: `${m.id} (${m.provider})` });
|
|
1101
|
+
}
|
|
1102
|
+
const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models, nativeContextLimits(config));
|
|
1103
|
+
const webSearchOverride = config.claudeCode?.webSearchSidecar;
|
|
1104
|
+
const visionOverride = config.claudeCode?.visionSidecar;
|
|
1105
|
+
// Auto is a RESOLUTION, recomputed per request — never stored state. Detection is
|
|
1106
|
+
// daemon-side, so it cannot see a key exported only in the user's terminal; the
|
|
1107
|
+
// GUI labels the badge with detectionScope for exactly that reason.
|
|
1108
|
+
const { defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens } = await import("../../claude/auth-detect");
|
|
1109
|
+
const { authModeIntent, resolveClaudeAuthMode } = await import("../../claude/auth-mode");
|
|
1110
|
+
const authDetection = detectClaudeAuth(defaultAuthDetectDeps(process.env, ownAdmissionTokens(config)));
|
|
1111
|
+
const resolvedAuthMode = resolveClaudeAuthMode(config, authDetection);
|
|
1112
|
+
return jsonResponse({
|
|
1113
|
+
enabled: config.claudeCode?.enabled !== false,
|
|
1114
|
+
// Three-state intent (devlog 260726_claude_auth_auto): an absent key is AUTO, not
|
|
1115
|
+
// subscription. The old coercion made every save convert an untouched auto config
|
|
1116
|
+
// into a sticky manual subscription with no way back.
|
|
1117
|
+
authMode: authModeIntent(config),
|
|
1118
|
+
/** Does the opencodex dummy marker get injected — NOT a claim about native auth. */
|
|
1119
|
+
markerMode: resolvedAuthMode.markerMode,
|
|
1120
|
+
authModeOrigin: resolvedAuthMode.origin,
|
|
1121
|
+
...(resolvedAuthMode.foundBy ? { authFoundBy: resolvedAuthMode.foundBy } : {}),
|
|
1122
|
+
authDetectionUnknown: authDetection.presence === "unknown",
|
|
1123
|
+
// Separate axis: with an admission key configured a token is injected regardless
|
|
1124
|
+
// of mode, so the GUI must never present subscription as "no token anywhere".
|
|
1125
|
+
admissionKeyActive: (config.apiKeys?.length ?? 0) > 0,
|
|
1126
|
+
detectionScope: "daemon",
|
|
1127
|
+
model: config.claudeCode?.model ?? "",
|
|
1128
|
+
smallFastModel: config.claudeCode?.smallFastModel ?? "",
|
|
1129
|
+
tierModels: config.claudeCode?.tierModels ?? {},
|
|
1130
|
+
modelMap: config.claudeCode?.modelMap ?? {},
|
|
1131
|
+
classifierModel: config.claudeCode?.classifierModel ?? "",
|
|
1132
|
+
classifierFallbacks: config.claudeCode?.classifierFallbacks ?? [],
|
|
1133
|
+
systemEnv: config.claudeCode?.systemEnv === true,
|
|
1134
|
+
autoConnectSupported: (ctx.deps.platform ?? process.platform) === "darwin",
|
|
1135
|
+
maxContextTokens: config.claudeCode?.maxContextTokens ?? null,
|
|
1136
|
+
alwaysEnableEffort: config.claudeCode?.alwaysEnableEffort === true,
|
|
1137
|
+
autoContext: config.claudeCode?.autoContext !== false,
|
|
1138
|
+
autoCompactWindow: config.claudeCode?.autoCompactWindow ?? null,
|
|
1139
|
+
blockedSkills: config.claudeCode?.blockedSkills ?? null,
|
|
1140
|
+
injectAgents: config.claudeCode?.injectAgents !== false,
|
|
1141
|
+
...(webSearchOverride && Object.keys(webSearchOverride).length > 0
|
|
1142
|
+
? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model } }
|
|
1143
|
+
: {}),
|
|
1144
|
+
...(visionOverride && Object.keys(visionOverride).length > 0
|
|
1145
|
+
? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model } }
|
|
1146
|
+
: {}),
|
|
1147
|
+
fastMode: config.fastMode,
|
|
1148
|
+
contextWindows,
|
|
1149
|
+
effectiveModelEnv: effectiveModelEnv(config.claudeCode, contextWindows),
|
|
1150
|
+
available,
|
|
1151
|
+
aliases,
|
|
1152
|
+
port: config.port,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
if (url.pathname === "/api/claude-code" && req.method === "PUT") {
|
|
1156
|
+
// NOTE: model / tierModels / maxContextTokens / alwaysEnableEffort are
|
|
1157
|
+
// CONFIG-ONLY back-compat fields — the GUI no longer offers controls for them
|
|
1158
|
+
// (default model is owned by Claude Code's /model picker; roster agents
|
|
1159
|
+
// supersede tiers; auto-context supersedes the max-context pair; effort rides
|
|
1160
|
+
// regardless on 2.1.207). PUT keeps validating them so hand-written configs
|
|
1161
|
+
// and older GUIs stay safe; GUI saves omit them and the spread preserves them.
|
|
1162
|
+
let parsedBody: unknown;
|
|
1163
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1164
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
1165
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1166
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1167
|
+
return prototype === Object.prototype || prototype === null;
|
|
1168
|
+
};
|
|
1169
|
+
if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
1170
|
+
const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1171
|
+
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1172
|
+
const section = body[field];
|
|
1173
|
+
if (section === undefined || section === null) continue;
|
|
1174
|
+
if (!isPlainObject(section)) return jsonResponse({ error: `${field} must be an object or null` }, 400);
|
|
1175
|
+
// Both overrides now speak their full unions (roadmap 060 web, 170
|
|
1176
|
+
// vision revised). Vision's third arm is "routed" (loopback through the
|
|
1177
|
+
// proxy's own router), never exa: exa is not an LLM, and accepting an
|
|
1178
|
+
// unknown literal would persist a backend the vision resolver reads as
|
|
1179
|
+
// unset (review F1's failure mode).
|
|
1180
|
+
const allowedBackends = field === "webSearchSidecar"
|
|
1181
|
+
? ["openai", "anthropic", "xai", "gemini", "exa"]
|
|
1182
|
+
: ["openai", "anthropic", "routed"];
|
|
1183
|
+
if (section.backend !== undefined && section.backend !== null
|
|
1184
|
+
&& !allowedBackends.includes(section.backend as string)) {
|
|
1185
|
+
return jsonResponse({ error: `${field}.backend must be ${allowedBackends.join(", ")}, or null` }, 400);
|
|
1186
|
+
}
|
|
1187
|
+
if (section.model !== undefined && typeof section.model !== "string") {
|
|
1188
|
+
return jsonResponse({ error: `${field}.model must be a string` }, 400);
|
|
1189
|
+
}
|
|
1190
|
+
// Vision override only: reject a model we can prove is blind. Unknown ids stay
|
|
1191
|
+
// allowed. Shares one policy module with /api/sidecar-settings so the two
|
|
1192
|
+
// gates cannot drift.
|
|
1193
|
+
if (field === "visionSidecar" && typeof section.model === "string" && section.model !== "") {
|
|
1194
|
+
const requested = section.model;
|
|
1195
|
+
const candidates = await visionCandidateRows(config);
|
|
1196
|
+
const hint = section.backend === "anthropic" || section.backend === "openai"
|
|
1197
|
+
|| section.backend === "routed"
|
|
1198
|
+
? section.backend
|
|
1199
|
+
: config.claudeCode?.visionSidecar?.backend;
|
|
1200
|
+
// Same coherence rule as /api/sidecar-settings (roadmap 170 r2).
|
|
1201
|
+
const effectiveBackend = hint ?? "openai";
|
|
1202
|
+
const namespaced = requested.includes("/");
|
|
1203
|
+
if (namespaced && effectiveBackend !== "routed") {
|
|
1204
|
+
return jsonResponse({ error: `visionSidecar.model "${requested}" is provider-namespaced; it requires backend "routed"` }, 400);
|
|
1205
|
+
}
|
|
1206
|
+
if (!namespaced && effectiveBackend === "routed") {
|
|
1207
|
+
return jsonResponse({ error: `visionSidecar.backend "routed" requires a provider-namespaced model ("provider/model"); got "${requested}"` }, 400);
|
|
1208
|
+
}
|
|
1209
|
+
if (visionDescriberIsProvablyBlind(config, requested, candidates, hint)) {
|
|
1210
|
+
return jsonResponse(visionDescriberRejection("visionSidecar.model", requested, config, candidates), 400);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Web-search override: membership gate (#2188). The executor set is closed,
|
|
1214
|
+
// so an id outside (runnable candidates ∪ auth slots) can never run. Same
|
|
1215
|
+
// module as /api/sidecar-settings — a gate on one route and a stale copy on
|
|
1216
|
+
// the other is no gate at all.
|
|
1217
|
+
if (field === "webSearchSidecar"
|
|
1218
|
+
&& (section.model !== undefined || section.backend !== undefined)) {
|
|
1219
|
+
const stored = config.claudeCode?.webSearchSidecar;
|
|
1220
|
+
const effectiveBackend = section.backend === "anthropic"
|
|
1221
|
+
? "anthropic"
|
|
1222
|
+
: section.backend === "openai"
|
|
1223
|
+
? "openai"
|
|
1224
|
+
: section.backend === null
|
|
1225
|
+
? config.webSearchSidecar?.backend ?? "openai"
|
|
1226
|
+
: stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
|
|
1227
|
+
const effectiveModel = section.model === ""
|
|
1228
|
+
? config.webSearchSidecar?.model
|
|
1229
|
+
: typeof section.model === "string"
|
|
1230
|
+
? section.model
|
|
1231
|
+
: stored?.model ?? config.webSearchSidecar?.model;
|
|
1232
|
+
const candidates = await webSearchCandidateRows(config);
|
|
1233
|
+
if (effectiveModel && webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
|
|
1234
|
+
return jsonResponse(webSearchModelRejection(
|
|
1235
|
+
"webSearchSidecar.model",
|
|
1236
|
+
effectiveBackend,
|
|
1237
|
+
effectiveModel,
|
|
1238
|
+
candidates,
|
|
1239
|
+
), 400);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const next = { ...(config.claudeCode ?? {}) };
|
|
1244
|
+
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1245
|
+
const section = body[field];
|
|
1246
|
+
if (section === undefined) continue;
|
|
1247
|
+
if (section === null || Object.keys(section as Record<string, unknown>).length === 0) {
|
|
1248
|
+
delete next[field];
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
// The per-field validation above guarantees vision only ever carries the two-member
|
|
1252
|
+
// union; the cast is the loop's shared-shape compromise, not a wider write path.
|
|
1253
|
+
const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string };
|
|
1254
|
+
const override = { ...next[field] } as NonNullable<OcxClaudeCodeConfig[typeof field]>;
|
|
1255
|
+
if (requested.backend === null) delete override.backend;
|
|
1256
|
+
else if (requested.backend !== undefined) override.backend = requested.backend as never;
|
|
1257
|
+
if (requested.model === "") delete override.model;
|
|
1258
|
+
else if (requested.model !== undefined) override.model = requested.model;
|
|
1259
|
+
// Indexed write across the field union collapses to an intersection; runtime
|
|
1260
|
+
// validation above already guarantees the per-field shape.
|
|
1261
|
+
if (Object.keys(override).length > 0) next[field] = override as never;
|
|
1262
|
+
else delete next[field];
|
|
1263
|
+
}
|
|
1264
|
+
if (body.enabled !== undefined) {
|
|
1265
|
+
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
1266
|
+
next.enabled = body.enabled;
|
|
1267
|
+
}
|
|
1268
|
+
if (body.authMode !== undefined) {
|
|
1269
|
+
// Three-state intent: "proxy" and "subscription" are stored literally and stick
|
|
1270
|
+
// forever; "auto" DELETES the key so the mode is resolved from detected Claude
|
|
1271
|
+
// auth on every launch. The 260720 round-trip contract survives as a superset —
|
|
1272
|
+
// storing "subscription" literally is also backward-safe, since older readers
|
|
1273
|
+
// only ever recognised "proxy".
|
|
1274
|
+
if (body.authMode !== "proxy" && body.authMode !== "subscription" && body.authMode !== "auto") {
|
|
1275
|
+
return jsonResponse({ error: "authMode must be \"auto\", \"proxy\", or \"subscription\"" }, 400);
|
|
1276
|
+
}
|
|
1277
|
+
if (body.authMode === "auto") delete next.authMode;
|
|
1278
|
+
else next.authMode = body.authMode;
|
|
1279
|
+
}
|
|
1280
|
+
if (body.systemEnv !== undefined) {
|
|
1281
|
+
if (typeof body.systemEnv !== "boolean") return jsonResponse({ error: "systemEnv must be a boolean" }, 400);
|
|
1282
|
+
next.systemEnv = body.systemEnv;
|
|
1283
|
+
}
|
|
1284
|
+
if (body.alwaysEnableEffort !== undefined) {
|
|
1285
|
+
if (typeof body.alwaysEnableEffort !== "boolean") return jsonResponse({ error: "alwaysEnableEffort must be a boolean" }, 400);
|
|
1286
|
+
if (body.alwaysEnableEffort) next.alwaysEnableEffort = true;
|
|
1287
|
+
else delete next.alwaysEnableEffort;
|
|
1288
|
+
}
|
|
1289
|
+
if (body.maxContextTokens !== undefined) {
|
|
1290
|
+
// CONFIG-ONLY back-compat (GUI control removed — superseded by auto-context):
|
|
1291
|
+
// null clears; otherwise a positive integer (devlog 136 B6).
|
|
1292
|
+
if (body.maxContextTokens === null) {
|
|
1293
|
+
delete next.maxContextTokens;
|
|
1294
|
+
} else if (typeof body.maxContextTokens !== "number" || !Number.isInteger(body.maxContextTokens) || body.maxContextTokens <= 0) {
|
|
1295
|
+
return jsonResponse({ error: "maxContextTokens must be a positive integer or null" }, 400);
|
|
1296
|
+
} else {
|
|
1297
|
+
next.maxContextTokens = body.maxContextTokens;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (body.autoContext !== undefined) {
|
|
1301
|
+
// Default-on boolean (devlog 260712 020): true = drop the key, false = store.
|
|
1302
|
+
if (typeof body.autoContext !== "boolean") return jsonResponse({ error: "autoContext must be a boolean" }, 400);
|
|
1303
|
+
if (body.autoContext) delete next.autoContext;
|
|
1304
|
+
else next.autoContext = false;
|
|
1305
|
+
}
|
|
1306
|
+
if (body.injectAgents !== undefined) {
|
|
1307
|
+
// Default-on boolean (devlog 260712 070): true = drop the key, false = store.
|
|
1308
|
+
if (typeof body.injectAgents !== "boolean") return jsonResponse({ error: "injectAgents must be a boolean" }, 400);
|
|
1309
|
+
if (body.injectAgents) delete next.injectAgents;
|
|
1310
|
+
else next.injectAgents = false;
|
|
1311
|
+
}
|
|
1312
|
+
if (body.autoCompactWindow !== undefined) {
|
|
1313
|
+
// null resets to the 350k default; otherwise the binary-accepted range
|
|
1314
|
+
// 100_000..1_000_000 (2.1.207 pSo/yDs — audit 021 #1).
|
|
1315
|
+
if (body.autoCompactWindow === null) {
|
|
1316
|
+
delete next.autoCompactWindow;
|
|
1317
|
+
} else if (typeof body.autoCompactWindow !== "number" || !Number.isInteger(body.autoCompactWindow) || body.autoCompactWindow < 100_000 || body.autoCompactWindow > 1_000_000) {
|
|
1318
|
+
return jsonResponse({ error: "autoCompactWindow must be an integer between 100000 and 1000000, or null" }, 400);
|
|
1319
|
+
} else {
|
|
1320
|
+
next.autoCompactWindow = body.autoCompactWindow;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (body.blockedSkills !== undefined) {
|
|
1324
|
+
// null resets to the default (["claude-api"]); an array (possibly empty = off)
|
|
1325
|
+
// must contain non-empty strings (devlog 060).
|
|
1326
|
+
if (body.blockedSkills === null) {
|
|
1327
|
+
delete next.blockedSkills;
|
|
1328
|
+
} else if (!Array.isArray(body.blockedSkills) || body.blockedSkills.some(s => typeof s !== "string" || s.trim() === "")) {
|
|
1329
|
+
return jsonResponse({ error: "blockedSkills must be an array of non-empty strings, or null" }, 400);
|
|
1330
|
+
} else {
|
|
1331
|
+
next.blockedSkills = (body.blockedSkills as string[]).map(s => s.trim());
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (body.tierModels !== undefined) {
|
|
1335
|
+
// CONFIG-ONLY back-compat (GUI pickers removed — roster agents supersede tiers).
|
|
1336
|
+
if (body.tierModels === null) {
|
|
1337
|
+
delete next.tierModels;
|
|
1338
|
+
} else if (!isPlainObject(body.tierModels)) {
|
|
1339
|
+
return jsonResponse({ error: "tierModels must be an object with string values, or null" }, 400);
|
|
1340
|
+
} else {
|
|
1341
|
+
for (const [tier, value] of Object.entries(body.tierModels)) {
|
|
1342
|
+
if (typeof value !== "string") return jsonResponse({ error: `tierModels.${tier} must be a string` }, 400);
|
|
1343
|
+
}
|
|
1344
|
+
const tierModels = body.tierModels as Record<string, string>;
|
|
1345
|
+
const tiers: Record<string, string> = {};
|
|
1346
|
+
for (const tier of ["opus", "sonnet", "haiku", "fable"] as const) {
|
|
1347
|
+
const value = tierModels[tier];
|
|
1348
|
+
if (value !== undefined && value.trim() !== "") tiers[tier] = value.trim();
|
|
1349
|
+
}
|
|
1350
|
+
if (Object.keys(tiers).length > 0) next.tierModels = tiers;
|
|
1351
|
+
else delete next.tierModels;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
let nextFastMode = config.fastMode;
|
|
1355
|
+
if (body.fastMode !== undefined) {
|
|
1356
|
+
if (body.fastMode !== true && body.fastMode !== false && body.fastMode !== null) {
|
|
1357
|
+
return jsonResponse({ error: "fastMode must be true, false, or null" }, 400);
|
|
1358
|
+
}
|
|
1359
|
+
nextFastMode = body.fastMode === null ? undefined : body.fastMode;
|
|
1360
|
+
}
|
|
1361
|
+
for (const field of ["model", "smallFastModel", "classifierModel"] as const) {
|
|
1362
|
+
const value = body[field];
|
|
1363
|
+
if (value === undefined) continue;
|
|
1364
|
+
if (typeof value !== "string") return jsonResponse({ error: `${field} must be a string` }, 400);
|
|
1365
|
+
if (value.trim() === "") delete next[field];
|
|
1366
|
+
else next[field] = value.trim();
|
|
1367
|
+
}
|
|
1368
|
+
if (body.classifierFallbacks !== undefined) {
|
|
1369
|
+
if (body.classifierFallbacks === null) {
|
|
1370
|
+
delete next.classifierFallbacks;
|
|
1371
|
+
} else {
|
|
1372
|
+
if (!Array.isArray(body.classifierFallbacks)) {
|
|
1373
|
+
return jsonResponse({ error: "classifierFallbacks must be an array of strings, or null" }, 400);
|
|
1374
|
+
}
|
|
1375
|
+
const list: string[] = [];
|
|
1376
|
+
for (const entry of body.classifierFallbacks) {
|
|
1377
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
1378
|
+
return jsonResponse({ error: "classifierFallbacks entries must be non-empty strings" }, 400);
|
|
1379
|
+
}
|
|
1380
|
+
list.push(entry.trim());
|
|
1381
|
+
}
|
|
1382
|
+
if (list.length > 0) next.classifierFallbacks = list;
|
|
1383
|
+
else delete next.classifierFallbacks;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (body.modelMap !== undefined) {
|
|
1387
|
+
if (body.modelMap === null) {
|
|
1388
|
+
delete next.modelMap;
|
|
1389
|
+
} else {
|
|
1390
|
+
if (!isPlainObject(body.modelMap)) {
|
|
1391
|
+
return jsonResponse({ error: "modelMap must be an object of string->string, or null" }, 400);
|
|
1392
|
+
}
|
|
1393
|
+
const map: Record<string, string> = {};
|
|
1394
|
+
for (const [k, v] of Object.entries(body.modelMap)) {
|
|
1395
|
+
if (typeof v !== "string" || k.trim() === "" || v.trim() === "") {
|
|
1396
|
+
return jsonResponse({ error: "modelMap entries must be non-empty strings" }, 400);
|
|
1397
|
+
}
|
|
1398
|
+
map[k.trim()] = v.trim();
|
|
1399
|
+
}
|
|
1400
|
+
if (Object.keys(map).length > 0) next.modelMap = map;
|
|
1401
|
+
else delete next.modelMap;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (body.fastMode !== undefined) config.fastMode = nextFastMode;
|
|
1405
|
+
config.claudeCode = next;
|
|
1406
|
+
// Stamp the migration sentinel on EVERY persist of this block. The migration reads
|
|
1407
|
+
// "a claudeCode block with no authMode" as a pre-upgrade subscriber and pins it to
|
|
1408
|
+
// literal subscription — correct for a config written before `auto` existed, fatal
|
|
1409
|
+
// for one written after. Without this, choosing Auto (which DELETES authMode) or
|
|
1410
|
+
// merely toggling Claude on (App.tsx PUTs `{enabled}` alone and creates the block)
|
|
1411
|
+
// would be converted into a sticky manual subscription by the next startServer, and
|
|
1412
|
+
// auto would survive exactly one proxy lifetime with no way back.
|
|
1413
|
+
if (!next.authModeMigratedAt) next.authModeMigratedAt = new Date().toISOString();
|
|
1414
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
1415
|
+
save(config);
|
|
1416
|
+
const warnings: string[] = [];
|
|
1417
|
+
// authMode changes must reconcile the injected system env too: switching back to
|
|
1418
|
+
// Subscription has to remove the opencodex-owned dummy ANTHROPIC_AUTH_TOKEN
|
|
1419
|
+
// (audit R1 blocker #1/#2, devlog 260720_claude_authmode_persist).
|
|
1420
|
+
if (body.systemEnv !== undefined || body.authMode !== undefined) {
|
|
1421
|
+
try {
|
|
1422
|
+
await applySystemEnvToggle(config, config.port);
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
warnings.push(`Failed to apply system environment setting: ${err instanceof Error ? err.message : String(err)}`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// Keep the file-backed live registry symmetric: OFF prunes immediately, while
|
|
1428
|
+
// ON and config changes restore definitions without requiring a restart.
|
|
1429
|
+
await syncClaudeAgentDefsBestEffort();
|
|
1430
|
+
return jsonResponse({ ok: true, enabled: next.enabled !== false, warnings });
|
|
1431
|
+
}
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|