@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
package/src/update/job.ts CHANGED
@@ -6,13 +6,17 @@ import {
6
6
  atomicWriteFile,
7
7
  getConfigDir,
8
8
  loadConfig,
9
+ } from "../config";
10
+ import {
9
11
  readPid,
10
12
  readRuntimePort,
11
13
  removePid,
12
14
  removeRuntimePort,
13
15
  verifyPidIdentity,
14
- } from "../config";
16
+ } from "../config/process-state";
15
17
  import { isProcessAlive, killProxy } from "../lib/process-control";
18
+ import { selfLaunchArgv } from "../lib/self-launch-argv";
19
+ import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
16
20
  import {
17
21
  buildWindowsElevatedArgumentList,
18
22
  resolveTrustedWindowsPowerShellExe,
@@ -37,6 +41,11 @@ import {
37
41
  import { isNewer } from "./notify";
38
42
  import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
39
43
  import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
44
+ import {
45
+ npmCachePreflightFailureMessage,
46
+ runNpmCachePreflight,
47
+ type NpmCachePreflightReason,
48
+ } from "./npm-cache-preflight.mjs";
40
49
 
41
50
  const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
42
51
  const UPDATE_JOB_FILENAME = "update-job.json";
@@ -244,9 +253,152 @@ function ensureJobDir(): void {
244
253
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
245
254
  }
246
255
 
256
+ /**
257
+ * Describe external text without reproducing it.
258
+ *
259
+ * Use this wherever an `Error.message`, a vendor stream, or any string this module did not
260
+ * compose would otherwise be interpolated into a persisted field. The result names the error's
261
+ * TYPE and size — enough to tell a reader what class of failure occurred — and never its text,
262
+ * which is where the paths and account names live.
263
+ */
264
+ /**
265
+ * A version string we are willing to repeat in a persisted field.
266
+ *
267
+ * Semver plus an optional prerelease/build tail, capped in length. Anything else is dropped
268
+ * rather than logged: `/healthz` is answered by whatever holds the port, so its `version` is
269
+ * external input on the same footing as an error message.
270
+ */
271
+ function isVersionLike(value: unknown): value is string {
272
+ return typeof value === "string"
273
+ && value.length <= 64
274
+ && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
275
+ }
276
+
277
+ function withheldSummary(error: unknown): string {
278
+ // `error.name` is writable, so it is external text like the message. A fixed classification
279
+ // is the only part of an unknown error we can state without repeating something we were
280
+ // handed: `new Error(...)` with `error.name = "Jane Doe"` was persisting the name verbatim.
281
+ const name = error instanceof Error ? "Error" : typeof error;
282
+ // NO MESSAGE TEXT, ever. An earlier version kept messages that carried no path, which sounds
283
+ // reasonable and is wrong: `spawn denied for Jane Doe` has no path in it and still names a
284
+ // person. There is no test on message CONTENT that separates a diagnostic from an identity,
285
+ // so the message does not cross this boundary at all.
286
+ const code = (error as { code?: unknown } | null)?.code;
287
+ // Only recognized codes — an arbitrary uppercase `error.code` can be attacker-shaped too.
288
+ const codeNote = typeof code === "string" && NPM_ERROR_CODES.has(code) ? ` ${code}` : "";
289
+ const text = error instanceof Error ? error.message : String(error ?? "");
290
+ // Node's own errors are structured the same way npm's output is: `syscall` and `errno` are
291
+ // named properties, not prose. Reading those gives a user the actual cause —
292
+ // `Error EACCES · syscall: mkdir · errno: -13` — without repeating a message that could name
293
+ // a person or a path. Both are shape-validated: a syscall is a short lowercase identifier and
294
+ // an errno is an integer, so neither can carry arbitrary text.
295
+ const parts = [`${name}${codeNote}`];
296
+ const syscall = (error as { syscall?: unknown } | null)?.syscall;
297
+ // Same explicit vocabulary as the npm field: a shape check accepts `janedoe`.
298
+ if (typeof syscall === "string" && POSIX_SYSCALLS.has(syscall)) parts.push(`syscall: ${syscall}`);
299
+ const errno = (error as { errno?: unknown } | null)?.errno;
300
+ if (typeof errno === "number" && Number.isInteger(errno)) parts.push(`errno: ${errno}`);
301
+ parts.push(`${Buffer.byteLength(text, "utf8")} bytes withheld`);
302
+ return parts.join(" · ");
303
+ }
304
+
305
+ /**
306
+ * Decide, per field, whether the value is ours to keep.
307
+ *
308
+ * `log` and `error` are composed from this module's own templates; every place that would have
309
+ * interpolated external text now calls `withheldSummary()` first, so the strings arriving here
310
+ * are ours by construction. `releaseNotesUrl` is compared against the module constant rather
311
+ * than pattern-matched, which is what stops a URL-shaped value from smuggling a path.
312
+ * `command` is rendered from validated parts.
313
+ */
314
+ function brandOwnComposedText(key: string, value: unknown): unknown {
315
+ if (key === "releaseNotesUrl") {
316
+ return value === RELEASE_NOTES_URL ? value : "";
317
+ }
318
+ if (key === "command") {
319
+ // Render the command shape first, then apply the same path test as every other field. The
320
+ // renderer only understands space-separated arguments; anything else reaching this field is
321
+ // not a command we built and must not be trusted because of where it was stored.
322
+ return typeof value === "string" ? withholdIfPathBearing(renderSafeCommand(value)) : value;
323
+ }
324
+ // `log` and `error` are ours by construction, but a caller can still slip external text in by
325
+ // interpolating it. Withhold any value that carries an absolute path of any form — that is a
326
+ // narrow, unambiguous test on strings we already control, not the free-text classification
327
+ // that failed nine times.
328
+ if (typeof value === "string") return withholdIfPathBearing(value);
329
+ if (Array.isArray(value)) return value.map(item => (typeof item === "string" ? withholdIfPathBearing(item) : item));
330
+ return value;
331
+ }
332
+
333
+ /** Absolute paths cannot appear in text this module composed; if one does, it came from outside. */
334
+ function withholdIfPathBearing(value: string): string {
335
+ const pathBearing = /[A-Za-z]:[\\/]/.test(value) // C:\ or C:/
336
+ || /\\\\/.test(value) // \\server\share
337
+ || /\\/.test(value) // any backslash
338
+ || /~[\w.-]*\//.test(value) // ~/ or ~user/ anywhere
339
+ || /[%$][A-Za-z_]/.test(value) // %APPDATA%, $HOME
340
+ || /\/[\w.\-~%]+\//.test(value) // any two-segment path run
341
+ || /\b(?:Users|home|Documents and Settings|AppData|Profiles)\b/i.test(value)
342
+ || /\r?\n/.test(value); // multi-line vendor output
343
+ if (!pathBearing) return value;
344
+ return `<withheld: ${Buffer.byteLength(value, "utf8")} bytes, may contain local paths>`;
345
+ }
346
+
347
+ /**
348
+ * Keep a command readable without persisting the launcher path it contains.
349
+ *
350
+ * The real npm worker command is `node /Users/<name>/.../bin/ocx.mjs update --tag latest`, so
351
+ * the account name is inside it by construction. Absolute path arguments are replaced with a
352
+ * placeholder and everything else — the binary name, the flags, the tag — is kept, which is the
353
+ * part a reader actually needs.
354
+ */
355
+ function renderSafeCommand(value: string): string {
356
+ if (!value) return value;
357
+ // Rebuild from a recognized shape rather than filtering the string we were handed. Content
358
+ // cannot distinguish `npm install Mary-Jane` — an account name — from a legitimate package
359
+ // argument, so anything that is not this exact shape is withheld by the caller's path test.
360
+ const parts = value.trim().split(/\s+/);
361
+ const tool = parts[0] === "$" ? parts[1] : parts[0];
362
+ if (tool !== undefined && /^(?:npm|bun|pnpm|yarn|node)$/.test(tool)) {
363
+ const rendered = parts.map(part =>
364
+ /^(?:[A-Za-z]:[\\/]|[\\/]|~|\\\\)/.test(part) ? "<path>" : part);
365
+ // Only fixed flags, our own package spec, and placeholders survive; a bare word that is not
366
+ // one of those is treated as unknown input and the whole value is withheld.
367
+ const allowed = rendered.every(part =>
368
+ part === "$" || part === "<path>"
369
+ || /^(?:npm|bun|pnpm|yarn|node)$/.test(part)
370
+ || /^-{1,2}[\w-]+$/.test(part)
371
+ || /^(?:install|add|update|i)$/.test(part)
372
+ || /^opencodex(?:@[\w.\-]+)?$/.test(part)
373
+ || /^(?:latest|preview|next|beta)$/.test(part)
374
+ || /^\d[\w.\-]*$/.test(part));
375
+ if (allowed) return rendered.join(" ");
376
+ }
377
+ return `<withheld: ${Buffer.byteLength(value, "utf8")} bytes, unrecognized command shape>`;
378
+ }
379
+
380
+ /**
381
+ * Fields that can carry free-form text and therefore need checking at the write boundary.
382
+ *
383
+ * The rest of the record is a closed vocabulary — statuses, channels, installers, versions, an
384
+ * id, timestamps — so checking it only risks mangling values that were never a disclosure
385
+ * route. Naming the risky fields keeps the boundary narrow and auditable.
386
+ */
387
+ const FREE_TEXT_JOB_FIELDS = new Set(["command", "error", "log", "releaseNotesUrl"]);
388
+
389
+ /** Apply the per-field rule at the single point where a job reaches disk. */
390
+ function sanitizePersistedUpdateJob(job: UpdateJobState): UpdateJobState {
391
+ return Object.fromEntries(
392
+ Object.entries(job).map(([key, item]) => [
393
+ key,
394
+ FREE_TEXT_JOB_FIELDS.has(key) ? brandOwnComposedText(key, item) : item,
395
+ ]),
396
+ ) as UpdateJobState;
397
+ }
398
+
247
399
  function writeJob(job: UpdateJobState): void {
248
400
  ensureJobDir();
249
- atomicWriteFile(updateJobPath(), `${JSON.stringify(job, null, 2)}\n`);
401
+ atomicWriteFile(updateJobPath(), `${JSON.stringify(sanitizePersistedUpdateJob(job), null, 2)}\n`);
250
402
  }
251
403
 
252
404
  export function readUpdateJob(jobId?: string | null): UpdateJobState | null {
@@ -260,6 +412,13 @@ export function readUpdateJob(jobId?: string | null): UpdateJobState | null {
260
412
  }
261
413
  }
262
414
 
415
+ /**
416
+ * Log lines are composed by this module, so brand them here rather than at nineteen call sites.
417
+ *
418
+ * The one thing a caller must never do is interpolate external text into a log line — an
419
+ * `Error.message`, a vendor stream, a path we were handed. Those go through
420
+ * `withheldSummary()`, which produces a branded description WITHOUT the text itself.
421
+ */
263
422
  function updateJob(job: UpdateJobState, patch: Partial<UpdateJobState>, logLine?: string): UpdateJobState {
264
423
  const current = readUpdateJob(job.id) ?? job;
265
424
  const next = {
@@ -306,7 +465,9 @@ export function restartCommand(
306
465
  const startArgs = pinPort
307
466
  ? [launcher, "start", "--port", String(Math.trunc(port))]
308
467
  : [launcher, "start"];
309
- const svcArgs = serviceInstalled ? [launcher, ...(serviceArgs ?? ["service", "install"])] : startArgs;
468
+ // Default to the non-registering refresh: an update path reaching here has an already
469
+ // installed service, and `install` would demand elevation on Windows scheduler backends.
470
+ const svcArgs = serviceInstalled ? [launcher, ...(serviceArgs ?? ["service", "repair"])] : startArgs;
310
471
  if (installer === "npm") {
311
472
  const bin = nodeBin();
312
473
  const args = svcArgs;
@@ -400,14 +561,12 @@ export function spawnGuiUpdateWorker(
400
561
  channel: Channel,
401
562
  restart: boolean,
402
563
  ): UpdateWorkerProcess {
403
- const script = process.argv[1];
404
- const args = [
405
- script,
564
+ const args = selfLaunchArgv([
406
565
  "__gui-update-worker",
407
566
  jobId,
408
567
  channel,
409
568
  restart ? "restart" : "no-restart",
410
- ];
569
+ ]);
411
570
  if (process.platform !== "win32") {
412
571
  return spawn(process.execPath, args, {
413
572
  detached: true,
@@ -429,7 +588,7 @@ export function spawnGuiUpdateWorker(
429
588
  ].join("; ");
430
589
  const launched = spawnSync(
431
590
  resolveTrustedWindowsPowerShellExe(),
432
- ["-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps],
591
+ ["-NoProfile", "-NoLogo", "-NonInteractive", "-Command", ps],
433
592
  { encoding: "utf8", windowsHide: true, timeout: 15_000 },
434
593
  );
435
594
  const pid = Number(String(launched.stdout ?? "").trim().split(/\r?\n/).pop());
@@ -501,8 +660,7 @@ export function startUpdateJob(
501
660
  try {
502
661
  child = resolvedDeps.spawnWorkerFn(id, channel, restart);
503
662
  } catch (error) {
504
- const message = error instanceof Error ? error.message : String(error);
505
- updateJob(job, { status: "failed", error: `Could not start update worker: ${message}` }, "Update worker failed to start.");
663
+ updateJob(job, { status: "failed", error: `Could not start update worker: ${withheldSummary(error)}` }, "Update worker failed to start.");
506
664
  throw new UpdateJobError("Could not start update worker", 500, "update_worker_start_failed");
507
665
  }
508
666
  if (typeof child.pid !== "number" || !Number.isSafeInteger(child.pid) || child.pid <= 0) {
@@ -515,7 +673,7 @@ export function startUpdateJob(
515
673
  if (!current || current.pid !== child.pid || (current.status !== "running" && current.status !== "restarting")) return;
516
674
  updateJob(
517
675
  current,
518
- { status: "failed", error: `Update worker failed to start: ${error.message}` },
676
+ { status: "failed", error: `Update worker failed to start: ${withheldSummary(error)}` },
519
677
  "Update worker emitted a startup error.",
520
678
  );
521
679
  });
@@ -523,6 +681,20 @@ export function startUpdateJob(
523
681
  return startedJob;
524
682
  }
525
683
 
684
+ /**
685
+ * Run an update step and record WHAT HAPPENED, not what the tool printed.
686
+ *
687
+ * Raw installer output used to be persisted verbatim, which put local paths and account names
688
+ * into a stored file. Six rounds of trying to sanitize it after the fact each produced a new
689
+ * leak — a wrap inside the keyword, a wrap inside the account name, an indented continuation,
690
+ * three consecutive wraps, an empty continuation line. Every fix was an attempt to reconstruct
691
+ * arbitrary multi-line text well enough to match it, and that is not a problem a redactor can
692
+ * win: the leak surface is whatever npm decides to print.
693
+ *
694
+ * So the raw stream is no longer persisted at all. The job keeps the command, its exit status,
695
+ * and a bounded, structured summary — enough to tell a user which step failed and how, with no
696
+ * free-form vendor text passing through the boundary. Detailed output stays ephemeral.
697
+ */
526
698
  function runLoggedCommand(job: UpdateJobState, bin: string, args: string[], timeout: number): { status: number | null; signal: NodeJS.Signals | null } {
527
699
  job = updateJob(job, {}, `$ ${formatCommand(bin, args)}`);
528
700
  const result = spawnSync(bin, args, {
@@ -532,11 +704,171 @@ function runLoggedCommand(job: UpdateJobState, bin: string, args: string[], time
532
704
  });
533
705
  const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
534
706
  const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
535
- if (stdout) job = updateJob(job, {}, stdout.slice(-4000));
536
- if (stderr) updateJob(job, {}, stderr.slice(-4000));
707
+ const summary = summarizeCommandOutput(stdout, stderr, result.status, result.signal);
708
+ if (summary) updateJob(job, {}, summary);
537
709
  return { status: result.status, signal: result.signal };
538
710
  }
539
711
 
712
+ /**
713
+ * Recognized npm/libc error codes, as an explicit set.
714
+ *
715
+ * A shape pattern like `E[A-Z]{3,}` is NOT a vocabulary: `C:\Users\ERROR\.npm` matches it, and
716
+ * the summary then re-emits the username the withheld output was protecting. Only codes on this
717
+ * list are surfaced, and only when they appear in npm's canonical `code <CODE>` position.
718
+ */
719
+ const NPM_ERROR_CODES = new Set([
720
+ "EACCES", "EPERM", "ENOENT", "EEXIST", "ENOTDIR", "EISDIR", "EMFILE", "ENFILE",
721
+ "ENOSPC", "EROFS", "EXDEV", "ELOOP", "ENAMETOOLONG", "ENOTEMPTY", "EBUSY",
722
+ "EAGAIN", "ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN",
723
+ "EPROTO", "ECONNABORTED", "EHOSTUNREACH", "ENETUNREACH", "EPIPE",
724
+ "E401", "E403", "E404", "E409", "E429", "E500", "E503",
725
+ "EINTEGRITY", "ERESOLVE", "ETARGET", "EPUBLISHCONFLICT", "ENEEDAUTH",
726
+ "EUSAGE", "EJSONPARSE", "EOTP", "EINVALIDTYPE", "ELIFECYCLE",
727
+ "ERR_SOCKET_TIMEOUT", "ERR_INVALID_ARG_TYPE", "ERR_MODULE_NOT_FOUND",
728
+ ]);
729
+
730
+ /** npm prints `npm ERR! code EACCES`; anchor on that position rather than scanning free text. */
731
+ const NPM_CODE_RECORD = /^\s*npm\s+ERR!\s+code\s+([A-Z][A-Z0-9_]{2,})\s*$/gm;
732
+
733
+ /**
734
+ * npm's failure output is STRUCTURED, not prose: `npm error <field> <value>`, one field per
735
+ * line (`npm ERR!` on npm 9 and earlier). That is what makes a useful summary possible without
736
+ * reproducing text — we can read named fields and keep the ones whose value cannot be a path.
737
+ *
738
+ * Fields kept, with a real example of each:
739
+ * code E404, EACCES, ETARGET the single most useful line for diagnosis
740
+ * syscall mkdir, open, getaddrinfo what npm was doing
741
+ * errno -13 the OS errno
742
+ * notarget No matching version ... version-resolution explanation, no path
743
+ * 404 404 Not Found - GET <url> registry URL, no local path
744
+ *
745
+ * Deliberately NOT kept: `path`, `dest`, `file`, `stack`, and the bare `Error: ...` line —
746
+ * every one of those is a filesystem path by definition. `A complete log of this run can be
747
+ * found in: <path>` is dropped for the same reason.
748
+ */
749
+ const NPM_FIELD_LINE = /^\s*npm\s+(?:error|ERR!)\s+([a-z0-9]+)\s+(.*)$/gim;
750
+
751
+ /**
752
+ * POSIX syscall names npm actually reports. An explicit vocabulary, not a shape.
753
+ *
754
+ * `^[a-z][a-z0-9_]{1,20}$` accepts `janedoe`, which is the whole problem: allowlisting the
755
+ * FIELD NAME while leaving its VALUE free-form just moves the leak one level in.
756
+ */
757
+ const POSIX_SYSCALLS = new Set([
758
+ "open", "openat", "close", "read", "write", "stat", "lstat", "fstat", "mkdir", "rmdir",
759
+ "unlink", "rename", "symlink", "readlink", "link", "chmod", "chown", "utimes", "access",
760
+ "scandir", "readdir", "copyfile", "realpath", "futime", "ftruncate", "fchmod", "fchown",
761
+ "connect", "getaddrinfo", "getnameinfo", "socket", "bind", "listen", "accept", "send",
762
+ "recv", "shutdown", "spawn", "spawnSync", "kill", "watch", "lchown", "lutimes", "mkdtemp",
763
+ ]);
764
+
765
+ /** Per-field value contracts. A field is only kept when its value satisfies its own rule. */
766
+ const KNOWN_REGISTRY_HOSTS = new Set([
767
+ "registry.npmjs.org",
768
+ "registry.yarnpkg.com",
769
+ "registry.npmmirror.com",
770
+ "npm.pkg.github.com",
771
+ ]);
772
+
773
+ const NPM_FIELD_VALIDATORS: Record<string, (value: string) => string | null> = {
774
+ // A recognized code, nothing else.
775
+ code: value => (NPM_ERROR_CODES.has(value) ? value : null),
776
+ // A known syscall name, nothing else.
777
+ syscall: value => (POSIX_SYSCALLS.has(value) ? value : null),
778
+ // An integer, rendered from the parsed number so the original string never passes through.
779
+ errno: value => (/^-?\d{1,10}$/.test(value) ? String(Number(value)) : null),
780
+ // Version resolution: the FACT only.
781
+ //
782
+ // Two narrowing attempts failed here and the second is the instructive one. Extracting any
783
+ // `name@version` also matched `jane.doe@example.com`. Pinning the NAME to our own package
784
+ // still left the VERSION free: `@bitkyc08/opencodex@99.99.99-JaneDoe` is a valid-looking
785
+ // spec, and a semver prerelease identifier can encode anything — the same lesson the
786
+ // `/healthz` version taught in round 13.
787
+ //
788
+ // There is no trusted resolved version available at this call site, so the spec is not
789
+ // rendered at all. `code: ETARGET` plus this fact already tells a user their requested
790
+ // version does not exist, which is the diagnostic that matters.
791
+ notarget: () => "no matching version",
792
+ };
793
+
794
+ /**
795
+ * HTTP status lines carry a registry URL. Render it from parsed parts rather than echoing the
796
+ * line: a URL can embed userinfo (`https://Jane:pw@host/`) or a path, and the raw text also
797
+ * defeats the path test because `https:/` looks like a drive letter.
798
+ */
799
+ function npmHttpStatusValue(field: string, value: string): string | null {
800
+ const url = /\bhttps?:\/\/[^\s]+/.exec(value)?.[0];
801
+ if (!url) return `HTTP ${field}`;
802
+ let parsed: URL;
803
+ try { parsed = new URL(url); } catch { return `HTTP ${field}`; }
804
+ // Only hosts we can name in advance. A shape check (`^[\w.-]+$`) accepts
805
+ // `janedoe.example`, a numeric host, or a punycode host — an arbitrary hostname is a
806
+ // disclosure channel, not a diagnostic. Knowing it was the public registry versus "some
807
+ // other host" is the part that helps, and that fits in an allowlist.
808
+ return KNOWN_REGISTRY_HOSTS.has(parsed.hostname.toLowerCase()) && !parsed.username && !parsed.password
809
+ ? `HTTP ${field} from ${parsed.hostname.toLowerCase()}`
810
+ : `HTTP ${field}`;
811
+ }
812
+
813
+ /**
814
+ * Extract the diagnostic fields npm names explicitly.
815
+ *
816
+ * Each kept value still passes `withholdIfPathBearing` before it is used: a registry URL is
817
+ * fine, but `syscall` and friends are only safe by convention, and a convention is not a
818
+ * guarantee. Values are length-capped so a hostile responder cannot pad the record.
819
+ */
820
+ function npmDiagnosticFields(text: string): string[] {
821
+ const seen = new Map<string, string>();
822
+ for (const match of text.matchAll(NPM_FIELD_LINE)) {
823
+ const field = match[1]!.toLowerCase();
824
+ const value = match[2]!.trim();
825
+ if (seen.has(field) || !value || value.length > 160) continue;
826
+ // Every kept field is RENDERED from a validated value, never echoed. Allowlisting the field
827
+ // name alone left the value free-form, so `npm error syscall janedoe` walked straight
828
+ // through — the field was recognized and the value was never checked against anything.
829
+ const validate = NPM_FIELD_VALIDATORS[field];
830
+ const rendered = validate
831
+ ? validate(value)
832
+ : (/^(?:404|401|403|409|429)$/.test(field) ? npmHttpStatusValue(field, value) : null);
833
+ if (rendered === null) continue;
834
+ seen.set(field, rendered);
835
+ }
836
+ return [...seen].map(([field, value]) => `${field}: ${value}`);
837
+ }
838
+
839
+ /**
840
+ * Build a structured, path-free summary of a command's result.
841
+ *
842
+ * Only three things cross the boundary: how the process ended, how much it printed, and any
843
+ * recognized error codes. None of those can carry a filesystem path or an account name.
844
+ */
845
+ export function summarizeCommandOutput(
846
+ stdout: string,
847
+ stderr: string,
848
+ status: number | null,
849
+ signal: NodeJS.Signals | null,
850
+ ): string | null {
851
+ if (!stdout && !stderr && status === 0) return null;
852
+
853
+ const parts: string[] = [];
854
+ parts.push(signal ? `terminated by ${signal}` : `exit ${status ?? "null"}`);
855
+
856
+ // Read npm's own named fields rather than reproducing its text. This is what makes a failed
857
+ // update diagnosable again: `code: E404 · 404: 404 Not Found - GET https://registry...` tells
858
+ // a user exactly what happened, and none of it can be a local path.
859
+ const fields = npmDiagnosticFields(`${stderr}\n${stdout}`);
860
+ if (fields.length > 0) parts.push(...fields);
861
+
862
+ const bytes = Buffer.byteLength(stdout, "utf8") + Buffer.byteLength(stderr, "utf8");
863
+ if (bytes > 0) {
864
+ parts.push(fields.length > 0
865
+ ? `${bytes} bytes of full output withheld`
866
+ : `${bytes} bytes of output withheld (no recognized diagnostic fields)`);
867
+ }
868
+
869
+ return parts.join(" · ");
870
+ }
871
+
540
872
  /**
541
873
  * Tear down anything that would make `ocx start` exit 1 with "already running"
542
874
  * (service wrapper respawn, stale pidfile + live /healthz) before a pinned spawn.
@@ -604,7 +936,7 @@ function spawnDetachedStart(
604
936
  });
605
937
  child.once("error", err => {
606
938
  try {
607
- updateJob(job, {}, `Pinned start spawn error: ${err instanceof Error ? err.message : String(err)}`);
939
+ updateJob(job, {}, `Pinned start spawn error: ${withheldSummary(err)}`);
608
940
  } catch { /* best-effort */ }
609
941
  });
610
942
  // Foreground `ocx start` keeps the listen process; EADDRINUSE/ghost races exit quickly
@@ -656,6 +988,8 @@ export interface RestartIo {
656
988
  serviceHealthTimeoutMs?: number;
657
989
  sleepMs?: (ms: number) => Promise<void>;
658
990
  now?: () => number;
991
+ /** Test seam — defaults to process.platform so the Windows-only branch is reachable off Windows. */
992
+ platform?: NodeJS.Platform;
659
993
  /** Service-mode install/reinstall command (defaults to spawnSync via runLoggedCommand). */
660
994
  runService?: (
661
995
  job: UpdateJobState,
@@ -790,11 +1124,17 @@ async function restartAfterUpdate(
790
1124
  const preServiceAllow = reclaimKillAllowlist();
791
1125
  const freed = await waitFn(port, hostname, reclaimOptsFor(preServiceAllow));
792
1126
  let skipServiceInstall = false;
793
- // Windows GUI update worker sets OCX_SERVICE=1 and is never elevated.
794
- // `schtasks /create` will UAC-fail and can race the subsequent direct start.
795
- // Keep systemd/launchd reinstall on non-Windows supervisors.
796
- if (process.platform === "win32" && process.env.OCX_SERVICE === "1") {
797
- updateJob(job, {}, "Skipping service reinstall from the non-elevated update worker; falling back to a direct proxy start.");
1127
+ // This skip existed because the refresh ran `ocx service install`, whose Windows
1128
+ // scheduler path always reaches `schtasks /create` elevation the GUI update worker
1129
+ // (OCX_SERVICE=1) never has. `service repair` rewrites the wrapper assets and
1130
+ // restarts the EXISTING task with no `/create`, so the reason no longer applies and
1131
+ // skipping would leave the dashboard-triggered update the most common Windows
1132
+ // path — with a stale service it could have refreshed.
1133
+ //
1134
+ // Only a caller that still passes install argv keeps the old behavior.
1135
+ const refreshRegisters = (svcArgs ?? []).includes("install");
1136
+ if ((io.platform ?? process.platform) === "win32" && process.env.OCX_SERVICE === "1" && refreshRegisters) {
1137
+ updateJob(job, {}, "Skipping service re-registration from the non-elevated update worker; falling back to a direct proxy start.");
798
1138
  skipServiceInstall = true;
799
1139
  }
800
1140
  if (!freed && !skipServiceInstall) {
@@ -804,8 +1144,16 @@ async function restartAfterUpdate(
804
1144
  `Port ${port} still busy after ${Math.trunc(RESTART_PORT_RECLAIM_MS / 1000)}s; refusing to hop — reinstall may fail until the port is free.`
805
1145
  + ` ${formatPortHolders(port, listPids, verifyOcx, preServiceAllow)}`,
806
1146
  );
807
- const liveAfter = listPids(port).filter(pid => pid !== process.pid && aliveFn(pid));
808
- if (liveAfter.length === 0) {
1147
+ const liveScan: ListenPidScan = io.scanListenPidsFn
1148
+ ? io.scanListenPidsFn(port)
1149
+ : io.listListenPidsFn
1150
+ // Test seam: an injected list represents a successful scan.
1151
+ ? { ok: true, pids: io.listListenPidsFn(port) }
1152
+ : scanListenPids(port);
1153
+ const liveAfter = liveScan.ok
1154
+ ? liveScan.pids.filter(pid => pid !== process.pid && aliveFn(pid))
1155
+ : null;
1156
+ if (liveAfter !== null && liveAfter.length === 0) {
809
1157
  // Non-elevated `service install` will UAC-fail anyway; skip straight to
810
1158
  // the direct-start fallthrough instead of burning another minute on it.
811
1159
  updateJob(job, {}, "Skipping service reinstall after reclaim timeout with no live holders; falling back to a direct proxy start.");
@@ -821,24 +1169,17 @@ async function restartAfterUpdate(
821
1169
  const result = run(job, cmd.bin, cmd.args);
822
1170
  serviceOk = result.status === 0;
823
1171
  if (!serviceOk) {
824
- // On Windows, `schtasks /create` requires an elevated token. The update worker
825
- // inherits the (non-admin) proxy's privileges, so a service-managed install
826
- // updated from the GUI or a normal terminal fails here with access denied.
827
- // Falling back to a direct proxy start keeps the update from leaving the proxy
828
- // stopped; the stale service manager can be refreshed later with an admin
829
- // `ocx service install`.
830
- //
831
- // That advice is Windows-only, and on macOS/Linux it now actively misleads:
832
- // `ocx service install` gained a non-zero exit for a service that registers
833
- // but does not serve, so this branch fires there for a reason elevation
834
- // cannot fix. Point at the command that prints the real reason instead.
1172
+ // The refresh that just failed was `ocx service repair` (serviceReinstallArgs),
1173
+ // which needs no elevation because it never calls `schtasks /create`. Advising
1174
+ // `install` here would send the user to re-registration a UAC prompt on
1175
+ // Windows and a possible WinSW-to-scheduler backend switch to fix a service
1176
+ // that is already registered. Point at the same command that failed so its
1177
+ // output explains why, on every platform.
835
1178
  updateJob(
836
1179
  job,
837
1180
  {},
838
- `Service reinstall failed (exit ${result.status ?? "?"}); falling back to a direct proxy start.`
839
- + (process.platform === "win32"
840
- ? " Run 'ocx service install' as administrator to refresh the background service manager."
841
- : " Run 'ocx service install' by hand to see the reason, then 'ocx service status'."),
1181
+ `Service refresh failed (exit ${result.status ?? "?"}); falling back to a direct proxy start.`
1182
+ + " Run 'ocx service repair' by hand to see the reason, then 'ocx service status'.",
842
1183
  );
843
1184
  }
844
1185
  } finally {
@@ -1041,26 +1382,16 @@ function stopWindowsServiceWrappersBestEffort(): void {
1041
1382
  * Best-effort termination of surviving Windows scheduler launcher/wrapper processes.
1042
1383
  * `schtasks /end` ends the task instance but often leaves wscript/cmd running the
1043
1384
  * `:loop` batch, which brings the proxy back during post-update reclaim.
1385
+ *
1386
+ * This used to match the bare filenames with -like '*name*', which could stop a
1387
+ * wrapper belonging to a DIFFERENT OpenCodex home under the same account. The
1388
+ * shared killer scopes to this home's canonical paths as complete tokens.
1044
1389
  */
1045
1390
  function killWindowsServiceWrapperProcesses(): void {
1046
- if (process.platform !== "win32") return;
1047
- try {
1048
- const ps = [
1049
- "$pats = @('opencodex-service.cmd','opencodex-service-launcher.vbs');",
1050
- "Get-CimInstance Win32_Process | Where-Object {",
1051
- " if ($_.ProcessId -eq $PID) { return $false };",
1052
- " $c = $_.CommandLine; if (-not $c) { return $false };",
1053
- " foreach ($p in $pats) { if ($c -like ('*' + $p + '*')) { return $true } };",
1054
- " $false",
1055
- "} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
1056
- ].join(" ");
1057
- spawnSync(resolveTrustedWindowsPowerShellExe(), [
1058
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
1059
- "-Command", ps,
1060
- ], { stdio: "ignore", timeout: 5000, windowsHide: true });
1061
- } catch {
1062
- /* best-effort */
1063
- }
1391
+ killWindowsSchedulerWrappers({
1392
+ scriptPath: join(getConfigDir(), "opencodex-service.cmd"),
1393
+ launcherPath: join(getConfigDir(), "opencodex-service-launcher.vbs"),
1394
+ });
1064
1395
  }
1065
1396
 
1066
1397
  /** Exposed for tests: drives the non-service restart path with injected io. */
@@ -1076,7 +1407,7 @@ function restartFailureHint(port: number): string {
1076
1407
  return `Update installed, but the restarted proxy did not stay healthy on port ${port}. `
1077
1408
  + `Try 'ocx start --port ${port}'. `
1078
1409
  + "If the update log shows bun postinstall or EPERM warnings, "
1079
- + "reinstall with 'npm install -g --allow-scripts=bun @iislee/opencodex'.";
1410
+ + "reinstall with 'npm install -g --allow-scripts=bun @bitkyc08/opencodex'.";
1080
1411
  }
1081
1412
 
1082
1413
  type AwaitHealthyResult =
@@ -1192,7 +1523,11 @@ async function defaultProbeProxyIdentity(
1192
1523
  if (!isOpencodexHealthz(body)) return null;
1193
1524
  return {
1194
1525
  pid: typeof body?.pid === "number" ? body.pid : null,
1195
- ...(typeof body?.version === "string" ? { version: body.version } : {}),
1526
+ // Validate the shape at the boundary where the value ENTERS, not where it is logged.
1527
+ // `/healthz` is answered by whatever is listening on that port, so a hostile or confused
1528
+ // responder can return any string here — and the restart-evidence reasons below
1529
+ // interpolate it into a persisted field. A version is a version or it is nothing.
1530
+ ...(isVersionLike(body?.version) ? { version: body.version } : {}),
1196
1531
  };
1197
1532
  } catch {
1198
1533
  return null;
@@ -1227,18 +1562,22 @@ export function npmSelfUpdateRestartEvidence(
1227
1562
  }
1228
1563
  if (livePid !== null) {
1229
1564
  if (expected !== null && identity.version && identity.version !== expected) {
1230
- return { ok: false, reason: `new pid but version ${identity.version} !== expected ${expected}` };
1565
+ // Never echo the REPORTED version: `/healthz` is answered by whatever holds the port,
1566
+ // and `2.7.41-JaneDoe` is valid semver. Say that it mismatched, and name only the
1567
+ // version we expected — which is ours.
1568
+ return { ok: false, reason: `new pid but reported version did not match expected ${expected}` };
1231
1569
  }
1232
1570
  return { ok: true, detail: `pid changed ${oldPid}→${livePid}` };
1233
1571
  }
1234
1572
  // Pre-update PID known but healthz omitted pid — only accept matching target version.
1235
- if (versionMatches) return { ok: true, detail: `version ${identity.version}` };
1573
+ // On a match the reported value equals `expected`, so render the trusted one.
1574
+ if (versionMatches) return { ok: true, detail: `version ${expected}` };
1236
1575
  return { ok: false, reason: "no PID in healthz and version did not match the update target" };
1237
1576
  }
1238
1577
 
1239
- if (versionMatches) return { ok: true, detail: `version ${identity.version}` };
1578
+ if (versionMatches) return { ok: true, detail: `version ${expected}` };
1240
1579
  if (expected !== null && identity.version && identity.version !== expected) {
1241
- return { ok: false, reason: `version ${identity.version} !== expected ${expected}` };
1580
+ return { ok: false, reason: `reported version did not match expected ${expected}` };
1242
1581
  }
1243
1582
  return { ok: false, reason: "no pre-update PID capture and no expected-version match" };
1244
1583
  }
@@ -1380,9 +1719,36 @@ async function confirmNpmExplicitRestart(
1380
1719
  return true;
1381
1720
  }
1382
1721
 
1383
- export async function runGuiUpdateWorker(jobId: string, channel: Channel, restart: boolean): Promise<void> {
1722
+ /**
1723
+ * Test seams for the GUI update worker.
1724
+ *
1725
+ * The cache pre-flight and the install/stop step were previously reached only through module
1726
+ * globals, so "the gate runs before the stop" could only be asserted by comparing source-string
1727
+ * positions — a test that stays green even if the call is unreachable. These make the ordering
1728
+ * observable: a failed pre-flight must leave `runCommand` untouched.
1729
+ */
1730
+ export interface GuiUpdateWorkerIo {
1731
+ cachePreflightFn?: () => { ok: boolean; reason: string };
1732
+ /** Force the resolved update target. A source checkout otherwise aborts before the npm branch. */
1733
+ checkForUpdateFn?: (channel: Channel) => ReturnType<typeof checkForUpdate>;
1734
+ /** Bypass the registry integrity probe, which runs before the cache gate and needs network. */
1735
+ integrityFn?: (version: string | null) => ReturnType<typeof checkUpdatePackageIntegrity>;
1736
+ runCommandFn?: (
1737
+ job: UpdateJobState,
1738
+ bin: string,
1739
+ args: string[],
1740
+ timeout: number,
1741
+ ) => { status: number | null; signal: NodeJS.Signals | null };
1742
+ }
1743
+
1744
+ export async function runGuiUpdateWorker(
1745
+ jobId: string,
1746
+ channel: Channel,
1747
+ restart: boolean,
1748
+ io: GuiUpdateWorkerIo = {},
1749
+ ): Promise<void> {
1384
1750
  let job = readUpdateJob(jobId);
1385
- const check = checkForUpdate(channel);
1751
+ const check = (io.checkForUpdateFn ?? checkForUpdate)(channel);
1386
1752
  const now = new Date().toISOString();
1387
1753
  // Capture the live listen target BEFORE the update command runs: the stop-first update
1388
1754
  // flow clears pid/runtime state, so this is the last moment the real port is knowable.
@@ -1427,7 +1793,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
1427
1793
  // Pre-flight integrity metadata check (same lanes as the CLI): anomalous registry
1428
1794
  // metadata for a resolved version fails the job BEFORE anything is spawned or the
1429
1795
  // proxy is stopped; transient registry failure degrades to a logged skip.
1430
- const integrity = checkUpdatePackageIntegrity(check.latestVersion);
1796
+ const integrity = (io.integrityFn ?? checkUpdatePackageIntegrity)(check.latestVersion);
1431
1797
  if (integrity.ok === false) {
1432
1798
  updateJob(job, { status: "failed", error: integrity.reason });
1433
1799
  return;
@@ -1444,6 +1810,17 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
1444
1810
  command: cmd.display,
1445
1811
  }, integrityLine);
1446
1812
 
1813
+ if (check.installer === "npm") {
1814
+ const cachePreflight = (io.cachePreflightFn ?? runNpmCachePreflight)();
1815
+ if (!cachePreflight.ok) {
1816
+ updateJob(job, {
1817
+ status: "failed",
1818
+ error: npmCachePreflightFailureMessage(cachePreflight.reason as NpmCachePreflightReason),
1819
+ }, "Update aborted before stopping the proxy because the npm cache pre-flight failed.");
1820
+ return;
1821
+ }
1822
+ }
1823
+
1447
1824
  if (process.platform === "win32") {
1448
1825
  try {
1449
1826
  const { getWindowsTrayStatus, startWindowsTray, stopWindowsTray } = await import("../tray/windows");
@@ -1460,7 +1837,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
1460
1837
  } catch (error) {
1461
1838
  updateJob(job, {
1462
1839
  status: "failed",
1463
- error: `Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`,
1840
+ error: `Could not stop the Windows tray; aborting before package replacement: ${withheldSummary(error)}`,
1464
1841
  });
1465
1842
  return;
1466
1843
  }
@@ -1471,7 +1848,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
1471
1848
  - 대안 분석: (1) 서버에서 runUpdate 직접 호출: process.exit/stdio/실행 파일 교체 위험. (2) GUI에서 CLI 명령 안내만 제공: 자동 업데이트 UX 부족. (3) 숨은 worker가 Node launcher/Bun 전역 명령을 실행: 상태 추적과 안전한 재시작이 가능.
1472
1849
  - 선택 근거: 현재 CLI의 npm self-update 우회를 재사용하면서도 GUI 서버 요청 생명주기와 설치 작업을 분리할 수 있어 가장 안정적이다.
1473
1850
  */
1474
- const result = runLoggedCommand(job, cmd.bin, cmd.args, UPDATE_TIMEOUT_MS);
1851
+ const result = (io.runCommandFn ?? runLoggedCommand)(job, cmd.bin, cmd.args, UPDATE_TIMEOUT_MS);
1475
1852
  if (result.status !== 0) {
1476
1853
  if (trayWasRunning) {
1477
1854
  try {
@@ -1489,11 +1866,11 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
1489
1866
  }
1490
1867
 
1491
1868
  if (trayWasInstalled) {
1492
- const trayArgs = [process.argv[1], ...planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs];
1869
+ const trayArgs = selfLaunchArgv(planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs);
1493
1870
  const tray = runLoggedCommand(job, process.execPath, trayArgs, 20_000);
1494
1871
  if (tray.status !== 0) {
1495
1872
  updateJob(job, {}, "Windows tray refresh failed; run 'ocx tray install'.");
1496
- if (trayWasRunning) runLoggedCommand(job, process.execPath, [process.argv[1], "tray", "start"], 15_000);
1873
+ if (trayWasRunning) runLoggedCommand(job, process.execPath, selfLaunchArgv(["tray", "start"]), 15_000);
1497
1874
  }
1498
1875
  }
1499
1876
 
@@ -1514,7 +1891,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar
1514
1891
  }
1515
1892
  updateJob(job, {
1516
1893
  status: "failed",
1517
- error: err instanceof Error ? err.message : String(err),
1894
+ error: withheldSummary(err),
1518
1895
  });
1519
1896
  }
1520
1897
  }