@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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows per-user NTFS ACL hardening for secret files and directories.
|
|
3
|
+
*
|
|
4
|
+
* On Windows, `chmod` only controls POSIX-style bits in the ACE list and does NOT remove
|
|
5
|
+
* inherited permissions from other users. Real per-user isolation requires icacls to:
|
|
6
|
+
* 1. Grant the current user full control (icacls path /grant:r "CURRENTUSER:(F)")
|
|
7
|
+
* 2. Disable inheritance (icacls path /inheritance:r)
|
|
8
|
+
* 3. Strip broad explicit grants by SID (Everyone, Users, Authenticated Users)
|
|
9
|
+
*
|
|
10
|
+
* Owner grant MUST precede `/inheritance:r`. That flag is destructive: it drops inherited
|
|
11
|
+
* ACEs immediately. If a later step times out after inheritance is removed but before an
|
|
12
|
+
* explicit owner ACE exists, the temp is left with a protected empty DACL — owned by the
|
|
13
|
+
* current user yet unreadable/ununlinkable until Full Control is restored (issue #596).
|
|
14
|
+
*
|
|
15
|
+
* On non-Windows platforms the helpers fall through to the caller's existing chmod-based
|
|
16
|
+
* behaviour: they return ok:true without invoking any external process.
|
|
17
|
+
*
|
|
18
|
+
* Design:
|
|
19
|
+
* hardenSecretPath(path, { required: false }) — non-fatal read-path mode.
|
|
20
|
+
* Never throws. Returns { ok, diagnostics? }.
|
|
21
|
+
* hardenSecretPath(path, { required: true }) — write-path mode.
|
|
22
|
+
* Throws a sanitized error (no raw path) on every Windows ACL failure,
|
|
23
|
+
* including genuine icacls timeouts. Required secret publication therefore
|
|
24
|
+
* fails closed; only required:false read-path probes may soft-fail.
|
|
25
|
+
* hardenSecretPathAsync / hardenSecretDirAsync — same policy, async icacls
|
|
26
|
+
* runner so the event loop is not held for the child lifetime (#612).
|
|
27
|
+
* HardenOptions.timeoutMemoKey — optional destination-path key for the
|
|
28
|
+
* timeout memo (atomic writers mint unique temps; never a parent directory).
|
|
29
|
+
* hardenSecretDir — same contract for directories.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { existsSync } from "node:fs";
|
|
33
|
+
import { env, platform } from "node:process";
|
|
34
|
+
|
|
35
|
+
const hardenedDirectories = new Set<string>();
|
|
36
|
+
const hardenedPaths = new Set<string>();
|
|
37
|
+
/** Paths whose harden TIMED OUT this process: do not re-stall every loadConfig on them. */
|
|
38
|
+
const timedOutPaths = new Set<string>();
|
|
39
|
+
|
|
40
|
+
export interface HardenResult {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
diagnostics?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HardenOptions {
|
|
46
|
+
required: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Optional timeout-memo key distinct from `targetPath` (issue #612).
|
|
49
|
+
* Atomic writers mint a fresh `.tmp` path per write; keying the timeout cache by the
|
|
50
|
+
* final destination path prevents re-stalling the event loop on every subsequent temp.
|
|
51
|
+
* Must NOT be a parent directory — directory ACLs are not authoritative for new files.
|
|
52
|
+
*/
|
|
53
|
+
timeoutMemoKey?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Total icacls budget per harden call — ALL steps share it, including the single
|
|
58
|
+
* timeout retry and the diagnostic verification pass (no per-attempt fresh budget:
|
|
59
|
+
* loadConfig hardens dir+config+auth sequentially, so per-attempt budgets stack
|
|
60
|
+
* into multi-minute startup stalls). Override with OPENCODEX_ACL_TIMEOUT_MS
|
|
61
|
+
* (integer ms, clamped to [1000, 60000]; invalid values fall back to 5000).
|
|
62
|
+
*/
|
|
63
|
+
const HARDEN_DEADLINE_DEFAULT_MS = 5_000;
|
|
64
|
+
const HARDEN_DEADLINE_MIN_MS = 1_000;
|
|
65
|
+
const HARDEN_DEADLINE_MAX_MS = 60_000;
|
|
66
|
+
|
|
67
|
+
/** Resolve the total harden budget once per call (env mutation cannot change it midway). */
|
|
68
|
+
function resolveHardenDeadlineMs(): number {
|
|
69
|
+
const raw = env["OPENCODEX_ACL_TIMEOUT_MS"]?.trim();
|
|
70
|
+
if (!raw) return HARDEN_DEADLINE_DEFAULT_MS;
|
|
71
|
+
const parsed = Number(raw);
|
|
72
|
+
if (!Number.isSafeInteger(parsed)) return HARDEN_DEADLINE_DEFAULT_MS;
|
|
73
|
+
return Math.min(HARDEN_DEADLINE_MAX_MS, Math.max(HARDEN_DEADLINE_MIN_MS, parsed));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface IcaclsResult {
|
|
77
|
+
success: boolean;
|
|
78
|
+
exitCode: number | null;
|
|
79
|
+
timedOut: boolean;
|
|
80
|
+
stdout: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type IcaclsRunner = (args: string[], timeoutMs: number) => IcaclsResult;
|
|
84
|
+
type AsyncIcaclsRunner = (args: string[], timeoutMs: number) => Promise<IcaclsResult>;
|
|
85
|
+
|
|
86
|
+
function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
87
|
+
// Bun.spawnSync with windowsHide: Node execFileSync has hung under the GUI/proxy even
|
|
88
|
+
// with windowsHide, and console-subsystem tools flash a visible window otherwise.
|
|
89
|
+
const result = Bun.spawnSync(["icacls.exe", ...args], {
|
|
90
|
+
stdin: "ignore",
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "ignore",
|
|
93
|
+
timeout: timeoutMs,
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
success: result.success,
|
|
98
|
+
exitCode: result.exitCode,
|
|
99
|
+
timedOut: result.exitedDueToTimeout ?? false,
|
|
100
|
+
stdout: result.stdout ? result.stdout.toString() : "",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Async icacls runner (#612): yields the event loop while waiting for the child.
|
|
106
|
+
* Timeout provenance is recorded by our timer (async Subprocess has no exitedDueToTimeout);
|
|
107
|
+
* we still await process exit before classifying so settlement is confirmed.
|
|
108
|
+
*/
|
|
109
|
+
async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
110
|
+
const proc = Bun.spawn(["icacls.exe", ...args], {
|
|
111
|
+
stdin: "ignore",
|
|
112
|
+
stdout: "pipe",
|
|
113
|
+
stderr: "ignore",
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
});
|
|
116
|
+
let timedOutByUs = false;
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
timedOutByUs = true;
|
|
119
|
+
try { proc.kill(); } catch { /* already exited */ }
|
|
120
|
+
}, Math.max(1, timeoutMs));
|
|
121
|
+
let exitCode: number | null = null;
|
|
122
|
+
try {
|
|
123
|
+
exitCode = await proc.exited;
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
const stdout = proc.stdout
|
|
128
|
+
? await new Response(proc.stdout).text().catch(() => "")
|
|
129
|
+
: "";
|
|
130
|
+
const timedOut = timedOutByUs;
|
|
131
|
+
return {
|
|
132
|
+
success: !timedOut && exitCode === 0,
|
|
133
|
+
exitCode: timedOut ? null : exitCode,
|
|
134
|
+
timedOut,
|
|
135
|
+
stdout,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let icaclsRunner: IcaclsRunner = defaultIcaclsRunner;
|
|
140
|
+
let asyncIcaclsRunner: AsyncIcaclsRunner = defaultAsyncIcaclsRunner;
|
|
141
|
+
let platformOverride: string | null = null;
|
|
142
|
+
let nowFn: () => number = Date.now;
|
|
143
|
+
|
|
144
|
+
/** Test seam: replace the icacls process runner. Pass null to restore the default. */
|
|
145
|
+
export function setIcaclsRunnerForTests(runner: IcaclsRunner | null): void {
|
|
146
|
+
icaclsRunner = runner ?? defaultIcaclsRunner;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Test seam: replace the async icacls runner. Pass null to restore the default. */
|
|
150
|
+
export function setAsyncIcaclsRunnerForTests(runner: AsyncIcaclsRunner | null): void {
|
|
151
|
+
asyncIcaclsRunner = runner ?? defaultAsyncIcaclsRunner;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Test seam: force the platform gate (e.g. "win32") so CI on POSIX reaches the runner. */
|
|
155
|
+
export function setPlatformForTests(value: string | null): void {
|
|
156
|
+
platformOverride = value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Test seam: injectable clock for deadline tests (no real sleeps). */
|
|
160
|
+
export function setNowForTests(fn: (() => number) | null): void {
|
|
161
|
+
nowFn = fn ?? Date.now;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Test seam: clear memo/failure caches between cases. */
|
|
165
|
+
export function resetHardenedStateForTests(): void {
|
|
166
|
+
hardenedDirectories.clear();
|
|
167
|
+
hardenedPaths.clear();
|
|
168
|
+
timedOutPaths.clear();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Forget a successful harden only after this exact ephemeral path is gone. */
|
|
172
|
+
export function forgetHardenedSecretPath(targetPath: string): void {
|
|
173
|
+
hardenedPaths.delete(targetPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Test seam for proving ephemeral success memos do not grow across replacements. */
|
|
177
|
+
export function hardenedSecretPathCountForTests(): number {
|
|
178
|
+
return hardenedPaths.size;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function effectivePlatform(): string {
|
|
182
|
+
return platformOverride ?? platform;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Test-aware platform predicate for callers that must avoid even a no-op ACL call. */
|
|
186
|
+
export function windowsSecretAclApplies(): boolean {
|
|
187
|
+
return (platformOverride ?? process.platform) === "win32";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Error carrying an honest code: ETIMEDOUT only for real timeouts, EICACLS otherwise. */
|
|
191
|
+
function icaclsError(step: string, result: IcaclsResult): NodeJS.ErrnoException {
|
|
192
|
+
const err = new Error(
|
|
193
|
+
result.timedOut ? `icacls ${step} timed out` : `icacls ${step} exited ${result.exitCode ?? "null"}`,
|
|
194
|
+
) as NodeJS.ErrnoException;
|
|
195
|
+
err.code = result.timedOut ? "ETIMEDOUT" : "EICACLS";
|
|
196
|
+
return err;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Return the current Windows username from the environment.
|
|
201
|
+
* Falls back to USERDOMAIN\USERNAME if USERNAME alone is ambiguous.
|
|
202
|
+
* The value is used directly in icacls arguments, so it must be present.
|
|
203
|
+
*/
|
|
204
|
+
function currentWindowsUser(): string | undefined {
|
|
205
|
+
const username = env["USERNAME"];
|
|
206
|
+
const domain = env["USERDOMAIN"];
|
|
207
|
+
if (!username) return undefined;
|
|
208
|
+
// USERDOMAIN is the machine/domain name; USERNAME is the account name.
|
|
209
|
+
// icacls accepts "DOMAIN\User" or just "User" for local accounts.
|
|
210
|
+
return domain ? `${domain}\\${username}` : username;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Run icacls to harden a single file system entry.
|
|
215
|
+
* Order is intentional (issue #596): grant the owner ACE first so a later
|
|
216
|
+
* `/inheritance:r` or `/remove:g` timeout cannot strand a protected zero-ACE DACL.
|
|
217
|
+
*
|
|
218
|
+
* We do NOT use a shell string; all arguments are passed as an array so no
|
|
219
|
+
* shell injection is possible even for paths with unusual characters.
|
|
220
|
+
*
|
|
221
|
+
* Throws the raw child_process error on failure (caller sanitizes).
|
|
222
|
+
*/
|
|
223
|
+
const BROAD_SIDS = ["*S-1-1-0", "*S-1-5-11", "*S-1-5-32-545"] as const;
|
|
224
|
+
|
|
225
|
+
function grantAce(user: string, directory: boolean): string {
|
|
226
|
+
return directory ? `${user}:(OI)(CI)(F)` : `${user}:(F)`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function runIcacls(targetPath: string, directory: boolean, deadline: number): void {
|
|
230
|
+
const user = currentWindowsUser();
|
|
231
|
+
if (!user) {
|
|
232
|
+
throw new Error("Cannot determine current Windows user for ACL hardening");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The deadline is owned by hardenEntry (total budget incl. retry + verification).
|
|
236
|
+
const run = (step: string, args: string[]): IcaclsResult => {
|
|
237
|
+
const remaining = deadline - nowFn();
|
|
238
|
+
if (remaining <= 0) {
|
|
239
|
+
throw icaclsError(step, { success: false, exitCode: null, timedOut: true, stdout: "" });
|
|
240
|
+
}
|
|
241
|
+
return icaclsRunner(args, remaining);
|
|
242
|
+
};
|
|
243
|
+
const runOrThrow = (step: string, args: string[]): void => {
|
|
244
|
+
const result = run(step, args);
|
|
245
|
+
if (!result.success) throw icaclsError(step, result);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Step 1: grant current user full control BEFORE any destructive ACL change.
|
|
249
|
+
// If this fails, inheritance is untouched and the writer keeps inherited access.
|
|
250
|
+
runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(user, directory)]);
|
|
251
|
+
|
|
252
|
+
// Step 2: disable inheritance and remove inherited ACEs. The explicit owner ACE
|
|
253
|
+
// from step 1 survives this transition, so a later failure still leaves cleanup access.
|
|
254
|
+
runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
|
|
255
|
+
|
|
256
|
+
// Step 3: remove broad explicit grants using stable SIDs (not localized names).
|
|
257
|
+
// Missing ACEs can yield a non-zero exit; verify with locale-independent /findsid
|
|
258
|
+
// before accepting the failure as harmless — a swallowed real failure would leave
|
|
259
|
+
// Everyone/Users/Authenticated Users grants while reporting hardened.
|
|
260
|
+
// `/remove:g` cannot remove the explicit current-user ACE installed in step 1.
|
|
261
|
+
const removal = run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
|
|
262
|
+
if (!removal.success) {
|
|
263
|
+
if (removal.timedOut) throw icaclsError("/remove:g", removal);
|
|
264
|
+
for (const sid of BROAD_SIDS) {
|
|
265
|
+
const found = run("/findsid", [targetPath, "/findsid", sid]);
|
|
266
|
+
if (!found.success) throw icaclsError("/findsid", found);
|
|
267
|
+
// icacls /findsid echoes the target path in its "SID Found" line only when the SID
|
|
268
|
+
// still holds an ACE; the summary lines carry only counts. Matching the path echo —
|
|
269
|
+
// not the (localized) prose — keeps the check locale-independent.
|
|
270
|
+
if (found.stdout.includes(targetPath)) {
|
|
271
|
+
throw icaclsError("/remove:g", removal);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Async counterpart of runIcacls — same step order and timeout/error classification (#612). */
|
|
278
|
+
async function runIcaclsAsync(targetPath: string, directory: boolean, deadline: number): Promise<void> {
|
|
279
|
+
const user = currentWindowsUser();
|
|
280
|
+
if (!user) {
|
|
281
|
+
throw new Error("Cannot determine current Windows user for ACL hardening");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const run = async (step: string, args: string[]): Promise<IcaclsResult> => {
|
|
285
|
+
const remaining = deadline - nowFn();
|
|
286
|
+
if (remaining <= 0) {
|
|
287
|
+
throw icaclsError(step, { success: false, exitCode: null, timedOut: true, stdout: "" });
|
|
288
|
+
}
|
|
289
|
+
return asyncIcaclsRunner(args, remaining);
|
|
290
|
+
};
|
|
291
|
+
const runOrThrow = async (step: string, args: string[]): Promise<void> => {
|
|
292
|
+
const result = await run(step, args);
|
|
293
|
+
if (!result.success) throw icaclsError(step, result);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await runOrThrow("/grant:r", [targetPath, "/grant:r", grantAce(user, directory)]);
|
|
297
|
+
await runOrThrow("/inheritance:r", [targetPath, "/inheritance:r"]);
|
|
298
|
+
|
|
299
|
+
const removal = await run("/remove:g", [targetPath, "/remove:g", ...BROAD_SIDS]);
|
|
300
|
+
if (!removal.success) {
|
|
301
|
+
if (removal.timedOut) throw icaclsError("/remove:g", removal);
|
|
302
|
+
for (const sid of BROAD_SIDS) {
|
|
303
|
+
const found = await run("/findsid", [targetPath, "/findsid", sid]);
|
|
304
|
+
if (!found.success) throw icaclsError("/findsid", found);
|
|
305
|
+
if (found.stdout.includes(targetPath)) {
|
|
306
|
+
throw icaclsError("/remove:g", removal);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Sanitize an error from a failed ACL operation into a safe diagnostic string.
|
|
314
|
+
* The raw path must not appear in the returned string (it may contain
|
|
315
|
+
* sensitive username components or PII from the home directory path).
|
|
316
|
+
*/
|
|
317
|
+
function sanitizeDiagnostics(error: unknown): string {
|
|
318
|
+
// We do not expose the raw error message or any path-like fragments —
|
|
319
|
+
// just an honest, code-specific cause (issue #160: a transient icacls stall
|
|
320
|
+
// must not read like filesystem non-support).
|
|
321
|
+
const code = error instanceof Error && "code" in error ? String((error as NodeJS.ErrnoException).code) : "";
|
|
322
|
+
switch (code) {
|
|
323
|
+
case "ETIMEDOUT":
|
|
324
|
+
return "ACL hardening timed out (ETIMEDOUT) — transient icacls stall; the volume may still support per-user NTFS ACLs";
|
|
325
|
+
case "EPERM":
|
|
326
|
+
case "EACCES":
|
|
327
|
+
return `ACL hardening failed (${code}) — permission denied running icacls`;
|
|
328
|
+
case "EICACLS":
|
|
329
|
+
return "ACL hardening failed (EICACLS) — icacls command error; filesystem may not support per-user NTFS ACLs";
|
|
330
|
+
default:
|
|
331
|
+
return `ACL hardening failed${code ? ` (${code})` : ""} — filesystem may not support per-user NTFS ACLs`;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isTimeoutError(error: unknown): boolean {
|
|
336
|
+
return error instanceof Error && "code" in error
|
|
337
|
+
&& String((error as NodeJS.ErrnoException).code) === "ETIMEDOUT";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Diagnostic-only post-timeout probe (never promotes to ok:true — a clean /findsid
|
|
342
|
+
* does not prove inheritance was disabled or the user grant ran; only a fully
|
|
343
|
+
* completed harden sequence may enter the hardened cache). Bounded by the remaining
|
|
344
|
+
* total budget; returns a short state note for the soft-fail diagnostic.
|
|
345
|
+
*/
|
|
346
|
+
function describeAclStateAfterTimeout(targetPath: string, deadline: number): string {
|
|
347
|
+
try {
|
|
348
|
+
for (const sid of BROAD_SIDS) {
|
|
349
|
+
const remaining = deadline - nowFn();
|
|
350
|
+
if (remaining <= 0) return "ACL state unverified (budget exhausted)";
|
|
351
|
+
const found = icaclsRunner([targetPath, "/findsid", sid], remaining);
|
|
352
|
+
if (!found.success) return "ACL state unverified (probe failed)";
|
|
353
|
+
if (found.stdout.includes(targetPath)) return "broad ACL grants still present";
|
|
354
|
+
}
|
|
355
|
+
return "no broad ACL grants detected (hardening still incomplete)";
|
|
356
|
+
} catch {
|
|
357
|
+
return "ACL state unverified (probe failed)";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function describeAclStateAfterTimeoutAsync(targetPath: string, deadline: number): Promise<string> {
|
|
362
|
+
try {
|
|
363
|
+
for (const sid of BROAD_SIDS) {
|
|
364
|
+
const remaining = deadline - nowFn();
|
|
365
|
+
if (remaining <= 0) return "ACL state unverified (budget exhausted)";
|
|
366
|
+
const found = await asyncIcaclsRunner([targetPath, "/findsid", sid], remaining);
|
|
367
|
+
if (!found.success) return "ACL state unverified (probe failed)";
|
|
368
|
+
if (found.stdout.includes(targetPath)) return "broad ACL grants still present";
|
|
369
|
+
}
|
|
370
|
+
return "no broad ACL grants detected (hardening still incomplete)";
|
|
371
|
+
} catch {
|
|
372
|
+
return "ACL state unverified (probe failed)";
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function timeoutMemoKey(targetPath: string, opts: HardenOptions): string {
|
|
377
|
+
// Destination-path memo only (issue #612). Never a parent directory — directory ACLs
|
|
378
|
+
// are not authoritative for newly created temps.
|
|
379
|
+
//
|
|
380
|
+
// Namespace by required-ness (#766): a soft `required:false` timeout during loadConfig
|
|
381
|
+
// must not poison a later `required:true` management-token harden of the same path.
|
|
382
|
+
const base = opts.timeoutMemoKey ?? targetPath;
|
|
383
|
+
return `${opts.required ? "required" : "optional"}:${base}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Shared harden flow for files and directories: one total budget (env-configurable)
|
|
388
|
+
* covering the initial attempt, ONE timeout retry, and the diagnostic verification.
|
|
389
|
+
* Required paths fail closed for every hardening failure, including genuine
|
|
390
|
+
* timeouts. Optional read paths soft-fail with an honest diagnostic.
|
|
391
|
+
*/
|
|
392
|
+
function hardenEntry(
|
|
393
|
+
targetPath: string,
|
|
394
|
+
directory: boolean,
|
|
395
|
+
opts: HardenOptions,
|
|
396
|
+
cache: Set<string>,
|
|
397
|
+
): HardenResult {
|
|
398
|
+
if (!existsSync(targetPath)) return { ok: true };
|
|
399
|
+
if (effectivePlatform() !== "win32") return { ok: true };
|
|
400
|
+
if (cache.has(targetPath)) return { ok: true };
|
|
401
|
+
const memoKey = timeoutMemoKey(targetPath, opts);
|
|
402
|
+
if (timedOutPaths.has(memoKey)) {
|
|
403
|
+
const diagnostics = "ACL hardening skipped — previous attempt timed out";
|
|
404
|
+
if (opts.required) throw new Error(diagnostics);
|
|
405
|
+
return { ok: false, diagnostics };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const deadline = nowFn() + resolveHardenDeadlineMs();
|
|
409
|
+
let lastErr: unknown;
|
|
410
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
411
|
+
if (attempt > 0 && deadline - nowFn() <= 0) break; // retry only while budget remains
|
|
412
|
+
try {
|
|
413
|
+
runIcacls(targetPath, directory, deadline);
|
|
414
|
+
cache.add(targetPath);
|
|
415
|
+
return { ok: true };
|
|
416
|
+
} catch (err) {
|
|
417
|
+
lastErr = err;
|
|
418
|
+
if (!isTimeoutError(err)) break; // real failures do not retry
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const diagnostics = sanitizeDiagnostics(lastErr);
|
|
423
|
+
if (isTimeoutError(lastErr)) {
|
|
424
|
+
timedOutPaths.add(memoKey);
|
|
425
|
+
const state = describeAclStateAfterTimeout(targetPath, deadline);
|
|
426
|
+
const annotated = `${diagnostics}; ${state}`;
|
|
427
|
+
if (opts.required) throw new Error(annotated);
|
|
428
|
+
console.warn(`[opencodex] ${annotated} — continuing without NTFS ACL harden`);
|
|
429
|
+
return { ok: false, diagnostics: annotated };
|
|
430
|
+
}
|
|
431
|
+
if (opts.required) throw new Error(diagnostics);
|
|
432
|
+
return { ok: false, diagnostics };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Async counterpart of hardenEntry — yields while waiting on icacls (#612). */
|
|
436
|
+
async function hardenEntryAsync(
|
|
437
|
+
targetPath: string,
|
|
438
|
+
directory: boolean,
|
|
439
|
+
opts: HardenOptions,
|
|
440
|
+
cache: Set<string>,
|
|
441
|
+
): Promise<HardenResult> {
|
|
442
|
+
if (!existsSync(targetPath)) return { ok: true };
|
|
443
|
+
if (effectivePlatform() !== "win32") return { ok: true };
|
|
444
|
+
if (cache.has(targetPath)) return { ok: true };
|
|
445
|
+
const memoKey = timeoutMemoKey(targetPath, opts);
|
|
446
|
+
if (timedOutPaths.has(memoKey)) {
|
|
447
|
+
const diagnostics = "ACL hardening skipped — previous attempt timed out";
|
|
448
|
+
if (opts.required) throw new Error(diagnostics);
|
|
449
|
+
return { ok: false, diagnostics };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const deadline = nowFn() + resolveHardenDeadlineMs();
|
|
453
|
+
let lastErr: unknown;
|
|
454
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
455
|
+
if (attempt > 0 && deadline - nowFn() <= 0) break;
|
|
456
|
+
try {
|
|
457
|
+
await runIcaclsAsync(targetPath, directory, deadline);
|
|
458
|
+
cache.add(targetPath);
|
|
459
|
+
return { ok: true };
|
|
460
|
+
} catch (err) {
|
|
461
|
+
lastErr = err;
|
|
462
|
+
if (!isTimeoutError(err)) break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const diagnostics = sanitizeDiagnostics(lastErr);
|
|
467
|
+
if (isTimeoutError(lastErr)) {
|
|
468
|
+
timedOutPaths.add(memoKey);
|
|
469
|
+
const state = await describeAclStateAfterTimeoutAsync(targetPath, deadline);
|
|
470
|
+
const annotated = `${diagnostics}; ${state}`;
|
|
471
|
+
if (opts.required) throw new Error(annotated);
|
|
472
|
+
console.warn(`[opencodex] ${annotated} — continuing without NTFS ACL harden`);
|
|
473
|
+
return { ok: false, diagnostics: annotated };
|
|
474
|
+
}
|
|
475
|
+
if (opts.required) throw new Error(diagnostics);
|
|
476
|
+
return { ok: false, diagnostics };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Harden a single file path with per-user NTFS ACLs on Windows.
|
|
481
|
+
* On non-Windows platforms, returns ok:true immediately (caller owns chmod).
|
|
482
|
+
*
|
|
483
|
+
* @param targetPath Absolute path to the file to harden.
|
|
484
|
+
* @param opts { required: boolean } — required:true throws on failure.
|
|
485
|
+
*/
|
|
486
|
+
export function hardenSecretPath(targetPath: string, opts: HardenOptions): HardenResult {
|
|
487
|
+
return hardenEntry(targetPath, false, opts, hardenedPaths);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Async harden for write paths that must not block the event loop (#612).
|
|
492
|
+
* Same success/timeout/error policy as hardenSecretPath.
|
|
493
|
+
*/
|
|
494
|
+
export function hardenSecretPathAsync(targetPath: string, opts: HardenOptions): Promise<HardenResult> {
|
|
495
|
+
return hardenEntryAsync(targetPath, false, opts, hardenedPaths);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Harden a directory path with per-user NTFS ACLs on Windows.
|
|
500
|
+
* On non-Windows platforms, returns ok:true immediately (caller owns chmod).
|
|
501
|
+
*
|
|
502
|
+
* @param targetPath Absolute path to the directory to harden.
|
|
503
|
+
* @param opts { required: boolean } — required:true throws on failure.
|
|
504
|
+
*/
|
|
505
|
+
export function hardenSecretDir(targetPath: string, opts: HardenOptions): HardenResult {
|
|
506
|
+
return hardenEntry(targetPath, true, opts, hardenedDirectories);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Async directory harden (#612). Same policy as hardenSecretDir.
|
|
511
|
+
*/
|
|
512
|
+
export function hardenSecretDirAsync(targetPath: string, opts: HardenOptions): Promise<HardenResult> {
|
|
513
|
+
return hardenEntryAsync(targetPath, true, opts, hardenedDirectories);
|
|
514
|
+
}
|