@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
@@ -0,0 +1,32 @@
1
+ import { createSupervisionLatch, inspectDesktopSupervision } from "./desktop-supervision.mjs";
2
+
3
+ export { createSupervisionLatch };
4
+ export type DesktopSupervision = ReturnType<typeof inspectDesktopSupervision>;
5
+ export type SupervisionInspector = (deps?: Parameters<typeof inspectDesktopSupervision>[0]) => DesktopSupervision;
6
+ export type SupervisionLatch = ReturnType<typeof createSupervisionLatch>;
7
+
8
+ export function desktopServiceRefusal(evidence: DesktopSupervision, blocked: boolean): string | null {
9
+ if (!blocked) return null;
10
+ const runtime = evidence.kind === "desktop" ? ` (pid ${evidence.runtimePid}, ${evidence.app})` : "";
11
+ return `OpenCodex Desktop supervises the running proxy${runtime}. `
12
+ + "Quit OpenCodex, then rerun 'ocx service install' to move startup management to this CLI.";
13
+ }
14
+
15
+ export function assertNoDesktopSupervision(
16
+ inspect: SupervisionInspector = inspectDesktopSupervision,
17
+ latch: SupervisionLatch = createSupervisionLatch(),
18
+ ): void {
19
+ const evidence = inspect();
20
+ const refusal = desktopServiceRefusal(evidence, latch.observe(evidence));
21
+ if (refusal) throw new Error(refusal);
22
+ }
23
+
24
+ export function desktopServiceCommandRefusal(
25
+ command: string,
26
+ inspect: SupervisionInspector = inspectDesktopSupervision,
27
+ latch: SupervisionLatch = createSupervisionLatch(),
28
+ ): string | null {
29
+ if (!["install", "repair", "start", "restart"].includes(command)) return null;
30
+ const evidence = inspect();
31
+ return desktopServiceRefusal(evidence, latch.observe(evidence));
32
+ }
@@ -1,9 +1,10 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { readPid } from "../config/process-state";
6
6
  import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
7
+ import { inspectDesktopSupervision, processIdentity, procfs, type ProcReader } from "./desktop-supervision.mjs";
7
8
 
8
9
  export interface DesktopStartupDiagnostic {
9
10
  /** Durable desktop claim; failed supervision does not release ownership. */
@@ -11,11 +12,13 @@ export interface DesktopStartupDiagnostic {
11
12
  loginEnabled: boolean;
12
13
  running: boolean;
13
14
  viable: boolean;
15
+ /** Live parentage when no durable claim exists; never converts supervision into ownership. */
16
+ supervisor?: { supervisorPid: number; runtimePid: number; app: string };
14
17
  }
15
18
 
16
- /** A login item alone is insufficient: the same app must own and supervise this proxy. */
19
+ /** A login item alone is insufficient: the same app must supervise this proxy. */
17
20
  export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viable">): DesktopStartupDiagnostic {
18
- return { ...facts, viable: facts.owned && facts.loginEnabled && facts.running };
21
+ return { ...facts, viable: (facts.owned || facts.supervisor !== undefined) && facts.loginEnabled && facts.running };
19
22
  }
20
23
 
21
24
  function run(command: string, args: string[]): string {
@@ -28,25 +31,12 @@ interface DesktopStartupDeps {
28
31
  uid?: number;
29
32
  ownership?: () => ServiceOwnershipResolution;
30
33
  readPid?: () => number | null;
34
+ readRuntimePortPid?: () => number | null;
31
35
  run?: typeof run;
32
36
  env?: NodeJS.ProcessEnv;
33
37
  proc?: ProcReader;
34
38
  }
35
39
 
36
- /** Linux process identity from procfs: executable target and parent pid. */
37
- interface ProcReader {
38
- exe(pid: number): string;
39
- parent(pid: number): number;
40
- }
41
- const procfs: ProcReader = {
42
- exe: pid => readlinkSync(`/proc/${pid}/exe`),
43
- // comm may contain spaces or parentheses, so fields are counted after the last ')'.
44
- parent: pid => {
45
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
46
- return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
47
- },
48
- };
49
-
50
40
  /** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
51
41
  export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
52
42
  const platform = deps.platform ?? process.platform;
@@ -56,9 +46,47 @@ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopS
56
46
  ? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
57
47
  }
58
48
 
59
- function processIdentity(pid: number, execute: typeof run): { parent: number; executable: string } | null {
60
- const row = /^(\d+)\s+(.+)$/.exec(execute("/bin/ps", ["-p", String(pid), "-o", "ppid=,comm="]));
61
- return row ? { parent: Number(row[1]), executable: realpathSync(row[2]!) } : null;
49
+ function macLoginRegistration(deps: DesktopStartupDeps) {
50
+ const execute = deps.run ?? run;
51
+ const path = join(deps.home ?? homedir(), "Library", "LaunchAgents", "OpenCodex.plist");
52
+ const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
53
+ const args = plist.ProgramArguments;
54
+ if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
55
+ || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
56
+ || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
57
+ || (plist.Program !== undefined && plist.Program !== args[0])) return null;
58
+ const app = realpathSync(args[0]);
59
+ const proxy = realpathSync(join(dirname(app), "ocx"));
60
+ accessSync(app, constants.X_OK);
61
+ accessSync(proxy, constants.X_OK);
62
+ const domain = `gui/${deps.uid ?? process.getuid!()}`;
63
+ const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
64
+ const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
65
+ const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
66
+ const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
67
+ const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
68
+ && !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
69
+ && program !== undefined && realpathSync(program) === app
70
+ && loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
71
+ return { app, proxy, loginEnabled };
72
+ }
73
+
74
+ function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => boolean): DesktopStartupDiagnostic | undefined {
75
+ const evidence = inspectDesktopSupervision(deps);
76
+ if (evidence.kind !== "desktop") return undefined;
77
+ let loginEnabled = false;
78
+ try { loginEnabled = loginFor(evidence.app); }
79
+ catch { /* Login failure revokes restart viability, not the observed live supervisor. */ }
80
+ if ((deps.ownership ?? resolveServiceOwnership)().kind !== "none") return undefined;
81
+ // Login probes can outlive the runtime or its parent; only the same live chain gets credit.
82
+ const final = inspectDesktopSupervision({ ...deps, targetPid: evidence.runtimePid });
83
+ if (final.kind !== "desktop" || final.runtimePid !== evidence.runtimePid
84
+ || final.supervisorPid !== evidence.supervisorPid || final.app !== evidence.app) return undefined;
85
+ // A new claim or revision during the last process probe invalidates the unowned projection.
86
+ const finalOwner = (deps.ownership ?? resolveServiceOwnership)();
87
+ if (finalOwner.kind !== "none" || finalOwner.revision !== ownerRevision) return undefined;
88
+ const { supervisorPid, runtimePid, app } = evidence;
89
+ return deriveDesktopStartup({ owned: false, loginEnabled, running: true, supervisor: { supervisorPid, runtimePid, app } });
62
90
  }
63
91
 
64
92
  /** Read-only macOS desktop ownership, login registration and live parent/child checks. */
@@ -66,6 +94,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
66
94
  if ((deps.platform ?? process.platform) !== "darwin") return undefined;
67
95
  const ownership = deps.ownership ?? resolveServiceOwnership;
68
96
  const owner = ownership();
97
+ if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
98
+ const login = macLoginRegistration(deps);
99
+ return login !== null && login.app === app && login.loginEnabled;
100
+ });
69
101
  if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
70
102
  const facts = { owned: true, loginEnabled: false, running: false };
71
103
  const execute = deps.run ?? run;
@@ -74,26 +106,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
74
106
  const home = deps.home ?? homedir();
75
107
  const id = readFileSync(join(home, "Library", "Application Support", "com.opencodex.desktop", "install-id"), "utf8").trim();
76
108
  if (id !== owner.ownership.installId) return deriveDesktopStartup(facts);
77
- const path = join(home, "Library", "LaunchAgents", "OpenCodex.plist");
78
- const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
79
- const args = plist.ProgramArguments;
80
- if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
81
- || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
82
- || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
83
- || (plist.Program !== undefined && plist.Program !== args[0])) return deriveDesktopStartup(facts);
84
- const app = realpathSync(args[0]);
85
- const proxy = realpathSync(join(dirname(app), "ocx"));
86
- accessSync(app, constants.X_OK);
87
- accessSync(proxy, constants.X_OK);
88
- const domain = `gui/${deps.uid ?? process.getuid!()}`;
89
- const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
90
- const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
91
- const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
92
- const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
93
- facts.loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
94
- && !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
95
- && program !== undefined && realpathSync(program) === app
96
- && loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
109
+ const login = macLoginRegistration(deps);
110
+ if (!login) return deriveDesktopStartup(facts);
111
+ const { app, proxy } = login;
112
+ facts.loginEnabled = login.loginEnabled;
97
113
  const pid = pidReader();
98
114
  if (pid !== null) {
99
115
  const child = processIdentity(pid, execute);
@@ -135,6 +151,16 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
135
151
  if ((deps.platform ?? process.platform) !== "linux") return undefined;
136
152
  const ownership = deps.ownership ?? resolveServiceOwnership;
137
153
  const owner = ownership();
154
+ if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
155
+ const home = deps.home ?? homedir();
156
+ const configured = (deps.env ?? process.env).XDG_CONFIG_HOME;
157
+ const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
158
+ // auto-launch's HOME entry must remain inside the session's XDG autostart search path.
159
+ if (resolve(config) !== resolve(home, ".config")) return false;
160
+ const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
161
+ const login = linuxLoginApp(entry);
162
+ return login !== null && realpathSync(login) === app;
163
+ });
138
164
  if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
139
165
  const facts = { owned: true, loginEnabled: false, running: false };
140
166
  const proc = deps.proc ?? procfs;
@@ -0,0 +1,26 @@
1
+ export type SupervisionEvidence =
2
+ | { kind: "desktop"; runtimePid: number; supervisorPid: number; app: string; proxy: string }
3
+ | { kind: "none" }
4
+ | { kind: "unknown"; reason: string; desktopSeen: boolean }
5
+ | { kind: "unsupported" };
6
+
7
+ export interface ProcReader {
8
+ exe(pid: number): string;
9
+ parent(pid: number): number;
10
+ }
11
+
12
+ export interface DesktopSupervisionDeps {
13
+ targetPid?: number;
14
+ platform?: NodeJS.Platform;
15
+ run?: (command: string, args: string[]) => string;
16
+ proc?: ProcReader;
17
+ readPid?: () => number | null;
18
+ readRuntimePortPid?: () => number | null;
19
+ }
20
+
21
+ export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
22
+ export const procfs: ProcReader;
23
+ export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
24
+ { parent: number; executable: string } | null;
25
+ export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
26
+ export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };
@@ -0,0 +1,140 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+
6
+ // Node >=18 also imports this module from bin/ocx.mjs; do not import TS or Bun APIs.
7
+ function run(command, args) {
8
+ return execFileSync(command, args, {
9
+ encoding: "utf8", timeout: 750, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"],
10
+ }).trim();
11
+ }
12
+
13
+ // Mirror src/config/paths.ts getConfigDir/expandUserPath and process-state.ts paths.
14
+ // Keep this Node-safe copy in sync; the focused startup test pins both implementations.
15
+ export function desktopSupervisionPaths() {
16
+ const raw = process.env.OPENCODEX_HOME?.trim();
17
+ const expanded = raw === "~" ? homedir()
18
+ : raw?.startsWith("~/") || raw?.startsWith("~\\") ? join(homedir(), raw.slice(2)) : raw;
19
+ const home = expanded ? resolve(expanded) : join(homedir(), ".opencodex");
20
+ return { pid: join(home, "ocx.pid"), runtimePort: join(home, "runtime-port.json") };
21
+ }
22
+
23
+ function validPid(pid) {
24
+ return Number.isSafeInteger(pid) && pid > 0;
25
+ }
26
+
27
+ function readState(path, parse) {
28
+ try { return parse(readFileSync(path, "utf8")); }
29
+ catch (error) {
30
+ if (error?.code === "ENOENT") return null;
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ function readPidFile() {
36
+ return readState(desktopSupervisionPaths().pid, raw => {
37
+ const text = raw.trim();
38
+ if (!/^\d+$/.test(text) || !validPid(Number(text))) throw new Error("invalid pid");
39
+ return Number(text);
40
+ });
41
+ }
42
+
43
+ function readRuntimePortPid() {
44
+ return readState(desktopSupervisionPaths().runtimePort, raw => {
45
+ // Only project pid: no credential validation, serialization or returned port state.
46
+ const pid = JSON.parse(raw)?.pid;
47
+ if (!validPid(pid)) throw new Error("invalid runtime pid");
48
+ return pid;
49
+ });
50
+ }
51
+
52
+ /** Linux identity fields count from the end of comm, which may contain spaces or ')'. */
53
+ export const procfs = {
54
+ exe: pid => readlinkSync(`/proc/${pid}/exe`),
55
+ parent: pid => {
56
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
57
+ return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
58
+ },
59
+ };
60
+
61
+ export function processIdentity(pid, execute = run, onExecutable) {
62
+ const text = execute("/bin/ps", ["-p", String(pid), "-o", "ppid=,comm="]).trim();
63
+ const row = /^(\d+)\s+(.+)$/.exec(text);
64
+ if (!row) {
65
+ if (text) throw new Error("invalid process identity");
66
+ return null;
67
+ }
68
+ onExecutable?.(row[2]);
69
+ return { parent: Number(row[1]), executable: realpathSync(row[2]) };
70
+ }
71
+
72
+ /** Live supervision is evidence, never a durable ownership claim. */
73
+ export function inspectDesktopSupervision(deps = {}) {
74
+ const platform = deps.platform ?? process.platform;
75
+ if (platform !== "darwin" && platform !== "linux") return { kind: "unsupported" };
76
+ let desktopSeen = false;
77
+ const unknown = reason => ({ kind: "unknown", reason, desktopSeen });
78
+ const seen = executable => { if (basename(executable) === "opencodex-desktop") desktopSeen = true; };
79
+ const readPid = deps.readPid ?? readPidFile;
80
+ const readPortPid = deps.readRuntimePortPid ?? readRuntimePortPid;
81
+ const proc = deps.proc ?? procfs;
82
+ const execute = deps.run ?? run;
83
+ function identity(pid, parent = false) {
84
+ if (platform === "darwin") return processIdentity(pid, execute, parent ? seen : undefined);
85
+ const executable = proc.exe(pid);
86
+ if (parent) seen(executable);
87
+ const parentPid = proc.parent(pid);
88
+ if (!Number.isSafeInteger(parentPid) || parentPid < 0) throw new Error("invalid parent pid");
89
+ return { parent: parentPid, executable: realpathSync(executable) };
90
+ }
91
+ function capture() {
92
+ const pidFile = readPid();
93
+ const portPid = readPortPid();
94
+ const pids = [deps.targetPid, pidFile, portPid].filter(pid => pid !== undefined && pid !== null);
95
+ if (pids.some(pid => !validPid(pid))) return { error: "invalid-pid" };
96
+ if (pids.some(pid => pid !== pids[0])) return { error: "pid-mismatch" };
97
+ const runtimePid = pids[0] ?? null;
98
+ const child = runtimePid !== null ? identity(runtimePid) : null;
99
+ const parent = child && child.parent > 1 ? identity(child.parent, true) : null;
100
+ const snapshot = { pidFile, portPid, runtimePid, child, parent };
101
+ if (!runtimePid || !child || !parent || basename(parent.executable) !== "opencodex-desktop") {
102
+ return { ...snapshot, kind: "none" };
103
+ }
104
+ seen(parent.executable);
105
+ const app = parent.executable;
106
+ if (platform === "darwin" && (basename(dirname(app)) !== "MacOS"
107
+ || basename(dirname(dirname(app))) !== "Contents")) return { error: "app-mismatch" };
108
+ const proxy = realpathSync(join(dirname(app), "ocx"));
109
+ if (child.executable !== proxy || (platform === "linux"
110
+ && (dirname(proxy) !== dirname(app) || basename(proxy) !== "ocx"))) return { error: "proxy-mismatch" };
111
+ accessSync(app, constants.X_OK);
112
+ accessSync(proxy, constants.X_OK);
113
+ return { ...snapshot, kind: "desktop", app, proxy };
114
+ }
115
+ try {
116
+ const first = capture();
117
+ if (first.error) return unknown(first.error);
118
+ const final = capture();
119
+ if (final.error) return unknown(final.error);
120
+ if (JSON.stringify(first) !== JSON.stringify(final)) return unknown("snapshot-changed");
121
+ return final.kind === "desktop"
122
+ ? { kind: "desktop", runtimePid: final.runtimePid, supervisorPid: final.child.parent, app: final.app, proxy: final.proxy }
123
+ : { kind: "none" };
124
+ } catch {
125
+ // Error messages can contain paths or file contents; expose only the bounded reason.
126
+ return unknown("probe-failed");
127
+ }
128
+ }
129
+
130
+ /** One operation retains its Desktop veto through inconclusive later probes. */
131
+ export function createSupervisionLatch() {
132
+ let blocked = false;
133
+ return {
134
+ observe(evidence) {
135
+ if (evidence.kind === "none") blocked = false;
136
+ else if (evidence.kind === "desktop" || (evidence.kind === "unknown" && evidence.desktopSeen)) blocked = true;
137
+ return blocked;
138
+ },
139
+ };
140
+ }
@@ -81,6 +81,7 @@ function probeVersionOnce(
81
81
  windowsShim ? ["/c", shimCommand] : [...args, "--version"],
82
82
  {
83
83
  timeout: VERSION_PROBE_TIMEOUT_MS, encoding: "utf8", stdio: "pipe",
84
+ env: { ...deps.env, OCX_NO_DESKTOP_HANDOFF: "1" },
84
85
  windowsHide: true, ...(windowsShim ? { windowsVerbatimArguments: true } : {}),
85
86
  },
86
87
  ) as SpawnSyncReturns<string>;
@@ -1,5 +1,7 @@
1
1
  import { findLiveProxy, SERVICE_STOP_LIVENESS } from "../server/proxy-liveness";
2
2
  import { existsSync, unlinkSync } from "node:fs";
3
+ import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
4
+ import { assertSelectedRuntimeWritable, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
3
5
  import { getConfigDir } from "../config";
4
6
  import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "../config/process-state";
5
7
  import { isWslRuntime } from "../codex/home";
@@ -14,16 +16,17 @@ import { assertSchedulerRegistrationBeforeStart } from "./repair";
14
16
  import { SERVICE_MANAGED_ENV, TASK, plistPath, removeServiceInstallStateRecords, writeServiceInstallState } from "./state";
15
17
  import type { ServiceBackend } from "./state";
16
18
  import { unitPath, isSystemd, installSystemd, startSystemd, stopSystemd, statusSystemd, uninstallSystemd, systemdServiceInstallCleanupOps } from "./systemd";
17
- import { writeWindowsSchedulerAssets, stageWindowsSchedulerRegistrationXml, removeWindowsSchedulerRegistrationStage, registerFreshWindowsSchedulerTask, recordWindowsSchedulerOwnership, removeNativeWindowsServiceForScheduler, installWindows, installWindowsNative, startWindows, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, statusWindows, statusWindowsXml, killWindowsServiceWrapperProcesses, uninstallWindows, classifyWindowsServiceStop } from "./windows-ops";
19
+ import { writeWindowsSchedulerAssets, stageWindowsSchedulerRegistrationXml, removeWindowsSchedulerRegistrationStage, registerFreshWindowsSchedulerTask, recordWindowsSchedulerOwnership, removeNativeWindowsServiceForScheduler, commitWindowsInstall, commitWindowsNativeInstall, type WindowsServiceInstallDeps, startWindows, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, statusWindows, statusWindowsXml, killWindowsServiceWrapperProcesses, uninstallWindows, classifyWindowsServiceStop } from "./windows-ops";
18
20
  import { schtasks, probeWindowsSchedulerTask, rollbackWindowsSchedulerTaskOwnedByAttempt, settleDelay } from "./windows-scheduler";
19
21
  import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
20
22
  import { windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
21
23
  import { win32 } from "node:path";
22
24
  import { launchdGuiDomain } from "./launchd";
23
25
  import { LABEL } from "./state";
26
+ import { assertNoDesktopSupervision, createSupervisionLatch, type SupervisionInspector, type SupervisionLatch } from "./desktop-command-guard";
24
27
 
25
28
  type ServiceOps = {
26
- install: () => void | Promise<void>; start: () => void; stop: () => void;
29
+ install: (runtime: DurableBunRuntime) => void | Promise<void>; start: () => void; stop: () => void;
27
30
  status: () => string; uninstall: () => void;
28
31
  };
29
32
 
@@ -32,18 +35,19 @@ export type ServiceInstallCleanupOps = {
32
35
  stop: () => void;
33
36
  };
34
37
 
35
- export function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
36
- if (process.platform === "darwin")
38
+ export function platformOps(backend: ServiceBackend = "scheduler", deps: { platform?: NodeJS.Platform; windowsInstall?: WindowsServiceInstallDeps } = {}): ServiceOps | null {
39
+ const platform = deps.platform ?? process.platform;
40
+ if (platform === "darwin")
37
41
  // Wrapped, not passed: `installLaunchd` reports whether it reloaded launchd, and only
38
42
  // `repairService` (for the `restart` verb) has any use for that. `ServiceOps.install` is
39
43
  // the generic install seam and deliberately promises nothing about a return value.
40
44
  return { install: () => { installLaunchd(); }, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
41
- if (process.platform === "win32") {
45
+ if (platform === "win32") {
42
46
  if (backend === "native")
43
- return { install: installWindowsNative, start: startWinswService, stop: stopWinswService, status: winswStatusSummary, uninstall: uninstallWinswService };
44
- return { install: installWindows, start: startWindows, stop: stopWindows, status: statusWindows, uninstall: uninstallWindows };
47
+ return { install: runtime => commitWindowsNativeInstall(runtime, deps.windowsInstall), start: startWinswService, stop: stopWinswService, status: winswStatusSummary, uninstall: uninstallWinswService };
48
+ return { install: runtime => commitWindowsInstall(runtime, deps.windowsInstall), start: startWindows, stop: stopWindows, status: statusWindows, uninstall: uninstallWindows };
45
49
  }
46
- if (process.platform === "linux") {
50
+ if (platform === "linux") {
47
51
  if (existsSync("/.dockerenv")) {
48
52
  console.error("Docker detected. Run 'ocx start' directly instead of using the service manager.");
49
53
  process.exit(1);
@@ -229,7 +233,9 @@ export async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyC
229
233
  }
230
234
  }
231
235
 
232
- export interface ServiceInstallPreparationDeps {
236
+ export interface ServiceInstallPreparationDeps extends RuntimePreflightDeps {
237
+ inspectSupervision?: SupervisionInspector;
238
+ supervisionLatch?: SupervisionLatch;
233
239
  diagnose?: () => ServiceDiagnostic;
234
240
  managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
235
241
  stopTrackedProxy?: () => Promise<unknown>;
@@ -245,6 +251,8 @@ export async function prepareServiceInstall(
245
251
  requestedBackend: ServiceBackend,
246
252
  deps: ServiceInstallPreparationDeps = {},
247
253
  ): Promise<void> {
254
+ const latch = deps.supervisionLatch ?? createSupervisionLatch();
255
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
248
256
  const diagnostic = (deps.diagnose ?? diagnoseService)();
249
257
  const platform = deps.platform ?? process.platform;
250
258
  const resolveOps = deps.managerOps ?? platformServiceInstallCleanupOps;
@@ -276,27 +284,35 @@ export async function prepareServiceInstall(
276
284
 
277
285
  export async function installServiceSafely(
278
286
  requestedBackend: ServiceBackend,
279
- install: () => void | Promise<void>,
287
+ install: (runtime: DurableBunRuntime) => void | Promise<void>,
280
288
  deps: ServiceInstallPreparationDeps = {},
281
289
  ): Promise<void> {
282
- await prepareServiceInstall(requestedBackend, deps);
283
- await install();
290
+ const latch = deps.supervisionLatch ?? createSupervisionLatch();
291
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
292
+ const runtime = Object.freeze({ ...(deps.selectRuntime ?? durableBunRuntime)() });
293
+ const configDir = (deps.configDir ?? getConfigDir)();
294
+ (deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: !existsSync(configDir) });
295
+ await prepareServiceInstall(requestedBackend, { ...deps, supervisionLatch: latch });
296
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
297
+ await install(runtime);
284
298
  }
285
299
 
286
- export interface FreshWindowsSchedulerInstallDeps {
300
+ export interface FreshWindowsSchedulerInstallDeps extends RuntimePreflightDeps {
301
+ inspectSupervision?: SupervisionInspector;
302
+ supervisionLatch?: SupervisionLatch;
287
303
  stageRegistrationXml?: (attemptNonce: string) => string;
288
304
  register?: (xmlPath: string, attemptNonce: string) => Promise<void>;
289
305
  recordOwnership?: () => boolean;
290
306
  prepare?: () => Promise<void>;
291
307
  removeNativeService?: () => void;
292
- publishAssets?: () => void;
308
+ publishAssets?: (runtime: DurableBunRuntime) => void;
293
309
  verifyBeforeRun?: (attemptNonce: string) => void | Promise<void>;
294
310
  /** Reads the newly registered task; empty or throwing reads are retried before rollback. */
295
311
  readSchedulerXml?: () => string;
296
312
  /** Bounded wait before retrying an unreadable fresh-install registration. */
297
313
  settleSchedulerRead?: (delayMs: number) => void | Promise<void>;
298
314
  runTask?: () => void;
299
- writeState?: () => void;
315
+ writeState?: (runtime: DurableBunRuntime) => void;
300
316
  rollbackTask?: (attemptNonce: string) => Promise<string | null>;
301
317
  removeStagedXml?: (xmlPath: string) => void;
302
318
  }
@@ -312,10 +328,14 @@ export interface FreshWindowsSchedulerInstallDeps {
312
328
  export async function installFreshWindowsSchedulerSafely(
313
329
  deps: FreshWindowsSchedulerInstallDeps = {},
314
330
  ): Promise<void> {
331
+ const latch = deps.supervisionLatch ?? createSupervisionLatch();
332
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
315
333
  const stage = deps.stageRegistrationXml ?? stageWindowsSchedulerRegistrationXml;
316
334
  const register = deps.register ?? registerFreshWindowsSchedulerTask;
317
335
  const recordOwnership = deps.recordOwnership ?? recordWindowsSchedulerOwnership;
318
- const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler"));
336
+ const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler", {
337
+ inspectSupervision: deps.inspectSupervision, supervisionLatch: latch,
338
+ }));
319
339
  const removeNativeService = deps.removeNativeService ?? removeNativeWindowsServiceForScheduler;
320
340
  const publishAssets = deps.publishAssets ?? writeWindowsSchedulerAssets;
321
341
  const verifyBeforeRun = deps.verifyBeforeRun ?? ((nonce: string) => (
@@ -331,7 +351,7 @@ export async function installFreshWindowsSchedulerSafely(
331
351
  )
332
352
  ));
333
353
  const runTask = deps.runTask ?? startWindows;
334
- const writeState = deps.writeState ?? (() => writeServiceInstallState("scheduler"));
354
+ const writeState = deps.writeState ?? ((runtime: DurableBunRuntime) => writeServiceInstallState("scheduler", undefined, {}, runtime));
335
355
  const rollbackTask = deps.rollbackTask ?? ((attemptNonce: string) => (
336
356
  rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK)
337
357
  ));
@@ -341,13 +361,16 @@ export async function installFreshWindowsSchedulerSafely(
341
361
 
342
362
  let stagedXml: string | null = null;
343
363
  const attemptNonce = randomUUID();
344
- const configRootWasAbsent = !existsSync(getConfigDir());
364
+ const configDir = (deps.configDir ?? getConfigDir)();
365
+ const configRootWasAbsent = !existsSync(configDir);
366
+ const runtime = Object.freeze({ ...(deps.selectRuntime ?? durableBunRuntime)() });
345
367
  let registered = false;
346
368
  let started = false;
347
369
  try {
348
370
  stagedXml = stage(attemptNonce);
349
371
  await register(stagedXml, attemptNonce);
350
372
  registered = true;
373
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
351
374
 
352
375
  // The destructive boundary begins only after Task Scheduler accepted the definition.
353
376
  // The registration has consumed its temporary XML. Remove it before claiming a newly
@@ -361,13 +384,16 @@ export async function installFreshWindowsSchedulerSafely(
361
384
  + "aborting before service-manager cleanup or asset publication.",
362
385
  );
363
386
  }
387
+ (deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: false });
364
388
  await prepare();
389
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
365
390
  removeNativeService();
366
- publishAssets();
391
+ publishAssets(runtime);
367
392
  await verifyBeforeRun(attemptNonce);
393
+ assertNoDesktopSupervision(deps.inspectSupervision, latch);
368
394
  runTask();
369
395
  started = true;
370
- writeState();
396
+ writeState(runtime);
371
397
  } catch (error) {
372
398
  const detail = error instanceof Error ? error.message : String(error);
373
399
  if (registered && !started) {
@@ -5,7 +5,7 @@ export interface OwnershipMutationLeaseOptions {
5
5
  readonly processAlive?: (pid: number) => boolean;
6
6
  readonly beforeRelease?: (lockPath: string) => void;
7
7
  readonly joinToken?: string;
8
- /** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */
8
+ /** Names the current executable at the recorded PID, identity unverified; uses `ps` on POSIX and omits the default image lookup on Windows. */
9
9
  readonly processImage?: (pid: number) => string | null;
10
10
  }
11
11
 
@@ -112,13 +112,11 @@ function readIncompleteOwner(path) {
112
112
  /** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */
113
113
  function processImage(pid) {
114
114
  try {
115
- if (process.platform === "win32") {
116
- const listed = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
117
- encoding: "utf8", timeout: 1_000, windowsHide: true,
118
- });
119
- const match = /^"([^"]+)","(\d+)"/.exec((listed.stdout ?? "").trim());
120
- return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null;
121
- }
115
+ // This module also runs in the Node npm/pnpm launcher, before Bun is available.
116
+ // Without a kernel-backed system-directory resolver, omit the optional Windows
117
+ // image label rather than searching the caller's directory/PATH or trusting env roots.
118
+ // PID, liveness, age and lease ownership decisions do not depend on this label.
119
+ if (process.platform === "win32") return null;
122
120
  const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 });
123
121
  const command = (listed.stdout ?? "").trim();
124
122
  return listed.status === 0 && command ? basename(command) : null;