@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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic extended-thinking signature round-trip through Codex's `encrypted_content` slot.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic requires the previous assistant turn's `thinking`/`redacted_thinking` blocks to be
|
|
5
|
+
* replayed VERBATIM (with their signatures) while extended thinking is enabled; a signature-less
|
|
6
|
+
* replay 400s ("Expected `thinking` or `redacted_thinking`, but found `tool_use`"). Codex round-trips
|
|
7
|
+
* whatever `encrypted_content` a reasoning output item carries (include: reasoning.encrypted_content
|
|
8
|
+
* is set whenever reasoning is on — codex-rs client.rs), so the proxy smuggles the real Anthropic
|
|
9
|
+
* signature (and any redacted blocks) inside a transparent `ocxr1:` + base64(JSON) envelope.
|
|
10
|
+
*
|
|
11
|
+
* Native OpenAI-encrypted blobs (no ocxr1 prefix) are left untouched by the decoder, and the
|
|
12
|
+
* passthrough scrub strips ocxr1 envelopes before native forwarding.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const OCX_REASONING_PREFIX = "ocxr1:";
|
|
16
|
+
|
|
17
|
+
export interface ReasoningEnvelope {
|
|
18
|
+
/** Anthropic thinking-block signature (signature_delta), if captured. */
|
|
19
|
+
sig?: string;
|
|
20
|
+
/** Raw redacted_thinking block data payloads, order preserved. */
|
|
21
|
+
red?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Hidden thinking text (hideThinkingSummary providers): the signature signs this exact text,
|
|
24
|
+
* so replay needs it even though the visible summary was suppressed.
|
|
25
|
+
*/
|
|
26
|
+
txt?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function encodeReasoningEnvelope(envelope: ReasoningEnvelope): string {
|
|
30
|
+
return OCX_REASONING_PREFIX + Buffer.from(JSON.stringify(envelope), "utf-8").toString("base64");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Decode an ocxr1 envelope; returns null for native (OpenAI-encrypted) blobs or garbage. */
|
|
34
|
+
export function decodeReasoningEnvelope(encryptedContent: string): ReasoningEnvelope | null {
|
|
35
|
+
if (!encryptedContent.startsWith(OCX_REASONING_PREFIX)) return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed: unknown = JSON.parse(Buffer.from(encryptedContent.slice(OCX_REASONING_PREFIX.length), "base64").toString("utf-8"));
|
|
38
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
39
|
+
const obj = parsed as { sig?: unknown; red?: unknown };
|
|
40
|
+
const envelope: ReasoningEnvelope = {};
|
|
41
|
+
if (typeof obj.sig === "string") envelope.sig = obj.sig;
|
|
42
|
+
if (Array.isArray(obj.red)) {
|
|
43
|
+
const red = obj.red.filter((r): r is string => typeof r === "string");
|
|
44
|
+
if (red.length > 0) envelope.red = red;
|
|
45
|
+
}
|
|
46
|
+
const txt = (parsed as { txt?: unknown }).txt;
|
|
47
|
+
if (typeof txt === "string" && txt.length > 0) envelope.txt = txt;
|
|
48
|
+
return envelope.sig || envelope.red || envelope.txt ? envelope : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
|
|
3
|
+
const inputTextSchema = z.object({ type: z.literal("input_text"), text: z.string() });
|
|
4
|
+
const plainTextSchema = z.object({ type: z.literal("text"), text: z.string() });
|
|
5
|
+
const inputImageBlockSchema = z.object({
|
|
6
|
+
type: z.literal("input_image"),
|
|
7
|
+
// codex-rs ImageDetail: auto|low|high|original (view_image --detail original).
|
|
8
|
+
detail: z.enum(["auto", "low", "high", "original"]).optional(),
|
|
9
|
+
image_url: z.string().optional(),
|
|
10
|
+
file_id: z.string().optional(),
|
|
11
|
+
}).refine(v => typeof v.image_url === "string" || typeof v.file_id === "string", {
|
|
12
|
+
message: "input_image requires at least one of image_url or file_id",
|
|
13
|
+
});
|
|
14
|
+
const inputFileBlockSchema = z.object({
|
|
15
|
+
type: z.literal("input_file"),
|
|
16
|
+
file_id: z.string().optional(),
|
|
17
|
+
filename: z.string().optional(),
|
|
18
|
+
file_data: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
const outputTextSchema = z.object({ type: z.literal("output_text"), text: z.string() });
|
|
21
|
+
const outputRefusalSchema = z.object({ type: z.literal("refusal"), refusal: z.string() });
|
|
22
|
+
const summaryTextSchema = z.object({ type: z.literal("summary_text"), text: z.string() });
|
|
23
|
+
const reasoningTextSchema = z.object({ type: z.literal("reasoning_text"), text: z.string() });
|
|
24
|
+
// codex-rs FunctionCallOutputContentItem (protocol/src/models.rs): input_text | input_image | encrypted_content.
|
|
25
|
+
const encryptedContentBlockSchema = z.object({ type: z.literal("encrypted_content"), encrypted_content: z.string() });
|
|
26
|
+
|
|
27
|
+
const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputFileBlockSchema]);
|
|
28
|
+
const outputContentBlockSchema = z.union([outputTextSchema, plainTextSchema, outputRefusalSchema]);
|
|
29
|
+
// Tool outputs on the wire mix codex-rs FunctionCallOutputContentItem with legacy output blocks.
|
|
30
|
+
const toolOutputContentBlockSchema = z.union([
|
|
31
|
+
outputTextSchema, plainTextSchema, outputRefusalSchema,
|
|
32
|
+
inputTextSchema, inputImageBlockSchema, encryptedContentBlockSchema,
|
|
33
|
+
]);
|
|
34
|
+
const toolOutputSchema = z.union([z.string(), z.array(toolOutputContentBlockSchema)]);
|
|
35
|
+
|
|
36
|
+
const userMessageItemSchema = z.object({
|
|
37
|
+
type: z.literal("message").optional(),
|
|
38
|
+
role: z.union([z.literal("user"), z.literal("developer")]),
|
|
39
|
+
content: z.union([z.string(), z.array(inputContentBlockSchema)]).optional(),
|
|
40
|
+
});
|
|
41
|
+
const systemMessageItemSchema = z.object({
|
|
42
|
+
type: z.literal("message").optional(),
|
|
43
|
+
role: z.literal("system"),
|
|
44
|
+
content: z.union([z.string(), z.array(inputContentBlockSchema)]).optional(),
|
|
45
|
+
});
|
|
46
|
+
const assistantMessageItemSchema = z.object({
|
|
47
|
+
type: z.literal("message").optional(),
|
|
48
|
+
role: z.literal("assistant"),
|
|
49
|
+
content: z.union([z.string(), z.array(outputContentBlockSchema)]).optional(),
|
|
50
|
+
phase: z.enum(["commentary", "final_answer"]).optional(),
|
|
51
|
+
});
|
|
52
|
+
const reasoningItemSchema = z.object({
|
|
53
|
+
type: z.literal("reasoning"),
|
|
54
|
+
id: z.string().optional(),
|
|
55
|
+
summary: z.array(summaryTextSchema).optional(),
|
|
56
|
+
content: z.array(reasoningTextSchema).optional(),
|
|
57
|
+
// Round-tripped opaque payload (native OpenAI encryption OR the proxy's ocxr1 envelope).
|
|
58
|
+
encrypted_content: z.string().optional(),
|
|
59
|
+
});
|
|
60
|
+
const functionCallItemSchema = z.object({
|
|
61
|
+
type: z.literal("function_call"),
|
|
62
|
+
id: z.string().optional(),
|
|
63
|
+
call_id: z.string().min(1),
|
|
64
|
+
name: z.string().min(1),
|
|
65
|
+
namespace: z.string().optional(),
|
|
66
|
+
arguments: z.string().optional(),
|
|
67
|
+
});
|
|
68
|
+
const functionCallOutputItemSchema = z.object({
|
|
69
|
+
type: z.literal("function_call_output"),
|
|
70
|
+
call_id: z.string().min(1),
|
|
71
|
+
output: toolOutputSchema.optional(),
|
|
72
|
+
});
|
|
73
|
+
const customToolCallItemSchema = z.object({
|
|
74
|
+
type: z.literal("custom_tool_call"),
|
|
75
|
+
id: z.string().optional(),
|
|
76
|
+
call_id: z.string().min(1),
|
|
77
|
+
name: z.string().min(1),
|
|
78
|
+
input: z.string(),
|
|
79
|
+
});
|
|
80
|
+
const customToolCallOutputItemSchema = z.object({
|
|
81
|
+
type: z.literal("custom_tool_call_output"),
|
|
82
|
+
call_id: z.string().min(1),
|
|
83
|
+
// codex-rs CustomToolCallOutput carries FunctionCallOutputPayload: string OR content items.
|
|
84
|
+
output: toolOutputSchema,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const inputItemSchema = z.union([
|
|
88
|
+
userMessageItemSchema,
|
|
89
|
+
systemMessageItemSchema,
|
|
90
|
+
assistantMessageItemSchema,
|
|
91
|
+
reasoningItemSchema,
|
|
92
|
+
functionCallItemSchema,
|
|
93
|
+
functionCallOutputItemSchema,
|
|
94
|
+
customToolCallItemSchema,
|
|
95
|
+
customToolCallOutputItemSchema,
|
|
96
|
+
z.object({ type: z.string() }).loose(),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
export const toolSchema = z.object({
|
|
100
|
+
type: z.literal("function"),
|
|
101
|
+
name: z.string().min(1),
|
|
102
|
+
description: z.string().optional(),
|
|
103
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
104
|
+
strict: z.boolean().optional(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const builtinToolSchema = z.object({ type: z.string() }).loose();
|
|
108
|
+
|
|
109
|
+
const hostedToolType = z.enum([
|
|
110
|
+
"web_search_preview", "file_search", "computer_use_preview",
|
|
111
|
+
"code_interpreter", "image_generation", "mcp",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const allowedToolEntrySchema = z.object({ type: z.string(), name: z.string().optional() });
|
|
115
|
+
|
|
116
|
+
export const toolChoiceSchema = z.union([
|
|
117
|
+
z.literal("auto"),
|
|
118
|
+
z.literal("none"),
|
|
119
|
+
z.literal("required"),
|
|
120
|
+
z.object({ type: z.literal("function"), name: z.string().min(1) }),
|
|
121
|
+
z.object({ type: z.literal("custom"), name: z.string().min(1) }),
|
|
122
|
+
z.object({ type: hostedToolType }),
|
|
123
|
+
z.object({ type: z.literal("allowed_tools"), mode: z.enum(["auto", "required"]), tools: z.array(allowedToolEntrySchema) }),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
export const reasoningConfigSchema = z.object({
|
|
127
|
+
effort: z.string().optional(),
|
|
128
|
+
summary: z.enum(["auto", "concise", "detailed", "none"]).optional(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const stopSchema = z.union([z.string(), z.array(z.string()), z.null()]);
|
|
132
|
+
|
|
133
|
+
export const responsesRequestSchema = z.object({
|
|
134
|
+
model: z.string().min(1),
|
|
135
|
+
input: z.union([z.string(), z.array(inputItemSchema)]).optional(),
|
|
136
|
+
instructions: z.union([z.string(), z.null()]).optional(),
|
|
137
|
+
tools: z.array(z.union([toolSchema, builtinToolSchema])).optional(),
|
|
138
|
+
tool_choice: toolChoiceSchema.optional(),
|
|
139
|
+
max_output_tokens: z.number().optional(),
|
|
140
|
+
temperature: z.number().optional(),
|
|
141
|
+
top_p: z.number().optional(),
|
|
142
|
+
stop: stopSchema.optional(),
|
|
143
|
+
stream: z.boolean().optional(),
|
|
144
|
+
reasoning: reasoningConfigSchema.nullable().optional(),
|
|
145
|
+
store: z.boolean().optional(),
|
|
146
|
+
previous_response_id: z.string().optional(),
|
|
147
|
+
parallel_tool_calls: z.boolean().optional(),
|
|
148
|
+
prompt_cache_key: z.string().optional(),
|
|
149
|
+
metadata: z.unknown().optional(),
|
|
150
|
+
user: z.string().optional(),
|
|
151
|
+
service_tier: z.string().optional(),
|
|
152
|
+
presence_penalty: z.number().optional(),
|
|
153
|
+
frequency_penalty: z.number().optional(),
|
|
154
|
+
background: z.unknown().optional(),
|
|
155
|
+
include: z.unknown().optional(),
|
|
156
|
+
prompt: z.unknown().optional(),
|
|
157
|
+
text: z.unknown().optional(),
|
|
158
|
+
truncation: z.unknown().optional(),
|
|
159
|
+
});
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
fsyncSync,
|
|
8
|
+
linkSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
opendirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { getConfigDir } from "../config";
|
|
20
|
+
import { forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
21
|
+
import type { OcxProviderContinuationState } from "../types";
|
|
22
|
+
|
|
23
|
+
export const RESPONSE_SPILL_VERSION = 1;
|
|
24
|
+
export const RESPONSE_SPILL_DIR_NAME = "responses-state-spill";
|
|
25
|
+
export const RESPONSE_SPILL_ORPHAN_GRACE_MS = 15 * 60_000;
|
|
26
|
+
export const RESPONSE_SPILL_SCAN_MAX = 4_096;
|
|
27
|
+
export const RESPONSE_SPILL_CLEANUP_MAX = 512;
|
|
28
|
+
|
|
29
|
+
const RESPONSE_SPILL_PUBLISH_RETRIES = 64;
|
|
30
|
+
const OWNED_SPILL_NAME = /^([A-Za-z0-9._-]{1,80})\.([0-9a-f]{12})\.([0-9a-f]{24})\.(\d+)\.(\d+)\.spill\.json$/;
|
|
31
|
+
const OWNED_SPILL_TEMP_NAME = /^\.response-spill\.[0-9]+\.[0-9a-f]{16}\.tmp$/;
|
|
32
|
+
|
|
33
|
+
export interface ResponseSpillPayload {
|
|
34
|
+
version: 1;
|
|
35
|
+
responseId: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
items: unknown[];
|
|
38
|
+
providers?: OcxProviderContinuationState;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ResponseSpillRef {
|
|
42
|
+
version: 1;
|
|
43
|
+
fileName: string;
|
|
44
|
+
digest: string;
|
|
45
|
+
payloadBytes: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ResponseSpillReadResult =
|
|
49
|
+
| { ok: true; payload: ResponseSpillPayload }
|
|
50
|
+
| { ok: false; reason: "missing" | "corrupt" };
|
|
51
|
+
|
|
52
|
+
export interface ResponseSpillCleanupResult {
|
|
53
|
+
scanned: number;
|
|
54
|
+
removed: number;
|
|
55
|
+
failed: number;
|
|
56
|
+
bytesRemoved: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ResponseSpillIoForTest {
|
|
60
|
+
write?: (fd: number, bytes: Uint8Array) => void;
|
|
61
|
+
fsync?: (fd: number) => void;
|
|
62
|
+
/** Directory-handle fsync seam for `fsyncDirectoryBestEffort` only. */
|
|
63
|
+
fsyncDir?: (fd: number) => void;
|
|
64
|
+
/** Directory-handle open seam for `fsyncDirectoryBestEffort` only. */
|
|
65
|
+
openDir?: (dir: string) => number;
|
|
66
|
+
/** Directory-handle close seam for `fsyncDirectoryBestEffort` only. */
|
|
67
|
+
closeDir?: (fd: number) => void;
|
|
68
|
+
link?: (tempPath: string, destinationPath: string) => void;
|
|
69
|
+
copyFileExcl?: (tempPath: string, destinationPath: string) => void;
|
|
70
|
+
unlink?: (path: string) => void;
|
|
71
|
+
/** Orphan-GC enumeration seam: returns the next directory entry NAME or
|
|
72
|
+
* null for end-of-directory. Lets tests prove the scan cap binds without
|
|
73
|
+
* materializing thousands of real files. */
|
|
74
|
+
readdirEntry?: () => string | null;
|
|
75
|
+
record?: (event: "write" | "fsync" | "close" | "harden" | "publish" | "dir-fsync" | "stub-swap") => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let spillIoForTest: ResponseSpillIoForTest | null = null;
|
|
79
|
+
let spillGeneration = 0;
|
|
80
|
+
|
|
81
|
+
export function setSpillIoForTest(io: ResponseSpillIoForTest | null): void {
|
|
82
|
+
spillIoForTest = io;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function record(event: "write" | "fsync" | "close" | "harden" | "publish" | "dir-fsync" | "stub-swap"): void {
|
|
86
|
+
spillIoForTest?.record?.(event);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fsyncDirectoryBestEffort(dir: string): void {
|
|
90
|
+
let fd: number | null = null;
|
|
91
|
+
// Record the durability attempt before opening the directory: Windows cannot
|
|
92
|
+
// open/fsync directory handles this way, but callers still cross this seam.
|
|
93
|
+
record("dir-fsync");
|
|
94
|
+
try {
|
|
95
|
+
fd = spillIoForTest?.openDir ? spillIoForTest.openDir(dir) : openSync(dir, "r");
|
|
96
|
+
try {
|
|
97
|
+
if (spillIoForTest?.fsyncDir) spillIoForTest.fsyncDir(fd);
|
|
98
|
+
else if (spillIoForTest?.fsync) spillIoForTest.fsync(fd);
|
|
99
|
+
else fsyncSync(fd);
|
|
100
|
+
} catch {
|
|
101
|
+
// Windows and some filesystems do not support fsync on directory handles.
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Directory missing or unreadable — nothing further to sync.
|
|
105
|
+
} finally {
|
|
106
|
+
if (fd !== null) {
|
|
107
|
+
try {
|
|
108
|
+
if (spillIoForTest?.closeDir) spillIoForTest.closeDir(fd);
|
|
109
|
+
else closeSync(fd);
|
|
110
|
+
} catch { /* best effort */ }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function noteStubSwapForTest(): void {
|
|
116
|
+
record("stub-swap");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function responseSpillDirectory(dir = getConfigDir()): string {
|
|
120
|
+
return join(dir, RESPONSE_SPILL_DIR_NAME);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sha256(value: string | Uint8Array): string {
|
|
124
|
+
return createHash("sha256").update(value).digest("hex");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sanitizeResponseId(responseId: string): string {
|
|
128
|
+
const visible = responseId
|
|
129
|
+
.normalize("NFC")
|
|
130
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
131
|
+
.replace(/_+/g, "_")
|
|
132
|
+
.slice(0, 80);
|
|
133
|
+
return visible || "response";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isErrno(error: unknown, code: string): boolean {
|
|
137
|
+
return !!error && typeof error === "object" && (error as NodeJS.ErrnoException).code === code;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function canUseExclusiveCopyFallback(error: unknown): boolean {
|
|
141
|
+
return process.platform === "win32" || ["EPERM", "EACCES", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EXDEV"]
|
|
142
|
+
.some(code => isErrno(error, code));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function harden(path: string, mode: number): void {
|
|
146
|
+
try {
|
|
147
|
+
chmodSync(path, mode);
|
|
148
|
+
} catch {
|
|
149
|
+
if (process.platform !== "win32") throw new Error("Response spill permission hardening failed");
|
|
150
|
+
}
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
const result = mode === 0o700
|
|
153
|
+
? hardenSecretDir(path, { required: true })
|
|
154
|
+
: hardenSecretPath(path, { required: true });
|
|
155
|
+
if (!result.ok) throw new Error("Response spill permission hardening failed");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeAll(fd: number, bytes: Uint8Array): void {
|
|
160
|
+
if (spillIoForTest?.write) spillIoForTest.write(fd, bytes);
|
|
161
|
+
else {
|
|
162
|
+
let offset = 0;
|
|
163
|
+
while (offset < bytes.byteLength) offset += writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
164
|
+
}
|
|
165
|
+
record("write");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function fsyncFile(fd: number): void {
|
|
169
|
+
if (spillIoForTest?.fsync) spillIoForTest.fsync(fd);
|
|
170
|
+
else fsyncSync(fd);
|
|
171
|
+
record("fsync");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function closeFile(fd: number): void {
|
|
175
|
+
closeSync(fd);
|
|
176
|
+
record("close");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function unlink(path: string): void {
|
|
180
|
+
try {
|
|
181
|
+
if (spillIoForTest?.unlink) spillIoForTest.unlink(path);
|
|
182
|
+
else unlinkSync(path);
|
|
183
|
+
forgetHardenedSecretPath(path);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (isErrno(error, "ENOENT")) forgetHardenedSecretPath(path);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function publishNoReplace(tempPath: string, destinationPath: string): void {
|
|
191
|
+
try {
|
|
192
|
+
if (spillIoForTest?.link) spillIoForTest.link(tempPath, destinationPath);
|
|
193
|
+
else linkSync(tempPath, destinationPath);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (isErrno(error, "EEXIST")) throw error;
|
|
196
|
+
if (!canUseExclusiveCopyFallback(error)) throw error;
|
|
197
|
+
let copied = false;
|
|
198
|
+
try {
|
|
199
|
+
if (spillIoForTest?.copyFileExcl) spillIoForTest.copyFileExcl(tempPath, destinationPath);
|
|
200
|
+
else copyFileSync(tempPath, destinationPath, constants.COPYFILE_EXCL);
|
|
201
|
+
copied = true;
|
|
202
|
+
harden(destinationPath, 0o600);
|
|
203
|
+
const copyFd = openSync(destinationPath, "r");
|
|
204
|
+
try {
|
|
205
|
+
if (spillIoForTest?.fsync) spillIoForTest.fsync(copyFd);
|
|
206
|
+
else fsyncSync(copyFd);
|
|
207
|
+
} finally {
|
|
208
|
+
closeSync(copyFd);
|
|
209
|
+
}
|
|
210
|
+
} catch (copyError) {
|
|
211
|
+
if (copied) {
|
|
212
|
+
try { unlink(destinationPath); } catch { /* startup GC reclaims an incomplete publication */ }
|
|
213
|
+
}
|
|
214
|
+
throw copyError;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
record("publish");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validSpillRef(ref: ResponseSpillRef): boolean {
|
|
221
|
+
return ref.version === 1
|
|
222
|
+
&& OWNED_SPILL_NAME.test(ref.fileName)
|
|
223
|
+
&& /^[0-9a-f]{64}$/.test(ref.digest)
|
|
224
|
+
&& Number.isSafeInteger(ref.payloadBytes)
|
|
225
|
+
&& ref.payloadBytes >= 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function validPayload(value: unknown, responseId: string): value is ResponseSpillPayload {
|
|
229
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
230
|
+
const payload = value as Record<string, unknown>;
|
|
231
|
+
const keys = Object.keys(payload);
|
|
232
|
+
if (keys.some(key => !["version", "responseId", "createdAt", "items", "providers"].includes(key))) return false;
|
|
233
|
+
if (payload.version !== 1 || payload.responseId !== responseId) return false;
|
|
234
|
+
if (typeof payload.createdAt !== "number" || !Number.isFinite(payload.createdAt)) return false;
|
|
235
|
+
if (!Array.isArray(payload.items)) return false;
|
|
236
|
+
if (payload.providers !== undefined) {
|
|
237
|
+
if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) return false;
|
|
238
|
+
for (const providerState of Object.values(payload.providers)) {
|
|
239
|
+
if (!providerState || typeof providerState !== "object" || Array.isArray(providerState)) return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function writeResponseSpillDurably(
|
|
246
|
+
responseId: string,
|
|
247
|
+
state: Omit<ResponseSpillPayload, "version" | "responseId">,
|
|
248
|
+
): ResponseSpillRef {
|
|
249
|
+
let tempPath: string | null = null;
|
|
250
|
+
let fd: number | null = null;
|
|
251
|
+
try {
|
|
252
|
+
const payload: ResponseSpillPayload = {
|
|
253
|
+
version: 1,
|
|
254
|
+
responseId,
|
|
255
|
+
createdAt: state.createdAt,
|
|
256
|
+
items: state.items,
|
|
257
|
+
...(state.providers ? { providers: state.providers } : {}),
|
|
258
|
+
};
|
|
259
|
+
const serialized = JSON.stringify(payload);
|
|
260
|
+
if (serialized === undefined) throw new Error("Response spill serialization failed");
|
|
261
|
+
const bytes = Buffer.from(serialized, "utf8");
|
|
262
|
+
const digest = sha256(bytes);
|
|
263
|
+
const idDigest = sha256(responseId).slice(0, 12);
|
|
264
|
+
const contentDigest = digest.slice(0, 24);
|
|
265
|
+
const dir = responseSpillDirectory();
|
|
266
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
267
|
+
harden(dir, 0o700);
|
|
268
|
+
|
|
269
|
+
tempPath = join(dir, `.response-spill.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
270
|
+
fd = openSync(tempPath, "wx", 0o600);
|
|
271
|
+
writeAll(fd, bytes);
|
|
272
|
+
fsyncFile(fd);
|
|
273
|
+
closeFile(fd);
|
|
274
|
+
fd = null;
|
|
275
|
+
harden(tempPath, 0o600);
|
|
276
|
+
record("harden");
|
|
277
|
+
const publishTempPath = tempPath;
|
|
278
|
+
|
|
279
|
+
for (let attempt = 0; attempt < RESPONSE_SPILL_PUBLISH_RETRIES; attempt++) {
|
|
280
|
+
spillGeneration += 1;
|
|
281
|
+
const fileName = `${sanitizeResponseId(responseId)}.${idDigest}.${contentDigest}.${spillGeneration}.${bytes.byteLength}.spill.json`;
|
|
282
|
+
if (!OWNED_SPILL_NAME.test(fileName)) throw new Error("Response spill name allocation failed");
|
|
283
|
+
const destinationPath = join(dir, fileName);
|
|
284
|
+
try {
|
|
285
|
+
publishNoReplace(publishTempPath, destinationPath);
|
|
286
|
+
fsyncDirectoryBestEffort(dir);
|
|
287
|
+
unlink(publishTempPath);
|
|
288
|
+
tempPath = null;
|
|
289
|
+
return { version: 1, fileName, digest, payloadBytes: bytes.byteLength };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (isErrno(error, "EEXIST")) continue;
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
throw new Error("Response spill publication retries exhausted");
|
|
296
|
+
} catch {
|
|
297
|
+
if (fd !== null) {
|
|
298
|
+
try { closeSync(fd); } catch { /* best effort */ }
|
|
299
|
+
}
|
|
300
|
+
if (tempPath) {
|
|
301
|
+
try { unlink(tempPath); } catch { /* best effort */ }
|
|
302
|
+
}
|
|
303
|
+
throw new Error("Response spill write failed");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function readResponseSpill(responseId: string, ref: ResponseSpillRef): ResponseSpillReadResult {
|
|
308
|
+
if (!validSpillRef(ref)) return { ok: false, reason: "corrupt" };
|
|
309
|
+
const match = OWNED_SPILL_NAME.exec(ref.fileName);
|
|
310
|
+
if (!match
|
|
311
|
+
|| match[2] !== sha256(responseId).slice(0, 12)
|
|
312
|
+
|| match[3] !== ref.digest.slice(0, 24)
|
|
313
|
+
|| !Number.isSafeInteger(Number(match[4]))
|
|
314
|
+
|| Number(match[4]) <= 0
|
|
315
|
+
|| Number(match[5]) !== ref.payloadBytes) return { ok: false, reason: "corrupt" };
|
|
316
|
+
const path = join(responseSpillDirectory(), ref.fileName);
|
|
317
|
+
let bytes: Buffer;
|
|
318
|
+
try {
|
|
319
|
+
const stat = lstatSync(path);
|
|
320
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return { ok: false, reason: "corrupt" };
|
|
321
|
+
if (stat.size !== ref.payloadBytes) return { ok: false, reason: "corrupt" };
|
|
322
|
+
bytes = readFileSync(path);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return { ok: false, reason: isErrno(error, "ENOENT") ? "missing" : "corrupt" };
|
|
325
|
+
}
|
|
326
|
+
if (bytes.byteLength !== ref.payloadBytes || sha256(bytes) !== ref.digest) return { ok: false, reason: "corrupt" };
|
|
327
|
+
try {
|
|
328
|
+
const payload = JSON.parse(bytes.toString("utf8")) as unknown;
|
|
329
|
+
return validPayload(payload, responseId) ? { ok: true, payload } : { ok: false, reason: "corrupt" };
|
|
330
|
+
} catch {
|
|
331
|
+
return { ok: false, reason: "corrupt" };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function deleteResponseSpill(ref: ResponseSpillRef): void {
|
|
336
|
+
if (!validSpillRef(ref)) return;
|
|
337
|
+
const dir = responseSpillDirectory();
|
|
338
|
+
try {
|
|
339
|
+
unlink(join(dir, ref.fileName));
|
|
340
|
+
fsyncDirectoryBestEffort(dir);
|
|
341
|
+
} catch { /* best effort */ }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function recoverOrphanedResponseSpills(
|
|
345
|
+
referencedFileNames: ReadonlySet<string>,
|
|
346
|
+
dir = responseSpillDirectory(),
|
|
347
|
+
opts?: { graceMs?: number },
|
|
348
|
+
): ResponseSpillCleanupResult {
|
|
349
|
+
const result: ResponseSpillCleanupResult = { scanned: 0, removed: 0, failed: 0, bytesRemoved: 0 };
|
|
350
|
+
const graceMs = opts?.graceMs ?? RESPONSE_SPILL_ORPHAN_GRACE_MS;
|
|
351
|
+
// ONE loop serves both the real directory handle and the injected test seam
|
|
352
|
+
// (review C2-2: two duplicated loops let the test prove only its own copy).
|
|
353
|
+
// The reader is called strictly AFTER the scan-cap check, so entry
|
|
354
|
+
// SCAN_MAX+1 is never requested from either source.
|
|
355
|
+
let handle: ReturnType<typeof opendirSync> | null = null;
|
|
356
|
+
const injected = spillIoForTest?.readdirEntry;
|
|
357
|
+
if (!injected) {
|
|
358
|
+
try { handle = opendirSync(dir); } catch { return result; }
|
|
359
|
+
}
|
|
360
|
+
const nextName = (): string | null => {
|
|
361
|
+
if (injected) return injected();
|
|
362
|
+
const entry = handle!.readSync();
|
|
363
|
+
return entry ? entry.name : null;
|
|
364
|
+
};
|
|
365
|
+
try {
|
|
366
|
+
while (result.scanned < RESPONSE_SPILL_SCAN_MAX) {
|
|
367
|
+
const name = nextName();
|
|
368
|
+
if (name === null) break;
|
|
369
|
+
result.scanned += 1;
|
|
370
|
+
if (result.removed + result.failed >= RESPONSE_SPILL_CLEANUP_MAX) break;
|
|
371
|
+
const spillMatch = OWNED_SPILL_NAME.exec(name);
|
|
372
|
+
const isOwnedTemp = OWNED_SPILL_TEMP_NAME.test(name);
|
|
373
|
+
if ((!spillMatch && !isOwnedTemp) || referencedFileNames.has(name)) continue;
|
|
374
|
+
const path = join(dir, name);
|
|
375
|
+
let stat: ReturnType<typeof lstatSync>;
|
|
376
|
+
try { stat = lstatSync(path); } catch { continue; }
|
|
377
|
+
if (!stat.isFile() || stat.isSymbolicLink() || Date.now() - stat.mtimeMs < graceMs) continue;
|
|
378
|
+
try {
|
|
379
|
+
unlink(path);
|
|
380
|
+
result.removed += 1;
|
|
381
|
+
result.bytesRemoved += stat.size;
|
|
382
|
+
} catch {
|
|
383
|
+
result.failed += 1;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} finally {
|
|
387
|
+
try { handle?.closeSync(); } catch { /* best effort */ }
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function responseSpillExistsForTests(ref: ResponseSpillRef): boolean {
|
|
393
|
+
return validSpillRef(ref) && existsSync(join(responseSpillDirectory(), ref.fileName));
|
|
394
|
+
}
|