@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -8,7 +8,7 @@ import { readIntegrationState, type IntegrationState, type IntegrationStatus } f
8
8
  import {
9
9
  applyIntegrationCoordinated, disableIntegrationCoordinated,
10
10
  overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
11
- type IntegrationWriteInput,
11
+ type CoordinatedIntegrationOptions, type IntegrationWriteInput,
12
12
  } from "./writer";
13
13
  import {
14
14
  asideProfileEnabled, asideProfileFailure, asideProfileScope, asideWriteInput,
@@ -260,7 +260,10 @@ export function mutateAsideProfiles(
260
260
  });
261
261
  }
262
262
 
263
- export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
263
+ export function refreshAsideProfiles(
264
+ input: AsideProfilesInput,
265
+ options?: { refreshOnly?: boolean; guard?: CoordinatedIntegrationOptions["guard"] },
266
+ ): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
264
267
  const policy = input.config.asideProfileSync;
265
268
  const selected = Object.values(policy?.profiles ?? {}).some(enabled => enabled === true);
266
269
  if (!selected && (policy?.allProfiles === false
@@ -272,11 +275,15 @@ export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<O
272
275
  try {
273
276
  const scope = asideProfileScope(ctx, profile);
274
277
  const owned = scope.store.readRecords().aside !== undefined;
278
+ if (options?.refreshOnly && !owned) continue;
275
279
  const bound = await asideWriteInput(ctx, scope);
276
280
  // A surviving ownership record means a removed block stays removed.
277
281
  // A newly discovered, enabled profile may receive its first safe apply.
278
282
  const operation = owned ? refreshIntegrationCoordinated : applyIntegrationCoordinated;
279
- const result = await operation(bound, { lockSeams: input.lockSeams });
283
+ const result = await operation(bound, {
284
+ lockSeams: input.lockSeams,
285
+ ...(options?.guard ? { guard: options.guard } : {}),
286
+ });
280
287
  outcomes.push({
281
288
  client: "aside", profileId: profile.id, ok: result.ok,
282
289
  ...(result.ok ? { changed: result.changed } : {}),
@@ -2,6 +2,7 @@ import { siblingOfLivePort } from "../codex/sibling-start";
2
2
  import { redactSecretString } from "../lib/redact";
3
3
  import type { ExportModel } from "../clients/config-export";
4
4
  import type { IntegrationClientId } from "./registry";
5
+ import type { CoordinatedIntegrationOptions, WriteOutcome } from "./writer";
5
6
  import {
6
7
  refreshOwnedIntegration,
7
8
  type OwnedIntegrationRefreshInput,
@@ -11,7 +12,8 @@ import {
11
12
  /** Refresh only previously connected clients; a refused file never blocks its peers. */
12
13
  export async function refreshOwnedCatalogIntegrations(
13
14
  input: Omit<OwnedIntegrationRefreshInput, "clientId">,
14
- clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid", "opencode", "kilo"],
15
+ clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"],
16
+ options: { refreshOnly?: boolean; admit?: () => boolean } = {},
15
17
  ): Promise<OwnedIntegrationRefreshOutcome[]> {
16
18
  // Client files are shared with the live proxy a sibling instance runs beside; their entries
17
19
  // point at the owner's port, and refreshing them here would re-point them at this one.
@@ -20,14 +22,28 @@ export async function refreshOwnedCatalogIntegrations(
20
22
  const loadModels = () => models ??= Promise.resolve().then(() =>
21
23
  typeof input.models === "function" ? input.models() : input.models);
22
24
  const outcomes: OwnedIntegrationRefreshOutcome[] = [];
25
+ const admit = options.admit;
26
+ const writeOptions: CoordinatedIntegrationOptions | undefined = admit ? {
27
+ guard: (frozen): WriteOutcome | null => admit() ? null : {
28
+ ok: false, reason: "superseded_store", state: "current", clientId: frozen.clientId,
29
+ message: "Background refresh superseded",
30
+ },
31
+ } : undefined;
23
32
  for (const clientId of clientIds) {
33
+ if (admit?.() === false) break;
24
34
  try {
25
35
  if (clientId === "aside") {
26
36
  const { refreshAsideProfiles } = await import("./aside-profiles");
27
- outcomes.push(...await refreshAsideProfiles({ ...input, models: loadModels }));
37
+ const asideOptions = options.refreshOnly === undefined ? writeOptions
38
+ : { ...writeOptions, refreshOnly: options.refreshOnly };
39
+ const bound = { ...input, models: loadModels };
40
+ outcomes.push(...await (asideOptions === undefined
41
+ ? refreshAsideProfiles(bound) : refreshAsideProfiles(bound, asideOptions)));
28
42
  continue;
29
43
  }
30
- const result = await refreshOwnedIntegration({ ...input, clientId, models: loadModels });
44
+ const bound = { ...input, clientId, models: loadModels };
45
+ const result = await (writeOptions === undefined
46
+ ? refreshOwnedIntegration(bound) : refreshOwnedIntegration(bound, writeOptions));
31
47
  if (result) outcomes.push(result);
32
48
  } catch (error) {
33
49
  const busy = error !== null && typeof error === "object"
@@ -149,6 +149,13 @@ const CLIENT_MANAGED_PATHS = {
149
149
  [...ZCODE_STORE_PROVIDER_RULES_PATH, `[providerId=${OPENCODE_PROVIDER_ID}]`],
150
150
  [...ZCODE_STORE_MODEL_RULES_PATH, DYNAMIC_SEGMENT],
151
151
  ],
152
+ /*
153
+ * Both roots, because the client picks between them rather than reading both:
154
+ * `document.provider ?? document.providers` in `command-code@1.66.0`. The
155
+ * builder chooses the one the target already uses, and a block we wrote under
156
+ * either root has to be removable from either root afterwards.
157
+ */
158
+ commandcode: [["provider", OPENCODE_PROVIDER_ID], ["providers", OPENCODE_PROVIDER_ID]],
152
159
  prime: [["providers", OPENCODE_PROVIDER_ID]],
153
160
  aside: [["providers", OPENCODE_PROVIDER_ID]],
154
161
  raycast: [["providers", `[id=${OPENCODE_PROVIDER_ID}]`]],
@@ -58,6 +58,8 @@ import {
58
58
  DSH_PROFILE_PROVIDER_PATH,
59
59
  type BuildContribution,
60
60
  type ConfigFormat,
61
+ commandCodeConfigPath,
62
+ commandCodeHomeDir,
61
63
  kiloConfigPath,
62
64
  kiloHomeDir,
63
65
  kiloCandidatePath,
@@ -369,6 +371,12 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
369
371
  buildContribution: buildZcodeStoreContribution,
370
372
  },
371
373
  },
374
+ commandcode: {
375
+ id: "commandcode",
376
+ configPath: (env = process.env, home = homedir()) => commandCodeConfigPath(env, home),
377
+ detectDir: (env = process.env, home = homedir()) => commandCodeHomeDir(env, home),
378
+ writerLock: { suffix: ".lock" },
379
+ },
372
380
  prime: {
373
381
  id: "prime",
374
382
  configPath: (env = process.env, home = homedir()) => primeConfigPath(env, home),
@@ -490,7 +490,8 @@ export function exportContextOf(input: {
490
490
  config: OcxConfig;
491
491
  port: number;
492
492
  droidReasoningDefaults?: Record<string, string>;
493
- }): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record<string, string> } {
493
+ document?: unknown;
494
+ }): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record<string, string>; document?: unknown } {
494
495
  return {
495
496
  /*
496
497
  * Composed through the SAME helper `ocx export` uses. Interpolating the
@@ -505,6 +506,9 @@ export function exportContextOf(input: {
505
506
  models: input.models,
506
507
  config: input.config,
507
508
  ...(input.droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: input.droidReasoningDefaults }),
509
+ // Forwarded only when the caller parsed a target: a client that resolves
510
+ // between two document roots (Command Code) needs the bytes on disk.
511
+ ...(input.document === undefined ? {} : { document: input.document }),
508
512
  };
509
513
  }
510
514
 
@@ -514,7 +518,10 @@ export function buildIntegrationContribution(
514
518
  parsed: unknown,
515
519
  record: OwnershipRecord | null,
516
520
  ): ManagedContribution {
517
- const context = exportContextOf(input);
521
+ // The parsed target travels with the context: a client whose reader picks
522
+ // between two document roots (Command Code) must see the bytes on disk, and
523
+ // routing it through this one builder keeps classify, plan and write agreeing.
524
+ const context = exportContextOf({ ...input, document: parsed });
518
525
  if (input.clientId === "droid" && input.droidReasoningDefaults === undefined && record?.configPath === effective.configPath) {
519
526
  const inherited = recordedDroidContributionMatches(parsed, record)
520
527
  ? droidDefaultsFromOwnedRows(context, parsed, record.fragmentPaths)
@@ -18,7 +18,7 @@ import { shouldInjectApiAuthHeader } from "../codex/inject";
18
18
  import { detachedConfigSnapshot } from "../config/admitted-identity";
19
19
  import { copyPlainData } from "../lib/plain-data";
20
20
  import type { OcxConfig } from "../types";
21
- import { defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
21
+ import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
22
22
  import { inspectKiloCandidates } from "./kilo-candidates";
23
23
  import {
24
24
  fingerprint,
@@ -727,9 +727,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
727
727
  // exact bytes when the snapshot was taken. Re-deriving it from the file would
728
728
  // mean guessing which entries are ours, and a wrong guess deletes a user's.
729
729
  const restoredRecord = entry.priorRecord;
730
+ let restoredDocument = clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined;
731
+ if (clientId === "commandcode" && restoredText !== null) {
732
+ const parsed = parseConfig(restoredText, "json");
733
+ // Restoring raw snapshot bytes must remain possible even when they are not parseable.
734
+ restoredDocument = parsed === PARSE_FAILED ? undefined : parsed;
735
+ }
730
736
  const fresh = buildIntegrationContribution(
731
737
  { ...input, droidReasoningDefaults: undefined }, rowTarget,
732
- clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined,
738
+ restoredDocument,
733
739
  restoredRecord,
734
740
  );
735
741
  /*
@@ -795,6 +801,11 @@ export interface CoordinatedIntegrationOptions {
795
801
  * non-null result refuses without writing anything.
796
802
  */
797
803
  revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
804
+ /**
805
+ * Synchronous last word, evaluated with no await between it and the transaction (after
806
+ * `revalidate` and any lock acquisition). A non-null result refuses without writing.
807
+ */
808
+ guard?: (frozen: IntegrationWriteInput) => WriteOutcome | null;
798
809
  }
799
810
 
800
811
  /** Freeze all mutable resolution seams before the first lock await. */
@@ -936,21 +947,22 @@ async function coordinatedWrite(
936
947
  if (!prepared.ok) return prepared.refusal;
937
948
  const frozen = prepared.value;
938
949
  const spec = INTEGRATION_CLIENTS[frozen.clientId];
950
+ const guarded = (bound: IntegrationWriteInput) => options?.guard?.(bound) ?? operation(bound);
939
951
  if (!spec.writerLock) {
940
952
  const refused = await options?.revalidate?.(frozen);
941
- return refused ?? operation(frozen);
953
+ return refused ?? guarded(frozen);
942
954
  }
943
955
 
944
956
  // An absent client home is not created merely to acquire a sibling lock.
945
957
  if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
946
958
  const refused = await options?.revalidate?.(frozen);
947
959
  if (refused) return refused;
948
- if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return operation(frozen);
960
+ if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return guarded(frozen);
949
961
  }
950
962
  return withClientLocks(
951
963
  frozen,
952
964
  spec.writerLock.suffix,
953
- () => operation(frozen),
965
+ () => guarded(frozen),
954
966
  options,
955
967
  );
956
968
  }
@@ -0,0 +1,32 @@
1
+ export interface DesktopCliIo {
2
+ stat?: (path: string) => { isFile(): boolean };
3
+ access?: (path: string, mode: number) => void;
4
+ }
5
+ export interface PathBunIo extends DesktopCliIo {
6
+ now?: () => number;
7
+ realpath?: (path: string) => string;
8
+ stat?: (path: string) => { isFile(): boolean; mode: number };
9
+ isRealBunBinary?: (path: string) => boolean;
10
+ spawnSync?: (path: string, args: string[], options: {
11
+ env: NodeJS.ProcessEnv;
12
+ encoding: "utf8";
13
+ timeout: number;
14
+ maxBuffer: number;
15
+ killSignal: "SIGKILL";
16
+ shell: false;
17
+ windowsHide: true;
18
+ stdio: ["ignore", "pipe", "pipe"];
19
+ }) => { status: number | null; stdout?: string; signal?: string | null; error?: unknown };
20
+ }
21
+ export declare function findPathBun(options: {
22
+ env?: NodeJS.ProcessEnv;
23
+ platform?: NodeJS.Platform;
24
+ pinnedVersion: string;
25
+ deadlineMs?: number;
26
+ io?: PathBunIo;
27
+ }): { path: string; version: string } | null;
28
+ export declare function findDesktopCli(options?: {
29
+ platform?: NodeJS.Platform;
30
+ home?: string;
31
+ io?: DesktopCliIo;
32
+ }): string | null;
@@ -0,0 +1,95 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { posix, win32 } from "node:path";
5
+ import { performance } from "node:perf_hooks";
6
+ import { isRealBunBinary } from "./bun-binary-validator.mjs";
7
+
8
+ const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
9
+ const MAX_VERSION_BYTES = 4096;
10
+ const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
11
+
12
+ function stableVersion(value) {
13
+ if (typeof value !== "string" || value.length > 64) return null;
14
+ const match = STABLE.exec(value);
15
+ if (!match) return null;
16
+ const numbers = match.slice(1, 4).map(Number);
17
+ return numbers.every(Number.isSafeInteger) ? numbers : null;
18
+ }
19
+
20
+ /** Discover the first absolute PATH Bun passing version-policy and identity checks. */
21
+ export function findPathBun({
22
+ env = process.env, platform = process.platform, pinnedVersion,
23
+ deadlineMs = 750, io = {},
24
+ }) {
25
+ const pin = stableVersion(pinnedVersion);
26
+ if (!pin || !Number.isFinite(deadlineMs) || deadlineMs <= 0) return null;
27
+ const paths = platform === "win32" ? win32 : posix;
28
+ const absolute = value => paths.isAbsolute(value)
29
+ && (platform !== "win32" || /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+)/.test(value));
30
+ const now = io.now ?? (() => performance.now());
31
+ const end = now() + deadlineMs;
32
+ const realpath = io.realpath ?? realpathSync.native;
33
+ const stat = io.stat ?? statSync;
34
+ const access = io.access ?? accessSync;
35
+ const validate = io.isRealBunBinary ?? isRealBunBinary;
36
+ const spawn = io.spawnSync ?? spawnSync;
37
+ const key = platform === "win32"
38
+ ? Object.keys(env).find(name => name.toLowerCase() === "path") : "PATH";
39
+ const directories = (env[key ?? "PATH"] ?? "").split(platform === "win32" ? ";" : ":");
40
+ const seen = new Set();
41
+ const probe = (path, args) => {
42
+ const remaining = Math.floor(end - now());
43
+ if (remaining <= 0) return null;
44
+ const result = spawn(path, args, {
45
+ env, encoding: "utf8", timeout: remaining, maxBuffer: MAX_VERSION_BYTES,
46
+ killSignal: "SIGKILL", shell: false, windowsHide: true,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ if (now() >= end || result.error || result.signal || result.status !== 0
50
+ || typeof result.stdout !== "string" || result.stdout.length > MAX_VERSION_BYTES) return null;
51
+ return result.stdout.trim();
52
+ };
53
+ for (const directory of directories) {
54
+ if (now() >= end) break;
55
+ if (!directory || !absolute(directory)) continue;
56
+ try {
57
+ const path = realpath(paths.join(directory, platform === "win32" ? "bun.exe" : "bun"));
58
+ if (!absolute(path)) continue;
59
+ const identity = platform === "win32" ? path.toLowerCase() : path;
60
+ if (seen.has(identity)) continue;
61
+ seen.add(identity);
62
+ const file = stat(path);
63
+ if (!file.isFile()) continue;
64
+ if (platform !== "win32" && ((file.mode & 0o022) || (stat(paths.dirname(path)).mode & 0o022))) continue;
65
+ access(path, constants.X_OK);
66
+ if (!validate(path)) continue;
67
+ const version = probe(path, ["--version"]);
68
+ const candidate = stableVersion(version);
69
+ if (!candidate || candidate[0] !== pin[0] || candidate[1] < pin[1]) continue;
70
+ if (probe(path, ["-e", IDENTITY_PROBE]) !== version) continue;
71
+ return { path, version };
72
+ } catch {
73
+ // Unreadable, unexecutable, vanished or invalid: try the next absolute candidate.
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /** Failure-text pointer only. Never execute or delegate to this binary. */
80
+ export function findDesktopCli({ platform = process.platform, home = homedir(), io = {} } = {}) {
81
+ if (platform !== "darwin") return null;
82
+ const stat = io.stat ?? statSync;
83
+ const access = io.access ?? accessSync;
84
+ for (const path of [
85
+ "/Applications/OpenCodex.app/Contents/MacOS/ocx",
86
+ posix.join(home, "Applications/OpenCodex.app/Contents/MacOS/ocx"),
87
+ ]) {
88
+ try {
89
+ if (!stat(path).isFile()) continue;
90
+ access(path, constants.X_OK);
91
+ return path;
92
+ } catch { /* Missing or not executable: no pointer for this candidate. */ }
93
+ }
94
+ return null;
95
+ }
@@ -0,0 +1,140 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { lstatSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import type { DurableBunRuntime } from "./bun-runtime";
6
+
7
+ export type RuntimePreflightReason = "spawn" | "timeout" | "create" | "remove" | "protocol";
8
+ export const RUNTIME_PREFLIGHT_TIMEOUT_MS = 5_000;
9
+ export const RUNTIME_PREFLIGHT_GUIDANCE = "The selected Bun runtime could not create and remove a directory in the OpenCodex config directory. "
10
+ + "Windows application policy may deny writes from this executable location. "
11
+ + "Set OPENCODEX_BUN_PATH to a trusted Bun executable allowed by your policy before launching ocx, "
12
+ + "or reinstall opencodex with npm install -g @bitkyc08/opencodex, then retry. No alternate runtime was selected.";
13
+
14
+ export class RuntimePreflightError extends Error {
15
+ readonly code = "OCX_RUNTIME_PREFLIGHT_FAILED";
16
+ constructor(readonly reason: RuntimePreflightReason) {
17
+ super(`${RUNTIME_PREFLIGHT_GUIDANCE} (preflight: ${reason})`);
18
+ this.name = "RuntimePreflightError";
19
+ }
20
+ }
21
+
22
+ type DirectoryIdentity = { dev: number; ino: number; isDirectory(): boolean; isSymbolicLink(): boolean };
23
+ export interface RuntimePreflightFs {
24
+ lstatSync: (path: string) => DirectoryIdentity;
25
+ mkdirSync: (path: string, mode: number) => unknown;
26
+ rmdirSync: (path: string) => void;
27
+ readdirSync: (path: string) => readonly string[];
28
+ }
29
+ export interface RuntimePreflightOptions {
30
+ platform?: NodeJS.Platform;
31
+ rootWasAbsent?: boolean;
32
+ spawnSync?: typeof spawnSync;
33
+ fs?: RuntimePreflightFs;
34
+ nonce?: () => string;
35
+ }
36
+ /** Caller seams keep one selection through admission, rendering and state publication. */
37
+ export interface RuntimePreflightDeps {
38
+ selectRuntime?: () => DurableBunRuntime;
39
+ assertRuntimeWritable?: typeof assertSelectedRuntimeWritable;
40
+ configDir?: () => string;
41
+ platform?: NodeJS.Platform;
42
+ }
43
+
44
+ const probeFs: RuntimePreflightFs = { lstatSync, mkdirSync, rmdirSync, readdirSync };
45
+
46
+ function sameDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): boolean {
47
+ const current = fs.lstatSync(path);
48
+ return current.isDirectory() && !current.isSymbolicLink()
49
+ && current.dev === owned.dev && current.ino === owned.ino;
50
+ }
51
+
52
+ function removeOwnedEmptyDirectory(fs: RuntimePreflightFs, path: string, owned: DirectoryIdentity): void {
53
+ try {
54
+ if (sameDirectory(fs, path, owned) && fs.readdirSync(path).length === 0
55
+ && sameDirectory(fs, path, owned)) fs.rmdirSync(path);
56
+ } catch { /* Never delete contents or a replacement; cleanup does not mask refusal. */ }
57
+ }
58
+
59
+ function prepareRoot(fs: RuntimePreflightFs, root: string, rootWasAbsent?: boolean): void {
60
+ try {
61
+ if (rootWasAbsent === undefined) {
62
+ try { fs.lstatSync(root); rootWasAbsent = false; }
63
+ catch (error) {
64
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
65
+ rootWasAbsent = true;
66
+ }
67
+ }
68
+ if (rootWasAbsent) {
69
+ try {
70
+ fs.mkdirSync(root, 0o700); // Exclusive, non-recursive: a missing parent refuses.
71
+ } catch (error) {
72
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
73
+ // Use a concurrently created root, subject to the directory check below.
74
+ }
75
+ }
76
+ const current = fs.lstatSync(root);
77
+ if (!current.isDirectory() || current.isSymbolicLink()) throw new RuntimePreflightError("create");
78
+ } catch {
79
+ throw new RuntimePreflightError("create");
80
+ }
81
+ }
82
+
83
+ function probeInProcess(fs: RuntimePreflightFs, path: string): void {
84
+ let owned: DirectoryIdentity | undefined;
85
+ try {
86
+ try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); }
87
+ catch { throw new RuntimePreflightError("create"); }
88
+ try { fs.rmdirSync(path); }
89
+ catch { throw new RuntimePreflightError("remove"); }
90
+ } finally {
91
+ if (owned) removeOwnedEmptyDirectory(fs, path, owned);
92
+ }
93
+ }
94
+
95
+ function probeChild(runtime: DurableBunRuntime, path: string, nonce: string, spawn: typeof spawnSync): void {
96
+ // The child emits only this invocation's nonce and a fixed result, never paths/errors.
97
+ const script = `const fs = require("node:fs"); const path = ${JSON.stringify(path)};
98
+ let result = "create", owned;
99
+ try { fs.mkdirSync(path, 0o700); owned = fs.lstatSync(path); result = "remove";
100
+ fs.rmdirSync(path); result = "ok";
101
+ } catch { /* result already names the failed step; details never leave the child */ } finally {
102
+ if (owned) { try { const now = fs.lstatSync(path);
103
+ if (now.isDirectory() && !now.isSymbolicLink() && now.dev === owned.dev && now.ino === owned.ino
104
+ && fs.readdirSync(path).length === 0) fs.rmdirSync(path);
105
+ } catch { /* leave any probe entry that cannot be proven ours */ } }
106
+ }
107
+ process.stdout.write(${JSON.stringify(nonce)} + ":" + result);
108
+ process.exitCode = result === "ok" ? 0 : 1;`;
109
+ let child: ReturnType<typeof spawnSync>;
110
+ try {
111
+ child = spawn(runtime.path, ["-e", script], {
112
+ shell: false, windowsHide: true, timeout: RUNTIME_PREFLIGHT_TIMEOUT_MS,
113
+ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 1024,
114
+ });
115
+ } catch { throw new RuntimePreflightError("spawn"); }
116
+ if (child.error) {
117
+ throw new RuntimePreflightError((child.error as NodeJS.ErrnoException).code === "ETIMEDOUT" ? "timeout" : "spawn");
118
+ }
119
+ const ack = String(child.stdout ?? "");
120
+ if (ack === `${nonce}:create` && child.status === 1) throw new RuntimePreflightError("create");
121
+ if (ack === `${nonce}:remove` && child.status === 1) throw new RuntimePreflightError("remove");
122
+ if (ack !== `${nonce}:ok` || child.status !== 0) throw new RuntimePreflightError("protocol");
123
+ }
124
+
125
+ /** Windows admission of ONLY the already selected runtime. No discovery or memoization. */
126
+ export function assertSelectedRuntimeWritable(
127
+ runtime: DurableBunRuntime,
128
+ configDir: string,
129
+ deps: RuntimePreflightOptions = {},
130
+ ): void {
131
+ if ((deps.platform ?? process.platform) !== "win32") return;
132
+ if (runtime.source === "standalone" && runtime.path !== process.execPath) throw new RuntimePreflightError("protocol");
133
+ const fs = deps.fs ?? probeFs;
134
+ // The config root is durable: never delete it by pathname, even on refusal.
135
+ prepareRoot(fs, configDir, deps.rootWasAbsent);
136
+ const nonce = (deps.nonce ?? randomUUID)();
137
+ const path = join(configDir, `.ocx-runtime-probe-${nonce}`);
138
+ if (runtime.source === "standalone") probeInProcess(fs, path);
139
+ else probeChild(runtime, path, nonce, deps.spawnSync ?? spawnSync);
140
+ }
@@ -0,0 +1,20 @@
1
+ import type { spawn } from "node:child_process";
2
+ import type { accessSync, lstatSync, statSync } from "node:fs";
3
+ import type { DesktopCliRecordRead, DesktopCliRecordIssue } from "./desktop-cli-record.mjs";
4
+ export type DesktopHandoffIssue = DesktopCliRecordIssue | "target-invalid" | "target-self" | "target-unusable" | "spawn-failed";
5
+ export type DesktopCliHandoffPlan =
6
+ | { kind: "continue"; reason: "excluded" | "missing" | "disabled" | "target-missing" | "windows-path-only" }
7
+ | { kind: "error"; issue: DesktopHandoffIssue }
8
+ | { kind: "handoff"; target: string };
9
+ export type DesktopCliHandoffExit =
10
+ | { kind: "continue" }
11
+ | { kind: "error"; issue: "spawn-failed" }
12
+ | { kind: "exit"; code: number; signal: NodeJS.Signals | null };
13
+ export function desktopHandoffExcluded(argv: string[], env?: NodeJS.ProcessEnv): boolean;
14
+ export function planDesktopCliHandoff(input?: {
15
+ argv?: string[]; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform;
16
+ recordRead?: DesktopCliRecordRead; selfPaths?: (string | undefined)[];
17
+ }, deps?: { stat?: typeof statSync; access?: typeof accessSync; realpath?: (path: string) => string }): DesktopCliHandoffPlan;
18
+ export function runDesktopCliHandoff(plan: Extract<DesktopCliHandoffPlan, { kind: "handoff" }>, launch: {
19
+ argv: string[]; env: NodeJS.ProcessEnv; proof: string; context: string;
20
+ }, deps?: { spawn?: typeof spawn; lstat?: typeof lstatSync; parent?: Pick<NodeJS.Process, "on" | "removeListener">; platform?: NodeJS.Platform }): Promise<DesktopCliHandoffExit>;
@@ -0,0 +1,92 @@
1
+ import { spawn } from "node:child_process";
2
+ import { accessSync, constants, lstatSync, realpathSync, statSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ import { readDesktopCliRecord } from "./desktop-cli-record.mjs";
5
+ import { isCodexCliUpdateInspectionArgv } from "../update/codex-cli-update-launch-policy.mjs";
6
+
7
+ const PROOF_PREFIX = "--ocx-internal-launch-proof=";
8
+
9
+ export function desktopHandoffExcluded(argv, env = process.env) {
10
+ const args = argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
11
+ return env.OCX_NO_DESKTOP_HANDOFF === "1"
12
+ || ["update", "uninstall", "remove"].includes(args[0])
13
+ || (args[0] ?? "").startsWith("__")
14
+ || isCodexCliUpdateInspectionArgv(["node", "ocx.mjs", ...args]);
15
+ }
16
+
17
+ export function planDesktopCliHandoff(input = {}, deps = {}) {
18
+ const platform = input.platform ?? process.platform;
19
+ if (platform === "win32") return { kind: "continue", reason: "windows-path-only" };
20
+ const argv = input.argv ?? process.argv.slice(2);
21
+ const env = input.env ?? process.env;
22
+ if (desktopHandoffExcluded(argv, env)) return { kind: "continue", reason: "excluded" };
23
+ const read = input.recordRead ?? readDesktopCliRecord({ env, platform: input.platform });
24
+ if (read.state === "missing" || read.state === "disabled") {
25
+ return { kind: "continue", reason: read.state };
26
+ }
27
+ if (read.state !== "ready") return { kind: "error", issue: read.issue };
28
+ const api = platform === "win32" ? win32 : posix;
29
+ const target = read.record.cliExecutable;
30
+ if (!api.isAbsolute(target)) return { kind: "error", issue: "target-invalid" };
31
+ const stat = deps.stat ?? statSync;
32
+ const access = deps.access ?? accessSync;
33
+ const realpath = deps.realpath ?? realpathSync;
34
+ try {
35
+ if (!stat(target).isFile()) return { kind: "error", issue: "target-invalid" };
36
+ access(target, platform === "win32" ? constants.F_OK : constants.X_OK);
37
+ const physical = realpath(target);
38
+ const key = path => platform === "win32" ? path.toLowerCase() : path;
39
+ for (const self of input.selfPaths ?? [process.argv[1], process.execPath]) {
40
+ if (!self) continue;
41
+ let own;
42
+ try { own = realpath(self); }
43
+ catch (error) { if (error?.code === "ENOENT") continue; throw error; }
44
+ if (key(own) === key(physical)) return { kind: "error", issue: "target-self" };
45
+ }
46
+ return { kind: "handoff", target: physical };
47
+ } catch (error) {
48
+ return error?.code === "ENOENT"
49
+ ? { kind: "continue", reason: "target-missing" }
50
+ : { kind: "error", issue: "target-unusable" };
51
+ }
52
+ }
53
+
54
+ export function runDesktopCliHandoff(plan, launch, deps = {}) {
55
+ const spawnChild = deps.spawn ?? spawn;
56
+ const parent = deps.parent ?? process;
57
+ const platform = deps.platform ?? process.platform;
58
+ const lstat = deps.lstat ?? lstatSync;
59
+ const spawnFailure = error => {
60
+ if (error?.code === "ENOENT") {
61
+ try { lstat(plan.target); }
62
+ catch (checkError) { if (checkError?.code === "ENOENT") return { kind: "continue" }; }
63
+ }
64
+ return { kind: "error", issue: "spawn-failed" };
65
+ };
66
+ const argv = launch.argv.filter(arg => !arg.startsWith(PROOF_PREFIX));
67
+ const env = { ...launch.env, OCX_NO_DESKTOP_HANDOFF: "1", OCX_NODE_LAUNCH_CONTEXT: launch.context };
68
+ delete env.OCX_BUN_RUNTIME_SOURCE;
69
+ delete env.OCX_BUN_RUNTIME_PATH;
70
+ return new Promise(resolveExit => {
71
+ let child;
72
+ try {
73
+ child = spawnChild(plan.target, [`${PROOF_PREFIX}${launch.proof}`, ...argv], {
74
+ stdio: "inherit", shell: false, windowsHide: true, env,
75
+ });
76
+ } catch (error) {
77
+ resolveExit(spawnFailure(error));
78
+ return;
79
+ }
80
+ const forwarded = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
81
+ const handlers = forwarded.map(signal => {
82
+ const handler = () => { try { child.kill(signal); } catch { /* child already exited */ } };
83
+ parent.on(signal, handler);
84
+ return [signal, handler];
85
+ });
86
+ const clear = () => { for (const [signal, handler] of handlers) parent.removeListener(signal, handler); };
87
+ let done = false;
88
+ const finish = result => { if (done) return; done = true; clear(); resolveExit(result); };
89
+ child.once("error", error => finish(spawnFailure(error)));
90
+ child.once("exit", (code, signal) => finish({ kind: "exit", code: code ?? 1, signal }));
91
+ });
92
+ }
@@ -0,0 +1,29 @@
1
+ import type { Stats } from "node:fs";
2
+
3
+ export const DESKTOP_CLI_RECORD_MAX_BYTES: number;
4
+ export type DesktopCliRecord = Readonly<{
5
+ platform: "darwin" | "win32" | "linux";
6
+ kind: "macos-app" | "windows-install" | "linux-deb";
7
+ cliExecutable: string;
8
+ }>;
9
+ export type DesktopCliRecordIssue = "record-invalid" | "record-too-large" | "record-pending" | "record-unreadable" | "record-unsafe";
10
+ export type DesktopCliRecordRead =
11
+ | { state: "missing"; path: string }
12
+ | { state: "disabled"; path: string; cleanupPending: boolean }
13
+ | { state: "ready"; path: string; record: DesktopCliRecord }
14
+ | { state: "invalid" | "unreadable"; path: string; issue: DesktopCliRecordIssue };
15
+ export type DesktopCliRecordOptions = {
16
+ platform?: NodeJS.Platform;
17
+ env?: NodeJS.ProcessEnv;
18
+ home?: string;
19
+ recordPath?: string;
20
+ };
21
+ export function desktopCliRecordPath(options?: DesktopCliRecordOptions): string;
22
+ export type DesktopCliRecordDeps = {
23
+ lstat?: (path: string) => Stats;
24
+ fstat?: (fd: number) => Stats;
25
+ open?: (path: string, flags: number) => number;
26
+ euid?: number;
27
+ checkAcl?: (directory: string, path: string) => boolean;
28
+ };
29
+ export function readDesktopCliRecord(options?: DesktopCliRecordOptions, deps?: DesktopCliRecordDeps): DesktopCliRecordRead;