@iislee/opencodex 2.11.0 → 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/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -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/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- 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 +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- 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 +6 -3
- 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/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- 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 +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- 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 +66 -4
- 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 +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- 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 +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- 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 +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- 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 +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- 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/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -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/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- 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 +33 -5
- 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.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- 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 +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- 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 +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- 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 +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- 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 +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- 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 +101 -32
- package/src/server/management/model-routes.ts +189 -131
- 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 +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- 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 +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- 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 +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- 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 +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- 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 +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -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 +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- 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 +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -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 +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- 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 +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- 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 +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
package/src/server/index.ts
CHANGED
|
@@ -16,16 +16,24 @@ import {
|
|
|
16
16
|
armClaudeCodeBaseline,
|
|
17
17
|
loadConfig,
|
|
18
18
|
saveConfig,
|
|
19
|
+
getConfigDir,
|
|
19
20
|
websocketsEnabled,
|
|
20
21
|
} from "../config";
|
|
21
22
|
import { reconcileOAuthProviders } from "../oauth";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
24
|
+
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
25
|
+
import { getCodexHome } from "../codex/paths";
|
|
26
|
+
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
27
|
+
import {
|
|
28
|
+
inspectNativeCodexOwnership,
|
|
29
|
+
type OwnershipInspection,
|
|
30
|
+
} from "../integrations/native/ownership-preflight";
|
|
31
|
+
import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
|
|
24
32
|
import {
|
|
25
33
|
reconcileLiveStateStores,
|
|
26
34
|
setLiveStateStoreConfig,
|
|
27
35
|
} from "../lib/state-store-registrations";
|
|
28
|
-
import {
|
|
36
|
+
import { startUserCostOverlayReconciler } from "../usage/user-cost-overlay-reconciler";
|
|
29
37
|
import {
|
|
30
38
|
configureAppOwnedMemoryBudget,
|
|
31
39
|
enforceAppOwnedMemoryBudget,
|
|
@@ -36,18 +44,26 @@ import {
|
|
|
36
44
|
registerDefaultAppOwnedMemoryStores,
|
|
37
45
|
registerDefaultAppOwnedObservedBuffers,
|
|
38
46
|
} from "../lib/app-owned-memory-stores";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
47
|
+
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
48
|
+
import { activateLab, labActivationRequired } from "../lib/lab-activation";
|
|
42
49
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
43
50
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
44
|
-
import {
|
|
51
|
+
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
52
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
45
53
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
46
54
|
import type { StorageCleanupPolicy } from "../types";
|
|
55
|
+
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
|
|
47
56
|
import {
|
|
48
57
|
CodexAccountCooldownError,
|
|
49
58
|
cooldownErrorMessage,
|
|
50
59
|
} from "../codex/auth-context";
|
|
60
|
+
import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
|
|
61
|
+
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex/account-namespaces";
|
|
62
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
63
|
+
import {
|
|
64
|
+
availableAccountGatedNativeModels,
|
|
65
|
+
resolveCodexModelEntitlements,
|
|
66
|
+
} from "../codex/model-entitlements";
|
|
51
67
|
export {
|
|
52
68
|
clearThreadAccountMap,
|
|
53
69
|
formatCodexProviderForLog,
|
|
@@ -56,7 +72,7 @@ export {
|
|
|
56
72
|
import { formatCodexProviderForLog } from "../codex/routing";
|
|
57
73
|
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
58
74
|
import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
|
|
59
|
-
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./gui-static";
|
|
75
|
+
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile, serveSessionBootstrap } from "./gui-static";
|
|
60
76
|
export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
|
|
61
77
|
export { resolveAdapter } from "./adapter-resolve";
|
|
62
78
|
import { formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
|
|
@@ -65,6 +81,7 @@ import {
|
|
|
65
81
|
getActiveTurnCount,
|
|
66
82
|
isDraining,
|
|
67
83
|
registerTurn,
|
|
84
|
+
runListenerShutdown,
|
|
68
85
|
setServerRef,
|
|
69
86
|
trackStreamLifetime,
|
|
70
87
|
tryAdmitTurn,
|
|
@@ -85,7 +102,6 @@ import {
|
|
|
85
102
|
addFinalRequestLog,
|
|
86
103
|
hydrateRequestLogsFromDisk,
|
|
87
104
|
httpStatusForRequestLogTerminal,
|
|
88
|
-
httpStatusForTerminalStatus,
|
|
89
105
|
inspectResponseLogSsePayload,
|
|
90
106
|
nextRequestLogId,
|
|
91
107
|
recordFirstOutput,
|
|
@@ -132,6 +148,8 @@ import {
|
|
|
132
148
|
admissionFields,
|
|
133
149
|
resolveApiAuth,
|
|
134
150
|
resolveResponsesApiAuth,
|
|
151
|
+
requestPolicyView,
|
|
152
|
+
type RequestPolicyView,
|
|
135
153
|
safeConfigDTO,
|
|
136
154
|
setCorsOrigin,
|
|
137
155
|
withCors,
|
|
@@ -153,24 +171,160 @@ import { handleChatCompletions } from "./chat-completions";
|
|
|
153
171
|
import { anthropicErrorResponse } from "../claude/outbound";
|
|
154
172
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
155
173
|
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
174
|
+
import {
|
|
175
|
+
bindNativeMainStartupLifecycle,
|
|
176
|
+
blockNativeMainStartupForUnownedServiceHome,
|
|
177
|
+
releaseNativeMainStartupLifecycle,
|
|
178
|
+
startNativeMainStartupLifecycle,
|
|
179
|
+
type NativeMainStartupGateDeps,
|
|
180
|
+
type NativeMainStartupLifecycle,
|
|
181
|
+
} from "../codex/native-profile-startup";
|
|
156
182
|
import { handleImages } from "./images";
|
|
157
183
|
import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
|
|
158
184
|
import { handleSearch } from "./search";
|
|
159
|
-
import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
|
|
160
|
-
import {
|
|
185
|
+
import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
|
|
186
|
+
import {
|
|
187
|
+
initializeManagementAuthState,
|
|
188
|
+
issueGuiSession,
|
|
189
|
+
managementPrincipal,
|
|
190
|
+
requireManagementAuth,
|
|
191
|
+
type ManagementAuthState,
|
|
192
|
+
} from "./management-auth";
|
|
193
|
+
import {
|
|
194
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
195
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
196
|
+
createLocalAttestationProof,
|
|
197
|
+
createLocalAttestationSecret,
|
|
198
|
+
} from "../lib/local-management-attestation";
|
|
199
|
+
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
200
|
+
import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
|
|
201
|
+
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
161
202
|
|
|
162
|
-
const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
203
|
+
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
163
204
|
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
164
205
|
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
206
|
+
const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
|
|
207
|
+
const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
|
|
165
208
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
export function exceedsLiveSidebandFrameByteLimit(frameBytes: number): boolean {
|
|
210
|
+
return frameBytes > MAX_WS_FRAME_BYTES;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function exceedsLiveSidebandPendingByteLimit(pendingBytes: number, incomingBytes: number): boolean {
|
|
214
|
+
return incomingBytes > LIVE_SIDEBAND_PENDING_BYTES_MAX - pendingBytes;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function webSocketFrameBytes(frame: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): number {
|
|
218
|
+
if (typeof frame === "string") return Buffer.byteLength(frame);
|
|
219
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
|
|
220
|
+
return frame.size;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type LiveSidebandPendingEnqueueResult = "queued" | "too-many-frames" | "too-many-bytes";
|
|
224
|
+
|
|
225
|
+
export function enqueueLiveSidebandPendingFrame(
|
|
226
|
+
data: Pick<WsData, "livePending" | "livePendingBytes">,
|
|
227
|
+
frame: string | Buffer,
|
|
228
|
+
frameBytes = webSocketFrameBytes(frame),
|
|
229
|
+
): LiveSidebandPendingEnqueueResult {
|
|
230
|
+
const pending = data.livePending ?? (data.livePending = []);
|
|
231
|
+
if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) return "too-many-frames";
|
|
232
|
+
const pendingBytes = data.livePendingBytes ?? 0;
|
|
233
|
+
if (exceedsLiveSidebandPendingByteLimit(pendingBytes, frameBytes)) return "too-many-bytes";
|
|
234
|
+
pending.push(frame);
|
|
235
|
+
data.livePendingBytes = pendingBytes + frameBytes;
|
|
236
|
+
return "queued";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type LiveSidebandWebSocketFactory = (
|
|
240
|
+
url: string,
|
|
241
|
+
headers: Record<string, string>,
|
|
242
|
+
) => WebSocket;
|
|
243
|
+
|
|
244
|
+
function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
|
|
245
|
+
ws.data.liveTurnAdmissionLease?.release();
|
|
246
|
+
ws.data.liveTurnAdmissionLease = undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Send one live-sideband frame to the upstream socket.
|
|
251
|
+
*
|
|
252
|
+
* Bun's `WebSocket.send` accepts `string | Blob | BufferSource`, but the DOM-lib
|
|
253
|
+
* `Buffer` can be backed by a `SharedArrayBuffer`, which `BufferSource` rejects.
|
|
254
|
+
* `Uint8Array.from` copies into a fresh `ArrayBuffer`-backed view, so a frame
|
|
255
|
+
* arriving from `node:buffer` still round-trips byte-for-byte.
|
|
256
|
+
*/
|
|
257
|
+
function sendUpstreamFrame(upstream: WebSocket, frame: string | Buffer): void {
|
|
258
|
+
if (typeof frame === "string") {
|
|
259
|
+
upstream.send(frame);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
upstream.send(Uint8Array.from(frame));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
|
|
266
|
+
if (upstream && ws.data.liveUpstream !== upstream) return;
|
|
267
|
+
if (ws.data.liveCloseFallback !== undefined) {
|
|
268
|
+
clearTimeout(ws.data.liveCloseFallback);
|
|
269
|
+
ws.data.liveCloseFallback = undefined;
|
|
171
270
|
}
|
|
172
271
|
ws.data.liveUpstream = undefined;
|
|
173
272
|
ws.data.livePending = undefined;
|
|
273
|
+
ws.data.livePendingBytes = undefined;
|
|
274
|
+
ws.data.cancel = undefined;
|
|
275
|
+
releaseLiveSidebandAdmission(ws);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: WebSocket): void {
|
|
279
|
+
if (ws.data.liveCloseFallback !== undefined) return;
|
|
280
|
+
ws.data.liveCloseFallback = setTimeout(() => {
|
|
281
|
+
ws.data.liveCloseFallback = undefined;
|
|
282
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
283
|
+
if (upstream.readyState === WebSocket.CLOSED) {
|
|
284
|
+
finalizeLiveSideband(ws, upstream);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// A close frame was already sent below. Retry once, but never surrender
|
|
288
|
+
// native-main ownership while the authenticated transport remains live.
|
|
289
|
+
try {
|
|
290
|
+
upstream.close(1000, "upstream close timeout");
|
|
291
|
+
} catch {
|
|
292
|
+
/* upstream is already unusable */
|
|
293
|
+
}
|
|
294
|
+
// Some implementations transition synchronously without delivering the
|
|
295
|
+
// close event. That is still an observed CLOSED transport and is safe to
|
|
296
|
+
// finalize. CONNECTING/CLOSING peers keep the lease so profile switching
|
|
297
|
+
// fails at its own bounded drain deadline instead of racing live traffic.
|
|
298
|
+
// The earlier CLOSED check narrowed `readyState` to 0|1|2 in the type
|
|
299
|
+
// system, but the socket can still transition to CLOSED (3) before this
|
|
300
|
+
// fallback fires; the cast keeps the runtime-identical check.
|
|
301
|
+
if ((upstream.readyState as number) === 3) finalizeLiveSideband(ws, upstream);
|
|
302
|
+
}, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
|
|
306
|
+
if (ws.data.liveClosing) return;
|
|
307
|
+
ws.data.liveClosing = true;
|
|
308
|
+
ws.data.livePending = undefined;
|
|
309
|
+
ws.data.livePendingBytes = undefined;
|
|
310
|
+
ws.data.cancel = undefined;
|
|
311
|
+
const upstream = ws.data.liveUpstream;
|
|
312
|
+
// Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
|
|
313
|
+
// constant CLOSED is 3; the numeric literal is the runtime-identical check.
|
|
314
|
+
if (!upstream || upstream.readyState === 3) {
|
|
315
|
+
finalizeLiveSideband(ws, upstream);
|
|
316
|
+
} else {
|
|
317
|
+
// The sideband holds a native-main admission lease. Do not release it just
|
|
318
|
+
// because the downstream left: its authenticated upstream remains live
|
|
319
|
+
// until the close event arrives or the transport is observed CLOSED. The
|
|
320
|
+
// bounded fallback only retries close; it does not release ownership.
|
|
321
|
+
armLiveSidebandCloseFallback(ws, upstream);
|
|
322
|
+
try {
|
|
323
|
+
upstream.close(code, reason);
|
|
324
|
+
} catch {
|
|
325
|
+
/* the fallback retries close without releasing ownership */
|
|
326
|
+
}
|
|
327
|
+
}
|
|
174
328
|
try {
|
|
175
329
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
176
330
|
ws.close(code, reason);
|
|
@@ -180,7 +334,12 @@ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""
|
|
|
180
334
|
}
|
|
181
335
|
}
|
|
182
336
|
|
|
183
|
-
function attachLiveSidebandUpstream(
|
|
337
|
+
function attachLiveSidebandUpstream(
|
|
338
|
+
ws: ServerWebSocket<WsData>,
|
|
339
|
+
createWebSocket: LiveSidebandWebSocketFactory = (url, headers) => (
|
|
340
|
+
new WebSocket(url, { headers } as unknown as string[])
|
|
341
|
+
),
|
|
342
|
+
): void {
|
|
184
343
|
const url = ws.data.liveUpstreamUrl;
|
|
185
344
|
if (!url) {
|
|
186
345
|
closeLiveSideband(ws, 1011, "missing upstream");
|
|
@@ -189,27 +348,24 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
189
348
|
let upstream: WebSocket;
|
|
190
349
|
try {
|
|
191
350
|
// Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
|
|
192
|
-
upstream =
|
|
351
|
+
upstream = createWebSocket(url, ws.data.liveUpstreamHeaders ?? {});
|
|
193
352
|
} catch {
|
|
194
353
|
closeLiveSideband(ws, 1011, "upstream connect failed");
|
|
195
354
|
return;
|
|
196
355
|
}
|
|
197
356
|
ws.data.liveUpstream = upstream;
|
|
198
|
-
ws.data.
|
|
199
|
-
|
|
200
|
-
upstream.close(1000, "client closed");
|
|
201
|
-
} catch {
|
|
202
|
-
/* ignore */
|
|
203
|
-
}
|
|
204
|
-
};
|
|
357
|
+
ws.data.liveClosing = false;
|
|
358
|
+
ws.data.cancel = () => closeLiveSideband(ws, 1000, "client closed");
|
|
205
359
|
|
|
206
360
|
upstream.addEventListener("open", () => {
|
|
361
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
207
362
|
ws.data.liveOpened = true;
|
|
208
363
|
const pending = ws.data.livePending ?? [];
|
|
209
364
|
ws.data.livePending = undefined;
|
|
365
|
+
ws.data.livePendingBytes = undefined;
|
|
210
366
|
for (const frame of pending) {
|
|
211
367
|
try {
|
|
212
|
-
upstream
|
|
368
|
+
sendUpstreamFrame(upstream, frame);
|
|
213
369
|
} catch {
|
|
214
370
|
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
215
371
|
return;
|
|
@@ -217,7 +373,12 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
217
373
|
}
|
|
218
374
|
});
|
|
219
375
|
upstream.addEventListener("message", (event) => {
|
|
376
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
220
377
|
try {
|
|
378
|
+
if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(event.data))) {
|
|
379
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
221
382
|
logLiveSidebandFrame("u2c", event.data);
|
|
222
383
|
if (typeof event.data === "string") ws.send(event.data);
|
|
223
384
|
else if (event.data instanceof ArrayBuffer) ws.send(event.data);
|
|
@@ -229,14 +390,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
229
390
|
}
|
|
230
391
|
});
|
|
231
392
|
upstream.addEventListener("close", (event) => {
|
|
393
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
394
|
+
ws.data.liveClosing = true;
|
|
395
|
+
finalizeLiveSideband(ws, upstream);
|
|
232
396
|
try {
|
|
233
397
|
ws.close(event.code || 1000, event.reason || "");
|
|
234
398
|
} catch {
|
|
235
399
|
/* ignore */
|
|
236
400
|
}
|
|
237
|
-
ws.data.liveUpstream = undefined;
|
|
238
401
|
});
|
|
239
402
|
upstream.addEventListener("error", () => {
|
|
403
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
240
404
|
closeLiveSideband(ws, 1011, "upstream error");
|
|
241
405
|
});
|
|
242
406
|
}
|
|
@@ -250,18 +414,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
250
414
|
// if (isEventStream && upstreamResponse.body) {
|
|
251
415
|
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
252
416
|
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
253
|
-
// #314 gated shape: win32
|
|
254
|
-
//
|
|
255
|
-
//
|
|
417
|
+
// #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
|
|
418
|
+
// upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
|
|
419
|
+
// requires explicit config-eager opt-in (`auto` always stays tee on darwin).
|
|
256
420
|
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
421
|
+
// Codex upstream WS runtime gating and the forced bounded single-reader branch
|
|
422
|
+
// are owned by responses/ws-upstream.ts and responses/core.ts respectively.
|
|
257
423
|
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
258
424
|
// new Response(eagerBody,
|
|
259
425
|
// Default shape (tee + background inspection):
|
|
260
426
|
// upstreamResponse.body.tee()
|
|
261
427
|
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
262
|
-
// process.platform === "win32"
|
|
263
|
-
// && !needsClientRewrite
|
|
264
|
-
// ? nativeBody
|
|
265
428
|
// relaySseWithFailedTail(repairedBody, upstream)
|
|
266
429
|
// new Response(clientBody
|
|
267
430
|
// markNativePassthroughSseResponse
|
|
@@ -271,12 +434,75 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
271
434
|
// trackSseForRequestLog(
|
|
272
435
|
// export function relaySseWithHeartbeat
|
|
273
436
|
|
|
274
|
-
export
|
|
275
|
-
|
|
437
|
+
export interface StartServerDeps {
|
|
438
|
+
/** Test-only seam; production always initializes its own management credential state. */
|
|
439
|
+
managementAuthState?: ManagementAuthState;
|
|
440
|
+
/** Test-only route dependencies, forwarded only after management admission succeeds. */
|
|
441
|
+
managementApi?: ManagementApiDeps;
|
|
442
|
+
/** Test-only native-main recovery dependencies; production constructs the normal manager. */
|
|
443
|
+
nativeMainStartup?: NativeMainStartupGateDeps;
|
|
444
|
+
/** Test-only ownership evidence; production inspects the installed service state. */
|
|
445
|
+
inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
|
|
446
|
+
/** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
|
|
447
|
+
liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
|
|
448
|
+
/** Test-only seam; production derives a fresh local-attestation secret per process. */
|
|
449
|
+
localAttestationSecret?: string;
|
|
450
|
+
/** Optional readiness gate; a fresh pending gate is created when omitted. */
|
|
451
|
+
readinessGate?: ReadinessGate;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function inspectStartupOwnership(deps: StartServerDeps): OwnershipInspection {
|
|
455
|
+
try {
|
|
456
|
+
return (deps.inspectNativeCodexOwnership ?? inspectNativeCodexOwnership)();
|
|
457
|
+
} catch {
|
|
458
|
+
return {
|
|
459
|
+
ownership: "unknown",
|
|
460
|
+
reason: "service-home ownership inspection failed",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/*
|
|
466
|
+
* #1046. `startServer` rewrites the Codex models cache during boot, and an
|
|
467
|
+
* app-server that started earlier keeps its own in-memory model list. The stale
|
|
468
|
+
* warning is not emitted here: `handleStart` runs a catalog sync moments later,
|
|
469
|
+
* so warning now would read an mtime that write is about to move, and both sites
|
|
470
|
+
* calling the helper independently would warn twice. This records the fact; the
|
|
471
|
+
* CLI start path owns the single decision.
|
|
472
|
+
*
|
|
473
|
+
* A caller that starts a server without `handleStart` (tests, embedded use)
|
|
474
|
+
* deliberately gets no warning — lifecycle diagnostics belong to whoever owns
|
|
475
|
+
* the lifecycle.
|
|
476
|
+
*/
|
|
477
|
+
let startupCacheInvalidationWrote = false;
|
|
478
|
+
|
|
479
|
+
/** #1046: did this process's startup cache invalidation actually write? */
|
|
480
|
+
export function consumeStartupCacheInvalidationWrite(): boolean {
|
|
481
|
+
const wrote = startupCacheInvalidationWrote;
|
|
482
|
+
startupCacheInvalidationWrote = false;
|
|
483
|
+
return wrote;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function warnAgentTaskRecoveryStartup(config: {
|
|
487
|
+
agentTaskRecovery?: { enabled?: boolean };
|
|
488
|
+
}): void {
|
|
489
|
+
if (config.agentTaskRecovery?.enabled !== true) return;
|
|
490
|
+
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
|
|
491
|
+
console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
|
|
492
|
+
console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
496
|
+
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
497
|
+
const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
|
|
498
|
+
warnAgentTaskRecoveryStartup(config);
|
|
276
499
|
setLiveStateStoreConfig(config);
|
|
277
500
|
applyProxyEnv(config);
|
|
278
501
|
assertServerAuthConfig(config);
|
|
279
|
-
const managementAuth = initializeManagementAuthState(config);
|
|
502
|
+
const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
|
|
503
|
+
let userCostOverlayReconciler: { stop(): void } | null = null;
|
|
504
|
+
// Arm synchronously before listen. A pending journal therefore makes __main__ unusable
|
|
505
|
+
// before any request can resolve its physical credential, while health/management/Pool stay live.
|
|
280
506
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
281
507
|
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
282
508
|
reconcileOAuthProviders(config);
|
|
@@ -310,7 +536,28 @@ export function startServer(port?: number) {
|
|
|
310
536
|
if (migrated) saveConfig(config);
|
|
311
537
|
}
|
|
312
538
|
}
|
|
313
|
-
|
|
539
|
+
// Resolve unattended service-home authority before any Codex lock, cache, owner,
|
|
540
|
+
// journal, or credential path. Both positive foreign evidence and an unprovable
|
|
541
|
+
// ownership state are non-authority.
|
|
542
|
+
startupCacheInvalidationWrote = false;
|
|
543
|
+
const startupCacheOwnership = inspectStartupOwnership(deps);
|
|
544
|
+
// Startup cache invalidation is best-effort and must never block the server from
|
|
545
|
+
// serving. It now takes K so it cannot race a convergence commit, but both the
|
|
546
|
+
// home resolution and the acquisition can fail on a machine with no Codex home —
|
|
547
|
+
// `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
|
|
548
|
+
// otherwise turn "no Codex installed" into "proxy will not start".
|
|
549
|
+
if (startupCacheOwnership.ownership === "owned") {
|
|
550
|
+
try {
|
|
551
|
+
const startupCodexHome = getCodexHome();
|
|
552
|
+
// #1046: record whether this actually rewrote the cache. `handleStart` ORs this
|
|
553
|
+
// with the later startup sync and warns ONCE about stale app-servers; warning
|
|
554
|
+
// here instead would read a catalog mtime the sync is about to move.
|
|
555
|
+
const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
|
|
556
|
+
invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
|
|
557
|
+
// A refused permit is not a write; only a completed run that returned true is.
|
|
558
|
+
startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
|
|
559
|
+
} catch { /* no readable Codex home: nothing to invalidate */ }
|
|
560
|
+
}
|
|
314
561
|
// Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
|
|
315
562
|
// the server can serve a request, and AFTER the startup migrations above — those run
|
|
316
563
|
// against a config nobody else holds and are the documented exception to the save
|
|
@@ -321,15 +568,12 @@ export function startServer(port?: number) {
|
|
|
321
568
|
// usage.jsonl already persists every request; rehydrate the in-memory Logs ring so
|
|
322
569
|
// /api/logs (and the GUI) survive `ocx stop` / `ocx start` process restarts.
|
|
323
570
|
hydrateRequestLogsFromDisk();
|
|
324
|
-
// #314: warn-only RSS observability (unref'd, idempotent — safe under repeated
|
|
325
|
-
// startServer(0) in tests). Snapshot surfaces via GET /api/system/memory.
|
|
326
|
-
startMemoryWatchdog();
|
|
327
571
|
registerDefaultAppOwnedMemoryStores();
|
|
328
572
|
registerDefaultAppOwnedObservedBuffers();
|
|
329
573
|
registerAppOwnedMemorySweepFallback();
|
|
330
574
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
331
575
|
enforceAppOwnedMemoryBudget();
|
|
332
|
-
|
|
576
|
+
registerCodexCooldownRecoveryProbeWorker(config);
|
|
333
577
|
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
334
578
|
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
335
579
|
// Heavy work runs in a Worker via the single-flight job controller.
|
|
@@ -337,9 +581,6 @@ export function startServer(port?: number) {
|
|
|
337
581
|
const applyPolicy = (policy: StorageCleanupPolicy) => {
|
|
338
582
|
config.storageCleanupPolicy = policy;
|
|
339
583
|
};
|
|
340
|
-
setStorageCleanupPolicyLiveSink(applyPolicy);
|
|
341
|
-
setStorageCleanupPolicyJobLiveApply(applyPolicy);
|
|
342
|
-
startStorageCleanupScheduler();
|
|
343
584
|
|
|
344
585
|
const listenPort = port ?? config.port ?? 10100;
|
|
345
586
|
setCorsOrigin(listenPort);
|
|
@@ -351,30 +592,89 @@ export function startServer(port?: number) {
|
|
|
351
592
|
const configuredHost = config.hostname?.trim();
|
|
352
593
|
const bindHost = !configuredHost || /^localhost$/i.test(configuredHost) ? "127.0.0.1" : configuredHost;
|
|
353
594
|
|
|
595
|
+
// Unauthenticated loopback listener (#1102). Off unless explicitly enabled.
|
|
596
|
+
const loopbackListener = config.unauthenticatedLoopbackListener;
|
|
597
|
+
const loopbackListenerPort = loopbackListener?.enabled ? loopbackListener.port : null;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Which listener a request arrived on, expressed as the only thing that differs: the bind
|
|
601
|
+
* address the auth and CORS decisions should see.
|
|
602
|
+
*
|
|
603
|
+
* The public listener passes the shared config through untouched, so its behaviour is
|
|
604
|
+
* byte-identical to before. The loopback listener substitutes 127.0.0.1, which is what makes
|
|
605
|
+
* `isApiAuthRequired` return false for it — the same code path a plain loopback bind has
|
|
606
|
+
* always taken, including the Host-header check inside `isAllowedRequestOrigin`.
|
|
607
|
+
*
|
|
608
|
+
* Built per request rather than once per listener so a management-API config change is
|
|
609
|
+
* picked up immediately instead of being frozen at listen time.
|
|
610
|
+
*/
|
|
611
|
+
const publicPolicy = (): RequestPolicyView => config;
|
|
612
|
+
const loopbackPolicy = (): RequestPolicyView => requestPolicyView(config, "127.0.0.1");
|
|
613
|
+
void publicPolicy;
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Routes the unauthenticated loopback listener will serve. Everything else 404s.
|
|
617
|
+
*
|
|
618
|
+
* This is an allowlist rather than a filter applied to the public handler, because a filter
|
|
619
|
+
* inverts the failure mode: a route added later would be reachable here by default. The four
|
|
620
|
+
* entries are exactly what a directly-spawned `codex app-server` needs.
|
|
621
|
+
*
|
|
622
|
+
* `GET /v1/models` is on the list for a reason that is easy to miss. When catalog
|
|
623
|
+
* materialization fails or finds no source, `syncCodex` warns and injects with
|
|
624
|
+
* `catalogPath: null`; Codex then builds an ONLINE model manager and `model/list` refreshes
|
|
625
|
+
* through `GET {base_url}/models`. Returning 404 there would leave the picker on its bundled
|
|
626
|
+
* fallback — fixing the direct-spawn host while breaking its model list.
|
|
627
|
+
*/
|
|
628
|
+
function loopbackRouteAllowed(url: URL, req: Request): boolean {
|
|
629
|
+
const path = url.pathname;
|
|
630
|
+
if (path === "/v1/responses") {
|
|
631
|
+
return req.method === "POST" || req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
632
|
+
}
|
|
633
|
+
if (path === "/v1/responses/compact") return req.method === "POST";
|
|
634
|
+
if (path === "/v1/models") return req.method === "GET";
|
|
635
|
+
// Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
|
|
636
|
+
// transport) — a directly-spawned `codex app-server` needs these for desktop
|
|
637
|
+
// voice the same way it needs /v1/responses. WebSocket upgrades only; plain
|
|
638
|
+
// HTTP on these paths stays rejected.
|
|
639
|
+
if (path === "/v1/realtime" || path === "/v1/live") {
|
|
640
|
+
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
|
|
354
645
|
// Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
|
|
355
646
|
// the server_is_overloaded code so clients can back off, but always return a JSON envelope.
|
|
356
|
-
|
|
647
|
+
// These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
|
|
648
|
+
// explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
|
|
649
|
+
// headers to a 503 on the loopback listener — no model runs and no credential is spent, but
|
|
650
|
+
// it is the one error path that would answer a rebinding origin with its own origin echoed
|
|
651
|
+
// back.
|
|
652
|
+
function drainingResponse(req: Request, policy: RequestPolicyView): Response {
|
|
357
653
|
const response = formatErrorResponse(503, "server_error", "Service shutting down");
|
|
358
654
|
const headers = new Headers(response.headers);
|
|
359
|
-
for (const [name, value] of Object.entries(corsHeaders(req,
|
|
655
|
+
for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
|
|
360
656
|
headers.set(name, value);
|
|
361
657
|
}
|
|
362
658
|
headers.set("Retry-After", "5");
|
|
363
659
|
return new Response(response.body, { status: 503, headers });
|
|
364
660
|
}
|
|
365
661
|
|
|
366
|
-
function serverBusyResponse(req: Request, resource: string): Response {
|
|
662
|
+
function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
|
|
367
663
|
return withCors(new Response(JSON.stringify({
|
|
368
664
|
error: { type: "server_error", code: "server_busy", message: `${resource} capacity reached` },
|
|
369
665
|
}), {
|
|
370
666
|
status: 503,
|
|
371
667
|
headers: { "Content-Type": "application/json", "Retry-After": "1" },
|
|
372
|
-
}), req,
|
|
668
|
+
}), req, policy);
|
|
373
669
|
}
|
|
374
670
|
|
|
375
|
-
async function runAdmittedHttpTurn(
|
|
671
|
+
async function runAdmittedHttpTurn(
|
|
672
|
+
req: Request,
|
|
673
|
+
policy: RequestPolicyView,
|
|
674
|
+
work: (lease: ActiveTurnLease) => Promise<Response>,
|
|
675
|
+
): Promise<Response> {
|
|
376
676
|
const lease = tryAdmitTurn();
|
|
377
|
-
if (!lease) return serverBusyResponse(req, "active turns");
|
|
677
|
+
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
378
678
|
let response: Response;
|
|
379
679
|
try {
|
|
380
680
|
response = await work(lease);
|
|
@@ -388,25 +688,102 @@ export function startServer(port?: number) {
|
|
|
388
688
|
return response;
|
|
389
689
|
}
|
|
390
690
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
691
|
+
// Readiness gate: one PRIVATE controller per startServer invocation, captured
|
|
692
|
+
// by this listener's closure. Starting/failing a second server in the same
|
|
693
|
+
// process can never reset or mutate this gate. handleStart creates the gate,
|
|
694
|
+
// passes it in, and transitions it after the post-startup sync settles. When
|
|
695
|
+
// no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
|
|
696
|
+
const readinessGate = deps.readinessGate ?? createReadinessGate();
|
|
697
|
+
// Actual bound port, filled in after Bun.serve binds so /readyz reports the
|
|
698
|
+
// real ephemeral port for startServer(0). /healthz keeps its existing port
|
|
699
|
+
// field (the requested listenPort) byte-for-byte.
|
|
700
|
+
let boundPort: number | null = null;
|
|
701
|
+
|
|
702
|
+
// Native-main startup ownership creates several SQLite coordination files in
|
|
703
|
+
// CODEX_HOME. When the user has disabled the Codex integration, starting the
|
|
704
|
+
// proxy must not manufacture those Codex artifacts merely to serve other
|
|
705
|
+
// clients; no Codex request can use this lifecycle in that state.
|
|
706
|
+
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
707
|
+
// between the two sites must not widen the service-install race.
|
|
708
|
+
const nativeOwnership = inspectStartupOwnership(deps);
|
|
709
|
+
const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
|
|
710
|
+
? nativeOwnership.ownership === "owned"
|
|
711
|
+
? startNativeMainStartupLifecycle(deps.nativeMainStartup)
|
|
712
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
713
|
+
nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
|
|
714
|
+
// #2108: an `unknown` verdict means the probe could not answer, not that this host
|
|
715
|
+
// is unownable. Hand the fence a way to re-ask so a host that becomes answerable
|
|
716
|
+
// after boot reopens on its own instead of needing `ocx restart`. A `foreign`
|
|
717
|
+
// verdict ignores this by design — that one is a fact, not a question.
|
|
718
|
+
{ reprobe: () => inspectStartupOwnership(deps).ownership },
|
|
719
|
+
)
|
|
720
|
+
: {
|
|
721
|
+
homeId: null,
|
|
722
|
+
settled: Promise.resolve({ status: "ready", homeId: null }),
|
|
723
|
+
release: async () => {},
|
|
724
|
+
};
|
|
725
|
+
let server: Server<WsData>;
|
|
726
|
+
let loopbackServer: Server<WsData> | null = null;
|
|
727
|
+
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
728
|
+
try {
|
|
729
|
+
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
|
|
730
|
+
// External `ocx config set` / direct config.json edits run in other
|
|
731
|
+
// processes; poll the file so Logs/Usage display prices follow them live.
|
|
732
|
+
// Started inside the guarded startup transaction so the catch below can
|
|
733
|
+
// release the owner-scoped lease on any listener failure.
|
|
734
|
+
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
735
|
+
const serveOptions = {
|
|
736
|
+
idleTimeout: 255,
|
|
737
|
+
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
|
|
738
|
+
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
739
|
+
// The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
|
|
740
|
+
// else. Rejecting here, before any handler runs, is what keeps the surface from growing
|
|
741
|
+
// silently when a route is added below.
|
|
742
|
+
if (requestServer === loopbackServer && !loopbackRouteAllowed(new URL(req.url), req)) {
|
|
743
|
+
return withCors(
|
|
744
|
+
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
745
|
+
req,
|
|
746
|
+
loopbackPolicy(),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
// Auth and CORS decisions below read `policy`, not `config`. For the public listener the
|
|
750
|
+
// two are the same object, so its behaviour is unchanged; for the loopback listener the
|
|
751
|
+
// view substitutes 127.0.0.1 as the bind address, which is what routes it through the
|
|
752
|
+
// same code path a plain loopback bind has always taken — Host-header check included.
|
|
753
|
+
// Routing, provider selection and response bodies keep using `config`.
|
|
754
|
+
const policy: RequestPolicyView = requestServer === loopbackServer ? loopbackPolicy() : config;
|
|
396
755
|
const url = new URL(req.url);
|
|
397
756
|
markActivity(`${req.method} ${url.pathname}`);
|
|
398
757
|
|
|
758
|
+
// Readiness is exact-GET on the literal /readyz path. Compare the DECODED
|
|
759
|
+
// pathname so an encoded variant like /readyz%2F (which decodes to
|
|
760
|
+
// /readyz/) cannot bypass the exact-path rejection and reach the GUI
|
|
761
|
+
// fallback (serveGuiFile decodes the pathname and would serve index.html
|
|
762
|
+
// with 200). Malformed percent-sequences fall back to the raw pathname,
|
|
763
|
+
// which still cannot match the exact literal below.
|
|
764
|
+
let readyzPath: string | undefined;
|
|
765
|
+
try {
|
|
766
|
+
const decoded = decodeURIComponent(url.pathname);
|
|
767
|
+
if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
|
|
768
|
+
} catch { /* malformed encoding — not a readiness path */ }
|
|
769
|
+
|
|
399
770
|
if (req.method === "OPTIONS") {
|
|
771
|
+
// /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
|
|
772
|
+
// path) must answer the deterministic JSON 404, never the generic 204
|
|
773
|
+
// preflight response that the SPA fallback would otherwise allow.
|
|
774
|
+
if (readyzPath !== undefined) {
|
|
775
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
776
|
+
}
|
|
400
777
|
const managementPreflight = url.pathname.startsWith("/api/");
|
|
401
778
|
const allowed = managementPreflight
|
|
402
779
|
? isAllowedManagementOrigin(req, config)
|
|
403
|
-
: isAllowedRequestOrigin(req,
|
|
780
|
+
: isAllowedRequestOrigin(req, policy);
|
|
404
781
|
if (!allowed) {
|
|
405
782
|
return new Response(null, { status: 403, headers: corsHeaders() });
|
|
406
783
|
}
|
|
407
784
|
return new Response(null, {
|
|
408
785
|
status: 204,
|
|
409
|
-
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req,
|
|
786
|
+
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, policy),
|
|
410
787
|
});
|
|
411
788
|
}
|
|
412
789
|
|
|
@@ -414,14 +791,14 @@ export function startServer(port?: number) {
|
|
|
414
791
|
// handshake-time only, so capture inbound headers and thread them into the pipeline.
|
|
415
792
|
if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
416
793
|
if (isDraining()) {
|
|
417
|
-
return drainingResponse(req);
|
|
794
|
+
return drainingResponse(req, policy);
|
|
418
795
|
}
|
|
419
|
-
const admission = resolveResponsesApiAuth(req,
|
|
796
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
420
797
|
if (!admission) {
|
|
421
|
-
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
798
|
+
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
422
799
|
}
|
|
423
|
-
if (!isAllowedRequestOrigin(req,
|
|
424
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req,
|
|
800
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
801
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
|
|
425
802
|
}
|
|
426
803
|
// WS transport gate: Codex's built-in `openai` provider hardcodes supports_websockets=true,
|
|
427
804
|
// so under Design B it always tries the WS transport first. When the feature is off, reject
|
|
@@ -429,26 +806,91 @@ export function startServer(port?: number) {
|
|
|
429
806
|
// session-scoped HTTP fallback (client.rs WebsocketStreamOutcome::FallbackToHttp) instead of
|
|
430
807
|
// surfacing broken-pipe errors from sockets a "disabled" feature would otherwise accept.
|
|
431
808
|
if (!websocketsEnabled(config)) {
|
|
432
|
-
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req,
|
|
809
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, policy);
|
|
433
810
|
}
|
|
434
811
|
const websocketLease = tryReserveCodexWebSocket();
|
|
435
|
-
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets");
|
|
436
|
-
|
|
812
|
+
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets", policy);
|
|
813
|
+
// Upgrade on the server that RECEIVED this request, not the captured `server`
|
|
814
|
+
// binding. They are the same object for the public listener, but the
|
|
815
|
+
// unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
|
|
816
|
+
// request to the public server's upgrade would fail or cross sockets.
|
|
817
|
+
if (requestServer.upgrade(req, {
|
|
437
818
|
data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
|
|
438
819
|
})) return undefined as unknown as Response;
|
|
439
820
|
websocketLease.release();
|
|
440
|
-
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req,
|
|
821
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
441
822
|
}
|
|
442
823
|
|
|
443
824
|
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
444
825
|
// service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
|
|
445
|
-
|
|
826
|
+
const healthPort = server.port ?? listenPort;
|
|
827
|
+
const response = jsonResponse({
|
|
828
|
+
status: "ok",
|
|
829
|
+
service: "opencodex",
|
|
830
|
+
version: VERSION,
|
|
831
|
+
uptime: process.uptime(),
|
|
832
|
+
pid: process.pid,
|
|
833
|
+
port: healthPort,
|
|
834
|
+
restartCapability: SYSTEM_RESTART_CAPABILITY_VERSION,
|
|
835
|
+
providerReloadCapability: LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
|
|
836
|
+
}, 200, req, policy);
|
|
837
|
+
const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
|
|
838
|
+
if (challenge) {
|
|
839
|
+
const proof = createLocalAttestationProof(localAttestationSecret, challenge, process.pid, healthPort);
|
|
840
|
+
if (proof) response.headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
|
|
841
|
+
}
|
|
842
|
+
return response;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Readiness: like /healthz this is exact GET and unauthenticated (so a client can
|
|
846
|
+
// back off BEFORE knowing the admission token), but stricter than liveness. The
|
|
847
|
+
// body carries only sanitized identity + the fixed status enum; the sync message,
|
|
848
|
+
// warning text, catalog path, provider output, and account data are never exposed.
|
|
849
|
+
// POST or "/readyz/" must NOT match (exact pathname + GET method): answer them
|
|
850
|
+
// with a JSON 404 here so they can never be silently accepted by the GUI SPA
|
|
851
|
+
// fallback (which would serve index.html with HTTP 200 once gui/dist exists).
|
|
852
|
+
if (readyzPath !== undefined) {
|
|
853
|
+
if (readyzPath !== "/readyz" || req.method !== "GET") {
|
|
854
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
855
|
+
}
|
|
856
|
+
// A draining proxy must never advertise ready: every data-plane branch
|
|
857
|
+
// answers drainingResponse while isDraining() is set, but the one-shot
|
|
858
|
+
// readiness gate is not mutated on shutdown (it is owned by the startup
|
|
859
|
+
// sync). Report pending so `ocx ready --wait` and external supervisors
|
|
860
|
+
// keep polling instead of promoting a proxy that is draining.
|
|
861
|
+
const status = isDraining() ? "pending" : readinessGate.getStatus();
|
|
862
|
+
const body = {
|
|
863
|
+
service: "opencodex",
|
|
864
|
+
version: VERSION,
|
|
865
|
+
uptime: process.uptime(),
|
|
866
|
+
pid: process.pid,
|
|
867
|
+
port: boundPort ?? listenPort,
|
|
868
|
+
status,
|
|
869
|
+
};
|
|
870
|
+
if (status === "ready") {
|
|
871
|
+
return jsonResponse(body, 200, req, policy);
|
|
872
|
+
}
|
|
873
|
+
// Pending/failed: 503 with a conservative Retry-After so well-behaved clients
|
|
874
|
+
// (and `ocx ready --wait`) back off instead of hot-looping.
|
|
875
|
+
const resp = jsonResponse(body, 503, req, policy);
|
|
876
|
+
const headers = new Headers(resp.headers);
|
|
877
|
+
headers.set("Retry-After", "1");
|
|
878
|
+
return new Response(resp.body, { status: 503, headers });
|
|
446
879
|
}
|
|
447
880
|
|
|
448
881
|
if (url.pathname.startsWith("/api/")) {
|
|
449
|
-
const
|
|
882
|
+
const localManagementAuth = {
|
|
883
|
+
attestationSecret: localAttestationSecret,
|
|
884
|
+
pid: process.pid,
|
|
885
|
+
port: boundPort ?? requestServer.port ?? listenPort,
|
|
886
|
+
};
|
|
887
|
+
const apiAuthError = requireManagementAuth(req, managementAuth, config, localManagementAuth);
|
|
450
888
|
if (apiAuthError) return withManagementCors(apiAuthError, req, config);
|
|
451
|
-
|
|
889
|
+
// Which credential passed the gate, resolved from the same session table the
|
|
890
|
+
// gate used. Consent-bearing routes need this: request headers are forgeable
|
|
891
|
+
// by anything holding the admin token, the credential is not.
|
|
892
|
+
const principal = managementPrincipal(req, managementAuth, config, localManagementAuth) ?? undefined;
|
|
893
|
+
const mgmtResponse = await handleManagementAPI(req, url, config, deps.managementApi, principal);
|
|
452
894
|
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
453
895
|
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
454
896
|
}
|
|
@@ -457,25 +899,79 @@ export function startServer(port?: number) {
|
|
|
457
899
|
// Model discovery never forwards Authorization upstream, so the broader admission
|
|
458
900
|
// set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
|
|
459
901
|
// remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
|
|
460
|
-
const admission = resolveApiAuth(req,
|
|
461
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
462
|
-
if (!isAllowedRequestOrigin(req,
|
|
463
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
902
|
+
const admission = resolveApiAuth(req, policy);
|
|
903
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
904
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
905
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
464
906
|
}
|
|
465
907
|
let goModels;
|
|
908
|
+
let modelEntitlements;
|
|
466
909
|
try {
|
|
467
|
-
goModels = await
|
|
910
|
+
[goModels, modelEntitlements] = await Promise.all([
|
|
911
|
+
fetchAllModels(config),
|
|
912
|
+
resolveCodexModelEntitlements(config),
|
|
913
|
+
]);
|
|
468
914
|
} catch (error) {
|
|
469
915
|
if (error instanceof CatalogGatherBusyError) {
|
|
470
916
|
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
471
917
|
status: 503,
|
|
472
918
|
headers: { "content-type": "application/json", "Retry-After": "1" },
|
|
473
|
-
}), req,
|
|
919
|
+
}), req, policy);
|
|
474
920
|
}
|
|
475
921
|
throw error;
|
|
476
922
|
}
|
|
477
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
478
|
-
const
|
|
923
|
+
const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
924
|
+
const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
|
|
925
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
926
|
+
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
927
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
928
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
929
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
930
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
931
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
932
|
+
modelEntitlements,
|
|
933
|
+
bareEligibleAccountIds,
|
|
934
|
+
);
|
|
935
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
936
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
937
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
938
|
+
));
|
|
939
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
940
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
941
|
+
));
|
|
942
|
+
const nativeSlugs = includeNativeOpenAi
|
|
943
|
+
? nativeOpenAiSlugs().filter(slug => (
|
|
944
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
945
|
+
))
|
|
946
|
+
: [];
|
|
947
|
+
const disabledNatives = disabledNativeSlugs(config);
|
|
948
|
+
const disabledModels = new Set(config.disabledModels ?? []);
|
|
949
|
+
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
950
|
+
const suppressedBareNativeSlugs = new Set([
|
|
951
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
952
|
+
...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
|
|
953
|
+
]);
|
|
954
|
+
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
955
|
+
? visibleCodexAccountSelectors(config)
|
|
956
|
+
: [];
|
|
957
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
958
|
+
const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
|
|
959
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
|
|
960
|
+
const target = accountTargets.get(selector);
|
|
961
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
962
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
963
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
964
|
+
return [selector, slugs.filter(slug => (
|
|
965
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
966
|
+
))] as const;
|
|
967
|
+
}))
|
|
968
|
+
: new Map<string, readonly string[]>();
|
|
969
|
+
const accountNativeSlugs = [...new Set(
|
|
970
|
+
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
971
|
+
)];
|
|
972
|
+
const desktopNativeSlugs = desktopVisibleNativeSlugs(config).filter(slug => (
|
|
973
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
974
|
+
));
|
|
479
975
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
480
976
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
481
977
|
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
@@ -489,10 +985,10 @@ export function startServer(port?: number) {
|
|
|
489
985
|
const wantsAnthropicList = req.headers.get("anthropic-version") !== null
|
|
490
986
|
|| url.searchParams.get("flavor") === "anthropic";
|
|
491
987
|
if (wantsAnthropicList && !url.searchParams.has("client_version")) {
|
|
492
|
-
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req,
|
|
988
|
+
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
493
989
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
494
990
|
buildDesktop3pRegistry(
|
|
495
|
-
|
|
991
|
+
desktopNativeSlugs,
|
|
496
992
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
497
993
|
config.claudeCode?.desktopProfile,
|
|
498
994
|
);
|
|
@@ -509,8 +1005,8 @@ export function startServer(port?: number) {
|
|
|
509
1005
|
: idsParam === "desktop"
|
|
510
1006
|
? "desktop3p" as const
|
|
511
1007
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
512
|
-
const data = buildAnthropicModelInfos(
|
|
513
|
-
return jsonResponse({ data }, 200, req,
|
|
1008
|
+
const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config));
|
|
1009
|
+
return jsonResponse({ data }, 200, req, policy);
|
|
514
1010
|
}
|
|
515
1011
|
if (url.searchParams.has("client_version")) {
|
|
516
1012
|
// Codex client → Codex catalog shape: native gpt + namespaced routed models,
|
|
@@ -519,8 +1015,39 @@ export function startServer(port?: number) {
|
|
|
519
1015
|
// Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
|
|
520
1016
|
// on-disk sync; codex-rs keeps them out of the picker itself).
|
|
521
1017
|
const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
522
|
-
|
|
523
|
-
|
|
1018
|
+
// Account rows use the same hidden-inclusive supported set as on-disk sync. This lets a
|
|
1019
|
+
// newly re-enabled native reappear under each selector before the next sync, while the
|
|
1020
|
+
// no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
|
|
1021
|
+
const catalogNativeSlugs = accountSelectors.length > 0
|
|
1022
|
+
? [...new Set([
|
|
1023
|
+
...availableAccountNativeSlugs,
|
|
1024
|
+
...accountNativeSlugs,
|
|
1025
|
+
])]
|
|
1026
|
+
: nativeSlugs;
|
|
1027
|
+
const entries = buildCatalogEntries(
|
|
1028
|
+
loadCatalogTemplate(),
|
|
1029
|
+
catalogNativeSlugs,
|
|
1030
|
+
goOrdered,
|
|
1031
|
+
config.subagentModels,
|
|
1032
|
+
websocketsEnabled(config),
|
|
1033
|
+
maMode as "v1" | "default" | "v2",
|
|
1034
|
+
exactComboCatalogSlugs(config),
|
|
1035
|
+
accountSelectors,
|
|
1036
|
+
suppressedBareNativeSlugs,
|
|
1037
|
+
new Set(),
|
|
1038
|
+
nativeContextLimits(config),
|
|
1039
|
+
accountNativeSlugs,
|
|
1040
|
+
accountNativeSlugsBySelector,
|
|
1041
|
+
config.keepNativeChatGptOnV1 === true,
|
|
1042
|
+
);
|
|
1043
|
+
return jsonResponse({
|
|
1044
|
+
models: applyNativeVisibility(
|
|
1045
|
+
entries,
|
|
1046
|
+
disabledModels,
|
|
1047
|
+
accountSelectors.length > 0,
|
|
1048
|
+
new Set(accountNativeSlugs),
|
|
1049
|
+
),
|
|
1050
|
+
}, 200, req, policy);
|
|
524
1051
|
}
|
|
525
1052
|
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
526
1053
|
// (pure availability list — disabled natives are omitted entirely).
|
|
@@ -548,14 +1075,39 @@ export function startServer(port?: number) {
|
|
|
548
1075
|
reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
|
|
549
1076
|
};
|
|
550
1077
|
};
|
|
551
|
-
const
|
|
552
|
-
...visibleNativeSlugs(config).map(id => ({
|
|
1078
|
+
const nativeModelRow = (id: string, metadataId = id) => ({
|
|
553
1079
|
id,
|
|
554
1080
|
object: "model",
|
|
555
1081
|
created: 0,
|
|
556
1082
|
owned_by: "openai",
|
|
557
|
-
...grokEffortFields(
|
|
558
|
-
|
|
1083
|
+
...grokEffortFields(
|
|
1084
|
+
nativeReasoningEfforts(metadataId),
|
|
1085
|
+
nativeDefaultReasoningEffort(metadataId),
|
|
1086
|
+
),
|
|
1087
|
+
});
|
|
1088
|
+
// Selector-active discovery follows the same complete supported set as the Codex catalog
|
|
1089
|
+
// for both bare and qualified rows. Without selectors, the live catalog continues to own
|
|
1090
|
+
// bare availability.
|
|
1091
|
+
const selectorNativeSlugs = accountSelectors.length > 0
|
|
1092
|
+
? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
|
|
1093
|
+
: [];
|
|
1094
|
+
const bareSelectorNativeSlugs = accountSelectors.length > 0
|
|
1095
|
+
? selectorNativeSlugs
|
|
1096
|
+
: [];
|
|
1097
|
+
const visibleNatives = includeNativeOpenAi
|
|
1098
|
+
? accountSelectors.length > 0
|
|
1099
|
+
? bareSelectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
|
|
1100
|
+
: visibleNativeSlugs(config)
|
|
1101
|
+
: [];
|
|
1102
|
+
const visibleAccountNatives = accountSelectors.flatMap(selector =>
|
|
1103
|
+
(accountNativeSlugsBySelector.get(selector) ?? []).filter(metadataId => !disabledNatives.has(metadataId)).flatMap(metadataId => {
|
|
1104
|
+
const id = `${selector}/${metadataId}`;
|
|
1105
|
+
return disabledModels.has(id) ? [] : [{ id, metadataId }];
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
const data = [
|
|
1109
|
+
...visibleNatives.map(id => nativeModelRow(id)),
|
|
1110
|
+
...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
|
|
559
1111
|
...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
|
|
560
1112
|
id: m.alias ?? `${m.provider}/${m.id}`,
|
|
561
1113
|
object: "model",
|
|
@@ -564,7 +1116,7 @@ export function startServer(port?: number) {
|
|
|
564
1116
|
...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
|
|
565
1117
|
})),
|
|
566
1118
|
];
|
|
567
|
-
return jsonResponse({ object: "list", data }, 200, req,
|
|
1119
|
+
return jsonResponse({ object: "list", data }, 200, req, policy);
|
|
568
1120
|
}
|
|
569
1121
|
|
|
570
1122
|
// Remote compaction v1 (codex-rs with Feature::RemoteCompactionV2 off — the default).
|
|
@@ -572,12 +1124,12 @@ export function startServer(port?: number) {
|
|
|
572
1124
|
// before the /v1/* 404 guard below.
|
|
573
1125
|
if (url.pathname === "/v1/responses/compact" && req.method === "POST") {
|
|
574
1126
|
if (isDraining()) {
|
|
575
|
-
return drainingResponse(req);
|
|
1127
|
+
return drainingResponse(req, policy);
|
|
576
1128
|
}
|
|
577
|
-
const admission = resolveResponsesApiAuth(req,
|
|
578
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
579
|
-
if (!isAllowedRequestOrigin(req,
|
|
580
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1129
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1130
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1131
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1132
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
581
1133
|
}
|
|
582
1134
|
const start = Date.now();
|
|
583
1135
|
const requestId = nextRequestLogId(start);
|
|
@@ -587,16 +1139,16 @@ export function startServer(port?: number) {
|
|
|
587
1139
|
...admissionFields(admission),
|
|
588
1140
|
inboundProtocol: "responses",
|
|
589
1141
|
};
|
|
590
|
-
return runAdmittedHttpTurn(req, async
|
|
1142
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
591
1143
|
let response: Response;
|
|
592
1144
|
try {
|
|
593
|
-
response = await handleResponsesCompact(req, config, logCtx);
|
|
1145
|
+
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
|
|
594
1146
|
} catch {
|
|
595
1147
|
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
596
1148
|
}
|
|
597
1149
|
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
598
1150
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
599
|
-
return withCors(response, req,
|
|
1151
|
+
return withCors(response, req, policy);
|
|
600
1152
|
});
|
|
601
1153
|
}
|
|
602
1154
|
|
|
@@ -606,12 +1158,12 @@ export function startServer(port?: number) {
|
|
|
606
1158
|
) {
|
|
607
1159
|
disableResponsesRequestTimeout(req, requestServer);
|
|
608
1160
|
if (isDraining()) {
|
|
609
|
-
return drainingResponse(req);
|
|
1161
|
+
return drainingResponse(req, policy);
|
|
610
1162
|
}
|
|
611
|
-
const admission = resolveApiAuth(req,
|
|
612
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
613
|
-
if (!isAllowedRequestOrigin(req,
|
|
614
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1163
|
+
const admission = resolveApiAuth(req, policy);
|
|
1164
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1165
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1166
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
615
1167
|
}
|
|
616
1168
|
const start = Date.now();
|
|
617
1169
|
const requestId = nextRequestLogId(start);
|
|
@@ -621,24 +1173,24 @@ export function startServer(port?: number) {
|
|
|
621
1173
|
...admissionFields(admission),
|
|
622
1174
|
};
|
|
623
1175
|
const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
|
|
624
|
-
return runAdmittedHttpTurn(req, async
|
|
625
|
-
const response = await handleImages(req, config, endpoint, logCtx);
|
|
1176
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1177
|
+
const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
|
|
626
1178
|
addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
627
|
-
return withCors(response, req,
|
|
1179
|
+
return withCors(response, req, policy);
|
|
628
1180
|
});
|
|
629
1181
|
}
|
|
630
1182
|
|
|
631
1183
|
if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
|
|
632
|
-
const admission = resolveApiAuth(req,
|
|
633
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
634
|
-
if (!isAllowedRequestOrigin(req,
|
|
635
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1184
|
+
const admission = resolveApiAuth(req, policy);
|
|
1185
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1186
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1187
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
636
1188
|
}
|
|
637
1189
|
const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
|
|
638
1190
|
const { resolveArtifactPath } = await import("../images/artifacts");
|
|
639
1191
|
const artifactPath = resolveArtifactPath(id);
|
|
640
1192
|
if (!artifactPath) {
|
|
641
|
-
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req,
|
|
1193
|
+
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, policy);
|
|
642
1194
|
}
|
|
643
1195
|
const file = Bun.file(artifactPath);
|
|
644
1196
|
const ext = artifactPath.split(".").pop()?.toLowerCase();
|
|
@@ -655,18 +1207,18 @@ export function startServer(port?: number) {
|
|
|
655
1207
|
"cache-control": "private, max-age=3600",
|
|
656
1208
|
"x-content-type-options": "nosniff",
|
|
657
1209
|
},
|
|
658
|
-
}), req,
|
|
1210
|
+
}), req, policy);
|
|
659
1211
|
}
|
|
660
1212
|
|
|
661
1213
|
if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
|
|
662
1214
|
disableResponsesRequestTimeout(req, requestServer);
|
|
663
1215
|
if (isDraining()) {
|
|
664
|
-
return drainingResponse(req);
|
|
1216
|
+
return drainingResponse(req, policy);
|
|
665
1217
|
}
|
|
666
|
-
const admission = resolveApiAuth(req,
|
|
667
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
668
|
-
if (!isAllowedRequestOrigin(req,
|
|
669
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1218
|
+
const admission = resolveApiAuth(req, policy);
|
|
1219
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1220
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1221
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
670
1222
|
}
|
|
671
1223
|
const start = Date.now();
|
|
672
1224
|
const requestId = nextRequestLogId(start);
|
|
@@ -675,23 +1227,22 @@ export function startServer(port?: number) {
|
|
|
675
1227
|
provider: "unknown",
|
|
676
1228
|
...admissionFields(admission),
|
|
677
1229
|
};
|
|
678
|
-
return runAdmittedHttpTurn(req, async
|
|
679
|
-
const response = await handleSearch(req, config, logCtx);
|
|
1230
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1231
|
+
const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
|
|
680
1232
|
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
681
1233
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
682
|
-
return withCors(response, req,
|
|
1234
|
+
return withCors(response, req, policy);
|
|
683
1235
|
});
|
|
684
1236
|
}
|
|
685
1237
|
|
|
686
1238
|
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
687
|
-
disableResponsesRequestTimeout(req, requestServer);
|
|
688
1239
|
if (isDraining()) {
|
|
689
|
-
return drainingResponse(req);
|
|
1240
|
+
return drainingResponse(req, policy);
|
|
690
1241
|
}
|
|
691
|
-
const admission = resolveResponsesApiAuth(req,
|
|
692
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
693
|
-
if (!isAllowedRequestOrigin(req,
|
|
694
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1242
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1243
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1244
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1245
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
695
1246
|
}
|
|
696
1247
|
const start = Date.now();
|
|
697
1248
|
const requestId = nextRequestLogId(start);
|
|
@@ -701,6 +1252,7 @@ export function startServer(port?: number) {
|
|
|
701
1252
|
...admissionFields(admission),
|
|
702
1253
|
inboundProtocol: "responses",
|
|
703
1254
|
};
|
|
1255
|
+
if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
|
|
704
1256
|
let logged = false;
|
|
705
1257
|
const finalizeNativePassthroughLog = (
|
|
706
1258
|
status: number,
|
|
@@ -710,13 +1262,15 @@ export function startServer(port?: number) {
|
|
|
710
1262
|
logged = true;
|
|
711
1263
|
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
712
1264
|
};
|
|
713
|
-
return runAdmittedHttpTurn(req, async turnAdmissionLease => {
|
|
1265
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
714
1266
|
const response = await handleResponses(req, config, logCtx, {
|
|
715
1267
|
turnAdmissionLease,
|
|
1268
|
+
admission,
|
|
1269
|
+
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
716
1270
|
abortSignal: req.signal,
|
|
717
1271
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
718
1272
|
onNativePassthroughTerminal: status => {
|
|
719
|
-
finalizeNativePassthroughLog(
|
|
1273
|
+
finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
720
1274
|
terminalStatus: status,
|
|
721
1275
|
closeReason: "terminal",
|
|
722
1276
|
});
|
|
@@ -725,7 +1279,7 @@ export function startServer(port?: number) {
|
|
|
725
1279
|
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
726
1280
|
},
|
|
727
1281
|
});
|
|
728
|
-
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req,
|
|
1282
|
+
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy);
|
|
729
1283
|
});
|
|
730
1284
|
}
|
|
731
1285
|
|
|
@@ -733,29 +1287,33 @@ export function startServer(port?: number) {
|
|
|
733
1287
|
// Claude Code posts `/v1/messages?beta=true` — pathname match ignores the query (003 G9).
|
|
734
1288
|
if (url.pathname === "/v1/messages/count_tokens" && req.method === "POST") {
|
|
735
1289
|
if (isDraining()) {
|
|
736
|
-
return drainingResponse(req);
|
|
1290
|
+
return drainingResponse(req, policy);
|
|
737
1291
|
}
|
|
738
|
-
const admission = resolveApiAuth(req,
|
|
1292
|
+
const admission = resolveApiAuth(req, policy);
|
|
739
1293
|
if (!admission) {
|
|
740
|
-
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req,
|
|
1294
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
|
|
741
1295
|
}
|
|
742
|
-
if (!isAllowedRequestOrigin(req,
|
|
743
|
-
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req,
|
|
1296
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1297
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
744
1298
|
}
|
|
745
|
-
return runAdmittedHttpTurn(req, async () => withCors(
|
|
1299
|
+
return runAdmittedHttpTurn(req, policy, async () => withCors(
|
|
1300
|
+
await handleClaudeCountTokens(req, config, policy),
|
|
1301
|
+
req,
|
|
1302
|
+
policy,
|
|
1303
|
+
));
|
|
746
1304
|
}
|
|
747
1305
|
|
|
748
1306
|
if (url.pathname === "/v1/messages" && req.method === "POST") {
|
|
749
1307
|
disableResponsesRequestTimeout(req, requestServer);
|
|
750
1308
|
if (isDraining()) {
|
|
751
|
-
return drainingResponse(req);
|
|
1309
|
+
return drainingResponse(req, policy);
|
|
752
1310
|
}
|
|
753
|
-
const admission = resolveApiAuth(req,
|
|
1311
|
+
const admission = resolveApiAuth(req, policy);
|
|
754
1312
|
if (!admission) {
|
|
755
|
-
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req,
|
|
1313
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
|
|
756
1314
|
}
|
|
757
|
-
if (!isAllowedRequestOrigin(req,
|
|
758
|
-
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req,
|
|
1315
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1316
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
759
1317
|
}
|
|
760
1318
|
const start = Date.now();
|
|
761
1319
|
const requestId = nextRequestLogId(start);
|
|
@@ -768,10 +1326,10 @@ export function startServer(port?: number) {
|
|
|
768
1326
|
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
|
|
769
1327
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
770
1328
|
// translated Anthropic stream here.
|
|
771
|
-
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
772
|
-
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1329
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1330
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
|
|
773
1331
|
req,
|
|
774
|
-
|
|
1332
|
+
policy,
|
|
775
1333
|
));
|
|
776
1334
|
}
|
|
777
1335
|
|
|
@@ -780,12 +1338,12 @@ export function startServer(port?: number) {
|
|
|
780
1338
|
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
|
781
1339
|
disableResponsesRequestTimeout(req, requestServer);
|
|
782
1340
|
if (isDraining()) {
|
|
783
|
-
return drainingResponse(req);
|
|
1341
|
+
return drainingResponse(req, policy);
|
|
784
1342
|
}
|
|
785
|
-
const admission = resolveResponsesApiAuth(req,
|
|
786
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
787
|
-
if (!isAllowedRequestOrigin(req,
|
|
788
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1343
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1344
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1345
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1346
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
789
1347
|
}
|
|
790
1348
|
const start = Date.now();
|
|
791
1349
|
const requestId = nextRequestLogId(start);
|
|
@@ -795,8 +1353,8 @@ export function startServer(port?: number) {
|
|
|
795
1353
|
...admissionFields(admission),
|
|
796
1354
|
inboundProtocol: "chat",
|
|
797
1355
|
};
|
|
798
|
-
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
799
|
-
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1356
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1357
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
|
|
800
1358
|
req,
|
|
801
1359
|
config,
|
|
802
1360
|
));
|
|
@@ -811,12 +1369,12 @@ export function startServer(port?: number) {
|
|
|
811
1369
|
) {
|
|
812
1370
|
disableResponsesRequestTimeout(req, requestServer);
|
|
813
1371
|
if (isDraining()) {
|
|
814
|
-
return drainingResponse(req);
|
|
1372
|
+
return drainingResponse(req, policy);
|
|
815
1373
|
}
|
|
816
|
-
const admission = resolveApiAuth(req,
|
|
817
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
818
|
-
if (!isAllowedRequestOrigin(req,
|
|
819
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req,
|
|
1374
|
+
const admission = resolveApiAuth(req, policy);
|
|
1375
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1376
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1377
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
820
1378
|
}
|
|
821
1379
|
const start = Date.now();
|
|
822
1380
|
const requestId = nextRequestLogId(start);
|
|
@@ -825,8 +1383,8 @@ export function startServer(port?: number) {
|
|
|
825
1383
|
provider: "unknown",
|
|
826
1384
|
...admissionFields(admission),
|
|
827
1385
|
};
|
|
828
|
-
return runAdmittedHttpTurn(req, async
|
|
829
|
-
const response = await handleLive(req, config, logCtx);
|
|
1386
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1387
|
+
const response = await handleLive(req, config, logCtx, turnAdmissionLease);
|
|
830
1388
|
addFinalRequestLog(
|
|
831
1389
|
requestId,
|
|
832
1390
|
start,
|
|
@@ -834,23 +1392,26 @@ export function startServer(port?: number) {
|
|
|
834
1392
|
response.status,
|
|
835
1393
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined,
|
|
836
1394
|
);
|
|
837
|
-
return withCors(response, req,
|
|
1395
|
+
return withCors(response, req, policy);
|
|
838
1396
|
});
|
|
839
1397
|
}
|
|
840
1398
|
|
|
841
|
-
// Voice / Realtime
|
|
842
|
-
// /v1/realtime?call_id= (or /v1/realtime/calls/{callId}).
|
|
1399
|
+
// Voice / Realtime WebSocket relay. Sideband joins: Frameless /v1/live/{callId};
|
|
1400
|
+
// Realtime v1 /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Standalone
|
|
1401
|
+
// sessions (codex-rs thread/realtime/start, WebSocket transport — the desktop voice
|
|
1402
|
+
// path): /v1/realtime?intent=quicksilver&model= and /v1/live?model=.
|
|
1403
|
+
// Transparent bidirectional relay.
|
|
843
1404
|
const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
|
|
844
|
-
? parseLiveSidebandTarget(url.pathname, url.searchParams)
|
|
1405
|
+
? parseLiveSidebandTarget(url.pathname, url.searchParams, url.search.replace(/^\?/, ""))
|
|
845
1406
|
: null;
|
|
846
1407
|
if (liveSidebandTarget) {
|
|
847
1408
|
if (isDraining()) {
|
|
848
|
-
return drainingResponse(req);
|
|
1409
|
+
return drainingResponse(req, policy);
|
|
849
1410
|
}
|
|
850
|
-
const admission = resolveApiAuth(req,
|
|
851
|
-
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
852
|
-
if (!isAllowedRequestOrigin(req,
|
|
853
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req,
|
|
1411
|
+
const admission = resolveApiAuth(req, policy);
|
|
1412
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1413
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1414
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
|
|
854
1415
|
}
|
|
855
1416
|
const start = Date.now();
|
|
856
1417
|
const requestId = nextRequestLogId(start);
|
|
@@ -859,22 +1420,34 @@ export function startServer(port?: number) {
|
|
|
859
1420
|
provider: "unknown",
|
|
860
1421
|
...admissionFields(admission),
|
|
861
1422
|
};
|
|
862
|
-
const
|
|
1423
|
+
const turnAdmissionLease = tryAdmitTurn();
|
|
1424
|
+
if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
|
|
1425
|
+
let resolved;
|
|
1426
|
+
try {
|
|
1427
|
+
resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease);
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
turnAdmissionLease.release();
|
|
1430
|
+
throw error;
|
|
1431
|
+
}
|
|
863
1432
|
if (resolved instanceof Response) {
|
|
1433
|
+
turnAdmissionLease.release();
|
|
864
1434
|
addFinalRequestLog(requestId, start, logCtx, resolved.status);
|
|
865
|
-
return withCors(resolved, req,
|
|
1435
|
+
return withCors(resolved, req, policy);
|
|
866
1436
|
}
|
|
867
1437
|
addFinalRequestLog(requestId, start, logCtx, 101);
|
|
868
|
-
if (
|
|
1438
|
+
if (requestServer.upgrade(req, {
|
|
869
1439
|
data: {
|
|
870
1440
|
kind: "live-sideband",
|
|
871
1441
|
liveUpstreamUrl: resolved.upstreamWsUrl,
|
|
872
1442
|
liveUpstreamHeaders: resolved.headers,
|
|
873
1443
|
livePending: [],
|
|
1444
|
+
livePendingBytes: 0,
|
|
874
1445
|
liveOpened: false,
|
|
1446
|
+
liveTurnAdmissionLease: turnAdmissionLease,
|
|
875
1447
|
} satisfies WsData,
|
|
876
1448
|
})) return undefined as unknown as Response;
|
|
877
|
-
|
|
1449
|
+
turnAdmissionLease.release();
|
|
1450
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
878
1451
|
}
|
|
879
1452
|
|
|
880
1453
|
// Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
|
|
@@ -882,12 +1455,17 @@ export function startServer(port?: number) {
|
|
|
882
1455
|
// endpoint clients — memories/*, realtime/* — would surface confusing
|
|
883
1456
|
// serde decode errors instead of a clean not-found).
|
|
884
1457
|
if (url.pathname.startsWith("/v1/")) {
|
|
885
|
-
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req,
|
|
1458
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
886
1459
|
}
|
|
887
1460
|
|
|
888
1461
|
const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
|
|
889
1462
|
? issueGuiSession(req, config, managementAuth)
|
|
890
1463
|
: null;
|
|
1464
|
+
// Dedicated bootstrap path: answer without requiring a packaged GUI build, so the
|
|
1465
|
+
// Vite dev server can mint an origin-bound loopback session on a fresh checkout.
|
|
1466
|
+
if (url.pathname === "/opencodex-session" && guiSessionCandidate) {
|
|
1467
|
+
return serveSessionBootstrap(guiSessionCandidate);
|
|
1468
|
+
}
|
|
891
1469
|
const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
|
|
892
1470
|
if (guiFile) return guiFile;
|
|
893
1471
|
if (url.pathname === "/" && req.method === "GET") {
|
|
@@ -897,6 +1475,7 @@ export function startServer(port?: number) {
|
|
|
897
1475
|
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
898
1476
|
},
|
|
899
1477
|
websocket: {
|
|
1478
|
+
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
900
1479
|
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
901
1480
|
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
902
1481
|
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
@@ -904,7 +1483,11 @@ export function startServer(port?: number) {
|
|
|
904
1483
|
// Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
|
|
905
1484
|
open(ws: ServerWebSocket<WsData>) {
|
|
906
1485
|
if (ws.data.kind === "live-sideband") {
|
|
907
|
-
|
|
1486
|
+
if (!ws.data.liveTurnAdmissionLease) {
|
|
1487
|
+
closeLiveSideband(ws, 1013, "server busy");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
|
|
908
1491
|
return;
|
|
909
1492
|
}
|
|
910
1493
|
if (!ws.data.admissionLease) {
|
|
@@ -916,15 +1499,24 @@ export function startServer(port?: number) {
|
|
|
916
1499
|
},
|
|
917
1500
|
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
918
1501
|
if (ws.data.kind === "live-sideband") {
|
|
1502
|
+
if (ws.data.liveClosing) return;
|
|
1503
|
+
const rawBytes = webSocketFrameBytes(raw);
|
|
1504
|
+
if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
|
|
1505
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
919
1508
|
logLiveSidebandFrame("c2u", raw);
|
|
920
1509
|
const upstream = ws.data.liveUpstream;
|
|
921
1510
|
if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
|
|
922
|
-
const
|
|
923
|
-
if (
|
|
1511
|
+
const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
|
|
1512
|
+
if (enqueueResult === "too-many-frames") {
|
|
924
1513
|
closeLiveSideband(ws, 1009, "too many pending frames");
|
|
925
1514
|
return;
|
|
926
1515
|
}
|
|
927
|
-
|
|
1516
|
+
if (enqueueResult === "too-many-bytes") {
|
|
1517
|
+
closeLiveSideband(ws, 1009, "too many pending bytes");
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
928
1520
|
return;
|
|
929
1521
|
}
|
|
930
1522
|
if (upstream.readyState !== WebSocket.OPEN) {
|
|
@@ -932,7 +1524,7 @@ export function startServer(port?: number) {
|
|
|
932
1524
|
return;
|
|
933
1525
|
}
|
|
934
1526
|
try {
|
|
935
|
-
upstream
|
|
1527
|
+
sendUpstreamFrame(upstream, raw);
|
|
936
1528
|
} catch {
|
|
937
1529
|
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
938
1530
|
}
|
|
@@ -1029,7 +1621,9 @@ export function startServer(port?: number) {
|
|
|
1029
1621
|
try {
|
|
1030
1622
|
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
1031
1623
|
const response = await handleResponses(req, config, logCtx, {
|
|
1624
|
+
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
1032
1625
|
forceEmptyResponseId: true,
|
|
1626
|
+
inboundTransport: "websocket",
|
|
1033
1627
|
abortSignal: turnAbort.signal,
|
|
1034
1628
|
turnAdmissionLease,
|
|
1035
1629
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
@@ -1057,9 +1651,12 @@ export function startServer(port?: number) {
|
|
|
1057
1651
|
finalizeLog(429);
|
|
1058
1652
|
// Codex Desktop rides this WS transport, so it must carry the same
|
|
1059
1653
|
// actionable text as HTTP; a frame has no headers, hence message-only.
|
|
1654
|
+
const accountSelector = typeof payload.model === "string"
|
|
1655
|
+
? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
|
|
1656
|
+
: undefined;
|
|
1060
1657
|
sendJsonFrame(ws, buildWsErrorFrame(429, {
|
|
1061
1658
|
type: "rate_limit_error",
|
|
1062
|
-
message: cooldownErrorMessage(err),
|
|
1659
|
+
message: cooldownErrorMessage(err, accountSelector),
|
|
1063
1660
|
}));
|
|
1064
1661
|
return;
|
|
1065
1662
|
}
|
|
@@ -1080,8 +1677,7 @@ export function startServer(port?: number) {
|
|
|
1080
1677
|
},
|
|
1081
1678
|
close(ws: ServerWebSocket<WsData>) {
|
|
1082
1679
|
if (ws.data.kind === "live-sideband") {
|
|
1083
|
-
ws
|
|
1084
|
-
ws.data.liveUpstream = undefined;
|
|
1680
|
+
closeLiveSideband(ws);
|
|
1085
1681
|
return;
|
|
1086
1682
|
}
|
|
1087
1683
|
unregisterCodexWebSocket(ws);
|
|
@@ -1090,10 +1686,69 @@ export function startServer(port?: number) {
|
|
|
1090
1686
|
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
1091
1687
|
},
|
|
1092
1688
|
},
|
|
1093
|
-
|
|
1689
|
+
} as const;
|
|
1690
|
+
|
|
1691
|
+
server = Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost });
|
|
1094
1692
|
|
|
1693
|
+
// Both binds are one startup transaction (#1102). If the loopback bind fails after the
|
|
1694
|
+
// public one succeeded, leaving the public listener up would strand it: the CLI's port
|
|
1695
|
+
// retry would read the failure as a public-port conflict and pick a different port,
|
|
1696
|
+
// accumulating listeners. Roll back and rethrow the original error instead.
|
|
1697
|
+
if (loopbackListenerPort !== null) {
|
|
1698
|
+
try {
|
|
1699
|
+
loopbackServer = Bun.serve<WsData>({
|
|
1700
|
+
...serveOptions,
|
|
1701
|
+
port: loopbackListenerPort,
|
|
1702
|
+
hostname: "127.0.0.1",
|
|
1703
|
+
});
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
try {
|
|
1706
|
+
// startServer is synchronous, so this rollback cannot await. Bun begins closing the
|
|
1707
|
+
// listen socket on the call itself; the caller sees the original bind error either
|
|
1708
|
+
// way, and the alternative — leaving the public listener up — is the failure this
|
|
1709
|
+
// rollback exists to prevent.
|
|
1710
|
+
void server.stop(true);
|
|
1711
|
+
} catch {
|
|
1712
|
+
/* the original bind error is the one worth reporting */
|
|
1713
|
+
}
|
|
1714
|
+
throw error;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
userCostOverlayReconciler?.stop();
|
|
1719
|
+
backgroundLifecycle?.releaseAfterFailedStart();
|
|
1720
|
+
void nativeMainLifecycle.release();
|
|
1721
|
+
throw error;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
bindNativeMainStartupLifecycle(server, nativeMainLifecycle);
|
|
1725
|
+
const nativeStop = server.stop.bind(server);
|
|
1726
|
+
const loopbackListenerRef = loopbackServer;
|
|
1727
|
+
Object.defineProperty(server, "stop", {
|
|
1728
|
+
configurable: true,
|
|
1729
|
+
value: async (closeActiveConnections?: boolean): Promise<void> => {
|
|
1730
|
+
// The orchestration lives in `runListenerShutdown` so its two competing properties —
|
|
1731
|
+
// cleanup completes, failure propagates — are testable without a live socket.
|
|
1732
|
+
await runListenerShutdown(
|
|
1733
|
+
[
|
|
1734
|
+
() => nativeStop(closeActiveConnections),
|
|
1735
|
+
...(loopbackListenerRef
|
|
1736
|
+
? [() => loopbackListenerRef.stop(closeActiveConnections)]
|
|
1737
|
+
: []),
|
|
1738
|
+
async () => {
|
|
1739
|
+
userCostOverlayReconciler?.stop();
|
|
1740
|
+
},
|
|
1741
|
+
],
|
|
1742
|
+
async () => {
|
|
1743
|
+
await backgroundLifecycle.release();
|
|
1744
|
+
await releaseNativeMainStartupLifecycle(server);
|
|
1745
|
+
},
|
|
1746
|
+
);
|
|
1747
|
+
},
|
|
1748
|
+
});
|
|
1095
1749
|
setServerRef(server);
|
|
1096
1750
|
const actualPort = server.port ?? listenPort;
|
|
1751
|
+
boundPort = actualPort;
|
|
1097
1752
|
setCorsOrigin(actualPort);
|
|
1098
1753
|
|
|
1099
1754
|
console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
|
|
@@ -1103,6 +1758,17 @@ export function startServer(port?: number) {
|
|
|
1103
1758
|
console.log(` GET /api/* → management API`);
|
|
1104
1759
|
console.log(` GET / → GUI dashboard`);
|
|
1105
1760
|
|
|
1761
|
+
if (loopbackServer) {
|
|
1762
|
+
// Loud on every start, not once at enable time. An operator who inherits a config, or
|
|
1763
|
+
// who forgot, has to be able to see that an unauthenticated surface is live without
|
|
1764
|
+
// reading the file.
|
|
1765
|
+
const loopbackPort = loopbackServer.port ?? loopbackListenerPort;
|
|
1766
|
+
console.warn(`⚠️ Unauthenticated loopback listener active on http://127.0.0.1:${loopbackPort}`);
|
|
1767
|
+
console.warn(` Any local process can use it without a credential — it spends account`);
|
|
1768
|
+
console.warn(` quota and paid provider credentials, and can starve authenticated`);
|
|
1769
|
+
console.warn(` remote clients. Not for shared or multi-tenant hosts.`);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1106
1772
|
// Prime pool-account quota in the background so the rotation engine has real
|
|
1107
1773
|
// usage scores from the first routing decision, even when the dashboard is
|
|
1108
1774
|
// never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
|
|
@@ -1114,13 +1780,31 @@ export function startServer(port?: number) {
|
|
|
1114
1780
|
&& isCanonicalOpenAiForwardProvider(openAiProvider)
|
|
1115
1781
|
&& providerCodexAccountMode("openai", openAiProvider) === "pool"
|
|
1116
1782
|
) {
|
|
1117
|
-
import("../codex/
|
|
1783
|
+
import("../codex/plan-from-token")
|
|
1784
|
+
.then(({ reconcileCodexPlansFromTokens }) => {
|
|
1785
|
+
try {
|
|
1786
|
+
reconcileCodexPlansFromTokens(config);
|
|
1787
|
+
} catch {
|
|
1788
|
+
// Derived plan metadata must not block WHAM priming.
|
|
1789
|
+
}
|
|
1790
|
+
return import("../codex/auth-api");
|
|
1791
|
+
})
|
|
1118
1792
|
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
|
|
1119
1793
|
.catch(() => {});
|
|
1120
1794
|
}
|
|
1121
1795
|
|
|
1122
1796
|
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
1123
|
-
|
|
1797
|
+
backgroundLifecycle.scheduleStartupRun();
|
|
1798
|
+
|
|
1799
|
+
// Compatibility Lab is optional: wire it only for installs that actually use it -- any
|
|
1800
|
+
// routing profile, or automation enabled on disk. This runs synchronously before
|
|
1801
|
+
// startServer returns, in the same turn as Bun.serve, so a policy route can never be
|
|
1802
|
+
// evaluated before its evidence provider is registered. That ordering is load-bearing:
|
|
1803
|
+
// the subagent-fallback chain routes synchronously and has nowhere to await.
|
|
1804
|
+
const labConfigDir = getConfigDir();
|
|
1805
|
+
if (labActivationRequired(config, labConfigDir)) {
|
|
1806
|
+
activateLab(config, labConfigDir);
|
|
1807
|
+
}
|
|
1124
1808
|
|
|
1125
1809
|
return server;
|
|
1126
1810
|
}
|