@iislee/opencodex 2.11.0 → 2.35.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 +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -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 +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -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 +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- 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 +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- 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 +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- 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 +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- 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 +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- 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 +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- 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 +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- 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 +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -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 +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- 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 +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -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/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -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 +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -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 +360 -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 +622 -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 +675 -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 +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -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 +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -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 +157 -33
- package/src/images/plan.ts +24 -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-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -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/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- 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 +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- 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/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- 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/service-secrets.ts +15 -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 +253 -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 +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -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/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- 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/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -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 +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -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 +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -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 +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -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 +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -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 +155 -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 +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- 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 +1021 -190
- package/src/server/lifecycle.ts +336 -14
- 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 +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -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 +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -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 +289 -33
- 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 +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- 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/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -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 +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -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 +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -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 +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- 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 +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- 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 +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- 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 +27 -16
- 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 +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- 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 => {
|
|
@@ -362,15 +603,99 @@ function backupPathFor(path: string): string {
|
|
|
362
603
|
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
363
604
|
}
|
|
364
605
|
|
|
606
|
+
/**
|
|
607
|
+
* True when a Codex binary lives inside a version manager's install tree.
|
|
608
|
+
*
|
|
609
|
+
* These trees are rewritten in place on upgrade, which destroys both the shim
|
|
610
|
+
* and the sibling .opencodex-real backup it restores from (#2412). The tempting
|
|
611
|
+
* repair — adopt the newly installed binary as a fresh original — is wrong
|
|
612
|
+
* twice: it records a provenance that never happened, and the next upgrade wipes
|
|
613
|
+
* it again, so the repair silently un-repairs on the version manager's schedule.
|
|
614
|
+
*
|
|
615
|
+
* Scope is the three managers named in the report. nvm/fnm/npm-prefix are
|
|
616
|
+
* deliberately excluded: a false positive here refuses a restore that would
|
|
617
|
+
* otherwise be correct.
|
|
618
|
+
*/
|
|
619
|
+
export function isVersionManagerOwnedCodexPath(path: string): boolean {
|
|
620
|
+
const normalized = path.replace(/\\/g, "/").toLowerCase();
|
|
621
|
+
return normalized.includes("/mise/installs/")
|
|
622
|
+
|| normalized.includes("/mise/shims/")
|
|
623
|
+
|| normalized.includes("/.asdf/installs/")
|
|
624
|
+
|| normalized.includes("/.asdf/shims/")
|
|
625
|
+
|| normalized.includes("/.volta/");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Why auto-restore refused, in the operator's own terms. Auto-restore used to
|
|
630
|
+
* return a bare `{ status: "ineligible" }`, and the CLI warns only when a
|
|
631
|
+
* message is present, so `ocx start`, `ocx ensure`, and `ocx service repair` all
|
|
632
|
+
* reported success while routing quietly stayed native (#2412, the cause behind
|
|
633
|
+
* the misleading green status in #2411).
|
|
634
|
+
*/
|
|
635
|
+
function destroyedShimMessage(file: ShimFileState): string {
|
|
636
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
637
|
+
? isShim(file.wrapperPath) ? "present but unusable" : "present but not an opencodex shim"
|
|
638
|
+
: "missing";
|
|
639
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
640
|
+
const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
641
|
+
if (!isVersionManagerOwnedCodexPath(file.wrapperPath)) {
|
|
642
|
+
return `${base} Re-run 'ocx codex-shim install' once the Codex binary is stable.`;
|
|
643
|
+
}
|
|
644
|
+
return `${base} This Codex binary is owned by a version manager (mise/asdf/volta), so opencodex will not wrap it as a new original — the next upgrade would overwrite the shim and its backup again. Route through Codex instead with 'ocx start', and use 'ocx service install' for autostart.`;
|
|
645
|
+
}
|
|
646
|
+
|
|
365
647
|
function shQuote(value: string): string {
|
|
366
648
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
367
649
|
}
|
|
368
650
|
|
|
369
|
-
|
|
651
|
+
// Provenance is required rather than defaulted: a default would let a caller pass an
|
|
652
|
+
// override binary and silently label it something else, which is precisely the
|
|
653
|
+
// path/marker disagreement this feature exists to prevent.
|
|
654
|
+
//
|
|
655
|
+
// The marker is scoped to the `ensure` invocation in every flavor below and is never
|
|
656
|
+
// exported into the shim's own environment. A shim wraps the real `codex`, so an
|
|
657
|
+
// exported marker would be inherited by Codex and everything it spawns — a shell that
|
|
658
|
+
// then ran a *different* Bun directly would carry a provenance describing a binary it
|
|
659
|
+
// is not executing.
|
|
660
|
+
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
|
|
370
661
|
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
371
662
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
372
663
|
return `#!/usr/bin/env sh
|
|
373
664
|
# ${SHIM_MARKER}
|
|
665
|
+
# ${UNIX_SHIM_REVISION_MARKER}
|
|
666
|
+
if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
|
|
667
|
+
if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
|
|
668
|
+
if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
|
|
669
|
+
(umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
|
|
670
|
+
fi
|
|
671
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
672
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
673
|
+
fi
|
|
674
|
+
OCX_SHIM_PROBE_ACTIVE=1
|
|
675
|
+
export OCX_SHIM_PROBE_ACTIVE
|
|
676
|
+
fi
|
|
677
|
+
if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
|
|
678
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
679
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
680
|
+
fi
|
|
681
|
+
case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
|
|
682
|
+
0)
|
|
683
|
+
OCX_SHIM_ACTIVE_DEPTH=1
|
|
684
|
+
;;
|
|
685
|
+
1)
|
|
686
|
+
OCX_SHIM_ACTIVE_DEPTH=2
|
|
687
|
+
;;
|
|
688
|
+
*)
|
|
689
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
690
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
691
|
+
;;
|
|
692
|
+
esac
|
|
693
|
+
# Dynamic launchers such as mise exec -- codex may resolve the command name
|
|
694
|
+
# back to this wrapper. An exec chain keeps the same PID. A legitimate nested
|
|
695
|
+
# Codex invocation may enter once with a new PID; repeated child-process
|
|
696
|
+
# redispatch reaches depth 2 and is rejected before it can form an infinite chain.
|
|
697
|
+
OCX_SHIM_ACTIVE_PID=$$
|
|
698
|
+
export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
|
|
374
699
|
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
375
700
|
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
376
701
|
export OPENCODEX_API_AUTH_TOKEN
|
|
@@ -406,7 +731,7 @@ case "$ocx_subcommand" in
|
|
|
406
731
|
;;
|
|
407
732
|
*)
|
|
408
733
|
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
409
|
-
${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
734
|
+
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
410
735
|
fi
|
|
411
736
|
;;
|
|
412
737
|
esac
|
|
@@ -414,6 +739,239 @@ exec ${shQuote(realCodexPath)} "$@"
|
|
|
414
739
|
`;
|
|
415
740
|
}
|
|
416
741
|
|
|
742
|
+
type UnixShimProbeResult = "cleanup" | "descendants" | "failed" | "recursive" | "timeout" | null;
|
|
743
|
+
|
|
744
|
+
let codexShimProbeHookForTests: (() => void) | null = null;
|
|
745
|
+
let codexShimProbeShellForTests: string | null = null;
|
|
746
|
+
let codexShimGuardedWriteHookForTests: (() => void) | null = null;
|
|
747
|
+
let codexShimFreshWriteHookForTests: (() => void) | null = null;
|
|
748
|
+
let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
|
|
749
|
+
let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
750
|
+
|
|
751
|
+
/** Narrow deterministic seam for transaction rollback tests. */
|
|
752
|
+
export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
|
|
753
|
+
codexShimProbeHookForTests = hook;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Selects a POSIX shell only for cross-shell probe regression tests. */
|
|
757
|
+
export function setCodexShimProbeShellForTests(path: string | null): void {
|
|
758
|
+
codexShimProbeShellForTests = path;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Shortens the successful-launcher observation window only for focused tests. */
|
|
762
|
+
export function setCodexShimProbeObservationMsForTests(value: number | null): void {
|
|
763
|
+
codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** Narrow deterministic seam for guarded partial-write rollback tests. */
|
|
767
|
+
export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
|
|
768
|
+
codexShimGuardedWriteHookForTests = hook;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Narrow deterministic seam for fresh-install partial-write rollback tests. */
|
|
772
|
+
export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): void {
|
|
773
|
+
codexShimFreshWriteHookForTests = hook;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* @internal Test-only seam for the rollback restore race.
|
|
778
|
+
*
|
|
779
|
+
* The window this closes opens after `sourceOccupied` is sampled and closes when
|
|
780
|
+
* the backup is republished, so no earlier hook can reach it: publishing from
|
|
781
|
+
* the fresh-write hook makes `sourceOccupied` true and skips the restore
|
|
782
|
+
* entirely.
|
|
783
|
+
*/
|
|
784
|
+
export function setCodexShimRollbackRestoreHookForTests(
|
|
785
|
+
hook: ((target: ShimFileState) => void) | null,
|
|
786
|
+
): void {
|
|
787
|
+
codexShimRollbackRestoreHookForTests = hook;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function readProbeMetadata(path: string, maxBytes: number): string | null {
|
|
791
|
+
try {
|
|
792
|
+
if (!existsSync(path)) return "";
|
|
793
|
+
const stat = lstatSync(path);
|
|
794
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
795
|
+
return readFileSync(path, "utf8").trim();
|
|
796
|
+
} catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
802
|
+
if (process.platform === "win32") return null;
|
|
803
|
+
const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
|
|
804
|
+
const markerPath = join(probeDir, "result");
|
|
805
|
+
const reentryPath = join(probeDir, "reentry");
|
|
806
|
+
const groupPath = join(probeDir, "group");
|
|
807
|
+
const stderrPath = join(probeDir, "stderr");
|
|
808
|
+
const env: NodeJS.ProcessEnv = {
|
|
809
|
+
...process.env,
|
|
810
|
+
OCX_SHIM_BYPASS: "1",
|
|
811
|
+
OCX_SHIM_PROBE: "1",
|
|
812
|
+
OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
|
|
813
|
+
};
|
|
814
|
+
delete env.OCX_SHIM_ACTIVE_PID;
|
|
815
|
+
delete env.OCX_SHIM_ACTIVE_DEPTH;
|
|
816
|
+
delete env.OCX_SHIM_PROBE_ACTIVE;
|
|
817
|
+
let groupId = 0;
|
|
818
|
+
try {
|
|
819
|
+
chmodSync(probeDir, 0o700);
|
|
820
|
+
const result = spawnSync(process.execPath, [
|
|
821
|
+
"-e",
|
|
822
|
+
CODEX_SHIM_INSTALL_PROBE_SCRIPT,
|
|
823
|
+
markerPath,
|
|
824
|
+
reentryPath,
|
|
825
|
+
groupPath,
|
|
826
|
+
stderrPath,
|
|
827
|
+
codexShimProbeShellForTests ?? "/bin/sh",
|
|
828
|
+
wrapperPath,
|
|
829
|
+
String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
|
|
830
|
+
String(MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
831
|
+
String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
|
|
832
|
+
String(codexShimProbeObservationMs),
|
|
833
|
+
], {
|
|
834
|
+
encoding: "utf8",
|
|
835
|
+
env,
|
|
836
|
+
timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
|
|
837
|
+
killSignal: "SIGKILL",
|
|
838
|
+
});
|
|
839
|
+
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
|
840
|
+
const marker = readProbeMetadata(markerPath, 64);
|
|
841
|
+
const reentryMarker = readProbeMetadata(reentryPath, 64);
|
|
842
|
+
const groupText = readProbeMetadata(groupPath, 64);
|
|
843
|
+
const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
844
|
+
groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
|
|
845
|
+
if (marker === null || reentryMarker === null || groupText === null || launcherStderr === null
|
|
846
|
+
|| !Number.isInteger(groupId) || groupId <= 0) return "cleanup";
|
|
847
|
+
const groupSurvived = unixProcessGroupAlive(groupId);
|
|
848
|
+
if (timedOut || marker || reentryMarker || groupSurvived) {
|
|
849
|
+
try {
|
|
850
|
+
terminateUnixProcessGroup(groupId);
|
|
851
|
+
} catch {
|
|
852
|
+
return "cleanup";
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (result.error && !timedOut) return "cleanup";
|
|
856
|
+
if (timedOut || marker === "timeout") return "timeout";
|
|
857
|
+
if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
|
|
858
|
+
if (reentryMarker !== "") return "cleanup";
|
|
859
|
+
if (marker === "descendants") return "descendants";
|
|
860
|
+
if (groupSurvived) return "descendants";
|
|
861
|
+
if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
|
|
862
|
+
return "recursive";
|
|
863
|
+
}
|
|
864
|
+
if (result.status !== 0) return "failed";
|
|
865
|
+
return null;
|
|
866
|
+
} catch {
|
|
867
|
+
if (Number.isInteger(groupId) && groupId > 0) {
|
|
868
|
+
try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
|
|
869
|
+
}
|
|
870
|
+
return "cleanup";
|
|
871
|
+
} finally {
|
|
872
|
+
try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
|
|
877
|
+
if (process.platform === "win32") return null;
|
|
878
|
+
codexShimProbeHookForTests?.();
|
|
879
|
+
return files
|
|
880
|
+
.filter(file => !file.preserveOnly)
|
|
881
|
+
.map(file => probeUnixShimInstall(file.wrapperPath))
|
|
882
|
+
.find(result => result !== null) ?? null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function unixProcessGroupAlive(groupId: number): boolean {
|
|
886
|
+
try {
|
|
887
|
+
process.kill(-groupId, 0);
|
|
888
|
+
return true;
|
|
889
|
+
} catch (error) {
|
|
890
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function terminateUnixProcessGroup(groupId: number): void {
|
|
895
|
+
try {
|
|
896
|
+
process.kill(-groupId, "SIGKILL");
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
899
|
+
}
|
|
900
|
+
const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
|
|
901
|
+
while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
|
|
902
|
+
if (unixProcessGroupAlive(groupId)) {
|
|
903
|
+
throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
interface FreshShimInstallJournalEntry {
|
|
908
|
+
target: ShimFileState;
|
|
909
|
+
movedOriginalFingerprint?: ShimPathFingerprint;
|
|
910
|
+
originalMovedToBackup: boolean;
|
|
911
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
912
|
+
wrapperWriteStarted: boolean;
|
|
913
|
+
/** dev/ino of the file our `writeShim()` created, recorded before anything can fail. */
|
|
914
|
+
writtenWrapperInode?: { dev: number; ino: number };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry[]): void {
|
|
918
|
+
const errors: Error[] = [];
|
|
919
|
+
for (const entry of [...journal].reverse()) {
|
|
920
|
+
const target = entry.target;
|
|
921
|
+
let sourceOccupied = false;
|
|
922
|
+
let ownsWrapperNow = false;
|
|
923
|
+
try {
|
|
924
|
+
const wrapper = stableShimPathProbe(target.wrapperPath);
|
|
925
|
+
// Ownership is the inode our own write created. There is no marker-text
|
|
926
|
+
// fallback: the markers are public, so a concurrent updater's wrapper carries
|
|
927
|
+
// them too, and treating that as proof is how we would delete a file we never
|
|
928
|
+
// wrote. An in-place truncation of our file keeps the inode and is still ours
|
|
929
|
+
// to clean up; a replacement renamed over it has a different inode and is not.
|
|
930
|
+
const ownsWrapper = !target.preserveOnly
|
|
931
|
+
&& entry.wrapperWriteStarted
|
|
932
|
+
&& wrapper !== null
|
|
933
|
+
&& wrapperInodeIsOurs(wrapper, entry.writtenWrapperInode, entry.writtenWrapperFingerprint);
|
|
934
|
+
ownsWrapperNow = ownsWrapper;
|
|
935
|
+
if (ownsWrapper) unlinkSync(target.wrapperPath);
|
|
936
|
+
else {
|
|
937
|
+
try {
|
|
938
|
+
lstatSync(target.originalPath);
|
|
939
|
+
sourceOccupied = true;
|
|
940
|
+
} catch (error) {
|
|
941
|
+
if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
} catch (error) {
|
|
945
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
if (entry.originalMovedToBackup && existsSync(target.backupPath)) {
|
|
949
|
+
const movedOriginal = shimPathFingerprint(target.backupPath);
|
|
950
|
+
if (!movedOriginal || !entry.movedOriginalFingerprint
|
|
951
|
+
|| !sameFingerprint(movedOriginal, entry.movedOriginalFingerprint)) {
|
|
952
|
+
throw new Error("Codex shim fresh-install backup changed during rollback");
|
|
953
|
+
}
|
|
954
|
+
if (sourceOccupied) {
|
|
955
|
+
// Something else occupies the source path. Dropping the backup is correct
|
|
956
|
+
// only when that something is a file we own; when a concurrent updater
|
|
957
|
+
// owns it, this backup is the user's real launcher and deleting it would
|
|
958
|
+
// lose the command entirely. Keep it in that case — a stray
|
|
959
|
+
// `codex.opencodex-real` is recoverable, a deleted launcher is not.
|
|
960
|
+
if (ownsWrapperNow) unlinkSync(target.backupPath);
|
|
961
|
+
} else {
|
|
962
|
+
// No-replace: sourceOccupied was sampled earlier, so a concurrent
|
|
963
|
+
// installer may have published a launcher at the original path since.
|
|
964
|
+
codexShimRollbackRestoreHookForTests?.(target);
|
|
965
|
+
restoreWithoutReplacing(target.backupPath, target.originalPath);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch (error) {
|
|
969
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
|
|
973
|
+
}
|
|
974
|
+
|
|
417
975
|
function windowsBatchValue(value: string): string {
|
|
418
976
|
return value
|
|
419
977
|
.replace(/%/g, "%%")
|
|
@@ -431,7 +989,7 @@ function windowsBatchSet(name: string, value: string): string {
|
|
|
431
989
|
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
|
|
432
990
|
}
|
|
433
991
|
|
|
434
|
-
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
992
|
+
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
435
993
|
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
436
994
|
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
437
995
|
return `@echo off\r
|
|
@@ -462,7 +1020,11 @@ if "%~1"=="" goto ensure_ocx\r
|
|
|
462
1020
|
shift\r
|
|
463
1021
|
goto scan_codex_args\r
|
|
464
1022
|
:ensure_ocx\r
|
|
1023
|
+
setlocal\r
|
|
1024
|
+
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
1025
|
+
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
465
1026
|
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
1027
|
+
endlocal\r
|
|
466
1028
|
:run_codex\r
|
|
467
1029
|
"%OCX_REAL_CODEX%" %*\r
|
|
468
1030
|
`;
|
|
@@ -472,7 +1034,7 @@ function psString(value: string): string {
|
|
|
472
1034
|
return `'${value.replace(/'/g, "''")}'`;
|
|
473
1035
|
}
|
|
474
1036
|
|
|
475
|
-
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
1037
|
+
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
476
1038
|
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
477
1039
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
478
1040
|
const tokenFile = serviceApiTokenFilePath();
|
|
@@ -497,7 +1059,17 @@ foreach ($argValue in $args) {
|
|
|
497
1059
|
}
|
|
498
1060
|
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
499
1061
|
if (-not $skipEnsure) {
|
|
500
|
-
|
|
1062
|
+
$priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
|
|
1063
|
+
$priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
|
|
1064
|
+
$env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
|
|
1065
|
+
$env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
|
|
1066
|
+
try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
|
|
1067
|
+
finally {
|
|
1068
|
+
if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
|
|
1069
|
+
else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
|
|
1070
|
+
if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
|
|
1071
|
+
else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
|
|
1072
|
+
}
|
|
501
1073
|
}
|
|
502
1074
|
& ${psString(realCodexPath)} @args
|
|
503
1075
|
exit $LASTEXITCODE
|
|
@@ -600,28 +1172,108 @@ function gitBashPath(path: string): string {
|
|
|
600
1172
|
return path.replace(/\\/g, "/");
|
|
601
1173
|
}
|
|
602
1174
|
|
|
603
|
-
|
|
604
|
-
|
|
1175
|
+
/**
|
|
1176
|
+
* Write the wrapper and return the identity of the inode this call created, or
|
|
1177
|
+
* `undefined` where the platform still writes the destination in place.
|
|
1178
|
+
*
|
|
1179
|
+
* Callers must derive ownership from the returned identity rather than from a
|
|
1180
|
+
* later `stat` of `wrapperPath`: the shim markers are public, so a concurrent
|
|
1181
|
+
* updater's wrapper can carry them, and a replacement landing between the write
|
|
1182
|
+
* and the observation is otherwise indistinguishable from our own file.
|
|
1183
|
+
*/
|
|
1184
|
+
function writeShim(wrapperPath: string, realCodexPath: string): { dev: number; ino: number } | undefined {
|
|
1185
|
+
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
605
1186
|
if (process.platform === "win32") {
|
|
606
1187
|
const lower = wrapperPath.toLowerCase();
|
|
607
1188
|
if (lower.endsWith(".ps1")) {
|
|
608
1189
|
// UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
|
|
609
1190
|
// codepage, which mangles non-ASCII paths embedded in the shim.
|
|
610
|
-
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
|
|
1191
|
+
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
|
|
611
1192
|
} else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
612
|
-
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
|
|
1193
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
|
|
613
1194
|
} else {
|
|
614
1195
|
// Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
|
|
615
1196
|
writeFileSync(
|
|
616
1197
|
wrapperPath,
|
|
617
|
-
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
|
|
1198
|
+
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), bunRuntimeSource, gitBashPath(serviceApiTokenFilePath())),
|
|
618
1199
|
"utf8",
|
|
619
1200
|
);
|
|
620
1201
|
}
|
|
1202
|
+
return undefined;
|
|
621
1203
|
} else {
|
|
622
|
-
|
|
623
|
-
|
|
1204
|
+
// Stage the wrapper as its own inode and rename it into place, so ownership
|
|
1205
|
+
// comes from the write itself rather than from observing the path afterwards.
|
|
1206
|
+
// Writing the destination directly leaves a window in which a concurrent
|
|
1207
|
+
// updater can replace the file between our write and our fingerprint; we would
|
|
1208
|
+
// then adopt that replacement as ours and unlink it during rollback, deleting
|
|
1209
|
+
// an executable we never wrote.
|
|
1210
|
+
// Hidden and non-executable while staged, so a crash between the write and the
|
|
1211
|
+
// rename cannot leave an executable `codex*` artifact that a glob or a shell
|
|
1212
|
+
// completion would surface.
|
|
1213
|
+
const staged = join(dirname(wrapperPath), `.${basename(wrapperPath)}.opencodex-staging.${process.pid}.${randomUUID()}`);
|
|
1214
|
+
let renamed = false;
|
|
1215
|
+
try {
|
|
1216
|
+
// "wx" fails if the staging path somehow exists, so we never inherit a file.
|
|
1217
|
+
writeFileSync(staged, buildUnixCodexShim(realCodexPath, bun, cli, bunRuntimeSource), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
1218
|
+
const stagedStat = lstatSync(staged);
|
|
1219
|
+
chmodSync(staged, 0o755);
|
|
1220
|
+
renameSync(staged, wrapperPath);
|
|
1221
|
+
renamed = true;
|
|
1222
|
+
// rename() preserves dev/ino and updates ctime, so identity is the inode
|
|
1223
|
+
// pair, captured from the file we created rather than from the destination.
|
|
1224
|
+
return { dev: stagedStat.dev, ino: stagedStat.ino };
|
|
1225
|
+
} finally {
|
|
1226
|
+
if (!renamed) {
|
|
1227
|
+
try { unlinkSync(staged); } catch { /* best-effort: nothing to clean up */ }
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* The fingerprint to record as "we wrote this", or `undefined` when the file at
|
|
1235
|
+
* `wrapperPath` is not the inode `writeShim()` created. Returning `undefined`
|
|
1236
|
+
* makes every rollback path treat the file as someone else's and leave it alone.
|
|
1237
|
+
*/
|
|
1238
|
+
function ownedWrapperFingerprint(
|
|
1239
|
+
wrapperPath: string,
|
|
1240
|
+
written: { dev: number; ino: number } | undefined,
|
|
1241
|
+
): ShimPathFingerprint | undefined {
|
|
1242
|
+
const probe = stableShimPathProbe(wrapperPath);
|
|
1243
|
+
if (!probe) return undefined;
|
|
1244
|
+
// Platforms that still write in place (Windows) have no staged identity; fall
|
|
1245
|
+
// back to the marker check they have always used.
|
|
1246
|
+
if (!written) return probe.prefix.includes(SHIM_MARKER) ? probe.fingerprint : undefined;
|
|
1247
|
+
if (probe.fingerprint.dev !== written.dev || probe.fingerprint.ino !== written.ino) return undefined;
|
|
1248
|
+
return probe.fingerprint;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Whether the file now at the wrapper path is one this transaction may unlink.
|
|
1253
|
+
*
|
|
1254
|
+
* The inode our write created is the authority. It stays ours through an in-place
|
|
1255
|
+
* truncation — a partial write we must clean up — and stops being ours the moment
|
|
1256
|
+
* someone renames a different file over the path, which is exactly the concurrent
|
|
1257
|
+
* updater we must not delete. Where no inode was recorded (Windows writes the
|
|
1258
|
+
* destination directly), fall back to the exact fingerprint recorded at the time.
|
|
1259
|
+
*/
|
|
1260
|
+
function wrapperInodeIsOurs(
|
|
1261
|
+
wrapper: StableShimPathProbe,
|
|
1262
|
+
written: { dev: number; ino: number } | undefined,
|
|
1263
|
+
recorded: ShimPathFingerprint | undefined,
|
|
1264
|
+
): boolean {
|
|
1265
|
+
// A different inode is conclusive: someone renamed their own file over ours.
|
|
1266
|
+
if (written && (wrapper.fingerprint.dev !== written.dev || wrapper.fingerprint.ino !== written.ino)) {
|
|
1267
|
+
return false;
|
|
624
1268
|
}
|
|
1269
|
+
// Same inode is not sufficient on its own — an in-place truncation (shell `>`,
|
|
1270
|
+
// writeFileSync) keeps it while replacing the contents. When we recorded a full
|
|
1271
|
+
// fingerprint, require it to still match; that covers our own partial write,
|
|
1272
|
+
// whose fingerprint we record before anything can fail.
|
|
1273
|
+
if (recorded !== undefined) return sameFingerprint(wrapper.fingerprint, recorded);
|
|
1274
|
+
// No recorded fingerprint: only the inode identity we captured at write time can
|
|
1275
|
+
// speak for us, and there is nothing else to distinguish this file.
|
|
1276
|
+
return written !== undefined;
|
|
625
1277
|
}
|
|
626
1278
|
|
|
627
1279
|
function stateFiles(state: ShimState): ShimFileState[] {
|
|
@@ -658,12 +1310,37 @@ function refreshShimFile(file: ShimFileState): boolean {
|
|
|
658
1310
|
}
|
|
659
1311
|
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
660
1312
|
if (file.wrapperPath !== file.originalPath) return false;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
return
|
|
1313
|
+
const replacement = stableShimPathProbe(file.wrapperPath);
|
|
1314
|
+
if (!replacement) return false;
|
|
1315
|
+
return applyGuardedRefreshTransaction([{
|
|
1316
|
+
file,
|
|
1317
|
+
expectedReplacement: replacement.fingerprint,
|
|
1318
|
+
sourcePath: file.wrapperPath,
|
|
1319
|
+
}]);
|
|
664
1320
|
}
|
|
665
1321
|
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
666
1322
|
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1323
|
+
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1324
|
+
if (!writtenWrapper || !writtenWrapper.prefix.includes(SHIM_MARKER)) {
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1328
|
+
let probeError: Error | null = null;
|
|
1329
|
+
try {
|
|
1330
|
+
unsafe = probeUnixShimFiles([file]);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1333
|
+
}
|
|
1334
|
+
const currentWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1335
|
+
const wrapperChangedDuringProbe = !currentWrapper
|
|
1336
|
+
|| !sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint);
|
|
1337
|
+
if (unsafe !== null || probeError || wrapperChangedDuringProbe) {
|
|
1338
|
+
if (currentWrapper && sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint)) {
|
|
1339
|
+
unlinkSync(file.wrapperPath);
|
|
1340
|
+
}
|
|
1341
|
+
if (probeError) throw probeError;
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
667
1344
|
return true;
|
|
668
1345
|
}
|
|
669
1346
|
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
@@ -683,7 +1360,9 @@ interface GuardedRefreshOperation {
|
|
|
683
1360
|
interface GuardedRefreshJournalEntry {
|
|
684
1361
|
operation: GuardedRefreshOperation;
|
|
685
1362
|
stagedOldBackupPath?: string;
|
|
1363
|
+
movedReplacementFingerprint?: ShimPathFingerprint;
|
|
686
1364
|
replacementMovedToBackup: boolean;
|
|
1365
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
687
1366
|
wrapperWriteStarted: boolean;
|
|
688
1367
|
}
|
|
689
1368
|
|
|
@@ -875,14 +1554,35 @@ function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]):
|
|
|
875
1554
|
}
|
|
876
1555
|
};
|
|
877
1556
|
for (const entry of [...journal].reverse()) {
|
|
1557
|
+
let sourceOccupied = false;
|
|
878
1558
|
attempt(() => {
|
|
879
|
-
|
|
1559
|
+
const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
|
|
1560
|
+
// No marker-text fallback: the markers are public, so a concurrent updater's
|
|
1561
|
+
// wrapper carries them too. No recorded inode identity means not ours.
|
|
1562
|
+
const ownsWrapper = entry.wrapperWriteStarted
|
|
1563
|
+
&& wrapper !== null
|
|
1564
|
+
&& entry.writtenWrapperFingerprint !== undefined
|
|
1565
|
+
&& sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1566
|
+
if (ownsWrapper) {
|
|
880
1567
|
unlinkSync(entry.operation.file.wrapperPath);
|
|
1568
|
+
} else {
|
|
1569
|
+
try {
|
|
1570
|
+
lstatSync(entry.operation.sourcePath);
|
|
1571
|
+
sourceOccupied = true;
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
|
|
1574
|
+
}
|
|
881
1575
|
}
|
|
882
1576
|
});
|
|
883
1577
|
attempt(() => {
|
|
884
1578
|
if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
|
|
885
|
-
|
|
1579
|
+
const movedReplacement = stableShimPathProbe(entry.operation.file.backupPath);
|
|
1580
|
+
if (!movedReplacement || !entry.movedReplacementFingerprint
|
|
1581
|
+
|| !sameFingerprint(movedReplacement.fingerprint, entry.movedReplacementFingerprint)) {
|
|
1582
|
+
throw new Error("Codex shim guarded refresh backup changed during rollback");
|
|
1583
|
+
}
|
|
1584
|
+
if (sourceOccupied) unlinkSync(entry.operation.file.backupPath);
|
|
1585
|
+
else renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
|
|
886
1586
|
}
|
|
887
1587
|
});
|
|
888
1588
|
attempt(() => {
|
|
@@ -902,6 +1602,8 @@ function applyGuardedRefreshTransaction(
|
|
|
902
1602
|
const journal: GuardedRefreshJournalEntry[] = [];
|
|
903
1603
|
let applyError: Error | null = null;
|
|
904
1604
|
let fingerprintMismatch = false;
|
|
1605
|
+
let unsafeLauncher = false;
|
|
1606
|
+
let wrapperChangedDuringProbe = false;
|
|
905
1607
|
const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
|
|
906
1608
|
|
|
907
1609
|
for (const [index, operation] of operations.entries()) {
|
|
@@ -925,15 +1627,43 @@ function applyGuardedRefreshTransaction(
|
|
|
925
1627
|
}
|
|
926
1628
|
renameSync(operation.sourcePath, operation.file.backupPath);
|
|
927
1629
|
entry.replacementMovedToBackup = true;
|
|
1630
|
+
const movedReplacement = stableShimPathProbe(operation.file.backupPath);
|
|
1631
|
+
if (!movedReplacement) throw new Error("Codex shim guarded refresh could not fingerprint the staged launcher");
|
|
1632
|
+
entry.movedReplacementFingerprint = movedReplacement.fingerprint;
|
|
928
1633
|
entry.wrapperWriteStarted = true;
|
|
929
|
-
writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
1634
|
+
const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
1635
|
+
// Claim our own partial write before the hook can fail (see fresh install).
|
|
1636
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
1637
|
+
codexShimGuardedWriteHookForTests?.();
|
|
1638
|
+
// Re-check: unset means a concurrent writer owns the path now, so rollback
|
|
1639
|
+
// must leave it alone.
|
|
1640
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
1641
|
+
if (!entry.writtenWrapperFingerprint && !writtenInode) {
|
|
1642
|
+
throw new Error("Codex shim guarded refresh could not fingerprint the generated wrapper");
|
|
1643
|
+
}
|
|
930
1644
|
} catch (error) {
|
|
931
1645
|
applyError = error instanceof Error ? error : new Error(String(error));
|
|
932
1646
|
break;
|
|
933
1647
|
}
|
|
934
1648
|
}
|
|
935
1649
|
|
|
936
|
-
if (!fingerprintMismatch && !applyError
|
|
1650
|
+
if (!fingerprintMismatch && !applyError) {
|
|
1651
|
+
try {
|
|
1652
|
+
unsafeLauncher = probeUnixShimFiles(operations.map(operation => operation.file)) !== null;
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (!fingerprintMismatch && !applyError && !unsafeLauncher) {
|
|
1659
|
+
wrapperChangedDuringProbe = journal.some(entry => {
|
|
1660
|
+
const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
|
|
1661
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1662
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (!fingerprintMismatch && !applyError && !unsafeLauncher && !wrapperChangedDuringProbe && commitState) {
|
|
937
1667
|
try {
|
|
938
1668
|
commitState();
|
|
939
1669
|
} catch (error) {
|
|
@@ -941,7 +1671,7 @@ function applyGuardedRefreshTransaction(
|
|
|
941
1671
|
}
|
|
942
1672
|
}
|
|
943
1673
|
|
|
944
|
-
if (fingerprintMismatch || applyError) {
|
|
1674
|
+
if (fingerprintMismatch || applyError || unsafeLauncher || wrapperChangedDuringProbe) {
|
|
945
1675
|
const rollbackErrors = rollbackGuardedRefresh(journal);
|
|
946
1676
|
if (applyError || rollbackErrors.length > 0) {
|
|
947
1677
|
throw new AggregateError(
|
|
@@ -964,10 +1694,186 @@ function applyGuardedRefreshTransaction(
|
|
|
964
1694
|
return true;
|
|
965
1695
|
}
|
|
966
1696
|
|
|
1697
|
+
interface ObsoleteUnixShimJournalEntry {
|
|
1698
|
+
file: ShimFileState;
|
|
1699
|
+
stagedWrapperPath: string;
|
|
1700
|
+
priorWrapperFingerprint: ShimPathFingerprint;
|
|
1701
|
+
backingFingerprint: ShimPathFingerprint;
|
|
1702
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
1703
|
+
wrapperWriteStarted: boolean;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function rollbackObsoleteUnixShimRefresh(journal: readonly ObsoleteUnixShimJournalEntry[]): Error[] {
|
|
1707
|
+
const errors: Error[] = [];
|
|
1708
|
+
const attempt = (operation: () => void): void => {
|
|
1709
|
+
try {
|
|
1710
|
+
operation();
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
for (const entry of [...journal].reverse()) {
|
|
1716
|
+
attempt(() => {
|
|
1717
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1718
|
+
const ownsWrapper = entry.wrapperWriteStarted
|
|
1719
|
+
&& wrapper !== null
|
|
1720
|
+
&& (entry.writtenWrapperFingerprint
|
|
1721
|
+
? sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
|
|
1722
|
+
: wrapper.prefix.includes(UNIX_SHIM_REVISION_MARKER));
|
|
1723
|
+
if (ownsWrapper) unlinkSync(entry.file.wrapperPath);
|
|
1724
|
+
});
|
|
1725
|
+
attempt(() => {
|
|
1726
|
+
if (!existsSync(entry.stagedWrapperPath)) return;
|
|
1727
|
+
if (existsSync(entry.file.wrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1728
|
+
else renameSync(entry.stagedWrapperPath, entry.file.wrapperPath);
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
return errors;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
type ObsoleteUnixShimRefreshResult =
|
|
1735
|
+
| { installed: true; message: string }
|
|
1736
|
+
| { installed: false; deferred: boolean; message: string };
|
|
1737
|
+
|
|
1738
|
+
function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnixShimRefreshResult {
|
|
1739
|
+
if (process.platform === "win32") {
|
|
1740
|
+
return { installed: false, deferred: false, message: "Codex autostart shim is already current." };
|
|
1741
|
+
}
|
|
1742
|
+
const candidates = files.filter(file => {
|
|
1743
|
+
if (file.preserveOnly || file.wrapperPath !== file.originalPath || !existsSync(file.wrapperPath)) return false;
|
|
1744
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1745
|
+
return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
|
|
1746
|
+
});
|
|
1747
|
+
if (candidates.length === 0) {
|
|
1748
|
+
return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const journal: ObsoleteUnixShimJournalEntry[] = [];
|
|
1752
|
+
const transactionId = `${process.pid}-${randomUUID()}`;
|
|
1753
|
+
let applyError: Error | null = null;
|
|
1754
|
+
for (const [index, file] of candidates.entries()) {
|
|
1755
|
+
const wrapper = stableShimPathProbe(file.wrapperPath);
|
|
1756
|
+
const backing = stableShimPathProbe(file.backupPath);
|
|
1757
|
+
if (!wrapper || !wrapper.prefix.includes(SHIM_MARKER) || isCurrentUnixShimProbe(wrapper) || !backing) {
|
|
1758
|
+
applyError = new Error("Codex autostart shim upgrade inputs changed before regeneration");
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
const entry: ObsoleteUnixShimJournalEntry = {
|
|
1762
|
+
file,
|
|
1763
|
+
stagedWrapperPath: `${file.wrapperPath}.upgrade-${transactionId}-${index}`,
|
|
1764
|
+
priorWrapperFingerprint: wrapper.fingerprint,
|
|
1765
|
+
backingFingerprint: backing.fingerprint,
|
|
1766
|
+
wrapperWriteStarted: false,
|
|
1767
|
+
};
|
|
1768
|
+
journal.push(entry);
|
|
1769
|
+
try {
|
|
1770
|
+
renameSync(file.wrapperPath, entry.stagedWrapperPath);
|
|
1771
|
+
const stagedWrapper = stableShimPathProbe(entry.stagedWrapperPath);
|
|
1772
|
+
if (!stagedWrapper
|
|
1773
|
+
|| !sameFingerprintAfterRename(stagedWrapper.fingerprint, entry.priorWrapperFingerprint)) {
|
|
1774
|
+
throw new Error("Codex autostart shim upgrade could not fingerprint the staged wrapper");
|
|
1775
|
+
}
|
|
1776
|
+
entry.wrapperWriteStarted = true;
|
|
1777
|
+
const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1778
|
+
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1779
|
+
if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) {
|
|
1780
|
+
throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper");
|
|
1781
|
+
}
|
|
1782
|
+
// Identity is the inode we created; a replacement that landed since the
|
|
1783
|
+
// rename leaves this unset so rollback treats the file as someone else's.
|
|
1784
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(file.wrapperPath, writtenInode);
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1792
|
+
let probeError: Error | null = null;
|
|
1793
|
+
if (!applyError) {
|
|
1794
|
+
try {
|
|
1795
|
+
unsafe = probeUnixShimFiles(candidates);
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const changedDuringProbe = !applyError && !probeError && journal.some(entry => {
|
|
1801
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1802
|
+
const backing = stableShimPathProbe(entry.file.backupPath);
|
|
1803
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1804
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
|
|
1805
|
+
|| !backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint);
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
if (applyError || probeError || changedDuringProbe) {
|
|
1809
|
+
const rollbackErrors = rollbackObsoleteUnixShimRefresh(journal);
|
|
1810
|
+
if (applyError || probeError || rollbackErrors.length > 0) {
|
|
1811
|
+
throw new AggregateError(
|
|
1812
|
+
[...(applyError ? [applyError] : []), ...(probeError ? [probeError] : []), ...rollbackErrors],
|
|
1813
|
+
"Codex autostart shim upgrade failed",
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
if (unsafe) {
|
|
1820
|
+
const cleanupErrors: Error[] = [];
|
|
1821
|
+
for (const entry of [...journal].reverse()) {
|
|
1822
|
+
try {
|
|
1823
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1824
|
+
if (!wrapper || !entry.writtenWrapperFingerprint
|
|
1825
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)) {
|
|
1826
|
+
throw new Error("Codex autostart shim upgrade lost wrapper ownership before removal");
|
|
1827
|
+
}
|
|
1828
|
+
const backing = stableShimPathProbe(entry.file.backupPath);
|
|
1829
|
+
if (!backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint)) {
|
|
1830
|
+
throw new Error("Codex autostart shim upgrade backing launcher changed before restoration");
|
|
1831
|
+
}
|
|
1832
|
+
unlinkSync(entry.file.wrapperPath);
|
|
1833
|
+
renameSync(entry.file.backupPath, entry.file.originalPath);
|
|
1834
|
+
if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (cleanupErrors.length > 0) {
|
|
1840
|
+
throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade safety removal failed");
|
|
1841
|
+
}
|
|
1842
|
+
if (existsSync(statePath())) unlinkSync(statePath());
|
|
1843
|
+
return {
|
|
1844
|
+
installed: false,
|
|
1845
|
+
deferred: false,
|
|
1846
|
+
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.",
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
const cleanupErrors: Error[] = [];
|
|
1851
|
+
for (const entry of journal) {
|
|
1852
|
+
try {
|
|
1853
|
+
if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade cleanup failed");
|
|
1859
|
+
return {
|
|
1860
|
+
installed: true,
|
|
1861
|
+
message: `Upgraded Codex autostart shim at ${candidates.map(file => file.wrapperPath).join(", ")} and validated the saved launcher.`,
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
967
1865
|
function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
|
|
968
1866
|
const existing = readState();
|
|
969
1867
|
if (existing) {
|
|
970
1868
|
const files = stateFiles(existing);
|
|
1869
|
+
if (!options.expectedReplacements && process.platform !== "win32") {
|
|
1870
|
+
const hasObsoleteShim = files.some(file => {
|
|
1871
|
+
if (file.preserveOnly) return false;
|
|
1872
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1873
|
+
return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
|
|
1874
|
+
});
|
|
1875
|
+
if (hasObsoleteShim) return refreshObsoleteUnixShims(files);
|
|
1876
|
+
}
|
|
971
1877
|
if (options.expectedReplacements) {
|
|
972
1878
|
const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
|
|
973
1879
|
if (!operations || operations.length === 0) {
|
|
@@ -1034,9 +1940,107 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1034
1940
|
for (const target of targets) {
|
|
1035
1941
|
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
1036
1942
|
}
|
|
1943
|
+
const freshJournal: FreshShimInstallJournalEntry[] = [];
|
|
1944
|
+
let freshApplyError: Error | null = null;
|
|
1037
1945
|
for (const target of targets) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1946
|
+
const entry: FreshShimInstallJournalEntry = {
|
|
1947
|
+
target,
|
|
1948
|
+
originalMovedToBackup: false,
|
|
1949
|
+
wrapperWriteStarted: false,
|
|
1950
|
+
};
|
|
1951
|
+
freshJournal.push(entry);
|
|
1952
|
+
try {
|
|
1953
|
+
if (existsSync(target.originalPath)) {
|
|
1954
|
+
renameSync(target.originalPath, target.backupPath);
|
|
1955
|
+
entry.originalMovedToBackup = true;
|
|
1956
|
+
// Metadata-only, and before the content probe: an empty or otherwise
|
|
1957
|
+
// unprobeable launcher must still be restorable during rollback.
|
|
1958
|
+
//
|
|
1959
|
+
// Only Unix reaches the rollback path (Windows rethrows freshApplyError
|
|
1960
|
+
// without rolling back), so only Unix may treat a missing fingerprint as
|
|
1961
|
+
// fatal. Throwing here on Windows would abort AFTER the original moved,
|
|
1962
|
+
// stranding the launcher at its backup path with nothing to restore it.
|
|
1963
|
+
const movedOriginalFingerprint = shimPathFingerprint(target.backupPath);
|
|
1964
|
+
if (movedOriginalFingerprint) entry.movedOriginalFingerprint = movedOriginalFingerprint;
|
|
1965
|
+
if (process.platform !== "win32") {
|
|
1966
|
+
if (!movedOriginalFingerprint) {
|
|
1967
|
+
throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
|
|
1968
|
+
}
|
|
1969
|
+
const movedOriginal = stableShimPathProbe(target.backupPath);
|
|
1970
|
+
// A content probe still runs where it can, purely as a consistency
|
|
1971
|
+
// check: disagreement means the file moved under us mid-install.
|
|
1972
|
+
if (movedOriginal && !sameFingerprint(movedOriginal.fingerprint, movedOriginalFingerprint)) {
|
|
1973
|
+
throw new Error("Codex shim fresh install staged launcher changed while being fingerprinted");
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (!target.preserveOnly) {
|
|
1978
|
+
entry.wrapperWriteStarted = true;
|
|
1979
|
+
const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
|
|
1980
|
+
entry.writtenWrapperInode = writtenInode;
|
|
1981
|
+
codexShimFreshWriteHookForTests?.();
|
|
1982
|
+
if (process.platform !== "win32") {
|
|
1983
|
+
if (!writtenInode) throw new Error("Codex shim fresh install could not fingerprint the generated wrapper");
|
|
1984
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(target.wrapperPath, writtenInode);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
freshApplyError = error instanceof Error ? error : new Error(String(error));
|
|
1989
|
+
break;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (process.platform !== "win32") {
|
|
1993
|
+
if (freshApplyError) {
|
|
1994
|
+
try {
|
|
1995
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1996
|
+
} catch (rollbackError) {
|
|
1997
|
+
throw new AggregateError([freshApplyError, rollbackError], "Codex shim installation and rollback failed");
|
|
1998
|
+
}
|
|
1999
|
+
throw freshApplyError;
|
|
2000
|
+
}
|
|
2001
|
+
let unsafe: UnixShimProbeResult = null;
|
|
2002
|
+
let probeError: Error | null = null;
|
|
2003
|
+
try {
|
|
2004
|
+
unsafe = probeUnixShimFiles(targets);
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
2007
|
+
}
|
|
2008
|
+
if (probeError) {
|
|
2009
|
+
try {
|
|
2010
|
+
rollbackFreshShimInstall(freshJournal);
|
|
2011
|
+
} catch (rollbackError) {
|
|
2012
|
+
throw new AggregateError([probeError, rollbackError], "Codex shim probe and install rollback failed");
|
|
2013
|
+
}
|
|
2014
|
+
throw probeError;
|
|
2015
|
+
}
|
|
2016
|
+
const wrapperChangedDuringProbe = freshJournal.some(entry => {
|
|
2017
|
+
if (entry.target.preserveOnly) return false;
|
|
2018
|
+
const wrapper = stableShimPathProbe(entry.target.wrapperPath);
|
|
2019
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
2020
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
2021
|
+
});
|
|
2022
|
+
if (unsafe || wrapperChangedDuringProbe) {
|
|
2023
|
+
rollbackFreshShimInstall(freshJournal);
|
|
2024
|
+
const reason = wrapperChangedDuringProbe
|
|
2025
|
+
? "the generated wrapper changed during its validation probe"
|
|
2026
|
+
: unsafe === "recursive"
|
|
2027
|
+
? "the saved launcher resolved back to the generated shim"
|
|
2028
|
+
: unsafe === "timeout"
|
|
2029
|
+
? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
|
|
2030
|
+
: unsafe === "descendants"
|
|
2031
|
+
? "the saved launcher left background descendants running after --version"
|
|
2032
|
+
: unsafe === "cleanup"
|
|
2033
|
+
? "the saved launcher's probe process group could not be terminated cleanly"
|
|
2034
|
+
: "the saved launcher failed its --version probe";
|
|
2035
|
+
return {
|
|
2036
|
+
installed: false,
|
|
2037
|
+
message: wrapperChangedDuringProbe
|
|
2038
|
+
? `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.`
|
|
2039
|
+
: `Refusing Codex autostart shim because ${reason}. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.`,
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
} else if (freshApplyError) {
|
|
2043
|
+
throw freshApplyError;
|
|
1040
2044
|
}
|
|
1041
2045
|
writeState(primaryState(targets));
|
|
1042
2046
|
return {
|
|
@@ -1069,29 +2073,46 @@ export function autoRestoreCodexShim(options: {
|
|
|
1069
2073
|
|
|
1070
2074
|
const files = stateFiles(state);
|
|
1071
2075
|
const replacementProbes = new Map<string, StableShimPathProbe>();
|
|
2076
|
+
const obsoleteShimProbes = new Map<string, StableShimPathProbe>();
|
|
1072
2077
|
const seen = new Set<string>();
|
|
1073
2078
|
let healthyCount = 0;
|
|
1074
2079
|
for (const file of files) {
|
|
1075
2080
|
if (seen.has(file.wrapperPath)) return { status: "ineligible" };
|
|
1076
2081
|
seen.add(file.wrapperPath);
|
|
1077
2082
|
if (file.preserveOnly) {
|
|
1078
|
-
if (!existsSync(file.backupPath) || existsSync(file.originalPath))
|
|
2083
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) {
|
|
2084
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2085
|
+
}
|
|
1079
2086
|
continue;
|
|
1080
2087
|
}
|
|
1081
|
-
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file))
|
|
2088
|
+
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) {
|
|
2089
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2090
|
+
}
|
|
1082
2091
|
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1083
2092
|
if (!probe) return { status: "deferred" };
|
|
1084
2093
|
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
1085
|
-
if (!isHealthyShimProbe(probe, state.platform))
|
|
2094
|
+
if (!isHealthyShimProbe(probe, state.platform)) {
|
|
2095
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2096
|
+
}
|
|
2097
|
+
if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
|
|
2098
|
+
obsoleteShimProbes.set(file.wrapperPath, probe);
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
1086
2101
|
healthyCount += 1;
|
|
1087
2102
|
continue;
|
|
1088
2103
|
}
|
|
2104
|
+
// A surviving backup would otherwise let the replacement path below wrap the
|
|
2105
|
+
// version manager's NEW binary as a fresh original — the same adoption the
|
|
2106
|
+
// missing-backup case refuses, arriving through the back door.
|
|
2107
|
+
if (isVersionManagerOwnedCodexPath(file.wrapperPath)) {
|
|
2108
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2109
|
+
}
|
|
1089
2110
|
replacementProbes.set(file.wrapperPath, probe);
|
|
1090
2111
|
}
|
|
1091
2112
|
|
|
1092
|
-
if (replacementProbes.size === 0) return { status: "healthy" };
|
|
2113
|
+
if (replacementProbes.size === 0 && obsoleteShimProbes.size === 0) return { status: "healthy" };
|
|
1093
2114
|
if (!options.enabled()) return { status: "disabled" };
|
|
1094
|
-
if (files.length > 1 && healthyCount > 0) {
|
|
2115
|
+
if (files.length > 1 && (healthyCount > 0 || (replacementProbes.size > 0 && obsoleteShimProbes.size > 0))) {
|
|
1095
2116
|
return {
|
|
1096
2117
|
status: "deferred",
|
|
1097
2118
|
message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
|
|
@@ -1103,6 +2124,19 @@ export function autoRestoreCodexShim(options: {
|
|
|
1103
2124
|
try {
|
|
1104
2125
|
options.afterRestoreLockAcquired?.();
|
|
1105
2126
|
(options.stabilitySleep ?? Bun.sleepSync)(CODEX_SHIM_REPLACEMENT_STABLE_MS);
|
|
2127
|
+
if (obsoleteShimProbes.size > 0) {
|
|
2128
|
+
for (const [path, firstProbe] of obsoleteShimProbes) {
|
|
2129
|
+
const secondProbe = stableShimPathProbe(path);
|
|
2130
|
+
if (!secondProbe || isCurrentUnixShimProbe(secondProbe)
|
|
2131
|
+
|| !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
|
|
2132
|
+
}
|
|
2133
|
+
const result = refreshObsoleteUnixShims(files);
|
|
2134
|
+
return result.installed
|
|
2135
|
+
? { status: "restored", message: result.message }
|
|
2136
|
+
: result.deferred
|
|
2137
|
+
? { status: "deferred", message: result.message }
|
|
2138
|
+
: { status: "ineligible", message: result.message };
|
|
2139
|
+
}
|
|
1106
2140
|
const expectedReplacements = new Map<string, ShimPathFingerprint>();
|
|
1107
2141
|
for (const [path, firstProbe] of replacementProbes) {
|
|
1108
2142
|
const secondProbe = stableShimPathProbe(path);
|