@bitkyc08/opencodex 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
package/src/codex/shim.ts
CHANGED
|
@@ -1,306 +1,73 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
|
|
5
2
|
import {
|
|
6
3
|
chmodSync,
|
|
7
|
-
closeSync,
|
|
8
4
|
existsSync,
|
|
9
|
-
fstatSync,
|
|
10
5
|
lstatSync,
|
|
11
|
-
linkSync,
|
|
12
|
-
mkdirSync,
|
|
13
|
-
mkdtempSync,
|
|
14
|
-
openSync,
|
|
15
6
|
readFileSync,
|
|
16
|
-
readdirSync,
|
|
17
|
-
readlinkSync,
|
|
18
|
-
readSync,
|
|
19
7
|
renameSync,
|
|
20
|
-
rmSync,
|
|
21
|
-
rmdirSync,
|
|
22
|
-
statSync,
|
|
23
|
-
symlinkSync,
|
|
24
|
-
type Stats,
|
|
25
8
|
unlinkSync,
|
|
26
9
|
writeFileSync,
|
|
27
10
|
} from "node:fs";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
11
|
+
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
12
|
+
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
30
13
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
31
|
-
import { isProcessAlive } from "../lib/process-control";
|
|
32
14
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
33
|
-
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
34
|
-
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
35
15
|
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
36
16
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
17
|
+
import {
|
|
18
|
+
buildUnixCodexShim,
|
|
19
|
+
buildWindowsCodexShim,
|
|
20
|
+
buildWindowsPowerShellCodexShim,
|
|
21
|
+
gitBashPath,
|
|
22
|
+
SHIM_MARKER,
|
|
23
|
+
UNIX_SHIM_REVISION_MARKER,
|
|
24
|
+
} from "./shim-templates";
|
|
25
|
+
import {
|
|
26
|
+
hasUsableBackingPath,
|
|
27
|
+
isCurrentUnixShimProbe,
|
|
28
|
+
isHealthyShimProbe,
|
|
29
|
+
isVersionManagerOwnedCodexPath,
|
|
30
|
+
restoreWithoutReplacing,
|
|
31
|
+
sameFingerprint,
|
|
32
|
+
sameFingerprintAfterRename,
|
|
33
|
+
sameStableShimPathProbe,
|
|
34
|
+
shimPathFingerprint,
|
|
35
|
+
stableShimPathProbe,
|
|
36
|
+
type ShimPathFingerprint,
|
|
37
|
+
type StableShimPathProbe,
|
|
38
|
+
} from "./shim-fingerprint";
|
|
39
|
+
import {
|
|
40
|
+
fileErrorCode,
|
|
41
|
+
readState,
|
|
42
|
+
readStateResult,
|
|
43
|
+
stateFiles,
|
|
44
|
+
statePath,
|
|
45
|
+
writeState,
|
|
46
|
+
type ShimFileState,
|
|
47
|
+
type ShimState,
|
|
48
|
+
} from "./shim-state-file";
|
|
49
|
+
import {
|
|
50
|
+
CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS,
|
|
51
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
52
|
+
probeUnixShimFiles,
|
|
53
|
+
type UnixShimProbeResult,
|
|
54
|
+
} from "./shim-probe";
|
|
55
|
+
import { tryAcquireShimRestoreLock } from "./shim-restore-lock";
|
|
56
|
+
|
|
57
|
+
export { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim } from "./shim-templates";
|
|
58
|
+
export { isVersionManagerOwnedCodexPath } from "./shim-fingerprint";
|
|
59
|
+
export { CODEX_SHIM_STATE_MAX_BYTES } from "./shim-state-file";
|
|
60
|
+
export { setCodexShimProbeHookForTests, setCodexShimProbeShellForTests, setCodexShimProbeObservationMsForTests } from "./shim-probe";
|
|
61
|
+
export type { CodexShimBackingForCommand } from "./shim-inspect";
|
|
62
|
+
export { isLocalAbsoluteInspectionPath, inspectCodexShimBackingForCommand } from "./shim-inspect";
|
|
37
63
|
|
|
38
|
-
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
39
|
-
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
|
|
40
|
-
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
41
64
|
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
42
|
-
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
43
|
-
const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
|
|
44
|
-
const CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS = 5_000;
|
|
45
|
-
const CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS = 1_000;
|
|
46
|
-
const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
|
|
47
|
-
const CODEX_SHIM_REENTRY_DIAGNOSTIC = "opencodex: saved Codex launcher resolved back to the autostart shim; run ocx codex-shim uninstall and reinstall Codex before enabling codexAutoStart.";
|
|
48
|
-
const CODEX_SHIM_INSTALL_PROBE_SCRIPT = `
|
|
49
|
-
const { spawn } = require("node:child_process");
|
|
50
|
-
const { readFileSync, writeFileSync } = require("node:fs");
|
|
51
|
-
const [markerPath, reentryPath, groupPath, stderrPath, launcherShellPath, wrapperPath, timeoutRaw, stderrLimitRaw, stderrDrainRaw, observationRaw] = process.argv.slice(1);
|
|
52
|
-
const timeoutMs = Number.parseInt(timeoutRaw, 10);
|
|
53
|
-
const stderrLimit = Number.parseInt(stderrLimitRaw, 10);
|
|
54
|
-
const stderrDrainMs = Number.parseInt(stderrDrainRaw, 10);
|
|
55
|
-
const observationMs = Number.parseInt(observationRaw, 10);
|
|
56
|
-
const probeStartedAt = Date.now();
|
|
57
|
-
const stderrChunks = [];
|
|
58
|
-
let stderrBytes = 0;
|
|
59
|
-
let launcher;
|
|
60
|
-
let probeLease;
|
|
61
|
-
let timer;
|
|
62
|
-
let stderrDrainTimer;
|
|
63
|
-
let observationTimer;
|
|
64
|
-
let reentryPollTimer;
|
|
65
|
-
let marker = "";
|
|
66
|
-
let finished = false;
|
|
67
|
-
|
|
68
|
-
function writeExclusive(path, value) {
|
|
69
|
-
writeFileSync(path, value, { flag: "wx", mode: 0o600 });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function appendStderr(value) {
|
|
73
|
-
if (stderrBytes >= stderrLimit) return;
|
|
74
|
-
const bytes = Buffer.from(value);
|
|
75
|
-
const retained = bytes.subarray(0, stderrLimit - stderrBytes);
|
|
76
|
-
stderrChunks.push(retained);
|
|
77
|
-
stderrBytes += retained.byteLength;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function groupAlive() {
|
|
81
|
-
if (!launcher || !launcher.pid) return false;
|
|
82
|
-
try {
|
|
83
|
-
process.kill(-launcher.pid, 0);
|
|
84
|
-
return true;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
return error && error.code !== "ESRCH";
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function killGroup() {
|
|
91
|
-
if (!launcher || !launcher.pid) return;
|
|
92
|
-
try { process.kill(-launcher.pid, "SIGKILL"); } catch (error) {
|
|
93
|
-
if (!error || error.code !== "ESRCH") appendStderr(String(error));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function setMarker(value) {
|
|
98
|
-
if (marker) return;
|
|
99
|
-
marker = value;
|
|
100
|
-
try { writeExclusive(markerPath, value + "\\n"); } catch (error) { appendStderr(String(error)); }
|
|
101
|
-
}
|
|
102
65
|
|
|
103
|
-
function reentryDetected() {
|
|
104
|
-
try { return readFileSync(reentryPath, "utf8").trim() === "recursive"; } catch { return false; }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function checkReentry() {
|
|
108
|
-
if (finished || !reentryDetected()) return;
|
|
109
|
-
setMarker("recursive");
|
|
110
|
-
killGroup();
|
|
111
|
-
finish(126);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function finish(status) {
|
|
115
|
-
if (finished) return;
|
|
116
|
-
finished = true;
|
|
117
|
-
if (timer) clearTimeout(timer);
|
|
118
|
-
if (stderrDrainTimer) clearTimeout(stderrDrainTimer);
|
|
119
|
-
if (observationTimer) clearTimeout(observationTimer);
|
|
120
|
-
if (reentryPollTimer) clearInterval(reentryPollTimer);
|
|
121
|
-
if (!marker && reentryDetected()) setMarker("recursive");
|
|
122
|
-
if (!marker && groupAlive()) {
|
|
123
|
-
setMarker("descendants");
|
|
124
|
-
killGroup();
|
|
125
|
-
}
|
|
126
|
-
try { writeExclusive(stderrPath, Buffer.concat(stderrChunks)); } catch { /* parent fails closed */ }
|
|
127
|
-
process.exit(marker === "timeout" ? 124 : marker === "descendants" ? 125 : marker === "recursive" ? 126 : status);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function finishAfterStderr(status) {
|
|
131
|
-
if (finished) return;
|
|
132
|
-
if (timer) {
|
|
133
|
-
clearTimeout(timer);
|
|
134
|
-
timer = undefined;
|
|
135
|
-
}
|
|
136
|
-
if (!launcher || !launcher.stderr || !probeLease) {
|
|
137
|
-
finish(status);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
let stderrEnded = launcher.stderr.readableEnded;
|
|
141
|
-
let leaseEnded = probeLease.readableEnded;
|
|
142
|
-
let observationElapsed = false;
|
|
143
|
-
const finishWhenReady = () => {
|
|
144
|
-
if (stderrEnded && leaseEnded && observationElapsed) finish(status);
|
|
145
|
-
};
|
|
146
|
-
launcher.stderr.once("end", () => {
|
|
147
|
-
stderrEnded = true;
|
|
148
|
-
finishWhenReady();
|
|
149
|
-
});
|
|
150
|
-
probeLease.once("end", () => {
|
|
151
|
-
leaseEnded = true;
|
|
152
|
-
finishWhenReady();
|
|
153
|
-
});
|
|
154
|
-
stderrDrainTimer = setTimeout(() => {
|
|
155
|
-
stderrEnded = true;
|
|
156
|
-
if (!marker && groupAlive()) {
|
|
157
|
-
setMarker("descendants");
|
|
158
|
-
killGroup();
|
|
159
|
-
finish(125);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
finishWhenReady();
|
|
163
|
-
}, stderrDrainMs);
|
|
164
|
-
const remainingObservationMs = Math.max(0, observationMs - (Date.now() - probeStartedAt));
|
|
165
|
-
observationTimer = setTimeout(() => {
|
|
166
|
-
observationElapsed = true;
|
|
167
|
-
if (!leaseEnded) {
|
|
168
|
-
setMarker(groupAlive() ? "descendants" : "timeout");
|
|
169
|
-
killGroup();
|
|
170
|
-
finish(marker === "descendants" ? 125 : 124);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
finishWhenReady();
|
|
174
|
-
}, remainingObservationMs);
|
|
175
|
-
finishWhenReady();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
|
|
180
|
-
detached: true,
|
|
181
|
-
env: process.env,
|
|
182
|
-
stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
183
|
-
});
|
|
184
|
-
if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
|
|
185
|
-
probeLease = launcher.stdio[3];
|
|
186
|
-
if (!probeLease) throw new Error("Codex shim probe launcher has no descendant lease pipe");
|
|
187
|
-
writeExclusive(groupPath, String(launcher.pid) + "\\n");
|
|
188
|
-
launcher.stderr.on("data", appendStderr);
|
|
189
|
-
reentryPollTimer = setInterval(checkReentry, 10);
|
|
190
|
-
launcher.once("error", error => {
|
|
191
|
-
appendStderr(String(error));
|
|
192
|
-
finishAfterStderr(127);
|
|
193
|
-
});
|
|
194
|
-
launcher.once("exit", code => finishAfterStderr(Number.isInteger(code) ? code : 127));
|
|
195
|
-
timer = setTimeout(() => {
|
|
196
|
-
setMarker("timeout");
|
|
197
|
-
killGroup();
|
|
198
|
-
finish(124);
|
|
199
|
-
}, timeoutMs);
|
|
200
|
-
} catch (error) {
|
|
201
|
-
appendStderr(String(error));
|
|
202
|
-
killGroup();
|
|
203
|
-
finish(127);
|
|
204
|
-
}
|
|
205
|
-
`;
|
|
206
|
-
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
207
66
|
let lastShimDiscoveryError: string | null = null;
|
|
208
67
|
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
209
68
|
export function lastCodexDiscoveryError(): string | null {
|
|
210
69
|
return lastShimDiscoveryError;
|
|
211
70
|
}
|
|
212
|
-
const CODEX_INTERNAL_COMMANDS = [
|
|
213
|
-
"app-server",
|
|
214
|
-
"archive",
|
|
215
|
-
"apply",
|
|
216
|
-
"cloud",
|
|
217
|
-
"completion",
|
|
218
|
-
"debug",
|
|
219
|
-
"delete",
|
|
220
|
-
"doctor",
|
|
221
|
-
"exec-server",
|
|
222
|
-
"features",
|
|
223
|
-
"fork",
|
|
224
|
-
"help",
|
|
225
|
-
"login",
|
|
226
|
-
"logout",
|
|
227
|
-
"mcp",
|
|
228
|
-
"plugin",
|
|
229
|
-
"sandbox",
|
|
230
|
-
"unarchive",
|
|
231
|
-
"update",
|
|
232
|
-
];
|
|
233
|
-
|
|
234
|
-
// Codex accepts global options before a subcommand. The shim must skip the value belonging to
|
|
235
|
-
// these options before it decides which first positional token is the real subcommand. Keep this
|
|
236
|
-
// list aligned with `codex --help`; `--option=value` and attached short forms stay one token.
|
|
237
|
-
const CODEX_GLOBAL_OPTIONS_WITH_VALUE = [
|
|
238
|
-
"-c", "--config",
|
|
239
|
-
"--enable", "--disable",
|
|
240
|
-
"--remote", "--remote-auth-token-env",
|
|
241
|
-
"-i", "--image",
|
|
242
|
-
"-m", "--model",
|
|
243
|
-
"--local-provider",
|
|
244
|
-
"-p", "--profile",
|
|
245
|
-
"-s", "--sandbox",
|
|
246
|
-
"-C", "--cd",
|
|
247
|
-
"--add-dir",
|
|
248
|
-
"-a", "--ask-for-approval",
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
interface ShimState {
|
|
252
|
-
platform: NodeJS.Platform;
|
|
253
|
-
wrapperPath: string;
|
|
254
|
-
originalPath: string;
|
|
255
|
-
backupPath: string;
|
|
256
|
-
wrappers?: ShimFileState[];
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
interface ShimFileState {
|
|
260
|
-
wrapperPath: string;
|
|
261
|
-
originalPath: string;
|
|
262
|
-
backupPath: string;
|
|
263
|
-
realPath?: string;
|
|
264
|
-
preserveOnly?: boolean;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export type CodexShimBackingForCommand =
|
|
268
|
-
| Readonly<{ status: "not-tracked" }>
|
|
269
|
-
| Readonly<{
|
|
270
|
-
status: "matched";
|
|
271
|
-
selectedRole: "wrapper" | "backing";
|
|
272
|
-
backingPath: string;
|
|
273
|
-
backingKind: "backup" | "real";
|
|
274
|
-
}>
|
|
275
|
-
| Readonly<{
|
|
276
|
-
status: "unknown";
|
|
277
|
-
reason:
|
|
278
|
-
| "state_invalid"
|
|
279
|
-
| "platform_mismatch"
|
|
280
|
-
| "ambiguous_match"
|
|
281
|
-
| "preserve_only"
|
|
282
|
-
| "backing_missing"
|
|
283
|
-
| "backing_mismatch"
|
|
284
|
-
| "binding_unavailable"
|
|
285
|
-
| "wrapper_unhealthy"
|
|
286
|
-
| "version_manager_refused";
|
|
287
|
-
}>;
|
|
288
|
-
|
|
289
|
-
interface ShimPathFingerprint {
|
|
290
|
-
dev: number;
|
|
291
|
-
ino: number;
|
|
292
|
-
kind: "file" | "symlink";
|
|
293
|
-
mode: number;
|
|
294
|
-
size: number;
|
|
295
|
-
mtimeMs: number;
|
|
296
|
-
ctimeMs: number;
|
|
297
|
-
target?: Omit<ShimPathFingerprint, "target">;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
interface StableShimPathProbe {
|
|
301
|
-
fingerprint: ShimPathFingerprint;
|
|
302
|
-
prefix: string;
|
|
303
|
-
}
|
|
304
71
|
|
|
305
72
|
interface InstallCodexShimInternalOptions {
|
|
306
73
|
expectedReplacements?: ReadonlyMap<string, ShimPathFingerprint>;
|
|
@@ -348,166 +115,6 @@ function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
|
348
115
|
}
|
|
349
116
|
}
|
|
350
117
|
|
|
351
|
-
function readShimProbePrefix(path: string): string {
|
|
352
|
-
const fd = openSync(path, "r");
|
|
353
|
-
try {
|
|
354
|
-
const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
|
|
355
|
-
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
356
|
-
return buffer.toString("utf8", 0, bytesRead);
|
|
357
|
-
} finally {
|
|
358
|
-
closeSync(fd);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function statFingerprint(path: string, follow: boolean): Omit<ShimPathFingerprint, "target"> | null {
|
|
363
|
-
try {
|
|
364
|
-
const stat = follow ? statSync(path) : lstatSync(path);
|
|
365
|
-
if (follow ? !stat.isFile() : (!stat.isFile() && !stat.isSymbolicLink())) return null;
|
|
366
|
-
return {
|
|
367
|
-
dev: stat.dev,
|
|
368
|
-
ino: stat.ino,
|
|
369
|
-
kind: stat.isSymbolicLink() ? "symlink" : "file",
|
|
370
|
-
mode: stat.mode,
|
|
371
|
-
size: stat.size,
|
|
372
|
-
mtimeMs: stat.mtimeMs,
|
|
373
|
-
ctimeMs: stat.ctimeMs,
|
|
374
|
-
};
|
|
375
|
-
} catch {
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function sameFingerprint(
|
|
381
|
-
left: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
382
|
-
right: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
383
|
-
): boolean {
|
|
384
|
-
return left.dev === right.dev
|
|
385
|
-
&& left.ino === right.ino
|
|
386
|
-
&& left.kind === right.kind
|
|
387
|
-
&& left.mode === right.mode
|
|
388
|
-
&& left.size === right.size
|
|
389
|
-
&& left.mtimeMs === right.mtimeMs
|
|
390
|
-
&& left.ctimeMs === right.ctimeMs
|
|
391
|
-
&& (!("target" in left) || !("target" in right)
|
|
392
|
-
? true
|
|
393
|
-
: left.target === undefined && right.target === undefined
|
|
394
|
-
? true
|
|
395
|
-
: left.target !== undefined && right.target !== undefined
|
|
396
|
-
? sameFingerprint(left.target, right.target)
|
|
397
|
-
: false);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
|
|
401
|
-
// rename changes the outer directory entry ctime on macOS; every other field,
|
|
402
|
-
// including a symlink target fingerprint, must remain identical.
|
|
403
|
-
return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function stableShimPathProbe(path: string): StableShimPathProbe | null {
|
|
407
|
-
const before = statFingerprint(path, false);
|
|
408
|
-
if (!before) return null;
|
|
409
|
-
const targetBefore = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
410
|
-
if (before.kind === "symlink" && !targetBefore) return null;
|
|
411
|
-
let prefix: string;
|
|
412
|
-
try {
|
|
413
|
-
prefix = readShimProbePrefix(path);
|
|
414
|
-
} catch {
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
const targetAfter = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
418
|
-
const after = statFingerprint(path, false);
|
|
419
|
-
if (!after || !sameFingerprint(before, after)) return null;
|
|
420
|
-
if (before.kind === "symlink") {
|
|
421
|
-
if (!targetBefore || !targetAfter || !sameFingerprint(targetBefore, targetAfter)) return null;
|
|
422
|
-
}
|
|
423
|
-
const fingerprint: ShimPathFingerprint = {
|
|
424
|
-
...before,
|
|
425
|
-
...(targetBefore ? { target: targetBefore } : {}),
|
|
426
|
-
};
|
|
427
|
-
const contentSize = fingerprint.target?.size ?? fingerprint.size;
|
|
428
|
-
return contentSize > 0 ? { fingerprint, prefix } : null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean {
|
|
432
|
-
return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Identity of whatever sits at `path`, read from metadata alone.
|
|
437
|
-
*
|
|
438
|
-
* `stableShimPathProbe` answers a different question: it reads content to decide
|
|
439
|
-
* whether a launcher looks like a healthy shim, and it deliberately returns null
|
|
440
|
-
* for a zero-byte file. That makes it the wrong instrument for rollback
|
|
441
|
-
* bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
|
|
442
|
-
* install moves it aside before writing our wrapper; if the move is recorded
|
|
443
|
-
* without a fingerprint, rollback cannot prove the backup is still the file it
|
|
444
|
-
* set aside and refuses to restore it — the launcher stays lost (#1625).
|
|
445
|
-
*
|
|
446
|
-
* Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
|
|
447
|
-
* re-reads them to reject a path that changed under us, following a symlink to
|
|
448
|
-
* fingerprint its target as well.
|
|
449
|
-
*/
|
|
450
|
-
function shimPathFingerprint(path: string): ShimPathFingerprint | null {
|
|
451
|
-
const before = statFingerprint(path, false);
|
|
452
|
-
if (!before) return null;
|
|
453
|
-
if (before.kind !== "symlink") {
|
|
454
|
-
const after = statFingerprint(path, false);
|
|
455
|
-
return after && sameFingerprint(before, after) ? before : null;
|
|
456
|
-
}
|
|
457
|
-
const targetBefore = statFingerprint(path, true);
|
|
458
|
-
if (!targetBefore) return null;
|
|
459
|
-
const targetAfter = statFingerprint(path, true);
|
|
460
|
-
const after = statFingerprint(path, false);
|
|
461
|
-
if (!targetAfter || !after
|
|
462
|
-
|| !sameFingerprint(targetBefore, targetAfter)
|
|
463
|
-
|| !sameFingerprint(before, after)) return null;
|
|
464
|
-
return { ...before, target: targetBefore };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Move `from` onto `to` without ever replacing an existing entry.
|
|
469
|
-
*
|
|
470
|
-
* `renameSync` silently clobbers the destination on POSIX, which is wrong for a
|
|
471
|
-
* rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
|
|
472
|
-
* a concurrent installer can publish its own launcher at the original path in
|
|
473
|
-
* between, and the restore would delete it. `link` fails EEXIST instead, which
|
|
474
|
-
* is the no-replace primitive we need and needs no native helper.
|
|
475
|
-
*
|
|
476
|
-
* `link` follows a symlink to its target rather than preserving the link, so a
|
|
477
|
-
* symlink launcher is republished with `symlink`, which is also no-replace: it
|
|
478
|
-
* fails EEXIST on an occupied destination. Checking existence and then renaming
|
|
479
|
-
* would reintroduce exactly the race this function exists to close.
|
|
480
|
-
*/
|
|
481
|
-
function restoreWithoutReplacing(from: string, to: string): void {
|
|
482
|
-
const source = lstatSync(from);
|
|
483
|
-
if (source.isSymbolicLink()) {
|
|
484
|
-
symlinkSync(readlinkSync(from), to);
|
|
485
|
-
unlinkSync(from);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
linkSync(from, to);
|
|
489
|
-
unlinkSync(from);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
|
|
493
|
-
if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
|
|
494
|
-
const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
|
|
495
|
-
return platform === "win32" || (mode & 0o111) !== 0;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
|
|
499
|
-
return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function hasUsableBackingPath(file: ShimFileState): boolean {
|
|
503
|
-
return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
|
|
504
|
-
.some(path => {
|
|
505
|
-
if (!path) return false;
|
|
506
|
-
const fingerprint = statFingerprint(path, true);
|
|
507
|
-
return fingerprint !== null && fingerprint.size > 0;
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
|
|
511
118
|
/**
|
|
512
119
|
* A PATH entry that reaches Windows through WSL drive interop
|
|
513
120
|
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
@@ -625,33 +232,6 @@ function backupPathFor(path: string): string {
|
|
|
625
232
|
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
626
233
|
}
|
|
627
234
|
|
|
628
|
-
/**
|
|
629
|
-
* True when a Codex binary lives inside a version manager's install tree.
|
|
630
|
-
*
|
|
631
|
-
* These trees are rewritten in place on upgrade, which destroys both the shim
|
|
632
|
-
* and the sibling .opencodex-real backup it restores from (#2412). The tempting
|
|
633
|
-
* repair — adopt the newly installed binary as a fresh original — is wrong
|
|
634
|
-
* twice: it records a provenance that never happened, and the next upgrade wipes
|
|
635
|
-
* it again, so the repair silently un-repairs on the version manager's schedule.
|
|
636
|
-
*
|
|
637
|
-
* Scope is the three managers named in the report. nvm/fnm/npm-prefix are
|
|
638
|
-
* deliberately excluded: a false positive here refuses a restore that would
|
|
639
|
-
* otherwise be correct.
|
|
640
|
-
*/
|
|
641
|
-
export function isVersionManagerOwnedCodexPath(
|
|
642
|
-
path: string,
|
|
643
|
-
platform: NodeJS.Platform = process.platform,
|
|
644
|
-
): boolean {
|
|
645
|
-
const normalized = (platform === "win32"
|
|
646
|
-
? win32.normalize(path).replace(/\\/g, "/")
|
|
647
|
-
: posix.normalize(path)).toLowerCase();
|
|
648
|
-
return normalized.includes("/mise/installs/")
|
|
649
|
-
|| normalized.includes("/mise/shims/")
|
|
650
|
-
|| normalized.includes("/.asdf/installs/")
|
|
651
|
-
|| normalized.includes("/.asdf/shims/")
|
|
652
|
-
|| normalized.includes("/.volta/");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
235
|
/**
|
|
656
236
|
* Why auto-restore refused, in the operator's own terms. Auto-restore used to
|
|
657
237
|
* return a bare `{ status: "ineligible" }`, and the CLI warns only when a
|
|
@@ -671,159 +251,9 @@ function destroyedShimMessage(file: ShimFileState): string {
|
|
|
671
251
|
return `${base} This Codex binary is owned by a version manager (mise/asdf/volta), so opencodex will not wrap it as a new original — the next upgrade would overwrite the shim and its backup again. Route through Codex instead with 'ocx start', and use 'ocx service install' for autostart.`;
|
|
672
252
|
}
|
|
673
253
|
|
|
674
|
-
function shQuote(value: string): string {
|
|
675
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Provenance is required rather than defaulted: a default would let a caller pass an
|
|
679
|
-
// override binary and silently label it something else, which is precisely the
|
|
680
|
-
// path/marker disagreement this feature exists to prevent.
|
|
681
|
-
//
|
|
682
|
-
// The marker is scoped to the `ensure` invocation in every flavor below and is never
|
|
683
|
-
// exported into the shim's own environment. A shim wraps the real `codex`, so an
|
|
684
|
-
// exported marker would be inherited by Codex and everything it spawns — a shell that
|
|
685
|
-
// then ran a *different* Bun directly would carry a provenance describing a binary it
|
|
686
|
-
// is not executing.
|
|
687
|
-
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
|
|
688
|
-
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
689
|
-
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
690
|
-
return `#!/usr/bin/env sh
|
|
691
|
-
# ${SHIM_MARKER}
|
|
692
|
-
# ${UNIX_SHIM_REVISION_MARKER}
|
|
693
|
-
if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
|
|
694
|
-
if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
|
|
695
|
-
if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
|
|
696
|
-
(umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
|
|
697
|
-
fi
|
|
698
|
-
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
699
|
-
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
700
|
-
fi
|
|
701
|
-
OCX_SHIM_PROBE_ACTIVE=1
|
|
702
|
-
export OCX_SHIM_PROBE_ACTIVE
|
|
703
|
-
fi
|
|
704
|
-
if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
|
|
705
|
-
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
706
|
-
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
707
|
-
fi
|
|
708
|
-
case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
|
|
709
|
-
0)
|
|
710
|
-
OCX_SHIM_ACTIVE_DEPTH=1
|
|
711
|
-
;;
|
|
712
|
-
1)
|
|
713
|
-
OCX_SHIM_ACTIVE_DEPTH=2
|
|
714
|
-
;;
|
|
715
|
-
*)
|
|
716
|
-
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
717
|
-
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
718
|
-
;;
|
|
719
|
-
esac
|
|
720
|
-
# Dynamic launchers such as mise exec -- codex may resolve the command name
|
|
721
|
-
# back to this wrapper. An exec chain keeps the same PID. A legitimate nested
|
|
722
|
-
# Codex invocation may enter once with a new PID; repeated child-process
|
|
723
|
-
# redispatch reaches depth 2 and is rejected before it can form an infinite chain.
|
|
724
|
-
OCX_SHIM_ACTIVE_PID=$$
|
|
725
|
-
export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
|
|
726
|
-
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
727
|
-
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
728
|
-
export OPENCODEX_API_AUTH_TOKEN
|
|
729
|
-
fi
|
|
730
|
-
ocx_subcommand=""
|
|
731
|
-
ocx_skip_next=0
|
|
732
|
-
for ocx_arg in "$@"; do
|
|
733
|
-
if [ "$ocx_skip_next" -eq 1 ]; then
|
|
734
|
-
ocx_skip_next=0
|
|
735
|
-
continue
|
|
736
|
-
fi
|
|
737
|
-
case "$ocx_arg" in
|
|
738
|
-
--)
|
|
739
|
-
break
|
|
740
|
-
;;
|
|
741
|
-
${valueOptions})
|
|
742
|
-
ocx_skip_next=1
|
|
743
|
-
;;
|
|
744
|
-
--help|-h|--version|-V)
|
|
745
|
-
ocx_subcommand="$ocx_arg"
|
|
746
|
-
break
|
|
747
|
-
;;
|
|
748
|
-
-*)
|
|
749
|
-
;;
|
|
750
|
-
*)
|
|
751
|
-
ocx_subcommand="$ocx_arg"
|
|
752
|
-
break
|
|
753
|
-
;;
|
|
754
|
-
esac
|
|
755
|
-
done
|
|
756
|
-
case "$ocx_subcommand" in
|
|
757
|
-
${internalCommands}|--help|-h|--version|-V)
|
|
758
|
-
;;
|
|
759
|
-
*)
|
|
760
|
-
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
761
|
-
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
762
|
-
fi
|
|
763
|
-
;;
|
|
764
|
-
esac
|
|
765
|
-
exec ${shQuote(realCodexPath)} "$@"
|
|
766
|
-
`;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
type UnixShimProbeCleanupPhase = "marker" | "reentry" | "group" | "stderr" | "group-id" | "termination" | "spawn" | "exception";
|
|
770
|
-
interface UnixShimProbeCleanup {
|
|
771
|
-
kind: "cleanup";
|
|
772
|
-
phase: UnixShimProbeCleanupPhase;
|
|
773
|
-
code: string;
|
|
774
|
-
status: number | null;
|
|
775
|
-
signal: string;
|
|
776
|
-
}
|
|
777
|
-
type UnixShimProbeResult = UnixShimProbeCleanup | "descendants" | "failed" | "recursive" | "timeout" | null;
|
|
778
|
-
|
|
779
|
-
const SHIM_PROBE_ERROR_CODES = new Set([
|
|
780
|
-
"EACCES", "EAGAIN", "EBADF", "ECANCELED", "EINTR", "EIO", "EMFILE", "ENFILE",
|
|
781
|
-
"ENOENT", "ENOEXEC", "ENOMEM", "ENOSPC", "EPERM", "EPIPE", "ESRCH", "ETIMEDOUT", "ETXTBSY",
|
|
782
|
-
]);
|
|
783
|
-
const SHIM_PROBE_SIGNALS = new Set([
|
|
784
|
-
"SIGABRT", "SIGBUS", "SIGHUP", "SIGILL", "SIGINT", "SIGKILL", "SIGPIPE", "SIGQUIT",
|
|
785
|
-
"SIGSEGV", "SIGTERM", "SIGTRAP", "SIGXCPU", "SIGXFSZ",
|
|
786
|
-
]);
|
|
787
|
-
|
|
788
|
-
/** Diagnostics cross a CLI boundary: never stringify arbitrary errors or metadata. */
|
|
789
|
-
function shimProbeCleanup(
|
|
790
|
-
phase: UnixShimProbeCleanupPhase, error?: unknown, status?: unknown, signal?: unknown,
|
|
791
|
-
): UnixShimProbeCleanup {
|
|
792
|
-
let code = error === undefined ? "none" : "unknown";
|
|
793
|
-
if (error !== null && typeof error === "object") {
|
|
794
|
-
try {
|
|
795
|
-
const value = Object.getOwnPropertyDescriptor(error, "code")?.value;
|
|
796
|
-
if (typeof value === "string" && SHIM_PROBE_ERROR_CODES.has(value)) code = value;
|
|
797
|
-
} catch { /* hostile accessors/proxies cannot turn diagnostics into an exception */ }
|
|
798
|
-
}
|
|
799
|
-
return {
|
|
800
|
-
kind: "cleanup", phase, code,
|
|
801
|
-
status: typeof status === "number" && Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
|
|
802
|
-
signal: typeof signal === "string" && SHIM_PROBE_SIGNALS.has(signal) ? signal : "none",
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
let codexShimProbeHookForTests: (() => void) | null = null;
|
|
807
|
-
let codexShimProbeShellForTests: string | null = null;
|
|
808
254
|
let codexShimGuardedWriteHookForTests: (() => void) | null = null;
|
|
809
255
|
let codexShimFreshWriteHookForTests: (() => void) | null = null;
|
|
810
256
|
let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
|
|
811
|
-
let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
812
|
-
|
|
813
|
-
/** Narrow deterministic seam for transaction rollback tests. */
|
|
814
|
-
export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
|
|
815
|
-
codexShimProbeHookForTests = hook;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/** Selects a POSIX shell only for cross-shell probe regression tests. */
|
|
819
|
-
export function setCodexShimProbeShellForTests(path: string | null): void {
|
|
820
|
-
codexShimProbeShellForTests = path;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/** Shortens the successful-launcher observation window only for focused tests. */
|
|
824
|
-
export function setCodexShimProbeObservationMsForTests(value: number | null): void {
|
|
825
|
-
codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
826
|
-
}
|
|
827
257
|
|
|
828
258
|
/** Narrow deterministic seam for guarded partial-write rollback tests. */
|
|
829
259
|
export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
|
|
@@ -849,136 +279,6 @@ export function setCodexShimRollbackRestoreHookForTests(
|
|
|
849
279
|
codexShimRollbackRestoreHookForTests = hook;
|
|
850
280
|
}
|
|
851
281
|
|
|
852
|
-
function readProbeMetadata(path: string, maxBytes: number): string | null {
|
|
853
|
-
try {
|
|
854
|
-
if (!existsSync(path)) return "";
|
|
855
|
-
const stat = lstatSync(path);
|
|
856
|
-
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
857
|
-
return readFileSync(path, "utf8").trim();
|
|
858
|
-
} catch {
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
864
|
-
if (process.platform === "win32") return null;
|
|
865
|
-
const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
|
|
866
|
-
const markerPath = join(probeDir, "result");
|
|
867
|
-
const reentryPath = join(probeDir, "reentry");
|
|
868
|
-
const groupPath = join(probeDir, "group");
|
|
869
|
-
const stderrPath = join(probeDir, "stderr");
|
|
870
|
-
const env: NodeJS.ProcessEnv = {
|
|
871
|
-
...process.env,
|
|
872
|
-
OCX_SHIM_BYPASS: "1",
|
|
873
|
-
OCX_SHIM_PROBE: "1",
|
|
874
|
-
OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
|
|
875
|
-
};
|
|
876
|
-
delete env.OCX_SHIM_ACTIVE_PID;
|
|
877
|
-
delete env.OCX_SHIM_ACTIVE_DEPTH;
|
|
878
|
-
delete env.OCX_SHIM_PROBE_ACTIVE;
|
|
879
|
-
let groupId = 0;
|
|
880
|
-
let probeStatus: unknown;
|
|
881
|
-
let probeSignal: unknown;
|
|
882
|
-
try {
|
|
883
|
-
chmodSync(probeDir, 0o700);
|
|
884
|
-
const result = spawnSync(process.execPath, [
|
|
885
|
-
"-e",
|
|
886
|
-
CODEX_SHIM_INSTALL_PROBE_SCRIPT,
|
|
887
|
-
markerPath,
|
|
888
|
-
reentryPath,
|
|
889
|
-
groupPath,
|
|
890
|
-
stderrPath,
|
|
891
|
-
codexShimProbeShellForTests ?? "/bin/sh",
|
|
892
|
-
wrapperPath,
|
|
893
|
-
String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
|
|
894
|
-
String(MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
895
|
-
String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
|
|
896
|
-
String(codexShimProbeObservationMs),
|
|
897
|
-
], {
|
|
898
|
-
encoding: "utf8",
|
|
899
|
-
env,
|
|
900
|
-
timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
|
|
901
|
-
killSignal: "SIGKILL",
|
|
902
|
-
});
|
|
903
|
-
probeStatus = result.status;
|
|
904
|
-
probeSignal = result.signal;
|
|
905
|
-
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
|
906
|
-
const marker = readProbeMetadata(markerPath, 64);
|
|
907
|
-
const reentryMarker = readProbeMetadata(reentryPath, 64);
|
|
908
|
-
const groupText = readProbeMetadata(groupPath, 64);
|
|
909
|
-
const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
910
|
-
groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
|
|
911
|
-
if (marker === null) return shimProbeCleanup("marker", result.error, probeStatus, probeSignal);
|
|
912
|
-
if (reentryMarker === null) return shimProbeCleanup("reentry", result.error, probeStatus, probeSignal);
|
|
913
|
-
if (groupText === null) return shimProbeCleanup("group", result.error, probeStatus, probeSignal);
|
|
914
|
-
if (launcherStderr === null) return shimProbeCleanup("stderr", result.error, probeStatus, probeSignal);
|
|
915
|
-
if (!Number.isInteger(groupId) || groupId <= 0) return shimProbeCleanup("group-id", result.error, probeStatus, probeSignal);
|
|
916
|
-
const groupSurvived = unixProcessGroupAlive(groupId);
|
|
917
|
-
if (timedOut || marker || reentryMarker || groupSurvived) {
|
|
918
|
-
try {
|
|
919
|
-
terminateUnixProcessGroup(groupId);
|
|
920
|
-
} catch (error) {
|
|
921
|
-
return shimProbeCleanup("termination", error, probeStatus, probeSignal);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
if (result.error && !timedOut) return shimProbeCleanup("spawn", result.error, probeStatus, probeSignal);
|
|
925
|
-
if (timedOut || marker === "timeout") return "timeout";
|
|
926
|
-
if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
|
|
927
|
-
if (reentryMarker !== "") return shimProbeCleanup("reentry", undefined, probeStatus, probeSignal);
|
|
928
|
-
if (marker === "descendants") return "descendants";
|
|
929
|
-
if (groupSurvived) return "descendants";
|
|
930
|
-
if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
|
|
931
|
-
return "recursive";
|
|
932
|
-
}
|
|
933
|
-
if (result.status !== 0) return "failed";
|
|
934
|
-
return null;
|
|
935
|
-
} catch (error) {
|
|
936
|
-
if (Number.isInteger(groupId) && groupId > 0) {
|
|
937
|
-
try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
|
|
938
|
-
}
|
|
939
|
-
return shimProbeCleanup("exception", error, probeStatus, probeSignal);
|
|
940
|
-
} finally {
|
|
941
|
-
try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
|
|
946
|
-
if (process.platform === "win32") return null;
|
|
947
|
-
codexShimProbeHookForTests?.();
|
|
948
|
-
return files
|
|
949
|
-
.filter(file => !file.preserveOnly)
|
|
950
|
-
.map(file => probeUnixShimInstall(file.wrapperPath))
|
|
951
|
-
.find(result => result !== null) ?? null;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
function unixProcessGroupAlive(groupId: number): boolean {
|
|
955
|
-
try {
|
|
956
|
-
process.kill(-groupId, 0);
|
|
957
|
-
return true;
|
|
958
|
-
} catch (error) {
|
|
959
|
-
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function terminateUnixProcessGroup(groupId: number): void {
|
|
964
|
-
let permissionError: unknown;
|
|
965
|
-
try {
|
|
966
|
-
process.kill(-groupId, "SIGKILL");
|
|
967
|
-
} catch (error) {
|
|
968
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
969
|
-
if (code === "EPERM") permissionError = error;
|
|
970
|
-
else if (code !== "ESRCH") throw error;
|
|
971
|
-
}
|
|
972
|
-
// A concurrently exiting group can briefly reject a second signal. Only
|
|
973
|
-
// observed disappearance clears that uncertainty; never send another signal.
|
|
974
|
-
const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
|
|
975
|
-
while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
|
|
976
|
-
if (unixProcessGroupAlive(groupId)) {
|
|
977
|
-
if (permissionError) throw permissionError;
|
|
978
|
-
throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
282
|
interface FreshShimInstallJournalEntry {
|
|
983
283
|
target: ShimFileState;
|
|
984
284
|
movedOriginalFingerprint?: ShimPathFingerprint;
|
|
@@ -1047,374 +347,6 @@ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry
|
|
|
1047
347
|
if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
|
|
1048
348
|
}
|
|
1049
349
|
|
|
1050
|
-
function windowsBatchValue(value: string): string {
|
|
1051
|
-
return value
|
|
1052
|
-
.replace(/%/g, "%%")
|
|
1053
|
-
.replace(/\^/g, "^^")
|
|
1054
|
-
.replace(/"/g, "")
|
|
1055
|
-
.replace(/[\r\n]/g, "");
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function windowsBatchSet(name: string, value: string): string {
|
|
1059
|
-
// Paths are rewritten to %USERPROFILE%-style env indirection: cmd.exe parses .cmd
|
|
1060
|
-
// files in the OEM codepage, so a literal non-ASCII profile prefix (Korean/Chinese
|
|
1061
|
-
// usernames) written as UTF-8 turns to mojibake. The env token expands natively in
|
|
1062
|
-
// the right codepage at parse time; no `chcp` here — this shim runs in the USER's
|
|
1063
|
-
// console and must not leak a codepage change into it.
|
|
1064
|
-
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
1068
|
-
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
1069
|
-
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
1070
|
-
return `@echo off\r
|
|
1071
|
-
rem ${SHIM_MARKER}\r
|
|
1072
|
-
setlocal\r
|
|
1073
|
-
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
|
|
1074
|
-
${windowsBatchSet("OCX_BUN", bunPath)}\r
|
|
1075
|
-
${windowsBatchSet("OCX_CLI", cliPath)}\r
|
|
1076
|
-
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
|
|
1077
|
-
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
|
|
1078
|
-
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
1079
|
-
goto scan_codex_args\r
|
|
1080
|
-
:scan_codex_args\r
|
|
1081
|
-
if "%~1"=="" goto ensure_ocx\r
|
|
1082
|
-
if "%~1"=="--" goto ensure_ocx\r
|
|
1083
|
-
${valueOptionChecks}\r
|
|
1084
|
-
${internalCommandChecks}\r
|
|
1085
|
-
if /I "%~1"=="--help" goto run_codex\r
|
|
1086
|
-
if /I "%~1"=="-h" goto run_codex\r
|
|
1087
|
-
if /I "%~1"=="--version" goto run_codex\r
|
|
1088
|
-
if /I "%~1"=="-V" goto run_codex\r
|
|
1089
|
-
set "OCX_SCAN_ARG=%~1"\r
|
|
1090
|
-
if "%OCX_SCAN_ARG:~0,1%"=="-" goto shift_codex_arg\r
|
|
1091
|
-
goto ensure_ocx\r
|
|
1092
|
-
:skip_option_value\r
|
|
1093
|
-
shift\r
|
|
1094
|
-
if "%~1"=="" goto ensure_ocx\r
|
|
1095
|
-
:shift_codex_arg\r
|
|
1096
|
-
shift\r
|
|
1097
|
-
goto scan_codex_args\r
|
|
1098
|
-
:ensure_ocx\r
|
|
1099
|
-
setlocal\r
|
|
1100
|
-
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
1101
|
-
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
1102
|
-
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
1103
|
-
endlocal\r
|
|
1104
|
-
:run_codex\r
|
|
1105
|
-
"%OCX_REAL_CODEX%" %*\r
|
|
1106
|
-
`;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function psString(value: string): string {
|
|
1110
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
1114
|
-
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
1115
|
-
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
1116
|
-
const tokenFile = serviceApiTokenFilePath();
|
|
1117
|
-
return `#!/usr/bin/env pwsh
|
|
1118
|
-
# ${SHIM_MARKER}
|
|
1119
|
-
$hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN
|
|
1120
|
-
$priorApiAuthToken = $env:OPENCODEX_API_AUTH_TOKEN
|
|
1121
|
-
try {
|
|
1122
|
-
if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
|
|
1123
|
-
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
|
|
1124
|
-
}
|
|
1125
|
-
$internalCommands = @(${internalCommands})
|
|
1126
|
-
$valueOptions = @(${valueOptions})
|
|
1127
|
-
$subcommand = ""
|
|
1128
|
-
$skipNext = $false
|
|
1129
|
-
foreach ($argValue in $args) {
|
|
1130
|
-
$argText = [string]$argValue
|
|
1131
|
-
if ($skipNext) { $skipNext = $false; continue }
|
|
1132
|
-
if ($argText -eq "--") { break }
|
|
1133
|
-
if ($valueOptions -contains $argText) { $skipNext = $true; continue }
|
|
1134
|
-
if (@("--help", "-h", "--version", "-V") -contains $argText) { $subcommand = $argText; break }
|
|
1135
|
-
if ($argText.StartsWith("-")) { continue }
|
|
1136
|
-
$subcommand = $argText
|
|
1137
|
-
break
|
|
1138
|
-
}
|
|
1139
|
-
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
1140
|
-
if (-not $skipEnsure) {
|
|
1141
|
-
$priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
|
|
1142
|
-
$priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
|
|
1143
|
-
$env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
|
|
1144
|
-
$env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
|
|
1145
|
-
try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
|
|
1146
|
-
finally {
|
|
1147
|
-
if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
|
|
1148
|
-
else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
|
|
1149
|
-
if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
|
|
1150
|
-
else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
& ${psString(realCodexPath)} @args
|
|
1154
|
-
$codexExitCode = $LASTEXITCODE
|
|
1155
|
-
} finally {
|
|
1156
|
-
if ($hadApiAuthToken) { $env:OPENCODEX_API_AUTH_TOKEN = $priorApiAuthToken }
|
|
1157
|
-
else { Remove-Item Env:\\OPENCODEX_API_AUTH_TOKEN -ErrorAction SilentlyContinue }
|
|
1158
|
-
}
|
|
1159
|
-
exit $codexExitCode
|
|
1160
|
-
`;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
interface ShimStateReadResult {
|
|
1164
|
-
state: ShimState | null;
|
|
1165
|
-
present: boolean;
|
|
1166
|
-
warning?: string;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
function fileErrorCode(error: unknown): string | undefined {
|
|
1170
|
-
return error && typeof error === "object" && "code" in error
|
|
1171
|
-
? String((error as { code?: unknown }).code)
|
|
1172
|
-
: undefined;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
|
|
1176
|
-
let lexicalBefore: Stats;
|
|
1177
|
-
try {
|
|
1178
|
-
lexicalBefore = lstatSync(path);
|
|
1179
|
-
if (lexicalBefore.isSymbolicLink() || !lexicalBefore.isFile()) {
|
|
1180
|
-
return { warning: `Codex shim state is not a direct regular file at ${path}; auto-restore skipped.` };
|
|
1181
|
-
}
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
if (fileErrorCode(error) === "ENOENT") return null;
|
|
1184
|
-
return { warning: `Codex shim state could not be inspected at ${path}.` };
|
|
1185
|
-
}
|
|
1186
|
-
let fd: number;
|
|
1187
|
-
try {
|
|
1188
|
-
fd = openSync(path, "r");
|
|
1189
|
-
} catch (error) {
|
|
1190
|
-
if (fileErrorCode(error) === "ENOENT") return null;
|
|
1191
|
-
return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
|
|
1192
|
-
}
|
|
1193
|
-
try {
|
|
1194
|
-
const before = fstatSync(fd);
|
|
1195
|
-
if (!before.isFile()) return { warning: `Codex shim state is not a regular file at ${path}; auto-restore skipped.` };
|
|
1196
|
-
if (before.size > maxBytes) {
|
|
1197
|
-
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
1198
|
-
}
|
|
1199
|
-
const buffer = Buffer.allocUnsafe(before.size);
|
|
1200
|
-
let offset = 0;
|
|
1201
|
-
while (offset < buffer.length) {
|
|
1202
|
-
const bytesRead = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
1203
|
-
if (bytesRead === 0) return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1204
|
-
offset += bytesRead;
|
|
1205
|
-
}
|
|
1206
|
-
const extra = Buffer.allocUnsafe(1);
|
|
1207
|
-
if (readSync(fd, extra, 0, 1, offset) !== 0) {
|
|
1208
|
-
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
1209
|
-
}
|
|
1210
|
-
const after = fstatSync(fd);
|
|
1211
|
-
let lexicalAfter: Stats;
|
|
1212
|
-
try {
|
|
1213
|
-
lexicalAfter = lstatSync(path);
|
|
1214
|
-
} catch {
|
|
1215
|
-
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1216
|
-
}
|
|
1217
|
-
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
1218
|
-
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
1219
|
-
|| lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
|
|
1220
|
-
|| lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino) {
|
|
1221
|
-
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1222
|
-
}
|
|
1223
|
-
return { bytes: buffer, content: buffer.toString("utf8") };
|
|
1224
|
-
} finally {
|
|
1225
|
-
closeSync(fd);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
function readStateResult(path = statePath()): ShimStateReadResult {
|
|
1230
|
-
const bounded = readBoundedRegularFile(path, CODEX_SHIM_STATE_MAX_BYTES);
|
|
1231
|
-
if (!bounded) return { state: null, present: false };
|
|
1232
|
-
if ("warning" in bounded) return { state: null, present: true, warning: bounded.warning };
|
|
1233
|
-
try {
|
|
1234
|
-
const value = JSON.parse(bounded.content) as unknown;
|
|
1235
|
-
if (!value || typeof value !== "object") return { state: null, present: true };
|
|
1236
|
-
const state = value as Record<string, unknown>;
|
|
1237
|
-
if (typeof state.platform !== "string") return { state: null, present: true };
|
|
1238
|
-
const validFile = (item: unknown): item is ShimFileState => {
|
|
1239
|
-
if (!item || typeof item !== "object") return false;
|
|
1240
|
-
const file = item as Record<string, unknown>;
|
|
1241
|
-
return typeof file.wrapperPath === "string"
|
|
1242
|
-
&& typeof file.originalPath === "string"
|
|
1243
|
-
&& typeof file.backupPath === "string"
|
|
1244
|
-
&& (file.realPath === undefined || typeof file.realPath === "string")
|
|
1245
|
-
&& (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
|
|
1246
|
-
};
|
|
1247
|
-
if (state.wrappers !== undefined) {
|
|
1248
|
-
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null, present: true };
|
|
1249
|
-
} else if (!validFile(state)) {
|
|
1250
|
-
return { state: null, present: true };
|
|
1251
|
-
}
|
|
1252
|
-
return { state: state as unknown as ShimState, present: true };
|
|
1253
|
-
} catch {
|
|
1254
|
-
return { state: null, present: true };
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
function readState(): ShimState | null {
|
|
1259
|
-
return readStateResult().state;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
export function isLocalAbsoluteInspectionPath(path: string, platform: NodeJS.Platform): boolean {
|
|
1263
|
-
if (platform !== "win32") return posix.isAbsolute(path);
|
|
1264
|
-
const normalized = path.replace(/\//g, "\\");
|
|
1265
|
-
// UNC and device namespaces can initiate remote I/O while a nominally local
|
|
1266
|
-
// inspection is resolving user-controlled paths. Root-relative paths are
|
|
1267
|
-
// drive-context dependent, so require an explicit local drive as well.
|
|
1268
|
-
return win32.isAbsolute(path)
|
|
1269
|
-
&& /^[a-z]:\\/i.test(normalized)
|
|
1270
|
-
&& !normalized.startsWith("\\\\");
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function windowsShimInspectionIsDeferred(platform: NodeJS.Platform): boolean {
|
|
1274
|
-
return platform === "win32";
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
/** Resolve one selected command through already-recorded shim state, without repair. */
|
|
1278
|
-
export function inspectCodexShimBackingForCommand(
|
|
1279
|
-
selectedCommand: string,
|
|
1280
|
-
platform: NodeJS.Platform = process.platform,
|
|
1281
|
-
configDir: string = getConfigDir(),
|
|
1282
|
-
): CodexShimBackingForCommand {
|
|
1283
|
-
// Pathname prechecks cannot prevent a writable Windows ancestor from being
|
|
1284
|
-
// replaced with a remote reparse point before the later state/fingerprint
|
|
1285
|
-
// reads. Keep the exported read-only helper fail-closed until those reads are
|
|
1286
|
-
// performed through a handle-bound Windows provenance layer.
|
|
1287
|
-
if (windowsShimInspectionIsDeferred(platform)) {
|
|
1288
|
-
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
1289
|
-
}
|
|
1290
|
-
if (!isLocalAbsoluteInspectionPath(configDir, platform)) {
|
|
1291
|
-
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1292
|
-
}
|
|
1293
|
-
const stateFile = join(configDir, "codex-shim.json");
|
|
1294
|
-
try {
|
|
1295
|
-
const stateEntry = lstatSync(stateFile);
|
|
1296
|
-
if (stateEntry.isSymbolicLink()) {
|
|
1297
|
-
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1298
|
-
}
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
if (fileErrorCode(error) !== "ENOENT") {
|
|
1301
|
-
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
const result = readStateResult(stateFile);
|
|
1305
|
-
if (!result.state) {
|
|
1306
|
-
return result.present
|
|
1307
|
-
? Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const })
|
|
1308
|
-
: Object.freeze({ status: "not-tracked" as const });
|
|
1309
|
-
}
|
|
1310
|
-
const pathApi = platform === "win32" ? win32 : posix;
|
|
1311
|
-
const samePath = (left: string, right: string): boolean => {
|
|
1312
|
-
const normalizedLeft = pathApi.resolve(left);
|
|
1313
|
-
const normalizedRight = pathApi.resolve(right);
|
|
1314
|
-
return platform === "win32"
|
|
1315
|
-
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
1316
|
-
: normalizedLeft === normalizedRight;
|
|
1317
|
-
};
|
|
1318
|
-
const files = stateFiles(result.state);
|
|
1319
|
-
if (files.some(file => !file.wrapperPath || !file.originalPath || !file.backupPath
|
|
1320
|
-
|| ![file.wrapperPath, file.originalPath, file.backupPath, file.realPath]
|
|
1321
|
-
.filter((path): path is string => typeof path === "string")
|
|
1322
|
-
.every(path => isLocalAbsoluteInspectionPath(path, platform)))) {
|
|
1323
|
-
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1324
|
-
}
|
|
1325
|
-
const wrapperKeys = files.map(file => platform === "win32"
|
|
1326
|
-
? pathApi.resolve(file.wrapperPath).toLowerCase()
|
|
1327
|
-
: pathApi.resolve(file.wrapperPath));
|
|
1328
|
-
if (new Set(wrapperKeys).size !== wrapperKeys.length) {
|
|
1329
|
-
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1330
|
-
}
|
|
1331
|
-
const selectedFingerprint = shimPathFingerprint(selectedCommand);
|
|
1332
|
-
if (!selectedFingerprint) {
|
|
1333
|
-
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
1334
|
-
}
|
|
1335
|
-
const selectedIdentity = selectedFingerprint.target ?? selectedFingerprint;
|
|
1336
|
-
const sameEffectiveIdentity = (fingerprint: ShimPathFingerprint | null): boolean => {
|
|
1337
|
-
if (!fingerprint) return false;
|
|
1338
|
-
const identity = fingerprint.target ?? fingerprint;
|
|
1339
|
-
return identity.dev === selectedIdentity.dev && identity.ino === selectedIdentity.ino;
|
|
1340
|
-
};
|
|
1341
|
-
const matches = files.flatMap(file => {
|
|
1342
|
-
const backingPath = file.realPath ?? file.backupPath;
|
|
1343
|
-
const roles: Array<"wrapper" | "backing"> = [];
|
|
1344
|
-
if (samePath(file.wrapperPath, selectedCommand)
|
|
1345
|
-
|| sameEffectiveIdentity(shimPathFingerprint(file.wrapperPath))) {
|
|
1346
|
-
roles.push("wrapper");
|
|
1347
|
-
}
|
|
1348
|
-
if (samePath(backingPath, selectedCommand)
|
|
1349
|
-
|| sameEffectiveIdentity(shimPathFingerprint(backingPath))) {
|
|
1350
|
-
roles.push("backing");
|
|
1351
|
-
}
|
|
1352
|
-
return roles.map(selectedRole => ({ file, backingPath, selectedRole }));
|
|
1353
|
-
});
|
|
1354
|
-
if (matches.length === 0) return Object.freeze({ status: "not-tracked" as const });
|
|
1355
|
-
if (result.state.platform !== platform) {
|
|
1356
|
-
return Object.freeze({ status: "unknown" as const, reason: "platform_mismatch" as const });
|
|
1357
|
-
}
|
|
1358
|
-
if (matches.length !== 1) {
|
|
1359
|
-
return Object.freeze({ status: "unknown" as const, reason: "ambiguous_match" as const });
|
|
1360
|
-
}
|
|
1361
|
-
const { file, backingPath, selectedRole } = matches[0]!;
|
|
1362
|
-
if (file.preserveOnly === true) {
|
|
1363
|
-
return Object.freeze({ status: "unknown" as const, reason: "preserve_only" as const });
|
|
1364
|
-
}
|
|
1365
|
-
const backing = statFingerprint(backingPath, true);
|
|
1366
|
-
if (!backing || backing.size <= 0 || samePath(backingPath, file.wrapperPath)) {
|
|
1367
|
-
return Object.freeze({ status: "unknown" as const, reason: "backing_missing" as const });
|
|
1368
|
-
}
|
|
1369
|
-
const wrapperProbe = stableShimPathProbe(file.wrapperPath);
|
|
1370
|
-
if (!wrapperProbe || !isHealthyShimProbe(wrapperProbe, result.state.platform)) {
|
|
1371
|
-
return Object.freeze({
|
|
1372
|
-
status: "unknown" as const,
|
|
1373
|
-
reason: isVersionManagerOwnedCodexPath(file.wrapperPath)
|
|
1374
|
-
? "version_manager_refused" as const
|
|
1375
|
-
: "wrapper_unhealthy" as const,
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
const wrapperIdentity = wrapperProbe.fingerprint.target ?? wrapperProbe.fingerprint;
|
|
1379
|
-
if (backing.dev === wrapperIdentity.dev && backing.ino === wrapperIdentity.ino) {
|
|
1380
|
-
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
1381
|
-
}
|
|
1382
|
-
const wrapperExt = extname(file.wrapperPath).toLowerCase();
|
|
1383
|
-
const invokesBacking = platform !== "win32"
|
|
1384
|
-
? wrapperProbe.prefix.includes(`exec ${shQuote(backingPath)} "$@"`)
|
|
1385
|
-
: wrapperExt === ".cmd" || wrapperExt === ".bat"
|
|
1386
|
-
? wrapperProbe.prefix.includes(windowsBatchSet("OCX_REAL_CODEX", backingPath))
|
|
1387
|
-
&& wrapperProbe.prefix.includes('"%OCX_REAL_CODEX%" %*')
|
|
1388
|
-
: wrapperExt === ".ps1"
|
|
1389
|
-
? wrapperProbe.prefix.includes(`& ${psString(backingPath)} @args`)
|
|
1390
|
-
: wrapperProbe.prefix.includes(`exec ${shQuote(gitBashPath(backingPath))} "$@"`);
|
|
1391
|
-
if (!invokesBacking) {
|
|
1392
|
-
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
1393
|
-
}
|
|
1394
|
-
return Object.freeze({
|
|
1395
|
-
status: "matched" as const,
|
|
1396
|
-
selectedRole,
|
|
1397
|
-
backingPath,
|
|
1398
|
-
backingKind: file.realPath !== undefined ? "real" as const : "backup" as const,
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
function statePath(): string {
|
|
1403
|
-
return join(getConfigDir(), "codex-shim.json");
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
function writeState(state: ShimState): void {
|
|
1407
|
-
const path = statePath();
|
|
1408
|
-
recordOwnedConfigPath(getConfigDir(), path);
|
|
1409
|
-
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1410
|
-
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
/** Git-Bash accepts `C:/...` but not backslashed paths inside sh scripts. */
|
|
1414
|
-
function gitBashPath(path: string): string {
|
|
1415
|
-
return path.replace(/\\/g, "/");
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
350
|
/**
|
|
1419
351
|
* Write the wrapper and return the identity of the inode this call created, or
|
|
1420
352
|
* `undefined` where the platform still writes the destination in place.
|
|
@@ -1519,12 +451,6 @@ function wrapperInodeIsOurs(
|
|
|
1519
451
|
return written !== undefined;
|
|
1520
452
|
}
|
|
1521
453
|
|
|
1522
|
-
function stateFiles(state: ShimState): ShimFileState[] {
|
|
1523
|
-
return state.wrappers?.length
|
|
1524
|
-
? state.wrappers
|
|
1525
|
-
: [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
454
|
function primaryState(files: ShimFileState[]): ShimState {
|
|
1529
455
|
const first = files[0]!;
|
|
1530
456
|
return { platform: process.platform, ...first, wrappers: files };
|
|
@@ -1611,152 +537,6 @@ interface GuardedRefreshJournalEntry {
|
|
|
1611
537
|
|
|
1612
538
|
let guardedRefreshTransactionId = 0;
|
|
1613
539
|
|
|
1614
|
-
interface ShimRestoreLock {
|
|
1615
|
-
release(): void;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
interface ShimRestoreLockRecord {
|
|
1619
|
-
version: 1;
|
|
1620
|
-
token: string;
|
|
1621
|
-
pid: number;
|
|
1622
|
-
createdAt: number;
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
interface ShimRestoreLockSnapshot {
|
|
1626
|
-
record: ShimRestoreLockRecord;
|
|
1627
|
-
ownerPath: string;
|
|
1628
|
-
lockIdentity: Pick<Stats, "dev" | "ino">;
|
|
1629
|
-
fingerprint: ShimPathFingerprint;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
function restoreLockPath(): string {
|
|
1633
|
-
return join(getConfigDir(), "codex-shim.autorestore.lock");
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
function sameFileIdentity(left: Pick<Stats, "dev" | "ino">, right: Pick<Stats, "dev" | "ino">): boolean {
|
|
1637
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
function readShimRestoreLockSnapshot(path: string): ShimRestoreLockSnapshot | null {
|
|
1641
|
-
let lockIdentity: Stats;
|
|
1642
|
-
let entries: string[];
|
|
1643
|
-
try {
|
|
1644
|
-
lockIdentity = lstatSync(path);
|
|
1645
|
-
if (!lockIdentity.isDirectory()) return null;
|
|
1646
|
-
entries = readdirSync(path);
|
|
1647
|
-
} catch {
|
|
1648
|
-
return null;
|
|
1649
|
-
}
|
|
1650
|
-
if (entries.length !== 1 || !entries[0].endsWith(".json")) return null;
|
|
1651
|
-
const ownerPath = join(path, entries[0]);
|
|
1652
|
-
const probe = stableShimPathProbe(ownerPath);
|
|
1653
|
-
if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
|
|
1654
|
-
try {
|
|
1655
|
-
const value = JSON.parse(probe.prefix) as Partial<ShimRestoreLockRecord>;
|
|
1656
|
-
if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
|
|
1657
|
-
|| typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
|
|
1658
|
-
|| typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
|
|
1659
|
-
if (entries[0] !== `${value.token}.json`) return null;
|
|
1660
|
-
const currentLockIdentity = lstatSync(path);
|
|
1661
|
-
if (!currentLockIdentity.isDirectory() || !sameFileIdentity(lockIdentity, currentLockIdentity)) return null;
|
|
1662
|
-
return {
|
|
1663
|
-
record: value as ShimRestoreLockRecord,
|
|
1664
|
-
ownerPath,
|
|
1665
|
-
lockIdentity,
|
|
1666
|
-
fingerprint: probe.fingerprint,
|
|
1667
|
-
};
|
|
1668
|
-
} catch {
|
|
1669
|
-
return null;
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
function sameShimRestoreLock(left: ShimRestoreLockSnapshot, right: ShimRestoreLockSnapshot): boolean {
|
|
1674
|
-
return left.record.token === right.record.token
|
|
1675
|
-
&& sameFileIdentity(left.lockIdentity, right.lockIdentity)
|
|
1676
|
-
&& sameFingerprint(left.fingerprint, right.fingerprint);
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
function reclaimStaleRestoreLock(path: string, beforeDelete?: () => void): boolean {
|
|
1680
|
-
const observed = readShimRestoreLockSnapshot(path);
|
|
1681
|
-
if (!observed) return false;
|
|
1682
|
-
const createdAt = Math.max(observed.record.createdAt, observed.fingerprint.mtimeMs);
|
|
1683
|
-
if (Date.now() - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
|
|
1684
|
-
if (isProcessAlive(observed.record.pid)) return false;
|
|
1685
|
-
const current = readShimRestoreLockSnapshot(path);
|
|
1686
|
-
if (!current || !sameShimRestoreLock(observed, current)) return false;
|
|
1687
|
-
beforeDelete?.();
|
|
1688
|
-
try {
|
|
1689
|
-
// The token is part of the owner filename. Even if the lock directory is
|
|
1690
|
-
// replaced after the comparison, this unlink cannot target a successor's
|
|
1691
|
-
// differently named owner record.
|
|
1692
|
-
unlinkSync(observed.ownerPath);
|
|
1693
|
-
rmdirSync(path);
|
|
1694
|
-
return true;
|
|
1695
|
-
} catch {
|
|
1696
|
-
return false;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreLock | null {
|
|
1701
|
-
const dir = getConfigDir();
|
|
1702
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1703
|
-
const path = restoreLockPath();
|
|
1704
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1705
|
-
let fd: number | null = null;
|
|
1706
|
-
let identity: Stats | null = null;
|
|
1707
|
-
let createdDirectory = false;
|
|
1708
|
-
const record: ShimRestoreLockRecord = {
|
|
1709
|
-
version: 1,
|
|
1710
|
-
token: `${process.pid}-${Date.now()}-${randomUUID()}`,
|
|
1711
|
-
pid: process.pid,
|
|
1712
|
-
createdAt: Date.now(),
|
|
1713
|
-
};
|
|
1714
|
-
const ownerPath = join(path, `${record.token}.json`);
|
|
1715
|
-
try {
|
|
1716
|
-
mkdirSync(path, { mode: 0o700 });
|
|
1717
|
-
createdDirectory = true;
|
|
1718
|
-
fd = openSync(ownerPath, "wx", 0o600);
|
|
1719
|
-
identity = fstatSync(fd);
|
|
1720
|
-
writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf8");
|
|
1721
|
-
identity = fstatSync(fd);
|
|
1722
|
-
let released = false;
|
|
1723
|
-
return {
|
|
1724
|
-
release(): void {
|
|
1725
|
-
if (released) return;
|
|
1726
|
-
released = true;
|
|
1727
|
-
try { closeSync(fd!); } catch { /* stale recovery handles an uncertain lock */ }
|
|
1728
|
-
try {
|
|
1729
|
-
const current = readShimRestoreLockSnapshot(path);
|
|
1730
|
-
if (identity && current && current.record.token === record.token
|
|
1731
|
-
&& sameFileIdentity(identity, current.fingerprint)) {
|
|
1732
|
-
unlinkSync(ownerPath);
|
|
1733
|
-
rmdirSync(path);
|
|
1734
|
-
}
|
|
1735
|
-
} catch { /* stale recovery handles release failures */ }
|
|
1736
|
-
},
|
|
1737
|
-
};
|
|
1738
|
-
} catch (error) {
|
|
1739
|
-
if (fd !== null) {
|
|
1740
|
-
try { closeSync(fd); } catch { /* best-effort close before ownership cleanup */ }
|
|
1741
|
-
try {
|
|
1742
|
-
const current = readShimRestoreLockSnapshot(path);
|
|
1743
|
-
if (identity && current && current.record.token === record.token
|
|
1744
|
-
&& sameFileIdentity(identity, current.fingerprint)) {
|
|
1745
|
-
unlinkSync(ownerPath);
|
|
1746
|
-
rmdirSync(path);
|
|
1747
|
-
}
|
|
1748
|
-
} catch { /* leave an uncertain lock for stale recovery */ }
|
|
1749
|
-
} else if (createdDirectory) {
|
|
1750
|
-
try { rmdirSync(path); } catch { /* another owner exists or cleanup is uncertain */ }
|
|
1751
|
-
}
|
|
1752
|
-
if (fileErrorCode(error) !== "EEXIST") throw error;
|
|
1753
|
-
if (attempt === 0 && reclaimStaleRestoreLock(path, beforeStaleDelete)) continue;
|
|
1754
|
-
return null;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return null;
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
540
|
function planGuardedRefreshTransaction(
|
|
1761
541
|
files: readonly ShimFileState[],
|
|
1762
542
|
expectedReplacements: ReadonlyMap<string, ShimPathFingerprint>,
|