@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,892 @@
1
+ /**
2
+ * What the service manager says about this machine, read-only and fail-closed.
3
+ *
4
+ * The question this answers is NOT "is a job loaded". Installation writes the
5
+ * definition BEFORE the state file (`service.ts` install paths) and embeds
6
+ * `CODEX_HOME`/`OPENCODEX_HOME` inside it, so an interrupted reinstall leaves a
7
+ * valid state file for one home beside an installed definition for another. A
8
+ * probe that only asked about registration would call that owned. On macOS it is
9
+ * worse: a logged-out user has the plist on disk with no GUI domain at all, so
10
+ * registration reports nothing while a foreign definition sits right there.
11
+ *
12
+ * So the probe reads the DEFINITION, parses the homes out of it, and reports
13
+ * what it saw. Comparing those homes is the caller's job — a probe that returned
14
+ * a verdict would be deciding ownership from half the evidence.
15
+ *
16
+ * Every command here is read-only and bounded. The user's proxy runs under a
17
+ * service manager while this executes; a probe that could start, stop or reload
18
+ * anything is not a probe. An unbounded one is not much better, since a wedged
19
+ * manager would hold the event loop.
20
+ */
21
+ import { spawnSync } from "node:child_process";
22
+ import { existsSync, lstatSync, readFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join, win32 as win32Path } from "node:path";
25
+ import {
26
+ resolveTrustedWindowsSchtasksExe,
27
+ resolveTrustedWindowsSystemDirectory,
28
+ } from "./lib/windows-elevation";
29
+ import { decodeWindowsTextBytes } from "./lib/windows-text";
30
+ import { WINSW_SERVICE_ID } from "./lib/winsw";
31
+
32
+ /** Short: this runs inside admission, and a slow answer is the same as none. */
33
+ export const SERVICE_PROBE_TIMEOUT_MS = 2_000;
34
+
35
+ export type ServiceManagerBackend = "launchd" | "systemd" | "scheduler" | "winsw";
36
+
37
+ export interface ServiceManagerClaim {
38
+ readonly backend: ServiceManagerBackend;
39
+ readonly definitionPath: string;
40
+ /**
41
+ * The homes the definition names. `null` means the definition deliberately
42
+ * omits that key, which is different from naming a different one: an install
43
+ * that ran without `CODEX_HOME` set writes no such key at all.
44
+ */
45
+ readonly homes: {
46
+ readonly codexHome: string | null;
47
+ readonly opencodexHome: string | null;
48
+ };
49
+ readonly registration: "present" | "absent";
50
+ }
51
+
52
+ export type ServiceManagerInstallation =
53
+ | { readonly kind: "absent" }
54
+ | { readonly kind: "present"; readonly claims: readonly ServiceManagerClaim[] }
55
+ | { readonly kind: "conflict"; readonly claims: readonly ServiceManagerClaim[] }
56
+ | { readonly kind: "unknown"; readonly reason: string };
57
+
58
+ /** Injected so a test can observe the EXACT argv production emits. */
59
+ export interface ProbeRunner {
60
+ (file: string, args: readonly string[]): {
61
+ status: number | null;
62
+ stdout: string;
63
+ stderr: string;
64
+ timedOut: boolean;
65
+ spawnFailed: boolean;
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Windows probe runner: preserves schtasks stdout/stderr as raw bytes so the
71
+ * UTF-16LE task XML is not corrupted by a UTF-8 decode.
72
+ */
73
+ export type RawProbeRunner = (file: string, args: readonly string[]) => {
74
+ status: number | null;
75
+ stdout: Buffer;
76
+ stderr: Buffer;
77
+ timedOut: boolean;
78
+ spawnFailed: boolean;
79
+ };
80
+
81
+ export const defaultProbeRunner: ProbeRunner = (file, args) => {
82
+ const result = spawnSync(file, [...args], {
83
+ encoding: "utf8",
84
+ windowsHide: true,
85
+ timeout: SERVICE_PROBE_TIMEOUT_MS,
86
+ });
87
+ return {
88
+ status: result.status,
89
+ stdout: String(result.stdout ?? ""),
90
+ stderr: String(result.stderr ?? ""),
91
+ // `signal` is SIGTERM when the timeout fired; a spawn failure sets `error`.
92
+ timedOut: result.signal !== null && result.error === undefined,
93
+ spawnFailed: result.error !== undefined,
94
+ };
95
+ };
96
+
97
+ export const defaultRawProbeRunner: RawProbeRunner = (file, args) => {
98
+ const result = spawnSync(file, [...args], {
99
+ encoding: "buffer",
100
+ windowsHide: true,
101
+ timeout: SERVICE_PROBE_TIMEOUT_MS,
102
+ });
103
+ return {
104
+ status: result.status,
105
+ stdout: Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.alloc(0),
106
+ stderr: Buffer.isBuffer(result.stderr) ? result.stderr : Buffer.alloc(0),
107
+ timedOut: result.signal !== null && result.error === undefined,
108
+ spawnFailed: result.error !== undefined,
109
+ };
110
+ };
111
+
112
+ export interface ProbeDeps {
113
+ readonly run?: ProbeRunner;
114
+ /** Raw-buffer runner for bounded Windows service-manager queries. */
115
+ readonly runRaw?: RawProbeRunner;
116
+ readonly platform?: NodeJS.Platform;
117
+ readonly uid?: number;
118
+ readonly home?: string;
119
+ /** Effective OpenCodex config dir (OPENCODEX_HOME). Overrides `<home>/.opencodex`. */
120
+ readonly configDir?: string;
121
+ /** Test seam for WinSW SCM status. Production uses bounded trusted `sc.exe query`. */
122
+ readonly winswStatus?: () => "started" | "stopped" | "nonexistent" | "unknown";
123
+ /** Test seam for redirected Windows legacy-codepage output. */
124
+ readonly windowsLocale?: string;
125
+ }
126
+
127
+ const LABEL = "com.opencodex.proxy";
128
+ const TASK = "opencodex-proxy";
129
+
130
+ /**
131
+ * `launchctl print` exits 113 for a service that is not there and 112 when the
132
+ * domain itself cannot be reached — measured against nonexistent targets rather
133
+ * than assumed. Only 113 is an answer; everything else is a failure to ask.
134
+ */
135
+ const LAUNCHCTL_NO_SUCH_SERVICE = 113;
136
+ /**
137
+ * 112 is an answer about the DOMAIN, not the label.
138
+ *
139
+ * Measured on macOS 27.0: querying a domain with no service name at all still
140
+ * returns it, and a domain that does not exist cannot be running one of our
141
+ * jobs. Treating it as "could not ask" would refuse every write on a fresh
142
+ * headless Mac — no GUI domain, and no installation either.
143
+ */
144
+ const LAUNCHCTL_NO_SUCH_DOMAIN = 112;
145
+
146
+ /**
147
+ * Residue on disk, distinguished from a path that could not be read.
148
+ *
149
+ * `existsSync` answers "no" for a dangling symlink and for a path whose parent
150
+ * denies traversal. Both are residue, not absence — only ENOENT is absence.
151
+ */
152
+ function artifactPresence(path: string): "present" | "absent" | "unreadable" {
153
+ try {
154
+ lstatSync(path);
155
+ return "present";
156
+ } catch (error) {
157
+ return (error as NodeJS.ErrnoException)?.code === "ENOENT" ? "absent" : "unreadable";
158
+ }
159
+ }
160
+
161
+ function unknown(reason: string): ServiceManagerInstallation {
162
+ return { kind: "unknown", reason };
163
+ }
164
+
165
+ /** Pull `<key>NAME</key><string>VALUE</string>` out of a plist body. */
166
+ function plistEnvValue(body: string, key: string): string | null {
167
+ const match = body.match(
168
+ new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([^<]*)</string>`),
169
+ );
170
+ return match ? match[1] : null;
171
+ }
172
+
173
+ /** Pull `Environment="NAME=VALUE"` (quoted or bare) out of a systemd unit. */
174
+ function unitEnvValue(body: string, key: string): string | null {
175
+ for (const line of body.split("\n")) {
176
+ const match = line.match(new RegExp(`^\\s*Environment=\\s*"?${key}=([^"\\n]*)"?\\s*$`));
177
+ if (match) return match[1];
178
+ }
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Did `systemctl --user` fail because the session bus could not be reached at all?
184
+ *
185
+ * These are the shapes reported on #2114 and #1939. The distinction that matters is
186
+ * "the question never left the machine" versus "systemd answered and said no" — only
187
+ * the former licenses reading the disk instead.
188
+ *
189
+ * **Locale caveat, stated rather than hidden:** systemd localizes these strings, so a
190
+ * non-English host will not match and keeps the old `unknown`. That is the safe
191
+ * direction — it fences rather than admits — but it does mean the fix does not reach
192
+ * every affected user. Forcing `LC_ALL=C` on the probe would remove the caveat and is
193
+ * the obvious follow-up; it is not done here because it changes every systemctl call
194
+ * this module makes, not just this branch.
195
+ */
196
+ function busUnreachable(stderr: string): boolean {
197
+ const err = stderr.trim();
198
+ return err.includes("Failed to connect to bus")
199
+ || err.includes("Failed to connect to user scope bus")
200
+ || err.includes("Failed to get D-Bus connection")
201
+ || err.includes("DBUS_SESSION_BUS_ADDRESS")
202
+ || err.includes("System has not been booted with systemd");
203
+ }
204
+
205
+ /**
206
+ * Ownership from the unit file alone, for when the bus cannot answer (#2114).
207
+ *
208
+ * A unit file is proof of installation that does not require a running bus, and the homes
209
+ * it names are what ownership is actually decided on. What the disk cannot tell us is
210
+ * whether systemd has the unit LOADED, so this reports `registration: "absent"` — the
211
+ * honest reading of "no running manager has it" — rather than inventing a live state.
212
+ *
213
+ * A foreign home therefore still blocks, which is the whole reason this consults the disk
214
+ * instead of widening the exit code.
215
+ */
216
+ function systemdUserUnitSearchPaths(home: string): string[] {
217
+ // systemd's user search path is not one directory. Checking only the canonical one and
218
+ // calling the rest absent is a fail-open: with the bus down a foreign unit in any other
219
+ // search dir is invisible, and "no answer" would be read as "no owner".
220
+ const xdgConfig = process.env.XDG_CONFIG_HOME?.trim();
221
+ const xdgData = process.env.XDG_DATA_HOME?.trim();
222
+ const dirs = [
223
+ xdgConfig ? join(xdgConfig, "systemd", "user") : join(home, ".config", "systemd", "user"),
224
+ join(home, ".config", "systemd", "user"),
225
+ xdgData ? join(xdgData, "systemd", "user") : join(home, ".local", "share", "systemd", "user"),
226
+ join(home, ".local", "share", "systemd", "user"),
227
+ ];
228
+ return [...new Set(dirs)].map(dir => join(dir, `${TASK}.service`));
229
+ }
230
+
231
+ function inspectSystemdOffline(home: string): ServiceManagerInstallation {
232
+ const candidates = systemdUserUnitSearchPaths(home);
233
+ const found = candidates.filter(path => artifactPresence(path) === "present");
234
+ if (candidates.some(path => artifactPresence(path) === "unreadable")) {
235
+ return unknown("the session bus is unreachable and a systemd unit could not be read");
236
+ }
237
+ if (found.length === 0) return { kind: "absent" };
238
+ if (found.length > 1) {
239
+ return unknown("the session bus is unreachable and more than one systemd unit file claims this proxy");
240
+ }
241
+ const definitionPath = found[0]!;
242
+ let body: string;
243
+ try {
244
+ body = readFileSync(definitionPath, "utf-8");
245
+ } catch (error) {
246
+ return unknown(`the session bus is unreachable and the systemd unit could not be read: ${String(error)}`);
247
+ }
248
+ return {
249
+ kind: "present",
250
+ claims: [{
251
+ backend: "systemd",
252
+ definitionPath,
253
+ homes: {
254
+ codexHome: unitEnvValue(body, "CODEX_HOME"),
255
+ opencodexHome: unitEnvValue(body, "OPENCODEX_HOME"),
256
+ },
257
+ registration: "absent",
258
+ }],
259
+ };
260
+ }
261
+
262
+ function inspectLaunchd(deps: Required<Pick<ProbeDeps, "run" | "uid" | "home">>): ServiceManagerInstallation {
263
+ const definitionPath = join(deps.home, "Library", "LaunchAgents", `${LABEL}.plist`);
264
+
265
+ /*
266
+ * BOTH domains, because they are independent and hold separate service sets.
267
+ * Measured on macOS 27.0: the shipped agent answers 0 under `gui/<uid>` and
268
+ * 113 under `user/<uid>`. Asking only one leaves the other free to hold a job
269
+ * this probe would then call absent.
270
+ */
271
+ let registration: "present" | "absent" = "absent";
272
+ let unreachableDomains = 0;
273
+ for (const domain of [`gui/${deps.uid}`, `user/${deps.uid}`]) {
274
+ const printed = deps.run("/bin/launchctl", ["print", `${domain}/${LABEL}`]);
275
+ if (printed.spawnFailed || printed.timedOut) {
276
+ return unknown(`launchctl could not be asked: ${printed.timedOut ? "timed out" : printed.stderr.trim()}`);
277
+ }
278
+ if (printed.status === 0) { registration = "present"; break; }
279
+ if (printed.status === LAUNCHCTL_NO_SUCH_SERVICE) continue;
280
+ if (printed.status === LAUNCHCTL_NO_SUCH_DOMAIN) { unreachableDomains += 1; continue; }
281
+ return unknown(`launchctl print exited ${String(printed.status)}: ${printed.stderr.trim()}`);
282
+ }
283
+
284
+ const definition = artifactPresence(definitionPath);
285
+ if (definition === "absent") {
286
+ // No file. A registration without one means launchd holds a definition whose
287
+ // file is gone — real, and not something to resolve unattended.
288
+ if (registration === "present") {
289
+ return unknown("launchd has a job loaded but its plist is missing");
290
+ }
291
+ /*
292
+ * Nothing staged, and every domain either answered "no such service" or does
293
+ * not exist. 112 is an answer ABOUT THE DOMAIN and is label-independent —
294
+ * querying a domain with no service name at all returns it — so an
295
+ * unreachable domain cannot be hiding a job of ours. Calling this `unknown`
296
+ * instead would refuse every write on a fresh headless Mac, which has no
297
+ * GUI domain and no installation either.
298
+ */
299
+ void unreachableDomains;
300
+ return { kind: "absent" };
301
+ }
302
+
303
+ let body: string;
304
+ try {
305
+ body = readFileSync(definitionPath, "utf-8");
306
+ } catch (error) {
307
+ // Present-but-unreadable cannot supply homes, and `present` without homes
308
+ // would compare equal to nothing and read as agreement.
309
+ return unknown(`the launchd plist exists but could not be read: ${String(error)}`);
310
+ }
311
+
312
+ return {
313
+ kind: "present",
314
+ claims: [{
315
+ backend: "launchd",
316
+ definitionPath,
317
+ homes: {
318
+ codexHome: plistEnvValue(body, "CODEX_HOME"),
319
+ opencodexHome: plistEnvValue(body, "OPENCODEX_HOME"),
320
+ },
321
+ registration,
322
+ }],
323
+ };
324
+ }
325
+
326
+ function systemdProperty(out: string, key: string): string | null {
327
+ for (const line of out.split("\n")) {
328
+ const match = line.match(new RegExp(`^${key}=(.*)$`));
329
+ if (match) return match[1].trim();
330
+ }
331
+ return null;
332
+ }
333
+
334
+ function inspectSystemd(deps: Required<Pick<ProbeDeps, "run" | "home">>): ServiceManagerInstallation {
335
+ const definitionPath = join(deps.home, ".config", "systemd", "user", `${TASK}.service`);
336
+
337
+ /*
338
+ * All four properties in one call. LoadState alone is not enough — it is
339
+ * orthogonal to ActiveState — and neither says whether the LOADED bytes match
340
+ * the file. NeedDaemonReload is that signal, and this repository already
341
+ * documents it as the systemd analogue of launchd's stale plist.
342
+ */
343
+ const shown = deps.run("systemctl", [
344
+ "--user", "show", TASK,
345
+ "-p", "LoadState", "-p", "ActiveState", "-p", "FragmentPath", "-p", "NeedDaemonReload",
346
+ ]);
347
+ if (shown.spawnFailed) return { kind: "absent" };
348
+ if (shown.timedOut) return unknown("systemctl could not be asked: timed out");
349
+ if (shown.status !== 0) {
350
+ // A missing unit still exits ZERO and says not-found; a non-zero status means
351
+ // the question never reached the bus.
352
+ //
353
+ // That is evidence about the BUS, not evidence that a foreign service owns this home
354
+ // (#2114). Calling it `unknown` fences native-main for the whole process, so a laptop
355
+ // with no session bus answers every native request with a 503 until `ocx restart`.
356
+ //
357
+ // Widening on the exit code alone would fail open, because with the bus down systemctl
358
+ // cannot see a foreign unit either. So ask the disk, which needs no bus, and fall back
359
+ // to `unknown` for every other non-zero exit.
360
+ if (busUnreachable(shown.stderr)) return inspectSystemdOffline(deps.home);
361
+ return unknown(`systemctl show exited ${String(shown.status)}: ${shown.stderr.trim()}`);
362
+ }
363
+
364
+ const loadState = systemdProperty(shown.stdout, "LoadState");
365
+ const activeState = systemdProperty(shown.stdout, "ActiveState");
366
+ const fragmentPath = systemdProperty(shown.stdout, "FragmentPath");
367
+ const needReload = systemdProperty(shown.stdout, "NeedDaemonReload");
368
+ if (loadState === null || activeState === null || needReload === null) {
369
+ return unknown("systemctl show did not report the properties it was asked for");
370
+ }
371
+ if (needReload === "yes") {
372
+ return unknown("systemd has a stale definition loaded; it needs daemon-reload");
373
+ }
374
+
375
+ const registration: "present" | "absent" =
376
+ loadState === "not-found" && activeState === "inactive" && !fragmentPath ? "absent" : "present";
377
+
378
+ if (artifactPresence(definitionPath) === "absent") {
379
+ return registration === "absent"
380
+ ? { kind: "absent" }
381
+ : unknown("systemd knows this unit but its file is missing");
382
+ }
383
+
384
+ let body: string;
385
+ try {
386
+ body = readFileSync(definitionPath, "utf-8");
387
+ } catch (error) {
388
+ return unknown(`the systemd unit exists but could not be read: ${String(error)}`);
389
+ }
390
+
391
+ return {
392
+ kind: "present",
393
+ claims: [{
394
+ backend: "systemd",
395
+ definitionPath,
396
+ homes: {
397
+ codexHome: unitEnvValue(body, "CODEX_HOME"),
398
+ opencodexHome: unitEnvValue(body, "OPENCODEX_HOME"),
399
+ },
400
+ registration,
401
+ }],
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Windows: walk the scheduled-task definition chain and report the homes it names.
407
+ *
408
+ * The chain is not one file: the task XML names only the launcher, the launcher
409
+ * (VBS) names only the batch wrapper, and the homes live in the wrapper's
410
+ * `set "CODEX_HOME=..."` / `set "OPENCODEX_HOME=..."` lines. Parsing the XML
411
+ * and stopping would find no homes and read that as agreement, so the walk goes
412
+ * all the way to the wrapper.
413
+ *
414
+ * A `set` line is OMITTED by `buildWindowsServiceScript` when the value was
415
+ * unset at install time (windowsBatchSet returns null for empty values), so a
416
+ * missing home stays `null` — the same contract the launchd/systemd probes use —
417
+ * and a definition that names no homes cannot be mistaken for agreement.
418
+ *
419
+ * Registration is answered by bounded `schtasks` queries so a definition staged
420
+ * on disk but never registered is still visible (the interrupted-install case).
421
+ * Every failure to ask is `unknown`, never absence.
422
+ */
423
+ function windowsTaskName(): string {
424
+ return "opencodex-proxy";
425
+ }
426
+
427
+ function windowsConfigDirPath(deps: { home: string; configDir?: string }): string {
428
+ if (deps.configDir) return deps.configDir;
429
+ return join(deps.home, ".opencodex");
430
+ }
431
+
432
+ /** Decode the XML entities emitted by the service-definition writers. */
433
+ function decodeXmlEntities(value: string): string {
434
+ return value
435
+ .replace(/&quot;/g, '"')
436
+ .replace(/&lt;/g, "<")
437
+ .replace(/&gt;/g, ">")
438
+ .replace(/&apos;/g, "'")
439
+ .replace(/&amp;/g, "&");
440
+ }
441
+
442
+ /** Pull the launcher path out of the task XML `<Arguments>` element. */
443
+ function windowsTaskArguments(xml: string): string | null {
444
+ const match = /<Arguments[^>]*>\s*([^<]*?)\s*<\/Arguments>/i.exec(xml);
445
+ return match ? decodeXmlEntities(match[1]!.trim()) : null;
446
+ }
447
+
448
+ /**
449
+ * Pull the wrapper path out of a VBS `shell.Run` line.
450
+ *
451
+ * `buildWindowsLauncherVbs` escapes a `"` inside a VBS string literal by
452
+ * doubling it, so a wrapper `C:\...\opencodex-service.cmd` is emitted as
453
+ * `shell.Run """C:\...\opencodex-service.cmd""", 0, True`.
454
+ */
455
+ function vbsWrappedCommand(body: string): string | null {
456
+ const match = /\.Run\s+"""([^"]*)"""/.exec(body);
457
+ if (match) {
458
+ const unwrapped = match[1]!.trim();
459
+ if (unwrapped.length > 0) return unwrapped;
460
+ }
461
+ const plain = /\.Run\s+"([^"]+)"/.exec(body);
462
+ return plain ? plain[1]!.trim() : null;
463
+ }
464
+
465
+ /** Pull one `set "NAME=value"` out of a batch wrapper. */
466
+ function batchSetValue(body: string, name: string): string | null {
467
+ const match = new RegExp(`^\\s*set\\s+"${name}=([^"]*)"\\s*$`, "im").exec(body);
468
+ return match ? match[1]!.trim() : null;
469
+ }
470
+
471
+ /** Resolve generated batch env indirection before comparing homes. */
472
+ function decodeBatchPathValue(
473
+ value: string,
474
+ env: Record<string, string | undefined> = process.env,
475
+ ): string {
476
+ const escapedPercent = "\u0000";
477
+ const tokens: Record<string, string | undefined> = {
478
+ USERPROFILE: env.USERPROFILE,
479
+ APPDATA: env.APPDATA,
480
+ LOCALAPPDATA: env.LOCALAPPDATA,
481
+ SYSTEMROOT: env.SystemRoot,
482
+ };
483
+ return value
484
+ .replace(/%%/g, escapedPercent)
485
+ .replace(/%([A-Za-z][A-Za-z0-9_]*)%/g, (whole, name: string) => {
486
+ const resolved = tokens[name.toUpperCase()];
487
+ return resolved === undefined ? whole : resolved;
488
+ })
489
+ .replaceAll(escapedPercent, "%");
490
+ }
491
+
492
+ /** Validate the generated wrapper before interpreting omitted optional homes. */
493
+ function wrapperLooksGenerated(body: string): boolean {
494
+ return /:loop\s*[\s\S]*^"%OCX_BUN%" "%OCX_CLI%" start\b[^\r\n]*$/im.test(body);
495
+ }
496
+
497
+ function normalizeWindowsPath(value: string): string {
498
+ return win32Path.normalize(value.replace(/\//g, "\\")).replace(/[\\]+$/, "").toLowerCase();
499
+ }
500
+
501
+ /** True only when a definition-provided path remains inside the effective OPENCODEX_HOME. */
502
+ function windowsPathInsideConfigDir(candidate: string, configDir: string): boolean {
503
+ const root = normalizeWindowsPath(configDir);
504
+ const path = normalizeWindowsPath(candidate);
505
+ const relative = win32Path.relative(root, path);
506
+ return relative === "" || (relative !== ".." && !relative.startsWith("..\\") && !win32Path.isAbsolute(relative));
507
+ }
508
+
509
+ /** Parse the first CSV field emitted by schtasks `/fo CSV`. */
510
+ function csvFirstField(line: string): string {
511
+ const trimmed = line.trim();
512
+ if (!trimmed.startsWith('"')) return (trimmed.split(",", 1)[0] ?? "").trim();
513
+ let value = "";
514
+ for (let i = 1; i < trimmed.length; i += 1) {
515
+ const ch = trimmed[i]!;
516
+ if (ch !== '"') {
517
+ value += ch;
518
+ continue;
519
+ }
520
+ if (trimmed[i + 1] === '"') {
521
+ value += '"';
522
+ i += 1;
523
+ continue;
524
+ }
525
+ break;
526
+ }
527
+ return value;
528
+ }
529
+
530
+ function windowsTaskListContains(body: string, taskName: string): boolean {
531
+ const target = taskName.toLowerCase();
532
+ return body.split(/\r?\n/).some(line => {
533
+ const field = csvFirstField(line).replace(/\//g, "\\").replace(/^\\+/, "");
534
+ return field.toLowerCase() === target;
535
+ });
536
+ }
537
+
538
+ /** English hosts provide a decisive fast path; other locales fall back to a full listing. */
539
+ const SCHTASKS_TASK_NOT_FOUND_EN = /cannot find the file specified/i;
540
+
541
+ /**
542
+ * Registration state of the scheduled task.
543
+ *
544
+ * The `/xml` query gives the authoritative registered definition. A nonzero
545
+ * result is locale-dependent. English's task-not-found message is decisive; all
546
+ * other nonzero responses use a bounded full listing as the locale-neutral
547
+ * fallback, and only a successful list without our task proves absence.
548
+ */
549
+ function probeWindowsTaskRegistration(
550
+ deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "windowsLocale">,
551
+ ): {
552
+ registered: "present" | "absent" | "unknown";
553
+ registeredXml: string;
554
+ } {
555
+ let schtasks: string;
556
+ try {
557
+ schtasks = resolveTrustedWindowsSchtasksExe();
558
+ } catch {
559
+ return { registered: "unknown", registeredXml: "" };
560
+ }
561
+
562
+ const queried = deps.runRaw(schtasks, ["/query", "/tn", windowsTaskName(), "/xml"]);
563
+ if (queried.spawnFailed || queried.timedOut) return { registered: "unknown", registeredXml: "" };
564
+ if (queried.status === 0) {
565
+ const registeredXml = decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })
566
+ || decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale });
567
+ return registeredXml
568
+ ? { registered: "present", registeredXml }
569
+ : { registered: "unknown", registeredXml: "" };
570
+ }
571
+
572
+ const queryText = `${decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })}\n${decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale })}`;
573
+ if (queried.status !== null && SCHTASKS_TASK_NOT_FOUND_EN.test(queryText)) {
574
+ return { registered: "absent", registeredXml: "" };
575
+ }
576
+
577
+ const listed = deps.runRaw(schtasks, ["/query", "/fo", "CSV", "/nh"]);
578
+ if (listed.spawnFailed || listed.timedOut || listed.status !== 0) {
579
+ return { registered: "unknown", registeredXml: "" };
580
+ }
581
+ const listing = decodeWindowsTextBytes(listed.stdout, { locale: deps.windowsLocale })
582
+ || decodeWindowsTextBytes(listed.stderr, { locale: deps.windowsLocale });
583
+ return windowsTaskListContains(listing, windowsTaskName())
584
+ ? { registered: "unknown", registeredXml: "" }
585
+ : { registered: "absent", registeredXml: "" };
586
+ }
587
+
588
+ type WinswRegistration = "present" | "absent" | "unknown";
589
+
590
+ /** Query WinSW registration through trusted System32 sc.exe; never execute the user-writable WinSW binary. */
591
+ function probeWinswRegistration(
592
+ deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "winswStatus">,
593
+ ): WinswRegistration {
594
+ if (deps.winswStatus) {
595
+ const injected = deps.winswStatus();
596
+ if (injected === "started" || injected === "stopped") return "present";
597
+ if (injected === "nonexistent") return "absent";
598
+ return "unknown";
599
+ }
600
+
601
+ let sc: string;
602
+ try {
603
+ sc = join(resolveTrustedWindowsSystemDirectory(), "sc.exe");
604
+ if (artifactPresence(sc) !== "present") return "unknown";
605
+ } catch {
606
+ return "unknown";
607
+ }
608
+
609
+ const queried = deps.runRaw(sc, ["query", WINSW_SERVICE_ID]);
610
+ if (queried.spawnFailed || queried.timedOut) return "unknown";
611
+ if (queried.status === 0) return "present";
612
+
613
+ // ERROR_SERVICE_DOES_NOT_EXIST (1060) is locale-invariant. Search raw byte
614
+ // text so localized OEM output cannot affect the numeric classification.
615
+ const text = `${queried.stdout.toString("latin1")}\n${queried.stderr.toString("latin1")}`;
616
+ return /\b1060\b/.test(text) ? "absent" : "unknown";
617
+ }
618
+
619
+ function inspectWindows(
620
+ deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
621
+ & Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
622
+ ): ServiceManagerInstallation {
623
+ const configDir = windowsConfigDirPath(deps);
624
+ const taskXmlPath = join(configDir, "opencodex-service-task.xml");
625
+ const task = artifactPresence(taskXmlPath);
626
+ const winsw = walkWinswChain(deps);
627
+ const winswInstalled = winsw.kind === "present" && winsw.claims[0].registration === "present";
628
+ const winswStaged = winsw.kind === "present" && winsw.claims[0].registration === "absent";
629
+
630
+ let xml = "";
631
+ if (task !== "absent") {
632
+ try {
633
+ xml = decodeWindowsTextBytes(readFileSync(taskXmlPath), { locale: deps.windowsLocale });
634
+ } catch (error) {
635
+ return unknown(`the scheduled-task XML exists but could not be read: ${String(error)}`);
636
+ }
637
+ }
638
+
639
+ const registration = probeWindowsTaskRegistration(deps);
640
+ if (registration.registered === "unknown") {
641
+ return unknown("Task Scheduler could not be asked whether opencodex-proxy is registered");
642
+ }
643
+ const schedulerRegistered = registration.registered === "present";
644
+
645
+ // Two live registrations are a conflict even if the staging copy of the task
646
+ // XML disappeared. The authoritative `/query /xml` definition is sufficient
647
+ // to walk the scheduler chain without inventing homes.
648
+ if (winswInstalled && schedulerRegistered) {
649
+ if (!registration.registeredXml.trim()) {
650
+ return unknown("Task Scheduler is registered but its definition XML could not be read");
651
+ }
652
+ const registeredWalk = walkWindowsChain(deps, registration.registeredXml, taskXmlPath);
653
+ if (registeredWalk.kind !== "present") return registeredWalk;
654
+ return {
655
+ kind: "conflict",
656
+ claims: [
657
+ winsw.claims[0],
658
+ { ...registeredWalk.claims[0], registration: "present" },
659
+ ],
660
+ };
661
+ }
662
+
663
+ if (winswInstalled) {
664
+ // A staged scheduler definition beside a registered WinSW service is an
665
+ // interrupted backend switch, not proof that WinSW alone owns the machine.
666
+ if (task !== "absent") {
667
+ return unknown("a scheduled-task definition is staged while the native WinSW service is registered");
668
+ }
669
+ return winsw;
670
+ }
671
+
672
+ if (winsw.kind === "unknown") return winsw;
673
+
674
+ // Staged-but-unregistered WinSW remains evidence. If Scheduler is also live
675
+ // or staged, neither half-finished backend switch can be chosen unattended.
676
+ if (winswStaged && (schedulerRegistered || task !== "absent")) {
677
+ return unknown("native WinSW and Task Scheduler definitions overlap during an incomplete backend switch");
678
+ }
679
+ if (winswStaged && task === "absent" && registration.registered === "absent") {
680
+ return winsw;
681
+ }
682
+
683
+ if (task === "absent") {
684
+ return schedulerRegistered
685
+ ? unknown("Task Scheduler holds opencodex-proxy but its task XML is missing")
686
+ : { kind: "absent" };
687
+ }
688
+
689
+ const staged = walkWindowsChain(deps, xml, taskXmlPath);
690
+ if (staged.kind !== "present") return staged;
691
+ const stagedClaim = staged.claims[0];
692
+
693
+ if (schedulerRegistered) {
694
+ if (!registration.registeredXml.trim()) {
695
+ return unknown("Task Scheduler is registered but its definition XML could not be read");
696
+ }
697
+ const registeredWalk = walkWindowsChain(deps, registration.registeredXml, taskXmlPath);
698
+ if (registeredWalk.kind !== "present") return registeredWalk;
699
+ const registeredClaim = registeredWalk.claims[0];
700
+ if (!homesEqual(registeredClaim.homes, stagedClaim.homes)) {
701
+ return unknown("the registered scheduled task names different homes than the staged task definition");
702
+ }
703
+ }
704
+
705
+ return {
706
+ kind: "present",
707
+ claims: [{
708
+ ...stagedClaim,
709
+ registration: schedulerRegistered ? "present" : "absent",
710
+ }],
711
+ };
712
+ }
713
+
714
+ /** Compare two home pairs with Windows path normalization (case, slashes, trailing separators). */
715
+ function homesEqual(
716
+ a: { codexHome: string | null; opencodexHome: string | null },
717
+ b: { codexHome: string | null; opencodexHome: string | null },
718
+ ): boolean {
719
+ const norm = (v: string | null): string | null => {
720
+ if (v === null) return null;
721
+ return v.replace(/[\\/]+$/, "").replace(/\//g, "\\").toLowerCase();
722
+ };
723
+ return norm(a.codexHome) === norm(b.codexHome) && norm(a.opencodexHome) === norm(b.opencodexHome);
724
+ }
725
+
726
+ /**
727
+ * Walk one scheduled-task definition (staged or registered XML) down to the
728
+ * generated batch wrapper and extract the homes it names. Definition-provided
729
+ * paths are followed only inside the effective OPENCODEX_HOME, preventing a
730
+ * foreign task from turning this ownership probe into an arbitrary local/UNC
731
+ * file read while preserving interrupted-reinstall diagnostics within the
732
+ * generated service-asset directory.
733
+ */
734
+ function walkWindowsChain(
735
+ deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale">,
736
+ xml: string,
737
+ definitionPath: string,
738
+ ): ServiceManagerInstallation {
739
+ const configDir = windowsConfigDirPath(deps);
740
+
741
+ const launcherArg = windowsTaskArguments(xml);
742
+ if (!launcherArg) {
743
+ return unknown("the scheduled-task XML names no launcher to run");
744
+ }
745
+ const launcherPath = /"([^"]+)"/.exec(launcherArg)?.[1];
746
+ if (!launcherPath) {
747
+ return unknown("the scheduled-task XML launcher argument is not a quoted path");
748
+ }
749
+ if (!windowsPathInsideConfigDir(launcherPath, configDir)) {
750
+ return unknown(`the scheduled-task XML names ${launcherPath}, outside the expected launcher directory ${configDir}`);
751
+ }
752
+
753
+ const launcher = artifactPresence(launcherPath);
754
+ if (launcher === "absent") {
755
+ return unknown(`the scheduled-task launcher is missing: ${launcherPath}`);
756
+ }
757
+ let launcherBody: string;
758
+ try {
759
+ launcherBody = decodeWindowsTextBytes(readFileSync(launcherPath), { locale: deps.windowsLocale });
760
+ } catch (error) {
761
+ return unknown(`the scheduled-task launcher could not be read: ${String(error)}`);
762
+ }
763
+
764
+ const wrapperPath = vbsWrappedCommand(launcherBody);
765
+ if (!wrapperPath) {
766
+ return unknown(`the launcher ${launcherPath} names no wrapper to run`);
767
+ }
768
+ if (!windowsPathInsideConfigDir(wrapperPath, configDir)) {
769
+ return unknown(`the scheduled-task launcher names ${wrapperPath}, outside the expected wrapper directory ${configDir}`);
770
+ }
771
+
772
+ const wrapper = artifactPresence(wrapperPath);
773
+ if (wrapper === "absent") {
774
+ return unknown(`the launcher wrapper is missing: ${wrapperPath}`);
775
+ }
776
+ let wrapperBody: string;
777
+ try {
778
+ wrapperBody = decodeWindowsTextBytes(readFileSync(wrapperPath), { locale: deps.windowsLocale });
779
+ } catch (error) {
780
+ return unknown(`the launcher wrapper could not be read: ${String(error)}`);
781
+ }
782
+
783
+ if (!wrapperLooksGenerated(wrapperBody)) {
784
+ return unknown(`the launcher wrapper does not look like a generated opencodex service wrapper: ${wrapperPath}`);
785
+ }
786
+
787
+ const rawCodexHome = batchSetValue(wrapperBody, "CODEX_HOME");
788
+ const rawOpencodexHome = batchSetValue(wrapperBody, "OPENCODEX_HOME");
789
+
790
+ return {
791
+ kind: "present",
792
+ claims: [{
793
+ backend: "scheduler",
794
+ definitionPath,
795
+ homes: {
796
+ codexHome: rawCodexHome === null ? null : decodeBatchPathValue(rawCodexHome),
797
+ opencodexHome: rawOpencodexHome === null ? null : decodeBatchPathValue(rawOpencodexHome),
798
+ },
799
+ registration: "absent",
800
+ }],
801
+ };
802
+ }
803
+
804
+ /**
805
+ * Walk the WinSW native-backend definition. SCM registration is queried via a
806
+ * trusted, bounded `sc.exe query`; the WinSW executable itself is never run by
807
+ * this read-only ownership probe.
808
+ */
809
+ function walkWinswChain(
810
+ deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
811
+ & Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
812
+ ): ServiceManagerInstallation {
813
+ const configDir = windowsConfigDirPath(deps);
814
+ const exePath = join(configDir, "winsw", `${WINSW_SERVICE_ID}.exe`);
815
+ const xmlPath = join(configDir, "winsw", `${WINSW_SERVICE_ID}.xml`);
816
+ const xml = artifactPresence(xmlPath);
817
+ const exe = artifactPresence(exePath);
818
+ const registration = probeWinswRegistration(deps);
819
+
820
+ if (xml === "absent" && exe === "absent" && registration === "absent") return { kind: "absent" };
821
+ // A query we could not ask is a question about a service that cannot exist: WinSW is an
822
+ // optional backend, and with neither its XML nor its exe on disk there is nothing for a
823
+ // registration to belong to. Fencing here on an `sc.exe` timeout is one of the two
824
+ // triggers behind #2108, where a scheduler-only install answers 503 until `ocx restart`.
825
+ //
826
+ // The disk outranks the unaskable query only when BOTH assets are gone. Either one
827
+ // present means a real install may be there and the old `unknown` still holds.
828
+ if (registration === "unknown" && xml === "absent" && exe === "absent") {
829
+ return { kind: "absent" };
830
+ }
831
+ if (registration === "unknown") {
832
+ return unknown("the native WinSW service registration could not be verified");
833
+ }
834
+ if (xml === "absent" || exe === "absent") {
835
+ return unknown("the native WinSW service registration could not be verified");
836
+ }
837
+
838
+ let body: string;
839
+ try {
840
+ body = decodeWindowsTextBytes(readFileSync(xmlPath), { locale: deps.windowsLocale });
841
+ } catch (error) {
842
+ return unknown(`the WinSW XML could not be read: ${String(error)}`);
843
+ }
844
+ if (!winswXmlLooksGenerated(body)) {
845
+ return unknown(`the WinSW XML does not look like a generated opencodex service definition: ${xmlPath}`);
846
+ }
847
+
848
+ const envValue = (name: string): string | null => {
849
+ const tag = new RegExp(`<env\\b[^>]*\\bname=["']${name}["'][^>]*>`, "i").exec(body);
850
+ if (!tag) return null;
851
+ const value = /value=(["'])(.*?)\1/i.exec(tag[0]);
852
+ return value ? decodeXmlEntities(value[2]!) : null;
853
+ };
854
+
855
+ return {
856
+ kind: "present",
857
+ claims: [{
858
+ backend: "winsw",
859
+ definitionPath: exePath,
860
+ homes: {
861
+ codexHome: envValue("CODEX_HOME"),
862
+ opencodexHome: envValue("OPENCODEX_HOME"),
863
+ },
864
+ registration,
865
+ }],
866
+ };
867
+ }
868
+
869
+ /** The generated WinSW XML embeds the SCM id and a `start --port` invocation. */
870
+ function winswXmlLooksGenerated(body: string): boolean {
871
+ return /<id>\s*opencodex-proxy-native\s*<\/id>/i.test(body)
872
+ && /<arguments>.*?start\s+--port\b/i.test(body);
873
+ }
874
+
875
+ export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): ServiceManagerInstallation {
876
+ const platform = deps.platform ?? process.platform;
877
+ const run = deps.run ?? defaultProbeRunner;
878
+ const runRaw = deps.runRaw ?? defaultRawProbeRunner;
879
+ const home = deps.home ?? homedir();
880
+ if (platform === "darwin") return inspectLaunchd({ run, uid: deps.uid ?? process.getuid?.() ?? 0, home });
881
+ if (platform === "linux") return inspectSystemd({ run, home });
882
+ if (platform === "win32") {
883
+ return inspectWindows({
884
+ runRaw,
885
+ home,
886
+ configDir: deps.configDir,
887
+ winswStatus: deps.winswStatus,
888
+ windowsLocale: deps.windowsLocale,
889
+ });
890
+ }
891
+ return unknown(`no service manager probe for platform ${platform}`);
892
+ }