@iislee/opencodex 2.11.0 → 2.35.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 +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -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 +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -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 +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- 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 +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- 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 +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- 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 +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- 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 +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- 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 +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- 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 +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- 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 +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -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 +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- 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 +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -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/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -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 +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -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 +360 -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 +622 -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 +675 -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 +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -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 +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -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 +157 -33
- package/src/images/plan.ts +24 -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-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -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/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- 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 +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- 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/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- 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/service-secrets.ts +15 -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 +253 -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 +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -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/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- 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/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -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 +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -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 +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -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 +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -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 +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -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 +155 -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 +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- 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 +1021 -190
- package/src/server/lifecycle.ts +336 -14
- 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 +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -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 +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -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 +289 -33
- 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 +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- 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/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -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 +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -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 +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -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 +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- 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 +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- 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 +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- 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 +27 -16
- 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 +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { markActivity } from "../lib/sidecar-tracker";
|
|
2
|
+
import { knownModelIdsForProvider } from "../router";
|
|
2
3
|
import {
|
|
3
4
|
buildWarmupCompletionFrames,
|
|
4
5
|
buildWsErrorFrame,
|
|
@@ -16,16 +17,25 @@ import {
|
|
|
16
17
|
armClaudeCodeBaseline,
|
|
17
18
|
loadConfig,
|
|
18
19
|
saveConfig,
|
|
20
|
+
getConfigDir,
|
|
19
21
|
websocketsEnabled,
|
|
20
22
|
} from "../config";
|
|
21
23
|
import { reconcileOAuthProviders } from "../oauth";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
+
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
25
|
+
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
26
|
+
import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../service";
|
|
27
|
+
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
28
|
+
import {
|
|
29
|
+
inspectNativeCodexOwnership,
|
|
30
|
+
type NativeCodexOwnership,
|
|
31
|
+
type OwnershipInspection,
|
|
32
|
+
} from "../integrations/native/ownership-preflight";
|
|
33
|
+
import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
|
|
24
34
|
import {
|
|
25
35
|
reconcileLiveStateStores,
|
|
26
36
|
setLiveStateStoreConfig,
|
|
27
37
|
} from "../lib/state-store-registrations";
|
|
28
|
-
import {
|
|
38
|
+
import { startUserCostOverlayReconciler } from "../usage/user-cost-overlay-reconciler";
|
|
29
39
|
import {
|
|
30
40
|
configureAppOwnedMemoryBudget,
|
|
31
41
|
enforceAppOwnedMemoryBudget,
|
|
@@ -36,18 +46,26 @@ import {
|
|
|
36
46
|
registerDefaultAppOwnedMemoryStores,
|
|
37
47
|
registerDefaultAppOwnedObservedBuffers,
|
|
38
48
|
} from "../lib/app-owned-memory-stores";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
49
|
+
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
50
|
+
import { activateLab, labActivationRequired } from "../lib/lab-activation";
|
|
42
51
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
43
52
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
44
|
-
import {
|
|
53
|
+
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
54
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
45
55
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
46
56
|
import type { StorageCleanupPolicy } from "../types";
|
|
57
|
+
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
|
|
47
58
|
import {
|
|
48
59
|
CodexAccountCooldownError,
|
|
49
60
|
cooldownErrorMessage,
|
|
50
61
|
} from "../codex/auth-context";
|
|
62
|
+
import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
|
|
63
|
+
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex/account-namespaces";
|
|
64
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
65
|
+
import {
|
|
66
|
+
availableAccountGatedNativeModels,
|
|
67
|
+
resolveCodexModelEntitlements,
|
|
68
|
+
} from "../codex/model-entitlements";
|
|
51
69
|
export {
|
|
52
70
|
clearThreadAccountMap,
|
|
53
71
|
formatCodexProviderForLog,
|
|
@@ -56,7 +74,7 @@ export {
|
|
|
56
74
|
import { formatCodexProviderForLog } from "../codex/routing";
|
|
57
75
|
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
58
76
|
import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
|
|
59
|
-
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./gui-static";
|
|
77
|
+
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile, serveSessionBootstrap } from "./gui-static";
|
|
60
78
|
export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
|
|
61
79
|
export { resolveAdapter } from "./adapter-resolve";
|
|
62
80
|
import { formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
|
|
@@ -65,6 +83,7 @@ import {
|
|
|
65
83
|
getActiveTurnCount,
|
|
66
84
|
isDraining,
|
|
67
85
|
registerTurn,
|
|
86
|
+
runListenerShutdown,
|
|
68
87
|
setServerRef,
|
|
69
88
|
trackStreamLifetime,
|
|
70
89
|
tryAdmitTurn,
|
|
@@ -85,13 +104,13 @@ import {
|
|
|
85
104
|
addFinalRequestLog,
|
|
86
105
|
hydrateRequestLogsFromDisk,
|
|
87
106
|
httpStatusForRequestLogTerminal,
|
|
88
|
-
httpStatusForTerminalStatus,
|
|
89
107
|
inspectResponseLogSsePayload,
|
|
90
108
|
nextRequestLogId,
|
|
91
109
|
recordFirstOutput,
|
|
92
110
|
type RequestLogContext,
|
|
93
111
|
type RequestLogEntry,
|
|
94
112
|
} from "./request-log";
|
|
113
|
+
import { sessionLaneIdFromRequest } from "./request-log-conversation";
|
|
95
114
|
export {
|
|
96
115
|
addFinalRequestLog,
|
|
97
116
|
filterRequestLogs,
|
|
@@ -132,6 +151,8 @@ import {
|
|
|
132
151
|
admissionFields,
|
|
133
152
|
resolveApiAuth,
|
|
134
153
|
resolveResponsesApiAuth,
|
|
154
|
+
requestPolicyView,
|
|
155
|
+
type RequestPolicyView,
|
|
135
156
|
safeConfigDTO,
|
|
136
157
|
setCorsOrigin,
|
|
137
158
|
withCors,
|
|
@@ -153,24 +174,165 @@ import { handleChatCompletions } from "./chat-completions";
|
|
|
153
174
|
import { anthropicErrorResponse } from "../claude/outbound";
|
|
154
175
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
155
176
|
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
177
|
+
import {
|
|
178
|
+
bindNativeMainStartupLifecycle,
|
|
179
|
+
blockNativeMainStartupForUnownedServiceHome,
|
|
180
|
+
prepareNativeMainStartupLifecycle,
|
|
181
|
+
releaseNativeMainStartupLifecycle,
|
|
182
|
+
type NativeMainStartupGateDeps,
|
|
183
|
+
type NativeMainStartupLifecycle,
|
|
184
|
+
} from "../codex/native-profile-startup";
|
|
156
185
|
import { handleImages } from "./images";
|
|
157
186
|
import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
|
|
158
187
|
import { handleSearch } from "./search";
|
|
159
|
-
import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
|
|
160
|
-
import {
|
|
188
|
+
import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
|
|
189
|
+
import {
|
|
190
|
+
initializeManagementAuthState,
|
|
191
|
+
issueGuiSession,
|
|
192
|
+
managementPrincipal,
|
|
193
|
+
requireManagementAuth,
|
|
194
|
+
type ManagementAuthState,
|
|
195
|
+
} from "./management-auth";
|
|
196
|
+
import {
|
|
197
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
198
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
199
|
+
createLocalAttestationProof,
|
|
200
|
+
createLocalAttestationSecret,
|
|
201
|
+
} from "../lib/local-management-attestation";
|
|
202
|
+
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
203
|
+
import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
|
|
204
|
+
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
205
|
+
import {
|
|
206
|
+
createRuntimePackageTreeIntegrityGuard,
|
|
207
|
+
type PackageTreeIntegrityGuard,
|
|
208
|
+
} from "../lib/package-tree-integrity";
|
|
209
|
+
import { detectInstall } from "../update/index";
|
|
161
210
|
|
|
162
|
-
const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
211
|
+
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
163
212
|
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
164
213
|
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
214
|
+
const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
|
|
215
|
+
const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
|
|
165
216
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
export function exceedsLiveSidebandFrameByteLimit(frameBytes: number): boolean {
|
|
218
|
+
return frameBytes > MAX_WS_FRAME_BYTES;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function exceedsLiveSidebandPendingByteLimit(pendingBytes: number, incomingBytes: number): boolean {
|
|
222
|
+
return incomingBytes > LIVE_SIDEBAND_PENDING_BYTES_MAX - pendingBytes;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function webSocketFrameBytes(frame: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): number {
|
|
226
|
+
if (typeof frame === "string") return Buffer.byteLength(frame);
|
|
227
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
|
|
228
|
+
return frame.size;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export type LiveSidebandPendingEnqueueResult = "queued" | "too-many-frames" | "too-many-bytes";
|
|
232
|
+
|
|
233
|
+
export function enqueueLiveSidebandPendingFrame(
|
|
234
|
+
data: Pick<WsData, "livePending" | "livePendingBytes">,
|
|
235
|
+
frame: string | Buffer,
|
|
236
|
+
frameBytes = webSocketFrameBytes(frame),
|
|
237
|
+
): LiveSidebandPendingEnqueueResult {
|
|
238
|
+
const pending = data.livePending ?? (data.livePending = []);
|
|
239
|
+
if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) return "too-many-frames";
|
|
240
|
+
const pendingBytes = data.livePendingBytes ?? 0;
|
|
241
|
+
if (exceedsLiveSidebandPendingByteLimit(pendingBytes, frameBytes)) return "too-many-bytes";
|
|
242
|
+
pending.push(frame);
|
|
243
|
+
data.livePendingBytes = pendingBytes + frameBytes;
|
|
244
|
+
return "queued";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type LiveSidebandWebSocketFactory = (
|
|
248
|
+
url: string,
|
|
249
|
+
headers: Record<string, string>,
|
|
250
|
+
) => WebSocket;
|
|
251
|
+
|
|
252
|
+
function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
|
|
253
|
+
ws.data.liveTurnAdmissionLease?.release();
|
|
254
|
+
ws.data.liveTurnAdmissionLease = undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Send one live-sideband frame to the upstream socket.
|
|
259
|
+
*
|
|
260
|
+
* Bun's `WebSocket.send` accepts `string | Blob | BufferSource`, but the DOM-lib
|
|
261
|
+
* `Buffer` can be backed by a `SharedArrayBuffer`, which `BufferSource` rejects.
|
|
262
|
+
* `Uint8Array.from` copies into a fresh `ArrayBuffer`-backed view, so a frame
|
|
263
|
+
* arriving from `node:buffer` still round-trips byte-for-byte.
|
|
264
|
+
*/
|
|
265
|
+
function sendUpstreamFrame(upstream: WebSocket, frame: string | Buffer): void {
|
|
266
|
+
if (typeof frame === "string") {
|
|
267
|
+
upstream.send(frame);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
upstream.send(Uint8Array.from(frame));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
|
|
274
|
+
if (upstream && ws.data.liveUpstream !== upstream) return;
|
|
275
|
+
if (ws.data.liveCloseFallback !== undefined) {
|
|
276
|
+
clearTimeout(ws.data.liveCloseFallback);
|
|
277
|
+
ws.data.liveCloseFallback = undefined;
|
|
171
278
|
}
|
|
172
279
|
ws.data.liveUpstream = undefined;
|
|
173
280
|
ws.data.livePending = undefined;
|
|
281
|
+
ws.data.livePendingBytes = undefined;
|
|
282
|
+
ws.data.cancel = undefined;
|
|
283
|
+
releaseLiveSidebandAdmission(ws);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: WebSocket): void {
|
|
287
|
+
if (ws.data.liveCloseFallback !== undefined) return;
|
|
288
|
+
ws.data.liveCloseFallback = setTimeout(() => {
|
|
289
|
+
ws.data.liveCloseFallback = undefined;
|
|
290
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
291
|
+
if (upstream.readyState === WebSocket.CLOSED) {
|
|
292
|
+
finalizeLiveSideband(ws, upstream);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// A close frame was already sent below. Retry once, but never surrender
|
|
296
|
+
// native-main ownership while the authenticated transport remains live.
|
|
297
|
+
try {
|
|
298
|
+
upstream.close(1000, "upstream close timeout");
|
|
299
|
+
} catch {
|
|
300
|
+
/* upstream is already unusable */
|
|
301
|
+
}
|
|
302
|
+
// Some implementations transition synchronously without delivering the
|
|
303
|
+
// close event. That is still an observed CLOSED transport and is safe to
|
|
304
|
+
// finalize. CONNECTING/CLOSING peers keep the lease so profile switching
|
|
305
|
+
// fails at its own bounded drain deadline instead of racing live traffic.
|
|
306
|
+
// The earlier CLOSED check narrowed `readyState` to 0|1|2 in the type
|
|
307
|
+
// system, but the socket can still transition to CLOSED (3) before this
|
|
308
|
+
// fallback fires; the cast keeps the runtime-identical check.
|
|
309
|
+
if ((upstream.readyState as number) === 3) finalizeLiveSideband(ws, upstream);
|
|
310
|
+
}, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
|
|
314
|
+
if (ws.data.liveClosing) return;
|
|
315
|
+
ws.data.liveClosing = true;
|
|
316
|
+
ws.data.livePending = undefined;
|
|
317
|
+
ws.data.livePendingBytes = undefined;
|
|
318
|
+
ws.data.cancel = undefined;
|
|
319
|
+
const upstream = ws.data.liveUpstream;
|
|
320
|
+
// Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
|
|
321
|
+
// constant CLOSED is 3; the numeric literal is the runtime-identical check.
|
|
322
|
+
if (!upstream || upstream.readyState === 3) {
|
|
323
|
+
finalizeLiveSideband(ws, upstream);
|
|
324
|
+
} else {
|
|
325
|
+
// The sideband holds a native-main admission lease. Do not release it just
|
|
326
|
+
// because the downstream left: its authenticated upstream remains live
|
|
327
|
+
// until the close event arrives or the transport is observed CLOSED. The
|
|
328
|
+
// bounded fallback only retries close; it does not release ownership.
|
|
329
|
+
armLiveSidebandCloseFallback(ws, upstream);
|
|
330
|
+
try {
|
|
331
|
+
upstream.close(code, reason);
|
|
332
|
+
} catch {
|
|
333
|
+
/* the fallback retries close without releasing ownership */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
174
336
|
try {
|
|
175
337
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
176
338
|
ws.close(code, reason);
|
|
@@ -180,7 +342,12 @@ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""
|
|
|
180
342
|
}
|
|
181
343
|
}
|
|
182
344
|
|
|
183
|
-
function attachLiveSidebandUpstream(
|
|
345
|
+
function attachLiveSidebandUpstream(
|
|
346
|
+
ws: ServerWebSocket<WsData>,
|
|
347
|
+
createWebSocket: LiveSidebandWebSocketFactory = (url, headers) => (
|
|
348
|
+
new WebSocket(url, { headers } as unknown as string[])
|
|
349
|
+
),
|
|
350
|
+
): void {
|
|
184
351
|
const url = ws.data.liveUpstreamUrl;
|
|
185
352
|
if (!url) {
|
|
186
353
|
closeLiveSideband(ws, 1011, "missing upstream");
|
|
@@ -189,27 +356,24 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
189
356
|
let upstream: WebSocket;
|
|
190
357
|
try {
|
|
191
358
|
// Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
|
|
192
|
-
upstream =
|
|
359
|
+
upstream = createWebSocket(url, ws.data.liveUpstreamHeaders ?? {});
|
|
193
360
|
} catch {
|
|
194
361
|
closeLiveSideband(ws, 1011, "upstream connect failed");
|
|
195
362
|
return;
|
|
196
363
|
}
|
|
197
364
|
ws.data.liveUpstream = upstream;
|
|
198
|
-
ws.data.
|
|
199
|
-
|
|
200
|
-
upstream.close(1000, "client closed");
|
|
201
|
-
} catch {
|
|
202
|
-
/* ignore */
|
|
203
|
-
}
|
|
204
|
-
};
|
|
365
|
+
ws.data.liveClosing = false;
|
|
366
|
+
ws.data.cancel = () => closeLiveSideband(ws, 1000, "client closed");
|
|
205
367
|
|
|
206
368
|
upstream.addEventListener("open", () => {
|
|
369
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
207
370
|
ws.data.liveOpened = true;
|
|
208
371
|
const pending = ws.data.livePending ?? [];
|
|
209
372
|
ws.data.livePending = undefined;
|
|
373
|
+
ws.data.livePendingBytes = undefined;
|
|
210
374
|
for (const frame of pending) {
|
|
211
375
|
try {
|
|
212
|
-
upstream
|
|
376
|
+
sendUpstreamFrame(upstream, frame);
|
|
213
377
|
} catch {
|
|
214
378
|
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
215
379
|
return;
|
|
@@ -217,7 +381,12 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
217
381
|
}
|
|
218
382
|
});
|
|
219
383
|
upstream.addEventListener("message", (event) => {
|
|
384
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
220
385
|
try {
|
|
386
|
+
if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(event.data))) {
|
|
387
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
221
390
|
logLiveSidebandFrame("u2c", event.data);
|
|
222
391
|
if (typeof event.data === "string") ws.send(event.data);
|
|
223
392
|
else if (event.data instanceof ArrayBuffer) ws.send(event.data);
|
|
@@ -229,14 +398,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
229
398
|
}
|
|
230
399
|
});
|
|
231
400
|
upstream.addEventListener("close", (event) => {
|
|
401
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
402
|
+
ws.data.liveClosing = true;
|
|
403
|
+
finalizeLiveSideband(ws, upstream);
|
|
232
404
|
try {
|
|
233
405
|
ws.close(event.code || 1000, event.reason || "");
|
|
234
406
|
} catch {
|
|
235
407
|
/* ignore */
|
|
236
408
|
}
|
|
237
|
-
ws.data.liveUpstream = undefined;
|
|
238
409
|
});
|
|
239
410
|
upstream.addEventListener("error", () => {
|
|
411
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
240
412
|
closeLiveSideband(ws, 1011, "upstream error");
|
|
241
413
|
});
|
|
242
414
|
}
|
|
@@ -250,18 +422,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
250
422
|
// if (isEventStream && upstreamResponse.body) {
|
|
251
423
|
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
252
424
|
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
253
|
-
// #314 gated shape: win32
|
|
254
|
-
//
|
|
255
|
-
//
|
|
425
|
+
// #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
|
|
426
|
+
// upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
|
|
427
|
+
// requires explicit config-eager opt-in (`auto` always stays tee on darwin).
|
|
256
428
|
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
429
|
+
// Codex upstream WS runtime gating and the forced bounded single-reader branch
|
|
430
|
+
// are owned by responses/ws-upstream.ts and responses/core.ts respectively.
|
|
257
431
|
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
258
432
|
// new Response(eagerBody,
|
|
259
433
|
// Default shape (tee + background inspection):
|
|
260
434
|
// upstreamResponse.body.tee()
|
|
261
435
|
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
262
|
-
// process.platform === "win32"
|
|
263
|
-
// && !needsClientRewrite
|
|
264
|
-
// ? nativeBody
|
|
265
436
|
// relaySseWithFailedTail(repairedBody, upstream)
|
|
266
437
|
// new Response(clientBody
|
|
267
438
|
// markNativePassthroughSseResponse
|
|
@@ -271,12 +442,92 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
271
442
|
// trackSseForRequestLog(
|
|
272
443
|
// export function relaySseWithHeartbeat
|
|
273
444
|
|
|
274
|
-
export
|
|
275
|
-
|
|
445
|
+
export interface StartServerDeps {
|
|
446
|
+
/** Test-only seam; production always initializes its own management credential state. */
|
|
447
|
+
managementAuthState?: ManagementAuthState;
|
|
448
|
+
/** Test-only route dependencies, forwarded only after management admission succeeds. */
|
|
449
|
+
managementApi?: ManagementApiDeps;
|
|
450
|
+
/** Test-only native-main recovery dependencies; production constructs the normal manager. */
|
|
451
|
+
nativeMainStartup?: NativeMainStartupGateDeps;
|
|
452
|
+
/** Test-only ownership evidence; production inspects the installed service state. */
|
|
453
|
+
inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
|
|
454
|
+
/** Test-only service-home resolver; production resolves the current homes directly. */
|
|
455
|
+
resolveServiceHomes?: typeof currentServiceHomes;
|
|
456
|
+
/** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
|
|
457
|
+
liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
|
|
458
|
+
/** Test-only seam; production derives a fresh local-attestation secret per process. */
|
|
459
|
+
localAttestationSecret?: string;
|
|
460
|
+
/** Optional readiness gate; a fresh pending gate is created when omitted. */
|
|
461
|
+
readinessGate?: ReadinessGate;
|
|
462
|
+
/** Test-only package-tree observation; production captures package.json identity at boot. */
|
|
463
|
+
packageTreeIntegrity?: PackageTreeIntegrityGuard;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function inspectStartupOwnership(
|
|
467
|
+
deps: StartServerDeps,
|
|
468
|
+
currentHomes: ReturnType<typeof currentServiceHomes> | null,
|
|
469
|
+
statePaths: readonly string[] | null,
|
|
470
|
+
): OwnershipInspection {
|
|
471
|
+
try {
|
|
472
|
+
if (currentHomes === null || statePaths === null) {
|
|
473
|
+
return {
|
|
474
|
+
ownership: "unknown",
|
|
475
|
+
reason: "startup service-home resolution failed",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (deps.inspectNativeCodexOwnership) {
|
|
479
|
+
return deps.inspectNativeCodexOwnership({ currentHomes, statePaths });
|
|
480
|
+
}
|
|
481
|
+
return inspectNativeCodexOwnership({ currentHomes, statePaths });
|
|
482
|
+
} catch {
|
|
483
|
+
return {
|
|
484
|
+
ownership: "unknown",
|
|
485
|
+
reason: "service-home ownership inspection failed",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/*
|
|
491
|
+
* #1046. `startServer` rewrites the Codex models cache during boot, and an
|
|
492
|
+
* app-server that started earlier keeps its own in-memory model list. The stale
|
|
493
|
+
* warning is not emitted here: `handleStart` runs a catalog sync moments later,
|
|
494
|
+
* so warning now would read an mtime that write is about to move, and both sites
|
|
495
|
+
* calling the helper independently would warn twice. This records the fact; the
|
|
496
|
+
* CLI start path owns the single decision.
|
|
497
|
+
*
|
|
498
|
+
* A caller that starts a server without `handleStart` (tests, embedded use)
|
|
499
|
+
* deliberately gets no warning — lifecycle diagnostics belong to whoever owns
|
|
500
|
+
* the lifecycle.
|
|
501
|
+
*/
|
|
502
|
+
let startupCacheInvalidationWrote = false;
|
|
503
|
+
|
|
504
|
+
/** #1046: did this process's startup cache invalidation actually write? */
|
|
505
|
+
export function consumeStartupCacheInvalidationWrite(): boolean {
|
|
506
|
+
const wrote = startupCacheInvalidationWrote;
|
|
507
|
+
startupCacheInvalidationWrote = false;
|
|
508
|
+
return wrote;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function warnAgentTaskRecoveryStartup(config: {
|
|
512
|
+
agentTaskRecovery?: { enabled?: boolean };
|
|
513
|
+
}): void {
|
|
514
|
+
if (config.agentTaskRecovery?.enabled !== true) return;
|
|
515
|
+
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
|
|
516
|
+
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.");
|
|
517
|
+
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.");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
521
|
+
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
522
|
+
const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
|
|
523
|
+
warnAgentTaskRecoveryStartup(config);
|
|
276
524
|
setLiveStateStoreConfig(config);
|
|
277
525
|
applyProxyEnv(config);
|
|
278
526
|
assertServerAuthConfig(config);
|
|
279
|
-
const managementAuth = initializeManagementAuthState(config);
|
|
527
|
+
const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
|
|
528
|
+
let userCostOverlayReconciler: { stop(): void } | null = null;
|
|
529
|
+
// Arm synchronously before listen. A pending journal therefore makes __main__ unusable
|
|
530
|
+
// before any request can resolve its physical credential, while health/management/Pool stay live.
|
|
280
531
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
281
532
|
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
282
533
|
reconcileOAuthProviders(config);
|
|
@@ -310,7 +561,40 @@ export function startServer(port?: number) {
|
|
|
310
561
|
if (migrated) saveConfig(config);
|
|
311
562
|
}
|
|
312
563
|
}
|
|
313
|
-
|
|
564
|
+
// Resolve unattended service-home authority before any Codex lock, cache, owner,
|
|
565
|
+
// journal, or credential path. Both positive foreign evidence and an unprovable
|
|
566
|
+
// ownership state are non-authority.
|
|
567
|
+
startupCacheInvalidationWrote = false;
|
|
568
|
+
const resolveServiceHomes = deps.resolveServiceHomes ?? currentServiceHomes;
|
|
569
|
+
let startupOwnershipHomes: ReturnType<typeof currentServiceHomes> | null = null;
|
|
570
|
+
let startupOwnershipStatePaths: readonly string[] | null = null;
|
|
571
|
+
try {
|
|
572
|
+
const homes = resolveServiceHomes();
|
|
573
|
+
const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
|
|
574
|
+
startupOwnershipHomes = homes;
|
|
575
|
+
startupOwnershipStatePaths = statePaths;
|
|
576
|
+
} catch { /* inspection below stays unknown */ }
|
|
577
|
+
const startupCacheOwnership = inspectStartupOwnership(
|
|
578
|
+
deps,
|
|
579
|
+
startupOwnershipHomes,
|
|
580
|
+
startupOwnershipStatePaths,
|
|
581
|
+
);
|
|
582
|
+
// Startup cache invalidation is best-effort and must never block the server from
|
|
583
|
+
// serving. It now takes K so it cannot race a convergence commit. Use the home
|
|
584
|
+
// paired with the ownership inspection; re-reading ambient CODEX_HOME here could
|
|
585
|
+
// invalidate a different installation after an environment or mount change.
|
|
586
|
+
if (startupCacheOwnership.ownership === "owned" && startupOwnershipHomes !== null) {
|
|
587
|
+
try {
|
|
588
|
+
const startupCodexHome = startupOwnershipHomes.codexHome;
|
|
589
|
+
// #1046: record whether this actually rewrote the cache. `handleStart` ORs this
|
|
590
|
+
// with the later startup sync and warns ONCE about stale app-servers; warning
|
|
591
|
+
// here instead would read a catalog mtime the sync is about to move.
|
|
592
|
+
const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
|
|
593
|
+
invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
|
|
594
|
+
// A refused permit is not a write; only a completed run that returned true is.
|
|
595
|
+
startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
|
|
596
|
+
} catch { /* no readable Codex home: nothing to invalidate */ }
|
|
597
|
+
}
|
|
314
598
|
// Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
|
|
315
599
|
// the server can serve a request, and AFTER the startup migrations above — those run
|
|
316
600
|
// against a config nobody else holds and are the documented exception to the save
|
|
@@ -321,15 +605,12 @@ export function startServer(port?: number) {
|
|
|
321
605
|
// usage.jsonl already persists every request; rehydrate the in-memory Logs ring so
|
|
322
606
|
// /api/logs (and the GUI) survive `ocx stop` / `ocx start` process restarts.
|
|
323
607
|
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
608
|
registerDefaultAppOwnedMemoryStores();
|
|
328
609
|
registerDefaultAppOwnedObservedBuffers();
|
|
329
610
|
registerAppOwnedMemorySweepFallback();
|
|
330
611
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
331
612
|
enforceAppOwnedMemoryBudget();
|
|
332
|
-
|
|
613
|
+
registerCodexCooldownRecoveryProbeWorker(config);
|
|
333
614
|
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
334
615
|
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
335
616
|
// Heavy work runs in a Worker via the single-flight job controller.
|
|
@@ -337,9 +618,6 @@ export function startServer(port?: number) {
|
|
|
337
618
|
const applyPolicy = (policy: StorageCleanupPolicy) => {
|
|
338
619
|
config.storageCleanupPolicy = policy;
|
|
339
620
|
};
|
|
340
|
-
setStorageCleanupPolicyLiveSink(applyPolicy);
|
|
341
|
-
setStorageCleanupPolicyJobLiveApply(applyPolicy);
|
|
342
|
-
startStorageCleanupScheduler();
|
|
343
621
|
|
|
344
622
|
const listenPort = port ?? config.port ?? 10100;
|
|
345
623
|
setCorsOrigin(listenPort);
|
|
@@ -351,30 +629,98 @@ export function startServer(port?: number) {
|
|
|
351
629
|
const configuredHost = config.hostname?.trim();
|
|
352
630
|
const bindHost = !configuredHost || /^localhost$/i.test(configuredHost) ? "127.0.0.1" : configuredHost;
|
|
353
631
|
|
|
632
|
+
// Unauthenticated loopback listener (#1102). Off unless explicitly enabled.
|
|
633
|
+
const loopbackListener = config.unauthenticatedLoopbackListener;
|
|
634
|
+
const loopbackListenerPort = loopbackListener?.enabled ? loopbackListener.port : null;
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Which listener a request arrived on, expressed as the only thing that differs: the bind
|
|
638
|
+
* address the auth and CORS decisions should see.
|
|
639
|
+
*
|
|
640
|
+
* The public listener passes the shared config through untouched, so its behaviour is
|
|
641
|
+
* byte-identical to before. The loopback listener substitutes 127.0.0.1, which is what makes
|
|
642
|
+
* `isApiAuthRequired` return false for it — the same code path a plain loopback bind has
|
|
643
|
+
* always taken, including the Host-header check inside `isAllowedRequestOrigin`.
|
|
644
|
+
*
|
|
645
|
+
* Built per request rather than once per listener so a management-API config change is
|
|
646
|
+
* picked up immediately instead of being frozen at listen time.
|
|
647
|
+
*/
|
|
648
|
+
const publicPolicy = (): RequestPolicyView => config;
|
|
649
|
+
const loopbackPolicy = (): RequestPolicyView => requestPolicyView(config, "127.0.0.1");
|
|
650
|
+
void publicPolicy;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Routes the unauthenticated loopback listener will serve. Everything else 404s.
|
|
654
|
+
*
|
|
655
|
+
* This is an allowlist rather than a filter applied to the public handler, because a filter
|
|
656
|
+
* inverts the failure mode: a route added later would be reachable here by default. The four
|
|
657
|
+
* entries are exactly what a directly-spawned `codex app-server` needs.
|
|
658
|
+
*
|
|
659
|
+
* `GET /v1/models` is on the list for a reason that is easy to miss. When catalog
|
|
660
|
+
* materialization fails or finds no source, `syncCodex` warns and injects with
|
|
661
|
+
* `catalogPath: null`; Codex then builds an ONLINE model manager and `model/list` refreshes
|
|
662
|
+
* through `GET {base_url}/models`. Returning 404 there would leave the picker on its bundled
|
|
663
|
+
* fallback — fixing the direct-spawn host while breaking its model list.
|
|
664
|
+
*/
|
|
665
|
+
function loopbackRouteAllowed(url: URL, req: Request): boolean {
|
|
666
|
+
const path = url.pathname;
|
|
667
|
+
if (path === "/v1/responses") {
|
|
668
|
+
return req.method === "POST" || req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
669
|
+
}
|
|
670
|
+
if (path === "/v1/responses/compact") return req.method === "POST";
|
|
671
|
+
if (path === "/v1/models") return req.method === "GET";
|
|
672
|
+
// Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
|
|
673
|
+
// transport) — a directly-spawned `codex app-server` needs these for desktop
|
|
674
|
+
// voice the same way it needs /v1/responses. WebSocket upgrades only; plain
|
|
675
|
+
// HTTP on these paths stays rejected.
|
|
676
|
+
if (path === "/v1/realtime" || path === "/v1/live") {
|
|
677
|
+
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
678
|
+
}
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
|
|
354
682
|
// Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
|
|
355
683
|
// the server_is_overloaded code so clients can back off, but always return a JSON envelope.
|
|
356
|
-
|
|
684
|
+
// These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
|
|
685
|
+
// explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
|
|
686
|
+
// headers to a 503 on the loopback listener — no model runs and no credential is spent, but
|
|
687
|
+
// it is the one error path that would answer a rebinding origin with its own origin echoed
|
|
688
|
+
// back.
|
|
689
|
+
function drainingResponse(req: Request, policy: RequestPolicyView): Response {
|
|
357
690
|
const response = formatErrorResponse(503, "server_error", "Service shutting down");
|
|
358
691
|
const headers = new Headers(response.headers);
|
|
359
|
-
for (const [name, value] of Object.entries(corsHeaders(req,
|
|
692
|
+
for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
|
|
360
693
|
headers.set(name, value);
|
|
361
694
|
}
|
|
362
695
|
headers.set("Retry-After", "5");
|
|
363
696
|
return new Response(response.body, { status: 503, headers });
|
|
364
697
|
}
|
|
365
698
|
|
|
366
|
-
function serverBusyResponse(req: Request, resource: string): Response {
|
|
699
|
+
function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
|
|
367
700
|
return withCors(new Response(JSON.stringify({
|
|
368
701
|
error: { type: "server_error", code: "server_busy", message: `${resource} capacity reached` },
|
|
369
702
|
}), {
|
|
370
703
|
status: 503,
|
|
371
704
|
headers: { "Content-Type": "application/json", "Retry-After": "1" },
|
|
372
|
-
}), req,
|
|
705
|
+
}), req, policy);
|
|
373
706
|
}
|
|
374
707
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
708
|
+
function packageTreeChangedResponse(req: Request, policy: RequestPolicyView, message: string): Response {
|
|
709
|
+
return withCors(new Response(JSON.stringify({
|
|
710
|
+
error: { type: "server_error", code: "package_tree_changed", message },
|
|
711
|
+
}), {
|
|
712
|
+
status: 503,
|
|
713
|
+
headers: { "Content-Type": "application/json" },
|
|
714
|
+
}), req, policy);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function runAdmittedHttpTurn(
|
|
718
|
+
req: Request,
|
|
719
|
+
policy: RequestPolicyView,
|
|
720
|
+
work: (lease: ActiveTurnLease) => Promise<Response>,
|
|
721
|
+
): Promise<Response> {
|
|
722
|
+
const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
723
|
+
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
378
724
|
let response: Response;
|
|
379
725
|
try {
|
|
380
726
|
response = await work(lease);
|
|
@@ -388,25 +734,180 @@ export function startServer(port?: number) {
|
|
|
388
734
|
return response;
|
|
389
735
|
}
|
|
390
736
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
737
|
+
// Readiness gate: one PRIVATE controller per startServer invocation, captured
|
|
738
|
+
// by this listener's closure. Starting/failing a second server in the same
|
|
739
|
+
// process can never reset or mutate this gate. handleStart creates the gate,
|
|
740
|
+
// passes it in, and transitions it after the post-startup sync settles. When
|
|
741
|
+
// no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
|
|
742
|
+
const readinessGate = deps.readinessGate ?? createReadinessGate();
|
|
743
|
+
const packageTreeIntegrity = deps.packageTreeIntegrity
|
|
744
|
+
?? createRuntimePackageTreeIntegrityGuard(detectInstall());
|
|
745
|
+
// Actual bound port, filled in after Bun.serve binds so /readyz reports the
|
|
746
|
+
// real ephemeral port for startServer(0). /healthz keeps its existing port
|
|
747
|
+
// field (the requested listenPort) byte-for-byte.
|
|
748
|
+
let boundPort: number | null = null;
|
|
749
|
+
|
|
750
|
+
// Native-main startup ownership creates several SQLite coordination files in
|
|
751
|
+
// CODEX_HOME. When the user has disabled the Codex integration, starting the
|
|
752
|
+
// proxy must not manufacture those Codex artifacts merely to serve other
|
|
753
|
+
// clients; no Codex request can use this lifecycle in that state.
|
|
754
|
+
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
755
|
+
// between the two sites must not widen the service-install race.
|
|
756
|
+
const nativeOwnership = inspectStartupOwnership(deps, startupOwnershipHomes, startupOwnershipStatePaths);
|
|
757
|
+
const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
|
|
758
|
+
&& startupOwnershipHomes !== null
|
|
759
|
+
? prepareNativeMainStartupLifecycle(
|
|
760
|
+
deps.nativeMainStartup,
|
|
761
|
+
{ codexHome: startupOwnershipHomes.codexHome, configDir: startupOwnershipHomes.opencodexHome },
|
|
762
|
+
)
|
|
763
|
+
: null;
|
|
764
|
+
let retryOwnershipHomes = startupOwnershipHomes;
|
|
765
|
+
let retryOwnershipStatePaths = startupOwnershipStatePaths;
|
|
766
|
+
let retryPreparedNativeMainLifecycle = preparedNativeMainLifecycle;
|
|
767
|
+
const reprobeNativeOwnership = (): NativeCodexOwnership => {
|
|
768
|
+
// If startup could not resolve the homes at all, preserve a bounded retry
|
|
769
|
+
// without guessing an authority. The first successful resolution is pinned
|
|
770
|
+
// together with its service-state paths before ownership is inspected.
|
|
771
|
+
if (retryOwnershipHomes === null || retryOwnershipStatePaths === null) {
|
|
772
|
+
try {
|
|
773
|
+
const homes = resolveServiceHomes();
|
|
774
|
+
const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
|
|
775
|
+
retryOwnershipHomes = homes;
|
|
776
|
+
retryOwnershipStatePaths = statePaths;
|
|
777
|
+
} catch {
|
|
778
|
+
return "unknown";
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const homes = retryOwnershipHomes;
|
|
782
|
+
const statePaths = retryOwnershipStatePaths;
|
|
783
|
+
const answer = inspectStartupOwnership(deps, homes, statePaths).ownership;
|
|
784
|
+
if (answer !== "owned") return answer;
|
|
785
|
+
retryPreparedNativeMainLifecycle ??= prepareNativeMainStartupLifecycle(
|
|
786
|
+
deps.nativeMainStartup,
|
|
787
|
+
{ codexHome: homes.codexHome, configDir: homes.opencodexHome },
|
|
788
|
+
);
|
|
789
|
+
// An ownership verdict without a lifecycle bound to that same home is not
|
|
790
|
+
// enough to reopen native-main admission.
|
|
791
|
+
return retryPreparedNativeMainLifecycle ? "owned" : "unknown";
|
|
792
|
+
};
|
|
793
|
+
const ownershipRetryOptions = {
|
|
794
|
+
reprobe: reprobeNativeOwnership,
|
|
795
|
+
expectedHomeId: () => retryPreparedNativeMainLifecycle?.homeId ?? null,
|
|
796
|
+
startOwnedLifecycle: () => {
|
|
797
|
+
if (!retryPreparedNativeMainLifecycle) {
|
|
798
|
+
throw new Error("Native-main ownership became known before its startup lifecycle was prepared.");
|
|
799
|
+
}
|
|
800
|
+
return retryPreparedNativeMainLifecycle.start();
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
|
|
804
|
+
? nativeOwnership.ownership === "owned"
|
|
805
|
+
? preparedNativeMainLifecycle
|
|
806
|
+
? preparedNativeMainLifecycle.start()
|
|
807
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
808
|
+
"ownership-unknown",
|
|
809
|
+
ownershipRetryOptions,
|
|
810
|
+
)
|
|
811
|
+
: nativeOwnership.ownership === "foreign"
|
|
812
|
+
? blockNativeMainStartupForUnownedServiceHome("foreign-ownership")
|
|
813
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
814
|
+
"ownership-unknown",
|
|
815
|
+
// #2108: an `unknown` verdict means the probe could not answer, not that this host
|
|
816
|
+
// is unownable. Hand the fence a way to re-ask so a host that becomes answerable
|
|
817
|
+
// after boot reopens on its own instead of needing `ocx restart`. A `foreign`
|
|
818
|
+
// verdict ignores this by design — that one is a fact, not a question.
|
|
819
|
+
ownershipRetryOptions,
|
|
820
|
+
)
|
|
821
|
+
: {
|
|
822
|
+
homeId: null,
|
|
823
|
+
settled: Promise.resolve({ status: "ready", homeId: null }),
|
|
824
|
+
release: async () => {},
|
|
825
|
+
};
|
|
826
|
+
let server: Server<WsData>;
|
|
827
|
+
let loopbackServer: Server<WsData> | null = null;
|
|
828
|
+
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
829
|
+
try {
|
|
830
|
+
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
|
|
831
|
+
// External `ocx config set` / direct config.json edits run in other
|
|
832
|
+
// processes; poll the file so Logs/Usage display prices follow them live.
|
|
833
|
+
// Started inside the guarded startup transaction so the catch below can
|
|
834
|
+
// release the owner-scoped lease on any listener failure.
|
|
835
|
+
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
836
|
+
const serveOptions = {
|
|
837
|
+
idleTimeout: 255,
|
|
838
|
+
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
|
|
839
|
+
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
840
|
+
// The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
|
|
841
|
+
// else. Rejecting here, before any handler runs, is what keeps the surface from growing
|
|
842
|
+
// silently when a route is added below.
|
|
843
|
+
if (requestServer === loopbackServer && !loopbackRouteAllowed(new URL(req.url), req)) {
|
|
844
|
+
return withCors(
|
|
845
|
+
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
846
|
+
req,
|
|
847
|
+
loopbackPolicy(),
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
// Auth and CORS decisions below read `policy`, not `config`. For the public listener the
|
|
851
|
+
// two are the same object, so its behaviour is unchanged; for the loopback listener the
|
|
852
|
+
// view substitutes 127.0.0.1 as the bind address, which is what routes it through the
|
|
853
|
+
// same code path a plain loopback bind has always taken — Host-header check included.
|
|
854
|
+
// Routing, provider selection and response bodies keep using `config`.
|
|
855
|
+
const policy: RequestPolicyView = requestServer === loopbackServer ? loopbackPolicy() : config;
|
|
396
856
|
const url = new URL(req.url);
|
|
397
857
|
markActivity(`${req.method} ${url.pathname}`);
|
|
398
858
|
|
|
859
|
+
// Readiness is exact-GET on the literal /readyz path. Compare the DECODED
|
|
860
|
+
// pathname so an encoded variant like /readyz%2F (which decodes to
|
|
861
|
+
// /readyz/) cannot bypass the exact-path rejection and reach the GUI
|
|
862
|
+
// fallback (serveGuiFile decodes the pathname and would serve index.html
|
|
863
|
+
// with 200). Malformed percent-sequences fall back to the raw pathname,
|
|
864
|
+
// which still cannot match the exact literal below.
|
|
865
|
+
let readyzPath: string | undefined;
|
|
866
|
+
try {
|
|
867
|
+
const decoded = decodeURIComponent(url.pathname);
|
|
868
|
+
if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
|
|
869
|
+
} catch { /* malformed encoding — not a readiness path */ }
|
|
870
|
+
|
|
871
|
+
const packageTreeStatus = packageTreeIntegrity.status();
|
|
872
|
+
if (!packageTreeStatus.ok && (
|
|
873
|
+
url.pathname === "/healthz"
|
|
874
|
+
|| readyzPath !== undefined
|
|
875
|
+
|| url.pathname.startsWith("/v1/")
|
|
876
|
+
)) {
|
|
877
|
+
const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
|
|
878
|
+
const response = url.pathname === "/healthz" || readyzPath !== undefined
|
|
879
|
+
? jsonResponse({
|
|
880
|
+
status: "restart_required",
|
|
881
|
+
service: "opencodex",
|
|
882
|
+
version: VERSION,
|
|
883
|
+
uptime: process.uptime(),
|
|
884
|
+
pid: process.pid,
|
|
885
|
+
port: boundPort ?? requestServer.port ?? listenPort,
|
|
886
|
+
error: { code: "package_tree_changed", message },
|
|
887
|
+
}, 503, req, policy)
|
|
888
|
+
: packageTreeChangedResponse(req, policy, message);
|
|
889
|
+
const headers = new Headers(response.headers);
|
|
890
|
+
headers.set("Retry-After", "5");
|
|
891
|
+
return new Response(response.body, { status: 503, headers });
|
|
892
|
+
}
|
|
893
|
+
|
|
399
894
|
if (req.method === "OPTIONS") {
|
|
895
|
+
// /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
|
|
896
|
+
// path) must answer the deterministic JSON 404, never the generic 204
|
|
897
|
+
// preflight response that the SPA fallback would otherwise allow.
|
|
898
|
+
if (readyzPath !== undefined) {
|
|
899
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
900
|
+
}
|
|
400
901
|
const managementPreflight = url.pathname.startsWith("/api/");
|
|
401
902
|
const allowed = managementPreflight
|
|
402
903
|
? isAllowedManagementOrigin(req, config)
|
|
403
|
-
: isAllowedRequestOrigin(req,
|
|
904
|
+
: isAllowedRequestOrigin(req, policy);
|
|
404
905
|
if (!allowed) {
|
|
405
906
|
return new Response(null, { status: 403, headers: corsHeaders() });
|
|
406
907
|
}
|
|
407
908
|
return new Response(null, {
|
|
408
909
|
status: 204,
|
|
409
|
-
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req,
|
|
910
|
+
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, policy),
|
|
410
911
|
});
|
|
411
912
|
}
|
|
412
913
|
|
|
@@ -414,14 +915,14 @@ export function startServer(port?: number) {
|
|
|
414
915
|
// handshake-time only, so capture inbound headers and thread them into the pipeline.
|
|
415
916
|
if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
416
917
|
if (isDraining()) {
|
|
417
|
-
return drainingResponse(req);
|
|
918
|
+
return drainingResponse(req, policy);
|
|
418
919
|
}
|
|
419
|
-
const admission = resolveResponsesApiAuth(req,
|
|
920
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
420
921
|
if (!admission) {
|
|
421
|
-
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req,
|
|
922
|
+
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
422
923
|
}
|
|
423
|
-
if (!isAllowedRequestOrigin(req,
|
|
424
|
-
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req,
|
|
924
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
925
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
|
|
425
926
|
}
|
|
426
927
|
// WS transport gate: Codex's built-in `openai` provider hardcodes supports_websockets=true,
|
|
427
928
|
// so under Design B it always tries the WS transport first. When the feature is off, reject
|
|
@@ -429,26 +930,96 @@ export function startServer(port?: number) {
|
|
|
429
930
|
// session-scoped HTTP fallback (client.rs WebsocketStreamOutcome::FallbackToHttp) instead of
|
|
430
931
|
// surfacing broken-pipe errors from sockets a "disabled" feature would otherwise accept.
|
|
431
932
|
if (!websocketsEnabled(config)) {
|
|
432
|
-
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req,
|
|
933
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, policy);
|
|
433
934
|
}
|
|
434
935
|
const websocketLease = tryReserveCodexWebSocket();
|
|
435
|
-
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets");
|
|
436
|
-
|
|
437
|
-
|
|
936
|
+
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets", policy);
|
|
937
|
+
// Upgrade on the server that RECEIVED this request, not the captured `server`
|
|
938
|
+
// binding. They are the same object for the public listener, but the
|
|
939
|
+
// unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
|
|
940
|
+
// request to the public server's upgrade would fail or cross sockets.
|
|
941
|
+
if (requestServer.upgrade(req, {
|
|
942
|
+
data: buildResponsesWsData(
|
|
943
|
+
selectForwardHeaders(req.headers),
|
|
944
|
+
admission,
|
|
945
|
+
websocketLease,
|
|
946
|
+
sessionLaneIdFromRequest(req.headers),
|
|
947
|
+
),
|
|
438
948
|
})) return undefined as unknown as Response;
|
|
439
949
|
websocketLease.release();
|
|
440
|
-
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req,
|
|
950
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
441
951
|
}
|
|
442
952
|
|
|
443
953
|
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
444
954
|
// service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
|
|
445
|
-
|
|
955
|
+
const healthPort = server.port ?? listenPort;
|
|
956
|
+
const response = jsonResponse({
|
|
957
|
+
status: "ok",
|
|
958
|
+
service: "opencodex",
|
|
959
|
+
version: VERSION,
|
|
960
|
+
uptime: process.uptime(),
|
|
961
|
+
pid: process.pid,
|
|
962
|
+
port: healthPort,
|
|
963
|
+
restartCapability: SYSTEM_RESTART_CAPABILITY_VERSION,
|
|
964
|
+
providerReloadCapability: LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
|
|
965
|
+
}, 200, req, policy);
|
|
966
|
+
const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
|
|
967
|
+
if (challenge) {
|
|
968
|
+
const proof = createLocalAttestationProof(localAttestationSecret, challenge, process.pid, healthPort);
|
|
969
|
+
if (proof) response.headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
|
|
970
|
+
}
|
|
971
|
+
return response;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Readiness: like /healthz this is exact GET and unauthenticated (so a client can
|
|
975
|
+
// back off BEFORE knowing the admission token), but stricter than liveness. The
|
|
976
|
+
// body carries only sanitized identity + the fixed status enum; the sync message,
|
|
977
|
+
// warning text, catalog path, provider output, and account data are never exposed.
|
|
978
|
+
// POST or "/readyz/" must NOT match (exact pathname + GET method): answer them
|
|
979
|
+
// with a JSON 404 here so they can never be silently accepted by the GUI SPA
|
|
980
|
+
// fallback (which would serve index.html with HTTP 200 once gui/dist exists).
|
|
981
|
+
if (readyzPath !== undefined) {
|
|
982
|
+
if (readyzPath !== "/readyz" || req.method !== "GET") {
|
|
983
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
984
|
+
}
|
|
985
|
+
// A draining proxy must never advertise ready: every data-plane branch
|
|
986
|
+
// answers drainingResponse while isDraining() is set, but the one-shot
|
|
987
|
+
// readiness gate is not mutated on shutdown (it is owned by the startup
|
|
988
|
+
// sync). Report pending so `ocx ready --wait` and external supervisors
|
|
989
|
+
// keep polling instead of promoting a proxy that is draining.
|
|
990
|
+
const status = isDraining() ? "pending" : readinessGate.getStatus();
|
|
991
|
+
const body = {
|
|
992
|
+
service: "opencodex",
|
|
993
|
+
version: VERSION,
|
|
994
|
+
uptime: process.uptime(),
|
|
995
|
+
pid: process.pid,
|
|
996
|
+
port: boundPort ?? listenPort,
|
|
997
|
+
status,
|
|
998
|
+
};
|
|
999
|
+
if (status === "ready") {
|
|
1000
|
+
return jsonResponse(body, 200, req, policy);
|
|
1001
|
+
}
|
|
1002
|
+
// Pending/failed: 503 with a conservative Retry-After so well-behaved clients
|
|
1003
|
+
// (and `ocx ready --wait`) back off instead of hot-looping.
|
|
1004
|
+
const resp = jsonResponse(body, 503, req, policy);
|
|
1005
|
+
const headers = new Headers(resp.headers);
|
|
1006
|
+
headers.set("Retry-After", "1");
|
|
1007
|
+
return new Response(resp.body, { status: 503, headers });
|
|
446
1008
|
}
|
|
447
1009
|
|
|
448
1010
|
if (url.pathname.startsWith("/api/")) {
|
|
449
|
-
const
|
|
1011
|
+
const localManagementAuth = {
|
|
1012
|
+
attestationSecret: localAttestationSecret,
|
|
1013
|
+
pid: process.pid,
|
|
1014
|
+
port: boundPort ?? requestServer.port ?? listenPort,
|
|
1015
|
+
};
|
|
1016
|
+
const apiAuthError = requireManagementAuth(req, managementAuth, config, localManagementAuth);
|
|
450
1017
|
if (apiAuthError) return withManagementCors(apiAuthError, req, config);
|
|
451
|
-
|
|
1018
|
+
// Which credential passed the gate, resolved from the same session table the
|
|
1019
|
+
// gate used. Consent-bearing routes need this: request headers are forgeable
|
|
1020
|
+
// by anything holding the admin token, the credential is not.
|
|
1021
|
+
const principal = managementPrincipal(req, managementAuth, config, localManagementAuth) ?? undefined;
|
|
1022
|
+
const mgmtResponse = await handleManagementAPI(req, url, config, deps.managementApi, principal);
|
|
452
1023
|
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
453
1024
|
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
454
1025
|
}
|
|
@@ -457,25 +1028,80 @@ export function startServer(port?: number) {
|
|
|
457
1028
|
// Model discovery never forwards Authorization upstream, so the broader admission
|
|
458
1029
|
// set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
|
|
459
1030
|
// 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,
|
|
1031
|
+
const admission = resolveApiAuth(req, policy);
|
|
1032
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1033
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1034
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
464
1035
|
}
|
|
465
1036
|
let goModels;
|
|
1037
|
+
let modelEntitlements;
|
|
466
1038
|
try {
|
|
467
|
-
goModels = await
|
|
1039
|
+
[goModels, modelEntitlements] = await Promise.all([
|
|
1040
|
+
fetchAllModels(config),
|
|
1041
|
+
resolveCodexModelEntitlements(config),
|
|
1042
|
+
]);
|
|
468
1043
|
} catch (error) {
|
|
469
1044
|
if (error instanceof CatalogGatherBusyError) {
|
|
470
1045
|
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
471
1046
|
status: 503,
|
|
472
1047
|
headers: { "content-type": "application/json", "Retry-After": "1" },
|
|
473
|
-
}), req,
|
|
1048
|
+
}), req, policy);
|
|
474
1049
|
}
|
|
475
1050
|
throw error;
|
|
476
1051
|
}
|
|
477
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
478
|
-
const
|
|
1052
|
+
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");
|
|
1053
|
+
const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
|
|
1054
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
1055
|
+
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
1056
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
1057
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
1058
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
1059
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
1060
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
1061
|
+
modelEntitlements,
|
|
1062
|
+
bareEligibleAccountIds,
|
|
1063
|
+
);
|
|
1064
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
1065
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1066
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
1067
|
+
));
|
|
1068
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1069
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
1070
|
+
));
|
|
1071
|
+
const nativeSlugs = includeNativeOpenAi
|
|
1072
|
+
? nativeOpenAiSlugs().filter(slug => (
|
|
1073
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
1074
|
+
))
|
|
1075
|
+
: [];
|
|
1076
|
+
const disabledNatives = disabledNativeSlugs(config);
|
|
1077
|
+
const disabledModels = new Set(config.disabledModels ?? []);
|
|
1078
|
+
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1079
|
+
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
1080
|
+
const suppressedBareNativeSlugs = new Set([
|
|
1081
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
1082
|
+
...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
|
|
1083
|
+
]);
|
|
1084
|
+
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
1085
|
+
? visibleCodexAccountSelectors(config)
|
|
1086
|
+
: [];
|
|
1087
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
1088
|
+
const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
|
|
1089
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
|
|
1090
|
+
const target = accountTargets.get(selector);
|
|
1091
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
1092
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
1093
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
1094
|
+
return [selector, slugs.filter(slug => (
|
|
1095
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
1096
|
+
))] as const;
|
|
1097
|
+
}))
|
|
1098
|
+
: new Map<string, readonly string[]>();
|
|
1099
|
+
const accountNativeSlugs = [...new Set(
|
|
1100
|
+
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
1101
|
+
)];
|
|
1102
|
+
const desktopNativeSlugs = desktopVisibleNativeSlugs(config).filter(slug => (
|
|
1103
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
1104
|
+
));
|
|
479
1105
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
480
1106
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
481
1107
|
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
@@ -489,10 +1115,10 @@ export function startServer(port?: number) {
|
|
|
489
1115
|
const wantsAnthropicList = req.headers.get("anthropic-version") !== null
|
|
490
1116
|
|| url.searchParams.get("flavor") === "anthropic";
|
|
491
1117
|
if (wantsAnthropicList && !url.searchParams.has("client_version")) {
|
|
492
|
-
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req,
|
|
1118
|
+
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
493
1119
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
494
1120
|
buildDesktop3pRegistry(
|
|
495
|
-
|
|
1121
|
+
desktopNativeSlugs,
|
|
496
1122
|
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
497
1123
|
config.claudeCode?.desktopProfile,
|
|
498
1124
|
);
|
|
@@ -509,8 +1135,8 @@ export function startServer(port?: number) {
|
|
|
509
1135
|
: idsParam === "desktop"
|
|
510
1136
|
? "desktop3p" as const
|
|
511
1137
|
: (/^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,
|
|
1138
|
+
const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config));
|
|
1139
|
+
return jsonResponse({ data }, 200, req, policy);
|
|
514
1140
|
}
|
|
515
1141
|
if (url.searchParams.has("client_version")) {
|
|
516
1142
|
// Codex client → Codex catalog shape: native gpt + namespaced routed models,
|
|
@@ -519,8 +1145,39 @@ export function startServer(port?: number) {
|
|
|
519
1145
|
// Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
|
|
520
1146
|
// on-disk sync; codex-rs keeps them out of the picker itself).
|
|
521
1147
|
const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
522
|
-
|
|
523
|
-
|
|
1148
|
+
// Account rows use the same hidden-inclusive supported set as on-disk sync. This lets a
|
|
1149
|
+
// newly re-enabled native reappear under each selector before the next sync, while the
|
|
1150
|
+
// no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
|
|
1151
|
+
const catalogNativeSlugs = accountSelectors.length > 0
|
|
1152
|
+
? [...new Set([
|
|
1153
|
+
...availableAccountNativeSlugs,
|
|
1154
|
+
...accountNativeSlugs,
|
|
1155
|
+
])]
|
|
1156
|
+
: nativeSlugs;
|
|
1157
|
+
const entries = buildCatalogEntries(
|
|
1158
|
+
loadCatalogTemplate(),
|
|
1159
|
+
catalogNativeSlugs,
|
|
1160
|
+
goOrdered,
|
|
1161
|
+
config.subagentModels,
|
|
1162
|
+
websocketsEnabled(config),
|
|
1163
|
+
maMode as "v1" | "default" | "v2",
|
|
1164
|
+
exactComboSlugs,
|
|
1165
|
+
accountSelectors,
|
|
1166
|
+
suppressedBareNativeSlugs,
|
|
1167
|
+
new Set(),
|
|
1168
|
+
nativeContextLimits(config),
|
|
1169
|
+
accountNativeSlugs,
|
|
1170
|
+
accountNativeSlugsBySelector,
|
|
1171
|
+
config.keepNativeChatGptOnV1 === true,
|
|
1172
|
+
);
|
|
1173
|
+
return jsonResponse({
|
|
1174
|
+
models: applyNativeVisibility(
|
|
1175
|
+
entries,
|
|
1176
|
+
disabledModels,
|
|
1177
|
+
accountSelectors.length > 0,
|
|
1178
|
+
new Set(accountNativeSlugs),
|
|
1179
|
+
),
|
|
1180
|
+
}, 200, req, policy);
|
|
524
1181
|
}
|
|
525
1182
|
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
526
1183
|
// (pure availability list — disabled natives are omitted entirely).
|
|
@@ -548,23 +1205,65 @@ export function startServer(port?: number) {
|
|
|
548
1205
|
reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
|
|
549
1206
|
};
|
|
550
1207
|
};
|
|
551
|
-
const
|
|
552
|
-
...visibleNativeSlugs(config).map(id => ({
|
|
1208
|
+
const nativeModelRow = (id: string, metadataId = id) => ({
|
|
553
1209
|
id,
|
|
554
1210
|
object: "model",
|
|
555
1211
|
created: 0,
|
|
556
1212
|
owned_by: "openai",
|
|
557
|
-
...grokEffortFields(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1213
|
+
...grokEffortFields(
|
|
1214
|
+
nativeReasoningEfforts(metadataId),
|
|
1215
|
+
nativeDefaultReasoningEffort(metadataId),
|
|
1216
|
+
),
|
|
1217
|
+
});
|
|
1218
|
+
// Selector-active discovery follows the same complete supported set as the Codex catalog
|
|
1219
|
+
// for both bare and qualified rows. Without selectors, the live catalog continues to own
|
|
1220
|
+
// bare availability.
|
|
1221
|
+
const selectorNativeSlugs = accountSelectors.length > 0
|
|
1222
|
+
? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
|
|
1223
|
+
: [];
|
|
1224
|
+
const bareSelectorNativeSlugs = accountSelectors.length > 0
|
|
1225
|
+
? selectorNativeSlugs
|
|
1226
|
+
: [];
|
|
1227
|
+
const visibleNatives = includeNativeOpenAi
|
|
1228
|
+
? accountSelectors.length > 0
|
|
1229
|
+
? bareSelectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
|
|
1230
|
+
: visibleNativeSlugs(config)
|
|
1231
|
+
: [];
|
|
1232
|
+
const visibleAccountNatives = accountSelectors.flatMap(selector =>
|
|
1233
|
+
(accountNativeSlugsBySelector.get(selector) ?? []).filter(metadataId => !disabledNatives.has(metadataId)).flatMap(metadataId => {
|
|
1234
|
+
const id = `${selector}/${metadataId}`;
|
|
1235
|
+
return disabledModels.has(id) ? [] : [{ id, metadataId }];
|
|
1236
|
+
})
|
|
1237
|
+
);
|
|
1238
|
+
const data = [
|
|
1239
|
+
...visibleNatives.map(id => nativeModelRow(id)),
|
|
1240
|
+
...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
|
|
1241
|
+
...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
|
|
1242
|
+
const publicId = m.alias ?? `${m.provider}/${m.id}`;
|
|
1243
|
+
const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
|
|
1244
|
+
const provider = config.providers[m.provider];
|
|
1245
|
+
const effective = provider
|
|
1246
|
+
? (await import("../providers/default-aliases")).effectiveModelAliases(
|
|
1247
|
+
config,
|
|
1248
|
+
provider,
|
|
1249
|
+
knownModelIdsForProvider(m.provider, provider, config),
|
|
1250
|
+
).get(m.id)
|
|
1251
|
+
: undefined;
|
|
1252
|
+
return {
|
|
1253
|
+
id: publicId,
|
|
1254
|
+
object: "model",
|
|
1255
|
+
created: 0,
|
|
1256
|
+
// This endpoint is an OpenAI-compatible inbound contract. Some clients use
|
|
1257
|
+
// owned_by as an adapter selector, so a virtual combo must name that wire
|
|
1258
|
+
// adapter rather than the internal catalog authority marker.
|
|
1259
|
+
owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
|
|
1260
|
+
...(isCombo ? { is_combo: true } : {}),
|
|
1261
|
+
...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
|
|
1262
|
+
...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
|
|
1263
|
+
};
|
|
565
1264
|
})),
|
|
566
1265
|
];
|
|
567
|
-
return jsonResponse({ object: "list", data }, 200, req,
|
|
1266
|
+
return jsonResponse({ object: "list", data }, 200, req, policy);
|
|
568
1267
|
}
|
|
569
1268
|
|
|
570
1269
|
// Remote compaction v1 (codex-rs with Feature::RemoteCompactionV2 off — the default).
|
|
@@ -572,12 +1271,12 @@ export function startServer(port?: number) {
|
|
|
572
1271
|
// before the /v1/* 404 guard below.
|
|
573
1272
|
if (url.pathname === "/v1/responses/compact" && req.method === "POST") {
|
|
574
1273
|
if (isDraining()) {
|
|
575
|
-
return drainingResponse(req);
|
|
1274
|
+
return drainingResponse(req, policy);
|
|
576
1275
|
}
|
|
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,
|
|
1276
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1277
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1278
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1279
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
581
1280
|
}
|
|
582
1281
|
const start = Date.now();
|
|
583
1282
|
const requestId = nextRequestLogId(start);
|
|
@@ -587,16 +1286,16 @@ export function startServer(port?: number) {
|
|
|
587
1286
|
...admissionFields(admission),
|
|
588
1287
|
inboundProtocol: "responses",
|
|
589
1288
|
};
|
|
590
|
-
return runAdmittedHttpTurn(req, async
|
|
1289
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
591
1290
|
let response: Response;
|
|
592
1291
|
try {
|
|
593
|
-
response = await handleResponsesCompact(req, config, logCtx);
|
|
1292
|
+
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
|
|
594
1293
|
} catch {
|
|
595
1294
|
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
596
1295
|
}
|
|
597
1296
|
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
598
1297
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
599
|
-
return withCors(response, req,
|
|
1298
|
+
return withCors(response, req, policy);
|
|
600
1299
|
});
|
|
601
1300
|
}
|
|
602
1301
|
|
|
@@ -606,12 +1305,12 @@ export function startServer(port?: number) {
|
|
|
606
1305
|
) {
|
|
607
1306
|
disableResponsesRequestTimeout(req, requestServer);
|
|
608
1307
|
if (isDraining()) {
|
|
609
|
-
return drainingResponse(req);
|
|
1308
|
+
return drainingResponse(req, policy);
|
|
610
1309
|
}
|
|
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,
|
|
1310
|
+
const admission = resolveApiAuth(req, policy);
|
|
1311
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1312
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1313
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
615
1314
|
}
|
|
616
1315
|
const start = Date.now();
|
|
617
1316
|
const requestId = nextRequestLogId(start);
|
|
@@ -621,24 +1320,24 @@ export function startServer(port?: number) {
|
|
|
621
1320
|
...admissionFields(admission),
|
|
622
1321
|
};
|
|
623
1322
|
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);
|
|
1323
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1324
|
+
const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
|
|
626
1325
|
addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
627
|
-
return withCors(response, req,
|
|
1326
|
+
return withCors(response, req, policy);
|
|
628
1327
|
});
|
|
629
1328
|
}
|
|
630
1329
|
|
|
631
1330
|
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,
|
|
1331
|
+
const admission = resolveApiAuth(req, policy);
|
|
1332
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1333
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1334
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
636
1335
|
}
|
|
637
1336
|
const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
|
|
638
1337
|
const { resolveArtifactPath } = await import("../images/artifacts");
|
|
639
1338
|
const artifactPath = resolveArtifactPath(id);
|
|
640
1339
|
if (!artifactPath) {
|
|
641
|
-
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req,
|
|
1340
|
+
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, policy);
|
|
642
1341
|
}
|
|
643
1342
|
const file = Bun.file(artifactPath);
|
|
644
1343
|
const ext = artifactPath.split(".").pop()?.toLowerCase();
|
|
@@ -655,18 +1354,18 @@ export function startServer(port?: number) {
|
|
|
655
1354
|
"cache-control": "private, max-age=3600",
|
|
656
1355
|
"x-content-type-options": "nosniff",
|
|
657
1356
|
},
|
|
658
|
-
}), req,
|
|
1357
|
+
}), req, policy);
|
|
659
1358
|
}
|
|
660
1359
|
|
|
661
1360
|
if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
|
|
662
1361
|
disableResponsesRequestTimeout(req, requestServer);
|
|
663
1362
|
if (isDraining()) {
|
|
664
|
-
return drainingResponse(req);
|
|
1363
|
+
return drainingResponse(req, policy);
|
|
665
1364
|
}
|
|
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,
|
|
1365
|
+
const admission = resolveApiAuth(req, policy);
|
|
1366
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1367
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1368
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
670
1369
|
}
|
|
671
1370
|
const start = Date.now();
|
|
672
1371
|
const requestId = nextRequestLogId(start);
|
|
@@ -675,23 +1374,22 @@ export function startServer(port?: number) {
|
|
|
675
1374
|
provider: "unknown",
|
|
676
1375
|
...admissionFields(admission),
|
|
677
1376
|
};
|
|
678
|
-
return runAdmittedHttpTurn(req, async
|
|
679
|
-
const response = await handleSearch(req, config, logCtx);
|
|
1377
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1378
|
+
const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
|
|
680
1379
|
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
681
1380
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
682
|
-
return withCors(response, req,
|
|
1381
|
+
return withCors(response, req, policy);
|
|
683
1382
|
});
|
|
684
1383
|
}
|
|
685
1384
|
|
|
686
1385
|
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
687
|
-
disableResponsesRequestTimeout(req, requestServer);
|
|
688
1386
|
if (isDraining()) {
|
|
689
|
-
return drainingResponse(req);
|
|
1387
|
+
return drainingResponse(req, policy);
|
|
690
1388
|
}
|
|
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,
|
|
1389
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1390
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1391
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1392
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
695
1393
|
}
|
|
696
1394
|
const start = Date.now();
|
|
697
1395
|
const requestId = nextRequestLogId(start);
|
|
@@ -701,6 +1399,7 @@ export function startServer(port?: number) {
|
|
|
701
1399
|
...admissionFields(admission),
|
|
702
1400
|
inboundProtocol: "responses",
|
|
703
1401
|
};
|
|
1402
|
+
if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
|
|
704
1403
|
let logged = false;
|
|
705
1404
|
const finalizeNativePassthroughLog = (
|
|
706
1405
|
status: number,
|
|
@@ -710,13 +1409,15 @@ export function startServer(port?: number) {
|
|
|
710
1409
|
logged = true;
|
|
711
1410
|
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
712
1411
|
};
|
|
713
|
-
return runAdmittedHttpTurn(req, async turnAdmissionLease => {
|
|
1412
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
714
1413
|
const response = await handleResponses(req, config, logCtx, {
|
|
715
1414
|
turnAdmissionLease,
|
|
1415
|
+
admission,
|
|
1416
|
+
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
716
1417
|
abortSignal: req.signal,
|
|
717
1418
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
718
1419
|
onNativePassthroughTerminal: status => {
|
|
719
|
-
finalizeNativePassthroughLog(
|
|
1420
|
+
finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
720
1421
|
terminalStatus: status,
|
|
721
1422
|
closeReason: "terminal",
|
|
722
1423
|
});
|
|
@@ -725,7 +1426,7 @@ export function startServer(port?: number) {
|
|
|
725
1426
|
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
726
1427
|
},
|
|
727
1428
|
});
|
|
728
|
-
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req,
|
|
1429
|
+
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy);
|
|
729
1430
|
});
|
|
730
1431
|
}
|
|
731
1432
|
|
|
@@ -733,29 +1434,33 @@ export function startServer(port?: number) {
|
|
|
733
1434
|
// Claude Code posts `/v1/messages?beta=true` — pathname match ignores the query (003 G9).
|
|
734
1435
|
if (url.pathname === "/v1/messages/count_tokens" && req.method === "POST") {
|
|
735
1436
|
if (isDraining()) {
|
|
736
|
-
return drainingResponse(req);
|
|
1437
|
+
return drainingResponse(req, policy);
|
|
737
1438
|
}
|
|
738
|
-
const admission = resolveApiAuth(req,
|
|
1439
|
+
const admission = resolveApiAuth(req, policy);
|
|
739
1440
|
if (!admission) {
|
|
740
|
-
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req,
|
|
1441
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
|
|
741
1442
|
}
|
|
742
|
-
if (!isAllowedRequestOrigin(req,
|
|
743
|
-
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req,
|
|
1443
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1444
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
744
1445
|
}
|
|
745
|
-
return runAdmittedHttpTurn(req, async () => withCors(
|
|
1446
|
+
return runAdmittedHttpTurn(req, policy, async () => withCors(
|
|
1447
|
+
await handleClaudeCountTokens(req, config, policy),
|
|
1448
|
+
req,
|
|
1449
|
+
policy,
|
|
1450
|
+
));
|
|
746
1451
|
}
|
|
747
1452
|
|
|
748
1453
|
if (url.pathname === "/v1/messages" && req.method === "POST") {
|
|
749
1454
|
disableResponsesRequestTimeout(req, requestServer);
|
|
750
1455
|
if (isDraining()) {
|
|
751
|
-
return drainingResponse(req);
|
|
1456
|
+
return drainingResponse(req, policy);
|
|
752
1457
|
}
|
|
753
|
-
const admission = resolveApiAuth(req,
|
|
1458
|
+
const admission = resolveApiAuth(req, policy);
|
|
754
1459
|
if (!admission) {
|
|
755
|
-
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req,
|
|
1460
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
|
|
756
1461
|
}
|
|
757
|
-
if (!isAllowedRequestOrigin(req,
|
|
758
|
-
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req,
|
|
1462
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1463
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
759
1464
|
}
|
|
760
1465
|
const start = Date.now();
|
|
761
1466
|
const requestId = nextRequestLogId(start);
|
|
@@ -768,10 +1473,10 @@ export function startServer(port?: number) {
|
|
|
768
1473
|
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
|
|
769
1474
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
770
1475
|
// translated Anthropic stream here.
|
|
771
|
-
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
772
|
-
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1476
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1477
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
|
|
773
1478
|
req,
|
|
774
|
-
|
|
1479
|
+
policy,
|
|
775
1480
|
));
|
|
776
1481
|
}
|
|
777
1482
|
|
|
@@ -780,12 +1485,12 @@ export function startServer(port?: number) {
|
|
|
780
1485
|
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
|
781
1486
|
disableResponsesRequestTimeout(req, requestServer);
|
|
782
1487
|
if (isDraining()) {
|
|
783
|
-
return drainingResponse(req);
|
|
1488
|
+
return drainingResponse(req, policy);
|
|
784
1489
|
}
|
|
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,
|
|
1490
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1491
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1492
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1493
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
789
1494
|
}
|
|
790
1495
|
const start = Date.now();
|
|
791
1496
|
const requestId = nextRequestLogId(start);
|
|
@@ -795,8 +1500,8 @@ export function startServer(port?: number) {
|
|
|
795
1500
|
...admissionFields(admission),
|
|
796
1501
|
inboundProtocol: "chat",
|
|
797
1502
|
};
|
|
798
|
-
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
799
|
-
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1503
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1504
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
|
|
800
1505
|
req,
|
|
801
1506
|
config,
|
|
802
1507
|
));
|
|
@@ -811,12 +1516,12 @@ export function startServer(port?: number) {
|
|
|
811
1516
|
) {
|
|
812
1517
|
disableResponsesRequestTimeout(req, requestServer);
|
|
813
1518
|
if (isDraining()) {
|
|
814
|
-
return drainingResponse(req);
|
|
1519
|
+
return drainingResponse(req, policy);
|
|
815
1520
|
}
|
|
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,
|
|
1521
|
+
const admission = resolveApiAuth(req, policy);
|
|
1522
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1523
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1524
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
820
1525
|
}
|
|
821
1526
|
const start = Date.now();
|
|
822
1527
|
const requestId = nextRequestLogId(start);
|
|
@@ -825,8 +1530,8 @@ export function startServer(port?: number) {
|
|
|
825
1530
|
provider: "unknown",
|
|
826
1531
|
...admissionFields(admission),
|
|
827
1532
|
};
|
|
828
|
-
return runAdmittedHttpTurn(req, async
|
|
829
|
-
const response = await handleLive(req, config, logCtx);
|
|
1533
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1534
|
+
const response = await handleLive(req, config, logCtx, turnAdmissionLease);
|
|
830
1535
|
addFinalRequestLog(
|
|
831
1536
|
requestId,
|
|
832
1537
|
start,
|
|
@@ -834,23 +1539,26 @@ export function startServer(port?: number) {
|
|
|
834
1539
|
response.status,
|
|
835
1540
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined,
|
|
836
1541
|
);
|
|
837
|
-
return withCors(response, req,
|
|
1542
|
+
return withCors(response, req, policy);
|
|
838
1543
|
});
|
|
839
1544
|
}
|
|
840
1545
|
|
|
841
|
-
// Voice / Realtime
|
|
842
|
-
// /v1/realtime?call_id= (or /v1/realtime/calls/{callId}).
|
|
1546
|
+
// Voice / Realtime WebSocket relay. Sideband joins: Frameless /v1/live/{callId};
|
|
1547
|
+
// Realtime v1 /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Standalone
|
|
1548
|
+
// sessions (codex-rs thread/realtime/start, WebSocket transport — the desktop voice
|
|
1549
|
+
// path): /v1/realtime?intent=quicksilver&model= and /v1/live?model=.
|
|
1550
|
+
// Transparent bidirectional relay.
|
|
843
1551
|
const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
|
|
844
|
-
? parseLiveSidebandTarget(url.pathname, url.searchParams)
|
|
1552
|
+
? parseLiveSidebandTarget(url.pathname, url.searchParams, url.search.replace(/^\?/, ""))
|
|
845
1553
|
: null;
|
|
846
1554
|
if (liveSidebandTarget) {
|
|
847
1555
|
if (isDraining()) {
|
|
848
|
-
return drainingResponse(req);
|
|
1556
|
+
return drainingResponse(req, policy);
|
|
849
1557
|
}
|
|
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,
|
|
1558
|
+
const admission = resolveApiAuth(req, policy);
|
|
1559
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1560
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1561
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
|
|
854
1562
|
}
|
|
855
1563
|
const start = Date.now();
|
|
856
1564
|
const requestId = nextRequestLogId(start);
|
|
@@ -859,22 +1567,34 @@ export function startServer(port?: number) {
|
|
|
859
1567
|
provider: "unknown",
|
|
860
1568
|
...admissionFields(admission),
|
|
861
1569
|
};
|
|
862
|
-
const
|
|
1570
|
+
const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
1571
|
+
if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
|
|
1572
|
+
let resolved;
|
|
1573
|
+
try {
|
|
1574
|
+
resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
turnAdmissionLease.release();
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
863
1579
|
if (resolved instanceof Response) {
|
|
1580
|
+
turnAdmissionLease.release();
|
|
864
1581
|
addFinalRequestLog(requestId, start, logCtx, resolved.status);
|
|
865
|
-
return withCors(resolved, req,
|
|
1582
|
+
return withCors(resolved, req, policy);
|
|
866
1583
|
}
|
|
867
1584
|
addFinalRequestLog(requestId, start, logCtx, 101);
|
|
868
|
-
if (
|
|
1585
|
+
if (requestServer.upgrade(req, {
|
|
869
1586
|
data: {
|
|
870
1587
|
kind: "live-sideband",
|
|
871
1588
|
liveUpstreamUrl: resolved.upstreamWsUrl,
|
|
872
1589
|
liveUpstreamHeaders: resolved.headers,
|
|
873
1590
|
livePending: [],
|
|
1591
|
+
livePendingBytes: 0,
|
|
874
1592
|
liveOpened: false,
|
|
1593
|
+
liveTurnAdmissionLease: turnAdmissionLease,
|
|
875
1594
|
} satisfies WsData,
|
|
876
1595
|
})) return undefined as unknown as Response;
|
|
877
|
-
|
|
1596
|
+
turnAdmissionLease.release();
|
|
1597
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
878
1598
|
}
|
|
879
1599
|
|
|
880
1600
|
// Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
|
|
@@ -882,12 +1602,17 @@ export function startServer(port?: number) {
|
|
|
882
1602
|
// endpoint clients — memories/*, realtime/* — would surface confusing
|
|
883
1603
|
// serde decode errors instead of a clean not-found).
|
|
884
1604
|
if (url.pathname.startsWith("/v1/")) {
|
|
885
|
-
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req,
|
|
1605
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
886
1606
|
}
|
|
887
1607
|
|
|
888
1608
|
const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
|
|
889
1609
|
? issueGuiSession(req, config, managementAuth)
|
|
890
1610
|
: null;
|
|
1611
|
+
// Dedicated bootstrap path: answer without requiring a packaged GUI build, so the
|
|
1612
|
+
// Vite dev server can mint an origin-bound loopback session on a fresh checkout.
|
|
1613
|
+
if (url.pathname === "/opencodex-session" && guiSessionCandidate) {
|
|
1614
|
+
return serveSessionBootstrap(guiSessionCandidate);
|
|
1615
|
+
}
|
|
891
1616
|
const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
|
|
892
1617
|
if (guiFile) return guiFile;
|
|
893
1618
|
if (url.pathname === "/" && req.method === "GET") {
|
|
@@ -897,6 +1622,7 @@ export function startServer(port?: number) {
|
|
|
897
1622
|
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
898
1623
|
},
|
|
899
1624
|
websocket: {
|
|
1625
|
+
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
900
1626
|
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
901
1627
|
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
902
1628
|
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
@@ -904,7 +1630,11 @@ export function startServer(port?: number) {
|
|
|
904
1630
|
// Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
|
|
905
1631
|
open(ws: ServerWebSocket<WsData>) {
|
|
906
1632
|
if (ws.data.kind === "live-sideband") {
|
|
907
|
-
|
|
1633
|
+
if (!ws.data.liveTurnAdmissionLease) {
|
|
1634
|
+
closeLiveSideband(ws, 1013, "server busy");
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
|
|
908
1638
|
return;
|
|
909
1639
|
}
|
|
910
1640
|
if (!ws.data.admissionLease) {
|
|
@@ -916,15 +1646,24 @@ export function startServer(port?: number) {
|
|
|
916
1646
|
},
|
|
917
1647
|
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
918
1648
|
if (ws.data.kind === "live-sideband") {
|
|
1649
|
+
if (ws.data.liveClosing) return;
|
|
1650
|
+
const rawBytes = webSocketFrameBytes(raw);
|
|
1651
|
+
if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
|
|
1652
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
919
1655
|
logLiveSidebandFrame("c2u", raw);
|
|
920
1656
|
const upstream = ws.data.liveUpstream;
|
|
921
1657
|
if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
|
|
922
|
-
const
|
|
923
|
-
if (
|
|
1658
|
+
const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
|
|
1659
|
+
if (enqueueResult === "too-many-frames") {
|
|
924
1660
|
closeLiveSideband(ws, 1009, "too many pending frames");
|
|
925
1661
|
return;
|
|
926
1662
|
}
|
|
927
|
-
|
|
1663
|
+
if (enqueueResult === "too-many-bytes") {
|
|
1664
|
+
closeLiveSideband(ws, 1009, "too many pending bytes");
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
928
1667
|
return;
|
|
929
1668
|
}
|
|
930
1669
|
if (upstream.readyState !== WebSocket.OPEN) {
|
|
@@ -932,7 +1671,7 @@ export function startServer(port?: number) {
|
|
|
932
1671
|
return;
|
|
933
1672
|
}
|
|
934
1673
|
try {
|
|
935
|
-
upstream
|
|
1674
|
+
sendUpstreamFrame(upstream, raw);
|
|
936
1675
|
} catch {
|
|
937
1676
|
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
938
1677
|
}
|
|
@@ -980,7 +1719,7 @@ export function startServer(port?: number) {
|
|
|
980
1719
|
return;
|
|
981
1720
|
}
|
|
982
1721
|
|
|
983
|
-
const turnAdmissionLease = tryAdmitTurn();
|
|
1722
|
+
const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
|
|
984
1723
|
if (!turnAdmissionLease) {
|
|
985
1724
|
sendJsonFrame(ws, buildWsErrorFrame(503, {
|
|
986
1725
|
type: "server_error",
|
|
@@ -1029,7 +1768,9 @@ export function startServer(port?: number) {
|
|
|
1029
1768
|
try {
|
|
1030
1769
|
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
1031
1770
|
const response = await handleResponses(req, config, logCtx, {
|
|
1771
|
+
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
1032
1772
|
forceEmptyResponseId: true,
|
|
1773
|
+
inboundTransport: "websocket",
|
|
1033
1774
|
abortSignal: turnAbort.signal,
|
|
1034
1775
|
turnAdmissionLease,
|
|
1035
1776
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
@@ -1057,9 +1798,12 @@ export function startServer(port?: number) {
|
|
|
1057
1798
|
finalizeLog(429);
|
|
1058
1799
|
// Codex Desktop rides this WS transport, so it must carry the same
|
|
1059
1800
|
// actionable text as HTTP; a frame has no headers, hence message-only.
|
|
1801
|
+
const accountSelector = typeof payload.model === "string"
|
|
1802
|
+
? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
|
|
1803
|
+
: undefined;
|
|
1060
1804
|
sendJsonFrame(ws, buildWsErrorFrame(429, {
|
|
1061
1805
|
type: "rate_limit_error",
|
|
1062
|
-
message: cooldownErrorMessage(err),
|
|
1806
|
+
message: cooldownErrorMessage(err, accountSelector),
|
|
1063
1807
|
}));
|
|
1064
1808
|
return;
|
|
1065
1809
|
}
|
|
@@ -1080,8 +1824,7 @@ export function startServer(port?: number) {
|
|
|
1080
1824
|
},
|
|
1081
1825
|
close(ws: ServerWebSocket<WsData>) {
|
|
1082
1826
|
if (ws.data.kind === "live-sideband") {
|
|
1083
|
-
ws
|
|
1084
|
-
ws.data.liveUpstream = undefined;
|
|
1827
|
+
closeLiveSideband(ws);
|
|
1085
1828
|
return;
|
|
1086
1829
|
}
|
|
1087
1830
|
unregisterCodexWebSocket(ws);
|
|
@@ -1090,10 +1833,69 @@ export function startServer(port?: number) {
|
|
|
1090
1833
|
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
1091
1834
|
},
|
|
1092
1835
|
},
|
|
1093
|
-
|
|
1836
|
+
} as const;
|
|
1837
|
+
|
|
1838
|
+
server = Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost });
|
|
1839
|
+
|
|
1840
|
+
// Both binds are one startup transaction (#1102). If the loopback bind fails after the
|
|
1841
|
+
// public one succeeded, leaving the public listener up would strand it: the CLI's port
|
|
1842
|
+
// retry would read the failure as a public-port conflict and pick a different port,
|
|
1843
|
+
// accumulating listeners. Roll back and rethrow the original error instead.
|
|
1844
|
+
if (loopbackListenerPort !== null) {
|
|
1845
|
+
try {
|
|
1846
|
+
loopbackServer = Bun.serve<WsData>({
|
|
1847
|
+
...serveOptions,
|
|
1848
|
+
port: loopbackListenerPort,
|
|
1849
|
+
hostname: "127.0.0.1",
|
|
1850
|
+
});
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
try {
|
|
1853
|
+
// startServer is synchronous, so this rollback cannot await. Bun begins closing the
|
|
1854
|
+
// listen socket on the call itself; the caller sees the original bind error either
|
|
1855
|
+
// way, and the alternative — leaving the public listener up — is the failure this
|
|
1856
|
+
// rollback exists to prevent.
|
|
1857
|
+
void server.stop(true);
|
|
1858
|
+
} catch {
|
|
1859
|
+
/* the original bind error is the one worth reporting */
|
|
1860
|
+
}
|
|
1861
|
+
throw error;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
userCostOverlayReconciler?.stop();
|
|
1866
|
+
backgroundLifecycle?.releaseAfterFailedStart();
|
|
1867
|
+
void nativeMainLifecycle.release();
|
|
1868
|
+
throw error;
|
|
1869
|
+
}
|
|
1094
1870
|
|
|
1871
|
+
bindNativeMainStartupLifecycle(server, nativeMainLifecycle);
|
|
1872
|
+
const nativeStop = server.stop.bind(server);
|
|
1873
|
+
const loopbackListenerRef = loopbackServer;
|
|
1874
|
+
Object.defineProperty(server, "stop", {
|
|
1875
|
+
configurable: true,
|
|
1876
|
+
value: async (closeActiveConnections?: boolean): Promise<void> => {
|
|
1877
|
+
// The orchestration lives in `runListenerShutdown` so its two competing properties —
|
|
1878
|
+
// cleanup completes, failure propagates — are testable without a live socket.
|
|
1879
|
+
await runListenerShutdown(
|
|
1880
|
+
[
|
|
1881
|
+
() => nativeStop(closeActiveConnections),
|
|
1882
|
+
...(loopbackListenerRef
|
|
1883
|
+
? [() => loopbackListenerRef.stop(closeActiveConnections)]
|
|
1884
|
+
: []),
|
|
1885
|
+
async () => {
|
|
1886
|
+
userCostOverlayReconciler?.stop();
|
|
1887
|
+
},
|
|
1888
|
+
],
|
|
1889
|
+
async () => {
|
|
1890
|
+
await backgroundLifecycle.release();
|
|
1891
|
+
await releaseNativeMainStartupLifecycle(server);
|
|
1892
|
+
},
|
|
1893
|
+
);
|
|
1894
|
+
},
|
|
1895
|
+
});
|
|
1095
1896
|
setServerRef(server);
|
|
1096
1897
|
const actualPort = server.port ?? listenPort;
|
|
1898
|
+
boundPort = actualPort;
|
|
1097
1899
|
setCorsOrigin(actualPort);
|
|
1098
1900
|
|
|
1099
1901
|
console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
|
|
@@ -1103,6 +1905,17 @@ export function startServer(port?: number) {
|
|
|
1103
1905
|
console.log(` GET /api/* → management API`);
|
|
1104
1906
|
console.log(` GET / → GUI dashboard`);
|
|
1105
1907
|
|
|
1908
|
+
if (loopbackServer) {
|
|
1909
|
+
// Loud on every start, not once at enable time. An operator who inherits a config, or
|
|
1910
|
+
// who forgot, has to be able to see that an unauthenticated surface is live without
|
|
1911
|
+
// reading the file.
|
|
1912
|
+
const loopbackPort = loopbackServer.port ?? loopbackListenerPort;
|
|
1913
|
+
console.warn(`⚠️ Unauthenticated loopback listener active on http://127.0.0.1:${loopbackPort}`);
|
|
1914
|
+
console.warn(` Any local process can use it without a credential — it spends account`);
|
|
1915
|
+
console.warn(` quota and paid provider credentials, and can starve authenticated`);
|
|
1916
|
+
console.warn(` remote clients. Not for shared or multi-tenant hosts.`);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1106
1919
|
// Prime pool-account quota in the background so the rotation engine has real
|
|
1107
1920
|
// usage scores from the first routing decision, even when the dashboard is
|
|
1108
1921
|
// never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
|
|
@@ -1114,13 +1927,31 @@ export function startServer(port?: number) {
|
|
|
1114
1927
|
&& isCanonicalOpenAiForwardProvider(openAiProvider)
|
|
1115
1928
|
&& providerCodexAccountMode("openai", openAiProvider) === "pool"
|
|
1116
1929
|
) {
|
|
1117
|
-
import("../codex/
|
|
1930
|
+
import("../codex/plan-from-token")
|
|
1931
|
+
.then(({ reconcileCodexPlansFromTokens }) => {
|
|
1932
|
+
try {
|
|
1933
|
+
reconcileCodexPlansFromTokens(config);
|
|
1934
|
+
} catch {
|
|
1935
|
+
// Derived plan metadata must not block WHAM priming.
|
|
1936
|
+
}
|
|
1937
|
+
return import("../codex/auth-api");
|
|
1938
|
+
})
|
|
1118
1939
|
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
|
|
1119
1940
|
.catch(() => {});
|
|
1120
1941
|
}
|
|
1121
1942
|
|
|
1122
1943
|
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
1123
|
-
|
|
1944
|
+
backgroundLifecycle.scheduleStartupRun();
|
|
1945
|
+
|
|
1946
|
+
// Compatibility Lab is optional: wire it only for installs that actually use it -- any
|
|
1947
|
+
// routing profile, or automation enabled on disk. This runs synchronously before
|
|
1948
|
+
// startServer returns, in the same turn as Bun.serve, so a policy route can never be
|
|
1949
|
+
// evaluated before its evidence provider is registered. That ordering is load-bearing:
|
|
1950
|
+
// the subagent-fallback chain routes synchronously and has nowhere to await.
|
|
1951
|
+
const labConfigDir = getConfigDir();
|
|
1952
|
+
if (labActivationRequired(config, labConfigDir)) {
|
|
1953
|
+
activateLab(config, labConfigDir);
|
|
1954
|
+
}
|
|
1124
1955
|
|
|
1125
1956
|
return server;
|
|
1126
1957
|
}
|