@vantran-se/openchamber-web 1.24.4 → 2.0.2

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-B82Oye3V.js} +1 -1
  5. package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
  6. package/dist/assets/FilesView-DjksmZRo.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
  11. package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
  14. package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
  17. package/dist/assets/PlanView-_eZvb-G5.js +5 -0
  18. package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
  19. package/dist/assets/SettingsView-BRfr46hb.js +29 -0
  20. package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
  22. package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
  26. package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
  29. package/dist/assets/de-CUO1-KqF.js +10 -0
  30. package/dist/assets/es-BlbC6sGJ.js +16 -0
  31. package/dist/assets/fr-D2nwe_0W.js +14 -0
  32. package/dist/assets/index-BQOoYIOu.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D0obJ2p8.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
  37. package/dist/assets/ja-CA7vnDsZ.js +16 -0
  38. package/dist/assets/ko-Chy6a9l-.js +16 -0
  39. package/dist/assets/main-B9JFK3vF.js +896 -0
  40. package/dist/assets/main-Bhiqnrhd.js +2 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
  42. package/dist/assets/miniChat-B0UOL5zA.js +2 -0
  43. package/dist/assets/mobile-BJppFIEo.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
  45. package/dist/assets/pl-CTR5A_1P.js +16 -0
  46. package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
  48. package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
  50. package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
  53. package/dist/assets/tr-Bh5vK6cW.js +7 -0
  54. package/dist/assets/uk-CLDkZFy_.js +16 -0
  55. package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
  57. package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-63H1h21j.js} +2 -2
  64. package/dist/assets/zh-CN-DODgcmYj.js +16 -0
  65. package/dist/assets/zh-TW-ByNfIwLA.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -0,0 +1,396 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { afterEach, describe, expect, it } from 'vitest';
7
+
8
+ import { killLiveTrees, killProcessTree, liveTreeCount, runCommand } from './run-command.js';
9
+
10
+ const node = process.execPath;
11
+
12
+ // Only ever a real pid of a process this test started: an integer greater than 1.
13
+ const isRealPid = (pid) => Number.isInteger(pid) && pid > 1;
14
+ const isAlive = (pid) => {
15
+ if (!isRealPid(pid)) return false;
16
+ try {
17
+ process.kill(pid, 0);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+ const waitFor = async (condition, timeoutMs = 10_000) => {
24
+ const started = Date.now();
25
+ while (!condition() && Date.now() - started < timeoutMs) {
26
+ await new Promise((resolve) => { setTimeout(resolve, 100); });
27
+ }
28
+ return condition();
29
+ };
30
+
31
+ // A child that starts a grandchild, writes both pids to a file, and waits forever.
32
+ //
33
+ // On Windows the grandchild is started with `detached`. Every Node process puts the children it
34
+ // starts into a job object of its own, which kills them when that Node process goes; here that Node
35
+ // process is the child, so a grandchild started the plain way dies with the child, and the control
36
+ // below would prove nothing there. git is not Node and uses no job object: measured on Windows,
37
+ // killing only git left its `docker exec` running. The detached grandchild stays out of the child's
38
+ // job, which is the shape git gives `docker exec`.
39
+ // It still has the child as its parent, which is what `taskkill /T` follows. On POSIX `detached`
40
+ // would move it into a process group of its own, out of reach of the group kill, so it is not used there.
41
+ const parentOfSleeper = (pidFile) => `
42
+ const { spawn } = require('node:child_process');
43
+ const windows = process.platform === 'win32';
44
+ const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore', detached: windows, windowsHide: true });
45
+ require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, JSON.stringify({ child: process.pid, grandchild: grandchild.pid }));
46
+ setInterval(() => {}, 1000);
47
+ `;
48
+
49
+ describe('runCommand', () => {
50
+ it('resolves the exit code and both outputs, also for a non-zero exit', async () => {
51
+ const result = await runCommand(node, ['-e', 'process.stdout.write("out"); process.stderr.write("err"); process.exitCode = 3;']);
52
+ expect(result).toEqual({ code: 3, stdout: 'out', stderr: 'err' });
53
+ });
54
+
55
+ it('writes stdin to the child and closes it', async () => {
56
+ const result = await runCommand(node, ['-e', 'process.stdin.pipe(process.stdout)'], { stdin: 'hello' });
57
+ expect(result).toEqual({ code: 0, stdout: 'hello', stderr: '' });
58
+ });
59
+
60
+ it('writes a Buffer to the child byte for byte', async () => {
61
+ const bytes = Buffer.from([0, 255, 10, 13, 128, 0]);
62
+ const result = await runCommand(node, ['-e', 'const chunks = []; process.stdin.on("data", (chunk) => chunks.push(chunk)).on("end", () => process.stdout.write(Buffer.concat(chunks).toString("hex")))'], { stdin: bytes });
63
+ expect(result.stdout).toBe(bytes.toString('hex'));
64
+ });
65
+
66
+ it('runs the child in the given working directory', async () => {
67
+ const directory = fs.realpathSync(os.tmpdir());
68
+ const result = await runCommand(node, ['-e', 'process.stdout.write(process.cwd())'], { cwd: directory });
69
+ expect(result.stdout).toBe(directory);
70
+ });
71
+
72
+ it('closes stdin when there is no input, so a reader does not hang', async () => {
73
+ const result = await runCommand(node, ['-e', 'process.stdin.on("data", () => {}).on("end", () => process.stdout.write("closed"))']);
74
+ expect(result.stdout).toBe('closed');
75
+ });
76
+
77
+ it('passes arguments as they are, with no shell in between', async () => {
78
+ const argument = '$(echo injected); echo "also" && `id`';
79
+ const result = await runCommand(node, ['-e', 'process.stdout.write(process.argv[1])', argument]);
80
+ expect(result.stdout).toBe(argument);
81
+ });
82
+
83
+ it('kills the child and rejects when it runs past the timeout', async () => {
84
+ const started = Date.now();
85
+ await expect(runCommand(node, ['-e', 'setInterval(() => {}, 1000)'], { timeoutMs: 500 })).rejects.toMatchObject({
86
+ name: 'SpaceError',
87
+ code: 'command_timeout',
88
+ });
89
+ expect(Date.now() - started).toBeLessThan(5000);
90
+ });
91
+
92
+ it('kills the child and rejects when it prints more than the cap', async () => {
93
+ const flood = 'const chunk = "x".repeat(65536); setInterval(() => process.stdout.write(chunk), 1);';
94
+ await expect(runCommand(node, ['-e', flood], { maxOutputBytes: 100_000, timeoutMs: 20_000 })).rejects.toMatchObject({
95
+ code: 'command_output_too_large',
96
+ });
97
+ });
98
+
99
+ it('rejects when the executable does not exist', async () => {
100
+ await expect(runCommand('/nonexistent/openchamber-no-such-binary', ['version'])).rejects.toMatchObject({
101
+ code: 'command_spawn_failed',
102
+ details: { errno: 'ENOENT' },
103
+ });
104
+ });
105
+ });
106
+
107
+ describe('runCommand with killTree', () => {
108
+ const leftovers = [];
109
+ afterEach(() => {
110
+ for (const pid of leftovers.splice(0)) {
111
+ if (isRealPid(pid) && isAlive(pid)) process.kill(pid, 'SIGKILL');
112
+ }
113
+ });
114
+
115
+ /** Runs the parent until its timeout and resolves the two pids and whether the child was still alive when the rejection came. */
116
+ const runParent = async (options) => {
117
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-tree-'));
118
+ const pidFile = path.join(directory, 'pids.json');
119
+ try {
120
+ const error = await runCommand(node, ['-e', parentOfSleeper(pidFile)], { timeoutMs: 3000, ...options }).catch((caught) => caught);
121
+ const pids = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
122
+ const childAliveAtRejection = isAlive(pids.child);
123
+ leftovers.push(pids.grandchild, pids.child);
124
+ expect(error).toMatchObject({ code: 'command_timeout' });
125
+ return { ...pids, childAliveAtRejection };
126
+ } finally {
127
+ fs.rmSync(directory, { recursive: true, force: true });
128
+ }
129
+ };
130
+
131
+ // The control: without the option a timeout kills the child alone, and the grandchild lives on.
132
+ // This is what a git push over ext:: did with its docker exec, measured on three machines.
133
+ it('leaves a grandchild running without the option', async () => {
134
+ const { grandchild } = await runParent({});
135
+ expect(isRealPid(grandchild)).toBe(true);
136
+ await new Promise((resolve) => { setTimeout(resolve, 500); });
137
+ expect(isAlive(grandchild)).toBe(true);
138
+ });
139
+
140
+ it('kills the grandchild too when the whole tree is killed', async () => {
141
+ const { grandchild } = await runParent({ killTree: true });
142
+ expect(isRealPid(grandchild)).toBe(true);
143
+ expect(await waitFor(() => !isAlive(grandchild))).toBe(true);
144
+ });
145
+
146
+ // A caller removes its temporary folder right after the rejection. On Windows a child that still
147
+ // held a file there made that removal fail with EBUSY and hid the real error.
148
+ it('rejects only after the killed child is gone', async () => {
149
+ const { child, childAliveAtRejection } = await runParent({ killTree: true });
150
+ expect(isRealPid(child)).toBe(true);
151
+ expect(childAliveAtRejection).toBe(false);
152
+ });
153
+
154
+ it('still resolves an ordinary run with the option', async () => {
155
+ expect(await runCommand(node, ['-e', 'process.stdout.write("ok")'], { killTree: true })).toEqual({ code: 0, stdout: 'ok', stderr: '' });
156
+ });
157
+ });
158
+
159
+ describe('killProcessTree', () => {
160
+ const recorder = () => {
161
+ const calls = [];
162
+ return {
163
+ calls,
164
+ kill: (pid, signal) => { calls.push(['kill', pid, signal]); },
165
+ spawnProcess: (file, args, options) => {
166
+ calls.push(['spawn', file, args, options]);
167
+ // A taskkill that finishes at once.
168
+ return { once: (event, handler) => { if (event === 'exit') handler(0); } };
169
+ },
170
+ };
171
+ };
172
+ const fakeChild = (pid) => ({ pid, kill: () => { throw new Error('the child alone must not be signalled here'); } });
173
+
174
+ // A fake process with pid 1 once became kill(-1) and closed every program on a developer's machine.
175
+ it.each([null, undefined, 0, 1, -1, -4242, 1.5, Number.NaN, '4242'])('refuses to signal anything for pid %s', async (pid) => {
176
+ for (const platform of ['linux', 'darwin', 'win32']) {
177
+ const { calls, kill, spawnProcess } = recorder();
178
+ expect(await killProcessTree(fakeChild(pid), { platform, kill, spawnProcess, systemRoot: 'C:\\Windows' })).toBe(false);
179
+ expect(calls).toEqual([]);
180
+ }
181
+ });
182
+
183
+ it('signals the process group on POSIX', async () => {
184
+ const { calls, kill, spawnProcess } = recorder();
185
+ expect(await killProcessTree(fakeChild(4242), { platform: 'linux', kill, spawnProcess })).toBe(true);
186
+ expect(calls).toEqual([['kill', -4242, 'SIGKILL']]);
187
+ });
188
+
189
+ it('runs taskkill by its absolute System32 path on Windows, hidden and without a shell, and waits for it', async () => {
190
+ const { calls, kill, spawnProcess } = recorder();
191
+ expect(await killProcessTree(fakeChild(4242), { platform: 'win32', kill, spawnProcess, systemRoot: 'C:\\Windows' })).toBe(true);
192
+ expect(calls).toEqual([['spawn', 'C:\\Windows\\System32\\taskkill.exe', ['/T', '/F', '/PID', '4242'], { shell: false, windowsHide: true, stdio: 'ignore' }]]);
193
+ });
194
+
195
+ it('waits until taskkill has exited', async () => {
196
+ const { kill } = recorder();
197
+ let exitedAt = 0;
198
+ const slow = () => ({
199
+ once: (event, handler) => {
200
+ if (event === 'exit') setTimeout(() => { exitedAt = Date.now(); handler(0); }, 200);
201
+ },
202
+ });
203
+ await killProcessTree(fakeChild(4242), { platform: 'win32', kill, spawnProcess: slow, systemRoot: 'C:\\Windows' });
204
+ expect(exitedAt).toBeGreaterThan(0);
205
+ });
206
+
207
+ it('stops waiting for a taskkill that never ends', async () => {
208
+ const { kill } = recorder();
209
+ const silent = () => ({ once: () => {} });
210
+ const started = Date.now();
211
+ expect(await killProcessTree(fakeChild(4242), { platform: 'win32', kill, spawnProcess: silent, systemRoot: 'C:\\Windows', waitMs: 50 })).toBe(true);
212
+ expect(Date.now() - started).toBeLessThan(2000);
213
+ });
214
+ });
215
+
216
+ describe('runCommand with killTree when the process itself goes', () => {
217
+ const leftovers = [];
218
+ afterEach(() => {
219
+ for (const pid of leftovers.splice(0)) {
220
+ if (isRealPid(pid) && isAlive(pid)) process.kill(pid, 'SIGKILL');
221
+ }
222
+ });
223
+
224
+ // A stand-in for the server: it starts a killTree child that never ends, then leaves. `exit` leaves
225
+ // with process.exit; `sigint` leaves from its own SIGINT handler, the way the server's handlers end.
226
+ const WRAPPER = (moduleUrl, parentScript) => `
227
+ import fs from 'node:fs';
228
+ import { runCommand } from ${JSON.stringify(moduleUrl)};
229
+ const [pidFile, mode] = process.argv.slice(2);
230
+ if (mode === 'sigint') process.on('SIGINT', () => process.exit(130));
231
+ // An exit listener registered before ours that throws, as the serve command registers one earlier.
232
+ if (mode === 'throwing') process.on('exit', () => { throw new Error('an earlier exit listener'); });
233
+ runCommand(process.execPath, ['-e', ${JSON.stringify(parentScript)}], { killTree: true, timeoutMs: 120000 }).catch(() => {});
234
+ const ready = setInterval(() => {
235
+ if (!fs.existsSync(pidFile)) return;
236
+ clearInterval(ready);
237
+ if (mode === 'exit' || mode === 'throwing') process.exit(0);
238
+ fs.writeFileSync(pidFile + '.ready', '');
239
+ setInterval(() => {}, 1000);
240
+ }, 50);
241
+ `;
242
+
243
+ const runWrapper = async (mode) => {
244
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-tree-exit-'));
245
+ try {
246
+ const pidFile = path.join(directory, 'pids.json');
247
+ const wrapperPath = path.join(directory, 'wrapper.mjs');
248
+ fs.writeFileSync(wrapperPath, WRAPPER(new URL('./run-command.js', import.meta.url).href, parentOfSleeper(pidFile)));
249
+ const wrapper = spawn(node, [wrapperPath, pidFile, mode], { stdio: 'ignore' });
250
+ const exited = new Promise((resolve) => { wrapper.on('exit', resolve); });
251
+ if (mode === 'sigint') {
252
+ expect(await waitFor(() => fs.existsSync(`${pidFile}.ready`))).toBe(true);
253
+ expect(isRealPid(wrapper.pid)).toBe(true);
254
+ process.kill(wrapper.pid, 'SIGINT');
255
+ }
256
+ await exited;
257
+ const pids = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
258
+ leftovers.push(pids.grandchild, pids.child);
259
+ return pids;
260
+ } finally {
261
+ fs.rmSync(directory, { recursive: true, force: true });
262
+ }
263
+ };
264
+
265
+ // On Windows this is the test that shows the exit handler is needed there: the grandchild is
266
+ // started outside the child's job, as git's `docker exec` is outside ours, so only the handler's
267
+ // `taskkill /T` reaches it.
268
+ it('kills the tree of a live child when the process leaves with process.exit', async () => {
269
+ const { child, grandchild } = await runWrapper('exit');
270
+ expect(isRealPid(child) && isRealPid(grandchild)).toBe(true);
271
+ expect(await waitFor(() => !isAlive(child) && !isAlive(grandchild), 5000)).toBe(true);
272
+ });
273
+
274
+ it('still kills the tree when an exit listener registered before ours throws', async () => {
275
+ const { child, grandchild } = await runWrapper('throwing');
276
+ expect(isRealPid(child) && isRealPid(grandchild)).toBe(true);
277
+ expect(await waitFor(() => !isAlive(child) && !isAlive(grandchild), 5000)).toBe(true);
278
+ });
279
+
280
+ it.skipIf(process.platform === 'win32')('kills the tree of a live child when the process leaves from its own SIGINT handler', async () => {
281
+ const { child, grandchild } = await runWrapper('sigint');
282
+ expect(isRealPid(child) && isRealPid(grandchild)).toBe(true);
283
+ expect(await waitFor(() => !isAlive(child) && !isAlive(grandchild), 5000)).toBe(true);
284
+ });
285
+ });
286
+
287
+ // A grandchild that left the process group (setsid) survives the group kill, and it holds the
288
+ // child's stdout, so the child's pipes never close. Only the bound on that wait ends it.
289
+ describe.skipIf(process.platform === 'win32')('runCommand with killTree and a grandchild that escaped', () => {
290
+ it('stops waiting for the close at its bound', async () => {
291
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-tree-escape-'));
292
+ const pidFile = path.join(directory, 'escaped.pid');
293
+ const script = `
294
+ const { spawn } = require('node:child_process');
295
+ const escaped = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true, stdio: ['ignore', 'inherit', 'inherit'] });
296
+ require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(escaped.pid));
297
+ setInterval(() => {}, 1000);
298
+ `;
299
+ const started = Date.now();
300
+ try {
301
+ await expect(runCommand(node, ['-e', script], { timeoutMs: 1000, killTree: true, killWaitMs: 300 })).rejects.toMatchObject({ code: 'command_timeout' });
302
+ expect(Date.now() - started).toBeLessThan(4000);
303
+ } finally {
304
+ const pid = Number(fs.readFileSync(pidFile, 'utf8'));
305
+ if (isRealPid(pid) && isAlive(pid)) process.kill(pid, 'SIGKILL');
306
+ fs.rmSync(directory, { recursive: true, force: true });
307
+ }
308
+ }, 20_000);
309
+ });
310
+
311
+ describe('the exit handler and its set of live trees', () => {
312
+ it('registers one exit listener, however many killTree children run', async () => {
313
+ await runCommand(node, ['-e', ''], { killTree: true });
314
+ const listeners = process.listenerCount('exit');
315
+ for (let run = 0; run < 3; run += 1) {
316
+ await runCommand(node, ['-e', ''], { killTree: true });
317
+ }
318
+ expect(process.listenerCount('exit')).toBe(listeners);
319
+ });
320
+
321
+ it.each([null, undefined, 0, 1, -1, 1.5, Number.NaN, '4242'])('signals nothing at exit for a child with pid %s', (pid) => {
322
+ for (const platform of ['linux', 'win32']) {
323
+ const calls = [];
324
+ killLiveTrees({
325
+ children: [{ pid }],
326
+ platform,
327
+ kill: (...args) => { calls.push(['kill', ...args]); },
328
+ spawnSyncProcess: (...args) => { calls.push(['spawnSync', ...args]); },
329
+ systemRoot: 'C:\\Windows',
330
+ });
331
+ expect(calls).toEqual([]);
332
+ }
333
+ });
334
+
335
+ it('kills the group on POSIX and runs taskkill on Windows, synchronously, and never throws', () => {
336
+ const calls = [];
337
+ killLiveTrees({ children: [{ pid: 4242 }], platform: 'linux', kill: (...args) => { calls.push(['kill', ...args]); throw new Error('ESRCH'); } });
338
+ killLiveTrees({ children: [{ pid: 4242 }], platform: 'win32', spawnSyncProcess: (file, args, options) => { calls.push(['spawnSync', file, args, options.windowsHide]); throw new Error('gone'); }, systemRoot: 'C:\\Windows' });
339
+ expect(calls).toEqual([
340
+ ['kill', -4242, 'SIGKILL'],
341
+ ['spawnSync', 'C:\\Windows\\System32\\taskkill.exe', ['/T', '/F', '/PID', '4242'], true],
342
+ ]);
343
+ });
344
+
345
+ it('empties the live set once it has killed', async () => {
346
+ const running = runCommand(node, ['-e', 'setInterval(() => {}, 1000)'], { killTree: true, timeoutMs: 3000 }).catch((error) => error);
347
+ try {
348
+ expect(await waitFor(() => liveTreeCount() === 1)).toBe(true);
349
+ const killed = [];
350
+ // The POSIX path with an injected kill on every platform, so nothing real ends the child here: on
351
+ // Windows the real path would run taskkill, and the child would end with code 1 before its timeout.
352
+ killLiveTrees({ platform: 'linux', kill: (pid) => { killed.push(pid); } });
353
+ expect(liveTreeCount()).toBe(0);
354
+ expect(killed).toHaveLength(1);
355
+ expect(killed[0]).toBeLessThan(-1);
356
+ } finally {
357
+ // The real kill comes from the timeout, and the test waits for it even when an assertion failed.
358
+ expect(await running).toMatchObject({ code: 'command_timeout' });
359
+ }
360
+ });
361
+
362
+ it('keeps nothing in the set after a spawn failure, a timeout or a normal run', async () => {
363
+ await expect(runCommand('/nonexistent/openchamber-no-such-binary', [], { killTree: true })).rejects.toMatchObject({ code: 'command_spawn_failed' });
364
+ expect(liveTreeCount()).toBe(0);
365
+ await expect(runCommand(node, ['-e', 'setInterval(() => {}, 1000)'], { killTree: true, timeoutMs: 500 })).rejects.toMatchObject({ code: 'command_timeout' });
366
+ expect(liveTreeCount()).toBe(0);
367
+ await runCommand(node, ['-e', ''], { killTree: true });
368
+ expect(liveTreeCount()).toBe(0);
369
+ });
370
+
371
+ // A child that exited while an escaped grandchild holds its pipes: its pid must not be kept, or the
372
+ // exit handler would kill whatever has that pid by then.
373
+ it.skipIf(process.platform === 'win32')('drops a child from the set when it exits, even while its pipes stay open', async () => {
374
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-tree-exited-'));
375
+ const pidFile = path.join(directory, 'pids.json');
376
+ const script = `
377
+ const { spawn } = require('node:child_process');
378
+ const escaped = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true, stdio: ['ignore', 'inherit', 'inherit'] });
379
+ require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, JSON.stringify({ child: process.pid, escaped: escaped.pid }));
380
+ setTimeout(() => process.exit(0), 200);
381
+ `;
382
+ const running = runCommand(node, ['-e', script], { killTree: true, timeoutMs: 3000, killWaitMs: 200 }).catch((error) => error);
383
+ try {
384
+ expect(await waitFor(() => fs.existsSync(pidFile))).toBe(true);
385
+ const { child } = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
386
+ expect(await waitFor(() => !isAlive(child))).toBe(true);
387
+ expect(await waitFor(() => liveTreeCount() === 0, 2000)).toBe(true);
388
+ expect(await running).toMatchObject({ code: 'command_timeout' });
389
+ } finally {
390
+ const { escaped } = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
391
+ if (isRealPid(escaped) && isAlive(escaped)) process.kill(escaped, 'SIGKILL');
392
+ fs.rmSync(directory, { recursive: true, force: true });
393
+ }
394
+ }, 20_000);
395
+ });
396
+
@@ -0,0 +1,190 @@
1
+ // The host's channel to the server inside a space, written once on top of `exec` for every place.
2
+ // The space's network cannot see it: requests run as `curl` inside the space against loopback.
3
+ //
4
+ // This is what stage 1b uses. The `connect` operation of the place contract, a real
5
+ // streaming channel, belongs to the dispatcher stage.
6
+
7
+ import crypto from 'node:crypto';
8
+
9
+ import { SpaceError } from './errors.js';
10
+ import {
11
+ EXEC_FAILED_CODES,
12
+ EXEC_INTERRUPTED_CODES,
13
+ buildCurlArgs,
14
+ buildCurlConfig,
15
+ isFromCurl,
16
+ parseResponse,
17
+ tail,
18
+ } from './exec-http.js';
19
+ import {
20
+ IMAGE_CAT,
21
+ IMAGE_ONLY_PATH,
22
+ IMAGE_SH,
23
+ SPACE_SERVER_HOST,
24
+ SPACE_SERVER_PORT,
25
+ SPACE_TOKEN_DIRECTORY,
26
+ SPACE_TOKEN_PATH,
27
+ TOOLS_PLUGIN_PATH,
28
+ spaceWorkPath,
29
+ } from './layout.js';
30
+
31
+ const REQUEST_SECONDS = 20;
32
+ // Agent code can own the server port when a stopped space starts. A listener that accepts and
33
+ // never answers must cost one short attempt, not a long one, and the whole wait has a wall-clock end.
34
+ const HEALTH_SECONDS = 3;
35
+ const READY_TIMEOUT_MS = 120_000;
36
+ const READY_PAUSE_MS = 500;
37
+ // A second stop for a caller whose clock stands still. The deadline is the real limit.
38
+ const READY_MAX_ATTEMPTS = 1_000;
39
+
40
+ // The place makes 43 characters of base64url. Anything else in the token file is not a token.
41
+ const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
42
+ const READ_TOKEN_TIMEOUT_MS = 10_000;
43
+
44
+ // The token goes through a temporary name, so the waiting server never reads half of it.
45
+ const WRITE_TOKEN_SCRIPT = [
46
+ IMAGE_ONLY_PATH,
47
+ 'umask 077;',
48
+ `mkdir -p ${SPACE_TOKEN_DIRECTORY} && chmod 700 ${SPACE_TOKEN_DIRECTORY}`,
49
+ `&& cat > ${SPACE_TOKEN_PATH}.new && mv ${SPACE_TOKEN_PATH}.new ${SPACE_TOKEN_PATH}`,
50
+ ].join(' ');
51
+
52
+ // Module resolution walks up from the project files, so one link above every project is enough.
53
+ // Only the plugin is linked, so project code does not quietly resolve our other packages.
54
+ const LINK_PLUGIN_SCRIPT = `${IMAGE_ONLY_PATH} mkdir -p "$1/node_modules/@opencode" && ln -sfn "$2" "$1/node_modules/@opencode/plugin"`;
55
+
56
+ const pause = (milliseconds) => new Promise((resolve) => { setTimeout(resolve, milliseconds); });
57
+
58
+ export const createSpaceToken = () => crypto.randomBytes(32).toString('base64url');
59
+
60
+ // Whatever listens on the port wrote this text, and that may be agent code. It is data.
61
+ const unreadableAnswer = (why) => new SpaceError('space_server_answer_unreadable', `The server inside the space ${why}`);
62
+
63
+ // The real health answer is a few hundred bytes. A larger one is not worth the time to parse.
64
+ const MAX_HEALTH_BODY_CHARACTERS = 64 * 1024;
65
+
66
+ const reportsReady = (answer) => {
67
+ if (answer.status !== 200 || answer.body.length > MAX_HEALTH_BODY_CHARACTERS) {
68
+ return false;
69
+ }
70
+ try {
71
+ return JSON.parse(answer.body)?.isOpenCodeReady === true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ };
76
+
77
+ /**
78
+ * `exec` is the place operation: `(spaceId, argv, { stdin, timeoutMs })`.
79
+ * `wait` is the pause between readiness attempts and `now` the clock in milliseconds.
80
+ * Both are injectable so tests do not sleep.
81
+ *
82
+ * Everything that comes back from inside a space is untrusted. No answer from inside
83
+ * makes this module throw anything but a SpaceError.
84
+ */
85
+ export function createSpaceServerChannel({ exec, wait = pause, now = Date.now }) {
86
+ const runFixedScript = async (spaceId, argv, stdin, what) => {
87
+ const result = await exec(spaceId, argv, { stdin });
88
+ if (result.code !== 0) {
89
+ throw new SpaceError('space_setup_failed', `Could not ${what} inside the space: ${tail(result.stderr) || `exit code ${result.code}`}`);
90
+ }
91
+ return result.stdout;
92
+ };
93
+
94
+ /** The token travels on stdin. It never appears in an argument, on the host or inside. */
95
+ const writeToken = (spaceId, token) => runFixedScript(spaceId, [IMAGE_SH, '-c', WRITE_TOKEN_SCRIPT], token, 'store the server token');
96
+
97
+ /**
98
+ * The host keeps no copy of the token. It reads it back when it needs it.
99
+ * The agent owns the file. It can make it huge, empty, or a FIFO that never ends, so the read has
100
+ * a short time limit and the result must look like a token.
101
+ */
102
+ const readToken = async (spaceId) => {
103
+ const unreadable = (why) => new SpaceError('space_token_unreadable', `The token file of the server inside the space ${why}. Something inside the space changed it.`);
104
+ let result;
105
+ try {
106
+ result = await exec(spaceId, [IMAGE_CAT, SPACE_TOKEN_PATH], { stdin: '', timeoutMs: READ_TOKEN_TIMEOUT_MS });
107
+ } catch (error) {
108
+ if (error.code === 'command_output_too_large') throw unreadable('is far too large');
109
+ if (EXEC_INTERRUPTED_CODES.includes(error.code)) throw unreadable('could not be read in time');
110
+ throw error;
111
+ }
112
+ if (result.code !== 0) {
113
+ throw new SpaceError('space_setup_failed', `Could not read the server token inside the space: ${tail(result.stderr) || `exit code ${result.code}`}`);
114
+ }
115
+ const token = result.stdout.trim();
116
+ if (!TOKEN_PATTERN.test(token)) {
117
+ throw unreadable(token === '' ? 'is empty' : 'does not hold a token');
118
+ }
119
+ return token;
120
+ };
121
+
122
+ const linkPlugin = (spaceId) => runFixedScript(spaceId, [IMAGE_SH, '-c', LINK_PLUGIN_SCRIPT, 'sh', spaceWorkPath(spaceId), TOOLS_PLUGIN_PATH], '', 'link the OpenCode plugin');
123
+
124
+ /**
125
+ * One HTTP request to the server inside. Resolves `{ status, headers, body }` for any status.
126
+ * The whole request, with its headers and body, is a curl config on stdin, so a cookie or
127
+ * a token in it shows up in no argument list. `headers` values are arrays, a header can repeat.
128
+ */
129
+ const request = async (spaceId, { method = 'GET', path, headers = {}, body = null, timeoutSeconds = REQUEST_SECONDS }) => {
130
+ const config = buildCurlConfig({ url: `http://${SPACE_SERVER_HOST}:${SPACE_SERVER_PORT}${path}`, method, headers, body });
131
+ let result;
132
+ try {
133
+ result = await exec(
134
+ spaceId,
135
+ buildCurlArgs(timeoutSeconds),
136
+ { stdin: `${config}\n`, timeoutMs: (timeoutSeconds + 10) * 1000 },
137
+ );
138
+ } catch (error) {
139
+ // Too much output is an answer from inside, not an interrupted Docker step.
140
+ if (error.code === 'command_output_too_large') {
141
+ throw unreadableAnswer('answered with more data than the host accepts');
142
+ }
143
+ // No Docker step was interrupted here, so this must not look like one to a rollback.
144
+ if (EXEC_INTERRUPTED_CODES.includes(error.code)) {
145
+ throw new SpaceError('space_server_unreachable', `The request to the server inside the space did not finish in time: ${error.message}`, { curlExitCode: null, execFailed: false });
146
+ }
147
+ throw error;
148
+ }
149
+ if (result.code !== 0) {
150
+ throw new SpaceError(
151
+ 'space_server_unreachable',
152
+ `The server inside the space did not answer: ${tail(result.stderr) || `curl exit code ${result.code}`}`,
153
+ { curlExitCode: result.code, execFailed: EXEC_FAILED_CODES.includes(result.code) && !isFromCurl(result.stderr) },
154
+ );
155
+ }
156
+ return parseResponse(result.stdout, unreadableAnswer);
157
+ };
158
+
159
+ /**
160
+ * Resolves when `/health` answers and reports OpenCode ready. The wait ends at a wall-clock
161
+ * deadline, whatever the thing on the port does. An unreadable answer, a wrong answer, and no
162
+ * answer all count as "not ready yet". Only a failure of `exec` itself ends the wait early.
163
+ */
164
+ const waitUntilReady = async (spaceId) => {
165
+ const started = now();
166
+ let last = 'no answer yet';
167
+ for (let attempt = 0; attempt < READY_MAX_ATTEMPTS && now() - started < READY_TIMEOUT_MS; attempt += 1) {
168
+ try {
169
+ if (reportsReady(await request(spaceId, { path: '/health', timeoutSeconds: HEALTH_SECONDS }))) {
170
+ return;
171
+ }
172
+ last = 'the answer does not report OpenCode ready';
173
+ } catch (error) {
174
+ const fromInside = error.code === 'space_server_answer_unreadable'
175
+ || (error.code === 'space_server_unreachable' && error.details?.execFailed !== true);
176
+ if (!fromInside) {
177
+ throw error;
178
+ }
179
+ last = error.message;
180
+ }
181
+ await wait(READY_PAUSE_MS);
182
+ }
183
+ throw new SpaceError(
184
+ 'space_server_not_ready',
185
+ `The server inside the space did not become ready within ${Math.round((now() - started) / 1000)} seconds (${last}). Look at the output of the space container, and check that the space has enough memory.`,
186
+ );
187
+ };
188
+
189
+ return { writeToken, readToken, linkPlugin, request, waitUntilReady };
190
+ }