@yansigit/opencodex 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,2170 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
closeSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
linkSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
openSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readlinkSync,
|
|
18
|
+
readSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
rmdirSync,
|
|
22
|
+
statSync,
|
|
23
|
+
symlinkSync,
|
|
24
|
+
type Stats,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { getConfigDir } from "../config";
|
|
29
|
+
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "../lib/bun-runtime";
|
|
30
|
+
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
31
|
+
import { isProcessAlive } from "../lib/process-control";
|
|
32
|
+
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
33
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
34
|
+
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
35
|
+
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
36
|
+
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
37
|
+
|
|
38
|
+
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
39
|
+
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
|
|
40
|
+
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
41
|
+
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
42
|
+
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
43
|
+
const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
|
|
44
|
+
const CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS = 5_000;
|
|
45
|
+
const CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS = 1_000;
|
|
46
|
+
const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
|
|
47
|
+
const CODEX_SHIM_REENTRY_DIAGNOSTIC = "opencodex: saved Codex launcher resolved back to the autostart shim; run ocx codex-shim uninstall and reinstall Codex before enabling codexAutoStart.";
|
|
48
|
+
const CODEX_SHIM_INSTALL_PROBE_SCRIPT = `
|
|
49
|
+
const { spawn } = require("node:child_process");
|
|
50
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
51
|
+
const [markerPath, reentryPath, groupPath, stderrPath, launcherShellPath, wrapperPath, timeoutRaw, stderrLimitRaw, stderrDrainRaw, observationRaw] = process.argv.slice(1);
|
|
52
|
+
const timeoutMs = Number.parseInt(timeoutRaw, 10);
|
|
53
|
+
const stderrLimit = Number.parseInt(stderrLimitRaw, 10);
|
|
54
|
+
const stderrDrainMs = Number.parseInt(stderrDrainRaw, 10);
|
|
55
|
+
const observationMs = Number.parseInt(observationRaw, 10);
|
|
56
|
+
const probeStartedAt = Date.now();
|
|
57
|
+
const stderrChunks = [];
|
|
58
|
+
let stderrBytes = 0;
|
|
59
|
+
let launcher;
|
|
60
|
+
let probeLease;
|
|
61
|
+
let timer;
|
|
62
|
+
let stderrDrainTimer;
|
|
63
|
+
let observationTimer;
|
|
64
|
+
let reentryPollTimer;
|
|
65
|
+
let marker = "";
|
|
66
|
+
let finished = false;
|
|
67
|
+
|
|
68
|
+
function writeExclusive(path, value) {
|
|
69
|
+
writeFileSync(path, value, { flag: "wx", mode: 0o600 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function appendStderr(value) {
|
|
73
|
+
if (stderrBytes >= stderrLimit) return;
|
|
74
|
+
const bytes = Buffer.from(value);
|
|
75
|
+
const retained = bytes.subarray(0, stderrLimit - stderrBytes);
|
|
76
|
+
stderrChunks.push(retained);
|
|
77
|
+
stderrBytes += retained.byteLength;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function groupAlive() {
|
|
81
|
+
if (!launcher || !launcher.pid) return false;
|
|
82
|
+
try {
|
|
83
|
+
process.kill(-launcher.pid, 0);
|
|
84
|
+
return true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return error && error.code !== "ESRCH";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function killGroup() {
|
|
91
|
+
if (!launcher || !launcher.pid) return;
|
|
92
|
+
try { process.kill(-launcher.pid, "SIGKILL"); } catch (error) {
|
|
93
|
+
if (!error || error.code !== "ESRCH") appendStderr(String(error));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setMarker(value) {
|
|
98
|
+
if (marker) return;
|
|
99
|
+
marker = value;
|
|
100
|
+
try { writeExclusive(markerPath, value + "\\n"); } catch (error) { appendStderr(String(error)); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function reentryDetected() {
|
|
104
|
+
try { return readFileSync(reentryPath, "utf8").trim() === "recursive"; } catch { return false; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkReentry() {
|
|
108
|
+
if (finished || !reentryDetected()) return;
|
|
109
|
+
setMarker("recursive");
|
|
110
|
+
killGroup();
|
|
111
|
+
finish(126);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function finish(status) {
|
|
115
|
+
if (finished) return;
|
|
116
|
+
finished = true;
|
|
117
|
+
if (timer) clearTimeout(timer);
|
|
118
|
+
if (stderrDrainTimer) clearTimeout(stderrDrainTimer);
|
|
119
|
+
if (observationTimer) clearTimeout(observationTimer);
|
|
120
|
+
if (reentryPollTimer) clearInterval(reentryPollTimer);
|
|
121
|
+
if (!marker && reentryDetected()) setMarker("recursive");
|
|
122
|
+
if (!marker && groupAlive()) {
|
|
123
|
+
setMarker("descendants");
|
|
124
|
+
killGroup();
|
|
125
|
+
}
|
|
126
|
+
try { writeExclusive(stderrPath, Buffer.concat(stderrChunks)); } catch { /* parent fails closed */ }
|
|
127
|
+
process.exit(marker === "timeout" ? 124 : marker === "descendants" ? 125 : marker === "recursive" ? 126 : status);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function finishAfterStderr(status) {
|
|
131
|
+
if (finished) return;
|
|
132
|
+
if (timer) {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
timer = undefined;
|
|
135
|
+
}
|
|
136
|
+
if (!launcher || !launcher.stderr || !probeLease) {
|
|
137
|
+
finish(status);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let stderrEnded = launcher.stderr.readableEnded;
|
|
141
|
+
let leaseEnded = probeLease.readableEnded;
|
|
142
|
+
let observationElapsed = false;
|
|
143
|
+
const finishWhenReady = () => {
|
|
144
|
+
if (stderrEnded && leaseEnded && observationElapsed) finish(status);
|
|
145
|
+
};
|
|
146
|
+
launcher.stderr.once("end", () => {
|
|
147
|
+
stderrEnded = true;
|
|
148
|
+
finishWhenReady();
|
|
149
|
+
});
|
|
150
|
+
probeLease.once("end", () => {
|
|
151
|
+
leaseEnded = true;
|
|
152
|
+
finishWhenReady();
|
|
153
|
+
});
|
|
154
|
+
stderrDrainTimer = setTimeout(() => {
|
|
155
|
+
stderrEnded = true;
|
|
156
|
+
if (!marker && groupAlive()) {
|
|
157
|
+
setMarker("descendants");
|
|
158
|
+
killGroup();
|
|
159
|
+
finish(125);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
finishWhenReady();
|
|
163
|
+
}, stderrDrainMs);
|
|
164
|
+
const remainingObservationMs = Math.max(0, observationMs - (Date.now() - probeStartedAt));
|
|
165
|
+
observationTimer = setTimeout(() => {
|
|
166
|
+
observationElapsed = true;
|
|
167
|
+
if (!leaseEnded) {
|
|
168
|
+
setMarker(groupAlive() ? "descendants" : "timeout");
|
|
169
|
+
killGroup();
|
|
170
|
+
finish(marker === "descendants" ? 125 : 124);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
finishWhenReady();
|
|
174
|
+
}, remainingObservationMs);
|
|
175
|
+
finishWhenReady();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
|
|
180
|
+
detached: true,
|
|
181
|
+
env: process.env,
|
|
182
|
+
stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
183
|
+
});
|
|
184
|
+
if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
|
|
185
|
+
probeLease = launcher.stdio[3];
|
|
186
|
+
if (!probeLease) throw new Error("Codex shim probe launcher has no descendant lease pipe");
|
|
187
|
+
writeExclusive(groupPath, String(launcher.pid) + "\\n");
|
|
188
|
+
launcher.stderr.on("data", appendStderr);
|
|
189
|
+
reentryPollTimer = setInterval(checkReentry, 10);
|
|
190
|
+
launcher.once("error", error => {
|
|
191
|
+
appendStderr(String(error));
|
|
192
|
+
finishAfterStderr(127);
|
|
193
|
+
});
|
|
194
|
+
launcher.once("exit", code => finishAfterStderr(Number.isInteger(code) ? code : 127));
|
|
195
|
+
timer = setTimeout(() => {
|
|
196
|
+
setMarker("timeout");
|
|
197
|
+
killGroup();
|
|
198
|
+
finish(124);
|
|
199
|
+
}, timeoutMs);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
appendStderr(String(error));
|
|
202
|
+
killGroup();
|
|
203
|
+
finish(127);
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
207
|
+
let lastShimDiscoveryError: string | null = null;
|
|
208
|
+
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
209
|
+
export function lastCodexDiscoveryError(): string | null {
|
|
210
|
+
return lastShimDiscoveryError;
|
|
211
|
+
}
|
|
212
|
+
const CODEX_INTERNAL_COMMANDS = [
|
|
213
|
+
"app-server",
|
|
214
|
+
"archive",
|
|
215
|
+
"apply",
|
|
216
|
+
"cloud",
|
|
217
|
+
"completion",
|
|
218
|
+
"debug",
|
|
219
|
+
"delete",
|
|
220
|
+
"doctor",
|
|
221
|
+
"exec-server",
|
|
222
|
+
"features",
|
|
223
|
+
"fork",
|
|
224
|
+
"help",
|
|
225
|
+
"login",
|
|
226
|
+
"logout",
|
|
227
|
+
"mcp",
|
|
228
|
+
"plugin",
|
|
229
|
+
"sandbox",
|
|
230
|
+
"unarchive",
|
|
231
|
+
"update",
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Codex accepts global options before a subcommand. The shim must skip the value belonging to
|
|
235
|
+
// these options before it decides which first positional token is the real subcommand. Keep this
|
|
236
|
+
// list aligned with `codex --help`; `--option=value` and attached short forms stay one token.
|
|
237
|
+
const CODEX_GLOBAL_OPTIONS_WITH_VALUE = [
|
|
238
|
+
"-c", "--config",
|
|
239
|
+
"--enable", "--disable",
|
|
240
|
+
"--remote", "--remote-auth-token-env",
|
|
241
|
+
"-i", "--image",
|
|
242
|
+
"-m", "--model",
|
|
243
|
+
"--local-provider",
|
|
244
|
+
"-p", "--profile",
|
|
245
|
+
"-s", "--sandbox",
|
|
246
|
+
"-C", "--cd",
|
|
247
|
+
"--add-dir",
|
|
248
|
+
"-a", "--ask-for-approval",
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
interface ShimState {
|
|
252
|
+
platform: NodeJS.Platform;
|
|
253
|
+
wrapperPath: string;
|
|
254
|
+
originalPath: string;
|
|
255
|
+
backupPath: string;
|
|
256
|
+
wrappers?: ShimFileState[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface ShimFileState {
|
|
260
|
+
wrapperPath: string;
|
|
261
|
+
originalPath: string;
|
|
262
|
+
backupPath: string;
|
|
263
|
+
realPath?: string;
|
|
264
|
+
preserveOnly?: boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface ShimPathFingerprint {
|
|
268
|
+
dev: number;
|
|
269
|
+
ino: number;
|
|
270
|
+
kind: "file" | "symlink";
|
|
271
|
+
mode: number;
|
|
272
|
+
size: number;
|
|
273
|
+
mtimeMs: number;
|
|
274
|
+
ctimeMs: number;
|
|
275
|
+
target?: Omit<ShimPathFingerprint, "target">;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface StableShimPathProbe {
|
|
279
|
+
fingerprint: ShimPathFingerprint;
|
|
280
|
+
prefix: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface InstallCodexShimInternalOptions {
|
|
284
|
+
expectedReplacements?: ReadonlyMap<string, ShimPathFingerprint>;
|
|
285
|
+
allowFreshInstall: boolean;
|
|
286
|
+
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export type CodexShimAutoRestoreResult =
|
|
290
|
+
| { status: "not-installed" | "healthy" | "disabled" }
|
|
291
|
+
| { status: "ineligible" | "deferred"; message?: string }
|
|
292
|
+
| { status: "restored"; message: string };
|
|
293
|
+
|
|
294
|
+
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
295
|
+
// Bundled Bun path (survives `ocx update`); all three shim builders
|
|
296
|
+
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
|
|
297
|
+
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
|
|
298
|
+
// Path and provenance resolve together so the marker always describes this binary.
|
|
299
|
+
const runtime = durableBunRuntime();
|
|
300
|
+
return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "..", "cli", "index.ts") };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commandNames(name: string): string[] {
|
|
304
|
+
if (process.platform !== "win32") return [name];
|
|
305
|
+
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
|
|
306
|
+
return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isShim(path: string): boolean {
|
|
310
|
+
try {
|
|
311
|
+
return readFileSync(path, "utf8").includes(SHIM_MARKER);
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
318
|
+
try {
|
|
319
|
+
const content = readFileSync(path, "utf8");
|
|
320
|
+
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
321
|
+
if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
|
|
322
|
+
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
323
|
+
return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readShimProbePrefix(path: string): string {
|
|
330
|
+
const fd = openSync(path, "r");
|
|
331
|
+
try {
|
|
332
|
+
const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
|
|
333
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
334
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
335
|
+
} finally {
|
|
336
|
+
closeSync(fd);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function statFingerprint(path: string, follow: boolean): Omit<ShimPathFingerprint, "target"> | null {
|
|
341
|
+
try {
|
|
342
|
+
const stat = follow ? statSync(path) : lstatSync(path);
|
|
343
|
+
if (follow ? !stat.isFile() : (!stat.isFile() && !stat.isSymbolicLink())) return null;
|
|
344
|
+
return {
|
|
345
|
+
dev: stat.dev,
|
|
346
|
+
ino: stat.ino,
|
|
347
|
+
kind: stat.isSymbolicLink() ? "symlink" : "file",
|
|
348
|
+
mode: stat.mode,
|
|
349
|
+
size: stat.size,
|
|
350
|
+
mtimeMs: stat.mtimeMs,
|
|
351
|
+
ctimeMs: stat.ctimeMs,
|
|
352
|
+
};
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sameFingerprint(
|
|
359
|
+
left: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
360
|
+
right: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
361
|
+
): boolean {
|
|
362
|
+
return left.dev === right.dev
|
|
363
|
+
&& left.ino === right.ino
|
|
364
|
+
&& left.kind === right.kind
|
|
365
|
+
&& left.mode === right.mode
|
|
366
|
+
&& left.size === right.size
|
|
367
|
+
&& left.mtimeMs === right.mtimeMs
|
|
368
|
+
&& left.ctimeMs === right.ctimeMs
|
|
369
|
+
&& (!("target" in left) || !("target" in right)
|
|
370
|
+
? true
|
|
371
|
+
: left.target === undefined && right.target === undefined
|
|
372
|
+
? true
|
|
373
|
+
: left.target !== undefined && right.target !== undefined
|
|
374
|
+
? sameFingerprint(left.target, right.target)
|
|
375
|
+
: false);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
|
|
379
|
+
// rename changes the outer directory entry ctime on macOS; every other field,
|
|
380
|
+
// including a symlink target fingerprint, must remain identical.
|
|
381
|
+
return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function stableShimPathProbe(path: string): StableShimPathProbe | null {
|
|
385
|
+
const before = statFingerprint(path, false);
|
|
386
|
+
if (!before) return null;
|
|
387
|
+
const targetBefore = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
388
|
+
if (before.kind === "symlink" && !targetBefore) return null;
|
|
389
|
+
let prefix: string;
|
|
390
|
+
try {
|
|
391
|
+
prefix = readShimProbePrefix(path);
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const targetAfter = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
396
|
+
const after = statFingerprint(path, false);
|
|
397
|
+
if (!after || !sameFingerprint(before, after)) return null;
|
|
398
|
+
if (before.kind === "symlink") {
|
|
399
|
+
if (!targetBefore || !targetAfter || !sameFingerprint(targetBefore, targetAfter)) return null;
|
|
400
|
+
}
|
|
401
|
+
const fingerprint: ShimPathFingerprint = {
|
|
402
|
+
...before,
|
|
403
|
+
...(targetBefore ? { target: targetBefore } : {}),
|
|
404
|
+
};
|
|
405
|
+
const contentSize = fingerprint.target?.size ?? fingerprint.size;
|
|
406
|
+
return contentSize > 0 ? { fingerprint, prefix } : null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean {
|
|
410
|
+
return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Identity of whatever sits at `path`, read from metadata alone.
|
|
415
|
+
*
|
|
416
|
+
* `stableShimPathProbe` answers a different question: it reads content to decide
|
|
417
|
+
* whether a launcher looks like a healthy shim, and it deliberately returns null
|
|
418
|
+
* for a zero-byte file. That makes it the wrong instrument for rollback
|
|
419
|
+
* bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
|
|
420
|
+
* install moves it aside before writing our wrapper; if the move is recorded
|
|
421
|
+
* without a fingerprint, rollback cannot prove the backup is still the file it
|
|
422
|
+
* set aside and refuses to restore it — the launcher stays lost (#1625).
|
|
423
|
+
*
|
|
424
|
+
* Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
|
|
425
|
+
* re-reads them to reject a path that changed under us, following a symlink to
|
|
426
|
+
* fingerprint its target as well.
|
|
427
|
+
*/
|
|
428
|
+
function shimPathFingerprint(path: string): ShimPathFingerprint | null {
|
|
429
|
+
const before = statFingerprint(path, false);
|
|
430
|
+
if (!before) return null;
|
|
431
|
+
if (before.kind !== "symlink") {
|
|
432
|
+
const after = statFingerprint(path, false);
|
|
433
|
+
return after && sameFingerprint(before, after) ? before : null;
|
|
434
|
+
}
|
|
435
|
+
const targetBefore = statFingerprint(path, true);
|
|
436
|
+
if (!targetBefore) return null;
|
|
437
|
+
const targetAfter = statFingerprint(path, true);
|
|
438
|
+
const after = statFingerprint(path, false);
|
|
439
|
+
if (!targetAfter || !after
|
|
440
|
+
|| !sameFingerprint(targetBefore, targetAfter)
|
|
441
|
+
|| !sameFingerprint(before, after)) return null;
|
|
442
|
+
return { ...before, target: targetBefore };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Move `from` onto `to` without ever replacing an existing entry.
|
|
447
|
+
*
|
|
448
|
+
* `renameSync` silently clobbers the destination on POSIX, which is wrong for a
|
|
449
|
+
* rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
|
|
450
|
+
* a concurrent installer can publish its own launcher at the original path in
|
|
451
|
+
* between, and the restore would delete it. `link` fails EEXIST instead, which
|
|
452
|
+
* is the no-replace primitive we need and needs no native helper.
|
|
453
|
+
*
|
|
454
|
+
* `link` follows a symlink to its target rather than preserving the link, so a
|
|
455
|
+
* symlink launcher is republished with `symlink`, which is also no-replace: it
|
|
456
|
+
* fails EEXIST on an occupied destination. Checking existence and then renaming
|
|
457
|
+
* would reintroduce exactly the race this function exists to close.
|
|
458
|
+
*/
|
|
459
|
+
function restoreWithoutReplacing(from: string, to: string): void {
|
|
460
|
+
const source = lstatSync(from);
|
|
461
|
+
if (source.isSymbolicLink()) {
|
|
462
|
+
symlinkSync(readlinkSync(from), to);
|
|
463
|
+
unlinkSync(from);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
linkSync(from, to);
|
|
467
|
+
unlinkSync(from);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
|
|
471
|
+
if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
|
|
472
|
+
const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
|
|
473
|
+
return platform === "win32" || (mode & 0o111) !== 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
|
|
477
|
+
return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function hasUsableBackingPath(file: ShimFileState): boolean {
|
|
481
|
+
return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
|
|
482
|
+
.some(path => {
|
|
483
|
+
if (!path) return false;
|
|
484
|
+
const fingerprint = statFingerprint(path, true);
|
|
485
|
+
return fingerprint !== null && fingerprint.size > 0;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* A PATH entry that reaches Windows through WSL drive interop
|
|
491
|
+
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
492
|
+
* /etc/wsl.conf [automount] root).
|
|
493
|
+
*/
|
|
494
|
+
export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
|
|
495
|
+
const root = automountRoot.replace(/\/+$/, "");
|
|
496
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
497
|
+
return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export type CodexPathScanDeps = {
|
|
501
|
+
pathValue?: string;
|
|
502
|
+
wsl?: boolean;
|
|
503
|
+
/** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
|
|
504
|
+
posixPaths?: boolean;
|
|
505
|
+
automountRoot?: string;
|
|
506
|
+
exists?: (path: string) => boolean;
|
|
507
|
+
isShimFile?: (path: string) => boolean;
|
|
508
|
+
isDirectory?: (path: string) => boolean;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
function realIsDirectory(path: string): boolean {
|
|
512
|
+
try {
|
|
513
|
+
return lstatSync(path).isDirectory();
|
|
514
|
+
} catch {
|
|
515
|
+
return true; // unreadable -> treat as unusable
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
520
|
+
lastShimDiscoveryError = null;
|
|
521
|
+
const exists = deps.exists ?? existsSync;
|
|
522
|
+
const shimFile = deps.isShimFile ?? isShim;
|
|
523
|
+
const isDir = deps.isDirectory ?? realIsDirectory;
|
|
524
|
+
const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
|
|
525
|
+
const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
|
|
526
|
+
const joinPath = usePosix ? posix.join : join;
|
|
527
|
+
const pathSep = usePosix ? ":" : delimiter;
|
|
528
|
+
const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
|
|
529
|
+
// Windows npm prefixes ship codex.exe/codex.cmd next to the extensionless sh launcher.
|
|
530
|
+
const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
|
|
531
|
+
let skippedInterop: string | null = null;
|
|
532
|
+
|
|
533
|
+
for (const dir of (deps.pathValue ?? process.env.PATH ?? "").split(pathSep).filter(Boolean)) {
|
|
534
|
+
if (wsl && isWindowsInteropDir(dir, automountRoot)) {
|
|
535
|
+
// A Windows-side codex reached through WSL PATH interop: a Unix shim written
|
|
536
|
+
// here would embed WSL-only paths and break every Windows-side invocation.
|
|
537
|
+
if (!skippedInterop) {
|
|
538
|
+
for (const name of interopNames) {
|
|
539
|
+
const path = joinPath(dir, name);
|
|
540
|
+
if (exists(path) && !shimFile(path) && !isDir(path)) { skippedInterop = path; break; }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
// Interop dirs carry Windows launcher names even when the scan is not skipping them.
|
|
546
|
+
const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
|
|
547
|
+
for (const name of names) {
|
|
548
|
+
const path = joinPath(dir, name);
|
|
549
|
+
if (!exists(path) || shimFile(path)) continue;
|
|
550
|
+
if (!isDir(path)) return path;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (skippedInterop) {
|
|
555
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
556
|
+
`Found a Windows codex at ${skippedInterop} via WSL PATH interop, but no Linux-side codex. ` +
|
|
557
|
+
"Refusing to shim a Windows launcher from WSL (a WSL shim breaks Windows invocations). " +
|
|
558
|
+
"Install codex inside WSL (npm i -g @openai/codex), or run 'ocx ensure' from Windows to shim the Windows side.",
|
|
559
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
566
|
+
lastShimDiscoveryError = null;
|
|
567
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
568
|
+
const exe = join(dir, "codex.exe");
|
|
569
|
+
if (existsSync(exe) && !isShim(exe)) {
|
|
570
|
+
try {
|
|
571
|
+
if (!lstatSync(exe).isDirectory()) {
|
|
572
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
573
|
+
`Found codex.exe at ${exe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
|
|
574
|
+
"install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart.",
|
|
575
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
576
|
+
);
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
} catch { /* keep scanning */ }
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const cmd = join(dir, "codex.cmd");
|
|
583
|
+
const ps1 = join(dir, "codex.ps1");
|
|
584
|
+
// npm also installs an extensionless `codex` sh launcher for Git-Bash/MSYS shells;
|
|
585
|
+
// leaving it unshimmed means Git-Bash users silently get no autostart.
|
|
586
|
+
const gitBashLauncher = join(dir, "codex");
|
|
587
|
+
const targets: ShimFileState[] = [];
|
|
588
|
+
for (const path of [cmd, ps1, gitBashLauncher]) {
|
|
589
|
+
if (!existsSync(path) || isShim(path)) continue;
|
|
590
|
+
try {
|
|
591
|
+
if (!lstatSync(path).isDirectory()) {
|
|
592
|
+
targets.push({ wrapperPath: path, originalPath: path, backupPath: backupPathFor(path) });
|
|
593
|
+
}
|
|
594
|
+
} catch { /* keep scanning */ }
|
|
595
|
+
}
|
|
596
|
+
if (targets.length > 0) return targets;
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function backupPathFor(path: string): string {
|
|
602
|
+
const ext = extname(path);
|
|
603
|
+
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function shQuote(value: string): string {
|
|
607
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Provenance is required rather than defaulted: a default would let a caller pass an
|
|
611
|
+
// override binary and silently label it something else, which is precisely the
|
|
612
|
+
// path/marker disagreement this feature exists to prevent.
|
|
613
|
+
//
|
|
614
|
+
// The marker is scoped to the `ensure` invocation in every flavor below and is never
|
|
615
|
+
// exported into the shim's own environment. A shim wraps the real `codex`, so an
|
|
616
|
+
// exported marker would be inherited by Codex and everything it spawns — a shell that
|
|
617
|
+
// then ran a *different* Bun directly would carry a provenance describing a binary it
|
|
618
|
+
// is not executing.
|
|
619
|
+
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
|
|
620
|
+
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
621
|
+
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
622
|
+
return `#!/usr/bin/env sh
|
|
623
|
+
# ${SHIM_MARKER}
|
|
624
|
+
# ${UNIX_SHIM_REVISION_MARKER}
|
|
625
|
+
if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
|
|
626
|
+
if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
|
|
627
|
+
if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
|
|
628
|
+
(umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
|
|
629
|
+
fi
|
|
630
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
631
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
632
|
+
fi
|
|
633
|
+
OCX_SHIM_PROBE_ACTIVE=1
|
|
634
|
+
export OCX_SHIM_PROBE_ACTIVE
|
|
635
|
+
fi
|
|
636
|
+
if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
|
|
637
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
638
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
639
|
+
fi
|
|
640
|
+
case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
|
|
641
|
+
0)
|
|
642
|
+
OCX_SHIM_ACTIVE_DEPTH=1
|
|
643
|
+
;;
|
|
644
|
+
1)
|
|
645
|
+
OCX_SHIM_ACTIVE_DEPTH=2
|
|
646
|
+
;;
|
|
647
|
+
*)
|
|
648
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
649
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
650
|
+
;;
|
|
651
|
+
esac
|
|
652
|
+
# Dynamic launchers such as mise exec -- codex may resolve the command name
|
|
653
|
+
# back to this wrapper. An exec chain keeps the same PID. A legitimate nested
|
|
654
|
+
# Codex invocation may enter once with a new PID; repeated child-process
|
|
655
|
+
# redispatch reaches depth 2 and is rejected before it can form an infinite chain.
|
|
656
|
+
OCX_SHIM_ACTIVE_PID=$$
|
|
657
|
+
export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
|
|
658
|
+
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
659
|
+
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
660
|
+
export OPENCODEX_API_AUTH_TOKEN
|
|
661
|
+
fi
|
|
662
|
+
ocx_subcommand=""
|
|
663
|
+
ocx_skip_next=0
|
|
664
|
+
for ocx_arg in "$@"; do
|
|
665
|
+
if [ "$ocx_skip_next" -eq 1 ]; then
|
|
666
|
+
ocx_skip_next=0
|
|
667
|
+
continue
|
|
668
|
+
fi
|
|
669
|
+
case "$ocx_arg" in
|
|
670
|
+
--)
|
|
671
|
+
break
|
|
672
|
+
;;
|
|
673
|
+
${valueOptions})
|
|
674
|
+
ocx_skip_next=1
|
|
675
|
+
;;
|
|
676
|
+
--help|-h|--version|-V)
|
|
677
|
+
ocx_subcommand="$ocx_arg"
|
|
678
|
+
break
|
|
679
|
+
;;
|
|
680
|
+
-*)
|
|
681
|
+
;;
|
|
682
|
+
*)
|
|
683
|
+
ocx_subcommand="$ocx_arg"
|
|
684
|
+
break
|
|
685
|
+
;;
|
|
686
|
+
esac
|
|
687
|
+
done
|
|
688
|
+
case "$ocx_subcommand" in
|
|
689
|
+
${internalCommands}|--help|-h|--version|-V)
|
|
690
|
+
;;
|
|
691
|
+
*)
|
|
692
|
+
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
693
|
+
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
694
|
+
fi
|
|
695
|
+
;;
|
|
696
|
+
esac
|
|
697
|
+
exec ${shQuote(realCodexPath)} "$@"
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
type UnixShimProbeResult = "cleanup" | "descendants" | "failed" | "recursive" | "timeout" | null;
|
|
702
|
+
|
|
703
|
+
let codexShimProbeHookForTests: (() => void) | null = null;
|
|
704
|
+
let codexShimProbeShellForTests: string | null = null;
|
|
705
|
+
let codexShimGuardedWriteHookForTests: (() => void) | null = null;
|
|
706
|
+
let codexShimFreshWriteHookForTests: (() => void) | null = null;
|
|
707
|
+
let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
|
|
708
|
+
let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
709
|
+
|
|
710
|
+
/** Narrow deterministic seam for transaction rollback tests. */
|
|
711
|
+
export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
|
|
712
|
+
codexShimProbeHookForTests = hook;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Selects a POSIX shell only for cross-shell probe regression tests. */
|
|
716
|
+
export function setCodexShimProbeShellForTests(path: string | null): void {
|
|
717
|
+
codexShimProbeShellForTests = path;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Shortens the successful-launcher observation window only for focused tests. */
|
|
721
|
+
export function setCodexShimProbeObservationMsForTests(value: number | null): void {
|
|
722
|
+
codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Narrow deterministic seam for guarded partial-write rollback tests. */
|
|
726
|
+
export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
|
|
727
|
+
codexShimGuardedWriteHookForTests = hook;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Narrow deterministic seam for fresh-install partial-write rollback tests. */
|
|
731
|
+
export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): void {
|
|
732
|
+
codexShimFreshWriteHookForTests = hook;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* @internal Test-only seam for the rollback restore race.
|
|
737
|
+
*
|
|
738
|
+
* The window this closes opens after `sourceOccupied` is sampled and closes when
|
|
739
|
+
* the backup is republished, so no earlier hook can reach it: publishing from
|
|
740
|
+
* the fresh-write hook makes `sourceOccupied` true and skips the restore
|
|
741
|
+
* entirely.
|
|
742
|
+
*/
|
|
743
|
+
export function setCodexShimRollbackRestoreHookForTests(
|
|
744
|
+
hook: ((target: ShimFileState) => void) | null,
|
|
745
|
+
): void {
|
|
746
|
+
codexShimRollbackRestoreHookForTests = hook;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function readProbeMetadata(path: string, maxBytes: number): string | null {
|
|
750
|
+
try {
|
|
751
|
+
if (!existsSync(path)) return "";
|
|
752
|
+
const stat = lstatSync(path);
|
|
753
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
754
|
+
return readFileSync(path, "utf8").trim();
|
|
755
|
+
} catch {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
761
|
+
if (process.platform === "win32") return null;
|
|
762
|
+
const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
|
|
763
|
+
const markerPath = join(probeDir, "result");
|
|
764
|
+
const reentryPath = join(probeDir, "reentry");
|
|
765
|
+
const groupPath = join(probeDir, "group");
|
|
766
|
+
const stderrPath = join(probeDir, "stderr");
|
|
767
|
+
const env: NodeJS.ProcessEnv = {
|
|
768
|
+
...process.env,
|
|
769
|
+
OCX_SHIM_BYPASS: "1",
|
|
770
|
+
OCX_SHIM_PROBE: "1",
|
|
771
|
+
OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
|
|
772
|
+
};
|
|
773
|
+
delete env.OCX_SHIM_ACTIVE_PID;
|
|
774
|
+
delete env.OCX_SHIM_ACTIVE_DEPTH;
|
|
775
|
+
delete env.OCX_SHIM_PROBE_ACTIVE;
|
|
776
|
+
let groupId = 0;
|
|
777
|
+
try {
|
|
778
|
+
chmodSync(probeDir, 0o700);
|
|
779
|
+
const result = spawnSync(process.execPath, [
|
|
780
|
+
"-e",
|
|
781
|
+
CODEX_SHIM_INSTALL_PROBE_SCRIPT,
|
|
782
|
+
markerPath,
|
|
783
|
+
reentryPath,
|
|
784
|
+
groupPath,
|
|
785
|
+
stderrPath,
|
|
786
|
+
codexShimProbeShellForTests ?? "/bin/sh",
|
|
787
|
+
wrapperPath,
|
|
788
|
+
String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
|
|
789
|
+
String(MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
790
|
+
String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
|
|
791
|
+
String(codexShimProbeObservationMs),
|
|
792
|
+
], {
|
|
793
|
+
encoding: "utf8",
|
|
794
|
+
env,
|
|
795
|
+
timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
|
|
796
|
+
killSignal: "SIGKILL",
|
|
797
|
+
});
|
|
798
|
+
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
|
799
|
+
const marker = readProbeMetadata(markerPath, 64);
|
|
800
|
+
const reentryMarker = readProbeMetadata(reentryPath, 64);
|
|
801
|
+
const groupText = readProbeMetadata(groupPath, 64);
|
|
802
|
+
const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
803
|
+
groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
|
|
804
|
+
if (marker === null || reentryMarker === null || groupText === null || launcherStderr === null
|
|
805
|
+
|| !Number.isInteger(groupId) || groupId <= 0) return "cleanup";
|
|
806
|
+
const groupSurvived = unixProcessGroupAlive(groupId);
|
|
807
|
+
if (timedOut || marker || reentryMarker || groupSurvived) {
|
|
808
|
+
try {
|
|
809
|
+
terminateUnixProcessGroup(groupId);
|
|
810
|
+
} catch {
|
|
811
|
+
return "cleanup";
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (result.error && !timedOut) return "cleanup";
|
|
815
|
+
if (timedOut || marker === "timeout") return "timeout";
|
|
816
|
+
if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
|
|
817
|
+
if (reentryMarker !== "") return "cleanup";
|
|
818
|
+
if (marker === "descendants") return "descendants";
|
|
819
|
+
if (groupSurvived) return "descendants";
|
|
820
|
+
if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
|
|
821
|
+
return "recursive";
|
|
822
|
+
}
|
|
823
|
+
if (result.status !== 0) return "failed";
|
|
824
|
+
return null;
|
|
825
|
+
} catch {
|
|
826
|
+
if (Number.isInteger(groupId) && groupId > 0) {
|
|
827
|
+
try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
|
|
828
|
+
}
|
|
829
|
+
return "cleanup";
|
|
830
|
+
} finally {
|
|
831
|
+
try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
|
|
836
|
+
if (process.platform === "win32") return null;
|
|
837
|
+
codexShimProbeHookForTests?.();
|
|
838
|
+
return files
|
|
839
|
+
.filter(file => !file.preserveOnly)
|
|
840
|
+
.map(file => probeUnixShimInstall(file.wrapperPath))
|
|
841
|
+
.find(result => result !== null) ?? null;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function unixProcessGroupAlive(groupId: number): boolean {
|
|
845
|
+
try {
|
|
846
|
+
process.kill(-groupId, 0);
|
|
847
|
+
return true;
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function terminateUnixProcessGroup(groupId: number): void {
|
|
854
|
+
try {
|
|
855
|
+
process.kill(-groupId, "SIGKILL");
|
|
856
|
+
} catch (error) {
|
|
857
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
858
|
+
}
|
|
859
|
+
const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
|
|
860
|
+
while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
|
|
861
|
+
if (unixProcessGroupAlive(groupId)) {
|
|
862
|
+
throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
interface FreshShimInstallJournalEntry {
|
|
867
|
+
target: ShimFileState;
|
|
868
|
+
movedOriginalFingerprint?: ShimPathFingerprint;
|
|
869
|
+
originalMovedToBackup: boolean;
|
|
870
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
871
|
+
wrapperWriteStarted: boolean;
|
|
872
|
+
/** dev/ino of the file our `writeShim()` created, recorded before anything can fail. */
|
|
873
|
+
writtenWrapperInode?: { dev: number; ino: number };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry[]): void {
|
|
877
|
+
const errors: Error[] = [];
|
|
878
|
+
for (const entry of [...journal].reverse()) {
|
|
879
|
+
const target = entry.target;
|
|
880
|
+
let sourceOccupied = false;
|
|
881
|
+
let ownsWrapperNow = false;
|
|
882
|
+
try {
|
|
883
|
+
const wrapper = stableShimPathProbe(target.wrapperPath);
|
|
884
|
+
// Ownership is the inode our own write created. There is no marker-text
|
|
885
|
+
// fallback: the markers are public, so a concurrent updater's wrapper carries
|
|
886
|
+
// them too, and treating that as proof is how we would delete a file we never
|
|
887
|
+
// wrote. An in-place truncation of our file keeps the inode and is still ours
|
|
888
|
+
// to clean up; a replacement renamed over it has a different inode and is not.
|
|
889
|
+
const ownsWrapper = !target.preserveOnly
|
|
890
|
+
&& entry.wrapperWriteStarted
|
|
891
|
+
&& wrapper !== null
|
|
892
|
+
&& wrapperInodeIsOurs(wrapper, entry.writtenWrapperInode, entry.writtenWrapperFingerprint);
|
|
893
|
+
ownsWrapperNow = ownsWrapper;
|
|
894
|
+
if (ownsWrapper) unlinkSync(target.wrapperPath);
|
|
895
|
+
else {
|
|
896
|
+
try {
|
|
897
|
+
lstatSync(target.originalPath);
|
|
898
|
+
sourceOccupied = true;
|
|
899
|
+
} catch (error) {
|
|
900
|
+
if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
} catch (error) {
|
|
904
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
if (entry.originalMovedToBackup && existsSync(target.backupPath)) {
|
|
908
|
+
const movedOriginal = shimPathFingerprint(target.backupPath);
|
|
909
|
+
if (!movedOriginal || !entry.movedOriginalFingerprint
|
|
910
|
+
|| !sameFingerprint(movedOriginal, entry.movedOriginalFingerprint)) {
|
|
911
|
+
throw new Error("Codex shim fresh-install backup changed during rollback");
|
|
912
|
+
}
|
|
913
|
+
if (sourceOccupied) {
|
|
914
|
+
// Something else occupies the source path. Dropping the backup is correct
|
|
915
|
+
// only when that something is a file we own; when a concurrent updater
|
|
916
|
+
// owns it, this backup is the user's real launcher and deleting it would
|
|
917
|
+
// lose the command entirely. Keep it in that case — a stray
|
|
918
|
+
// `codex.opencodex-real` is recoverable, a deleted launcher is not.
|
|
919
|
+
if (ownsWrapperNow) unlinkSync(target.backupPath);
|
|
920
|
+
} else {
|
|
921
|
+
// No-replace: sourceOccupied was sampled earlier, so a concurrent
|
|
922
|
+
// installer may have published a launcher at the original path since.
|
|
923
|
+
codexShimRollbackRestoreHookForTests?.(target);
|
|
924
|
+
restoreWithoutReplacing(target.backupPath, target.originalPath);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} catch (error) {
|
|
928
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function windowsBatchValue(value: string): string {
|
|
935
|
+
return value
|
|
936
|
+
.replace(/%/g, "%%")
|
|
937
|
+
.replace(/\^/g, "^^")
|
|
938
|
+
.replace(/"/g, "")
|
|
939
|
+
.replace(/[\r\n]/g, "");
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function windowsBatchSet(name: string, value: string): string {
|
|
943
|
+
// Paths are rewritten to %USERPROFILE%-style env indirection: cmd.exe parses .cmd
|
|
944
|
+
// files in the OEM codepage, so a literal non-ASCII profile prefix (Korean/Chinese
|
|
945
|
+
// usernames) written as UTF-8 turns to mojibake. The env token expands natively in
|
|
946
|
+
// the right codepage at parse time; no `chcp` here — this shim runs in the USER's
|
|
947
|
+
// console and must not leak a codepage change into it.
|
|
948
|
+
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
952
|
+
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
953
|
+
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
954
|
+
return `@echo off\r
|
|
955
|
+
rem ${SHIM_MARKER}\r
|
|
956
|
+
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
|
|
957
|
+
${windowsBatchSet("OCX_BUN", bunPath)}\r
|
|
958
|
+
${windowsBatchSet("OCX_CLI", cliPath)}\r
|
|
959
|
+
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
|
|
960
|
+
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
|
|
961
|
+
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
962
|
+
goto scan_codex_args\r
|
|
963
|
+
:scan_codex_args\r
|
|
964
|
+
if "%~1"=="" goto ensure_ocx\r
|
|
965
|
+
if "%~1"=="--" goto ensure_ocx\r
|
|
966
|
+
${valueOptionChecks}\r
|
|
967
|
+
${internalCommandChecks}\r
|
|
968
|
+
if /I "%~1"=="--help" goto run_codex\r
|
|
969
|
+
if /I "%~1"=="-h" goto run_codex\r
|
|
970
|
+
if /I "%~1"=="--version" goto run_codex\r
|
|
971
|
+
if /I "%~1"=="-V" goto run_codex\r
|
|
972
|
+
set "OCX_SCAN_ARG=%~1"\r
|
|
973
|
+
if "%OCX_SCAN_ARG:~0,1%"=="-" goto shift_codex_arg\r
|
|
974
|
+
goto ensure_ocx\r
|
|
975
|
+
:skip_option_value\r
|
|
976
|
+
shift\r
|
|
977
|
+
if "%~1"=="" goto ensure_ocx\r
|
|
978
|
+
:shift_codex_arg\r
|
|
979
|
+
shift\r
|
|
980
|
+
goto scan_codex_args\r
|
|
981
|
+
:ensure_ocx\r
|
|
982
|
+
setlocal\r
|
|
983
|
+
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
984
|
+
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
985
|
+
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
986
|
+
endlocal\r
|
|
987
|
+
:run_codex\r
|
|
988
|
+
"%OCX_REAL_CODEX%" %*\r
|
|
989
|
+
`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function psString(value: string): string {
|
|
993
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
997
|
+
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
998
|
+
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
999
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
1000
|
+
return `#!/usr/bin/env pwsh
|
|
1001
|
+
# ${SHIM_MARKER}
|
|
1002
|
+
if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
|
|
1003
|
+
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
|
|
1004
|
+
}
|
|
1005
|
+
$internalCommands = @(${internalCommands})
|
|
1006
|
+
$valueOptions = @(${valueOptions})
|
|
1007
|
+
$subcommand = ""
|
|
1008
|
+
$skipNext = $false
|
|
1009
|
+
foreach ($argValue in $args) {
|
|
1010
|
+
$argText = [string]$argValue
|
|
1011
|
+
if ($skipNext) { $skipNext = $false; continue }
|
|
1012
|
+
if ($argText -eq "--") { break }
|
|
1013
|
+
if ($valueOptions -contains $argText) { $skipNext = $true; continue }
|
|
1014
|
+
if (@("--help", "-h", "--version", "-V") -contains $argText) { $subcommand = $argText; break }
|
|
1015
|
+
if ($argText.StartsWith("-")) { continue }
|
|
1016
|
+
$subcommand = $argText
|
|
1017
|
+
break
|
|
1018
|
+
}
|
|
1019
|
+
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
1020
|
+
if (-not $skipEnsure) {
|
|
1021
|
+
$priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
|
|
1022
|
+
$priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
|
|
1023
|
+
$env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
|
|
1024
|
+
$env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
|
|
1025
|
+
try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
|
|
1026
|
+
finally {
|
|
1027
|
+
if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
|
|
1028
|
+
else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
|
|
1029
|
+
if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
|
|
1030
|
+
else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
& ${psString(realCodexPath)} @args
|
|
1034
|
+
exit $LASTEXITCODE
|
|
1035
|
+
`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
interface ShimStateReadResult {
|
|
1039
|
+
state: ShimState | null;
|
|
1040
|
+
warning?: string;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function fileErrorCode(error: unknown): string | undefined {
|
|
1044
|
+
return error && typeof error === "object" && "code" in error
|
|
1045
|
+
? String((error as { code?: unknown }).code)
|
|
1046
|
+
: undefined;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function readBoundedRegularFile(path: string, maxBytes: number): { content: string } | { warning: string } | null {
|
|
1050
|
+
let fd: number;
|
|
1051
|
+
try {
|
|
1052
|
+
fd = openSync(path, "r");
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
if (fileErrorCode(error) === "ENOENT") return null;
|
|
1055
|
+
return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const before = fstatSync(fd);
|
|
1059
|
+
if (!before.isFile()) return { warning: `Codex shim state is not a regular file at ${path}; auto-restore skipped.` };
|
|
1060
|
+
if (before.size > maxBytes) {
|
|
1061
|
+
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
1062
|
+
}
|
|
1063
|
+
const buffer = Buffer.allocUnsafe(before.size);
|
|
1064
|
+
let offset = 0;
|
|
1065
|
+
while (offset < buffer.length) {
|
|
1066
|
+
const bytesRead = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
1067
|
+
if (bytesRead === 0) return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1068
|
+
offset += bytesRead;
|
|
1069
|
+
}
|
|
1070
|
+
const extra = Buffer.allocUnsafe(1);
|
|
1071
|
+
if (readSync(fd, extra, 0, 1, offset) !== 0) {
|
|
1072
|
+
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
1073
|
+
}
|
|
1074
|
+
const after = fstatSync(fd);
|
|
1075
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
1076
|
+
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
1077
|
+
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1078
|
+
}
|
|
1079
|
+
return { content: buffer.toString("utf8") };
|
|
1080
|
+
} finally {
|
|
1081
|
+
closeSync(fd);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function readStateResult(): ShimStateReadResult {
|
|
1086
|
+
const bounded = readBoundedRegularFile(statePath(), CODEX_SHIM_STATE_MAX_BYTES);
|
|
1087
|
+
if (!bounded) return { state: null };
|
|
1088
|
+
if ("warning" in bounded) return { state: null, warning: bounded.warning };
|
|
1089
|
+
try {
|
|
1090
|
+
const value = JSON.parse(bounded.content) as unknown;
|
|
1091
|
+
if (!value || typeof value !== "object") return { state: null };
|
|
1092
|
+
const state = value as Record<string, unknown>;
|
|
1093
|
+
if (typeof state.platform !== "string") return { state: null };
|
|
1094
|
+
const validFile = (item: unknown): item is ShimFileState => {
|
|
1095
|
+
if (!item || typeof item !== "object") return false;
|
|
1096
|
+
const file = item as Record<string, unknown>;
|
|
1097
|
+
return typeof file.wrapperPath === "string"
|
|
1098
|
+
&& typeof file.originalPath === "string"
|
|
1099
|
+
&& typeof file.backupPath === "string"
|
|
1100
|
+
&& (file.realPath === undefined || typeof file.realPath === "string")
|
|
1101
|
+
&& (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
|
|
1102
|
+
};
|
|
1103
|
+
if (state.wrappers !== undefined) {
|
|
1104
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null };
|
|
1105
|
+
} else if (!validFile(state)) {
|
|
1106
|
+
return { state: null };
|
|
1107
|
+
}
|
|
1108
|
+
return { state: state as unknown as ShimState };
|
|
1109
|
+
} catch {
|
|
1110
|
+
return { state: null };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function readState(): ShimState | null {
|
|
1115
|
+
return readStateResult().state;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function statePath(): string {
|
|
1119
|
+
return join(getConfigDir(), "codex-shim.json");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function writeState(state: ShimState): void {
|
|
1123
|
+
const path = statePath();
|
|
1124
|
+
recordOwnedConfigPath(getConfigDir(), path);
|
|
1125
|
+
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1126
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/** Git-Bash accepts `C:/...` but not backslashed paths inside sh scripts. */
|
|
1130
|
+
function gitBashPath(path: string): string {
|
|
1131
|
+
return path.replace(/\\/g, "/");
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Write the wrapper and return the identity of the inode this call created, or
|
|
1136
|
+
* `undefined` where the platform still writes the destination in place.
|
|
1137
|
+
*
|
|
1138
|
+
* Callers must derive ownership from the returned identity rather than from a
|
|
1139
|
+
* later `stat` of `wrapperPath`: the shim markers are public, so a concurrent
|
|
1140
|
+
* updater's wrapper can carry them, and a replacement landing between the write
|
|
1141
|
+
* and the observation is otherwise indistinguishable from our own file.
|
|
1142
|
+
*/
|
|
1143
|
+
function writeShim(wrapperPath: string, realCodexPath: string): { dev: number; ino: number } | undefined {
|
|
1144
|
+
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
1145
|
+
if (process.platform === "win32") {
|
|
1146
|
+
const lower = wrapperPath.toLowerCase();
|
|
1147
|
+
if (lower.endsWith(".ps1")) {
|
|
1148
|
+
// UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
|
|
1149
|
+
// codepage, which mangles non-ASCII paths embedded in the shim.
|
|
1150
|
+
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
|
|
1151
|
+
} else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
1152
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
|
|
1153
|
+
} else {
|
|
1154
|
+
// Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
|
|
1155
|
+
writeFileSync(
|
|
1156
|
+
wrapperPath,
|
|
1157
|
+
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), bunRuntimeSource, gitBashPath(serviceApiTokenFilePath())),
|
|
1158
|
+
"utf8",
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
return undefined;
|
|
1162
|
+
} else {
|
|
1163
|
+
// Stage the wrapper as its own inode and rename it into place, so ownership
|
|
1164
|
+
// comes from the write itself rather than from observing the path afterwards.
|
|
1165
|
+
// Writing the destination directly leaves a window in which a concurrent
|
|
1166
|
+
// updater can replace the file between our write and our fingerprint; we would
|
|
1167
|
+
// then adopt that replacement as ours and unlink it during rollback, deleting
|
|
1168
|
+
// an executable we never wrote.
|
|
1169
|
+
// Hidden and non-executable while staged, so a crash between the write and the
|
|
1170
|
+
// rename cannot leave an executable `codex*` artifact that a glob or a shell
|
|
1171
|
+
// completion would surface.
|
|
1172
|
+
const staged = join(dirname(wrapperPath), `.${basename(wrapperPath)}.opencodex-staging.${process.pid}.${randomUUID()}`);
|
|
1173
|
+
let renamed = false;
|
|
1174
|
+
try {
|
|
1175
|
+
// "wx" fails if the staging path somehow exists, so we never inherit a file.
|
|
1176
|
+
writeFileSync(staged, buildUnixCodexShim(realCodexPath, bun, cli, bunRuntimeSource), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
1177
|
+
const stagedStat = lstatSync(staged);
|
|
1178
|
+
chmodSync(staged, 0o755);
|
|
1179
|
+
renameSync(staged, wrapperPath);
|
|
1180
|
+
renamed = true;
|
|
1181
|
+
// rename() preserves dev/ino and updates ctime, so identity is the inode
|
|
1182
|
+
// pair, captured from the file we created rather than from the destination.
|
|
1183
|
+
return { dev: stagedStat.dev, ino: stagedStat.ino };
|
|
1184
|
+
} finally {
|
|
1185
|
+
if (!renamed) {
|
|
1186
|
+
try { unlinkSync(staged); } catch { /* best-effort: nothing to clean up */ }
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* The fingerprint to record as "we wrote this", or `undefined` when the file at
|
|
1194
|
+
* `wrapperPath` is not the inode `writeShim()` created. Returning `undefined`
|
|
1195
|
+
* makes every rollback path treat the file as someone else's and leave it alone.
|
|
1196
|
+
*/
|
|
1197
|
+
function ownedWrapperFingerprint(
|
|
1198
|
+
wrapperPath: string,
|
|
1199
|
+
written: { dev: number; ino: number } | undefined,
|
|
1200
|
+
): ShimPathFingerprint | undefined {
|
|
1201
|
+
const probe = stableShimPathProbe(wrapperPath);
|
|
1202
|
+
if (!probe) return undefined;
|
|
1203
|
+
// Platforms that still write in place (Windows) have no staged identity; fall
|
|
1204
|
+
// back to the marker check they have always used.
|
|
1205
|
+
if (!written) return probe.prefix.includes(SHIM_MARKER) ? probe.fingerprint : undefined;
|
|
1206
|
+
if (probe.fingerprint.dev !== written.dev || probe.fingerprint.ino !== written.ino) return undefined;
|
|
1207
|
+
return probe.fingerprint;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Whether the file now at the wrapper path is one this transaction may unlink.
|
|
1212
|
+
*
|
|
1213
|
+
* The inode our write created is the authority. It stays ours through an in-place
|
|
1214
|
+
* truncation — a partial write we must clean up — and stops being ours the moment
|
|
1215
|
+
* someone renames a different file over the path, which is exactly the concurrent
|
|
1216
|
+
* updater we must not delete. Where no inode was recorded (Windows writes the
|
|
1217
|
+
* destination directly), fall back to the exact fingerprint recorded at the time.
|
|
1218
|
+
*/
|
|
1219
|
+
function wrapperInodeIsOurs(
|
|
1220
|
+
wrapper: StableShimPathProbe,
|
|
1221
|
+
written: { dev: number; ino: number } | undefined,
|
|
1222
|
+
recorded: ShimPathFingerprint | undefined,
|
|
1223
|
+
): boolean {
|
|
1224
|
+
// A different inode is conclusive: someone renamed their own file over ours.
|
|
1225
|
+
if (written && (wrapper.fingerprint.dev !== written.dev || wrapper.fingerprint.ino !== written.ino)) {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
// Same inode is not sufficient on its own — an in-place truncation (shell `>`,
|
|
1229
|
+
// writeFileSync) keeps it while replacing the contents. When we recorded a full
|
|
1230
|
+
// fingerprint, require it to still match; that covers our own partial write,
|
|
1231
|
+
// whose fingerprint we record before anything can fail.
|
|
1232
|
+
if (recorded !== undefined) return sameFingerprint(wrapper.fingerprint, recorded);
|
|
1233
|
+
// No recorded fingerprint: only the inode identity we captured at write time can
|
|
1234
|
+
// speak for us, and there is nothing else to distinguish this file.
|
|
1235
|
+
return written !== undefined;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function stateFiles(state: ShimState): ShimFileState[] {
|
|
1239
|
+
return state.wrappers?.length
|
|
1240
|
+
? state.wrappers
|
|
1241
|
+
: [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function primaryState(files: ShimFileState[]): ShimState {
|
|
1245
|
+
const first = files[0];
|
|
1246
|
+
return { platform: process.platform, ...first, wrappers: files };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function replaceOwnedBackup(sourcePath: string, backupPath: string): void {
|
|
1250
|
+
const oldBackupPath = `${backupPath}.old-${process.pid}`;
|
|
1251
|
+
if (existsSync(oldBackupPath)) unlinkSync(oldBackupPath);
|
|
1252
|
+
if (existsSync(backupPath)) renameSync(backupPath, oldBackupPath);
|
|
1253
|
+
try {
|
|
1254
|
+
renameSync(sourcePath, backupPath);
|
|
1255
|
+
if (existsSync(oldBackupPath)) unlinkSync(oldBackupPath);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if (!existsSync(backupPath) && existsSync(oldBackupPath)) renameSync(oldBackupPath, backupPath);
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function refreshShimFile(file: ShimFileState): boolean {
|
|
1263
|
+
if (file.preserveOnly) {
|
|
1264
|
+
if (existsSync(file.originalPath) && !isShim(file.originalPath)) {
|
|
1265
|
+
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
1271
|
+
if (file.wrapperPath !== file.originalPath) return false;
|
|
1272
|
+
const replacement = stableShimPathProbe(file.wrapperPath);
|
|
1273
|
+
if (!replacement) return false;
|
|
1274
|
+
return applyGuardedRefreshTransaction([{
|
|
1275
|
+
file,
|
|
1276
|
+
expectedReplacement: replacement.fingerprint,
|
|
1277
|
+
sourcePath: file.wrapperPath,
|
|
1278
|
+
}]);
|
|
1279
|
+
}
|
|
1280
|
+
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
1281
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1282
|
+
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1283
|
+
if (!writtenWrapper || !writtenWrapper.prefix.includes(SHIM_MARKER)) {
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1287
|
+
let probeError: Error | null = null;
|
|
1288
|
+
try {
|
|
1289
|
+
unsafe = probeUnixShimFiles([file]);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1292
|
+
}
|
|
1293
|
+
const currentWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1294
|
+
const wrapperChangedDuringProbe = !currentWrapper
|
|
1295
|
+
|| !sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint);
|
|
1296
|
+
if (unsafe !== null || probeError || wrapperChangedDuringProbe) {
|
|
1297
|
+
if (currentWrapper && sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint)) {
|
|
1298
|
+
unlinkSync(file.wrapperPath);
|
|
1299
|
+
}
|
|
1300
|
+
if (probeError) throw probeError;
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
1306
|
+
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
1307
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
interface GuardedRefreshOperation {
|
|
1314
|
+
file: ShimFileState;
|
|
1315
|
+
expectedReplacement: ShimPathFingerprint;
|
|
1316
|
+
sourcePath: string;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
interface GuardedRefreshJournalEntry {
|
|
1320
|
+
operation: GuardedRefreshOperation;
|
|
1321
|
+
stagedOldBackupPath?: string;
|
|
1322
|
+
movedReplacementFingerprint?: ShimPathFingerprint;
|
|
1323
|
+
replacementMovedToBackup: boolean;
|
|
1324
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
1325
|
+
wrapperWriteStarted: boolean;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
let guardedRefreshTransactionId = 0;
|
|
1329
|
+
|
|
1330
|
+
interface ShimRestoreLock {
|
|
1331
|
+
release(): void;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
interface ShimRestoreLockRecord {
|
|
1335
|
+
version: 1;
|
|
1336
|
+
token: string;
|
|
1337
|
+
pid: number;
|
|
1338
|
+
createdAt: number;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
interface ShimRestoreLockSnapshot {
|
|
1342
|
+
record: ShimRestoreLockRecord;
|
|
1343
|
+
ownerPath: string;
|
|
1344
|
+
lockIdentity: Pick<Stats, "dev" | "ino">;
|
|
1345
|
+
fingerprint: ShimPathFingerprint;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function restoreLockPath(): string {
|
|
1349
|
+
return join(getConfigDir(), "codex-shim.autorestore.lock");
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function sameFileIdentity(left: Pick<Stats, "dev" | "ino">, right: Pick<Stats, "dev" | "ino">): boolean {
|
|
1353
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function readShimRestoreLockSnapshot(path: string): ShimRestoreLockSnapshot | null {
|
|
1357
|
+
let lockIdentity: Stats;
|
|
1358
|
+
let entries: string[];
|
|
1359
|
+
try {
|
|
1360
|
+
lockIdentity = lstatSync(path);
|
|
1361
|
+
if (!lockIdentity.isDirectory()) return null;
|
|
1362
|
+
entries = readdirSync(path);
|
|
1363
|
+
} catch {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
if (entries.length !== 1 || !entries[0].endsWith(".json")) return null;
|
|
1367
|
+
const ownerPath = join(path, entries[0]);
|
|
1368
|
+
const probe = stableShimPathProbe(ownerPath);
|
|
1369
|
+
if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
|
|
1370
|
+
try {
|
|
1371
|
+
const value = JSON.parse(probe.prefix) as Partial<ShimRestoreLockRecord>;
|
|
1372
|
+
if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
|
|
1373
|
+
|| typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
|
|
1374
|
+
|| typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
|
|
1375
|
+
if (entries[0] !== `${value.token}.json`) return null;
|
|
1376
|
+
const currentLockIdentity = lstatSync(path);
|
|
1377
|
+
if (!currentLockIdentity.isDirectory() || !sameFileIdentity(lockIdentity, currentLockIdentity)) return null;
|
|
1378
|
+
return {
|
|
1379
|
+
record: value as ShimRestoreLockRecord,
|
|
1380
|
+
ownerPath,
|
|
1381
|
+
lockIdentity,
|
|
1382
|
+
fingerprint: probe.fingerprint,
|
|
1383
|
+
};
|
|
1384
|
+
} catch {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function sameShimRestoreLock(left: ShimRestoreLockSnapshot, right: ShimRestoreLockSnapshot): boolean {
|
|
1390
|
+
return left.record.token === right.record.token
|
|
1391
|
+
&& sameFileIdentity(left.lockIdentity, right.lockIdentity)
|
|
1392
|
+
&& sameFingerprint(left.fingerprint, right.fingerprint);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function reclaimStaleRestoreLock(path: string, beforeDelete?: () => void): boolean {
|
|
1396
|
+
const observed = readShimRestoreLockSnapshot(path);
|
|
1397
|
+
if (!observed) return false;
|
|
1398
|
+
const createdAt = Math.max(observed.record.createdAt, observed.fingerprint.mtimeMs);
|
|
1399
|
+
if (Date.now() - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
|
|
1400
|
+
if (isProcessAlive(observed.record.pid)) return false;
|
|
1401
|
+
const current = readShimRestoreLockSnapshot(path);
|
|
1402
|
+
if (!current || !sameShimRestoreLock(observed, current)) return false;
|
|
1403
|
+
beforeDelete?.();
|
|
1404
|
+
try {
|
|
1405
|
+
// The token is part of the owner filename. Even if the lock directory is
|
|
1406
|
+
// replaced after the comparison, this unlink cannot target a successor's
|
|
1407
|
+
// differently named owner record.
|
|
1408
|
+
unlinkSync(observed.ownerPath);
|
|
1409
|
+
rmdirSync(path);
|
|
1410
|
+
return true;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreLock | null {
|
|
1417
|
+
const dir = getConfigDir();
|
|
1418
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1419
|
+
const path = restoreLockPath();
|
|
1420
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1421
|
+
let fd: number | null = null;
|
|
1422
|
+
let identity: Stats | null = null;
|
|
1423
|
+
let createdDirectory = false;
|
|
1424
|
+
const record: ShimRestoreLockRecord = {
|
|
1425
|
+
version: 1,
|
|
1426
|
+
token: `${process.pid}-${Date.now()}-${randomUUID()}`,
|
|
1427
|
+
pid: process.pid,
|
|
1428
|
+
createdAt: Date.now(),
|
|
1429
|
+
};
|
|
1430
|
+
const ownerPath = join(path, `${record.token}.json`);
|
|
1431
|
+
try {
|
|
1432
|
+
mkdirSync(path, { mode: 0o700 });
|
|
1433
|
+
createdDirectory = true;
|
|
1434
|
+
fd = openSync(ownerPath, "wx", 0o600);
|
|
1435
|
+
identity = fstatSync(fd);
|
|
1436
|
+
writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf8");
|
|
1437
|
+
identity = fstatSync(fd);
|
|
1438
|
+
let released = false;
|
|
1439
|
+
return {
|
|
1440
|
+
release(): void {
|
|
1441
|
+
if (released) return;
|
|
1442
|
+
released = true;
|
|
1443
|
+
try { closeSync(fd!); } catch { /* stale recovery handles an uncertain lock */ }
|
|
1444
|
+
try {
|
|
1445
|
+
const current = readShimRestoreLockSnapshot(path);
|
|
1446
|
+
if (identity && current && current.record.token === record.token
|
|
1447
|
+
&& sameFileIdentity(identity, current.fingerprint)) {
|
|
1448
|
+
unlinkSync(ownerPath);
|
|
1449
|
+
rmdirSync(path);
|
|
1450
|
+
}
|
|
1451
|
+
} catch { /* stale recovery handles release failures */ }
|
|
1452
|
+
},
|
|
1453
|
+
};
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
if (fd !== null) {
|
|
1456
|
+
try { closeSync(fd); } catch { /* best-effort close before ownership cleanup */ }
|
|
1457
|
+
try {
|
|
1458
|
+
const current = readShimRestoreLockSnapshot(path);
|
|
1459
|
+
if (identity && current && current.record.token === record.token
|
|
1460
|
+
&& sameFileIdentity(identity, current.fingerprint)) {
|
|
1461
|
+
unlinkSync(ownerPath);
|
|
1462
|
+
rmdirSync(path);
|
|
1463
|
+
}
|
|
1464
|
+
} catch { /* leave an uncertain lock for stale recovery */ }
|
|
1465
|
+
} else if (createdDirectory) {
|
|
1466
|
+
try { rmdirSync(path); } catch { /* another owner exists or cleanup is uncertain */ }
|
|
1467
|
+
}
|
|
1468
|
+
if (fileErrorCode(error) !== "EEXIST") throw error;
|
|
1469
|
+
if (attempt === 0 && reclaimStaleRestoreLock(path, beforeStaleDelete)) continue;
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function planGuardedRefreshTransaction(
|
|
1477
|
+
files: readonly ShimFileState[],
|
|
1478
|
+
expectedReplacements: ReadonlyMap<string, ShimPathFingerprint>,
|
|
1479
|
+
): GuardedRefreshOperation[] | null {
|
|
1480
|
+
const operations: GuardedRefreshOperation[] = [];
|
|
1481
|
+
const seen = new Set<string>();
|
|
1482
|
+
for (const file of files) {
|
|
1483
|
+
if (seen.has(file.wrapperPath)) return null;
|
|
1484
|
+
seen.add(file.wrapperPath);
|
|
1485
|
+
if (file.preserveOnly) {
|
|
1486
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) return null;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (!hasUsableBackingPath(file)) return null;
|
|
1490
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1491
|
+
if (!probe) return null;
|
|
1492
|
+
const expectedReplacement = expectedReplacements.get(file.wrapperPath);
|
|
1493
|
+
if (!expectedReplacement) {
|
|
1494
|
+
if (!isHealthyShimProbe(probe, process.platform)) return null;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (file.wrapperPath !== file.originalPath
|
|
1498
|
+
|| probe.prefix.includes(SHIM_MARKER)
|
|
1499
|
+
|| !sameFingerprint(probe.fingerprint, expectedReplacement)) return null;
|
|
1500
|
+
operations.push({ file, expectedReplacement, sourcePath: file.wrapperPath });
|
|
1501
|
+
}
|
|
1502
|
+
if (operations.length !== expectedReplacements.size) return null;
|
|
1503
|
+
return operations;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]): Error[] {
|
|
1507
|
+
const rollbackErrors: Error[] = [];
|
|
1508
|
+
const attempt = (operation: () => void): void => {
|
|
1509
|
+
try {
|
|
1510
|
+
operation();
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
rollbackErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
for (const entry of [...journal].reverse()) {
|
|
1516
|
+
let sourceOccupied = false;
|
|
1517
|
+
attempt(() => {
|
|
1518
|
+
const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
|
|
1519
|
+
// No marker-text fallback: the markers are public, so a concurrent updater's
|
|
1520
|
+
// wrapper carries them too. No recorded inode identity means not ours.
|
|
1521
|
+
const ownsWrapper = entry.wrapperWriteStarted
|
|
1522
|
+
&& wrapper !== null
|
|
1523
|
+
&& entry.writtenWrapperFingerprint !== undefined
|
|
1524
|
+
&& sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1525
|
+
if (ownsWrapper) {
|
|
1526
|
+
unlinkSync(entry.operation.file.wrapperPath);
|
|
1527
|
+
} else {
|
|
1528
|
+
try {
|
|
1529
|
+
lstatSync(entry.operation.sourcePath);
|
|
1530
|
+
sourceOccupied = true;
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
attempt(() => {
|
|
1537
|
+
if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
|
|
1538
|
+
const movedReplacement = stableShimPathProbe(entry.operation.file.backupPath);
|
|
1539
|
+
if (!movedReplacement || !entry.movedReplacementFingerprint
|
|
1540
|
+
|| !sameFingerprint(movedReplacement.fingerprint, entry.movedReplacementFingerprint)) {
|
|
1541
|
+
throw new Error("Codex shim guarded refresh backup changed during rollback");
|
|
1542
|
+
}
|
|
1543
|
+
if (sourceOccupied) unlinkSync(entry.operation.file.backupPath);
|
|
1544
|
+
else renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
attempt(() => {
|
|
1548
|
+
if (entry.stagedOldBackupPath && existsSync(entry.stagedOldBackupPath)) {
|
|
1549
|
+
renameSync(entry.stagedOldBackupPath, entry.operation.file.backupPath);
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
return rollbackErrors;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function applyGuardedRefreshTransaction(
|
|
1557
|
+
operations: readonly GuardedRefreshOperation[],
|
|
1558
|
+
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void,
|
|
1559
|
+
commitState?: () => void,
|
|
1560
|
+
): boolean {
|
|
1561
|
+
const journal: GuardedRefreshJournalEntry[] = [];
|
|
1562
|
+
let applyError: Error | null = null;
|
|
1563
|
+
let fingerprintMismatch = false;
|
|
1564
|
+
let unsafeLauncher = false;
|
|
1565
|
+
let wrapperChangedDuringProbe = false;
|
|
1566
|
+
const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
|
|
1567
|
+
|
|
1568
|
+
for (const [index, operation] of operations.entries()) {
|
|
1569
|
+
beforeGuardedRefresh?.(operation.sourcePath, index);
|
|
1570
|
+
const probe = stableShimPathProbe(operation.sourcePath);
|
|
1571
|
+
if (!probe || !sameFingerprint(probe.fingerprint, operation.expectedReplacement)) {
|
|
1572
|
+
fingerprintMismatch = true;
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
const entry: GuardedRefreshJournalEntry = {
|
|
1576
|
+
operation,
|
|
1577
|
+
replacementMovedToBackup: false,
|
|
1578
|
+
wrapperWriteStarted: false,
|
|
1579
|
+
};
|
|
1580
|
+
journal.push(entry);
|
|
1581
|
+
try {
|
|
1582
|
+
if (existsSync(operation.file.backupPath)) {
|
|
1583
|
+
entry.stagedOldBackupPath = `${operation.file.backupPath}.autorestore-${transactionId}-${index}`;
|
|
1584
|
+
if (existsSync(entry.stagedOldBackupPath)) unlinkSync(entry.stagedOldBackupPath);
|
|
1585
|
+
renameSync(operation.file.backupPath, entry.stagedOldBackupPath);
|
|
1586
|
+
}
|
|
1587
|
+
renameSync(operation.sourcePath, operation.file.backupPath);
|
|
1588
|
+
entry.replacementMovedToBackup = true;
|
|
1589
|
+
const movedReplacement = stableShimPathProbe(operation.file.backupPath);
|
|
1590
|
+
if (!movedReplacement) throw new Error("Codex shim guarded refresh could not fingerprint the staged launcher");
|
|
1591
|
+
entry.movedReplacementFingerprint = movedReplacement.fingerprint;
|
|
1592
|
+
entry.wrapperWriteStarted = true;
|
|
1593
|
+
const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
1594
|
+
// Claim our own partial write before the hook can fail (see fresh install).
|
|
1595
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
1596
|
+
codexShimGuardedWriteHookForTests?.();
|
|
1597
|
+
// Re-check: unset means a concurrent writer owns the path now, so rollback
|
|
1598
|
+
// must leave it alone.
|
|
1599
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
1600
|
+
if (!entry.writtenWrapperFingerprint && !writtenInode) {
|
|
1601
|
+
throw new Error("Codex shim guarded refresh could not fingerprint the generated wrapper");
|
|
1602
|
+
}
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (!fingerprintMismatch && !applyError) {
|
|
1610
|
+
try {
|
|
1611
|
+
unsafeLauncher = probeUnixShimFiles(operations.map(operation => operation.file)) !== null;
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (!fingerprintMismatch && !applyError && !unsafeLauncher) {
|
|
1618
|
+
wrapperChangedDuringProbe = journal.some(entry => {
|
|
1619
|
+
const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
|
|
1620
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1621
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (!fingerprintMismatch && !applyError && !unsafeLauncher && !wrapperChangedDuringProbe && commitState) {
|
|
1626
|
+
try {
|
|
1627
|
+
commitState();
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (fingerprintMismatch || applyError || unsafeLauncher || wrapperChangedDuringProbe) {
|
|
1634
|
+
const rollbackErrors = rollbackGuardedRefresh(journal);
|
|
1635
|
+
if (applyError || rollbackErrors.length > 0) {
|
|
1636
|
+
throw new AggregateError(
|
|
1637
|
+
[...(applyError ? [applyError] : []), ...rollbackErrors],
|
|
1638
|
+
"Codex shim guarded refresh failed",
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const cleanupErrors: Error[] = [];
|
|
1645
|
+
for (const entry of journal) {
|
|
1646
|
+
try {
|
|
1647
|
+
if (entry.stagedOldBackupPath && existsSync(entry.stagedOldBackupPath)) unlinkSync(entry.stagedOldBackupPath);
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex shim guarded refresh cleanup failed");
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
interface ObsoleteUnixShimJournalEntry {
|
|
1657
|
+
file: ShimFileState;
|
|
1658
|
+
stagedWrapperPath: string;
|
|
1659
|
+
priorWrapperFingerprint: ShimPathFingerprint;
|
|
1660
|
+
backingFingerprint: ShimPathFingerprint;
|
|
1661
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
1662
|
+
wrapperWriteStarted: boolean;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function rollbackObsoleteUnixShimRefresh(journal: readonly ObsoleteUnixShimJournalEntry[]): Error[] {
|
|
1666
|
+
const errors: Error[] = [];
|
|
1667
|
+
const attempt = (operation: () => void): void => {
|
|
1668
|
+
try {
|
|
1669
|
+
operation();
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
for (const entry of [...journal].reverse()) {
|
|
1675
|
+
attempt(() => {
|
|
1676
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1677
|
+
const ownsWrapper = entry.wrapperWriteStarted
|
|
1678
|
+
&& wrapper !== null
|
|
1679
|
+
&& (entry.writtenWrapperFingerprint
|
|
1680
|
+
? sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
|
|
1681
|
+
: wrapper.prefix.includes(UNIX_SHIM_REVISION_MARKER));
|
|
1682
|
+
if (ownsWrapper) unlinkSync(entry.file.wrapperPath);
|
|
1683
|
+
});
|
|
1684
|
+
attempt(() => {
|
|
1685
|
+
if (!existsSync(entry.stagedWrapperPath)) return;
|
|
1686
|
+
if (existsSync(entry.file.wrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1687
|
+
else renameSync(entry.stagedWrapperPath, entry.file.wrapperPath);
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return errors;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
type ObsoleteUnixShimRefreshResult =
|
|
1694
|
+
| { installed: true; message: string }
|
|
1695
|
+
| { installed: false; deferred: boolean; message: string };
|
|
1696
|
+
|
|
1697
|
+
function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnixShimRefreshResult {
|
|
1698
|
+
if (process.platform === "win32") {
|
|
1699
|
+
return { installed: false, deferred: false, message: "Codex autostart shim is already current." };
|
|
1700
|
+
}
|
|
1701
|
+
const candidates = files.filter(file => {
|
|
1702
|
+
if (file.preserveOnly || file.wrapperPath !== file.originalPath || !existsSync(file.wrapperPath)) return false;
|
|
1703
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1704
|
+
return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
|
|
1705
|
+
});
|
|
1706
|
+
if (candidates.length === 0) {
|
|
1707
|
+
return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
const journal: ObsoleteUnixShimJournalEntry[] = [];
|
|
1711
|
+
const transactionId = `${process.pid}-${randomUUID()}`;
|
|
1712
|
+
let applyError: Error | null = null;
|
|
1713
|
+
for (const [index, file] of candidates.entries()) {
|
|
1714
|
+
const wrapper = stableShimPathProbe(file.wrapperPath);
|
|
1715
|
+
const backing = stableShimPathProbe(file.backupPath);
|
|
1716
|
+
if (!wrapper || !wrapper.prefix.includes(SHIM_MARKER) || isCurrentUnixShimProbe(wrapper) || !backing) {
|
|
1717
|
+
applyError = new Error("Codex autostart shim upgrade inputs changed before regeneration");
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
const entry: ObsoleteUnixShimJournalEntry = {
|
|
1721
|
+
file,
|
|
1722
|
+
stagedWrapperPath: `${file.wrapperPath}.upgrade-${transactionId}-${index}`,
|
|
1723
|
+
priorWrapperFingerprint: wrapper.fingerprint,
|
|
1724
|
+
backingFingerprint: backing.fingerprint,
|
|
1725
|
+
wrapperWriteStarted: false,
|
|
1726
|
+
};
|
|
1727
|
+
journal.push(entry);
|
|
1728
|
+
try {
|
|
1729
|
+
renameSync(file.wrapperPath, entry.stagedWrapperPath);
|
|
1730
|
+
const stagedWrapper = stableShimPathProbe(entry.stagedWrapperPath);
|
|
1731
|
+
if (!stagedWrapper
|
|
1732
|
+
|| !sameFingerprintAfterRename(stagedWrapper.fingerprint, entry.priorWrapperFingerprint)) {
|
|
1733
|
+
throw new Error("Codex autostart shim upgrade could not fingerprint the staged wrapper");
|
|
1734
|
+
}
|
|
1735
|
+
entry.wrapperWriteStarted = true;
|
|
1736
|
+
const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1737
|
+
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1738
|
+
if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) {
|
|
1739
|
+
throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper");
|
|
1740
|
+
}
|
|
1741
|
+
// Identity is the inode we created; a replacement that landed since the
|
|
1742
|
+
// rename leaves this unset so rollback treats the file as someone else's.
|
|
1743
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(file.wrapperPath, writtenInode);
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1751
|
+
let probeError: Error | null = null;
|
|
1752
|
+
if (!applyError) {
|
|
1753
|
+
try {
|
|
1754
|
+
unsafe = probeUnixShimFiles(candidates);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const changedDuringProbe = !applyError && !probeError && journal.some(entry => {
|
|
1760
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1761
|
+
const backing = stableShimPathProbe(entry.file.backupPath);
|
|
1762
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1763
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
|
|
1764
|
+
|| !backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint);
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
if (applyError || probeError || changedDuringProbe) {
|
|
1768
|
+
const rollbackErrors = rollbackObsoleteUnixShimRefresh(journal);
|
|
1769
|
+
if (applyError || probeError || rollbackErrors.length > 0) {
|
|
1770
|
+
throw new AggregateError(
|
|
1771
|
+
[...(applyError ? [applyError] : []), ...(probeError ? [probeError] : []), ...rollbackErrors],
|
|
1772
|
+
"Codex autostart shim upgrade failed",
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
if (unsafe) {
|
|
1779
|
+
const cleanupErrors: Error[] = [];
|
|
1780
|
+
for (const entry of [...journal].reverse()) {
|
|
1781
|
+
try {
|
|
1782
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1783
|
+
if (!wrapper || !entry.writtenWrapperFingerprint
|
|
1784
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)) {
|
|
1785
|
+
throw new Error("Codex autostart shim upgrade lost wrapper ownership before removal");
|
|
1786
|
+
}
|
|
1787
|
+
const backing = stableShimPathProbe(entry.file.backupPath);
|
|
1788
|
+
if (!backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint)) {
|
|
1789
|
+
throw new Error("Codex autostart shim upgrade backing launcher changed before restoration");
|
|
1790
|
+
}
|
|
1791
|
+
unlinkSync(entry.file.wrapperPath);
|
|
1792
|
+
renameSync(entry.file.backupPath, entry.file.originalPath);
|
|
1793
|
+
if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (cleanupErrors.length > 0) {
|
|
1799
|
+
throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade safety removal failed");
|
|
1800
|
+
}
|
|
1801
|
+
if (existsSync(statePath())) unlinkSync(statePath());
|
|
1802
|
+
return {
|
|
1803
|
+
installed: false,
|
|
1804
|
+
deferred: false,
|
|
1805
|
+
message: "Removed an obsolete Codex autostart shim because its saved launcher failed current validation. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.",
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const cleanupErrors: Error[] = [];
|
|
1810
|
+
for (const entry of journal) {
|
|
1811
|
+
try {
|
|
1812
|
+
if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade cleanup failed");
|
|
1818
|
+
return {
|
|
1819
|
+
installed: true,
|
|
1820
|
+
message: `Upgraded Codex autostart shim at ${candidates.map(file => file.wrapperPath).join(", ")} and validated the saved launcher.`,
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
|
|
1825
|
+
const existing = readState();
|
|
1826
|
+
if (existing) {
|
|
1827
|
+
const files = stateFiles(existing);
|
|
1828
|
+
if (!options.expectedReplacements && process.platform !== "win32") {
|
|
1829
|
+
const hasObsoleteShim = files.some(file => {
|
|
1830
|
+
if (file.preserveOnly) return false;
|
|
1831
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1832
|
+
return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
|
|
1833
|
+
});
|
|
1834
|
+
if (hasObsoleteShim) return refreshObsoleteUnixShims(files);
|
|
1835
|
+
}
|
|
1836
|
+
if (options.expectedReplacements) {
|
|
1837
|
+
const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
|
|
1838
|
+
if (!operations || operations.length === 0) {
|
|
1839
|
+
return { installed: false, message: "Codex shim auto-restore deferred because tracked launchers changed." };
|
|
1840
|
+
}
|
|
1841
|
+
const originalStateBytes = readFileSync(statePath());
|
|
1842
|
+
const commitState = (): void => {
|
|
1843
|
+
try {
|
|
1844
|
+
writeState(primaryState(files));
|
|
1845
|
+
} catch (writeError) {
|
|
1846
|
+
try {
|
|
1847
|
+
writeFileSync(statePath(), originalStateBytes);
|
|
1848
|
+
} catch (restoreError) {
|
|
1849
|
+
throw new AggregateError(
|
|
1850
|
+
[writeError, restoreError],
|
|
1851
|
+
"Codex shim state commit and restoration failed",
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
throw writeError;
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
if (!applyGuardedRefreshTransaction(operations, options.beforeGuardedRefresh, commitState)) {
|
|
1858
|
+
return { installed: false, message: "Codex shim auto-restore deferred because tracked launchers changed." };
|
|
1859
|
+
}
|
|
1860
|
+
return {
|
|
1861
|
+
installed: true,
|
|
1862
|
+
message: `Codex update detected. Backed up new launcher and refreshed shim at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
let refreshed = false;
|
|
1866
|
+
for (const file of files) refreshed = refreshShimFile(file) || refreshed;
|
|
1867
|
+
const allInstalled = files.every(file => file.preserveOnly
|
|
1868
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
1869
|
+
: existsSync(file.wrapperPath)
|
|
1870
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
1871
|
+
&& isShim(file.wrapperPath));
|
|
1872
|
+
if (refreshed || allInstalled) {
|
|
1873
|
+
writeState(primaryState(files));
|
|
1874
|
+
if (refreshed) {
|
|
1875
|
+
return {
|
|
1876
|
+
installed: true,
|
|
1877
|
+
message: `Codex update detected. Backed up new launcher and refreshed shim at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
installed: false,
|
|
1882
|
+
message: `Codex autostart shim already installed at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (!options.allowFreshInstall) {
|
|
1888
|
+
return { installed: false, message: "Codex shim auto-restore requires a valid prior installation." };
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const targets: ShimFileState[] | null = process.platform === "win32"
|
|
1892
|
+
? findWindowsCodexTargets()
|
|
1893
|
+
: (() => {
|
|
1894
|
+
const originalPath = findCodexOnPath();
|
|
1895
|
+
return originalPath ? [{ wrapperPath: originalPath, originalPath, backupPath: backupPathFor(originalPath) }] : null;
|
|
1896
|
+
})();
|
|
1897
|
+
if (!targets) return { installed: false, message: lastShimDiscoveryError ?? "Could not find a codex executable on PATH." };
|
|
1898
|
+
|
|
1899
|
+
for (const target of targets) {
|
|
1900
|
+
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
1901
|
+
}
|
|
1902
|
+
const freshJournal: FreshShimInstallJournalEntry[] = [];
|
|
1903
|
+
let freshApplyError: Error | null = null;
|
|
1904
|
+
for (const target of targets) {
|
|
1905
|
+
const entry: FreshShimInstallJournalEntry = {
|
|
1906
|
+
target,
|
|
1907
|
+
originalMovedToBackup: false,
|
|
1908
|
+
wrapperWriteStarted: false,
|
|
1909
|
+
};
|
|
1910
|
+
freshJournal.push(entry);
|
|
1911
|
+
try {
|
|
1912
|
+
if (existsSync(target.originalPath)) {
|
|
1913
|
+
renameSync(target.originalPath, target.backupPath);
|
|
1914
|
+
entry.originalMovedToBackup = true;
|
|
1915
|
+
// Metadata-only, and before the content probe: an empty or otherwise
|
|
1916
|
+
// unprobeable launcher must still be restorable during rollback.
|
|
1917
|
+
//
|
|
1918
|
+
// Only Unix reaches the rollback path (Windows rethrows freshApplyError
|
|
1919
|
+
// without rolling back), so only Unix may treat a missing fingerprint as
|
|
1920
|
+
// fatal. Throwing here on Windows would abort AFTER the original moved,
|
|
1921
|
+
// stranding the launcher at its backup path with nothing to restore it.
|
|
1922
|
+
const movedOriginalFingerprint = shimPathFingerprint(target.backupPath);
|
|
1923
|
+
if (movedOriginalFingerprint) entry.movedOriginalFingerprint = movedOriginalFingerprint;
|
|
1924
|
+
if (process.platform !== "win32") {
|
|
1925
|
+
if (!movedOriginalFingerprint) {
|
|
1926
|
+
throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
|
|
1927
|
+
}
|
|
1928
|
+
const movedOriginal = stableShimPathProbe(target.backupPath);
|
|
1929
|
+
// A content probe still runs where it can, purely as a consistency
|
|
1930
|
+
// check: disagreement means the file moved under us mid-install.
|
|
1931
|
+
if (movedOriginal && !sameFingerprint(movedOriginal.fingerprint, movedOriginalFingerprint)) {
|
|
1932
|
+
throw new Error("Codex shim fresh install staged launcher changed while being fingerprinted");
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (!target.preserveOnly) {
|
|
1937
|
+
entry.wrapperWriteStarted = true;
|
|
1938
|
+
const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
|
|
1939
|
+
entry.writtenWrapperInode = writtenInode;
|
|
1940
|
+
codexShimFreshWriteHookForTests?.();
|
|
1941
|
+
if (process.platform !== "win32") {
|
|
1942
|
+
if (!writtenInode) throw new Error("Codex shim fresh install could not fingerprint the generated wrapper");
|
|
1943
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(target.wrapperPath, writtenInode);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
freshApplyError = error instanceof Error ? error : new Error(String(error));
|
|
1948
|
+
break;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (process.platform !== "win32") {
|
|
1952
|
+
if (freshApplyError) {
|
|
1953
|
+
try {
|
|
1954
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1955
|
+
} catch (rollbackError) {
|
|
1956
|
+
throw new AggregateError([freshApplyError, rollbackError], "Codex shim installation and rollback failed");
|
|
1957
|
+
}
|
|
1958
|
+
throw freshApplyError;
|
|
1959
|
+
}
|
|
1960
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1961
|
+
let probeError: Error | null = null;
|
|
1962
|
+
try {
|
|
1963
|
+
unsafe = probeUnixShimFiles(targets);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1966
|
+
}
|
|
1967
|
+
if (probeError) {
|
|
1968
|
+
try {
|
|
1969
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1970
|
+
} catch (rollbackError) {
|
|
1971
|
+
throw new AggregateError([probeError, rollbackError], "Codex shim probe and install rollback failed");
|
|
1972
|
+
}
|
|
1973
|
+
throw probeError;
|
|
1974
|
+
}
|
|
1975
|
+
const wrapperChangedDuringProbe = freshJournal.some(entry => {
|
|
1976
|
+
if (entry.target.preserveOnly) return false;
|
|
1977
|
+
const wrapper = stableShimPathProbe(entry.target.wrapperPath);
|
|
1978
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1979
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1980
|
+
});
|
|
1981
|
+
if (unsafe || wrapperChangedDuringProbe) {
|
|
1982
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1983
|
+
const reason = wrapperChangedDuringProbe
|
|
1984
|
+
? "the generated wrapper changed during its validation probe"
|
|
1985
|
+
: unsafe === "recursive"
|
|
1986
|
+
? "the saved launcher resolved back to the generated shim"
|
|
1987
|
+
: unsafe === "timeout"
|
|
1988
|
+
? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
|
|
1989
|
+
: unsafe === "descendants"
|
|
1990
|
+
? "the saved launcher left background descendants running after --version"
|
|
1991
|
+
: unsafe === "cleanup"
|
|
1992
|
+
? "the saved launcher's probe process group could not be terminated cleanly"
|
|
1993
|
+
: "the saved launcher failed its --version probe";
|
|
1994
|
+
return {
|
|
1995
|
+
installed: false,
|
|
1996
|
+
message: wrapperChangedDuringProbe
|
|
1997
|
+
? `Refusing Codex autostart shim because ${reason}. The concurrent launcher was preserved, and your previous launcher was kept alongside it as \`<codex>.opencodex-real\`; retry after the Codex update finishes, and remove that backup once you are satisfied the launcher on PATH is the one you want.`
|
|
1998
|
+
: `Refusing Codex autostart shim because ${reason}. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.`,
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
} else if (freshApplyError) {
|
|
2002
|
+
throw freshApplyError;
|
|
2003
|
+
}
|
|
2004
|
+
writeState(primaryState(targets));
|
|
2005
|
+
return {
|
|
2006
|
+
installed: true,
|
|
2007
|
+
message: `Codex autostart shim installed at ${targets.map(t => t.wrapperPath).join(", ")}. Original saved at ${targets.map(t => t.backupPath).join(", ")}.`,
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
export function installCodexShim(): { installed: boolean; message: string } {
|
|
2012
|
+
return installCodexShimInternal({ allowFreshInstall: true });
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
export function autoRestoreCodexShim(options: {
|
|
2016
|
+
enabled: () => boolean;
|
|
2017
|
+
stabilitySleep?: (ms: number) => void;
|
|
2018
|
+
/** Narrow deterministic seam used to hold the interprocess lock in tests. */
|
|
2019
|
+
afterRestoreLockAcquired?: () => void;
|
|
2020
|
+
/** Narrow deterministic seam for stale-lock compare-and-delete tests. */
|
|
2021
|
+
beforeStaleRestoreLockDelete?: () => void;
|
|
2022
|
+
/** Narrow deterministic race seam for the guarded transaction tests. */
|
|
2023
|
+
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
2024
|
+
}): CodexShimAutoRestoreResult {
|
|
2025
|
+
const stateRead = readStateResult();
|
|
2026
|
+
const state = stateRead.state;
|
|
2027
|
+
if (!state) {
|
|
2028
|
+
if (stateRead.warning) return { status: "ineligible", message: stateRead.warning };
|
|
2029
|
+
return { status: existsSync(statePath()) ? "ineligible" : "not-installed" };
|
|
2030
|
+
}
|
|
2031
|
+
if (state.platform !== process.platform) return { status: "ineligible" };
|
|
2032
|
+
|
|
2033
|
+
const files = stateFiles(state);
|
|
2034
|
+
const replacementProbes = new Map<string, StableShimPathProbe>();
|
|
2035
|
+
const obsoleteShimProbes = new Map<string, StableShimPathProbe>();
|
|
2036
|
+
const seen = new Set<string>();
|
|
2037
|
+
let healthyCount = 0;
|
|
2038
|
+
for (const file of files) {
|
|
2039
|
+
if (seen.has(file.wrapperPath)) return { status: "ineligible" };
|
|
2040
|
+
seen.add(file.wrapperPath);
|
|
2041
|
+
if (file.preserveOnly) {
|
|
2042
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) return { status: "ineligible" };
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) return { status: "ineligible" };
|
|
2046
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
2047
|
+
if (!probe) return { status: "deferred" };
|
|
2048
|
+
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
2049
|
+
if (!isHealthyShimProbe(probe, state.platform)) return { status: "ineligible" };
|
|
2050
|
+
if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
|
|
2051
|
+
obsoleteShimProbes.set(file.wrapperPath, probe);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
healthyCount += 1;
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
replacementProbes.set(file.wrapperPath, probe);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
if (replacementProbes.size === 0 && obsoleteShimProbes.size === 0) return { status: "healthy" };
|
|
2061
|
+
if (!options.enabled()) return { status: "disabled" };
|
|
2062
|
+
if (files.length > 1 && (healthyCount > 0 || (replacementProbes.size > 0 && obsoleteShimProbes.size > 0))) {
|
|
2063
|
+
return {
|
|
2064
|
+
status: "deferred",
|
|
2065
|
+
message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const lock = tryAcquireShimRestoreLock(options.beforeStaleRestoreLockDelete);
|
|
2070
|
+
if (!lock) return { status: "deferred" };
|
|
2071
|
+
try {
|
|
2072
|
+
options.afterRestoreLockAcquired?.();
|
|
2073
|
+
(options.stabilitySleep ?? Bun.sleepSync)(CODEX_SHIM_REPLACEMENT_STABLE_MS);
|
|
2074
|
+
if (obsoleteShimProbes.size > 0) {
|
|
2075
|
+
for (const [path, firstProbe] of obsoleteShimProbes) {
|
|
2076
|
+
const secondProbe = stableShimPathProbe(path);
|
|
2077
|
+
if (!secondProbe || isCurrentUnixShimProbe(secondProbe)
|
|
2078
|
+
|| !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
|
|
2079
|
+
}
|
|
2080
|
+
const result = refreshObsoleteUnixShims(files);
|
|
2081
|
+
return result.installed
|
|
2082
|
+
? { status: "restored", message: result.message }
|
|
2083
|
+
: result.deferred
|
|
2084
|
+
? { status: "deferred", message: result.message }
|
|
2085
|
+
: { status: "ineligible", message: result.message };
|
|
2086
|
+
}
|
|
2087
|
+
const expectedReplacements = new Map<string, ShimPathFingerprint>();
|
|
2088
|
+
for (const [path, firstProbe] of replacementProbes) {
|
|
2089
|
+
const secondProbe = stableShimPathProbe(path);
|
|
2090
|
+
if (!secondProbe || secondProbe.prefix.includes(SHIM_MARKER)
|
|
2091
|
+
|| !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
|
|
2092
|
+
expectedReplacements.set(path, secondProbe.fingerprint);
|
|
2093
|
+
}
|
|
2094
|
+
const result = installCodexShimInternal({
|
|
2095
|
+
allowFreshInstall: false,
|
|
2096
|
+
expectedReplacements,
|
|
2097
|
+
beforeGuardedRefresh: options.beforeGuardedRefresh,
|
|
2098
|
+
});
|
|
2099
|
+
return result.installed
|
|
2100
|
+
? { status: "restored", message: result.message }
|
|
2101
|
+
: { status: "deferred" };
|
|
2102
|
+
} finally {
|
|
2103
|
+
lock.release();
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
2108
|
+
const state = readState();
|
|
2109
|
+
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
|
|
2110
|
+
const files = stateFiles(state);
|
|
2111
|
+
for (const file of files) {
|
|
2112
|
+
if (file.preserveOnly) continue;
|
|
2113
|
+
if (existsSync(file.wrapperPath) && isShim(file.wrapperPath)) unlinkSync(file.wrapperPath);
|
|
2114
|
+
}
|
|
2115
|
+
for (const file of files) {
|
|
2116
|
+
if (existsSync(file.backupPath) && !existsSync(file.originalPath)) renameSync(file.backupPath, file.originalPath);
|
|
2117
|
+
}
|
|
2118
|
+
if (existsSync(statePath())) unlinkSync(statePath());
|
|
2119
|
+
return { removed: true, message: `Codex autostart shim removed. Restored ${files.map(f => f.originalPath).join(", ")}.` };
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/** True if a Codex autostart shim is currently installed (state file present). */
|
|
2123
|
+
export function isCodexShimInstalled(): boolean {
|
|
2124
|
+
return diagnoseCodexShim().installed;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
export interface CodexShimDiagnostic {
|
|
2128
|
+
installed: boolean;
|
|
2129
|
+
healthy: boolean;
|
|
2130
|
+
summary: string;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
|
|
2134
|
+
export function diagnoseCodexShim(): CodexShimDiagnostic {
|
|
2135
|
+
const state = readState();
|
|
2136
|
+
if (!state) {
|
|
2137
|
+
if (existsSync(statePath())) {
|
|
2138
|
+
return {
|
|
2139
|
+
installed: true,
|
|
2140
|
+
healthy: false,
|
|
2141
|
+
summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
return {
|
|
2145
|
+
installed: false,
|
|
2146
|
+
healthy: false,
|
|
2147
|
+
summary: "Codex autostart shim is not installed.",
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
const files = stateFiles(state);
|
|
2151
|
+
const healthy = files.length > 0 && files.every(file => file.preserveOnly
|
|
2152
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
2153
|
+
: existsSync(file.wrapperPath)
|
|
2154
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
2155
|
+
&& isHealthyShim(file.wrapperPath, state.platform));
|
|
2156
|
+
const summary = files.map(file => {
|
|
2157
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
2158
|
+
? isShim(file.wrapperPath)
|
|
2159
|
+
? "shim present"
|
|
2160
|
+
: "present but not an opencodex shim"
|
|
2161
|
+
: "missing";
|
|
2162
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
2163
|
+
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
2164
|
+
}).join("\n");
|
|
2165
|
+
return { installed: true, healthy, summary };
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
export function codexShimStatus(): string {
|
|
2169
|
+
return diagnoseCodexShim().summary;
|
|
2170
|
+
}
|