@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,165 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { loadConfig, readRuntimePort } from "../config";
|
|
3
|
+
import { configuredAdminToken } from "./admin-secrets";
|
|
4
|
+
|
|
5
|
+
export function isProcessAlive(pid: number): boolean {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function waitForExit(pid: number, timeoutMs: number): boolean {
|
|
15
|
+
const deadline = Date.now() + timeoutMs;
|
|
16
|
+
const marker = new Int32Array(new SharedArrayBuffer(4));
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
if (!isProcessAlive(pid)) return true;
|
|
19
|
+
Atomics.wait(marker, 0, 0, 50);
|
|
20
|
+
}
|
|
21
|
+
return !isProcessAlive(pid);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Injectable seams so the graceful-stop flow is unit-testable without a live proxy. */
|
|
25
|
+
export interface GracefulStopIo {
|
|
26
|
+
fetchFn?: typeof fetch;
|
|
27
|
+
readRuntime?: (pid: number) => { port: number; hostname?: string } | null;
|
|
28
|
+
waitExit?: (pid: number, timeoutMs: number) => boolean;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
exitTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Host to POST /api/stop against: follow the recorded bind hostname when it names a
|
|
35
|
+
* concrete address (a proxy bound to ::1 or a LAN IP is unreachable on 127.0.0.1);
|
|
36
|
+
* loopback aliases and wildcard binds all answer on IPv4 loopback.
|
|
37
|
+
*/
|
|
38
|
+
export function gracefulStopHost(hostname: string | undefined): string {
|
|
39
|
+
const trimmed = (hostname ?? "").trim();
|
|
40
|
+
const lower = trimmed.toLowerCase();
|
|
41
|
+
if (!trimmed || lower === "localhost" || trimmed === "127.0.0.1" || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") {
|
|
42
|
+
return "127.0.0.1";
|
|
43
|
+
}
|
|
44
|
+
if (lower === "::1" || lower === "[::1]") return "[::1]";
|
|
45
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
46
|
+
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Outcome of a graceful stop attempt. `"refused"` is distinct from failure: the proxy answered
|
|
51
|
+
* that it must NOT be stopped from here, so callers must not escalate to a forced kill.
|
|
52
|
+
*/
|
|
53
|
+
export type GracefulStopResult = boolean | "refused";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ask a running proxy to stop itself via the management API (`POST /api/stop`), which
|
|
57
|
+
* drains in-flight turns, restores native Codex, and cleans its pid/runtime files.
|
|
58
|
+
* This is the only way to get a GRACEFUL stop on Windows, where the POSIX
|
|
59
|
+
* SIGTERM-then-SIGKILL ladder does not exist and `taskkill /F` gives the proxy no
|
|
60
|
+
* chance to run its shutdown handlers. Returns false when the proxy can't be reached
|
|
61
|
+
* or doesn't exit in time — callers fall back to {@link killProxy}. Returns `"refused"`
|
|
62
|
+
* when the proxy declines the stop (HTTP 409), which callers must NOT force past.
|
|
63
|
+
*/
|
|
64
|
+
export async function stopProxyGracefully(pid: number, io: GracefulStopIo = {}): Promise<GracefulStopResult> {
|
|
65
|
+
const readRuntime = io.readRuntime ?? readRuntimePort;
|
|
66
|
+
const runtime = readRuntime(pid);
|
|
67
|
+
if (!runtime?.port) return false;
|
|
68
|
+
const env = io.env ?? process.env;
|
|
69
|
+
const headers: Record<string, string> = {};
|
|
70
|
+
const token = configuredAdminToken(env.OPENCODEX_HOME?.trim() || undefined, env as NodeJS.ProcessEnv);
|
|
71
|
+
if (token) headers["x-opencodex-api-key"] = token;
|
|
72
|
+
const fetchFn = io.fetchFn ?? fetch;
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetchFn(`http://${gracefulStopHost(runtime.hostname)}:${runtime.port}/api/stop`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers,
|
|
77
|
+
// Hung proxies with many CLOSE_WAIT clients can be slow to accept; give them
|
|
78
|
+
// longer than a health poll so we prefer drain over taskkill /F.
|
|
79
|
+
signal: AbortSignal.timeout(io.exitTimeoutMs ? Math.min(io.exitTimeoutMs, 10_000) : 10_000),
|
|
80
|
+
});
|
|
81
|
+
// 409 is the proxy REFUSING to stop (a service installed under another home owns it and
|
|
82
|
+
// would respawn it anyway). That is a policy answer, not a dead endpoint — escalating to
|
|
83
|
+
// SIGTERM here would run the daemon's cleanup and strip shared config out from under the
|
|
84
|
+
// still-running service. Report the refusal instead of forcing.
|
|
85
|
+
if (res.status === 409) return "refused";
|
|
86
|
+
if (!res.ok) return false;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const waitExit = io.waitExit ?? waitForExit;
|
|
91
|
+
// Honor the server's own drain window: /api/stop answers 200 first, then drains for
|
|
92
|
+
// config.shutdownTimeoutMs. Waiting less than that hard-kills mid-drain.
|
|
93
|
+
const exitTimeoutMs = io.exitTimeoutMs ?? drainDeadlineMs();
|
|
94
|
+
return waitExit(pid, exitTimeoutMs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function drainDeadlineMs(): number {
|
|
98
|
+
try {
|
|
99
|
+
return (loadConfig().shutdownTimeoutMs ?? 5000) + 3000;
|
|
100
|
+
} catch {
|
|
101
|
+
return 8000;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Graceful-first stop: management-API drain, then the platform kill ladder. */
|
|
106
|
+
export async function stopProxy(pid: number): Promise<void> {
|
|
107
|
+
if (!isProcessAlive(pid)) return;
|
|
108
|
+
const runtime = readRuntimePort(pid);
|
|
109
|
+
const graceful = await stopProxyGracefully(pid);
|
|
110
|
+
if (graceful === "refused") {
|
|
111
|
+
// The proxy refused on purpose (foreign service owns it). Forcing would strip shared
|
|
112
|
+
// config while that service keeps the proxy alive.
|
|
113
|
+
throw new Error(
|
|
114
|
+
"The running proxy refused to stop: a service installed under a different "
|
|
115
|
+
+ "CODEX_HOME/OPENCODEX_HOME owns it. Run the stop from that home.",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (graceful) {
|
|
119
|
+
await waitForStoppedPort(runtime, pid);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
killProxy(pid);
|
|
123
|
+
await waitForStoppedPort(runtime, pid);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** After stop/kill, wait for the former listen port to become bindable (Windows drain). */
|
|
127
|
+
async function waitForStoppedPort(
|
|
128
|
+
runtime: { port: number; hostname?: string } | null | undefined,
|
|
129
|
+
stoppedPid?: number,
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
if (!runtime?.port) return;
|
|
132
|
+
try {
|
|
133
|
+
const { reclaimListenPort } = await import("../server/port-reclaim");
|
|
134
|
+
await reclaimListenPort(runtime.port, runtime.hostname ?? "127.0.0.1", {
|
|
135
|
+
timeoutMs: 15_000,
|
|
136
|
+
intervalMs: 100,
|
|
137
|
+
scanIntervalMs: 500,
|
|
138
|
+
// Only the process we just stopped — never kill a newly started twin proxy.
|
|
139
|
+
killOcxHolders: !!(stoppedPid && stoppedPid > 0),
|
|
140
|
+
onlyKillPids: stoppedPid && stoppedPid > 0 ? [stoppedPid] : [],
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
/* best-effort — callers that need a hard guarantee reclaim again before bind */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function killProxy(pid: number): void {
|
|
148
|
+
if (!isProcessAlive(pid)) return;
|
|
149
|
+
if (process.platform === "win32") {
|
|
150
|
+
// Windows process.kill(SIGTERM/SIGINT) is TerminateProcess — not a graceful signal.
|
|
151
|
+
// Graceful drain happens only via stopProxyGracefully() (POST /api/stop). This path
|
|
152
|
+
// is the hard fallback: taskkill /T /F so the process tree exits (ghost LISTEN /
|
|
153
|
+
// CLOSE_WAIT are then cleared by reclaimListenPort / SetTcpEntry).
|
|
154
|
+
const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
|
|
155
|
+
try {
|
|
156
|
+
execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], { stdio: "pipe", windowsHide: true });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (isProcessAlive(pid)) throw err;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
process.kill(pid, "SIGTERM");
|
|
162
|
+
if (!waitForExit(pid, 5000)) process.kill(pid, "SIGKILL");
|
|
163
|
+
}
|
|
164
|
+
if (!waitForExit(pid, 5000)) throw new Error(`process ${pid} did not exit`);
|
|
165
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
assessUrlDestination,
|
|
4
|
+
DestinationDnsResolutionError,
|
|
5
|
+
providerAllowsPrivateNetwork,
|
|
6
|
+
providerDestinationConfigError,
|
|
7
|
+
resolvePublicAddresses,
|
|
8
|
+
} from "./destination-policy";
|
|
9
|
+
import { pinnedHttpGet } from "./pinned-http";
|
|
10
|
+
import { outboundProxyConfigured } from "./proxy-env";
|
|
11
|
+
import { publicProviderBaseUrl } from "./provider-url";
|
|
12
|
+
|
|
13
|
+
type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
|
|
14
|
+
type ProviderOutboundConfig = Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"> & {
|
|
15
|
+
fetch?: typeof globalThis.fetch;
|
|
16
|
+
};
|
|
17
|
+
export interface ProviderOutboundDependencies {
|
|
18
|
+
resolveAddresses?: typeof resolvePublicAddresses;
|
|
19
|
+
pinnedGet?: typeof pinnedHttpGet;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ProviderOutboundPolicyError extends Error {
|
|
23
|
+
override readonly name = "ProviderOutboundPolicyError";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pickPinnedAddress(addresses: Array<{ address: string; family: number }>): { address: string; family: number } {
|
|
27
|
+
return addresses.find(address => address.family === 4) ?? addresses[0]!;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function configuredProxyFor(): boolean {
|
|
31
|
+
return outboundProxyConfigured();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeProxyHostname(hostname: string): string {
|
|
35
|
+
const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
36
|
+
return normalized.startsWith("[") && normalized.endsWith("]")
|
|
37
|
+
? normalized.slice(1, -1)
|
|
38
|
+
: normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function noProxyMatches(url: URL): boolean {
|
|
42
|
+
const raw = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
43
|
+
const hostname = normalizeProxyHostname(url.hostname);
|
|
44
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
45
|
+
for (const rawEntry of raw.split(",")) {
|
|
46
|
+
let entry = rawEntry.trim().toLowerCase();
|
|
47
|
+
if (!entry) continue;
|
|
48
|
+
if (entry === "*") return true;
|
|
49
|
+
entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!;
|
|
50
|
+
|
|
51
|
+
let entryHost = entry;
|
|
52
|
+
let entryPort = "";
|
|
53
|
+
const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry);
|
|
54
|
+
if (bracketed) {
|
|
55
|
+
entryHost = bracketed[1]!;
|
|
56
|
+
entryPort = bracketed[2] ?? "";
|
|
57
|
+
} else if ((entry.match(/:/g)?.length ?? 0) === 1) {
|
|
58
|
+
const separator = entry.lastIndexOf(":");
|
|
59
|
+
const possiblePort = entry.slice(separator + 1);
|
|
60
|
+
if (/^\d+$/.test(possiblePort)) {
|
|
61
|
+
entryHost = entry.slice(0, separator);
|
|
62
|
+
entryPort = possiblePort;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (entryPort && entryPort !== port) continue;
|
|
66
|
+
entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, ""));
|
|
67
|
+
if (!entryHost) continue;
|
|
68
|
+
if (hostname === entryHost || hostname.endsWith(`.${entryHost}`)) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let proxyBoundaryWarned = false;
|
|
74
|
+
let proxyDnsDegradationWarned = false;
|
|
75
|
+
|
|
76
|
+
function warnProxyBoundaryOnce(): void {
|
|
77
|
+
if (proxyBoundaryWarned) return;
|
|
78
|
+
proxyBoundaryWarned = true;
|
|
79
|
+
console.warn(
|
|
80
|
+
"[opencodex] Provider outbound proxy mode preserves Bun proxy/NO_PROXY routing and validates "
|
|
81
|
+
+ "the URL plus available local DNS results; the final route and peer cannot be pinned locally.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function warnProxyDnsDegradationOnce(): void {
|
|
86
|
+
if (proxyDnsDegradationWarned) return;
|
|
87
|
+
proxyDnsDegradationWarned = true;
|
|
88
|
+
console.warn(
|
|
89
|
+
"[opencodex] Local DNS could not resolve a proxied provider hostname; continuing after URL/literal checks. "
|
|
90
|
+
+ "The proxy-selected peer cannot be verified or pinned locally.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function providerRedirectError(response: Response, requestUrl: string): Promise<string | null> {
|
|
95
|
+
if (response.status < 300 || response.status >= 400) return null;
|
|
96
|
+
try { await response.body?.cancel(); } catch { /* ignore cancellation failures */ }
|
|
97
|
+
const location = response.headers.get("location");
|
|
98
|
+
let target = "the final upstream URL";
|
|
99
|
+
if (location) {
|
|
100
|
+
try { target = publicProviderBaseUrl(new URL(location, requestUrl).toString()); } catch { /* keep fallback */ }
|
|
101
|
+
}
|
|
102
|
+
return `provider returned ${response.status} redirect to ${target}; configure the final provider URL directly`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function providerOutboundGet(
|
|
106
|
+
name: string,
|
|
107
|
+
provider: ProviderOutboundConfig,
|
|
108
|
+
url: string,
|
|
109
|
+
init: ProviderGetInit = {},
|
|
110
|
+
dependencies: ProviderOutboundDependencies = {},
|
|
111
|
+
): Promise<Response> {
|
|
112
|
+
if (provider.fetch) {
|
|
113
|
+
// A caller-owned executor cannot be peer-pinned here. This branch keeps literal/config
|
|
114
|
+
// checks and redirect blocking, but does not provide the resolved-address guarantees of
|
|
115
|
+
// the built-in transport. Main-request migration must define that executor contract first.
|
|
116
|
+
const assessment = assessUrlDestination(url);
|
|
117
|
+
if (assessment?.kind === "metadata" || assessment?.kind === "link-local" || assessment?.kind === "unspecified") {
|
|
118
|
+
throw new ProviderOutboundPolicyError(`provider URL targets ${assessment.detail}`);
|
|
119
|
+
}
|
|
120
|
+
// Registry defaults count here too, not just the operator flag: a stock Ollama entry is
|
|
121
|
+
// local by definition and previously passed config validation only to be refused at the
|
|
122
|
+
// fetch (#758). Metadata/link-local/unspecified were already rejected above.
|
|
123
|
+
const allowPrivate = providerAllowsPrivateNetwork(name, provider);
|
|
124
|
+
if (!allowPrivate) {
|
|
125
|
+
const destinationError = providerDestinationConfigError(name, {
|
|
126
|
+
baseUrl: url,
|
|
127
|
+
allowPrivateNetwork: false,
|
|
128
|
+
});
|
|
129
|
+
if (destinationError) throw new ProviderOutboundPolicyError(destinationError);
|
|
130
|
+
}
|
|
131
|
+
return provider.fetch(url, { ...init, method: "GET", redirect: "manual" });
|
|
132
|
+
}
|
|
133
|
+
const parsed = new URL(url);
|
|
134
|
+
const proxyConfigured = configuredProxyFor();
|
|
135
|
+
const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
|
|
136
|
+
const pinnedGet = dependencies.pinnedGet ?? pinnedHttpGet;
|
|
137
|
+
const allowPrivate = providerAllowsPrivateNetwork(name, provider);
|
|
138
|
+
let resolved: Awaited<ReturnType<typeof resolvePublicAddresses>>;
|
|
139
|
+
try {
|
|
140
|
+
resolved = await resolveAddresses(url, {
|
|
141
|
+
context: "provider URL",
|
|
142
|
+
allowPrivateNetwork: allowPrivate,
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const dnsResolutionFailed = error instanceof DestinationDnsResolutionError
|
|
146
|
+
|| (error instanceof Error && error.name === "DestinationDnsResolutionError");
|
|
147
|
+
if (!dnsResolutionFailed) {
|
|
148
|
+
throw new ProviderOutboundPolicyError(error instanceof Error ? error.message : "provider destination was blocked");
|
|
149
|
+
}
|
|
150
|
+
if (!proxyConfigured) throw error;
|
|
151
|
+
warnProxyBoundaryOnce();
|
|
152
|
+
warnProxyDnsDegradationOnce();
|
|
153
|
+
return globalThis.fetch(url, { ...init, method: "GET", redirect: "manual" });
|
|
154
|
+
}
|
|
155
|
+
if (proxyConfigured && !resolved.privateNetwork) {
|
|
156
|
+
warnProxyBoundaryOnce();
|
|
157
|
+
return globalThis.fetch(url, { ...init, method: "GET", redirect: "manual" });
|
|
158
|
+
}
|
|
159
|
+
if (proxyConfigured && resolved.privateNetwork && !noProxyMatches(parsed)) {
|
|
160
|
+
const hostname = normalizeProxyHostname(parsed.hostname);
|
|
161
|
+
throw new Error(
|
|
162
|
+
`provider URL resolves to a private-network destination; add ${hostname} to NO_PROXY before using allowPrivateNetwork with an outbound proxy`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return pinnedGet(url, pickPinnedAddress(resolved.addresses), init.signal ?? undefined, {
|
|
166
|
+
headers: init.headers,
|
|
167
|
+
rejectUnauthorized: true,
|
|
168
|
+
context: "provider response",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Strip credentials and non-routing URL components before displaying a provider URL. */
|
|
2
|
+
export function publicProviderBaseUrl(baseUrl: string): string {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(baseUrl.trim());
|
|
5
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "(invalid URL)";
|
|
6
|
+
parsed.username = "";
|
|
7
|
+
parsed.password = "";
|
|
8
|
+
parsed.search = "";
|
|
9
|
+
parsed.hash = "";
|
|
10
|
+
return parsed.toString().replace(/\/$/, baseUrl.endsWith("/") ? "/" : "");
|
|
11
|
+
} catch {
|
|
12
|
+
return "(invalid URL)";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const OUTBOUND_PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"] as const;
|
|
2
|
+
export const PROXY_ENV_KEYS = [...OUTBOUND_PROXY_ENV_KEYS, "NO_PROXY"] as const;
|
|
3
|
+
|
|
4
|
+
export type ProxyEnvKey = typeof PROXY_ENV_KEYS[number];
|
|
5
|
+
export type ProxyEnvMap = Record<string, string | undefined>;
|
|
6
|
+
|
|
7
|
+
export function proxyEnvPresent(
|
|
8
|
+
key: ProxyEnvKey,
|
|
9
|
+
env: ProxyEnvMap = process.env,
|
|
10
|
+
): boolean {
|
|
11
|
+
return Boolean(env[key]?.trim() || env[key.toLowerCase()]?.trim());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function outboundProxyConfigured(
|
|
15
|
+
env: ProxyEnvMap = process.env,
|
|
16
|
+
): boolean {
|
|
17
|
+
return OUTBOUND_PROXY_ENV_KEYS.some(key => proxyEnvPresent(key, env));
|
|
18
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const REDACTED_SECRET = "[REDACTED]";
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn)$/i;
|
|
4
|
+
|
|
5
|
+
const SECRET_VALUE_PATTERNS: Array<[RegExp, string]> = [
|
|
6
|
+
[/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi, `Bearer ${REDACTED_SECRET}`],
|
|
7
|
+
[/\b(sk-[A-Za-z0-9][A-Za-z0-9._-]{6,})\b/g, REDACTED_SECRET],
|
|
8
|
+
// GitHub tokens (classic + fine-grained + OAuth/refresh): ghp_/gho_/ghu_/ghs_/ghr_/github_pat_.
|
|
9
|
+
[/\b(gh[pousr]_[A-Za-z0-9_]{8,}|github_pat_[A-Za-z0-9_]{20,})\b/g, REDACTED_SECRET],
|
|
10
|
+
// GitHub Copilot API tokens: semicolon-joined k=v grammar starting with tid=…
|
|
11
|
+
// (e.g. "tid=abc123;exp=1699999999;sku=copilot_pro;…:sig"). Redact the whole token —
|
|
12
|
+
// a Bearer-prefix rule alone leaves the suffix intact.
|
|
13
|
+
[/\btid=[A-Za-z0-9-]+(?:;[A-Za-z0-9_.-]+=[^;\s"']*)+(?::[A-Za-z0-9+/=_-]+)?/g, REDACTED_SECRET],
|
|
14
|
+
[/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)=)([^&\s"',;]+)/gi, `$1${REDACTED_SECRET}`],
|
|
15
|
+
[/((?:"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$3`],
|
|
16
|
+
// Raw JSON "token" field values (Copilot token exchange bodies echo the credential here).
|
|
17
|
+
[/(("token"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$4`],
|
|
18
|
+
[/\b(arn:aws:[A-Za-z0-9_-]+:[A-Za-z0-9-]*:\d{12}:[A-Za-z0-9_/:+=,.@-]+)\b/g, REDACTED_SECRET],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
type HeaderRecord = Record<string, string | string[] | undefined>;
|
|
22
|
+
|
|
23
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
if (value === null || typeof value !== "object") return false;
|
|
25
|
+
const prototype = Object.getPrototypeOf(value);
|
|
26
|
+
return prototype === Object.prototype || prototype === null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isSensitiveKey(key: string): boolean {
|
|
30
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function redactSecretString(value: string): string {
|
|
34
|
+
let redacted = value;
|
|
35
|
+
for (const [pattern, replacement] of SECRET_VALUE_PATTERNS) {
|
|
36
|
+
redacted = redacted.replace(pattern, replacement);
|
|
37
|
+
}
|
|
38
|
+
return redacted;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function redactSecrets(value: unknown): unknown {
|
|
42
|
+
if (typeof value === "string") return redactSecretString(value);
|
|
43
|
+
if (Array.isArray(value)) return value.map(item => redactSecrets(item));
|
|
44
|
+
if (value instanceof Date) return value;
|
|
45
|
+
if (!isPlainObject(value)) return value;
|
|
46
|
+
|
|
47
|
+
const result: Record<string, unknown> = {};
|
|
48
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
49
|
+
result[key] = isSensitiveKey(key) ? REDACTED_SECRET : redactSecrets(entryValue);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function redactHeaders(headers: Headers | HeaderRecord): Record<string, string> {
|
|
55
|
+
const result: Record<string, string> = {};
|
|
56
|
+
const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers);
|
|
57
|
+
|
|
58
|
+
for (const [rawKey, rawValue] of entries) {
|
|
59
|
+
const key = rawKey.toLowerCase();
|
|
60
|
+
if (rawValue === undefined) continue;
|
|
61
|
+
const value = Array.isArray(rawValue) ? rawValue.join(", ") : String(rawValue);
|
|
62
|
+
result[key] = isSensitiveKey(key) ? REDACTED_SECRET : redactSecretString(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function redactUrlForLog(url: string): string {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = new URL(url);
|
|
71
|
+
parsed.username = "";
|
|
72
|
+
parsed.password = "";
|
|
73
|
+
parsed.search = "";
|
|
74
|
+
parsed.hash = "";
|
|
75
|
+
return parsed.toString();
|
|
76
|
+
} catch {
|
|
77
|
+
return redactSecretString(url.split("?")[0] ?? url);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const USER_HOME_PATH_PATTERNS: Array<[RegExp, string]> = [
|
|
82
|
+
// Windows: C:\Users\<name>\... -> C:\Users\[USER]\...
|
|
83
|
+
[/([A-Za-z]:\\Users\\)[^\\/]+/gi, "$1[USER]"],
|
|
84
|
+
// POSIX: /Users/<name>/... (macOS) and /home/<name>/... (Linux)
|
|
85
|
+
[/(\/(?:Users|home)\/)[^/]+/gi, "$1[USER]"],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Path segments whose name alone looks sensitive. Masked so a configured path
|
|
89
|
+
// cannot surface a secret-flavored substring in diagnostics or logs.
|
|
90
|
+
const SENSITIVE_SEGMENT_PATTERN = /(^|[\\/])([^\\/]*(?:secret|password|passwd|token|api[-_]?key|apikey|credential|email)[^\\/]*)(?=[\\/]|$)/gi;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Mask the username segment of an absolute home path so diagnostics can print
|
|
94
|
+
* paths without leaking the OS account name, and mask any path segment whose
|
|
95
|
+
* name looks sensitive (token/secret/password/credential/email/...). Path-focused
|
|
96
|
+
* and secret-safe: also runs {@link redactSecretString} for token-shaped values.
|
|
97
|
+
*/
|
|
98
|
+
export function redactUserPath(path: string): string {
|
|
99
|
+
let masked = path;
|
|
100
|
+
for (const [pattern, replacement] of USER_HOME_PATH_PATTERNS) {
|
|
101
|
+
masked = masked.replace(pattern, replacement);
|
|
102
|
+
}
|
|
103
|
+
masked = masked.replace(SENSITIVE_SEGMENT_PATTERN, (_m, sep: string) => `${sep}[REDACTED]`);
|
|
104
|
+
return redactSecretString(masked);
|
|
105
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseRetryAfterMs } from "../combos";
|
|
2
|
+
import { classifyError, parseRetryAfterFromMessage } from "./errors";
|
|
3
|
+
|
|
4
|
+
/** Small default when a retryable 429 has no upstream Retry-After (#507). */
|
|
5
|
+
export const DEFAULT_RETRYABLE_429_RETRY_AFTER_SEC = "2";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate a raw upstream Retry-After for client exposure.
|
|
9
|
+
* Instant-retry `"0"` is kept even though cooldown parsers reject it.
|
|
10
|
+
*/
|
|
11
|
+
export function validateClientRetryAfterHeader(
|
|
12
|
+
value: string | null | undefined,
|
|
13
|
+
now: number,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const trimmed = value?.trim();
|
|
16
|
+
if (!trimmed || trimmed.length > 128) return undefined;
|
|
17
|
+
// Instant-retry "0" is a valid client directive but rejected by cooldown parsers
|
|
18
|
+
// (parseRetryAfterMs requires seconds > 0). Preserve it for client headers.
|
|
19
|
+
if (trimmed === "0") return trimmed;
|
|
20
|
+
return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pick a Retry-After value that clients (especially Codex) can back off on.
|
|
25
|
+
*
|
|
26
|
+
* Order: validated upstream header → delay embedded in the error message →
|
|
27
|
+
* small default for retryable rate-limit 429s. Quota-exhausted 429s get no
|
|
28
|
+
* default — hammering a spent quota is not a backoff problem.
|
|
29
|
+
*
|
|
30
|
+
* Pass `includeDefault: false` when the value feeds cooldown/failover metadata:
|
|
31
|
+
* the synthetic client fallback must not shorten combo cooldowns from 60s to 2s.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveClientRetryAfter(opts: {
|
|
34
|
+
status: number;
|
|
35
|
+
message?: string;
|
|
36
|
+
upstreamRetryAfter?: string | null;
|
|
37
|
+
now?: number;
|
|
38
|
+
includeDefault?: boolean;
|
|
39
|
+
}): string | undefined {
|
|
40
|
+
const now = opts.now ?? Date.now();
|
|
41
|
+
const fromHeader = validateClientRetryAfterHeader(opts.upstreamRetryAfter, now);
|
|
42
|
+
if (fromHeader) return fromHeader;
|
|
43
|
+
|
|
44
|
+
const message = opts.message ?? "";
|
|
45
|
+
const fromMessage = parseRetryAfterFromMessage(message);
|
|
46
|
+
if (fromMessage !== undefined) return String(fromMessage);
|
|
47
|
+
|
|
48
|
+
if (opts.includeDefault === false) return undefined;
|
|
49
|
+
if (opts.status !== 429) return undefined;
|
|
50
|
+
const classified = classifyError(429, "rate_limit_error", message || "Too Many Requests");
|
|
51
|
+
if (classified.type === "insufficient_quota" || classified.code === "insufficient_quota") {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return DEFAULT_RETRYABLE_429_RETRY_AFTER_SEC;
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
|
|
5
|
+
export function serviceApiTokenFilePath(): string {
|
|
6
|
+
return join(getConfigDir(), "service-api-token");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* App-side service token loading (WinSW native mode has no batch wrapper to read the
|
|
11
|
+
* token file into the environment). Pure: returns the token or null — the CALLER
|
|
12
|
+
* assigns it to process.env.OPENCODEX_API_AUTH_TOKEN. Loads only when the env token
|
|
13
|
+
* is empty and OCX_API_TOKEN_FILE names a readable file.
|
|
14
|
+
*/
|
|
15
|
+
export function loadServiceTokenFromFile(env: Record<string, string | undefined>): string | null {
|
|
16
|
+
if (env.OPENCODEX_API_AUTH_TOKEN?.trim()) return null;
|
|
17
|
+
const file = env.OCX_API_TOKEN_FILE?.trim();
|
|
18
|
+
if (!file) return null;
|
|
19
|
+
try {
|
|
20
|
+
const token = readFileSync(file, "utf8").trim();
|
|
21
|
+
return token || null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-call intercept source models.
|
|
3
|
+
*
|
|
4
|
+
* Codex's hard-coded helper model is not stable across client versions: it was
|
|
5
|
+
* `gpt-5.4-mini` up to 0.144.x and became `gpt-5.6-luna` in 0.145.0. The
|
|
6
|
+
* intercept therefore matches a prefix SET, and every surface that names the
|
|
7
|
+
* intercepted model (management API, GUI badges/tooltips, CLI) reads it from
|
|
8
|
+
* here instead of hard-coding a slug that goes stale on the next client bump.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_SHADOW_SOURCE_MODELS = ["gpt-5.4-mini", "gpt-5.6-luna"] as const;
|
|
11
|
+
|
|
12
|
+
/** Normalize a persisted `sourceModels` override; falls back to the defaults. */
|
|
13
|
+
export function shadowSourceModels(configured?: unknown): string[] {
|
|
14
|
+
const configuredStrings = Array.isArray(configured)
|
|
15
|
+
? configured
|
|
16
|
+
.filter((v): v is string => typeof v === "string" && v.trim() !== "")
|
|
17
|
+
.map(v => v.trim())
|
|
18
|
+
: [];
|
|
19
|
+
return configuredStrings.length > 0 ? configuredStrings : [...DEFAULT_SHADOW_SOURCE_MODELS];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when `modelId` is one of Codex's helper/shadow source models.
|
|
24
|
+
* Routed ids (`provider/model`) are hard-excluded: a shadow call is always a
|
|
25
|
+
* bare native slug, and an explicit routed selection must never be hijacked.
|
|
26
|
+
*/
|
|
27
|
+
export function isShadowSourceModel(modelId: string, configured?: unknown): boolean {
|
|
28
|
+
if (modelId.includes("/")) return false;
|
|
29
|
+
return shadowSourceModels(configured).some(prefix => modelId.startsWith(prefix));
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight in-flight breadcrumb for sidecar work (web-search / vision), read by crash-guard.
|
|
3
|
+
*
|
|
4
|
+
* The proxy's unhandled rejections arrive with a native-only stack, so the throw site is invisible.
|
|
5
|
+
* To confirm or rule out the strong correlation with sidecar activity (the gpt-mini web-search /
|
|
6
|
+
* vision passes that run an EXTRA upstream fetch alongside the main turn), each sidecar call brackets
|
|
7
|
+
* itself with enter()/exit(). crash-guard then records whether any sidecar was in flight when a
|
|
8
|
+
* rejection fired — turning a guess into evidence without a debugger.
|
|
9
|
+
*/
|
|
10
|
+
let inFlight = 0;
|
|
11
|
+
let lastLabel = "";
|
|
12
|
+
let lastEnterAt = 0;
|
|
13
|
+
|
|
14
|
+
export function sidecarEnter(label: string): () => void {
|
|
15
|
+
inFlight++;
|
|
16
|
+
lastLabel = truncateRetainedUtf8(label, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
17
|
+
lastEnterAt = Date.now();
|
|
18
|
+
let exited = false;
|
|
19
|
+
return () => {
|
|
20
|
+
if (exited) return;
|
|
21
|
+
exited = true;
|
|
22
|
+
if (inFlight > 0) inFlight--;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sidecarBreadcrumb(): { inFlight: number; lastLabel: string; sinceMs: number } {
|
|
27
|
+
return {
|
|
28
|
+
inFlight,
|
|
29
|
+
lastLabel,
|
|
30
|
+
sinceMs: lastEnterAt ? Date.now() - lastEnterAt : 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generic last-activity breadcrumb (any code path, not just sidecars). The proxy's native-only
|
|
36
|
+
* rejections carry no source location, so we record the most recent meaningful activity —
|
|
37
|
+
* request path + a short note — to correlate the next fault with what the daemon was doing.
|
|
38
|
+
*/
|
|
39
|
+
let lastActivity = "";
|
|
40
|
+
let lastActivityAt = 0;
|
|
41
|
+
|
|
42
|
+
export function markActivity(note: string): void {
|
|
43
|
+
lastActivity = truncateRetainedUtf8(note, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
44
|
+
lastActivityAt = Date.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function activityBreadcrumb(): { note: string; sinceMs: number } {
|
|
48
|
+
return { note: lastActivity, sinceMs: lastActivityAt ? Date.now() - lastActivityAt : 0 };
|
|
49
|
+
}
|
|
50
|
+
import { truncateRetainedUtf8 } from "./admission";
|
|
51
|
+
|
|
52
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|