@yansigit/opencodex 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { formatErrorResponse } from "../bridge";
|
|
3
|
+
import {
|
|
4
|
+
apiKeyTransportConfigError,
|
|
5
|
+
booleanRecordConfigError,
|
|
6
|
+
modelAdapterRecordConfigError,
|
|
7
|
+
modelPreferHostedToolsConfigError,
|
|
8
|
+
codexAutoStartEnabled,
|
|
9
|
+
nonBlankStringArrayConfigError,
|
|
10
|
+
positiveIntegerConfigError,
|
|
11
|
+
positiveIntegerRecordConfigError,
|
|
12
|
+
providerBaseUrlConfigError,
|
|
13
|
+
providerHeadersConfigError,
|
|
14
|
+
providerModelCostsConfigError,
|
|
15
|
+
reasoningSummaryDeliveryRecordConfigError,
|
|
16
|
+
retryOn429PolicyConfigError,
|
|
17
|
+
requestPacingConfigError,
|
|
18
|
+
sanitizeModelCostsForDisplay,
|
|
19
|
+
upstreamHttpVersionConfigError,
|
|
20
|
+
} from "../config";
|
|
21
|
+
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
22
|
+
import { redactSecretString } from "../lib/redact";
|
|
23
|
+
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
24
|
+
import { providerConfigSeed } from "../providers/derive";
|
|
25
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
26
|
+
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
27
|
+
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
28
|
+
import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
|
|
29
|
+
|
|
30
|
+
let _corsOrigin = "http://localhost:10100";
|
|
31
|
+
export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
|
|
32
|
+
/** The proxy's own listening port. No admission check uses it: both loopback predicates key on hostname alone. */
|
|
33
|
+
export function configuredPort(): string {
|
|
34
|
+
try { return new URL(_corsOrigin).port; } catch { return "10100"; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseHttpHost(value: string | null): { hostname: string; port: string } | null {
|
|
38
|
+
if (!value) return null;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(`http://${value}`);
|
|
41
|
+
return { hostname: parsed.hostname.toLowerCase(), port: parsed.port };
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isLoopbackRequestHost(value: string | null): boolean {
|
|
48
|
+
const parsed = parseHttpHost(value);
|
|
49
|
+
if (!parsed) return true;
|
|
50
|
+
// Loopback is a trust boundary by hostname, not by port. `ssh -L 20100:localhost:10100`
|
|
51
|
+
// legitimately arrives as `Host: localhost:20100`, and refusing it took the whole /v1/*
|
|
52
|
+
// data plane down with it, not just CORS. The sibling isLoopbackOriginValue() dropped its
|
|
53
|
+
// own port check for the same reason in e4e06125b ("same-trust-boundary"). Port equality
|
|
54
|
+
// was never the rebinding defense: a rebinding browser connects to the real port and sends
|
|
55
|
+
// it verbatim, so the hostname check below is what rejected it then and now.
|
|
56
|
+
//
|
|
57
|
+
// Scope of that guarantee: it holds for Hosts `parseHttpHost` can parse. An unparseable
|
|
58
|
+
// Host still returns true above — pre-existing behavior, not browser-reachable (a browser
|
|
59
|
+
// composes Host from its own connection), and pinned by a characterization test in
|
|
60
|
+
// tests/server-loopback-host-gate.test.ts. Tightening it is separate work.
|
|
61
|
+
return isLoopbackHostname(parsed.hostname);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isLoopbackOriginValue(value: string): boolean {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = new URL(value);
|
|
67
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
68
|
+
return isLoopbackHostname(parsed.hostname);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isSameOriginAsRequest(req: Request, origin: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
return origin === new URL(req.url).origin;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isAllowedRequestOrigin(req: Request, config: RequestPolicyView): boolean {
|
|
83
|
+
const origin = req.headers.get("Origin");
|
|
84
|
+
if (!isApiAuthRequired(config)) {
|
|
85
|
+
if (!isLoopbackRequestHost(req.headers.get("Host"))) return false;
|
|
86
|
+
return !origin || isLoopbackOriginValue(origin) || isExtraAllowedOrigin(origin, config);
|
|
87
|
+
}
|
|
88
|
+
return !origin || isLoopbackOriginValue(origin) || isSameOriginAsRequest(req, origin) || isExtraAllowedOrigin(origin, config);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isExtraAllowedOrigin(origin: string, cfg: RequestPolicyView): boolean {
|
|
92
|
+
if (!cfg.corsAllowOrigins?.length) return false;
|
|
93
|
+
const parsedOrigin = comparableOrigin(origin);
|
|
94
|
+
return cfg.corsAllowOrigins.some(allowed => {
|
|
95
|
+
const parsedAllowed = comparableOrigin(allowed);
|
|
96
|
+
return parsedOrigin !== null && parsedAllowed !== null
|
|
97
|
+
? parsedAllowed === parsedOrigin
|
|
98
|
+
: allowed === origin;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function comparableOrigin(value: string): string | null {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = new URL(value);
|
|
105
|
+
if (parsed.origin !== "null") return parsed.origin;
|
|
106
|
+
// WHATWG URL exposes authority-based custom schemes (for example browser
|
|
107
|
+
// extensions) as opaque `null` origins. Compare their scheme + authority so
|
|
108
|
+
// one allowlisted extension cannot admit every other opaque origin.
|
|
109
|
+
return parsed.host ? `${parsed.protocol}//${parsed.host}` : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function managementRequestOrigin(req: Request, config: OcxConfig): string | null {
|
|
116
|
+
const host = req.headers.get("Host");
|
|
117
|
+
const parsedHost = parseHttpHost(host);
|
|
118
|
+
if (!host || !parsedHost) return null;
|
|
119
|
+
if (!isApiAuthRequired(config) && !isLoopbackHostname(parsedHost.hostname)) return null;
|
|
120
|
+
try {
|
|
121
|
+
const protocol = new URL(req.url).protocol;
|
|
122
|
+
if (protocol !== "http:" && protocol !== "https:") return null;
|
|
123
|
+
return new URL(`${protocol}//${host}`).origin;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isAllowedManagementOrigin(req: Request, config: OcxConfig): boolean {
|
|
130
|
+
const requestOrigin = managementRequestOrigin(req, config);
|
|
131
|
+
if (!requestOrigin) return false;
|
|
132
|
+
const origin = req.headers.get("Origin");
|
|
133
|
+
// Exact match against the process-derived origin, or an operator-listed corsAllowOrigins
|
|
134
|
+
// entry (covers TLS-terminator https://… when the process observes http://…).
|
|
135
|
+
return !origin || origin === requestOrigin || isExtraAllowedOrigin(origin, config);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function browserSecurityHeaders(): Record<string, string> {
|
|
139
|
+
return {
|
|
140
|
+
"X-Frame-Options": "DENY",
|
|
141
|
+
"Content-Security-Policy": "frame-ancestors 'none'",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Baseline data-plane request headers. ChatGPT-Account-Id is required for browser/Electron
|
|
147
|
+
* ChatGPT & Codex App voice preflights (direct forward auth matches the bearer to this account
|
|
148
|
+
* id). The OpenAI-Alpha .. X-OAI-Attestation block covers GPT-Live voice protocol headers
|
|
149
|
+
* relayed by the /v1/live call-create path.
|
|
150
|
+
*/
|
|
151
|
+
const STATIC_ALLOWED_REQUEST_HEADERS =
|
|
152
|
+
"Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key, Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id, Session-Id, Thread-Id, Originator, X-OAI-Attestation";
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A fixed allow-list cannot enumerate vendor telemetry headers: the OpenAI and Anthropic
|
|
156
|
+
* browser SDKs send `X-Stainless-*` describing runtime and retry state, and the browser blocks
|
|
157
|
+
* the real request when the preflight omits even one of them (#1773).
|
|
158
|
+
*
|
|
159
|
+
* Echo what an already-allowed origin asked for, and fall back to the static list otherwise.
|
|
160
|
+
* The echo is deliberately gated on the origin check that ran first: this widens which headers
|
|
161
|
+
* an admitted caller may send, never which origins are admitted, and it grants nothing to an
|
|
162
|
+
* origin that would have been rejected anyway. Authentication is unchanged — the preflight
|
|
163
|
+
* itself carries no credential and produces no auth or account-pool side effect.
|
|
164
|
+
*/
|
|
165
|
+
function allowedRequestHeaders(req?: Request): string {
|
|
166
|
+
const requested = req?.headers.get("Access-Control-Request-Headers")?.trim();
|
|
167
|
+
if (!requested) return STATIC_ALLOWED_REQUEST_HEADERS;
|
|
168
|
+
const seen = new Set(STATIC_ALLOWED_REQUEST_HEADERS.split(",").map(h => h.trim().toLowerCase()));
|
|
169
|
+
const extra: string[] = [];
|
|
170
|
+
for (const raw of requested.split(",")) {
|
|
171
|
+
const name = raw.trim();
|
|
172
|
+
// Header names are case-insensitive on the wire, so normalize before de-duplicating;
|
|
173
|
+
// echo the caller's spelling for the ones we add.
|
|
174
|
+
if (!name || seen.has(name.toLowerCase())) continue;
|
|
175
|
+
seen.add(name.toLowerCase());
|
|
176
|
+
extra.push(name);
|
|
177
|
+
}
|
|
178
|
+
return extra.length === 0 ? STATIC_ALLOWED_REQUEST_HEADERS : `${STATIC_ALLOWED_REQUEST_HEADERS}, ${extra.join(", ")}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function corsHeaders(req?: Request, config?: RequestPolicyView): Record<string, string> {
|
|
182
|
+
const origin = req?.headers.get("Origin");
|
|
183
|
+
const originAllowed = Boolean(origin && req && config && isAllowedRequestOrigin(req, config));
|
|
184
|
+
const allowOrigin = originAllowed && origin ? origin : _corsOrigin;
|
|
185
|
+
return {
|
|
186
|
+
"Access-Control-Allow-Origin": allowOrigin,
|
|
187
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
188
|
+
"Access-Control-Allow-Headers": allowedRequestHeaders(originAllowed ? req : undefined),
|
|
189
|
+
// A response that varies by the request's headers must say so, or a shared cache can
|
|
190
|
+
// replay one client's allow-list to a client that asked for different headers.
|
|
191
|
+
"Vary": "Origin, Access-Control-Request-Headers",
|
|
192
|
+
...browserSecurityHeaders(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function managementCorsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
|
|
197
|
+
const headers = corsHeaders();
|
|
198
|
+
const origin = req?.headers.get("Origin");
|
|
199
|
+
if (origin && req && config && isAllowedManagementOrigin(req, config)) {
|
|
200
|
+
headers["Access-Control-Allow-Origin"] = origin;
|
|
201
|
+
}
|
|
202
|
+
return headers;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function withCors(response: Response, req: Request, config: RequestPolicyView): Response {
|
|
206
|
+
const headers = new Headers(response.headers);
|
|
207
|
+
for (const [name, value] of Object.entries(corsHeaders(req, config))) {
|
|
208
|
+
headers.set(name, value);
|
|
209
|
+
}
|
|
210
|
+
return new Response(response.body, {
|
|
211
|
+
status: response.status,
|
|
212
|
+
statusText: response.statusText,
|
|
213
|
+
headers,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function withManagementCors(response: Response, req: Request, config: OcxConfig): Response {
|
|
218
|
+
const headers = new Headers(response.headers);
|
|
219
|
+
for (const [name, value] of Object.entries(managementCorsHeaders(req, config))) {
|
|
220
|
+
headers.set(name, value);
|
|
221
|
+
}
|
|
222
|
+
return new Response(response.body, {
|
|
223
|
+
status: response.status,
|
|
224
|
+
statusText: response.statusText,
|
|
225
|
+
headers,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function jsonResponse(data: unknown, status = 200, req?: Request, config?: RequestPolicyView): Response {
|
|
230
|
+
return new Response(JSON.stringify(data), {
|
|
231
|
+
status,
|
|
232
|
+
headers: { "Content-Type": "application/json", ...corsHeaders(req, config) },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// The parameter is vestigial — the token has always come from the environment — but callers
|
|
237
|
+
// pass a config, so keep accepting one. Typed as `unknown` rather than `OcxConfig` so a narrow
|
|
238
|
+
// policy view can reach it too (#1102); widening to OcxConfig here would force every caller in
|
|
239
|
+
// the admission path back to the full config.
|
|
240
|
+
export function configuredApiAuthToken(_config?: unknown): string | undefined {
|
|
241
|
+
const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
242
|
+
return token || undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function configuredAdminAuthToken(): string | undefined {
|
|
246
|
+
const token = process.env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim();
|
|
247
|
+
return token || undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
251
|
+
// A fully-qualified "localhost." is the same host as "localhost": curl and some clients
|
|
252
|
+
// send the trailing dot verbatim, and refusing it 403s a legitimate loopback caller.
|
|
253
|
+
const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase().replace(/\.$/, "");
|
|
254
|
+
return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function isApiAuthRequired(config: Pick<OcxConfig, "hostname">): boolean {
|
|
258
|
+
return !isLoopbackHostname(config.hostname);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The slice of config that decides admission and CORS, and nothing else (#1102).
|
|
263
|
+
*
|
|
264
|
+
* The unauthenticated loopback listener shares this process with the public one: same routing,
|
|
265
|
+
* same account pool, same drain. The only thing it must see differently is its own bind
|
|
266
|
+
* address, because `isApiAuthRequired` reads `hostname` and the shared config says "0.0.0.0".
|
|
267
|
+
*
|
|
268
|
+
* Two ways to express that were rejected. Passing the whole config with `hostname` rewritten
|
|
269
|
+
* and holding it for the listener's lifetime would go stale the moment the management API
|
|
270
|
+
* changes a setting. Adding an `allowUnauthenticated` parameter to the resolvers would create a
|
|
271
|
+
* callable admission bypass that the PUBLIC listener could also reach — the switch would exist
|
|
272
|
+
* on the wrong side of the boundary.
|
|
273
|
+
*
|
|
274
|
+
* So this type is deliberately narrow: it cannot masquerade as a business config, and a policy
|
|
275
|
+
* view that leaks into a routing path fails to typecheck rather than silently taking effect.
|
|
276
|
+
*/
|
|
277
|
+
export type RequestPolicyView = Pick<OcxConfig, "hostname" | "corsAllowOrigins" | "apiKeys">;
|
|
278
|
+
|
|
279
|
+
/** Derive the per-request policy view for a listener. Cheap enough to build per request. */
|
|
280
|
+
export function requestPolicyView(config: OcxConfig, bindHostname: string): RequestPolicyView {
|
|
281
|
+
return {
|
|
282
|
+
hostname: bindHostname,
|
|
283
|
+
...(config.corsAllowOrigins ? { corsAllowOrigins: config.corsAllowOrigins } : {}),
|
|
284
|
+
...(config.apiKeys ? { apiKeys: config.apiKeys } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function assertServerAuthConfig(config: OcxConfig): void {
|
|
289
|
+
const hasConfiguredDataCredential = !!configuredApiAuthToken(config)
|
|
290
|
+
|| (config.apiKeys ?? []).some(entry => !!entry.key.trim());
|
|
291
|
+
if (isApiAuthRequired(config) && !hasConfiguredDataCredential) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"A data-plane credential (OPENCODEX_API_AUTH_TOKEN or config.apiKeys) is required when binding opencodex to a non-loopback hostname",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function secretEquals(actual: string, expected: string | undefined): boolean {
|
|
299
|
+
if (!expected) return false;
|
|
300
|
+
const enc = new TextEncoder();
|
|
301
|
+
const actualBytes = enc.encode(actual);
|
|
302
|
+
const expectedBytes = enc.encode(expected);
|
|
303
|
+
return expectedBytes.length === actualBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Which admission a data-plane request used.
|
|
308
|
+
*
|
|
309
|
+
* `configured` carries the matched key's id so a request can be attributed to
|
|
310
|
+
* the key that opened it. The other two exist so an unattributed request is a
|
|
311
|
+
* stated fact rather than a missing field: neither has a configured entry to
|
|
312
|
+
* point at, and a sentinel string in the id would collide with a hand-edited
|
|
313
|
+
* entry that happens to be named `loopback`.
|
|
314
|
+
*/
|
|
315
|
+
/**
|
|
316
|
+
* HOW an admission credential was presented.
|
|
317
|
+
*
|
|
318
|
+
* The credential IDENTITY (which key matched) and its PRESENTATION (which header carried it)
|
|
319
|
+
* are different facts, and #1686 needs both: a proxy secret arriving as a bearer on the
|
|
320
|
+
* Responses transport is admissible, but only if the upstream credential is then guaranteed to
|
|
321
|
+
* be substituted. Collapsing the two is what made that flow unexpressible.
|
|
322
|
+
*/
|
|
323
|
+
export type DataPlaneAdmissionSource = "loopback" | "dedicated" | "bearer" | "x-api-key";
|
|
324
|
+
|
|
325
|
+
export type DataPlaneAdmission =
|
|
326
|
+
| { kind: "configured"; keyId: string; source: DataPlaneAdmissionSource }
|
|
327
|
+
| { kind: "environment"; source: DataPlaneAdmissionSource }
|
|
328
|
+
| { kind: "loopback"; source: "loopback" };
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Which admission secret `token` is, or null when it is none of them.
|
|
332
|
+
*
|
|
333
|
+
* Identical comparisons in an identical order to the boolean form this replaces —
|
|
334
|
+
* `secretEquals` still length-guards before `timingSafeEqual`. The only
|
|
335
|
+
* difference is that the matched entry's id survives the loop instead of being
|
|
336
|
+
* discarded, which is what makes per-key attribution possible without touching
|
|
337
|
+
* the admission decision itself.
|
|
338
|
+
*/
|
|
339
|
+
export function resolveDataPlaneAdmissionSecret(
|
|
340
|
+
token: string,
|
|
341
|
+
config: Pick<OcxConfig, "apiKeys">,
|
|
342
|
+
source: DataPlaneAdmissionSource = "dedicated",
|
|
343
|
+
): DataPlaneAdmission | null {
|
|
344
|
+
const actual = token.trim();
|
|
345
|
+
if (!actual) return null;
|
|
346
|
+
if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment", source };
|
|
347
|
+
for (const k of config.apiKeys ?? []) {
|
|
348
|
+
if (secretEquals(actual, k.key)) return { kind: "configured", keyId: k.id, source };
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Whether `token` is a data-plane admission secret. */
|
|
354
|
+
export function isDataPlaneAdmissionSecret(token: string, config: OcxConfig): boolean {
|
|
355
|
+
return resolveDataPlaneAdmissionSecret(token, config) !== null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Split an admission into the fields a log row records.
|
|
360
|
+
*
|
|
361
|
+
* `apiKeyId` is set only for a configured key. The other two kinds have no
|
|
362
|
+
* configured entry to name, and folding them into the id as sentinel strings
|
|
363
|
+
* would collide with a hand-edited entry that happens to be called `loopback` —
|
|
364
|
+
* ids are only validated as non-empty strings.
|
|
365
|
+
*/
|
|
366
|
+
export function admissionFields(admission: DataPlaneAdmission): {
|
|
367
|
+
admissionKind: DataPlaneAdmission["kind"];
|
|
368
|
+
apiKeyId?: string;
|
|
369
|
+
} {
|
|
370
|
+
return admission.kind === "configured"
|
|
371
|
+
? { admissionKind: "configured", apiKeyId: admission.keyId }
|
|
372
|
+
: { admissionKind: admission.kind };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export type ApiAuthDisposition = "required" | "accepted" | "rejected";
|
|
376
|
+
|
|
377
|
+
export interface ApiAuthMatrixRow {
|
|
378
|
+
endpoint: string;
|
|
379
|
+
bearer: ApiAuthDisposition;
|
|
380
|
+
dedicated: ApiAuthDisposition;
|
|
381
|
+
xApiKey: ApiAuthDisposition;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Which headers each data-plane endpoint actually accepts, shipped to the GUI so
|
|
386
|
+
* it stops describing the rule from memory. The dashboard has been telling users
|
|
387
|
+
* that Chat Completions takes `Authorization: Bearer`, which this file has never
|
|
388
|
+
* allowed — that route uses the dedicated-header-only wrapper because
|
|
389
|
+
* `Authorization` there may belong to Codex Direct passthrough.
|
|
390
|
+
*
|
|
391
|
+
* It lives next to the wrappers it describes, and a test drives real requests
|
|
392
|
+
* against every cell rather than reading the table back to itself.
|
|
393
|
+
*/
|
|
394
|
+
export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
|
|
395
|
+
// #1686: a bearer that is one of OUR admission secrets is now accepted here. It is safe
|
|
396
|
+
// because materializeCodexUpstreamAuth substitutes the stored main credential rather than
|
|
397
|
+
// forwarding it; a bearer that is NOT our secret stays unadmitted and remains Codex Direct
|
|
398
|
+
// passthrough, so the two bearer domains still never mix. `x-api-key` is still rejected.
|
|
399
|
+
{ endpoint: "/v1/responses", bearer: "accepted", dedicated: "accepted", xApiKey: "rejected" },
|
|
400
|
+
{ endpoint: "/v1/chat/completions", bearer: "accepted", dedicated: "accepted", xApiKey: "rejected" },
|
|
401
|
+
{ endpoint: "/v1/messages", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
402
|
+
{ endpoint: "/v1/models", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
/** Whether `token` is the environment-provided management secret. */
|
|
406
|
+
export function isManagementAdmissionSecret(token: string): boolean {
|
|
407
|
+
const actual = token.trim();
|
|
408
|
+
return !!actual && secretEquals(actual, configuredAdminAuthToken());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Whether `token` is one of the proxy's own admission secrets and must never reach an upstream. */
|
|
412
|
+
export function isProxyAdmissionSecret(token: string, config: OcxConfig): boolean {
|
|
413
|
+
const actual = token.trim();
|
|
414
|
+
if (!actual) return false;
|
|
415
|
+
if (/^ocx_(?:data|admin|session)_/.test(actual) || /^ocx_[0-9a-f]{40}$/.test(actual)) return true;
|
|
416
|
+
return isDataPlaneAdmissionSecret(actual, config) || isManagementAdmissionSecret(actual);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export class ForwardAdmissionCredentialError extends Error {
|
|
420
|
+
constructor() {
|
|
421
|
+
super("OpenCodex admission credentials cannot be forwarded upstream");
|
|
422
|
+
this.name = "ForwardAdmissionCredentialError";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function validateForwardAdmissionCredential(headers: Headers, config: OcxConfig): void {
|
|
427
|
+
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
428
|
+
if (bearer && isProxyAdmissionSecret(bearer, config)) throw new ForwardAdmissionCredentialError();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Resolving form of `hasValidApiAuth`: identical header precedence, identical
|
|
433
|
+
* decision, but it names the admission instead of collapsing it to a boolean.
|
|
434
|
+
*/
|
|
435
|
+
export function resolveApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
|
|
436
|
+
// A loopback bind never reads a token at all, so there is no key to name.
|
|
437
|
+
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
438
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
439
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
440
|
+
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
441
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
442
|
+
// Anthropic-SDK clients (Claude Code with ANTHROPIC_API_KEY) authenticate via x-api-key.
|
|
443
|
+
const apiKey = req.headers.get("x-api-key")?.trim();
|
|
444
|
+
if (apiKey) return resolveDataPlaneAdmissionSecret(apiKey, config, "x-api-key");
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function hasValidApiAuth(req: Request, config: RequestPolicyView): boolean {
|
|
449
|
+
return resolveApiAuth(req, config) !== null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function requireApiAuth(req: Request, config: RequestPolicyView, _kind: "data-plane"): Response | null {
|
|
453
|
+
if (hasValidApiAuth(req, config)) return null;
|
|
454
|
+
return formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Admission for OpenAI Responses transports whose Authorization header belongs to
|
|
459
|
+
* Codex Direct. Remote binds must use the dedicated proxy header so the two bearer
|
|
460
|
+
* domains can never be confused.
|
|
461
|
+
*/
|
|
462
|
+
export function resolveResponsesApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
|
|
463
|
+
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
464
|
+
// The dedicated header still WINS, because it is unambiguous.
|
|
465
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
466
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
467
|
+
// #1686: a bearer may also be one of OUR admission secrets. Rejecting it outright meant a
|
|
468
|
+
// Codex client configured with `env_key` could not reach Direct at all. Admitting it is only
|
|
469
|
+
// safe because the upstream credential is then SUBSTITUTED rather than forwarded -- see
|
|
470
|
+
// materializeCodexUpstreamAuth. A bearer that is NOT our secret stays unadmitted here and
|
|
471
|
+
// remains Codex Direct passthrough, so the two bearer domains still never mix.
|
|
472
|
+
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
473
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
474
|
+
// `x-api-key` is deliberately NOT accepted on this transport.
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
export function requireResponsesApiAuth(req: Request, config: RequestPolicyView): Response | null {
|
|
481
|
+
if (resolveResponsesApiAuth(req, config)) return null;
|
|
482
|
+
return formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const FORBIDDEN_PROVIDER_RUNTIME_FIELDS = [
|
|
486
|
+
"virtualModels", "codexAuthContext", "selectedForwardHeaders",
|
|
487
|
+
"sidecarOutcomeRecorder", "_codexAccountOverride", "_codexAccountRequired",
|
|
488
|
+
] as const;
|
|
489
|
+
|
|
490
|
+
function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
|
|
491
|
+
const actualKeys = Object.keys(actual).sort();
|
|
492
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
493
|
+
if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, i) => key !== expectedKeys[i])) return false;
|
|
494
|
+
return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function positiveWindowValue(value: unknown): boolean {
|
|
498
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Shape-check the two context overlays before the canonical seed comparison drops them.
|
|
503
|
+
*
|
|
504
|
+
* `null` means "clear this" and is normalized by the PATCH field mask, but this validator
|
|
505
|
+
* also runs for POST and reload, where a whole provider object lands on disk verbatim. A
|
|
506
|
+
* `null` surviving there would be a value no reader expects, so full objects must carry a
|
|
507
|
+
* real number or omit the field.
|
|
508
|
+
*/
|
|
509
|
+
function nativeContextOverlayError(raw: Record<string, unknown>): string | null {
|
|
510
|
+
if (Object.hasOwn(raw, "contextWindow") && !positiveWindowValue(raw.contextWindow)) {
|
|
511
|
+
return "provider openai contextWindow must be a positive safe integer";
|
|
512
|
+
}
|
|
513
|
+
if (Object.hasOwn(raw, "modelContextWindows")) {
|
|
514
|
+
const windows = raw.modelContextWindows;
|
|
515
|
+
if (typeof windows !== "object" || windows === null || Array.isArray(windows)) {
|
|
516
|
+
return "provider openai modelContextWindows must be a plain object";
|
|
517
|
+
}
|
|
518
|
+
for (const [model, value] of Object.entries(windows as Record<string, unknown>)) {
|
|
519
|
+
if (model.trim() === "") return "provider openai modelContextWindows keys must be nonblank model ids";
|
|
520
|
+
if (!positiveWindowValue(value)) {
|
|
521
|
+
return "provider openai modelContextWindows values must be positive safe integers";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Validate a provider object arriving at the management write boundary. Returns an error
|
|
530
|
+
* string, or null when the provider may be persisted. Caller-controlled names/fields are
|
|
531
|
+
* redacted and JSON-escaped so secrets never reach the response.
|
|
532
|
+
*/
|
|
533
|
+
export function providerManagementConfigError(name: unknown, provider: unknown): string | null {
|
|
534
|
+
if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
535
|
+
return "provider must be a plain object";
|
|
536
|
+
}
|
|
537
|
+
const raw = provider as Record<string, unknown>;
|
|
538
|
+
for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) {
|
|
539
|
+
if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`;
|
|
540
|
+
}
|
|
541
|
+
if (name === "chatgpt") return "provider chatgpt is reserved for internal credential compatibility";
|
|
542
|
+
if (name === "openai-multi") return "provider openai-multi is reserved for legacy config migration";
|
|
543
|
+
if (name === "openai") {
|
|
544
|
+
const entry = getProviderRegistryEntry(name);
|
|
545
|
+
const seed = entry ? providerConfigSeed(entry) : undefined;
|
|
546
|
+
if (!Object.hasOwn(raw, "codexAccountMode") || (raw.codexAccountMode !== "pool" && raw.codexAccountMode !== "direct")) {
|
|
547
|
+
return "provider openai codexAccountMode must be pool or direct";
|
|
548
|
+
}
|
|
549
|
+
if (seed) seed.codexAccountMode = raw.codexAccountMode;
|
|
550
|
+
const canonicalCandidate = { ...raw };
|
|
551
|
+
delete canonicalCandidate.responsesSnapshotRepair;
|
|
552
|
+
// modelCosts is a user-owned display overlay, not part of the canonical
|
|
553
|
+
// forward seed; it is validated separately below (providerModelCostsConfigError).
|
|
554
|
+
delete canonicalCandidate.modelCosts;
|
|
555
|
+
// requestPacing is a user-owned transport overlay, not part of the canonical seed.
|
|
556
|
+
delete canonicalCandidate.requestPacing;
|
|
557
|
+
// Context windows are the same kind of user-owned overlay as requestPacing: the operator
|
|
558
|
+
// narrowing what their own native rows advertise. They can only ever LOWER the measured
|
|
559
|
+
// window (see nativeOpenAiContextWindow), so admitting them cannot widen what the proxy
|
|
560
|
+
// claims. Validated first — this function also guards POST/reload, where nothing
|
|
561
|
+
// normalizes the shape afterwards, so a bad value would reach disk.
|
|
562
|
+
const contextOverlayError = nativeContextOverlayError(raw);
|
|
563
|
+
if (contextOverlayError) return contextOverlayError;
|
|
564
|
+
delete canonicalCandidate.contextWindow;
|
|
565
|
+
delete canonicalCandidate.modelContextWindows;
|
|
566
|
+
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
567
|
+
if (!canonical) {
|
|
568
|
+
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
569
|
+
}
|
|
570
|
+
} else if (Object.hasOwn(raw, "codexAccountMode")) {
|
|
571
|
+
return `provider ${name} must not include codexAccountMode`;
|
|
572
|
+
}
|
|
573
|
+
const typed = provider as unknown as OcxProviderConfig;
|
|
574
|
+
const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
|
|
575
|
+
if (baseUrlError) return `provider ${name} ${baseUrlError}`;
|
|
576
|
+
if (effectiveGoogleMode(name, typed) === "vertex" && typed.location !== undefined) {
|
|
577
|
+
const locationError = googleVertexLocationConfigError(typed.location);
|
|
578
|
+
if (locationError) return `provider ${name} ${locationError}`;
|
|
579
|
+
}
|
|
580
|
+
const destinationError = providerDestinationConfigError(name, typed);
|
|
581
|
+
if (destinationError) return `provider ${name} ${destinationError}`;
|
|
582
|
+
const headersError = providerHeadersConfigError(typed.headers);
|
|
583
|
+
if (headersError) return `provider ${name} ${headersError}`;
|
|
584
|
+
const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
|
|
585
|
+
if (retryOn429Error) {
|
|
586
|
+
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
587
|
+
// it before it reaches the management API response.
|
|
588
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${retryOn429Error}`;
|
|
589
|
+
}
|
|
590
|
+
const requestPacingError = requestPacingConfigError(raw.requestPacing);
|
|
591
|
+
if (requestPacingError) {
|
|
592
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
|
|
593
|
+
}
|
|
594
|
+
const upstreamHttpVersionError = upstreamHttpVersionConfigError(raw.upstreamHttpVersion);
|
|
595
|
+
if (upstreamHttpVersionError) {
|
|
596
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
|
|
597
|
+
}
|
|
598
|
+
const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
|
|
599
|
+
if (modelCostsError) {
|
|
600
|
+
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
601
|
+
// it before it reaches the management API response (same rule as retryOn429 above).
|
|
602
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${modelCostsError}`;
|
|
603
|
+
}
|
|
604
|
+
const apiKeyTransportError = apiKeyTransportConfigError(typed);
|
|
605
|
+
if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
|
|
606
|
+
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
607
|
+
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
608
|
+
const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
|
|
609
|
+
if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
|
|
610
|
+
const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
|
|
611
|
+
raw.modelReasoningSummaryDelivery,
|
|
612
|
+
raw.modelSupportsReasoningSummaries,
|
|
613
|
+
);
|
|
614
|
+
if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`;
|
|
615
|
+
const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed);
|
|
616
|
+
if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`;
|
|
617
|
+
const preferHostedToolsError = modelPreferHostedToolsConfigError(
|
|
618
|
+
raw.modelPreferHostedTools,
|
|
619
|
+
"modelPreferHostedTools",
|
|
620
|
+
name,
|
|
621
|
+
typed,
|
|
622
|
+
);
|
|
623
|
+
if (preferHostedToolsError) return `provider ${name} ${preferHostedToolsError}`;
|
|
624
|
+
if (raw.responsesSnapshotRepair !== undefined && typeof raw.responsesSnapshotRepair !== "boolean") {
|
|
625
|
+
return `provider ${name} responsesSnapshotRepair must be a boolean`;
|
|
626
|
+
}
|
|
627
|
+
const defaultMaxOutputError = positiveIntegerConfigError(raw.defaultMaxOutputTokens, "defaultMaxOutputTokens");
|
|
628
|
+
if (defaultMaxOutputError) return `provider ${name} ${defaultMaxOutputError}`;
|
|
629
|
+
const maxOutputError = positiveIntegerRecordConfigError(raw.modelMaxOutputTokens, "modelMaxOutputTokens");
|
|
630
|
+
if (maxOutputError) return `provider ${name} ${maxOutputError}`;
|
|
631
|
+
const structuredOutputOptOutError = nonBlankStringArrayConfigError(
|
|
632
|
+
raw.noStructuredOutputModels,
|
|
633
|
+
"noStructuredOutputModels",
|
|
634
|
+
);
|
|
635
|
+
if (structuredOutputOptOutError) return `provider ${name} ${structuredOutputOptOutError}`;
|
|
636
|
+
const openRouterError = openRouterRoutingConfigError(typed);
|
|
637
|
+
if (openRouterError) return `provider ${name} ${openRouterError}`;
|
|
638
|
+
if (typed.authMode === "local") {
|
|
639
|
+
// "local" bypasses key-requirement enforcement (api-keys/key-failover treat non-oauth/
|
|
640
|
+
// forward as key auth; openai-chat skips credential checks for local). Only providers
|
|
641
|
+
// whose registry entry is genuinely local (Ollama/vLLM/LM Studio) may claim it.
|
|
642
|
+
const entry = getProviderRegistryEntry(name);
|
|
643
|
+
if (entry && entry.authKind !== "local") {
|
|
644
|
+
return `provider ${name} cannot use authMode "local" — its registry entry requires ${entry.authKind} auth`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (typed.authMode === "forward") {
|
|
648
|
+
const normalizedName = name.trim().toLowerCase();
|
|
649
|
+
const base = typed.baseUrl.replace(/\/+$/, "");
|
|
650
|
+
const isBuiltInChatGptForward = normalizedName === "openai"
|
|
651
|
+
&& typed.adapter === "openai-responses"
|
|
652
|
+
&& base === "https://chatgpt.com/backend-api/codex";
|
|
653
|
+
if (isBuiltInChatGptForward) return null;
|
|
654
|
+
return `provider ${name} uses reserved authMode "forward"; configure ChatGPT passthrough via the built-in provider`;
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function publicProviderBaseUrl(baseUrl: string): string {
|
|
660
|
+
try {
|
|
661
|
+
const parsed = new URL(baseUrl.trim());
|
|
662
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "(invalid URL)";
|
|
663
|
+
parsed.username = "";
|
|
664
|
+
parsed.password = "";
|
|
665
|
+
parsed.search = "";
|
|
666
|
+
parsed.hash = "";
|
|
667
|
+
return parsed.toString().replace(/\/$/, baseUrl.endsWith("/") ? "/" : "");
|
|
668
|
+
} catch {
|
|
669
|
+
return "(invalid URL)";
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function copyIfDefined<K extends keyof OcxProviderConfig>(
|
|
674
|
+
out: Record<string, unknown>,
|
|
675
|
+
provider: OcxProviderConfig,
|
|
676
|
+
key: K,
|
|
677
|
+
): void {
|
|
678
|
+
const value = provider[key];
|
|
679
|
+
if (value !== undefined) out[key as string] = value as unknown;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** Public dashboard DTO for config.json: provider entries with secrets stripped and documented fields exposed (including `modelCosts`). */
|
|
683
|
+
export function safeConfigDTO(config: OcxConfig): unknown {
|
|
684
|
+
const providers: Record<string, Record<string, unknown>> = {};
|
|
685
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
686
|
+
const dto: Record<string, unknown> = {
|
|
687
|
+
adapter: provider.adapter,
|
|
688
|
+
baseUrl: publicProviderBaseUrl(provider.baseUrl),
|
|
689
|
+
hasApiKey: !!provider.apiKey,
|
|
690
|
+
hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0,
|
|
691
|
+
};
|
|
692
|
+
if (name === "xai") {
|
|
693
|
+
dto.xaiResponsesOptInState = xaiResponsesOptInState(provider);
|
|
694
|
+
}
|
|
695
|
+
for (const key of [
|
|
696
|
+
"defaultModel",
|
|
697
|
+
"disabled",
|
|
698
|
+
"allowPrivateNetwork",
|
|
699
|
+
"authMode",
|
|
700
|
+
"apiKeyTransport",
|
|
701
|
+
"keyOptional",
|
|
702
|
+
"freeTier",
|
|
703
|
+
"liveModels",
|
|
704
|
+
"requestPacing",
|
|
705
|
+
"models",
|
|
706
|
+
"contextWindow",
|
|
707
|
+
"modelContextWindows",
|
|
708
|
+
"defaultMaxOutputTokens",
|
|
709
|
+
"modelMaxOutputTokens",
|
|
710
|
+
"openRouterRouting",
|
|
711
|
+
"modelOpenRouterRouting",
|
|
712
|
+
"reasoningEfforts",
|
|
713
|
+
"modelReasoningEfforts",
|
|
714
|
+
"reasoningWireFormat",
|
|
715
|
+
"noVisionModels",
|
|
716
|
+
"noReasoningModels",
|
|
717
|
+
"noTemperatureModels",
|
|
718
|
+
"noTopPModels",
|
|
719
|
+
"noPenaltyModels",
|
|
720
|
+
"noStructuredOutputModels",
|
|
721
|
+
"upstreamHttpVersion",
|
|
722
|
+
"autoToolChoiceOnlyModels",
|
|
723
|
+
"preserveReasoningContentModels",
|
|
724
|
+
"requiresReasoningPlaceholderModels",
|
|
725
|
+
"escapeBuiltinToolNames",
|
|
726
|
+
] as const) {
|
|
727
|
+
copyIfDefined(dto, provider, key);
|
|
728
|
+
}
|
|
729
|
+
const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
|
|
730
|
+
if (modelCosts) dto.modelCosts = modelCosts;
|
|
731
|
+
// Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
|
|
732
|
+
// still pointed at the same vendor route, and a usage restriction the user needs to see
|
|
733
|
+
// must not disappear because the row was renamed. Prefer the same-name entry so an
|
|
734
|
+
// unrenamed provider keeps its exact registry note.
|
|
735
|
+
const registryNote = (providerMatchesRegistryTransport(name, provider)
|
|
736
|
+
? getProviderRegistryEntry(name)
|
|
737
|
+
: registryEntryForProviderDestination(provider))?.note;
|
|
738
|
+
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
739
|
+
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
740
|
+
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
|
|
741
|
+
providers[name] = dto;
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
port: config.port,
|
|
745
|
+
hostname: config.hostname ?? "127.0.0.1",
|
|
746
|
+
defaultProvider: config.defaultProvider,
|
|
747
|
+
codexAutoStart: codexAutoStartEnabled(config),
|
|
748
|
+
websockets: config.websockets,
|
|
749
|
+
providers,
|
|
750
|
+
};
|
|
751
|
+
}
|