@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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 +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/update/index.ts
CHANGED
|
@@ -7,8 +7,12 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { getConfigDir, loadConfig } from "../config";
|
|
10
|
-
import { readPid, readRuntimePort } from "../config/process-state";
|
|
10
|
+
import { readPid, readRuntimePort, getRuntimePortPath } from "../config/process-state";
|
|
11
11
|
import { pendingTeardownOutstanding } from "../config/pending-teardown";
|
|
12
|
+
import type { ServiceOwnership } from "../service/state";
|
|
13
|
+
import { planUpdateRuntimeHandling, planStoppedRuntimeRecovery, inspectPackageRuntimeLiveness } from "./runtime-ownership.mjs";
|
|
14
|
+
import { unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
|
|
15
|
+
import { withUpdateOwnershipLease, readUpdateRuntimeTarget } from "./ownership-transaction";
|
|
12
16
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
13
17
|
import { pnpmInvocation, pnpmInvocationForPath, resolvePnpmCommands } from "./pnpm-invocation.mjs";
|
|
14
18
|
import { detectInstallFromPath } from "./install-detection.mjs";
|
|
@@ -26,6 +30,7 @@ import {
|
|
|
26
30
|
} from "./npm-cache-preflight.mjs";
|
|
27
31
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
28
32
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
33
|
+
import { packageVersion } from "../lib/package-version";
|
|
29
34
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
30
35
|
|
|
31
36
|
/**
|
|
@@ -79,6 +84,7 @@ function runPnpmCandidate(
|
|
|
79
84
|
encoding: "utf8",
|
|
80
85
|
timeout: 20_000,
|
|
81
86
|
windowsHide: true,
|
|
87
|
+
env: unprivilegedOwnershipMutationEnvironment(process.env),
|
|
82
88
|
...invocation.options,
|
|
83
89
|
});
|
|
84
90
|
}
|
|
@@ -121,7 +127,7 @@ function runOwnedPnpm(
|
|
|
121
127
|
encoding: "utf8",
|
|
122
128
|
timeout: 180_000,
|
|
123
129
|
windowsHide: true,
|
|
124
|
-
env: target.env,
|
|
130
|
+
env: unprivilegedOwnershipMutationEnvironment(target.env),
|
|
125
131
|
...target.options,
|
|
126
132
|
});
|
|
127
133
|
}
|
|
@@ -143,11 +149,7 @@ export function resolvePnpmActiveLauncher(owner: PnpmGlobalOwner): string | null
|
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
export function currentVersion(): string {
|
|
146
|
-
|
|
147
|
-
return (JSON.parse(readFileSync(join(HERE, "..", "..", "package.json"), "utf8")).version as string) ?? "?";
|
|
148
|
-
} catch {
|
|
149
|
-
return "?";
|
|
150
|
-
}
|
|
152
|
+
return packageVersion("?");
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
export function defaultUpdateTag(current: string): Channel {
|
|
@@ -259,7 +261,7 @@ export function latestVersion(
|
|
|
259
261
|
encoding: "utf8",
|
|
260
262
|
timeout: 12000,
|
|
261
263
|
windowsHide: true,
|
|
262
|
-
|
|
264
|
+
env: unprivilegedOwnershipMutationEnvironment(manager.env ?? process.env),
|
|
263
265
|
...manager.options,
|
|
264
266
|
});
|
|
265
267
|
return r.status === 0 && typeof r.stdout === "string" ? (r.stdout.trim() || null) : null;
|
|
@@ -313,13 +315,46 @@ export function checkUpdatePackageIntegrity(
|
|
|
313
315
|
encoding: "utf8",
|
|
314
316
|
timeout: 12000,
|
|
315
317
|
windowsHide: true,
|
|
316
|
-
|
|
318
|
+
env: unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
|
|
317
319
|
...target.options,
|
|
318
320
|
});
|
|
319
321
|
});
|
|
320
322
|
return result;
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
/**
|
|
326
|
+
* The recorded runtime owner, in the shape the shared update rule reads.
|
|
327
|
+
*
|
|
328
|
+
* Fails CLOSED. A resolution this process could not obtain is not evidence that nobody owns
|
|
329
|
+
* the runtime, and treating it as such is how an unreadable record reactivates the npm
|
|
330
|
+
* launcher over a takeover the user consented to.
|
|
331
|
+
*/
|
|
332
|
+
interface RuntimeOwnershipObservation {
|
|
333
|
+
readonly ownership: ServiceOwnership | null;
|
|
334
|
+
readonly ownershipUnknown: boolean;
|
|
335
|
+
readonly subjectToken: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function resolvedRuntimeOwnership(): Promise<RuntimeOwnershipObservation> {
|
|
339
|
+
try {
|
|
340
|
+
const { resolveServiceOwnership } = await import("../service");
|
|
341
|
+
const resolution = resolveServiceOwnership();
|
|
342
|
+
if (resolution.kind === "owned") return {
|
|
343
|
+
ownership: resolution.ownership,
|
|
344
|
+
ownershipUnknown: false,
|
|
345
|
+
subjectToken: JSON.stringify(["owned", resolution.revision, resolution.ownership]),
|
|
346
|
+
};
|
|
347
|
+
if (resolution.kind === "none") return {
|
|
348
|
+
ownership: null,
|
|
349
|
+
ownershipUnknown: false,
|
|
350
|
+
subjectToken: JSON.stringify(["none", resolution.revision]),
|
|
351
|
+
};
|
|
352
|
+
return { ownership: null, ownershipUnknown: true, subjectToken: "unknown" };
|
|
353
|
+
} catch {
|
|
354
|
+
return { ownership: null, ownershipUnknown: true, subjectToken: "unknown" };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
323
358
|
/**
|
|
324
359
|
* `ocx update` fallback for source checkouts and Bun global installs. npm and pnpm global installs
|
|
325
360
|
* are updated in the Node bin launcher before Bun starts, so Windows does not replace the running
|
|
@@ -378,6 +413,8 @@ export async function runUpdate(): Promise<void> {
|
|
|
378
413
|
process.exit(1);
|
|
379
414
|
}
|
|
380
415
|
|
|
416
|
+
const { serviceStatePaths } = await import("../service");
|
|
417
|
+
const updateExitCode = await withUpdateOwnershipLease(serviceStatePaths(), async mutation => {
|
|
381
418
|
// Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
|
|
382
419
|
// unloads it, so a successful update must repair/restart it afterwards.
|
|
383
420
|
let serviceWasInstalled = false;
|
|
@@ -385,6 +422,18 @@ export async function runUpdate(): Promise<void> {
|
|
|
385
422
|
const { isServiceInstalled } = await import("../service");
|
|
386
423
|
serviceWasInstalled = isServiceInstalled();
|
|
387
424
|
} catch { /* best-effort */ }
|
|
425
|
+
// What this update may do to the runtime. A desktop takeover vetoes both the stop and the
|
|
426
|
+
// service refresh below; see `planUpdateRuntimeHandling` for why each half is wrong.
|
|
427
|
+
const initialOwnership = await resolvedRuntimeOwnership();
|
|
428
|
+
const runtimePlan = planUpdateRuntimeHandling({
|
|
429
|
+
...initialOwnership,
|
|
430
|
+
serviceInstalled: serviceWasInstalled,
|
|
431
|
+
});
|
|
432
|
+
if (runtimePlan.notice) console.log(runtimePlan.notice);
|
|
433
|
+
if (!runtimePlan.mayReplacePackage) {
|
|
434
|
+
console.error("⚠️ Update stopped before tray handoff, runtime stop, or package replacement because runtime ownership is unknown.");
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
388
437
|
let trayWasInstalled = false;
|
|
389
438
|
let trayWasRunning = false;
|
|
390
439
|
if (process.platform === "win32") {
|
|
@@ -402,7 +451,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
402
451
|
trayWasRunning = trayPlan.restoreOnFailure;
|
|
403
452
|
} catch (error) {
|
|
404
453
|
console.error(`⚠️ Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`);
|
|
405
|
-
|
|
454
|
+
return 1;
|
|
406
455
|
}
|
|
407
456
|
}
|
|
408
457
|
|
|
@@ -421,6 +470,79 @@ export async function runUpdate(): Promise<void> {
|
|
|
421
470
|
...(runtimeTrusted && livePid ? { oldPid: livePid } : {}),
|
|
422
471
|
};
|
|
423
472
|
|
|
473
|
+
const currentPackageRuntimeLiveness = () => inspectPackageRuntimeLiveness({
|
|
474
|
+
capturedTarget: capturedListen,
|
|
475
|
+
readCurrentTarget: () => readUpdateRuntimeTarget(getRuntimePortPath(), capturedListen.hostname),
|
|
476
|
+
probe: target => probeProxyLiveness(target.port, target.hostname),
|
|
477
|
+
}).overall;
|
|
478
|
+
const hadRuntimeState = serviceWasInstalled || !!livePid || !!preUpdateRt;
|
|
479
|
+
|
|
480
|
+
let stopAttempted = false;
|
|
481
|
+
let postUpdateLauncher = installer === "pnpm" && owner
|
|
482
|
+
? join(owner.packagePath, "bin", "ocx.mjs")
|
|
483
|
+
: join(packageRoot(), "bin", "ocx.mjs");
|
|
484
|
+
let postUpdateLauncherUsable = true;
|
|
485
|
+
const sameOwner = (current: RuntimeOwnershipObservation) => {
|
|
486
|
+
const before = initialOwnership.ownership, after = current.ownership;
|
|
487
|
+
return before?.owner === after?.owner && before?.installId === after?.installId
|
|
488
|
+
&& before?.consentGeneration === after?.consentGeneration;
|
|
489
|
+
};
|
|
490
|
+
const startProxyDirectly = async (): Promise<boolean> => {
|
|
491
|
+
if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) return false;
|
|
492
|
+
const env = mutation.controlEnvironment();
|
|
493
|
+
delete env.OCX_SERVICE;
|
|
494
|
+
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
|
|
495
|
+
detached: true, stdio: "ignore", windowsHide: true, env: withProcessRuntimeProvenance(env),
|
|
496
|
+
});
|
|
497
|
+
let failed = false;
|
|
498
|
+
child.once("error", () => { failed = true; });
|
|
499
|
+
child.unref();
|
|
500
|
+
const deadline = Date.now() + 30_000;
|
|
501
|
+
while (!failed && Date.now() < deadline) {
|
|
502
|
+
const current = readUpdateRuntimeTarget(getRuntimePortPath(), capturedListen.hostname);
|
|
503
|
+
if (current.kind === "target" && current.target.port === capturedListen.port
|
|
504
|
+
&& await proxyIdentityAt(current.target.port, { hostname: current.target.hostname, expectedPid: current.target.pid },
|
|
505
|
+
{ timeoutMs: 1500, deadlineAt: deadline, attempts: 1 })) return true;
|
|
506
|
+
await Bun.sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
507
|
+
}
|
|
508
|
+
console.warn("⚠️ Recovery did not report a healthy proxy before its deadline; run 'ocx service repair' or 'ocx start'.");
|
|
509
|
+
return false;
|
|
510
|
+
};
|
|
511
|
+
const recoverStoppedRuntime = async (reason: string): Promise<void> => {
|
|
512
|
+
try {
|
|
513
|
+
const current = await resolvedRuntimeOwnership();
|
|
514
|
+
const recovery = planStoppedRuntimeRecovery({
|
|
515
|
+
stopAttempted, ...current, sameOwner: sameOwner(current),
|
|
516
|
+
liveness: currentPackageRuntimeLiveness(), serviceInstalled: serviceWasInstalled,
|
|
517
|
+
launcherUsable: postUpdateLauncherUsable && existsSync(postUpdateLauncher), hadRuntimeState,
|
|
518
|
+
});
|
|
519
|
+
if (recovery.action === "manual") {
|
|
520
|
+
console.warn(`⚠️ ${reason}; runtime recovery requires manual review (${recovery.reason}).`);
|
|
521
|
+
} else if (recovery.action === "service") {
|
|
522
|
+
const { serviceReinstallArgs, isServiceViable } = await import("../service");
|
|
523
|
+
const service = spawnSync(process.execPath, [postUpdateLauncher, ...serviceReinstallArgs()], {
|
|
524
|
+
stdio: updateChildStdio(), windowsHide: true,
|
|
525
|
+
env: mutation.controlEnvironment({ ...process.env, OCX_BAKE_PORT: String(capturedListen.port) }),
|
|
526
|
+
});
|
|
527
|
+
if (service.status !== 0 || !isServiceViable()) {
|
|
528
|
+
const nowOwned = await resolvedRuntimeOwnership();
|
|
529
|
+
const fallback = planStoppedRuntimeRecovery({
|
|
530
|
+
stopAttempted, ...nowOwned, sameOwner: sameOwner(nowOwned),
|
|
531
|
+
liveness: currentPackageRuntimeLiveness(), serviceInstalled: false,
|
|
532
|
+
launcherUsable: postUpdateLauncherUsable && existsSync(postUpdateLauncher), hadRuntimeState,
|
|
533
|
+
});
|
|
534
|
+
if (fallback.action === "direct") await startProxyDirectly();
|
|
535
|
+
else console.warn("⚠️ Service recovery was not confirmed; no second proxy was started.");
|
|
536
|
+
}
|
|
537
|
+
} else if (recovery.action === "direct") {
|
|
538
|
+
await startProxyDirectly();
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
console.warn("⚠️ Runtime recovery could not be verified; run 'ocx service repair' or 'ocx start' after checking ownership.");
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
try {
|
|
424
546
|
// Never replace package files under a live proxy: the running server dynamic-imports
|
|
425
547
|
// modules after startup, so an in-place update leaves it executing mixed old/new code.
|
|
426
548
|
// Gate on the service and the runtime-port record too, not just the pid file — a
|
|
@@ -431,12 +553,13 @@ export async function runUpdate(): Promise<void> {
|
|
|
431
553
|
// shared client config still points at a proxy that is gone; installing over that
|
|
432
554
|
// silently skips the recovery the receipt was written to trigger (#3008).
|
|
433
555
|
// Full `ocx stop` semantics (drain, service stop, restore).
|
|
434
|
-
|
|
435
|
-
if (serviceWasInstalled || readPid() || readRuntimePort() || pendingTeardownOutstanding()) {
|
|
556
|
+
|
|
557
|
+
if (runtimePlan.mayStopRuntime && (serviceWasInstalled || readPid() || readRuntimePort() || pendingTeardownOutstanding())) {
|
|
436
558
|
stopAttempted = true;
|
|
437
559
|
console.log("⏹ Stopping the running proxy before updating...");
|
|
438
560
|
const stopStdio = updateChildStdio();
|
|
439
561
|
const stop = spawnSync(process.execPath, selfLaunchArgv(["stop"]), {
|
|
562
|
+
env: mutation.controlEnvironment(),
|
|
440
563
|
stdio: stopStdio,
|
|
441
564
|
encoding: stopStdio === "pipe" ? "utf8" : undefined,
|
|
442
565
|
windowsHide: true,
|
|
@@ -472,7 +595,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
472
595
|
? `⚠️ Could not confirm the proxy on ${capturedListen.hostname}:${capturedListen.port} is stopped; aborting the update. Run 'ocx stop' and retry.`
|
|
473
596
|
: "⚠️ Could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.");
|
|
474
597
|
}
|
|
475
|
-
|
|
598
|
+
return 1;
|
|
476
599
|
}
|
|
477
600
|
if (historyOnlyStop || historyRestoreIncomplete()) {
|
|
478
601
|
console.warn(
|
|
@@ -493,26 +616,34 @@ export async function runUpdate(): Promise<void> {
|
|
|
493
616
|
}
|
|
494
617
|
}
|
|
495
618
|
|
|
496
|
-
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
497
|
-
|
|
498
619
|
const installStdio = updateChildStdio();
|
|
499
|
-
// Every post-update action below receives this path. For pnpm it is replaced only
|
|
500
|
-
// by a path returned after tree+shim verification; on rollback, activePath is
|
|
501
|
-
// likewise returned only after the old group has been verified again.
|
|
502
|
-
let postUpdateLauncher = join(packageRoot(), "bin", "ocx.mjs");
|
|
503
|
-
// The pnpm owner preflight has verified this package tree and global group. Keep that exact
|
|
504
|
-
// package path as the recovery starting point; the path returned by the update transaction
|
|
505
|
-
// replaces it only after post-update tree+shim verification succeeds.
|
|
506
|
-
if (installer === "pnpm" && owner) {
|
|
507
|
-
postUpdateLauncher = join(owner.packagePath, "bin", "ocx.mjs");
|
|
508
|
-
}
|
|
509
|
-
let postUpdateLauncherUsable = true;
|
|
510
620
|
let r: {
|
|
511
621
|
status: number | null;
|
|
512
622
|
signal?: NodeJS.Signals | null;
|
|
513
623
|
stdout?: string | Buffer | null;
|
|
514
624
|
stderr?: string | Buffer | null;
|
|
515
|
-
};
|
|
625
|
+
} | null = null;
|
|
626
|
+
let replacementRefusal: string | null = null;
|
|
627
|
+
// Ownership can change while registry work is in flight. Unknown at this exact
|
|
628
|
+
// boundary blocks replacement; a confirmed desktop claim still permits updating the idle
|
|
629
|
+
// npm installation while leaving the bundled sidecar alone.
|
|
630
|
+
const replacementOwnership = await resolvedRuntimeOwnership();
|
|
631
|
+
const replacementPlan = planUpdateRuntimeHandling({
|
|
632
|
+
...replacementOwnership,
|
|
633
|
+
serviceInstalled: serviceWasInstalled,
|
|
634
|
+
});
|
|
635
|
+
const replacementLiveness = currentPackageRuntimeLiveness();
|
|
636
|
+
if (replacementOwnership.subjectToken !== initialOwnership.subjectToken
|
|
637
|
+
|| !replacementPlan.mayReplacePackage
|
|
638
|
+
|| replacementLiveness !== "dead") {
|
|
639
|
+
replacementRefusal = replacementPlan.notice
|
|
640
|
+
?? (replacementLiveness === "live"
|
|
641
|
+
? "⚠️ Update stopped because a proxy became live after the stop decision; rerun from the beginning."
|
|
642
|
+
: "⚠️ Update stopped because runtime ownership or liveness changed after the stop decision; rerun from the beginning.");
|
|
643
|
+
} else {
|
|
644
|
+
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
645
|
+
|
|
646
|
+
// Every post-update action below receives the verified active launcher.
|
|
516
647
|
if (installer === "pnpm") {
|
|
517
648
|
let update: ReturnType<typeof runPnpmGlobalUpdate>;
|
|
518
649
|
try {
|
|
@@ -558,9 +689,27 @@ export async function runUpdate(): Promise<void> {
|
|
|
558
689
|
timeout: 180000,
|
|
559
690
|
windowsHide: true,
|
|
560
691
|
...target.options,
|
|
692
|
+
env: mutation.unprivilegedEnvironment(),
|
|
561
693
|
});
|
|
562
694
|
}
|
|
563
|
-
if (installStdio === "pipe") logSpawnOutput("", r);
|
|
695
|
+
if (r && installStdio === "pipe") logSpawnOutput("", r);
|
|
696
|
+
}
|
|
697
|
+
if (replacementRefusal) {
|
|
698
|
+
await recoverStoppedRuntime("package replacement was refused");
|
|
699
|
+
if (trayWasRunning) {
|
|
700
|
+
try {
|
|
701
|
+
const { startWindowsTray } = await import("../tray/windows");
|
|
702
|
+
startWindowsTray();
|
|
703
|
+
} catch { /* preserve the ownership refusal */ }
|
|
704
|
+
}
|
|
705
|
+
console.error(replacementRefusal);
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
if (!r) throw new Error("update replacement returned no result");
|
|
709
|
+
const postInstallPlan = planUpdateRuntimeHandling({
|
|
710
|
+
...(await resolvedRuntimeOwnership()),
|
|
711
|
+
serviceInstalled: serviceWasInstalled,
|
|
712
|
+
});
|
|
564
713
|
if (r.status === 0) {
|
|
565
714
|
console.log(`\n✅ Updated${latest ? ` to v${latest}` : ""}.`);
|
|
566
715
|
// Re-enter through the verified active package launcher. This keeps the Codex
|
|
@@ -572,6 +721,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
572
721
|
const shim = spawnSync(process.execPath, [postUpdateLauncher, "codex-shim", "install"], {
|
|
573
722
|
stdio: "inherit",
|
|
574
723
|
windowsHide: true,
|
|
724
|
+
env: mutation.unprivilegedEnvironment(),
|
|
575
725
|
});
|
|
576
726
|
if (shim.status !== 0) console.warn("⚠️ Shim repair skipped: run 'ocx codex-shim install'.");
|
|
577
727
|
}
|
|
@@ -580,18 +730,18 @@ export async function runUpdate(): Promise<void> {
|
|
|
580
730
|
}
|
|
581
731
|
if (trayWasInstalled) {
|
|
582
732
|
const trayArgs = planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs;
|
|
583
|
-
const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayArgs], { stdio: "inherit", windowsHide: true });
|
|
733
|
+
const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayArgs], { stdio: "inherit", windowsHide: true, env: mutation.unprivilegedEnvironment() });
|
|
584
734
|
if (tray.status === 0) {
|
|
585
735
|
console.log("🔧 Refreshed Windows tray startup paths.");
|
|
586
736
|
} else {
|
|
587
737
|
console.warn("⚠️ Windows tray refresh failed. Run 'ocx tray install'.");
|
|
588
|
-
if (trayWasRunning) spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true });
|
|
738
|
+
if (trayWasRunning) spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true, env: mutation.unprivilegedEnvironment() });
|
|
589
739
|
}
|
|
590
740
|
}
|
|
591
741
|
// The stop above unloaded any managed service; repair it with the NEW files
|
|
592
742
|
// (spawn the fresh cli.ts so updated code writes the baked paths) so a
|
|
593
743
|
// launchd/schtasks/systemd user isn't left with the background proxy down.
|
|
594
|
-
if (
|
|
744
|
+
if (postInstallPlan.mayRestoreService) {
|
|
595
745
|
console.log("🔁 Refreshing the background service with the updated files...");
|
|
596
746
|
const { serviceReinstallArgs } = await import("../service");
|
|
597
747
|
const { reclaimListenPort } = await import("../server/port-reclaim");
|
|
@@ -610,6 +760,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
610
760
|
try {
|
|
611
761
|
const svcStdio = updateChildStdio();
|
|
612
762
|
const svc = spawnSync(process.execPath, [postUpdateLauncher, ...serviceReinstallArgs()], {
|
|
763
|
+
env: mutation.controlEnvironment(),
|
|
613
764
|
stdio: svcStdio,
|
|
614
765
|
encoding: svcStdio === "pipe" ? "utf8" : undefined,
|
|
615
766
|
windowsHide: true,
|
|
@@ -641,6 +792,17 @@ export async function runUpdate(): Promise<void> {
|
|
|
641
792
|
? ` Run 'ocx service repair', then 'ocx start --port ${capturedListen.port}'.`
|
|
642
793
|
: ` Run 'ocx service repair' to see the reason, then 'ocx start --port ${capturedListen.port}'.`);
|
|
643
794
|
} else {
|
|
795
|
+
// Re-read rather than reuse the plan from before the package install: the app can
|
|
796
|
+
// claim the runtime during an update that takes minutes, and the refusal the
|
|
797
|
+
// repair above just returned is indistinguishable from any other failure here.
|
|
798
|
+
const nowOwned = planUpdateRuntimeHandling({
|
|
799
|
+
...(await resolvedRuntimeOwnership()),
|
|
800
|
+
serviceInstalled: true,
|
|
801
|
+
});
|
|
802
|
+
if (!nowOwned.mayStopRuntime) {
|
|
803
|
+
console.warn(nowOwned.notice ?? "⚠️ The background runtime is owned elsewhere; not starting a second proxy.");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
644
806
|
console.warn(
|
|
645
807
|
serviceRefreshed
|
|
646
808
|
? "⚠️ Service refresh left a non-viable manager (stale or missing assets) — starting the proxy directly instead."
|
|
@@ -652,49 +814,31 @@ export async function runUpdate(): Promise<void> {
|
|
|
652
814
|
console.warn(process.platform === "win32"
|
|
653
815
|
? " Run 'ocx service repair' to refresh the background service."
|
|
654
816
|
: " Run 'ocx service repair' to refresh the background service and see why it failed.");
|
|
655
|
-
|
|
656
|
-
delete env.OCX_SERVICE;
|
|
657
|
-
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
|
|
658
|
-
detached: true,
|
|
659
|
-
stdio: "ignore",
|
|
660
|
-
windowsHide: true,
|
|
661
|
-
env: withProcessRuntimeProvenance(env),
|
|
662
|
-
});
|
|
663
|
-
child.unref();
|
|
664
|
-
console.log(`✅ Proxy starting on port ${capturedListen.port}.`);
|
|
817
|
+
if (await startProxyDirectly()) console.log(`✅ Proxy is healthy on port ${capturedListen.port}.`);
|
|
665
818
|
}
|
|
666
819
|
}
|
|
667
820
|
} finally {
|
|
668
821
|
if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
|
|
669
822
|
else process.env.OCX_BAKE_PORT = prevBake;
|
|
670
823
|
}
|
|
671
|
-
} else {
|
|
824
|
+
} else if (postInstallPlan.mayStopRuntime) {
|
|
672
825
|
console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, capturedListen.port)}`);
|
|
673
826
|
}
|
|
674
827
|
} else {
|
|
675
828
|
if (stopAttempted && trayWasRunning && postUpdateLauncherUsable) {
|
|
676
|
-
spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true });
|
|
677
|
-
}
|
|
678
|
-
if (stopAttempted && serviceWasInstalled && postUpdateLauncherUsable) {
|
|
679
|
-
const service = spawnSync(process.execPath, [postUpdateLauncher, "service", "repair"], {
|
|
680
|
-
stdio: "inherit",
|
|
681
|
-
windowsHide: true,
|
|
682
|
-
});
|
|
683
|
-
if (service.status !== 0) console.warn("⚠️ Previous background service could not be restored; run 'ocx service repair'.");
|
|
684
|
-
} else if (stopAttempted && postUpdateLauncherUsable) {
|
|
685
|
-
const env = { ...process.env };
|
|
686
|
-
delete env.OCX_SERVICE;
|
|
687
|
-
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
|
|
688
|
-
detached: true,
|
|
689
|
-
stdio: "ignore",
|
|
690
|
-
windowsHide: true,
|
|
691
|
-
env: withProcessRuntimeProvenance(env),
|
|
692
|
-
});
|
|
693
|
-
child.unref();
|
|
694
|
-
} else if (stopAttempted) {
|
|
695
|
-
console.error("opencodex: no verified active launcher remains for automatic recovery; reinstall opencodex manually.");
|
|
829
|
+
spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true, env: mutation.unprivilegedEnvironment() });
|
|
696
830
|
}
|
|
831
|
+
await recoverStoppedRuntime("package update failed");
|
|
697
832
|
console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`);
|
|
698
|
-
|
|
833
|
+
return 1;
|
|
699
834
|
}
|
|
835
|
+
} catch (error) {
|
|
836
|
+
await recoverStoppedRuntime("update failed unexpectedly");
|
|
837
|
+
if (trayWasRunning) {
|
|
838
|
+
try { const { startWindowsTray } = await import("../tray/windows"); startWindowsTray(); } catch { /* retain original failure */ }
|
|
839
|
+
}
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
if (updateExitCode) process.exit(updateExitCode);
|
|
700
844
|
}
|
package/src/update/job.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { listListenPids, reclaimListenPort, scanListenPids, type ListenPidScan }
|
|
|
26
26
|
import { dropWindowsTcpRowsForLocalPort } from "../server/windows-tcp-drop";
|
|
27
27
|
import { isOpencodexHealthz, probeHostname, proxyIdentityAt, type HealthzIdentity } from "../server/proxy-liveness";
|
|
28
28
|
import { isServiceInstalled, isServiceViable, readServiceBackend, stopWindows } from "../service";
|
|
29
|
+
import { runUpdateRestartWithOwnershipLease, type ServiceOwnershipResolution } from "./restart-ownership";
|
|
29
30
|
import {
|
|
30
31
|
type Channel,
|
|
31
32
|
type Installer,
|
|
@@ -1804,6 +1805,8 @@ export interface GuiUpdateWorkerIo {
|
|
|
1804
1805
|
resolvePnpmActiveLauncherFn?: (owner: PnpmGlobalOwner) => string | null;
|
|
1805
1806
|
/** Restart seams used by focused worker tests; the verified launcher is always injected. */
|
|
1806
1807
|
restartIo?: RestartIo;
|
|
1808
|
+
/** Resolves who owns the runtime; defaults to the shared service install state. */
|
|
1809
|
+
resolveOwnershipFn?: () => ServiceOwnershipResolution;
|
|
1807
1810
|
runCommandFn?: (
|
|
1808
1811
|
job: UpdateJobState,
|
|
1809
1812
|
bin: string,
|
|
@@ -1967,11 +1970,12 @@ export async function runGuiUpdateWorker(
|
|
|
1967
1970
|
}
|
|
1968
1971
|
|
|
1969
1972
|
if (restart) {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
...io.restartIo,
|
|
1973
|
-
|
|
1974
|
-
})
|
|
1973
|
+
const outcome = await runUpdateRestartWithOwnershipLease(io.resolveOwnershipFn, async () => {
|
|
1974
|
+
job = updateJob(job!, { status: "restarting" }, "Update installed. Restarting proxy...");
|
|
1975
|
+
return finishGuiUpdateRestart(job!, captured, check.installer, { ...io.restartIo, packageLauncherPathFn: () => activeLauncher });
|
|
1976
|
+
});
|
|
1977
|
+
if (outcome.kind === "veto") { updateJob(job, { status: "succeeded", restarted: false }, outcome.notice); return; }
|
|
1978
|
+
if (!outcome.value) return;
|
|
1975
1979
|
updateJob(job, { status: "succeeded", restarted: true }, "Restart requested and proxy is healthy.");
|
|
1976
1980
|
return;
|
|
1977
1981
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
acquireOwnershipMutationLease,
|
|
4
|
+
ownershipMutationLeaseChildEnvironment,
|
|
5
|
+
unprivilegedOwnershipMutationEnvironment,
|
|
6
|
+
} from "../service/ownership-mutation-lease.mjs";
|
|
7
|
+
|
|
8
|
+
type Environment = Record<string, string | undefined>;
|
|
9
|
+
export interface UpdateOwnershipTransaction {
|
|
10
|
+
controlEnvironment(environment?: Environment): Environment;
|
|
11
|
+
unprivilegedEnvironment(environment?: Environment): Environment;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One lease spans awaited stop, replacement and recovery. Never publish its token globally. */
|
|
15
|
+
export async function withUpdateOwnershipLease<T>(
|
|
16
|
+
paths: string[], run: (transaction: UpdateOwnershipTransaction) => Promise<T>,
|
|
17
|
+
): Promise<T> {
|
|
18
|
+
const lease = acquireOwnershipMutationLease(paths);
|
|
19
|
+
let released = false;
|
|
20
|
+
const release = () => { if (!released) { released = true; lease.release(); } };
|
|
21
|
+
try {
|
|
22
|
+
return await run({
|
|
23
|
+
controlEnvironment: (env = process.env) => ownershipMutationLeaseChildEnvironment(env, lease.token),
|
|
24
|
+
unprivilegedEnvironment: (env = process.env) => unprivilegedOwnershipMutationEnvironment(env),
|
|
25
|
+
});
|
|
26
|
+
} finally { release(); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type UpdateRuntimeTarget =
|
|
30
|
+
| { kind: "absent" | "unknown" }
|
|
31
|
+
| { kind: "target"; target: { pid: number; port: number; hostname: string } };
|
|
32
|
+
|
|
33
|
+
/** A malformed/unreadable record is uncertainty, never proof that the current runtime is absent. */
|
|
34
|
+
export function readUpdateRuntimeTarget(path: string, fallbackHostname: string): UpdateRuntimeTarget {
|
|
35
|
+
let raw: string;
|
|
36
|
+
try { raw = readFileSync(path, "utf8"); }
|
|
37
|
+
catch (error) { return { kind: (error as NodeJS.ErrnoException).code === "ENOENT" ? "absent" : "unknown" }; }
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(raw);
|
|
40
|
+
if (!Number.isSafeInteger(value?.pid) || value.pid <= 0
|
|
41
|
+
|| !Number.isInteger(value?.port) || value.port <= 0 || value.port > 65535
|
|
42
|
+
|| (value.hostname !== undefined && (typeof value.hostname !== "string" || !value.hostname.trim()))) {
|
|
43
|
+
return { kind: "unknown" };
|
|
44
|
+
}
|
|
45
|
+
return { kind: "target", target: { pid: value.pid, port: value.port, hostname: value.hostname?.trim() ?? fallbackHostname } };
|
|
46
|
+
} catch { return { kind: "unknown" }; }
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { resolveServiceOwnership } from "../service";
|
|
2
|
+
import type { ServiceOwnershipResolution } from "../service";
|
|
3
|
+
import { serviceStatePaths } from "../service/state";
|
|
4
|
+
import {
|
|
5
|
+
acquireOwnershipMutationLease,
|
|
6
|
+
OWNERSHIP_MUTATION_LEASE_TOKEN_ENV,
|
|
7
|
+
} from "../service/ownership-mutation-lease.mjs";
|
|
8
|
+
import { planUpdateRuntimeHandling } from "./runtime-ownership.mjs";
|
|
9
|
+
|
|
10
|
+
export type { ServiceOwnershipResolution };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Why a dashboard update must not restart a runtime it did not stop.
|
|
14
|
+
*
|
|
15
|
+
* The worker defaults to restarting. When the package updater it just ran left a
|
|
16
|
+
* foreign-owned runtime alone — which is the correct behaviour under a desktop takeover —
|
|
17
|
+
* the sidecar's unchanged pid fails the worker's restart evidence, so it reclaims the port,
|
|
18
|
+
* runs the repair that now refuses, and falls through to a direct start. The app's runtime
|
|
19
|
+
* ends up replaced by an npm proxy immediately after the update declined to touch it.
|
|
20
|
+
*
|
|
21
|
+
* Returns the line to report, or null when the restart may proceed. It lives beside the
|
|
22
|
+
* worker rather than inside it because `src/update/job.ts` is one line under the repository
|
|
23
|
+
* size cap.
|
|
24
|
+
*/
|
|
25
|
+
export function updateRestartVeto(
|
|
26
|
+
resolve: () => ServiceOwnershipResolution = resolveServiceOwnership,
|
|
27
|
+
): string | null {
|
|
28
|
+
const owner = resolve();
|
|
29
|
+
const plan = planUpdateRuntimeHandling({
|
|
30
|
+
ownership: owner.kind === "owned" ? owner.ownership : null,
|
|
31
|
+
ownershipUnknown: owner.kind === "unknown",
|
|
32
|
+
// The restart decision does not refresh the service; only the stop veto is read here.
|
|
33
|
+
serviceInstalled: false,
|
|
34
|
+
});
|
|
35
|
+
if (plan.mayStopRuntime) return null;
|
|
36
|
+
return plan.notice ?? "The background runtime is owned elsewhere; it was left running.";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runUpdateRestartWithOwnershipLease<T>(
|
|
40
|
+
resolve: (() => ServiceOwnershipResolution) | undefined,
|
|
41
|
+
restart: () => Promise<T>,
|
|
42
|
+
): Promise<{ readonly kind: "veto"; readonly notice: string } | { readonly kind: "ran"; readonly value: T }> {
|
|
43
|
+
const lease = acquireOwnershipMutationLease(serviceStatePaths());
|
|
44
|
+
const previous = process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
|
|
45
|
+
process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = lease.token;
|
|
46
|
+
try {
|
|
47
|
+
const veto = updateRestartVeto(resolve);
|
|
48
|
+
return veto ? { kind: "veto", notice: veto } : { kind: "ran", value: await restart() };
|
|
49
|
+
} finally {
|
|
50
|
+
if (previous === undefined) delete process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
|
|
51
|
+
else process.env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV] = previous;
|
|
52
|
+
lease.release();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare function planUpdateRuntimeHandling(input: {
|
|
2
|
+
ownership: { owner: string; installId: string; consentGeneration: number } | null;
|
|
3
|
+
ownershipUnknown?: boolean;
|
|
4
|
+
serviceInstalled: boolean;
|
|
5
|
+
}): {
|
|
6
|
+
mayReplacePackage: boolean;
|
|
7
|
+
mayStopRuntime: boolean;
|
|
8
|
+
mayRestoreService: boolean;
|
|
9
|
+
notice: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export declare function planStoppedRuntimeRecovery(input: {
|
|
13
|
+
stopAttempted: boolean;
|
|
14
|
+
ownership: { owner: string; installId: string; consentGeneration: number } | null;
|
|
15
|
+
ownershipUnknown?: boolean;
|
|
16
|
+
sameOwner: boolean;
|
|
17
|
+
liveness: "live" | "dead" | "unknown";
|
|
18
|
+
serviceInstalled: boolean;
|
|
19
|
+
launcherUsable: boolean;
|
|
20
|
+
hadRuntimeState: boolean;
|
|
21
|
+
}): {
|
|
22
|
+
action: "none" | "manual" | "service" | "direct";
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RuntimeTarget = { port: number; hostname: string };
|
|
27
|
+
type RuntimeLiveness = "live" | "dead" | "unknown";
|
|
28
|
+
|
|
29
|
+
export declare function inspectPackageRuntimeLiveness(input: {
|
|
30
|
+
capturedTarget: RuntimeTarget;
|
|
31
|
+
readCurrentTarget():
|
|
32
|
+
| { kind: "target"; target: RuntimeTarget }
|
|
33
|
+
| { kind: "absent" }
|
|
34
|
+
| { kind: "unknown" };
|
|
35
|
+
probe(target: RuntimeTarget): RuntimeLiveness;
|
|
36
|
+
}): {
|
|
37
|
+
current: RuntimeLiveness | "absent";
|
|
38
|
+
captured: RuntimeLiveness;
|
|
39
|
+
overall: RuntimeLiveness;
|
|
40
|
+
};
|