@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  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/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,145 @@
1
+ import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
+ import { posix, win32 } from "node:path";
3
+ import { isWslRuntime, wslAutomountRoot } from "./home";
4
+ import { SHIM_MARKER } from "./shim-templates";
5
+
6
+ /**
7
+ * A PATH entry that reaches Windows through WSL drive interop
8
+ * (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
9
+ * /etc/wsl.conf [automount] root).
10
+ */
11
+ export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
12
+ const root = automountRoot.replace(/\/+$/, "");
13
+ const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
+ return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
15
+ }
16
+
17
+ export type CodexPathScanDeps = {
18
+ pathValue?: string;
19
+ wsl?: boolean;
20
+ /** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
21
+ posixPaths?: boolean;
22
+ automountRoot?: string;
23
+ exists?: (path: string) => boolean;
24
+ isShimFile?: (path: string) => boolean;
25
+ isDirectory?: (path: string) => boolean;
26
+ realpath?: (path: string) => string;
27
+ executableEligible?: (path: string) => boolean;
28
+ };
29
+
30
+ export type CodexPathCandidate = {
31
+ path: string;
32
+ isShim: boolean;
33
+ };
34
+
35
+ export function realIsDirectory(path: string): boolean {
36
+ try {
37
+ return lstatSync(path).isDirectory();
38
+ } catch {
39
+ return true; // unreadable -> treat as unusable
40
+ }
41
+ }
42
+
43
+ const SHIM_HEADER_MAX_BYTES = 16 * 1024;
44
+
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 {
54
+ let fd: number | undefined;
55
+ try {
56
+ if (process.platform !== "win32") {
57
+ try { accessSync(path, constants.X_OK); } catch { return null; }
58
+ }
59
+ // Follow npm launcher symlinks, but never read a known special file.
60
+ if (!statSync(path).isFile()) return null;
61
+ const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NONBLOCK);
62
+ fd = openSync(path, flags);
63
+ // The entry can change after stat; a replacement FIFO must not block open or read.
64
+ if (!fstatSync(fd).isFile()) return null;
65
+ const header = Buffer.allocUnsafe(SHIM_HEADER_MAX_BYTES);
66
+ let total = 0;
67
+ while (total < header.length) {
68
+ const count = readSync(fd, header, total, header.length - total, total);
69
+ if (count === 0) break;
70
+ total += count;
71
+ }
72
+ return header.subarray(0, total).toString("utf8").includes(SHIM_MARKER);
73
+ } catch {
74
+ // An unreadable command still shadows later PATH entries; do not claim they are active.
75
+ return false;
76
+ } finally {
77
+ if (fd !== undefined) closeSync(fd);
78
+ }
79
+ }
80
+
81
+ function commandNames(name: string): string[] {
82
+ if (process.platform !== "win32") return [name];
83
+ const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
84
+ return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
85
+ }
86
+
87
+ /** The shell-local bin directory that fnm creates for one interactive shell. */
88
+ export function isFnmMultishellPath(path: string, posixPaths: boolean): boolean {
89
+ const normalized = (posixPaths
90
+ ? posix.normalize(path)
91
+ : win32.normalize(path).replace(/\\/g, "/")).toLowerCase();
92
+ return /(?:^|\/)fnm_multishells?(?:\/|$)/.test(normalized);
93
+ }
94
+
95
+ /** Find the command the shell would actually use, including an OpenCodex shim. */
96
+ export function findFirstCodexOnPath(deps: CodexPathScanDeps = {}): CodexPathCandidate | null {
97
+ const exists = deps.exists ?? existsSync;
98
+ const shimFile = deps.isShimFile ?? inspectShimFile;
99
+ const isDir = deps.isDirectory ?? realIsDirectory;
100
+ const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
101
+ const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
102
+ const joinPath = usePosix ? posix.join : win32.join;
103
+ const pathSep = usePosix ? ":" : win32.delimiter;
104
+ const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
105
+ const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
106
+
107
+ for (const dir of (deps.pathValue ?? process.env.PATH ?? "").split(pathSep).filter(Boolean)) {
108
+ if (wsl && isWindowsInteropDir(dir, automountRoot)) continue;
109
+ const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
110
+ for (const name of names) {
111
+ const path = joinPath(dir, name);
112
+ if (!exists(path) || isDir(path)) continue;
113
+ const isShim = shimFile(path);
114
+ if (isShim !== null) return { path, isShim };
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Resolve a Codex command discovered through fnm's temporary PATH entry.
122
+ *
123
+ * A temporary path is safe to use only when its physical target is outside the
124
+ * temporary tree. Returning null is deliberate: callers must not wrap the
125
+ * shell-local path or silently choose a lower-priority Codex installation.
126
+ */
127
+ export function resolveStableFnmCodexPath(
128
+ path: string,
129
+ posixPaths: boolean,
130
+ realpath: ((path: string) => string) | undefined = realpathSync.native,
131
+ ): string | null {
132
+ if (!isFnmMultishellPath(path, posixPaths)) return path;
133
+ try {
134
+ // fnm normally links the temporary directory to the durable installation,
135
+ // while npm may link the command file again into its package internals.
136
+ // Resolve only the directory so the command basename remains the stable
137
+ // installation entry that the installer is allowed to replace.
138
+ const pathTools = posixPaths ? posix : win32;
139
+ const resolvedParent = realpath(pathTools.dirname(path));
140
+ if (!resolvedParent || isFnmMultishellPath(resolvedParent, posixPaths) || !pathTools.isAbsolute(resolvedParent)) return null;
141
+ return pathTools.join(resolvedParent, pathTools.basename(path));
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
@@ -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
 
@@ -1,21 +1,31 @@
1
1
  import {
2
2
  closeSync,
3
+ constants,
3
4
  existsSync,
4
5
  fstatSync,
5
6
  lstatSync,
6
7
  mkdirSync,
7
8
  openSync,
8
9
  readSync,
10
+ renameSync,
11
+ unlinkSync,
9
12
  writeFileSync,
10
13
  type Stats,
11
14
  } from "node:fs";
12
- import { join } from "node:path";
15
+ import { randomUUID } from "node:crypto";
16
+ import { dirname, isAbsolute, join, resolve } from "node:path";
13
17
  import { getConfigDir } from "../config";
14
18
  import { recordOwnedConfigPath } from "../lib/config-ownership";
15
19
 
16
20
  export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
17
21
 
18
22
  interface ShimState {
23
+ schema?: 2;
24
+ mode?: "path-overlay";
25
+ launcherPath?: string;
26
+ wrapperIdentity?: OverlayIdentity;
27
+ envIdentity?: OverlayIdentity;
28
+ transactionId?: string;
19
29
  platform: NodeJS.Platform;
20
30
  wrapperPath: string;
21
31
  originalPath: string;
@@ -23,6 +33,51 @@ interface ShimState {
23
33
  wrappers?: ShimFileState[];
24
34
  }
25
35
 
36
+ export interface OverlayIdentity { dev: number; ino: number }
37
+ export interface OverlayState {
38
+ schema: 2;
39
+ mode: "path-overlay";
40
+ platform: NodeJS.Platform;
41
+ wrapperPath: string;
42
+ launcherPath: string;
43
+ // Older schema-2 records remain readable, but absent identities confer no mutation authority.
44
+ wrapperIdentity?: OverlayIdentity;
45
+ envIdentity?: OverlayIdentity;
46
+ transactionId?: string;
47
+ }
48
+
49
+ export function decodeOverlayState(value: unknown, configDir: string): OverlayState | null {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
51
+ const state = value as Record<string, unknown>;
52
+ const platforms = ["aix", "darwin", "freebsd", "linux", "openbsd", "sunos", "android", "netbsd", "haiku", "cygwin"];
53
+ const validPath = (path: unknown): path is string => typeof path === "string" && isAbsolute(path) && !/[\x00-\x1f\x7f]/.test(path);
54
+ if (state.schema !== 2 || state.mode !== "path-overlay" || !platforms.includes(String(state.platform))
55
+ || !validPath(configDir) || !validPath(state.wrapperPath) || !validPath(state.launcherPath)
56
+ || state.wrapperPath !== join(resolve(configDir), "bin", "codex")
57
+ || dirname(state.wrapperPath).includes(":") || resolve(state.launcherPath) === state.wrapperPath) return null;
58
+ for (const field of ["originalPath", "backupPath", "realPath"]) {
59
+ if (state[field] !== undefined && state[field] !== state.launcherPath) return null;
60
+ }
61
+ if (state.preserveOnly !== undefined && state.preserveOnly !== false) return null;
62
+ if (state.wrappers !== undefined) {
63
+ if (!Array.isArray(state.wrappers) || state.wrappers.length !== 1) return null;
64
+ const file = state.wrappers[0];
65
+ if (!file || typeof file !== "object" || file.wrapperPath !== state.wrapperPath
66
+ || file.originalPath !== state.launcherPath || file.backupPath !== state.launcherPath
67
+ || (file.realPath !== undefined && file.realPath !== state.launcherPath) || file.preserveOnly) return null;
68
+ }
69
+ for (const key of ["wrapperIdentity", "envIdentity"]) {
70
+ const id = state[key] as Partial<OverlayIdentity> | undefined;
71
+ if (id !== undefined && (!id || !Number.isSafeInteger(id.dev) || !Number.isSafeInteger(id.ino))) return null;
72
+ }
73
+ if (state.transactionId !== undefined && (typeof state.transactionId !== "string" || !/^[a-f0-9-]{36}$/.test(state.transactionId))) return null;
74
+ return state as unknown as OverlayState;
75
+ }
76
+
77
+ export function readOverlayState(configDir = getConfigDir()): OverlayState | null {
78
+ return decodeOverlayState(readStateResult(join(configDir, "codex-shim.json")).state, configDir);
79
+ }
80
+
26
81
  interface ShimFileState {
27
82
  wrapperPath: string;
28
83
  originalPath: string;
@@ -43,7 +98,7 @@ function fileErrorCode(error: unknown): string | undefined {
43
98
  : undefined;
44
99
  }
45
100
 
46
- function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
101
+ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string; stat: Stats } | { warning: string } | null {
47
102
  let lexicalBefore: Stats;
48
103
  try {
49
104
  lexicalBefore = lstatSync(path);
@@ -56,7 +111,7 @@ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer
56
111
  }
57
112
  let fd: number;
58
113
  try {
59
- fd = openSync(path, "r");
114
+ fd = openSync(path, process.platform === "win32" ? "r" : constants.O_RDONLY | constants.O_NONBLOCK);
60
115
  } catch (error) {
61
116
  if (fileErrorCode(error) === "ENOENT") return null;
62
117
  return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
@@ -87,11 +142,16 @@ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer
87
142
  }
88
143
  if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
89
144
  || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
145
+ || before.mode !== after.mode || before.uid !== after.uid
90
146
  || lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
91
- || lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino) {
147
+ || lexicalBefore.mode !== before.mode || lexicalBefore.uid !== before.uid
148
+ || lexicalBefore.size !== before.size || lexicalBefore.mtimeMs !== before.mtimeMs || lexicalBefore.ctimeMs !== before.ctimeMs
149
+ || !lexicalAfter.isFile() || lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino
150
+ || lexicalAfter.mode !== after.mode || lexicalAfter.uid !== after.uid
151
+ || lexicalAfter.size !== after.size || lexicalAfter.mtimeMs !== after.mtimeMs || lexicalAfter.ctimeMs !== after.ctimeMs) {
92
152
  return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
93
153
  }
94
- return { bytes: buffer, content: buffer.toString("utf8") };
154
+ return { bytes: buffer, content: buffer.toString("utf8"), stat: after };
95
155
  } finally {
96
156
  closeSync(fd);
97
157
  }
@@ -105,6 +165,15 @@ function readStateResult(path = statePath()): ShimStateReadResult {
105
165
  const value = JSON.parse(bounded.content) as unknown;
106
166
  if (!value || typeof value !== "object") return { state: null, present: true };
107
167
  const state = value as Record<string, unknown>;
168
+ if (state.mode !== undefined || state.schema !== undefined) {
169
+ // Legacy records stay readable; private overlay authority requires safe ownership.
170
+ // Non-writable mode drift is readable for opt-in repair, never a healthy installation.
171
+ if (process.platform !== "win32" && (bounded.stat.uid !== process.getuid?.() || (bounded.stat.mode & 0o022))) {
172
+ return { state: null, present: true, warning: "Private Codex shim state is not owned by the current user or is group/world writable; preserving artifacts. Remove the private files manually before reinstalling." };
173
+ }
174
+ const overlay = decodeOverlayState(value, dirname(path));
175
+ return { state: overlay ? { ...overlay, originalPath: overlay.launcherPath, backupPath: overlay.launcherPath } : null, present: true };
176
+ }
108
177
  if (typeof state.platform !== "string") return { state: null, present: true };
109
178
  const validFile = (item: unknown): item is ShimFileState => {
110
179
  if (!item || typeof item !== "object") return false;
@@ -134,18 +203,35 @@ function statePath(): string {
134
203
  return join(getConfigDir(), "codex-shim.json");
135
204
  }
136
205
 
137
- function writeState(state: ShimState): void {
206
+ function writeState(state: ShimState | OverlayState): void {
138
207
  const path = statePath();
139
208
  recordOwnedConfigPath(getConfigDir(), path);
140
209
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
210
+ if (state.mode === "path-overlay") {
211
+ if (!decodeOverlayState(state, getConfigDir()) || !state.wrapperIdentity || !state.envIdentity) {
212
+ throw new Error("Invalid Codex overlay state: recorded ownership identities are required");
213
+ }
214
+ const staged = `${path}.${randomUUID()}.tmp`;
215
+ try {
216
+ writeFileSync(staged, JSON.stringify(state, null, 2) + "\n", { flag: "wx", mode: 0o600 });
217
+ renameSync(staged, path);
218
+ } finally {
219
+ try { unlinkSync(staged); } catch { /* committed or absent */ }
220
+ }
221
+ return;
222
+ }
141
223
  writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
142
224
  }
143
225
 
144
226
  function stateFiles(state: ShimState): ShimFileState[] {
227
+ if (state.mode === "path-overlay" && state.launcherPath) return [{
228
+ wrapperPath: state.wrapperPath, originalPath: state.launcherPath,
229
+ backupPath: state.launcherPath, realPath: state.launcherPath,
230
+ }];
145
231
  return state.wrappers?.length
146
232
  ? state.wrappers
147
233
  : [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
148
234
  }
149
235
 
150
236
  export type { ShimState, ShimFileState };
151
- export { fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };
237
+ export { readBoundedRegularFile, fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };
package/src/codex/shim.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  unlinkSync,
9
9
  writeFileSync,
10
10
  } from "node:fs";
11
- import { basename, delimiter, dirname, extname, join, posix } from "node:path";
11
+ import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
12
12
  import { durableBunRuntime } from "../lib/bun-runtime";
13
13
  import type { BunRuntimeSource } from "../lib/bun-runtime";
14
14
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
@@ -53,14 +53,28 @@ import {
53
53
  type UnixShimProbeResult,
54
54
  } from "./shim-probe";
55
55
  import { tryAcquireShimRestoreLock } from "./shim-restore-lock";
56
+ import {
57
+ isWindowsInteropDir,
58
+ isExecutableCodexCandidate,
59
+ inspectShimFile,
60
+ realIsDirectory,
61
+ resolveStableFnmCodexPath,
62
+ type CodexPathScanDeps,
63
+ } from "./shim-path-resolution";
56
64
 
57
65
  export { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim } from "./shim-templates";
58
66
  export { isVersionManagerOwnedCodexPath } from "./shim-fingerprint";
59
67
  export { CODEX_SHIM_STATE_MAX_BYTES } from "./shim-state-file";
60
68
  export { setCodexShimProbeHookForTests, setCodexShimProbeShellForTests, setCodexShimProbeObservationMsForTests } from "./shim-probe";
61
69
  export type { CodexShimBackingForCommand } from "./shim-inspect";
70
+ export type { CodexPathScanDeps } from "./shim-path-resolution";
71
+ export { isWindowsInteropDir } from "./shim-path-resolution";
62
72
  export { isLocalAbsoluteInspectionPath, inspectCodexShimBackingForCommand } from "./shim-inspect";
63
73
 
74
+ import { isShim, diagnoseCodexShim } from "./shim-diagnostics";
75
+ import { installUnixOverlay, autoRestoreUnixOverlay, uninstallUnixCodexShim, overlayPaths } from "./shim-overlay";
76
+ export { diagnoseCodexShim, codexShimStatus, type CodexShimDiagnostic } from "./shim-diagnostics";
77
+
64
78
  export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
65
79
 
66
80
  let lastShimDiscoveryError: string | null = null;
@@ -75,6 +89,17 @@ interface InstallCodexShimInternalOptions {
75
89
  beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
76
90
  }
77
91
 
92
+ function discoveredCodexPath(path: string, posixPaths: boolean, realpath?: (path: string) => string): string | null {
93
+ const resolved = resolveStableFnmCodexPath(path, posixPaths, realpath);
94
+ if (resolved !== null) return resolved;
95
+ lastShimDiscoveryError = truncateRetainedUtf8(
96
+ `Found codex at ${path} inside fnm's temporary multishell path, but it did not resolve to a durable Node installation. `
97
+ + "Refusing to install a shim; retry after fnm selects a stable installation.",
98
+ MAX_DIAGNOSTIC_VALUE_BYTES,
99
+ );
100
+ return null;
101
+ }
102
+
78
103
  export type CodexShimAutoRestoreResult =
79
104
  | { status: "not-installed" | "healthy" | "disabled" }
80
105
  | { status: "ineligible" | "deferred"; message?: string }
@@ -95,65 +120,18 @@ function commandNames(name: string): string[] {
95
120
  return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
96
121
  }
97
122
 
98
- function isShim(path: string): boolean {
99
- try {
100
- return readFileSync(path, "utf8").includes(SHIM_MARKER);
101
- } catch {
102
- return false;
103
- }
104
- }
105
-
106
- function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
107
- try {
108
- const content = readFileSync(path, "utf8");
109
- if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
110
- if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
111
- if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
112
- return true;
113
- } catch {
114
- return false;
115
- }
116
- }
117
-
118
- /**
119
- * A PATH entry that reaches Windows through WSL drive interop
120
- * (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
121
- * /etc/wsl.conf [automount] root).
122
- */
123
- export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
124
- const root = automountRoot.replace(/\/+$/, "");
125
- const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
- return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
127
- }
128
-
129
- export type CodexPathScanDeps = {
130
- pathValue?: string;
131
- wsl?: boolean;
132
- /** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
133
- posixPaths?: boolean;
134
- automountRoot?: string;
135
- exists?: (path: string) => boolean;
136
- isShimFile?: (path: string) => boolean;
137
- isDirectory?: (path: string) => boolean;
138
- };
139
-
140
- function realIsDirectory(path: string): boolean {
141
- try {
142
- return lstatSync(path).isDirectory();
143
- } catch {
144
- return true; // unreadable -> treat as unusable
145
- }
146
- }
147
-
148
123
  export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
149
124
  lastShimDiscoveryError = null;
150
125
  const exists = deps.exists ?? existsSync;
151
- const shimFile = deps.isShimFile ?? isShim;
126
+ const shimFile = deps.isShimFile ?? (process.platform === "win32" ? isShim : (path: string) => inspectShimFile(path) !== false);
152
127
  const isDir = deps.isDirectory ?? realIsDirectory;
128
+ const eligible = deps.executableEligible ?? (deps.exists ? () => true : isExecutableCodexCandidate);
153
129
  const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
154
130
  const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
155
- const joinPath = usePosix ? posix.join : join;
156
- const pathSep = usePosix ? ":" : delimiter;
131
+ // The injected path flavour governs join and separator alike; host-native `join` and
132
+ // `delimiter` made a Windows-flavoured scan split `C:\...` on a Linux host.
133
+ const joinPath = usePosix ? posix.join : win32.join;
134
+ const pathSep = usePosix ? ":" : win32.delimiter;
157
135
  const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
158
136
  // Windows npm prefixes ship codex.exe/codex.cmd next to the extensionless sh launcher.
159
137
  const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
@@ -175,8 +153,14 @@ export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
175
153
  const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
176
154
  for (const name of names) {
177
155
  const path = joinPath(dir, name);
178
- if (!exists(path) || shimFile(path)) continue;
179
- if (!isDir(path)) return path;
156
+ if (!exists(path)) continue;
157
+ if (usePosix && (path === overlayPaths().wrapper || !eligible(path))) continue;
158
+ if (shimFile(path)) continue;
159
+ if (!isDir(path)) {
160
+ const resolved = discoveredCodexPath(path, usePosix, deps.realpath);
161
+ if (resolved !== null) return resolved;
162
+ return null;
163
+ }
180
164
  }
181
165
  }
182
166
 
@@ -195,11 +179,13 @@ function findWindowsCodexTargets(): ShimFileState[] | null {
195
179
  lastShimDiscoveryError = null;
196
180
  for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
197
181
  const exe = join(dir, "codex.exe");
198
- if (existsSync(exe) && !isShim(exe)) {
182
+ const stableExe = existsSync(exe) && !isShim(exe) ? discoveredCodexPath(exe, false) : exe;
183
+ if (stableExe === null) return null;
184
+ if (existsSync(stableExe) && !isShim(stableExe)) {
199
185
  try {
200
- if (!lstatSync(exe).isDirectory()) {
186
+ if (!lstatSync(stableExe).isDirectory()) {
201
187
  lastShimDiscoveryError = truncateRetainedUtf8(
202
- `Found codex.exe at ${exe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
188
+ `Found codex.exe at ${stableExe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
203
189
  "install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart.",
204
190
  MAX_DIAGNOSTIC_VALUE_BYTES,
205
191
  );
@@ -216,9 +202,12 @@ function findWindowsCodexTargets(): ShimFileState[] | null {
216
202
  const targets: ShimFileState[] = [];
217
203
  for (const path of [cmd, ps1, gitBashLauncher]) {
218
204
  if (!existsSync(path) || isShim(path)) continue;
205
+ const stablePath = discoveredCodexPath(path, false);
206
+ if (stablePath === null) return null;
207
+ if (!existsSync(stablePath) || isShim(stablePath)) continue;
219
208
  try {
220
- if (!lstatSync(path).isDirectory()) {
221
- targets.push({ wrapperPath: path, originalPath: path, backupPath: backupPathFor(path) });
209
+ if (!lstatSync(stablePath).isDirectory()) {
210
+ targets.push({ wrapperPath: stablePath, originalPath: stablePath, backupPath: backupPathFor(stablePath) });
222
211
  }
223
212
  } catch { /* keep scanning */ }
224
213
  }
@@ -1072,7 +1061,8 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
1072
1061
  };
1073
1062
  }
1074
1063
 
1075
- export function installCodexShim(): { installed: boolean; message: string } {
1064
+ export function installCodexShim(): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
1065
+ if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath);
1076
1066
  return installCodexShimInternal({ allowFreshInstall: true });
1077
1067
  }
1078
1068
 
@@ -1086,6 +1076,7 @@ export function autoRestoreCodexShim(options: {
1086
1076
  /** Narrow deterministic race seam for the guarded transaction tests. */
1087
1077
  beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
1088
1078
  }): CodexShimAutoRestoreResult {
1079
+ if (process.platform !== "win32") return autoRestoreUnixOverlay(options);
1089
1080
  const stateRead = readStateResult();
1090
1081
  const state = stateRead.state;
1091
1082
  if (!state) {
@@ -1181,6 +1172,7 @@ export function autoRestoreCodexShim(options: {
1181
1172
  }
1182
1173
 
1183
1174
  export function uninstallCodexShim(): { removed: boolean; message: string } {
1175
+ if (process.platform !== "win32") return uninstallUnixCodexShim();
1184
1176
  const state = readState();
1185
1177
  if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
1186
1178
  const files = stateFiles(state);
@@ -1199,48 +1191,3 @@ export function uninstallCodexShim(): { removed: boolean; message: string } {
1199
1191
  export function isCodexShimInstalled(): boolean {
1200
1192
  return diagnoseCodexShim().installed;
1201
1193
  }
1202
-
1203
- export interface CodexShimDiagnostic {
1204
- installed: boolean;
1205
- healthy: boolean;
1206
- summary: string;
1207
- }
1208
-
1209
- /** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
1210
- export function diagnoseCodexShim(): CodexShimDiagnostic {
1211
- const state = readState();
1212
- if (!state) {
1213
- if (existsSync(statePath())) {
1214
- return {
1215
- installed: true,
1216
- healthy: false,
1217
- summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
1218
- };
1219
- }
1220
- return {
1221
- installed: false,
1222
- healthy: false,
1223
- summary: "Codex autostart shim is not installed.",
1224
- };
1225
- }
1226
- const files = stateFiles(state);
1227
- const healthy = files.length > 0 && files.every(file => file.preserveOnly
1228
- ? existsSync(file.backupPath) && !existsSync(file.originalPath)
1229
- : existsSync(file.wrapperPath)
1230
- && (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
1231
- && isHealthyShim(file.wrapperPath, state.platform));
1232
- const summary = files.map(file => {
1233
- const wrapper = existsSync(file.wrapperPath)
1234
- ? isShim(file.wrapperPath)
1235
- ? "shim present"
1236
- : "present but not an opencodex shim"
1237
- : "missing";
1238
- const backup = existsSync(file.backupPath) ? "present" : "missing";
1239
- return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
1240
- }).join("\n");
1241
- return { installed: true, healthy, summary };
1242
- }
1243
-
1244
- export function codexShimStatus(): string {
1245
- return diagnoseCodexShim().summary;
1246
- }
@@ -1,3 +1,4 @@
1
+ import { codexAccountUsesCreditsAfterLimit } from "./account-credit-use";
1
2
  /**
2
3
  * Quota-aware subagent model fallback (issue #374).
3
4
  *
@@ -233,7 +234,7 @@ export function isNativeModelQuotaExhausted(
233
234
  // Subagent fallback reads the same score, so a stale terminal reading would push
234
235
  // subagents off a native model whose window has already reset. Thread the caller's clock
235
236
  // rather than letting the scorer read wall time - the two would silently diverge.
236
- const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId), now);
237
+ const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId), now, codexAccountUsesCreditsAfterLimit(config, resolvedAccountId));
237
238
  if (usage >= CODEX_UNKNOWN_USAGE_SCORE) return false;
238
239
  return usage >= quotaThreshold(config, resolvedAccountId);
239
240
  }