@iislee/opencodex 2.11.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/LICENSE +21 -0
- package/README.md +225 -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 +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.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/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.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/gemini.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-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/kiro.svg +14 -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 +1 -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 +102 -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.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -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 +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -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 +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -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 +189 -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 +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -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 +829 -0
- package/src/images/plan.ts +133 -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/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 +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -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 +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -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 +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -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 +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -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 +471 -0
- package/src/service.ts +2554 -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 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -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 +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** In-memory ring buffer of debug log lines for `ocx debug logs` / GUI tailing. */
|
|
2
|
+
|
|
3
|
+
export interface DebugLogEntry {
|
|
4
|
+
/** Monotonic cursor for pagination; survives same-millisecond bursts. */
|
|
5
|
+
seq: number;
|
|
6
|
+
at: number;
|
|
7
|
+
line: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_LINES = 2_000;
|
|
11
|
+
const MAX_DEBUG_SUBSCRIBERS = 64;
|
|
12
|
+
const MAX_DEBUG_LINE_BYTES = 16 * 1024;
|
|
13
|
+
const buffer: DebugLogEntry[] = [];
|
|
14
|
+
interface DebugSubscriberRegistration { lease: AdmissionLease }
|
|
15
|
+
const listeners = new Map<(entry: DebugLogEntry) => void, DebugSubscriberRegistration>();
|
|
16
|
+
const subscriberGate = createAdmissionGate("debug_subscribers", MAX_DEBUG_SUBSCRIBERS);
|
|
17
|
+
let nextSeq = 1;
|
|
18
|
+
let bufferBytes = 0;
|
|
19
|
+
|
|
20
|
+
function removeOldestEntry(): number {
|
|
21
|
+
const entry = buffer.shift();
|
|
22
|
+
if (!entry) return 0;
|
|
23
|
+
const bytes = retainedUtf8Bytes(entry.line);
|
|
24
|
+
bufferBytes -= bytes;
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function appendDebugLogLine(line: string): void {
|
|
29
|
+
const retainedLine = truncateRetainedUtf8(line, MAX_DEBUG_LINE_BYTES);
|
|
30
|
+
const entry: DebugLogEntry = { seq: nextSeq++, at: Date.now(), line: retainedLine };
|
|
31
|
+
buffer.push(entry);
|
|
32
|
+
bufferBytes += retainedUtf8Bytes(retainedLine);
|
|
33
|
+
while (buffer.length > MAX_LINES) removeOldestEntry();
|
|
34
|
+
enforceAppOwnedMemoryBudget();
|
|
35
|
+
for (const listener of listeners.keys()) {
|
|
36
|
+
try { listener(entry); } catch { /* listeners must not break logging */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getDebugLogEntries(options?: { after?: number; limit?: number }): DebugLogEntry[] {
|
|
41
|
+
const after = options?.after ?? 0;
|
|
42
|
+
const limit = options?.limit ?? 500;
|
|
43
|
+
const filtered = after > 0 ? buffer.filter(entry => entry.seq > after) : buffer;
|
|
44
|
+
if (filtered.length <= limit) return filtered;
|
|
45
|
+
return filtered.slice(-limit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function subscribeDebugLogEntries(listener: (entry: DebugLogEntry) => void): () => void {
|
|
49
|
+
let registration = listeners.get(listener);
|
|
50
|
+
if (!registration) {
|
|
51
|
+
const lease = subscriberGate.tryAcquire();
|
|
52
|
+
if (!lease) throw new ResourceAdmissionError("debug_subscribers", MAX_DEBUG_SUBSCRIBERS);
|
|
53
|
+
registration = { lease };
|
|
54
|
+
listeners.set(listener, registration);
|
|
55
|
+
}
|
|
56
|
+
let disposed = false;
|
|
57
|
+
return () => {
|
|
58
|
+
if (disposed) return;
|
|
59
|
+
disposed = true;
|
|
60
|
+
if (listeners.get(listener) !== registration) return;
|
|
61
|
+
listeners.delete(listener);
|
|
62
|
+
registration.lease.release();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function debugBufferMetrics(): { entries: number; bytes: number; subscribers: AdmissionMetrics; oldestAt: number | null } {
|
|
67
|
+
return { entries: buffer.length, bytes: bufferBytes, subscribers: subscriberGate.metrics(), oldestAt: buffer[0]?.at ?? null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function evictOldestDebugEntryForBudget(): number {
|
|
71
|
+
return removeOldestEntry();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Test isolation. */
|
|
75
|
+
export function resetDebugLogBufferForTests(): void {
|
|
76
|
+
buffer.length = 0;
|
|
77
|
+
bufferBytes = 0;
|
|
78
|
+
for (const registration of listeners.values()) registration.lease.release();
|
|
79
|
+
listeners.clear();
|
|
80
|
+
nextSeq = 1;
|
|
81
|
+
}
|
|
82
|
+
import { createAdmissionGate, ResourceAdmissionError, retainedUtf8Bytes, truncateRetainedUtf8, type AdmissionLease, type AdmissionMetrics } from "./admission";
|
|
83
|
+
import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-controllable debug flags.
|
|
3
|
+
* Provider debug: `ocx debug provider on|off|status|reset|logs [-f]` (or OCX_DEBUG=1 on start).
|
|
4
|
+
* Usage capture: `ocx debug usage on|off|status|reset|logs [-f]` (or OPENCODEX_USAGE_DEBUG=1).
|
|
5
|
+
* Injection log: `ocx debug injection on|off|status|reset` (or OCX_INJECTION_DEBUG=1) —
|
|
6
|
+
* multi-agent guidance-injection console lines, default OFF.
|
|
7
|
+
* Claude inbound capture: `ocx debug claude on|off|status|reset` (or OCX_CLAUDE_DEBUG=1) —
|
|
8
|
+
* allowlist-scalar ring of inbound Anthropic request metadata, default OFF.
|
|
9
|
+
* `/api/debug` and `ocx debug` override env defaults without restart.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const DEBUG_ENV = {
|
|
13
|
+
debug: "OCX_DEBUG",
|
|
14
|
+
usage: "OPENCODEX_USAGE_DEBUG",
|
|
15
|
+
injection: "OCX_INJECTION_DEBUG",
|
|
16
|
+
claude: "OCX_CLAUDE_DEBUG",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** Legacy env var that still enables provider debug logging. */
|
|
20
|
+
const LEGACY_DEBUG_ENV = ["OCX_DEBUG_FRAMES"] as const;
|
|
21
|
+
|
|
22
|
+
export type DebugFlag = keyof typeof DEBUG_ENV;
|
|
23
|
+
|
|
24
|
+
export interface DebugSettingsView {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
usage: boolean;
|
|
27
|
+
injection: boolean;
|
|
28
|
+
claude: boolean;
|
|
29
|
+
runtimeOverride: Partial<Record<DebugFlag, boolean>>;
|
|
30
|
+
env: Record<DebugFlag, boolean>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const runtimeOverride: Partial<Record<DebugFlag, boolean>> = {};
|
|
34
|
+
|
|
35
|
+
function envFlag(name: string): boolean {
|
|
36
|
+
return process.env[name] === "1";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function legacyDebugEnvEnabled(): boolean {
|
|
40
|
+
return LEGACY_DEBUG_ENV.some(name => envFlag(name));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isDebugEnabled(): boolean {
|
|
44
|
+
if (runtimeOverride.debug !== undefined) return runtimeOverride.debug;
|
|
45
|
+
return envFlag(DEBUG_ENV.debug) || legacyDebugEnvEnabled();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @deprecated Use isDebugEnabled(). */
|
|
49
|
+
export function isFramesDebugEnabled(): boolean {
|
|
50
|
+
return isDebugEnabled();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isUsageDebugEnabled(): boolean {
|
|
54
|
+
if (runtimeOverride.usage !== undefined) return runtimeOverride.usage;
|
|
55
|
+
return envFlag(DEBUG_ENV.usage);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Multi-agent guidance-injection log lines (default OFF; GUI checkbox / API / CLI). */
|
|
59
|
+
export function isInjectionDebugEnabled(): boolean {
|
|
60
|
+
if (runtimeOverride.injection !== undefined) return runtimeOverride.injection;
|
|
61
|
+
return envFlag(DEBUG_ENV.injection);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Claude inbound request capture (default OFF; GUI toggle / API / CLI). */
|
|
65
|
+
export function isClaudeDebugEnabled(): boolean {
|
|
66
|
+
if (runtimeOverride.claude !== undefined) return runtimeOverride.claude;
|
|
67
|
+
return envFlag(DEBUG_ENV.claude);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getDebugSettings(): DebugSettingsView {
|
|
71
|
+
return {
|
|
72
|
+
enabled: isDebugEnabled(),
|
|
73
|
+
usage: isUsageDebugEnabled(),
|
|
74
|
+
injection: isInjectionDebugEnabled(),
|
|
75
|
+
claude: isClaudeDebugEnabled(),
|
|
76
|
+
runtimeOverride: { ...runtimeOverride },
|
|
77
|
+
env: {
|
|
78
|
+
debug: envFlag(DEBUG_ENV.debug) || legacyDebugEnvEnabled(),
|
|
79
|
+
usage: envFlag(DEBUG_ENV.usage),
|
|
80
|
+
injection: envFlag(DEBUG_ENV.injection),
|
|
81
|
+
claude: envFlag(DEBUG_ENV.claude),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function setDebugSettings(partial: Partial<Record<DebugFlag, boolean>>): DebugSettingsView {
|
|
87
|
+
for (const key of ["debug", "usage", "injection", "claude"] as const) {
|
|
88
|
+
if (partial[key] !== undefined) runtimeOverride[key] = partial[key];
|
|
89
|
+
}
|
|
90
|
+
return getDebugSettings();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function clearDebugSetting(flag: DebugFlag): DebugSettingsView {
|
|
94
|
+
delete runtimeOverride[flag];
|
|
95
|
+
return getDebugSettings();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function clearDebugSettings(): DebugSettingsView {
|
|
99
|
+
for (const key of ["debug", "usage", "injection", "claude"] as const) {
|
|
100
|
+
delete runtimeOverride[key];
|
|
101
|
+
}
|
|
102
|
+
return getDebugSettings();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Test isolation: drop runtime overrides only (env vars untouched). */
|
|
106
|
+
export function resetDebugSettingsForTests(): void {
|
|
107
|
+
clearDebugSettings();
|
|
108
|
+
}
|
package/src/lib/debug.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { appendDebugLogLine } from "./debug-log-buffer";
|
|
2
|
+
import { isDebugEnabled } from "./debug-settings";
|
|
3
|
+
import { redactSecrets } from "./redact";
|
|
4
|
+
|
|
5
|
+
function emitDebugLine(line: string): void {
|
|
6
|
+
if (!isDebugEnabled()) return;
|
|
7
|
+
try {
|
|
8
|
+
appendDebugLogLine(line);
|
|
9
|
+
console.error(line);
|
|
10
|
+
} catch {
|
|
11
|
+
/* diagnostics must never affect request handling */
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Opt-in provider diagnostics. Streaming adapters stay quiet unless provider debug is on
|
|
16
|
+
// (`ocx debug provider on`, GUI Logs toggle, or OCX_DEBUG=1). Tail with `ocx debug provider logs -f`.
|
|
17
|
+
|
|
18
|
+
export function debugDroppedFrame(adapter: string, payload: string): void {
|
|
19
|
+
if (!isDebugEnabled()) return;
|
|
20
|
+
emitDebugLine(`[ocx:frame-drop] ${adapter}: dropped malformed upstream frame (payload redacted, bytes=${payload.length})`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Provider-agnostic diagnostic logging: `[ocx:<adapter>:<event>] {...}`. */
|
|
24
|
+
export function debugProviderDiagnostic(adapter: string, event: string, details: Record<string, unknown>): void {
|
|
25
|
+
if (!isDebugEnabled()) return;
|
|
26
|
+
try {
|
|
27
|
+
emitDebugLine(`[ocx:${adapter}:${event}] ${JSON.stringify(redactSecrets(details))}`);
|
|
28
|
+
} catch {
|
|
29
|
+
/* diagnostics must never affect request handling */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { getProviderRegistryEntry } from "../providers/registry";
|
|
4
|
+
import type { OcxProviderConfig } from "../types";
|
|
5
|
+
|
|
6
|
+
const BLOCKED_METADATA_HOSTS = new Set([
|
|
7
|
+
"instance-data.ec2.internal",
|
|
8
|
+
"metadata.azure.internal",
|
|
9
|
+
"metadata.google.internal",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const BLOCKED_METADATA_IPV4 = new Set([
|
|
13
|
+
"100.100.100.200",
|
|
14
|
+
"169.254.169.254",
|
|
15
|
+
"169.254.170.2",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const BLOCKED_METADATA_IPV6 = new Set([
|
|
19
|
+
"fd00:ec2::254",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export type DestinationKind =
|
|
23
|
+
| "public"
|
|
24
|
+
| "hostname"
|
|
25
|
+
| "localhost"
|
|
26
|
+
| "loopback"
|
|
27
|
+
| "private"
|
|
28
|
+
| "link-local"
|
|
29
|
+
| "unspecified"
|
|
30
|
+
| "metadata";
|
|
31
|
+
|
|
32
|
+
interface DestinationAssessment {
|
|
33
|
+
kind: DestinationKind;
|
|
34
|
+
detail: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeHostname(hostname: string): string {
|
|
38
|
+
const trimmed = hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
39
|
+
return trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseIpv4(hostname: string): number[] | null {
|
|
43
|
+
const parts = hostname.split(".");
|
|
44
|
+
if (parts.length !== 4) return null;
|
|
45
|
+
const octets = parts.map(part => Number(part));
|
|
46
|
+
return octets.every(octet => Number.isInteger(octet) && octet >= 0 && octet <= 255) ? octets : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function classifyIpv4(hostname: string): DestinationAssessment {
|
|
50
|
+
if (BLOCKED_METADATA_IPV4.has(hostname)) return { kind: "metadata", detail: "blocked metadata endpoint" };
|
|
51
|
+
const octets = parseIpv4(hostname);
|
|
52
|
+
if (!octets) return { kind: "public", detail: "public IP" };
|
|
53
|
+
const [a, b, c] = octets;
|
|
54
|
+
if (a === 127) return { kind: "loopback", detail: "loopback address" };
|
|
55
|
+
if (a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127)) {
|
|
56
|
+
return { kind: "private", detail: "private-network address" };
|
|
57
|
+
}
|
|
58
|
+
if (a === 169 && b === 254) return { kind: "link-local", detail: "link-local address" };
|
|
59
|
+
if (a === 0) return { kind: "unspecified", detail: "unspecified address" };
|
|
60
|
+
// Reserved / non-public ranges (review finding, PR #96): protocol-assignment,
|
|
61
|
+
// documentation, benchmark, multicast, and reserved-future space never name a
|
|
62
|
+
// legitimate provider endpoint.
|
|
63
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2)) return { kind: "private", detail: "reserved address" };
|
|
64
|
+
if (a === 198 && (b === 18 || b === 19)) return { kind: "private", detail: "benchmark address" };
|
|
65
|
+
if (a === 198 && b === 51 && c === 100) return { kind: "private", detail: "documentation address" };
|
|
66
|
+
if (a === 203 && b === 0 && c === 113) return { kind: "private", detail: "documentation address" };
|
|
67
|
+
if (a >= 224) return { kind: "private", detail: "multicast/reserved address" };
|
|
68
|
+
return { kind: "public", detail: "public IP" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function firstIpv6Hextet(hostname: string): number | null {
|
|
72
|
+
const head = hostname.split(":")[0];
|
|
73
|
+
if (!head) return 0;
|
|
74
|
+
const parsed = Number.parseInt(head, 16);
|
|
75
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function classifyIpv6(hostname: string): DestinationAssessment {
|
|
79
|
+
if (BLOCKED_METADATA_IPV6.has(hostname)) return { kind: "metadata", detail: "blocked metadata endpoint" };
|
|
80
|
+
const mappedIpv4 = hostname.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i)?.[1];
|
|
81
|
+
if (mappedIpv4) return classifyIpv4(mappedIpv4);
|
|
82
|
+
// Decode hex IPv4-mapped IPv6: ::ffff:7f00:1 → 127.0.0.1
|
|
83
|
+
// The dotted-decimal regex above only matches ::ffff:127.0.0.1; without this,
|
|
84
|
+
// hex form bypasses all private/loopback checks (hextet is 0 → classified "public").
|
|
85
|
+
const hexMapped = hostname.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
86
|
+
if (hexMapped) {
|
|
87
|
+
const hi = Number.parseInt(hexMapped[1], 16);
|
|
88
|
+
const lo = Number.parseInt(hexMapped[2], 16);
|
|
89
|
+
const ipv4 = `${(hi >> 8) & 255}.${hi & 255}.${(lo >> 8) & 255}.${lo & 255}`;
|
|
90
|
+
return classifyIpv4(ipv4);
|
|
91
|
+
}
|
|
92
|
+
if (hostname === "::1") return { kind: "loopback", detail: "loopback address" };
|
|
93
|
+
if (hostname === "::") return { kind: "unspecified", detail: "unspecified address" };
|
|
94
|
+
const hextet = firstIpv6Hextet(hostname);
|
|
95
|
+
if (hextet === null) return { kind: "private", detail: "non-global address" };
|
|
96
|
+
// Multicast ff00::/8, deprecated site-local fec0::/10, ULA fc00::/7, link-local fe80::/10.
|
|
97
|
+
if (hextet >= 0xff00) return { kind: "private", detail: "multicast address" };
|
|
98
|
+
if (hextet >= 0xfe80 && hextet <= 0xfebf) return { kind: "link-local", detail: "link-local address" };
|
|
99
|
+
if (hextet >= 0xfec0 && hextet <= 0xfeff) return { kind: "private", detail: "site-local address" };
|
|
100
|
+
if (hextet >= 0xfc00 && hextet <= 0xfdff) return { kind: "private", detail: "private-network address" };
|
|
101
|
+
// Documentation 2001:db8::/32 (inside global-unicast 2000::/3).
|
|
102
|
+
if (hextet === 0x2001) {
|
|
103
|
+
const second = Number.parseInt(hostname.split(":")[1] || "0", 16);
|
|
104
|
+
if (second === 0xdb8) return { kind: "private", detail: "documentation address" };
|
|
105
|
+
}
|
|
106
|
+
// Only global unicast 2000::/3 is treated as a public image/CDN peer.
|
|
107
|
+
if (hextet >= 0x2000 && hextet <= 0x3fff) return { kind: "public", detail: "public IP" };
|
|
108
|
+
return { kind: "private", detail: "non-global address" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assessDestination(baseUrl: string): DestinationAssessment | null {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = new URL(baseUrl.trim());
|
|
114
|
+
const hostname = normalizeHostname(parsed.hostname);
|
|
115
|
+
if (!hostname) return null;
|
|
116
|
+
if (BLOCKED_METADATA_HOSTS.has(hostname)) return { kind: "metadata", detail: "blocked metadata endpoint" };
|
|
117
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
118
|
+
return { kind: "localhost", detail: "localhost destination" };
|
|
119
|
+
}
|
|
120
|
+
const ipKind = isIP(hostname);
|
|
121
|
+
if (ipKind === 4) return classifyIpv4(hostname);
|
|
122
|
+
if (ipKind === 6) return classifyIpv6(hostname);
|
|
123
|
+
return { kind: "hostname", detail: "hostname destination" };
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function registryAllowsPrivateNetwork(name: string): boolean {
|
|
130
|
+
return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether a provider may reach loopback/private addresses.
|
|
135
|
+
*
|
|
136
|
+
* Two sources, and both have to be consulted at every boundary: the operator's explicit
|
|
137
|
+
* `allowPrivateNetwork`, and the registry's `allowPrivateNetworkByDefault` for entries that are
|
|
138
|
+
* local BY DEFINITION (Ollama, vLLM, LM Studio, LiteLLM). Config validation already read both;
|
|
139
|
+
* outbound discovery read only the first, so a stock Ollama entry passed validation and was then
|
|
140
|
+
* refused at the fetch (#758).
|
|
141
|
+
*
|
|
142
|
+
* This grants nothing new. Metadata, link-local and unspecified destinations are rejected before
|
|
143
|
+
* this is consulted, and a provider without either source still cannot reach a private address.
|
|
144
|
+
*/
|
|
145
|
+
export function providerAllowsPrivateNetwork(
|
|
146
|
+
name: string,
|
|
147
|
+
provider: Pick<OcxProviderConfig, "allowPrivateNetwork">,
|
|
148
|
+
): boolean {
|
|
149
|
+
return provider.allowPrivateNetwork === true || registryAllowsPrivateNetwork(name);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function providerDestinationConfigError(name: string, provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">): string | null {
|
|
153
|
+
const assessment = assessDestination(provider.baseUrl);
|
|
154
|
+
if (!assessment) return null;
|
|
155
|
+
if (assessment.kind === "public" || assessment.kind === "hostname") return null;
|
|
156
|
+
if (assessment.kind === "metadata") return "baseUrl targets a blocked metadata endpoint";
|
|
157
|
+
if (providerAllowsPrivateNetwork(name, provider)) return null;
|
|
158
|
+
return `baseUrl points to a ${assessment.detail}; set allowPrivateNetwork:true only for intentionally local/self-hosted providers`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function assertProviderDestinationAllowed(name: string, provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">): void {
|
|
162
|
+
const error = providerDestinationConfigError(name, provider);
|
|
163
|
+
if (error) throw new Error(`provider ${name} ${error}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Async companion to {@link providerDestinationConfigError} for hostname destinations:
|
|
168
|
+
* resolves A/AAAA records and classifies every address, so a hostname that points at
|
|
169
|
+
* loopback/private/metadata space is caught at provider write time (review finding,
|
|
170
|
+
* PR #96 — the sync path must stay literal-only because the router hot path and
|
|
171
|
+
* config load are synchronous). DNS failures return null: config-time validation is
|
|
172
|
+
* advisory and must not hard-fail offline startups. DNS rebinding after validation is
|
|
173
|
+
* a recorded residual for this loopback proxy (devlog 260712_pr_batch_landing 000).
|
|
174
|
+
*
|
|
175
|
+
* `allowBenchmarkAddresses` is only for the exact canonical ChatGPT Codex seed under
|
|
176
|
+
* Clash fake-IP DNS (198.18.0.0/15). Every other non-public answer — including mixed
|
|
177
|
+
* benchmark + private/metadata sets — still fails.
|
|
178
|
+
*/
|
|
179
|
+
export async function providerDestinationResolvedError(
|
|
180
|
+
name: string,
|
|
181
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">,
|
|
182
|
+
options?: { allowBenchmarkAddresses?: boolean },
|
|
183
|
+
): Promise<string | null> {
|
|
184
|
+
const syncError = providerDestinationConfigError(name, provider);
|
|
185
|
+
if (syncError) return syncError;
|
|
186
|
+
let hostname: string;
|
|
187
|
+
try {
|
|
188
|
+
hostname = normalizeHostname(new URL(provider.baseUrl.trim()).hostname);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!hostname || isIP(hostname) !== 0 || hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
193
|
+
return null; // literals and localhost are fully handled by the sync path
|
|
194
|
+
}
|
|
195
|
+
if (providerAllowsPrivateNetwork(name, provider)) return null;
|
|
196
|
+
let addresses: { address: string }[];
|
|
197
|
+
try {
|
|
198
|
+
addresses = await lookup(hostname, { all: true, verbatim: true });
|
|
199
|
+
} catch {
|
|
200
|
+
return null; // unresolvable now ≠ malicious; the provider simply won't connect
|
|
201
|
+
}
|
|
202
|
+
for (const { address } of addresses) {
|
|
203
|
+
const ipKind = isIP(address);
|
|
204
|
+
const assessment = ipKind === 4 ? classifyIpv4(address) : ipKind === 6 ? classifyIpv6(normalizeHostname(address)) : null;
|
|
205
|
+
if (!assessment || assessment.kind === "public") continue;
|
|
206
|
+
// Clash fake-IP only: 198.18/19 benchmark detail. Mixed dangerous sets still reject.
|
|
207
|
+
if (
|
|
208
|
+
options?.allowBenchmarkAddresses
|
|
209
|
+
&& assessment.kind === "private"
|
|
210
|
+
&& assessment.detail === "benchmark address"
|
|
211
|
+
) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (assessment.kind === "metadata") return `baseUrl hostname ${hostname} resolves to a blocked metadata endpoint (${address})`;
|
|
215
|
+
return `baseUrl hostname ${hostname} resolves to a ${assessment.detail} (${address}); set allowPrivateNetwork:true only for intentionally local/self-hosted providers`;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface UrlDestinationAssessment {
|
|
221
|
+
kind: DestinationKind;
|
|
222
|
+
detail: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class DestinationDnsResolutionError extends Error {
|
|
226
|
+
override readonly name = "DestinationDnsResolutionError";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Synchronous literal URL destination assessment — classifies the hostname
|
|
231
|
+
* without DNS resolution. Returns null for unparseable URLs.
|
|
232
|
+
*/
|
|
233
|
+
export function assessUrlDestination(url: string): UrlDestinationAssessment | null {
|
|
234
|
+
return assessDestination(url);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Async DNS-resolved URL safety check. By default, rejects every non-public
|
|
239
|
+
* address. Provider diagnostics may explicitly admit loopback/private answers;
|
|
240
|
+
* metadata, link-local, and unspecified addresses remain unconditionally denied.
|
|
241
|
+
* Returns the validated addresses so direct callers can pin the connect peer and
|
|
242
|
+
* avoid a second, rebindable resolution. DNS failures remain fail-closed here;
|
|
243
|
+
* the provider proxy wrapper alone may recognize that typed failure and degrade.
|
|
244
|
+
*/
|
|
245
|
+
export async function resolvePublicAddresses(
|
|
246
|
+
url: string,
|
|
247
|
+
options?: string | { context?: string; allowPrivateNetwork?: boolean },
|
|
248
|
+
): Promise<{
|
|
249
|
+
hostname: string;
|
|
250
|
+
addresses: { address: string; family: number }[];
|
|
251
|
+
privateNetwork: boolean;
|
|
252
|
+
}> {
|
|
253
|
+
const context = typeof options === "string"
|
|
254
|
+
? `${options.trim() || "image"} URL`
|
|
255
|
+
: options?.context?.trim() || "image URL";
|
|
256
|
+
const privateNetworkAllowed = typeof options === "object" && options?.allowPrivateNetwork === true;
|
|
257
|
+
let hostname: string;
|
|
258
|
+
try {
|
|
259
|
+
hostname = normalizeHostname(new URL(url.trim()).hostname);
|
|
260
|
+
} catch {
|
|
261
|
+
throw new Error(`${context} is not a valid URL`);
|
|
262
|
+
}
|
|
263
|
+
if (!hostname) throw new Error(`${context} has no hostname`);
|
|
264
|
+
const literalAssessment = assessDestination(url);
|
|
265
|
+
let privateNetwork = false;
|
|
266
|
+
if (literalAssessment && literalAssessment.kind !== "public" && literalAssessment.kind !== "hostname") {
|
|
267
|
+
const allowedPrivateLiteral = privateNetworkAllowed
|
|
268
|
+
&& (literalAssessment.kind === "localhost"
|
|
269
|
+
|| literalAssessment.kind === "loopback"
|
|
270
|
+
|| literalAssessment.kind === "private");
|
|
271
|
+
if (!allowedPrivateLiteral) throw new Error(`${context} targets ${literalAssessment.detail}`);
|
|
272
|
+
privateNetwork = true;
|
|
273
|
+
}
|
|
274
|
+
// Literal public IPs: no DNS round-trip; pin the literal itself.
|
|
275
|
+
const literalKind = isIP(hostname);
|
|
276
|
+
if (literalKind !== 0) {
|
|
277
|
+
return { hostname, addresses: [{ address: hostname, family: literalKind }], privateNetwork };
|
|
278
|
+
}
|
|
279
|
+
let addresses: { address: string; family: number }[];
|
|
280
|
+
try {
|
|
281
|
+
addresses = await lookup(hostname, { all: true, verbatim: true });
|
|
282
|
+
} catch {
|
|
283
|
+
// If DNS fails, we can't verify — fail-closed (unlike provider config-time validation,
|
|
284
|
+
// this is a runtime fetch to an untrusted URL, so be conservative).
|
|
285
|
+
throw new DestinationDnsResolutionError(`${context} hostname ${hostname} could not be resolved`);
|
|
286
|
+
}
|
|
287
|
+
if (addresses.length === 0) {
|
|
288
|
+
throw new DestinationDnsResolutionError(`${context} hostname ${hostname} could not be resolved`);
|
|
289
|
+
}
|
|
290
|
+
const validatedAddresses: { address: string; family: number }[] = [];
|
|
291
|
+
for (const { address, family } of addresses) {
|
|
292
|
+
// Prefer classifying from the address string itself — do not trust a mislabeled
|
|
293
|
+
// resolver `family` that could skip IPv4/IPv6 private checks.
|
|
294
|
+
const ipKind = isIP(address) || (family === 4 || family === 6 ? family : 0);
|
|
295
|
+
const assessment = ipKind === 4 ? classifyIpv4(address) : ipKind === 6 ? classifyIpv6(normalizeHostname(address)) : null;
|
|
296
|
+
if (!assessment || assessment.kind !== "public") {
|
|
297
|
+
const allowedPrivateAddress = privateNetworkAllowed
|
|
298
|
+
&& assessment
|
|
299
|
+
&& (assessment.kind === "loopback" || assessment.kind === "private");
|
|
300
|
+
if (!allowedPrivateAddress) {
|
|
301
|
+
throw new Error(`${context} hostname ${hostname} resolves to ${assessment?.detail ?? "an unsafe address"} (${address})`);
|
|
302
|
+
}
|
|
303
|
+
privateNetwork = true;
|
|
304
|
+
}
|
|
305
|
+
validatedAddresses.push({ address, family: ipKind === 4 || ipKind === 6 ? ipKind : (family || 4) });
|
|
306
|
+
}
|
|
307
|
+
return { hostname, addresses: validatedAddresses, privateNetwork };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Void assertion wrapper around {@link resolvePublicAddresses} for call sites
|
|
312
|
+
* that only need the safety check.
|
|
313
|
+
*/
|
|
314
|
+
export async function assertUrlResolvesPublic(url: string): Promise<void> {
|
|
315
|
+
await resolvePublicAddresses(url);
|
|
316
|
+
}
|