@iislee/opencodex 2.11.0 → 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/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -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/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- 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 +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- 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 +6 -3
- 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/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- 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 +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- 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 +66 -4
- 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 +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- 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 +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- 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 +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- 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 +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- 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/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -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/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- 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 +33 -5
- 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.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- 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 +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- 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 +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- 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 +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- 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 +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- 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 +101 -32
- package/src/server/management/model-routes.ts +189 -131
- 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 +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- 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 +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- 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 +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- 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 +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- 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 +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -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 +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- 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 +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -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 +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- 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 +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- 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 +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/shim.ts
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
3
5
|
import {
|
|
4
6
|
chmodSync,
|
|
5
7
|
closeSync,
|
|
6
8
|
existsSync,
|
|
7
9
|
fstatSync,
|
|
8
10
|
lstatSync,
|
|
11
|
+
linkSync,
|
|
9
12
|
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
10
14
|
openSync,
|
|
11
15
|
readFileSync,
|
|
12
16
|
readdirSync,
|
|
17
|
+
readlinkSync,
|
|
13
18
|
readSync,
|
|
14
19
|
renameSync,
|
|
20
|
+
rmSync,
|
|
15
21
|
rmdirSync,
|
|
16
22
|
statSync,
|
|
23
|
+
symlinkSync,
|
|
17
24
|
type Stats,
|
|
18
25
|
unlinkSync,
|
|
19
26
|
writeFileSync,
|
|
20
27
|
} from "node:fs";
|
|
21
28
|
import { getConfigDir } from "../config";
|
|
22
|
-
import {
|
|
29
|
+
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "../lib/bun-runtime";
|
|
30
|
+
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
23
31
|
import { isProcessAlive } from "../lib/process-control";
|
|
24
32
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
25
33
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
@@ -28,10 +36,173 @@ import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
|
28
36
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
29
37
|
|
|
30
38
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
39
|
+
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
|
|
31
40
|
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
32
41
|
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
33
42
|
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
34
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
|
+
`;
|
|
35
206
|
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
36
207
|
let lastShimDiscoveryError: string | null = null;
|
|
37
208
|
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
@@ -120,11 +291,13 @@ export type CodexShimAutoRestoreResult =
|
|
|
120
291
|
| { status: "ineligible" | "deferred"; message?: string }
|
|
121
292
|
| { status: "restored"; message: string };
|
|
122
293
|
|
|
123
|
-
function cliEntry(): { bun: string; cli: string } {
|
|
294
|
+
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
124
295
|
// Bundled Bun path (survives `ocx update`); all three shim builders
|
|
125
296
|
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
|
|
126
297
|
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
|
|
127
|
-
|
|
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") };
|
|
128
301
|
}
|
|
129
302
|
|
|
130
303
|
function commandNames(name: string): string[] {
|
|
@@ -145,6 +318,7 @@ function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
|
145
318
|
try {
|
|
146
319
|
const content = readFileSync(path, "utf8");
|
|
147
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;
|
|
148
322
|
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
149
323
|
return true;
|
|
150
324
|
} catch {
|
|
@@ -201,6 +375,12 @@ function sameFingerprint(
|
|
|
201
375
|
: false);
|
|
202
376
|
}
|
|
203
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
|
+
|
|
204
384
|
function stableShimPathProbe(path: string): StableShimPathProbe | null {
|
|
205
385
|
const before = statFingerprint(path, false);
|
|
206
386
|
if (!before) return null;
|
|
@@ -230,12 +410,73 @@ function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPat
|
|
|
230
410
|
return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
|
|
231
411
|
}
|
|
232
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
|
+
|
|
233
470
|
function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
|
|
234
471
|
if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
|
|
235
472
|
const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
|
|
236
473
|
return platform === "win32" || (mode & 0o111) !== 0;
|
|
237
474
|
}
|
|
238
475
|
|
|
476
|
+
function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
|
|
477
|
+
return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
|
|
478
|
+
}
|
|
479
|
+
|
|
239
480
|
function hasUsableBackingPath(file: ShimFileState): boolean {
|
|
240
481
|
return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
|
|
241
482
|
.some(path => {
|
|
@@ -366,11 +607,54 @@ function shQuote(value: string): string {
|
|
|
366
607
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
367
608
|
}
|
|
368
609
|
|
|
369
|
-
|
|
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 {
|
|
370
620
|
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
371
621
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
372
622
|
return `#!/usr/bin/env sh
|
|
373
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
|
|
374
658
|
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
375
659
|
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
376
660
|
export OPENCODEX_API_AUTH_TOKEN
|
|
@@ -406,7 +690,7 @@ case "$ocx_subcommand" in
|
|
|
406
690
|
;;
|
|
407
691
|
*)
|
|
408
692
|
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
409
|
-
${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
693
|
+
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
410
694
|
fi
|
|
411
695
|
;;
|
|
412
696
|
esac
|
|
@@ -414,6 +698,239 @@ exec ${shQuote(realCodexPath)} "$@"
|
|
|
414
698
|
`;
|
|
415
699
|
}
|
|
416
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
|
+
|
|
417
934
|
function windowsBatchValue(value: string): string {
|
|
418
935
|
return value
|
|
419
936
|
.replace(/%/g, "%%")
|
|
@@ -431,7 +948,7 @@ function windowsBatchSet(name: string, value: string): string {
|
|
|
431
948
|
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
|
|
432
949
|
}
|
|
433
950
|
|
|
434
|
-
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
951
|
+
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
435
952
|
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
436
953
|
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
437
954
|
return `@echo off\r
|
|
@@ -462,7 +979,11 @@ if "%~1"=="" goto ensure_ocx\r
|
|
|
462
979
|
shift\r
|
|
463
980
|
goto scan_codex_args\r
|
|
464
981
|
:ensure_ocx\r
|
|
982
|
+
setlocal\r
|
|
983
|
+
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
984
|
+
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
465
985
|
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
986
|
+
endlocal\r
|
|
466
987
|
:run_codex\r
|
|
467
988
|
"%OCX_REAL_CODEX%" %*\r
|
|
468
989
|
`;
|
|
@@ -472,7 +993,7 @@ function psString(value: string): string {
|
|
|
472
993
|
return `'${value.replace(/'/g, "''")}'`;
|
|
473
994
|
}
|
|
474
995
|
|
|
475
|
-
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
996
|
+
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
476
997
|
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
477
998
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
478
999
|
const tokenFile = serviceApiTokenFilePath();
|
|
@@ -497,7 +1018,17 @@ foreach ($argValue in $args) {
|
|
|
497
1018
|
}
|
|
498
1019
|
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
499
1020
|
if (-not $skipEnsure) {
|
|
500
|
-
|
|
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
|
+
}
|
|
501
1032
|
}
|
|
502
1033
|
& ${psString(realCodexPath)} @args
|
|
503
1034
|
exit $LASTEXITCODE
|
|
@@ -600,30 +1131,110 @@ function gitBashPath(path: string): string {
|
|
|
600
1131
|
return path.replace(/\\/g, "/");
|
|
601
1132
|
}
|
|
602
1133
|
|
|
603
|
-
|
|
604
|
-
|
|
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();
|
|
605
1145
|
if (process.platform === "win32") {
|
|
606
1146
|
const lower = wrapperPath.toLowerCase();
|
|
607
1147
|
if (lower.endsWith(".ps1")) {
|
|
608
1148
|
// UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
|
|
609
1149
|
// codepage, which mangles non-ASCII paths embedded in the shim.
|
|
610
|
-
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
|
|
1150
|
+
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
|
|
611
1151
|
} else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
612
|
-
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
|
|
1152
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
|
|
613
1153
|
} else {
|
|
614
1154
|
// Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
|
|
615
1155
|
writeFileSync(
|
|
616
1156
|
wrapperPath,
|
|
617
|
-
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
|
|
1157
|
+
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), bunRuntimeSource, gitBashPath(serviceApiTokenFilePath())),
|
|
618
1158
|
"utf8",
|
|
619
1159
|
);
|
|
620
1160
|
}
|
|
1161
|
+
return undefined;
|
|
621
1162
|
} else {
|
|
622
|
-
|
|
623
|
-
|
|
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
|
+
}
|
|
624
1189
|
}
|
|
625
1190
|
}
|
|
626
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
|
+
|
|
627
1238
|
function stateFiles(state: ShimState): ShimFileState[] {
|
|
628
1239
|
return state.wrappers?.length
|
|
629
1240
|
? state.wrappers
|
|
@@ -658,12 +1269,37 @@ function refreshShimFile(file: ShimFileState): boolean {
|
|
|
658
1269
|
}
|
|
659
1270
|
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
660
1271
|
if (file.wrapperPath !== file.originalPath) return false;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
return
|
|
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
|
+
}]);
|
|
664
1279
|
}
|
|
665
1280
|
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
666
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
|
+
}
|
|
667
1303
|
return true;
|
|
668
1304
|
}
|
|
669
1305
|
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
@@ -683,7 +1319,9 @@ interface GuardedRefreshOperation {
|
|
|
683
1319
|
interface GuardedRefreshJournalEntry {
|
|
684
1320
|
operation: GuardedRefreshOperation;
|
|
685
1321
|
stagedOldBackupPath?: string;
|
|
1322
|
+
movedReplacementFingerprint?: ShimPathFingerprint;
|
|
686
1323
|
replacementMovedToBackup: boolean;
|
|
1324
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
687
1325
|
wrapperWriteStarted: boolean;
|
|
688
1326
|
}
|
|
689
1327
|
|
|
@@ -875,14 +1513,35 @@ function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]):
|
|
|
875
1513
|
}
|
|
876
1514
|
};
|
|
877
1515
|
for (const entry of [...journal].reverse()) {
|
|
1516
|
+
let sourceOccupied = false;
|
|
878
1517
|
attempt(() => {
|
|
879
|
-
|
|
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) {
|
|
880
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
|
+
}
|
|
881
1534
|
}
|
|
882
1535
|
});
|
|
883
1536
|
attempt(() => {
|
|
884
1537
|
if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
|
|
885
|
-
|
|
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);
|
|
886
1545
|
}
|
|
887
1546
|
});
|
|
888
1547
|
attempt(() => {
|
|
@@ -902,6 +1561,8 @@ function applyGuardedRefreshTransaction(
|
|
|
902
1561
|
const journal: GuardedRefreshJournalEntry[] = [];
|
|
903
1562
|
let applyError: Error | null = null;
|
|
904
1563
|
let fingerprintMismatch = false;
|
|
1564
|
+
let unsafeLauncher = false;
|
|
1565
|
+
let wrapperChangedDuringProbe = false;
|
|
905
1566
|
const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
|
|
906
1567
|
|
|
907
1568
|
for (const [index, operation] of operations.entries()) {
|
|
@@ -925,15 +1586,43 @@ function applyGuardedRefreshTransaction(
|
|
|
925
1586
|
}
|
|
926
1587
|
renameSync(operation.sourcePath, operation.file.backupPath);
|
|
927
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;
|
|
928
1592
|
entry.wrapperWriteStarted = true;
|
|
929
|
-
writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
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
|
+
}
|
|
930
1603
|
} catch (error) {
|
|
931
1604
|
applyError = error instanceof Error ? error : new Error(String(error));
|
|
932
1605
|
break;
|
|
933
1606
|
}
|
|
934
1607
|
}
|
|
935
1608
|
|
|
936
|
-
if (!fingerprintMismatch && !applyError
|
|
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) {
|
|
937
1626
|
try {
|
|
938
1627
|
commitState();
|
|
939
1628
|
} catch (error) {
|
|
@@ -941,7 +1630,7 @@ function applyGuardedRefreshTransaction(
|
|
|
941
1630
|
}
|
|
942
1631
|
}
|
|
943
1632
|
|
|
944
|
-
if (fingerprintMismatch || applyError) {
|
|
1633
|
+
if (fingerprintMismatch || applyError || unsafeLauncher || wrapperChangedDuringProbe) {
|
|
945
1634
|
const rollbackErrors = rollbackGuardedRefresh(journal);
|
|
946
1635
|
if (applyError || rollbackErrors.length > 0) {
|
|
947
1636
|
throw new AggregateError(
|
|
@@ -964,10 +1653,186 @@ function applyGuardedRefreshTransaction(
|
|
|
964
1653
|
return true;
|
|
965
1654
|
}
|
|
966
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
|
+
|
|
967
1824
|
function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
|
|
968
1825
|
const existing = readState();
|
|
969
1826
|
if (existing) {
|
|
970
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
|
+
}
|
|
971
1836
|
if (options.expectedReplacements) {
|
|
972
1837
|
const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
|
|
973
1838
|
if (!operations || operations.length === 0) {
|
|
@@ -1034,9 +1899,107 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1034
1899
|
for (const target of targets) {
|
|
1035
1900
|
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
1036
1901
|
}
|
|
1902
|
+
const freshJournal: FreshShimInstallJournalEntry[] = [];
|
|
1903
|
+
let freshApplyError: Error | null = null;
|
|
1037
1904
|
for (const target of targets) {
|
|
1038
|
-
|
|
1039
|
-
|
|
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;
|
|
1040
2003
|
}
|
|
1041
2004
|
writeState(primaryState(targets));
|
|
1042
2005
|
return {
|
|
@@ -1069,6 +2032,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1069
2032
|
|
|
1070
2033
|
const files = stateFiles(state);
|
|
1071
2034
|
const replacementProbes = new Map<string, StableShimPathProbe>();
|
|
2035
|
+
const obsoleteShimProbes = new Map<string, StableShimPathProbe>();
|
|
1072
2036
|
const seen = new Set<string>();
|
|
1073
2037
|
let healthyCount = 0;
|
|
1074
2038
|
for (const file of files) {
|
|
@@ -1083,15 +2047,19 @@ export function autoRestoreCodexShim(options: {
|
|
|
1083
2047
|
if (!probe) return { status: "deferred" };
|
|
1084
2048
|
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
1085
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
|
+
}
|
|
1086
2054
|
healthyCount += 1;
|
|
1087
2055
|
continue;
|
|
1088
2056
|
}
|
|
1089
2057
|
replacementProbes.set(file.wrapperPath, probe);
|
|
1090
2058
|
}
|
|
1091
2059
|
|
|
1092
|
-
if (replacementProbes.size === 0) return { status: "healthy" };
|
|
2060
|
+
if (replacementProbes.size === 0 && obsoleteShimProbes.size === 0) return { status: "healthy" };
|
|
1093
2061
|
if (!options.enabled()) return { status: "disabled" };
|
|
1094
|
-
if (files.length > 1 && healthyCount > 0) {
|
|
2062
|
+
if (files.length > 1 && (healthyCount > 0 || (replacementProbes.size > 0 && obsoleteShimProbes.size > 0))) {
|
|
1095
2063
|
return {
|
|
1096
2064
|
status: "deferred",
|
|
1097
2065
|
message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
|
|
@@ -1103,6 +2071,19 @@ export function autoRestoreCodexShim(options: {
|
|
|
1103
2071
|
try {
|
|
1104
2072
|
options.afterRestoreLockAcquired?.();
|
|
1105
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
|
+
}
|
|
1106
2087
|
const expectedReplacements = new Map<string, ShimPathFingerprint>();
|
|
1107
2088
|
for (const [path, firstProbe] of replacementProbes) {
|
|
1108
2089
|
const secondProbe = stableShimPathProbe(path);
|