@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getRuntimePortPath, readRuntimePort } from "../config/process-state";
|
|
3
|
+
import { packageVersion } from "../lib/package-version";
|
|
4
|
+
import { parseStrictSemver } from "../lib/strict-semver";
|
|
5
|
+
import { acquireOwnershipMutationLease, OWNERSHIP_MUTATION_LEASE_TOKEN_ENV } from "../service/ownership-mutation-lease.mjs";
|
|
6
|
+
import { serviceStatePaths } from "../service/state";
|
|
7
|
+
import { assertUpdateRestartConfiguration, assertUpdateRestartHome, type UpdateRestartHome } from "./update-restart-home";
|
|
8
|
+
|
|
9
|
+
export const UPDATE_RESTART_CHILD_ENV = "OCX_UPDATE_RESTART_CHILD";
|
|
10
|
+
export interface UpdateRestartChildMarker { home: UpdateRestartHome; version: string; port: number; hostname: string; deadlineAt: number }
|
|
11
|
+
export interface UpdateRestartChildGuard { check(port?: number, hostname?: string): void; complete(): void }
|
|
12
|
+
|
|
13
|
+
function parseMarker(raw: string): UpdateRestartChildMarker {
|
|
14
|
+
if (raw.length > 8192) throw new Error("update_restart_child_marker_invalid");
|
|
15
|
+
const value = JSON.parse(raw) as UpdateRestartChildMarker;
|
|
16
|
+
if (!value || !Number.isSafeInteger(value.deadlineAt) || !Number.isInteger(value.port)
|
|
17
|
+
|| typeof value.hostname !== "string" || value.port < 1 || value.port > 65535 || typeof value.version !== "string"
|
|
18
|
+
|| value.version === "0.0.0" || !parseStrictSemver(value.version) || !value.home
|
|
19
|
+
|| !Number.isSafeInteger(value.home.revision)) throw new Error("update_restart_child_marker_invalid");
|
|
20
|
+
for (const directory of [value.home.config, value.home.codex]) {
|
|
21
|
+
if (!directory || typeof directory.path !== "string" || !directory.path
|
|
22
|
+
|| !Number.isSafeInteger(directory.dev) || !Number.isSafeInteger(directory.ino)) {
|
|
23
|
+
throw new Error("update_restart_child_marker_invalid");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UpdateRestartChildIo {
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
version?: () => string;
|
|
33
|
+
checkHome?: (home: UpdateRestartHome) => void;
|
|
34
|
+
acquire?: (waitMs: number) => { release(): void };
|
|
35
|
+
onExit?: (release: () => void) => void;
|
|
36
|
+
checkState?: (marker: UpdateRestartChildMarker) => void;
|
|
37
|
+
armDeadline?: (remainingMs: number) => () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Admit the replacement before CLI preflight; custody belongs to this child PID. */
|
|
41
|
+
export function admitUpdateRestartChild(argv: string[], io: UpdateRestartChildIo = {}): UpdateRestartChildGuard | undefined {
|
|
42
|
+
const env = io.env ?? process.env;
|
|
43
|
+
const raw = env[UPDATE_RESTART_CHILD_ENV];
|
|
44
|
+
if (raw === undefined) return undefined;
|
|
45
|
+
delete env[UPDATE_RESTART_CHILD_ENV];
|
|
46
|
+
delete env[OWNERSHIP_MUTATION_LEASE_TOKEN_ENV];
|
|
47
|
+
const marker = parseMarker(raw);
|
|
48
|
+
if (argv.length !== 3 || argv[0] !== "start" || argv[1] !== "--port" || argv[2] !== String(marker.port)) {
|
|
49
|
+
throw new Error("update_restart_child_command_changed");
|
|
50
|
+
}
|
|
51
|
+
const now = io.now ?? Date.now;
|
|
52
|
+
const check = (port = marker.port, hostname = marker.hostname) => {
|
|
53
|
+
if (now() >= marker.deadlineAt) throw new Error("update_restart_deadline_expired");
|
|
54
|
+
const bindHost = (host: string) => !host.trim() || /^localhost\.?$/i.test(host.trim()) ? "127.0.0.1" : host.trim();
|
|
55
|
+
if (port !== marker.port || bindHost(hostname) !== bindHost(marker.hostname) || (io.version ?? packageVersion)() !== marker.version) throw new Error("update_restart_child_identity_changed");
|
|
56
|
+
(io.checkHome ?? assertUpdateRestartHome)(marker.home);
|
|
57
|
+
(io.checkState ?? (expected => {
|
|
58
|
+
assertUpdateRestartConfiguration(expected.hostname);
|
|
59
|
+
const current = readRuntimePort();
|
|
60
|
+
if (existsSync(getRuntimePortPath()) && (!current || current.pid !== process.pid
|
|
61
|
+
|| current.port !== expected.port || current.siblingOfPort !== undefined)) {
|
|
62
|
+
throw new Error("update_restart_competing_runtime");
|
|
63
|
+
}
|
|
64
|
+
}))(marker);
|
|
65
|
+
};
|
|
66
|
+
check();
|
|
67
|
+
const lease = (io.acquire ?? (waitMs => acquireOwnershipMutationLease(serviceStatePaths(), { waitMs })))(Math.min(2000, marker.deadlineAt - now()));
|
|
68
|
+
let released = false;
|
|
69
|
+
let cancelDeadline: (() => void) | undefined;
|
|
70
|
+
const release = () => { if (!released) { released = true; cancelDeadline?.(); lease.release(); } };
|
|
71
|
+
try {
|
|
72
|
+
check();
|
|
73
|
+
cancelDeadline = (io.armDeadline ?? (remainingMs => {
|
|
74
|
+
const timer = setTimeout(() => process.exit(1), remainingMs);
|
|
75
|
+
timer.unref();
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}))(marker.deadlineAt - now());
|
|
78
|
+
(io.onExit ?? (callback => { process.once("exit", callback); }))(release);
|
|
79
|
+
return { check, complete: () => { check(); release(); } };
|
|
80
|
+
} catch (error) { release(); throw error; }
|
|
81
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { readClientConnectionState } from "../client/state";
|
|
3
|
+
import { readConfigDiagnostics } from "../config/diagnostics";
|
|
4
|
+
import { getConfigDir } from "../config/paths";
|
|
5
|
+
import { currentCodexHome, resolveServiceOwnership, resolveServiceState } from "../service/state";
|
|
6
|
+
|
|
7
|
+
export interface UpdateRestartHome {
|
|
8
|
+
config: { path: string; dev: number; ino: number };
|
|
9
|
+
codex: { path: string; dev: number; ino: number };
|
|
10
|
+
revision: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function physicalDirectory(path: string): UpdateRestartHome["config"] {
|
|
14
|
+
const canonical = realpathSync.native(path);
|
|
15
|
+
const stat = statSync(canonical);
|
|
16
|
+
if (!stat.isDirectory()) throw new Error("update_restart_home_unverified");
|
|
17
|
+
return { path: canonical, dev: stat.dev, ino: stat.ino };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Only an unclaimed standalone home is eligible for this narrow update path. */
|
|
21
|
+
export function readUpdateRestartHome(): UpdateRestartHome {
|
|
22
|
+
const state = resolveServiceState();
|
|
23
|
+
const owner = resolveServiceOwnership();
|
|
24
|
+
if (state.kind !== "none" || owner.kind !== "none") throw new Error("update_restart_owner_unverified");
|
|
25
|
+
return { config: physicalDirectory(getConfigDir()), codex: physicalDirectory(currentCodexHome()), revision: owner.revision };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function assertUpdateRestartHome(expected: UpdateRestartHome): void {
|
|
29
|
+
const current = readUpdateRestartHome();
|
|
30
|
+
if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error("update_restart_home_changed");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read-only eligibility shared by parent pre-stop and child admission. */
|
|
34
|
+
export function assertUpdateRestartConfiguration(hostname: string): void {
|
|
35
|
+
const diagnostics = readConfigDiagnostics();
|
|
36
|
+
if (readClientConnectionState().kind !== "disconnected" || diagnostics.error
|
|
37
|
+
|| (diagnostics.config.hostname ?? "") !== hostname) throw new Error("update_restart_configuration_changed");
|
|
38
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Agent, request, type ClientRequest, type IncomingMessage } from "node:http";
|
|
2
|
+
import { connect, type Socket } from "node:net";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
4
|
+
import {
|
|
5
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
6
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
7
|
+
createLocalAttestationChallenge,
|
|
8
|
+
isLocalAttestationSecret,
|
|
9
|
+
verifyLocalAttestationProof,
|
|
10
|
+
} from "../lib/local-management-attestation";
|
|
11
|
+
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
12
|
+
import { isOpencodexHealthz, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
13
|
+
import { computeVersionSkew } from "./version-skew";
|
|
14
|
+
|
|
15
|
+
const FAILURE = "update_restart_stop_failed";
|
|
16
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
17
|
+
|
|
18
|
+
/** This agent owns exactly one direct TCP connection, including on Bun's HTTP layer. */
|
|
19
|
+
class AttestedAgent extends Agent {
|
|
20
|
+
socket?: Socket;
|
|
21
|
+
private connected = false;
|
|
22
|
+
|
|
23
|
+
constructor(private readonly hostname: string, private readonly port: number) {
|
|
24
|
+
super({ keepAlive: true, maxSockets: 1, maxTotalSockets: 1, maxFreeSockets: 1 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override createConnection(
|
|
28
|
+
_options: unknown,
|
|
29
|
+
callback?: (error: Error | null, stream: Duplex) => void,
|
|
30
|
+
): Socket | undefined {
|
|
31
|
+
if (this.connected) {
|
|
32
|
+
// Returning no socket with an error also refuses queued reconnect attempts.
|
|
33
|
+
callback?.(new Error(FAILURE), this.socket!);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
this.connected = true;
|
|
37
|
+
this.socket = connect({ host: this.hostname, port: this.port, autoSelectFamily: false });
|
|
38
|
+
return this.socket;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkDeadline(deadlineAt: number): void {
|
|
43
|
+
if (!Number.isFinite(deadlineAt) || Date.now() >= deadlineAt) throw new Error(FAILURE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(response: IncomingMessage): Promise<Record<string, unknown>> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const chunks: Buffer[] = [];
|
|
49
|
+
let size = 0;
|
|
50
|
+
const fail = () => { reject(new Error(FAILURE)); response.destroy(); };
|
|
51
|
+
response.on("error", fail);
|
|
52
|
+
response.on("aborted", fail);
|
|
53
|
+
response.on("data", (chunk: Buffer) => {
|
|
54
|
+
size += chunk.length;
|
|
55
|
+
if (size > MAX_RESPONSE_BYTES) { fail(); return; }
|
|
56
|
+
chunks.push(chunk);
|
|
57
|
+
});
|
|
58
|
+
response.on("end", () => {
|
|
59
|
+
try {
|
|
60
|
+
const value: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
61
|
+
if (!response.complete || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
62
|
+
throw new Error(FAILURE);
|
|
63
|
+
}
|
|
64
|
+
resolve(value as Record<string, unknown>);
|
|
65
|
+
} catch { reject(new Error(FAILURE)); }
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type AttestedTarget = { target: LiveProxy & { pid: number }; secret: string; deadlineAt: number };
|
|
71
|
+
type JsonReply = { response: IncomingMessage; body: Record<string, unknown> };
|
|
72
|
+
|
|
73
|
+
class AttestedSession {
|
|
74
|
+
readonly agent: AttestedAgent;
|
|
75
|
+
readonly challenge = createLocalAttestationChallenge();
|
|
76
|
+
private readonly hostname: string;
|
|
77
|
+
private readonly timer: ReturnType<typeof setTimeout>;
|
|
78
|
+
private current?: ClientRequest;
|
|
79
|
+
|
|
80
|
+
constructor(private readonly options: AttestedTarget) {
|
|
81
|
+
const { target, secret, deadlineAt } = options;
|
|
82
|
+
checkDeadline(deadlineAt);
|
|
83
|
+
if (target.source !== "runtime" || target.role !== undefined || target.packageTreeFenced
|
|
84
|
+
|| !Number.isSafeInteger(target.pid) || target.pid <= 0
|
|
85
|
+
|| !Number.isInteger(target.port) || target.port < 1 || target.port > 65535
|
|
86
|
+
|| !isLocalAttestationSecret(secret)) throw new Error(FAILURE);
|
|
87
|
+
const host = probeHostname(target.hostname);
|
|
88
|
+
this.hostname = host.startsWith("[") ? host.slice(1, -1) : host;
|
|
89
|
+
this.agent = new AttestedAgent(this.hostname, target.port);
|
|
90
|
+
this.timer = setTimeout(() => {
|
|
91
|
+
this.current?.destroy(new Error(FAILURE));
|
|
92
|
+
this.agent.socket?.destroy(new Error(FAILURE));
|
|
93
|
+
}, Math.min(deadlineAt - Date.now(), 2_147_483_647));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
exchange(adminToken?: string): Promise<JsonReply> {
|
|
97
|
+
const stop = adminToken !== undefined;
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const req = request({
|
|
100
|
+
hostname: this.hostname, port: this.options.target.port, agent: this.agent,
|
|
101
|
+
method: stop ? "POST" : "GET", path: stop ? "/api/stop" : "/healthz",
|
|
102
|
+
maxHeaderSize: MAX_RESPONSE_BYTES,
|
|
103
|
+
headers: stop ? { "content-length": "0" } : { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: this.challenge },
|
|
104
|
+
});
|
|
105
|
+
this.current = req;
|
|
106
|
+
req.on("error", reject);
|
|
107
|
+
req.on("response", response => {
|
|
108
|
+
readJson(response).then(body => resolve({ response, body }), reject);
|
|
109
|
+
});
|
|
110
|
+
req.once("socket", (socket: Socket) => {
|
|
111
|
+
try {
|
|
112
|
+
checkDeadline(this.options.deadlineAt);
|
|
113
|
+
if (socket !== this.agent.socket || socket.destroyed || !socket.writable
|
|
114
|
+
|| (stop && (socket.connecting || socket.readableEnded || socket.writableEnded))) {
|
|
115
|
+
throw new Error(FAILURE);
|
|
116
|
+
}
|
|
117
|
+
// No end/flushHeaders/write, or credential header, before this socket check.
|
|
118
|
+
if (adminToken !== undefined) req.setHeader("X-OpenCodex-API-Key", adminToken);
|
|
119
|
+
req.end();
|
|
120
|
+
} catch { req.destroy(new Error(FAILURE)); }
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
validateHealth(health: JsonReply): void {
|
|
126
|
+
const { target, secret, deadlineAt } = this.options;
|
|
127
|
+
const proof = health.response.headers[LOCAL_ATTESTATION_PROOF_HEADER];
|
|
128
|
+
checkDeadline(deadlineAt);
|
|
129
|
+
if (health.response.statusCode !== 200 || !isOpencodexHealthz(health.body)
|
|
130
|
+
|| health.body.status !== "ok" || health.body.role !== undefined
|
|
131
|
+
|| health.body.pid !== target.pid || health.body.port !== target.port
|
|
132
|
+
|| !verifyLocalAttestationProof(secret, this.challenge, target.pid, target.port, typeof proof === "string" ? proof : null)) {
|
|
133
|
+
throw new Error(FAILURE);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
close(): void {
|
|
138
|
+
clearTimeout(this.timer);
|
|
139
|
+
this.current?.destroy();
|
|
140
|
+
this.agent.destroy();
|
|
141
|
+
this.agent.socket?.destroy();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Proof and credential delivery must remain on one established channel; never retry. */
|
|
146
|
+
export async function stopAttestedUpdateTarget(options: AttestedTarget & {
|
|
147
|
+
cliVersion: string;
|
|
148
|
+
beforeStop: () => void;
|
|
149
|
+
adminToken: string;
|
|
150
|
+
}): Promise<void> {
|
|
151
|
+
let session: AttestedSession | undefined;
|
|
152
|
+
try {
|
|
153
|
+
const { cliVersion, deadlineAt, beforeStop, adminToken } = options;
|
|
154
|
+
if (!adminToken) throw new Error(FAILURE);
|
|
155
|
+
session = new AttestedSession(options);
|
|
156
|
+
const health = await session.exchange();
|
|
157
|
+
session.validateHealth(health);
|
|
158
|
+
if (health.body.restartCapability !== SYSTEM_RESTART_CAPABILITY_VERSION
|
|
159
|
+
|| computeVersionSkew(cliVersion, typeof health.body.version === "string" ? health.body.version : undefined).relation !== "cli-newer"
|
|
160
|
+
|| /\bclose\b/i.test(health.response.headers.connection ?? "")) throw new Error(FAILURE);
|
|
161
|
+
checkDeadline(deadlineAt);
|
|
162
|
+
beforeStop();
|
|
163
|
+
// Synchronous candidate revalidation and POST enqueue: no intervening await.
|
|
164
|
+
const stopped = await session.exchange(adminToken);
|
|
165
|
+
checkDeadline(deadlineAt);
|
|
166
|
+
if (stopped.response.statusCode !== 200 || stopped.body.success !== true
|
|
167
|
+
|| stopped.body.sharedTeardown !== "performed") throw new Error(FAILURE);
|
|
168
|
+
} catch {
|
|
169
|
+
// Never expose parser/network/callback errors (which may contain credentials).
|
|
170
|
+
throw new Error(FAILURE);
|
|
171
|
+
} finally {
|
|
172
|
+
session?.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Observe only the caller's exact newly started runtime; ordinary liveness is not proof. */
|
|
177
|
+
export async function observeAttestedUpdateReplacement(options: AttestedTarget & {
|
|
178
|
+
version: string;
|
|
179
|
+
}): Promise<LiveProxy> {
|
|
180
|
+
let session: AttestedSession | undefined;
|
|
181
|
+
try {
|
|
182
|
+
session = new AttestedSession(options);
|
|
183
|
+
const health = await session.exchange();
|
|
184
|
+
session.validateHealth(health);
|
|
185
|
+
if (health.body.version !== options.version) throw new Error(FAILURE);
|
|
186
|
+
return { pid: options.target.pid, port: options.target.port, hostname: options.target.hostname,
|
|
187
|
+
source: "runtime", version: options.version };
|
|
188
|
+
} catch {
|
|
189
|
+
throw new Error("update_restart_replacement_observation_failed");
|
|
190
|
+
} finally {
|
|
191
|
+
session?.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { getRuntimePortPath, readRuntimePort, readProcessCommandLine, isOcxStartCommandLine, type RuntimePortState } from "../config/process-state";
|
|
5
|
+
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
6
|
+
import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
|
|
7
|
+
import { isProcessAlive } from "../lib/process-control";
|
|
8
|
+
import { parseStrictSemver } from "../lib/strict-semver";
|
|
9
|
+
import { startArgv } from "../lib/self-launch-argv";
|
|
10
|
+
import { diagnoseService } from "../service/diagnostics";
|
|
11
|
+
import { inspectGuardedManagerTarget } from "../service/guarded-manager-target";
|
|
12
|
+
import { acquireOwnershipMutationLease, unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
|
|
13
|
+
import { serviceStatePaths } from "../service/state";
|
|
14
|
+
import { findLiveProxy, probeEndpointLiveness, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
15
|
+
import { waitForPortAvailable } from "../server/ports";
|
|
16
|
+
import { UPDATE_RESTART_CHILD_ENV, type UpdateRestartChildMarker } from "./update-restart-child";
|
|
17
|
+
import { assertUpdateRestartConfiguration, assertUpdateRestartHome, readUpdateRestartHome, type UpdateRestartHome } from "./update-restart-home";
|
|
18
|
+
import { observeAttestedUpdateReplacement, stopAttestedUpdateTarget } from "./update-restart-transport";
|
|
19
|
+
import type { UpdateRestartCandidate } from "./update-restart-candidate";
|
|
20
|
+
import { INCOMPLETE_INSTALL_RECOVERY } from "./restart-failure";
|
|
21
|
+
import { computeVersionSkew } from "./version-skew";
|
|
22
|
+
|
|
23
|
+
export interface UpdateRestartChild { pid?: number; exitCode: number | null; signalCode: string | null }
|
|
24
|
+
export interface UpdateRestartIo {
|
|
25
|
+
now(): number;
|
|
26
|
+
acquire(): { release(): void };
|
|
27
|
+
home(): UpdateRestartHome;
|
|
28
|
+
checkHome(home: UpdateRestartHome): void;
|
|
29
|
+
runtime(): RuntimePortState | null;
|
|
30
|
+
standalone(target: UpdateRestartCandidate["target"]): boolean;
|
|
31
|
+
/** Whether the executable the replacement will run is complete (not the npm `bun` placeholder). */
|
|
32
|
+
runtimeReady(): boolean;
|
|
33
|
+
stop(candidate: UpdateRestartCandidate, deadlineAt: number, beforeStop: () => void): Promise<void>;
|
|
34
|
+
stopped(candidate: UpdateRestartCandidate, deadlineAt: number): Promise<boolean>;
|
|
35
|
+
start(marker: UpdateRestartChildMarker): UpdateRestartChild;
|
|
36
|
+
observe(deadlineAt: number, childPid: number): Promise<LiveProxy | null>;
|
|
37
|
+
wait(ms: number): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
const ELIGIBILITY_REASONS = ["windows", "unsupported_platform", "foreground", "service", "shared_or_client",
|
|
40
|
+
"package_tree_fenced", "unverifiable_ancestry", "target_changed", "configuration_changed"] as const;
|
|
41
|
+
export type UpdateRestartEligibilityReason = (typeof ELIGIBILITY_REASONS)[number];
|
|
42
|
+
|
|
43
|
+
/** Carries only a closed reason code across the eligibility/transport boundary. */
|
|
44
|
+
export class UpdateRestartEligibilityError extends Error {
|
|
45
|
+
constructor(readonly reason: UpdateRestartEligibilityReason) { super("Update restart eligibility refused"); }
|
|
46
|
+
}
|
|
47
|
+
export type UpdateRestartResult = { ok: true; live: LiveProxy }
|
|
48
|
+
| { ok: false; code: string; reason?: UpdateRestartEligibilityReason };
|
|
49
|
+
|
|
50
|
+
function sameRuntime(candidate: UpdateRestartCandidate, current: RuntimePortState | null): boolean {
|
|
51
|
+
const expected = candidate.runtime;
|
|
52
|
+
return !!current && current.pid === expected.pid && current.port === expected.port
|
|
53
|
+
&& current.hostname === expected.hostname && current.attestationSecret === expected.attestationSecret
|
|
54
|
+
&& current.siblingOfPort === undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A terminal update transaction: it never enters generic restart recovery. */
|
|
58
|
+
export async function runUpdateRestart(candidate: UpdateRestartCandidate, deadlineAt: number, io: UpdateRestartIo): Promise<UpdateRestartResult> {
|
|
59
|
+
let phase = "eligibility";
|
|
60
|
+
let lease: { release(): void } | undefined;
|
|
61
|
+
// Recorded here because the stop transport sanitizes anything thrown by beforeStop.
|
|
62
|
+
let runtimeRefused = false;
|
|
63
|
+
let eligibilityReason: UpdateRestartEligibilityReason | undefined;
|
|
64
|
+
try {
|
|
65
|
+
if (!parseStrictSemver(candidate.cliVersion) || candidate.cliVersion === "0.0.0"
|
|
66
|
+
|| computeVersionSkew(candidate.cliVersion, candidate.target.version).relation !== "cli-newer") {
|
|
67
|
+
throw new Error("version");
|
|
68
|
+
}
|
|
69
|
+
const withinDeadline = () => { if (io.now() >= deadlineAt) throw new Error("deadline"); };
|
|
70
|
+
withinDeadline();
|
|
71
|
+
lease = io.acquire();
|
|
72
|
+
const home = candidate.home;
|
|
73
|
+
if (JSON.stringify(io.home()) !== JSON.stringify(home)) throw new Error("home_changed");
|
|
74
|
+
const revalidate = () => {
|
|
75
|
+
withinDeadline();
|
|
76
|
+
io.checkHome(home);
|
|
77
|
+
const currentRuntime = io.runtime();
|
|
78
|
+
if (!sameRuntime(candidate, currentRuntime)) {
|
|
79
|
+
eligibilityReason = currentRuntime?.siblingOfPort !== undefined ? "shared_or_client" : "target_changed";
|
|
80
|
+
throw new Error("target");
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
if (!io.standalone(candidate.target)) throw new Error("target");
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof UpdateRestartEligibilityError && ELIGIBILITY_REASONS.includes(error.reason)) eligibilityReason = error.reason;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
if (!io.runtimeReady()) {
|
|
89
|
+
runtimeRefused = true;
|
|
90
|
+
throw new Error("runtime");
|
|
91
|
+
}
|
|
92
|
+
withinDeadline();
|
|
93
|
+
};
|
|
94
|
+
revalidate();
|
|
95
|
+
phase = "stop";
|
|
96
|
+
await io.stop(candidate, deadlineAt, revalidate);
|
|
97
|
+
withinDeadline();
|
|
98
|
+
phase = "settle";
|
|
99
|
+
if (!await io.stopped(candidate, deadlineAt)) throw new Error("stop_unconfirmed");
|
|
100
|
+
// Shutdown is confirmed from here on and nothing has launched yet.
|
|
101
|
+
phase = "prelaunch";
|
|
102
|
+
withinDeadline();
|
|
103
|
+
io.checkHome(home);
|
|
104
|
+
// An in-place npm install can still be swapping its Bun in; wait for it inside the deadline,
|
|
105
|
+
// then repeat the launch preconditions synchronously so the wait opens no gap before spawn.
|
|
106
|
+
phase = "runtime";
|
|
107
|
+
while (!io.runtimeReady()) {
|
|
108
|
+
withinDeadline();
|
|
109
|
+
await io.wait(Math.min(100, deadlineAt - io.now()));
|
|
110
|
+
}
|
|
111
|
+
phase = "prelaunch";
|
|
112
|
+
withinDeadline();
|
|
113
|
+
io.checkHome(home);
|
|
114
|
+
phase = "start";
|
|
115
|
+
const child = io.start({ home, version: candidate.cliVersion, port: candidate.target.port, hostname: candidate.runtime.hostname ?? "", deadlineAt });
|
|
116
|
+
// The child must acquire its own lease and validate this frozen handoff before preflight.
|
|
117
|
+
lease.release();
|
|
118
|
+
lease = undefined;
|
|
119
|
+
if (!Number.isSafeInteger(child.pid) || !child.pid || child.pid === candidate.target.pid) throw new Error("launch_unknown");
|
|
120
|
+
phase = "replacement";
|
|
121
|
+
while (io.now() < deadlineAt) {
|
|
122
|
+
if (child.exitCode !== null || child.signalCode !== null) throw new Error("child_exited");
|
|
123
|
+
io.checkHome(home);
|
|
124
|
+
const live = await io.observe(deadlineAt, child.pid);
|
|
125
|
+
withinDeadline();
|
|
126
|
+
if (live) {
|
|
127
|
+
io.checkHome(home);
|
|
128
|
+
if (live.source !== "runtime" || live.pid !== child.pid || live.port !== candidate.target.port
|
|
129
|
+
|| (live.hostname ?? "") !== (candidate.target.hostname ?? "") || live.role === "client"
|
|
130
|
+
|| live.packageTreeFenced || live.version !== candidate.cliVersion) throw new Error("replacement_mismatch");
|
|
131
|
+
return { ok: true, live };
|
|
132
|
+
}
|
|
133
|
+
await io.wait(Math.min(100, deadlineAt - io.now()));
|
|
134
|
+
}
|
|
135
|
+
throw new Error("deadline");
|
|
136
|
+
} catch {
|
|
137
|
+
return { ok: false, code: runtimeRefused ? "update_restart_runtime_incomplete" : `update_restart_${phase}_failed`,
|
|
138
|
+
...(eligibilityReason ? { reason: eligibilityReason } : {}) };
|
|
139
|
+
}
|
|
140
|
+
finally { lease?.release(); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Sanitized, actionable text for each terminal update-restart code; it claims only what that phase proved. */
|
|
144
|
+
export function describeUpdateRestartFailure(code: string, reason?: UpdateRestartEligibilityReason): string {
|
|
145
|
+
if (reason && (code === "update_restart_eligibility_failed" || code === "update_restart_stop_failed")) {
|
|
146
|
+
const action = (() => {
|
|
147
|
+
switch (reason) {
|
|
148
|
+
case "windows": return "Restart from a newer CLI is unavailable on Windows. Run `ocx status`, then restart through the owning service or desktop app.";
|
|
149
|
+
case "unsupported_platform": return "Restart from a newer CLI requires macOS or Linux. Run `ocx status`, then use the owning lifecycle manager.";
|
|
150
|
+
case "service": return "An installed or active service owns lifecycle control. Run `ocx status`, then use the owning installation's `ocx service restart`.";
|
|
151
|
+
case "foreground": return "The proxy is attached to a parent process. Run `ocx status`; if it runs in a terminal, restart it in that terminal, otherwise use its owning app or supervisor.";
|
|
152
|
+
case "shared_or_client": return "This is a shared or connected-client runtime. Run `ocx status` and restart through its owning Hub, service or desktop app.";
|
|
153
|
+
case "package_tree_fenced": return `The proxy's package files are unsettled. Wait for the install to finish. ${INCOMPLETE_INSTALL_RECOVERY}`;
|
|
154
|
+
case "unverifiable_ancestry": return "The proxy's process ancestry or manager could not be verified. Run `ocx status` to identify its owner before restarting.";
|
|
155
|
+
case "configuration_changed": return "The selected configuration or connection changed. Run `ocx status` to inspect the intended proxy before restarting.";
|
|
156
|
+
case "target_changed": return "The captured proxy identity changed. Run `ocx status` to inspect the intended proxy before restarting.";
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
return `${action} Nothing was stopped by this restart attempt.`;
|
|
160
|
+
}
|
|
161
|
+
switch (code) {
|
|
162
|
+
case "update_restart_runtime_incomplete":
|
|
163
|
+
return `This installation's Bun runtime is incomplete; nothing was stopped. Wait for the install to finish, then run \`ocx restart\` again. ${INCOMPLETE_INSTALL_RECOVERY}`;
|
|
164
|
+
case "update_restart_eligibility_failed":
|
|
165
|
+
return "The running proxy is not eligible for an update restart from this CLI (it is supervised, shared, or changed); nothing was stopped. Run `ocx status` to inspect the intended proxy.";
|
|
166
|
+
case "update_restart_stop_failed":
|
|
167
|
+
return "The guarded stop of the old proxy could not be confirmed; inspect `ocx status` before retrying.";
|
|
168
|
+
case "update_restart_settle_failed":
|
|
169
|
+
return "The old proxy was not confirmed stopped, so no second proxy was started; inspect `ocx status` before retrying.";
|
|
170
|
+
case "update_restart_prelaunch_failed":
|
|
171
|
+
return "The old proxy stopped, but its home, ownership or deadline changed before launch, so nothing was launched. Check `ocx status`; if no proxy is running, run `ocx start`.";
|
|
172
|
+
case "update_restart_runtime_failed":
|
|
173
|
+
return `The old proxy stopped, but this installation's Bun runtime did not finish installing in time, so nothing was launched. ${INCOMPLETE_INSTALL_RECOVERY} Run \`ocx status\`; if no proxy is running, run \`ocx start\` once the install completes.`;
|
|
174
|
+
case "update_restart_start_failed":
|
|
175
|
+
return "The old proxy stopped, but the new proxy's launch could not be confirmed. Check `ocx status`; if no proxy is running, run `ocx start`.";
|
|
176
|
+
case "update_restart_replacement_failed":
|
|
177
|
+
return "The new proxy did not verify as the expected replacement in time; inspect `ocx status` before retrying.";
|
|
178
|
+
default:
|
|
179
|
+
return "Update restart could not be confirmed; inspect `ocx status` before retrying.";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function standalone(target: UpdateRestartCandidate["target"]): boolean {
|
|
184
|
+
try { assertUpdateRestartConfiguration(target.hostname ?? ""); }
|
|
185
|
+
catch { throw new UpdateRestartEligibilityError("configuration_changed"); }
|
|
186
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
187
|
+
throw new UpdateRestartEligibilityError(process.platform === "win32" ? "windows" : "unsupported_platform");
|
|
188
|
+
}
|
|
189
|
+
const host = probeHostname(target.hostname).replace(/^\[|\]$/g, "");
|
|
190
|
+
if (target.role === "client") throw new UpdateRestartEligibilityError("shared_or_client");
|
|
191
|
+
if (target.packageTreeFenced) throw new UpdateRestartEligibilityError("package_tree_fenced");
|
|
192
|
+
if (!isIP(host) || target.source !== "runtime") throw new UpdateRestartEligibilityError("unverifiable_ancestry");
|
|
193
|
+
const command = readProcessCommandLine(target.pid);
|
|
194
|
+
if (!command || !isOcxStartCommandLine(command)) throw new UpdateRestartEligibilityError("unverifiable_ancestry");
|
|
195
|
+
if (diagnoseService().installed) throw new UpdateRestartEligibilityError("service");
|
|
196
|
+
const manager = inspectGuardedManagerTarget(target.pid, target.port);
|
|
197
|
+
if (manager.kind !== "absent") throw new UpdateRestartEligibilityError(manager.kind === "bound" ? "service" : "unverifiable_ancestry");
|
|
198
|
+
let parent: string;
|
|
199
|
+
try {
|
|
200
|
+
parent = execFileSync("ps", ["-o", "ppid=", "-p", String(target.pid)], {
|
|
201
|
+
encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"],
|
|
202
|
+
}).trim();
|
|
203
|
+
} catch { throw new UpdateRestartEligibilityError("unverifiable_ancestry"); }
|
|
204
|
+
if (!/^[1-9]\d*$/.test(parent)) throw new UpdateRestartEligibilityError("unverifiable_ancestry");
|
|
205
|
+
if (parent !== "1") throw new UpdateRestartEligibilityError("foreground");
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Production composition keeps stop/start side effects out of cli/index.ts. */
|
|
210
|
+
export function restartFromCurrentInstallation(candidate: UpdateRestartCandidate, deadlineAt: number, environment: NodeJS.ProcessEnv): Promise<UpdateRestartResult> {
|
|
211
|
+
const argv = startArgv(candidate.target.port);
|
|
212
|
+
const executable = process.execPath;
|
|
213
|
+
return runUpdateRestart(candidate, deadlineAt, {
|
|
214
|
+
now: Date.now,
|
|
215
|
+
acquire: () => acquireOwnershipMutationLease(serviceStatePaths(), { waitMs: Math.max(0, Math.min(2000, deadlineAt - Date.now())) }),
|
|
216
|
+
home: readUpdateRestartHome, checkHome: assertUpdateRestartHome, runtime: readRuntimePort, standalone,
|
|
217
|
+
runtimeReady: () => isRealBunBinary(executable),
|
|
218
|
+
stop: async (target, deadline, beforeStop) => {
|
|
219
|
+
const token = configuredAdminToken();
|
|
220
|
+
if (!token) throw new Error("update_restart_credential_unavailable");
|
|
221
|
+
await stopAttestedUpdateTarget({ target: target.target, secret: target.runtime.attestationSecret,
|
|
222
|
+
cliVersion: target.cliVersion, deadlineAt: deadline, beforeStop, adminToken: token });
|
|
223
|
+
},
|
|
224
|
+
stopped: async (target, deadline) => {
|
|
225
|
+
while (Date.now() < deadline && isProcessAlive(target.target.pid)) await Bun.sleep(Math.min(100, deadline - Date.now()));
|
|
226
|
+
if (Date.now() >= deadline || existsSync(getRuntimePortPath())) return false;
|
|
227
|
+
const endpoint = { port: target.target.port, hostname: target.target.hostname };
|
|
228
|
+
if (!await waitForPortAvailable(endpoint.port, probeHostname(endpoint.hostname),
|
|
229
|
+
{ timeoutMs: Math.max(0, Math.min(1000, deadline - Date.now())), intervalMs: 50 })) return false;
|
|
230
|
+
if (Date.now() >= deadline) return false;
|
|
231
|
+
return await probeEndpointLiveness(endpoint, { timeoutMs: Math.min(250, deadline - Date.now()) }) === "dead";
|
|
232
|
+
},
|
|
233
|
+
start: marker => {
|
|
234
|
+
const env = unprivilegedOwnershipMutationEnvironment(environment);
|
|
235
|
+
env.OPENCODEX_HOME = marker.home.config.path;
|
|
236
|
+
env.CODEX_HOME = marker.home.codex.path;
|
|
237
|
+
env[UPDATE_RESTART_CHILD_ENV] = JSON.stringify(marker);
|
|
238
|
+
const child = spawn(executable, argv, { detached: true, stdio: "ignore", windowsHide: true, env });
|
|
239
|
+
child.on("error", () => {}); // exit/health evidence reports failure; never retry a possibly launched child.
|
|
240
|
+
child.unref();
|
|
241
|
+
return child;
|
|
242
|
+
},
|
|
243
|
+
observe: async (deadline, childPid) => {
|
|
244
|
+
const live = await findLiveProxy({ deadlineAt: deadline, attempts: 1 });
|
|
245
|
+
if (!live) return null;
|
|
246
|
+
if (live.pid !== childPid) throw new Error("update_restart_replacement_changed");
|
|
247
|
+
const runtime = readRuntimePort(childPid);
|
|
248
|
+
if (!runtime?.attestationSecret || runtime.port !== candidate.target.port
|
|
249
|
+
|| runtime.hostname !== candidate.runtime.hostname || runtime.siblingOfPort !== undefined) {
|
|
250
|
+
throw new Error("update_restart_replacement_unattested");
|
|
251
|
+
}
|
|
252
|
+
const attested = await observeAttestedUpdateReplacement({ target: { ...live, pid: childPid },
|
|
253
|
+
secret: runtime.attestationSecret, version: candidate.cliVersion, deadlineAt: deadline });
|
|
254
|
+
const current = readRuntimePort(childPid);
|
|
255
|
+
if (JSON.stringify(current) !== JSON.stringify(runtime)) throw new Error("update_restart_replacement_changed");
|
|
256
|
+
return attested;
|
|
257
|
+
},
|
|
258
|
+
wait: ms => Bun.sleep(ms),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Model-row display search; server attribution and report totals stay unchanged. */
|
|
2
|
+
import { CliUsageError } from "./runtime-api";
|
|
3
|
+
|
|
4
|
+
export interface UsageModelView {
|
|
5
|
+
query: string;
|
|
6
|
+
matchedModelCount: number;
|
|
7
|
+
returnedModelCount: number;
|
|
8
|
+
limit: 100;
|
|
9
|
+
truncated: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SearchableModel {
|
|
13
|
+
model: string;
|
|
14
|
+
provider: string;
|
|
15
|
+
resolvedModel?: string;
|
|
16
|
+
requests: number;
|
|
17
|
+
totalTokens: number;
|
|
18
|
+
estimatedCostUsd?: number;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Unlike exact attribution flags, an explicit empty search is a valid view. */
|
|
23
|
+
export function takeUsageSearchOption(args: string[]): string | undefined {
|
|
24
|
+
const indices = args.flatMap((arg, index) => arg === "--search" || arg.startsWith("--search=") ? [index] : []);
|
|
25
|
+
if (indices.length > 1) throw new CliUsageError("--search was given more than once");
|
|
26
|
+
const index = indices[0];
|
|
27
|
+
if (index === undefined) return undefined;
|
|
28
|
+
const arg = args[index]!;
|
|
29
|
+
if (arg.startsWith("--search=")) {
|
|
30
|
+
args.splice(index, 1);
|
|
31
|
+
return arg.slice("--search=".length);
|
|
32
|
+
}
|
|
33
|
+
const value = args[index + 1];
|
|
34
|
+
if (value === undefined || value.startsWith("--")) throw new CliUsageError("--search requires a value; use --search= for an empty search");
|
|
35
|
+
args.splice(index, 2);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function selectUsageModelView<T extends { models?: unknown }>(report: T, rawQuery: string): Omit<T, "models"> & { models: SearchableModel[]; modelView: UsageModelView } {
|
|
40
|
+
if (!report || !Array.isArray(report.models)) throw new Error("Invalid usage model rows; retry after checking the runtime.");
|
|
41
|
+
const models = report.models.map((raw: unknown): SearchableModel => {
|
|
42
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid usage model row.");
|
|
43
|
+
const row = raw as Record<string, unknown>;
|
|
44
|
+
if (typeof row.model !== "string" || typeof row.provider !== "string"
|
|
45
|
+
|| (row.resolvedModel !== undefined && typeof row.resolvedModel !== "string")
|
|
46
|
+
|| ![row.requests, row.totalTokens].every(value => typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
47
|
+
|| (row.estimatedCostUsd !== undefined && (typeof row.estimatedCostUsd !== "number" || !Number.isFinite(row.estimatedCostUsd)))) {
|
|
48
|
+
throw new Error("Invalid usage model row.");
|
|
49
|
+
}
|
|
50
|
+
return row as SearchableModel;
|
|
51
|
+
});
|
|
52
|
+
const query = rawQuery.trim().toLowerCase();
|
|
53
|
+
const matching = models.toSorted((a, b) => b.totalTokens - a.totalTokens).filter(row => !query
|
|
54
|
+
|| row.model.toLowerCase().includes(query) || row.provider.toLowerCase().includes(query)
|
|
55
|
+
|| (row.resolvedModel ?? "").toLowerCase().includes(query));
|
|
56
|
+
const selected = matching.slice(0, 100);
|
|
57
|
+
return { ...report, models: selected, modelView: { query, matchedModelCount: matching.length, returnedModelCount: selected.length, limit: 100, truncated: matching.length > selected.length } };
|
|
58
|
+
}
|