@bitkyc08/opencodex 2.77.0 → 2.78.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.
Files changed (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -0,0 +1,509 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync, closeSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync,
4
+ realpathSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, type Stats,
5
+ } from "node:fs";
6
+ import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { getConfigDir } from "../config";
8
+ import { durableBunRuntime } from "../lib/bun-runtime";
9
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
10
+ import { truncateRetainedUtf8 } from "../lib/admission";
11
+ import type { CodexShimDiagnostic } from "./shim-diagnostics";
12
+ import type { CodexShimAutoRestoreResult } from "./shim";
13
+ import { sameFingerprint, shimPathFingerprint, stableShimPathProbe } from "./shim-fingerprint";
14
+ import { migrateLegacyUnixShim } from "./shim-migration";
15
+ import { findFirstCodexOnPath, isExecutableCodexCandidate } from "./shim-path-resolution";
16
+ import { CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS, MAX_DIAGNOSTIC_VALUE_BYTES, probeUnixShimFiles } from "./shim-probe";
17
+ import { tryAcquireShimRestoreLock } from "./shim-restore-lock";
18
+ import {
19
+ decodeOverlayState, fileErrorCode, readBoundedRegularFile, readStateResult, statePath,
20
+ type OverlayIdentity, type OverlayState, type ShimState,
21
+ } from "./shim-state-file";
22
+ import { buildUnixCodexShim, SHIM_MARKER, UNIX_SHIM_REVISION_MARKER, shQuote } from "./shim-templates";
23
+
24
+ type Snapshot = { identity: OverlayIdentity; mode: number; content: string; uid?: number; gid?: number };
25
+ type Publication = { path: string; staged: string; created: Snapshot; prior: Snapshot | null; rollback: string };
26
+ type Journal = { version: 1; token: string; launcher: string; files: Publication[] };
27
+ type InstallResult = { installed: boolean; message: string; refused?: boolean; runnable?: boolean };
28
+ type RestoreOptions = {
29
+ enabled: () => boolean;
30
+ stabilitySleep?: (ms: number) => void;
31
+ afterRestoreLockAcquired?: () => void;
32
+ beforeStaleRestoreLockDelete?: () => void;
33
+ };
34
+ const JOURNAL_NAME = "codex-shim.transaction.json";
35
+ const ENV_HEADER = "# opencodex managed Codex PATH\n";
36
+ const PRIVATE_FILE_MAX_BYTES = 64 * 1024;
37
+
38
+ export function overlayPaths(configDir = getConfigDir()): { wrapper: string; env: string } {
39
+ return { wrapper: join(resolve(configDir), "bin", "codex"), env: join(resolve(configDir), "codex-shell-env.sh") };
40
+ }
41
+ export function overlayActivationHint(configDir = getConfigDir()): string {
42
+ return `For sh/bash/zsh, run . ${shQuote(overlayPaths(configDir).env)} and add that line after other PATH setup in your shell startup file. Absolute Codex paths and GUI launchers bypass this shim.`;
43
+ }
44
+ function sameId(left: OverlayIdentity | null | undefined, right: OverlayIdentity | null | undefined): boolean {
45
+ return !!left && !!right && left.dev === right.dev && left.ino === right.ino;
46
+ }
47
+ function lexical(path: string): Stats | null {
48
+ try { return lstatSync(path); }
49
+ catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
50
+ }
51
+ function snapshot(path: string): Snapshot | null {
52
+ const before = lexical(path);
53
+ if (!before) return null;
54
+ if (!before.isFile() || before.isSymbolicLink() || before.uid !== process.getuid?.() || (before.mode & 0o022)) {
55
+ throw new Error("Private Codex artifact is not an owned regular file");
56
+ }
57
+ const bounded = readBoundedRegularFile(path, PRIVATE_FILE_MAX_BYTES);
58
+ if (!bounded || "warning" in bounded) throw new Error("Private Codex artifact is unreadable or changed");
59
+ const after = lstatSync(path);
60
+ if (!sameId(before, after) || before.mode !== after.mode || before.uid !== after.uid
61
+ || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
62
+ || !sameId(bounded.stat, after) || bounded.stat.mode !== after.mode || bounded.stat.uid !== after.uid
63
+ || bounded.stat.size !== after.size || bounded.stat.mtimeMs !== after.mtimeMs || bounded.stat.ctimeMs !== after.ctimeMs) {
64
+ throw new Error("Private Codex artifact changed");
65
+ }
66
+ return { identity: { dev: after.dev, ino: after.ino }, mode: after.mode & 0o7777,
67
+ content: bounded.content, uid: after.uid, gid: after.gid };
68
+ }
69
+ function unchanged(path: string, expected: Snapshot | null): boolean {
70
+ const current = snapshot(path);
71
+ return expected ? !!current && sameId(current.identity, expected.identity)
72
+ && current.content === expected.content && current.mode === expected.mode
73
+ && (expected.uid === undefined || current.uid === expected.uid)
74
+ && (expected.gid === undefined || current.gid === expected.gid) : current === null;
75
+ }
76
+
77
+ /** Validate the physical configured home alias; managed bin may never be a symlink. */
78
+ function namespace(create: boolean): () => void {
79
+ const home = resolve(getConfigDir());
80
+ if (home.includes(":") || /[\x00-\x1f\x7f]/.test(home)) throw new Error("Private Codex home cannot contain colon or control characters");
81
+ if (!lexical(home)) {
82
+ if (!create) throw new Error("Private Codex home is missing");
83
+ mkdirSync(home, { recursive: true, mode: 0o700 });
84
+ }
85
+ const physical = realpathSync.native(home);
86
+ const root = statSync(home);
87
+ if (!root.isDirectory() || root.uid !== process.getuid?.() || (root.mode & 0o022)) {
88
+ throw new Error("Private Codex home must be owned by the current user and not group/world writable");
89
+ }
90
+ const bin = join(home, "bin");
91
+ if (!lexical(bin) && create) mkdirSync(bin, { mode: 0o700 });
92
+ const child = lexical(bin);
93
+ const trusted = (value: Stats): boolean => value.isDirectory()
94
+ && value.uid === process.getuid?.() && (value.mode & 0o022) === 0;
95
+ if (!trusted(root) || (child && (!trusted(child) || child.isSymbolicLink()))) {
96
+ throw new Error("Private Codex home/bin must be owned by the current user and not group/world writable");
97
+ }
98
+ return () => {
99
+ if (realpathSync.native(home) !== physical || !sameId(root, statSync(home)) || !trusted(statSync(home))) {
100
+ throw new Error("Private Codex home changed during operation");
101
+ }
102
+ const current = lexical(bin);
103
+ if (child ? !current || !sameId(child, current) || !trusted(current) || current.isSymbolicLink() : current !== null) {
104
+ throw new Error("Private Codex bin changed during operation");
105
+ }
106
+ };
107
+ }
108
+ function scriptFor(launcher: string): string {
109
+ const runtime = durableBunRuntime();
110
+ return buildUnixCodexShim(launcher, runtime.path, join(import.meta.dir, "..", "cli", "index.ts"), runtime.source);
111
+ }
112
+ function shellEnvironment(): string {
113
+ return [ENV_HEADER.trimEnd(), `ocx_shim_dir=${shQuote(dirname(overlayPaths().wrapper))}`,
114
+ 'ocx_shim_rest="${PATH-}"', 'ocx_shim_path="$ocx_shim_dir"', 'ocx_shim_more=1',
115
+ 'while [ "$ocx_shim_more" = 1 ]; do', ' case "$ocx_shim_rest" in',
116
+ ' *:*) ocx_shim_part="${ocx_shim_rest%%:*}"; ocx_shim_rest="${ocx_shim_rest#*:}" ;;',
117
+ ' *) ocx_shim_part="$ocx_shim_rest"; ocx_shim_more=0 ;;', ' esac',
118
+ ' if [ "$ocx_shim_part" != "$ocx_shim_dir" ]; then ocx_shim_path="$ocx_shim_path:$ocx_shim_part"; fi',
119
+ 'done', 'export PATH="$ocx_shim_path"',
120
+ 'unset ocx_shim_dir ocx_shim_rest ocx_shim_path ocx_shim_more ocx_shim_part', ""].join("\n");
121
+ }
122
+ function ownedWrapper(value: Snapshot, state: OverlayState): boolean {
123
+ return sameId(value.identity, state.wrapperIdentity)
124
+ && value.content.includes(SHIM_MARKER) && value.content.includes(`exec ${shQuote(state.launcherPath)} "$@"`);
125
+ }
126
+ function requireOverlayIdentities(state: OverlayState): void {
127
+ if (!state.wrapperIdentity || !state.envIdentity) {
128
+ throw new Error("Private Codex shim ownership identities are missing; preserving all files. Remove the private files manually, then reinstall with ocx codex-shim install.");
129
+ }
130
+ }
131
+ function ownedEnvironment(value: Snapshot, state: OverlayState): boolean {
132
+ return sameId(value.identity, state.envIdentity) && value.content === shellEnvironment();
133
+ }
134
+ function revalidateOverlay(state: OverlayState, saved: Snapshot | null, wrapper: Snapshot | null, env: Snapshot | null): void {
135
+ if (!unchanged(statePath(), saved) || !unchanged(state.wrapperPath, wrapper) || !unchanged(overlayPaths().env, env)) {
136
+ throw new Error("Private Codex installation changed during inspection; preserving artifacts");
137
+ }
138
+ }
139
+ /** Validate the whole installation before endorsing either execution or activation. */
140
+ function inspectOverlay(state: OverlayState): {
141
+ wrapper: Snapshot | null; environmentPresent: boolean; complete: boolean; revalidate: () => void;
142
+ } {
143
+ requireOverlayIdentities(state);
144
+ const saved = snapshot(statePath());
145
+ const recorded = saved ? decodeOverlayState(JSON.parse(saved.content), getConfigDir()) : null;
146
+ if (!saved || !recorded || recorded.platform !== state.platform || recorded.wrapperPath !== state.wrapperPath
147
+ || recorded.launcherPath !== state.launcherPath || recorded.transactionId !== state.transactionId
148
+ || !sameId(recorded.wrapperIdentity, state.wrapperIdentity) || !sameId(recorded.envIdentity, state.envIdentity)) {
149
+ throw new Error("Private Codex shim state changed; preserving artifacts");
150
+ }
151
+ const wrapper = snapshot(state.wrapperPath);
152
+ const env = snapshot(overlayPaths().env);
153
+ if (wrapper && !ownedWrapper(wrapper, state)) throw new Error("Private Codex launcher was replaced; preserving it");
154
+ if (env && !ownedEnvironment(env, state)) throw new Error("Codex shell environment file was replaced; preserving it");
155
+ const revalidate = () => revalidateOverlay(state, saved, wrapper, env);
156
+ revalidate();
157
+ return { wrapper, environmentPresent: !!env, complete: saved.mode === 0o600 && !!env && env.mode === 0o600, revalidate };
158
+ }
159
+ function nativeProbe(path: string) {
160
+ if (!isExecutableCodexCandidate(path)) return null;
161
+ const probe = stableShimPathProbe(path);
162
+ return probe && !probe.prefix.includes(SHIM_MARKER) ? probe : null;
163
+ }
164
+ function overlayRunnable(state: OverlayState, wrapper: Snapshot | null): boolean {
165
+ return state.platform === process.platform && !!wrapper && ownedWrapper(wrapper, state)
166
+ && wrapper.content.includes(UNIX_SHIM_REVISION_MARKER) && wrapper.content === scriptFor(state.launcherPath)
167
+ && isExecutableCodexCandidate(state.wrapperPath) && !!nativeProbe(state.launcherPath);
168
+ }
169
+ export function overlayDiagnostic(state: ShimState): CodexShimDiagnostic {
170
+ const overlay = decodeOverlayState(state, getConfigDir());
171
+ let runnable = false;
172
+ let active: boolean | null = false;
173
+ let integrityMessage = "";
174
+ let complete = false;
175
+ try {
176
+ const check = namespace(false);
177
+ const inspection = overlay ? inspectOverlay(overlay) : null;
178
+ const wrapper = inspection?.wrapper;
179
+ complete = !!inspection?.complete;
180
+ runnable = !!overlay && !!inspection?.environmentPresent && overlayRunnable(overlay, wrapper ?? null);
181
+ if (inspection && !inspection.complete) integrityMessage = "Private Codex state and shell environment must be present with mode 0600; run ocx codex-shim install to repair them.";
182
+ if (!complete) active = null;
183
+ if (complete && overlay && wrapper) {
184
+ try {
185
+ const selected = findFirstCodexOnPath();
186
+ if (selected) {
187
+ const identity = shimPathFingerprint(selected.path);
188
+ active = identity ? sameId(identity.target ?? identity, wrapper.identity) : null;
189
+ }
190
+ } catch { active = null; }
191
+ }
192
+ check();
193
+ inspection?.revalidate();
194
+ } catch (error) { active = null; runnable = false; integrityMessage = failure(error).message; }
195
+ return { installed: true, healthy: complete && runnable && active === true, runnable, active,
196
+ summary: `Codex PATH shim: ${complete && runnable ? "ready" : "unhealthy"} at ${state.wrapperPath}; launcher ${state.launcherPath}; PATH ${active === null ? "unverified" : active ? "active" : "inactive"}. ${complete && runnable ? overlayActivationHint() : integrityMessage || "Run ocx codex-shim install to repair the shim."}` };
197
+ }
198
+
199
+ /** Capture the created inode from its descriptor, never by trusting a later pathname read. */
200
+ function stage(path: string, content: string, mode: number): Snapshot {
201
+ const fd = openSync(path, "wx", 0o600);
202
+ const created = fstatSync(fd);
203
+ try {
204
+ writeFileSync(fd, content, "utf8");
205
+ if (!sameId(created, lexical(path))) throw new Error("Staged Codex file replaced");
206
+ chmodSync(path, mode);
207
+ const pinned = snapshot(path);
208
+ if (!pinned || !sameId(created, pinned.identity) || pinned.content !== content) throw new Error("Staged Codex file changed");
209
+ return pinned;
210
+ } catch (error) {
211
+ if (sameId(created, lexical(path))) unlinkSync(path);
212
+ throw error;
213
+ } finally { closeSync(fd); }
214
+ }
215
+
216
+ function removePinned(path: string, expected: Snapshot): boolean {
217
+ if (!lexical(path)) return true;
218
+ if (!unchanged(path, expected)) return false;
219
+ unlinkSync(path);
220
+ return true;
221
+ }
222
+ function journalPath(): string { return join(getConfigDir(), JOURNAL_NAME); }
223
+ function validSnapshot(value: unknown): value is Snapshot {
224
+ if (!value || typeof value !== "object") return false;
225
+ const item = value as Snapshot;
226
+ return !!item.identity && Number.isSafeInteger(item.identity.dev) && Number.isSafeInteger(item.identity.ino)
227
+ && Number.isInteger(item.mode) && item.mode >= 0 && item.mode <= 0o7777 && typeof item.content === "string"
228
+ // Older journals have no owner fields; snapshot() still requires current-user ownership.
229
+ && (item.uid === undefined || Number.isSafeInteger(item.uid) && item.uid >= 0)
230
+ && (item.gid === undefined || Number.isSafeInteger(item.gid) && item.gid >= 0);
231
+ }
232
+ function readJournal(): { journal: Journal; saved: Snapshot } | null {
233
+ const saved = snapshot(journalPath());
234
+ if (!saved) return null;
235
+ const value = JSON.parse(saved.content) as Journal;
236
+ if (value.version !== 1 || !/^[a-f0-9-]{36}$/.test(value.token) || typeof value.launcher !== "string"
237
+ || !Array.isArray(value.files) || value.files.length !== 3) throw new Error("Invalid Codex publication journal");
238
+ const targets = [overlayPaths().wrapper, overlayPaths().env, statePath()];
239
+ for (const [index, file] of value.files.entries()) {
240
+ if (file.path !== targets[index] || file.staged !== `${file.path}.${value.token}.stage`
241
+ || file.rollback !== `${file.path}.${value.token}.rollback` || !validSnapshot(file.created)
242
+ || (file.prior !== null && !validSnapshot(file.prior))) throw new Error("Invalid Codex publication paths");
243
+ }
244
+ const state = decodeOverlayState(JSON.parse(value.files[2]!.created.content), getConfigDir());
245
+ if (!state || state.transactionId !== value.token || state.launcherPath !== value.launcher
246
+ || !sameId(state.wrapperIdentity, value.files[0]!.created.identity)
247
+ || !sameId(state.envIdentity, value.files[1]!.created.identity)
248
+ || !ownedWrapper(value.files[0]!.created, state) || value.files[1]!.created.content !== shellEnvironment()) {
249
+ throw new Error("Invalid Codex publication binding");
250
+ }
251
+ return { journal: value, saved };
252
+ }
253
+ /** State is the commit marker; uncommitted private publications are rolled back by identity. */
254
+ function recoverJournal(check: () => void): void {
255
+ const pending = readJournal();
256
+ if (!pending) return;
257
+ const { journal, saved } = pending;
258
+ const state = readStateResult().state;
259
+ const committed = state?.transactionId === journal.token && unchanged(statePath(), journal.files[2]!.created);
260
+ let complete = true;
261
+ for (const file of [...journal.files].reverse()) {
262
+ check();
263
+ try {
264
+ let discardPrior = committed;
265
+ if (!committed && unchanged(file.path, file.created)) {
266
+ if (file.prior) {
267
+ if (!unchanged(file.rollback, file.prior)) throw new Error("Prior Codex artifact changed");
268
+ renameSync(file.rollback, file.path);
269
+ } else if (!removePinned(file.path, file.created)) throw new Error("Concurrent Codex publication");
270
+ discardPrior = true;
271
+ } else if (!committed && !lexical(file.path) && file.prior) {
272
+ if (!unchanged(file.rollback, file.prior)) throw new Error("Prior Codex artifact unavailable");
273
+ linkSync(file.rollback, file.path);
274
+ discardPrior = true;
275
+ } else if (!committed) {
276
+ discardPrior = unchanged(file.path, file.prior);
277
+ if (!discardPrior) complete = false;
278
+ }
279
+ if (!removePinned(file.staged, file.created)) complete = false;
280
+ if (file.prior && discardPrior && !removePinned(file.rollback, file.prior)) complete = false;
281
+ } catch { complete = false; } // Keep uncertain targets and prior generations for manual recovery.
282
+ }
283
+ check();
284
+ if (!complete || !removePinned(journalPath(), saved)) throw new Error("Concurrent Codex artifacts preserved; inspect the publication journal before retrying");
285
+ }
286
+
287
+ function publishOverlay(launcher: string, state: OverlayState | null, check: () => void, currentState: Snapshot | null): InstallResult {
288
+ const paths = overlayPaths();
289
+ if (state) requireOverlayIdentities(state);
290
+ if (!isAbsolute(launcher) || /[\x00-\x1f\x7f]/.test(launcher)) throw new Error("Unsupported native Codex launcher path");
291
+ const initialNative = nativeProbe(launcher);
292
+ if (!initialNative || resolve(launcher) === paths.wrapper) throw new Error("Native Codex launcher is unusable; repair it with its package manager");
293
+ const wrapper = snapshot(paths.wrapper);
294
+ const env = snapshot(paths.env);
295
+ if (wrapper && (!state || !ownedWrapper(wrapper, state))) throw new Error("Refusing an unowned private Codex launcher");
296
+ if (env && (!state || !ownedEnvironment(env, state))) {
297
+ throw new Error("Refusing an unowned Codex shell environment file");
298
+ }
299
+ if (!unchanged(statePath(), currentState)) throw new Error("Codex shim state changed before publication");
300
+ const script = scriptFor(launcher);
301
+ if (state && wrapper?.content === script && isExecutableCodexCandidate(paths.wrapper) && env?.content === shellEnvironment()
302
+ && env.mode === 0o600 && currentState?.mode === 0o600) {
303
+ check();
304
+ revalidateOverlay(state, currentState, wrapper, env);
305
+ return { installed: false, runnable: true, message: `Codex PATH shim already installed. ${overlayActivationHint()}` };
306
+ }
307
+ const token = randomUUID();
308
+ const files: Publication[] = [];
309
+ let persisted = false;
310
+ let journalSnapshot: Snapshot | null = null;
311
+ const add = (path: string, content: string, mode: number, prior: Snapshot | null): Publication => {
312
+ const staged = `${path}.${token}.stage`;
313
+ const file = { path, staged, created: stage(staged, content, mode), prior, rollback: `${path}.${token}.rollback` };
314
+ files.push(file);
315
+ return file;
316
+ };
317
+ try {
318
+ const wrapperFile = add(paths.wrapper, script, 0o755, wrapper);
319
+ const envFile = add(paths.env, shellEnvironment(), 0o600, env);
320
+ const nextState: OverlayState = { schema: 2, mode: "path-overlay", platform: process.platform,
321
+ wrapperPath: paths.wrapper, launcherPath: launcher, wrapperIdentity: wrapperFile.created.identity,
322
+ envIdentity: envFile.created.identity, transactionId: token };
323
+ if (!decodeOverlayState(nextState, getConfigDir())) throw new Error("Invalid staged Codex overlay state");
324
+ add(statePath(), JSON.stringify(nextState, null, 2) + "\n", 0o600, currentState);
325
+ const probeDir = join(dirname(paths.wrapper), `.probe-${token}`);
326
+ mkdirSync(probeDir, { mode: 0o700 });
327
+ const probeIdentity = lstatSync(probeDir);
328
+ const probeWrapper = join(probeDir, "codex");
329
+ let unsafe: ReturnType<typeof probeUnixShimFiles>;
330
+ try {
331
+ linkSync(wrapperFile.staged, probeWrapper);
332
+ unsafe = probeUnixShimFiles([{ wrapperPath: probeWrapper, originalPath: launcher, backupPath: launcher, realPath: launcher }],
333
+ { PATH: `${probeDir}:${process.env.PATH ?? ""}` });
334
+ } finally {
335
+ check();
336
+ if (sameId(probeIdentity, lexical(probeDir))) {
337
+ removePinned(probeWrapper, wrapperFile.created);
338
+ try { rmdirSync(probeDir); } catch { /* preserve uncertain probe contents */ }
339
+ }
340
+ }
341
+ if (unsafe) {
342
+ const reason = unsafe === "recursive" ? "the saved launcher resolved back to the generated shim"
343
+ : unsafe === "timeout" ? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
344
+ : unsafe === "descendants" ? "the saved launcher left background descendants running after --version"
345
+ : typeof unsafe === "object" ? `the saved launcher's probe process group could not be terminated cleanly [phase=${unsafe.phase}; code=${unsafe.code}; status=${unsafe.status ?? "none"}; signal=${unsafe.signal}]`
346
+ : "the saved launcher failed its --version probe";
347
+ throw new Error(`Refusing Codex PATH shim because ${reason}. Native launcher was preserved.`);
348
+ }
349
+ const nativeAfter = nativeProbe(launcher);
350
+ if (!nativeAfter || !sameFingerprint(initialNative.fingerprint, nativeAfter.fingerprint) || initialNative.prefix !== nativeAfter.prefix) {
351
+ throw new Error("Native launcher changed during validation; retry after the package manager finishes");
352
+ }
353
+ check();
354
+ for (const file of files) {
355
+ if (!unchanged(file.path, file.prior) || !unchanged(file.staged, file.created)) throw new Error("Private Codex inputs changed during validation");
356
+ recordOwnedConfigPath(getConfigDir(), file.path);
357
+ if (file.prior) {
358
+ linkSync(file.path, file.rollback);
359
+ if (!unchanged(file.rollback, file.prior)) throw new Error("Private Codex rollback artifact changed");
360
+ }
361
+ }
362
+ check();
363
+ journalSnapshot = stage(journalPath(), JSON.stringify({ version: 1, token, launcher, files } satisfies Journal) + "\n", 0o600);
364
+ persisted = true;
365
+ for (const file of files) {
366
+ const native = nativeProbe(launcher);
367
+ if (!native || !sameFingerprint(initialNative.fingerprint, native.fingerprint) || native.prefix !== initialNative.prefix) {
368
+ throw new Error("Native launcher changed before publication; retry after the package manager finishes");
369
+ }
370
+ check();
371
+ if (!unchanged(file.path, file.prior) || !unchanged(file.staged, file.created)) throw new Error("Private Codex publication changed");
372
+ if (file.prior) renameSync(file.staged, file.path);
373
+ else { linkSync(file.staged, file.path); removePinned(file.staged, file.created); }
374
+ }
375
+ recoverJournal(check);
376
+ return { installed: true, runnable: true, message: `Codex PATH shim installed at ${paths.wrapper}; native launcher ${launcher}. ${overlayActivationHint()}` };
377
+ } catch (error) {
378
+ if (persisted) {
379
+ try { recoverJournal(check); }
380
+ catch { throw new Error("Codex publication could not finish recovery; concurrent artifacts and the recovery journal were preserved"); }
381
+ }
382
+ throw error;
383
+ } finally {
384
+ if (!persisted) {
385
+ check();
386
+ for (const file of files) {
387
+ removePinned(file.staged, file.created);
388
+ if (file.prior) removePinned(file.rollback, file.prior);
389
+ }
390
+ if (journalSnapshot) removePinned(journalPath(), journalSnapshot);
391
+ }
392
+ }
393
+ }
394
+
395
+ function failure(error: unknown): InstallResult {
396
+ const message = error instanceof Error ? error.message : "Codex overlay operation failed";
397
+ return { installed: false, refused: true, message: truncateRetainedUtf8(message.replace(/[\r\n]/g, " "), MAX_DIAGNOSTIC_VALUE_BYTES) };
398
+ }
399
+ function installLocked(discover: () => string | null, automatic: boolean, check: () => void): InstallResult {
400
+ recoverJournal(check);
401
+ const pinnedState = snapshot(statePath());
402
+ const result = readStateResult();
403
+ const state = result.state;
404
+ if (!unchanged(statePath(), pinnedState)) throw new Error("Codex shim state changed during inspection");
405
+ if (result.present && !state) throw new Error(result.warning ?? "Invalid Codex shim state; refusing installation");
406
+ if (state && state.platform !== process.platform) throw new Error("Codex shim platform mismatch");
407
+ let launcher: string | null;
408
+ let overlay: OverlayState | null = null;
409
+ if (state?.mode === "path-overlay") {
410
+ overlay = decodeOverlayState(state, getConfigDir());
411
+ if (!overlay) throw new Error("Invalid Codex overlay state");
412
+ requireOverlayIdentities(overlay);
413
+ launcher = overlay.launcherPath;
414
+ } else if (state) {
415
+ if (automatic) throw new Error("Legacy Unix shim requires explicit migration: run ocx codex-shim install");
416
+ const migration = migrateLegacyUnixShim(state);
417
+ if (migration.status !== "native-restored") throw new Error(migration.message);
418
+ launcher = migration.launcherPath;
419
+ } else {
420
+ const discovered = automatic ? null : discover();
421
+ launcher = discovered ? resolve(discovered) : null;
422
+ }
423
+ if (!launcher) throw new Error("Native Codex launcher not found; install or repair Codex with its package manager");
424
+ check();
425
+ return publishOverlay(launcher, overlay, check, pinnedState);
426
+ }
427
+ export function installUnixOverlay(discover: () => string | null): InstallResult {
428
+ let lock: ReturnType<typeof tryAcquireShimRestoreLock> = null;
429
+ try {
430
+ const check = namespace(true);
431
+ lock = tryAcquireShimRestoreLock();
432
+ if (!lock) throw new Error("Codex shim operation is already in progress; retry later");
433
+ check();
434
+ return installLocked(discover, false, check);
435
+ } catch (error) { return failure(error); }
436
+ finally { lock?.release(); }
437
+ }
438
+ export function autoRestoreUnixOverlay(options: RestoreOptions): CodexShimAutoRestoreResult {
439
+ const result = readStateResult();
440
+ if (!result.state) return { status: result.present ? "ineligible" : "not-installed", ...(result.warning ? { message: result.warning } : {}) };
441
+ const state = decodeOverlayState(result.state, getConfigDir());
442
+ if (!state || state.platform !== process.platform) return { status: "ineligible", message: "Legacy Unix shim requires explicit migration: run ocx codex-shim install" };
443
+ try {
444
+ const check = namespace(false);
445
+ const inspection = inspectOverlay(state);
446
+ const runnable = overlayRunnable(state, inspection.wrapper);
447
+ check();
448
+ const pending = lexical(journalPath());
449
+ inspection.revalidate();
450
+ if (inspection.complete && runnable && !pending) return { status: "healthy" };
451
+ } catch (error) { return { status: "ineligible", message: failure(error).message }; }
452
+ if (!nativeProbe(state.launcherPath)) return { status: "ineligible", message: "Recorded native Codex launcher is unusable; repair it with its package manager" };
453
+ if (!options.enabled()) return { status: "disabled" };
454
+ let lock: ReturnType<typeof tryAcquireShimRestoreLock> = null;
455
+ try {
456
+ const check = namespace(true);
457
+ lock = tryAcquireShimRestoreLock(options.beforeStaleRestoreLockDelete);
458
+ if (!lock) return { status: "deferred" };
459
+ options.afterRestoreLockAcquired?.();
460
+ check();
461
+ const native = nativeProbe(state.launcherPath);
462
+ (options.stabilitySleep ?? Bun.sleepSync)(100);
463
+ const observed = nativeProbe(state.launcherPath);
464
+ if (!native || !observed || !sameFingerprint(native.fingerprint, observed.fingerprint) || native.prefix !== observed.prefix) {
465
+ return { status: "deferred", message: "Native launcher changed during observation; retry after the package manager finishes" };
466
+ }
467
+ const installed = installLocked(() => null, true, check);
468
+ return installed.installed ? { status: "restored", message: installed.message } : { status: "healthy" };
469
+ } catch (error) { return { status: "deferred", message: failure(error).message }; }
470
+ finally { lock?.release(); }
471
+ }
472
+ export function uninstallUnixCodexShim(): { removed: boolean; message: string } {
473
+ let lock: ReturnType<typeof tryAcquireShimRestoreLock> = null;
474
+ try {
475
+ const check = namespace(false);
476
+ lock = tryAcquireShimRestoreLock();
477
+ if (!lock) throw new Error("Codex shim operation is already in progress");
478
+ check();
479
+ recoverJournal(check);
480
+ const stateFile = snapshot(statePath());
481
+ const result = readStateResult();
482
+ const state = result.state;
483
+ if (!unchanged(statePath(), stateFile)) throw new Error("Codex shim state changed during removal");
484
+ if (!state) return { removed: false, message: result.present ? "Invalid Codex shim state; preserving artifacts" : "Codex autostart shim is not installed." };
485
+ if (!stateFile) throw new Error("Codex state changed during removal");
486
+ if (state.mode !== "path-overlay") {
487
+ const migration = migrateLegacyUnixShim(state);
488
+ if (migration.status !== "native-restored") return { removed: false, message: migration.message };
489
+ } else {
490
+ const overlay = decodeOverlayState(state, getConfigDir());
491
+ if (!overlay || overlay.platform !== process.platform) throw new Error("Invalid private Codex shim state");
492
+ requireOverlayIdentities(overlay);
493
+ const wrapper = snapshot(overlay.wrapperPath);
494
+ const env = snapshot(overlayPaths().env);
495
+ if (wrapper && !ownedWrapper(wrapper, overlay)) throw new Error("Private Codex launcher is no longer owned; preserving it and its state");
496
+ if (env && !ownedEnvironment(env, overlay)) {
497
+ throw new Error("Codex shell environment file is no longer owned; preserving it and its state");
498
+ }
499
+ for (const [path, pinned] of [[overlay.wrapperPath, wrapper], [overlayPaths().env, env]] as const) {
500
+ check();
501
+ if (pinned && !removePinned(path, pinned)) throw new Error("Concurrent private Codex replacement preserved");
502
+ }
503
+ }
504
+ check();
505
+ if (!removePinned(statePath(), stateFile)) throw new Error("Concurrent Codex state replacement preserved");
506
+ return { removed: true, message: "Codex PATH shim removed. Native Codex launcher was preserved. Remove the codex-shell-env source line from your shell startup file." };
507
+ } catch (error) { return { removed: false, message: failure(error).message }; }
508
+ finally { lock?.release(); }
509
+ }
@@ -1,4 +1,4 @@
1
- import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
1
+ import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
2
  import { posix, win32 } from "node:path";
3
3
  import { isWslRuntime, wslAutomountRoot } from "./home";
4
4
  import { SHIM_MARKER } from "./shim-templates";
@@ -24,6 +24,7 @@ export type CodexPathScanDeps = {
24
24
  isShimFile?: (path: string) => boolean;
25
25
  isDirectory?: (path: string) => boolean;
26
26
  realpath?: (path: string) => string;
27
+ executableEligible?: (path: string) => boolean;
27
28
  };
28
29
 
29
30
  export type CodexPathCandidate = {
@@ -41,9 +42,20 @@ export function realIsDirectory(path: string): boolean {
41
42
 
42
43
  const SHIM_HEADER_MAX_BYTES = 16 * 1024;
43
44
 
44
- function inspectShimFile(path: string): boolean | null {
45
+ /** Shared POSIX eligibility, following launcher symlinks without opening special files. */
46
+ export function isExecutableCodexCandidate(path: string): boolean {
47
+ try {
48
+ if (process.platform !== "win32") accessSync(path, constants.X_OK);
49
+ return statSync(path).isFile();
50
+ } catch { return false; }
51
+ }
52
+
53
+ export function inspectShimFile(path: string): boolean | null {
45
54
  let fd: number | undefined;
46
55
  try {
56
+ if (process.platform !== "win32") {
57
+ try { accessSync(path, constants.X_OK); } catch { return null; }
58
+ }
47
59
  // Follow npm launcher symlinks, but never read a known special file.
48
60
  if (!statSync(path).isFile()) return null;
49
61
  const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NONBLOCK);
@@ -246,7 +246,7 @@ function readProbeMetadata(path: string, maxBytes: number): string | null {
246
246
  }
247
247
  }
248
248
 
249
- function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
249
+ function probeUnixShimInstall(wrapperPath: string, childEnv?: NodeJS.ProcessEnv): UnixShimProbeResult {
250
250
  if (process.platform === "win32") return null;
251
251
  const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
252
252
  const markerPath = join(probeDir, "result");
@@ -255,6 +255,7 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
255
255
  const stderrPath = join(probeDir, "stderr");
256
256
  const env: NodeJS.ProcessEnv = {
257
257
  ...process.env,
258
+ ...childEnv,
258
259
  BUN_BE_BUN: "1",
259
260
  OCX_SHIM_BYPASS: "1",
260
261
  OCX_SHIM_PROBE: "1",
@@ -329,12 +330,12 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
329
330
  }
330
331
  }
331
332
 
332
- function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
333
+ function probeUnixShimFiles(files: readonly ShimFileState[], childEnv?: NodeJS.ProcessEnv): UnixShimProbeResult {
333
334
  if (process.platform === "win32") return null;
334
335
  codexShimProbeHookForTests?.();
335
336
  return files
336
337
  .filter(file => !file.preserveOnly)
337
- .map(file => probeUnixShimInstall(file.wrapperPath))
338
+ .map(file => probeUnixShimInstall(file.wrapperPath, childEnv))
338
339
  .find(result => result !== null) ?? null;
339
340
  }
340
341