@vantran-se/openchamber-web 1.24.4 → 2.0.1

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-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.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-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.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-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.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,7 @@
1
+ import { LIVE_DOCKER_ENABLED, createLiveDockerPlace } from './docker-live-support.js';
2
+ import { runEscapeSuite } from './escape-suite.js';
3
+
4
+ runEscapeSuite('docker (live)', {
5
+ enabled: LIVE_DOCKER_ENABLED,
6
+ setup: async () => createLiveDockerPlace(),
7
+ });
@@ -0,0 +1,375 @@
1
+ // Test support. A fake command runner that answers like the docker CLI for the
2
+ // few subcommands the Docker place uses, and records every call.
3
+
4
+ import { SpaceError } from '../errors.js';
5
+ import { GATEKEEPER_PROGRAM_PATH, IMAGE_CAT, IMAGE_CURL, IMAGE_SH, SPACE_HOME, SPACE_TOKEN_PATH, TOOLS_MOUNT_PATH } from '../layout.js';
6
+
7
+ const ok = (stdout = '') => ({ code: 0, stdout, stderr: '' });
8
+ const failed = (stderr, stdout = '') => ({ code: 1, stdout, stderr });
9
+
10
+ const NOT_FOUND_TEXT = {
11
+ container: (name) => `Error response from daemon: No such container: ${name}`,
12
+ network: (name) => `Error response from daemon: network ${name} not found`,
13
+ volume: (name) => `Error response from daemon: get ${name}: no such volume`,
14
+ image: (name) => `Error response from daemon: No such image: ${name}`,
15
+ };
16
+
17
+ const ISOLATED_GATEWAY_OPTION = 'com.docker.network.bridge.gateway_mode_ipv4';
18
+
19
+ const readPairs = (args, flag) => {
20
+ const pairs = {};
21
+ args.forEach((arg, index) => {
22
+ if (arg === flag) {
23
+ const pair = args[index + 1];
24
+ pairs[pair.slice(0, pair.indexOf('='))] = pair.slice(pair.indexOf('=') + 1);
25
+ }
26
+ });
27
+ return pairs;
28
+ };
29
+
30
+ const readFlag = (args, flag) => args[args.indexOf(flag) + 1];
31
+
32
+ let nextContainerId = 1;
33
+ // Like a real id: hex, and never equal to a container name.
34
+ const newContainerId = () => (nextContainerId++).toString(16).padStart(64, 'f');
35
+
36
+ const volumeMountEntry = ({ volume, destination, readOnly = false }) => ({
37
+ Type: 'volume',
38
+ Name: volume,
39
+ Source: `/var/lib/docker/volumes/${volume}/_data`,
40
+ Destination: destination,
41
+ RW: !readOnly,
42
+ });
43
+
44
+ /**
45
+ * A `docker inspect` entry for a container that matches the requested hardening.
46
+ * `volumes` are the volumes of the space, `toolsVolume` is mounted read-only at the tools path.
47
+ * `mounts`, as `{ volume, destination, readOnly }`, replaces both when the test needs exact destinations.
48
+ * `aliases` are the network aliases on `network`, as a gatekeeper has.
49
+ */
50
+ export function hardenedContainerEntry({
51
+ name,
52
+ labels,
53
+ network,
54
+ volumes = [],
55
+ toolsVolume = null,
56
+ mounts = null,
57
+ env = ['HOME=/home/space'],
58
+ running = true,
59
+ memoryBytes = 4294967296,
60
+ tmpfs = '/tmp:rw,exec,nosuid,size=256m',
61
+ aliases = [],
62
+ }) {
63
+ const mountList = mounts ?? [
64
+ ...volumes.map((volume) => ({ volume, destination: `/mnt/${volume}` })),
65
+ ...(toolsVolume ? [{ volume: toolsVolume, destination: TOOLS_MOUNT_PATH, readOnly: true }] : []),
66
+ ];
67
+ const [tmpfsPath, tmpfsOptions] = String(tmpfs).split(':');
68
+ return {
69
+ Id: newContainerId(),
70
+ Name: `/${name}`,
71
+ State: { Running: running, Status: running ? 'running' : 'created' },
72
+ Config: { User: '1000:1000', Labels: labels, Env: env },
73
+ HostConfig: {
74
+ ReadonlyRootfs: true,
75
+ Privileged: false,
76
+ CapDrop: ['ALL'],
77
+ CapAdd: null,
78
+ SecurityOpt: ['no-new-privileges'],
79
+ Init: true,
80
+ PidsLimit: 512,
81
+ Memory: memoryBytes,
82
+ MemorySwap: memoryBytes,
83
+ ShmSize: 67108864,
84
+ LogConfig: { Type: 'local', Config: { compress: 'false', 'max-file': '1', 'max-size': '10m' } },
85
+ Tmpfs: { [tmpfsPath]: tmpfsOptions },
86
+ Binds: null,
87
+ VolumesFrom: null,
88
+ PidMode: '',
89
+ IpcMode: 'private',
90
+ UTSMode: '',
91
+ UsernsMode: '',
92
+ CgroupnsMode: 'private',
93
+ NetworkMode: network,
94
+ Runtime: 'runc',
95
+ Devices: [],
96
+ PortBindings: {},
97
+ },
98
+ Mounts: mountList.map(volumeMountEntry),
99
+ NetworkSettings: { Networks: { [network]: aliases.length > 0 ? { Aliases: aliases } : {} } },
100
+ };
101
+ }
102
+
103
+ /** A `docker network inspect` entry for an internal network with no address on the host. */
104
+ export const internalNetworkEntry = ({ name, labels, options = { [ISOLATED_GATEWAY_OPTION]: 'isolated' } }) => ({
105
+ Name: name,
106
+ Internal: true,
107
+ EnableIPv6: false,
108
+ // Like the real engine: no gateway address in isolated mode.
109
+ IPAM: { Config: [options[ISOLATED_GATEWAY_OPTION] === 'isolated' ? { Subnet: '172.19.0.0/16' } : { Subnet: '172.19.0.0/16', Gateway: '172.19.0.1' }] },
110
+ Options: options,
111
+ Labels: labels,
112
+ });
113
+
114
+ /** A `docker network inspect` entry for an ordinary bridge, as the outer network of a space is. */
115
+ export const bridgeNetworkEntry = ({ name, labels }) => ({
116
+ Name: name,
117
+ Internal: false,
118
+ EnableIPv6: false,
119
+ IPAM: { Config: [{ Subnet: '172.20.0.0/16', Gateway: '172.20.0.1' }] },
120
+ Options: {},
121
+ Labels: labels,
122
+ });
123
+
124
+ const HEALTHY_ANSWER = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{"status":"ok","isOpenCodeReady":true}';
125
+ const jsonAnswer = (body) => `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n${body}`;
126
+ const CONTROL_ANSWERS = {
127
+ '/health': jsonAnswer('{"ready":true,"mode":"allowlist","domains":0,"grants":0}'),
128
+ '/journal': jsonAnswer('{"records":[],"dropped":0}'),
129
+ '/network': jsonAnswer('{"ok":true}'),
130
+ '/grants': jsonAnswer('{"ok":true}'),
131
+ };
132
+
133
+ const roleOf = (args) => readPairs(args, '--label')['openchamber.space.role'];
134
+
135
+ const readMounts = (args) => args
136
+ .filter((arg, index) => args[index - 1] === '--mount')
137
+ .map((arg) => {
138
+ const fields = arg.split(',');
139
+ const field = (name) => fields.find((part) => part.startsWith(`${name}=`)).slice(name.length + 1);
140
+ return { volume: field('src'), destination: field('dst'), readOnly: fields.includes('readonly') };
141
+ });
142
+
143
+ /**
144
+ * `failAt(args)` returns true for the call that must fail. `timeoutAt(args)` returns
145
+ * true for the call whose CLI is killed by the timeout: the runner rejects, and the
146
+ * daemon finishes the step later, when the place calls `wait`. `interruptionCode` is the
147
+ * code of that rejection. `resources` seeds
148
+ * existing containers, networks and volumes as `{ kind, name, entry }`. A seeded tools volume
149
+ * with `filled: true` holds its fill marker, and a seeded home volume with `token` holds that server token.
150
+ * `alterContainer(entry)` changes what inspect reports for a new space container.
151
+ * `serverReady: false` makes the server inside every space refuse connections, and
152
+ * `gatekeeperReady: false` does the same for every gatekeeper's control channel.
153
+ * `start` is where the clock of `now()` begins. `wait(ms)` moves that clock, so nothing here sleeps.
154
+ * `beforeFill()` runs before a tools fill ends, so a test can hold it open.
155
+ */
156
+ export function createFakeDocker({
157
+ failAt = () => false,
158
+ timeoutAt = () => false,
159
+ interruptionCode = 'command_timeout',
160
+ resources = [],
161
+ imagePresent = true,
162
+ alterContainer = (entry) => entry,
163
+ serverReady = true,
164
+ gatekeeperReady = true,
165
+ beforeFill = async () => {},
166
+ start = new Date('2026-09-20T08:00:00.000Z'),
167
+ } = {}) {
168
+ let clock = start.getTime();
169
+ const calls = [];
170
+ const late = [];
171
+ const state = new Map(resources.map((resource) => [`${resource.kind}:${resource.name}`, resource]));
172
+ // Volume name to the key in its fill marker. A tools volume name ends with its key.
173
+ const filled = new Map(resources.filter((resource) => resource.filled).map((resource) => [resource.name, resource.name.slice(-16)]));
174
+ // The token is a file in the home volume, so it outlives the container.
175
+ const tokens = new Map(resources.filter((resource) => resource.token).map((resource) => [resource.name, resource.token]));
176
+ // The gatekeeper's program is a file in its tmpfs, so it is gone when the container stops.
177
+ const programs = new Map();
178
+ const homeOf = (container) => state.get(`container:${container}`)?.entry.Mounts.find((mount) => mount.Destination === SPACE_HOME)?.Name ?? container;
179
+ const add = (kind, name, entry) => state.set(`${kind}:${name}`, { kind, name, entry });
180
+ // The docker CLI takes a container name or a container id.
181
+ const containerKey = (nameOrId) => (state.has(`container:${nameOrId}`)
182
+ ? `container:${nameOrId}`
183
+ : Array.from(state.keys()).find((key) => key.startsWith('container:') && state.get(key).entry.Id === nameOrId));
184
+ const nameConflict = (name) => failed(`Error response from daemon: Conflict. The container name "/${name}" is already in use by container "${state.get(`container:${name}`).entry.Id}". You have to remove (or rename) that container to be able to reuse that name.`);
185
+ const ofKind = (kind) => Array.from(state.values()).filter((resource) => resource.kind === kind);
186
+
187
+ const matchesFilters = (resource, args) => {
188
+ const labels = (resource.kind === 'container' ? resource.entry.Config?.Labels : resource.entry.Labels) ?? {};
189
+ return args.every((arg, index) => {
190
+ if (args[index - 1] !== '--filter') return true;
191
+ const pair = arg.slice('label='.length);
192
+ return labels[pair.slice(0, pair.indexOf('='))] === pair.slice(pair.indexOf('=') + 1);
193
+ });
194
+ };
195
+
196
+ // Like the real CLI: entries that exist go to stdout even when another name is missing.
197
+ const inspect = (kind, names) => {
198
+ const keyOf = (name) => (kind === 'container' ? containerKey(name) : `${kind}:${name}`);
199
+ const found = names.map((name) => state.get(keyOf(name))).filter(Boolean).map((resource) => resource.entry);
200
+ const missing = names.filter((name) => !state.has(keyOf(name)));
201
+ const stdout = JSON.stringify(found);
202
+ return missing.length === 0 ? ok(stdout) : failed(missing.map(NOT_FOUND_TEXT[kind]).join('\n'), stdout);
203
+ };
204
+
205
+ const addContainer = (args, { running }) => {
206
+ const name = readFlag(args, '--name');
207
+ // Like the real engine: a name that is in use is refused, and nothing is made.
208
+ if (state.has(`container:${name}`)) return nameConflict(name);
209
+ const mounts = readMounts(args);
210
+ // Like the real engine: a missing `src=` volume is created on the spot, without labels.
211
+ for (const { volume } of mounts) {
212
+ if (!state.has(`volume:${volume}`)) add('volume', volume, { Name: volume, Labels: null });
213
+ }
214
+ const entry = hardenedContainerEntry({
215
+ name,
216
+ labels: readPairs(args, '--label'),
217
+ network: readFlag(args, '--network'),
218
+ mounts,
219
+ env: args.filter((arg, index) => args[index - 1] === '--env'),
220
+ running,
221
+ memoryBytes: Number(readFlag(args, '--memory')),
222
+ tmpfs: args.includes('--tmpfs') ? readFlag(args, '--tmpfs') : undefined,
223
+ aliases: args.includes('--network-alias') ? [readFlag(args, '--network-alias')] : [],
224
+ });
225
+ add('container', name, args[0] === 'create' ? alterContainer(entry) : entry);
226
+ // `docker create` prints the id of the new container.
227
+ return ok(`${entry.Id}\n`);
228
+ };
229
+
230
+ // What runs inside a space. The server inside answers once its token is there.
231
+ // A gatekeeper answers on its control channel once its program is there.
232
+ const execInside = (args, stdin) => {
233
+ const container = args[args.indexOf('--user') + 2];
234
+ const argv = args.slice(args.indexOf('--user') + 3);
235
+ if (!state.get(`container:${container}`)?.entry.State.Running) return failed(`Error response from daemon: container ${container} is not running`);
236
+ if (state.get(`container:${container}`).entry.Config.Labels?.['openchamber.space.role'] === 'gatekeeper') {
237
+ if (argv[0] === IMAGE_SH && argv[2].includes(`${GATEKEEPER_PROGRAM_PATH}.new`)) {
238
+ programs.set(container, String(stdin));
239
+ return ok();
240
+ }
241
+ if (argv[0] !== IMAGE_CURL) return ok();
242
+ if (!gatekeeperReady || !programs.has(container)) return { code: 7, stdout: '', stderr: 'curl: (7) Failed to connect' };
243
+ const path = Object.keys(CONTROL_ANSWERS).find((candidate) => String(stdin).includes(`${candidate}"`));
244
+ return path ? ok(CONTROL_ANSWERS[path]) : ok('HTTP/1.1 404 Not Found\r\n\r\n{"error":"no such control endpoint"}');
245
+ }
246
+ const home = homeOf(container);
247
+ if (argv[0] === IMAGE_CURL) return serverReady && tokens.has(home) ? ok(HEALTHY_ANSWER) : { code: 7, stdout: '', stderr: 'curl: (7) Failed to connect' };
248
+ if (argv[0] === IMAGE_SH && argv[2].includes(`${SPACE_TOKEN_PATH}.new`)) {
249
+ tokens.set(home, String(stdin));
250
+ return ok();
251
+ }
252
+ if (argv[0] === IMAGE_CAT && argv[1] === SPACE_TOKEN_PATH) return ok(`${tokens.get(home)}\n`);
253
+ if (argv[0] === IMAGE_SH) return ok();
254
+ return ok('1000\n');
255
+ };
256
+
257
+ // The three one-shots: volume ownership, the tools filler, and the fill marker check.
258
+ const runOneShot = (args, { stuck, fails }) => {
259
+ const role = roleOf(args);
260
+ const [toolsMount] = readMounts(args);
261
+ // Measured with CLI 29.3.0: a docker CLI that cannot reach the daemon exits with 1.
262
+ if (role === 'tools-check' && fails) return failed('failed to connect to the docker API at tcp://127.0.0.1:1');
263
+ if (role === 'tools-check') {
264
+ // The check script exits with 42 for a missing marker, and prints the marker otherwise.
265
+ const key = filled.get(toolsMount.volume);
266
+ return key ? ok(key) : { code: 42, stdout: '', stderr: '' };
267
+ }
268
+ // The one-shot removes itself when it ends. A stuck one is still there.
269
+ const result = addContainer(args, { running: true });
270
+ if (result.code !== 0) return result;
271
+ if (!stuck) state.delete(`container:${readFlag(args, '--name')}`);
272
+ if (fails) return failed('Error response from daemon: simulated failure');
273
+ if (role === 'tools-fill' && !stuck) filled.set(toolsMount.volume, readPairs(args, '--label')['openchamber.space.tools.key']);
274
+ return result;
275
+ };
276
+
277
+ const answer = (args, { stuck = false, fails = false, stdin = '' } = {}) => {
278
+ const [first, second] = args;
279
+ if (first === 'run') return runOneShot(args, { stuck, fails });
280
+ // A failed `docker create` can still leave the container behind.
281
+ if (first === 'create') return fails ? (addContainer(args, { running: false }), failed('Error response from daemon: simulated failure')) : addContainer(args, { running: false });
282
+ if (fails) return failed('Error response from daemon: simulated failure');
283
+ if (first === 'inspect') return inspect('container', args.slice(3));
284
+ if (first === 'image' && second === 'inspect') return imagePresent ? ok('[{}]') : failed(NOT_FOUND_TEXT.image(args[2]), '[]');
285
+ if (first === 'pull') return ok();
286
+ if (first === 'ps') return ok(ofKind('container').filter((resource) => matchesFilters(resource, args)).map((resource) => resource.name).join('\n'));
287
+ if (first === 'rm') {
288
+ const name = args[args.length - 1];
289
+ // Like the real engine: without --force a running container stays.
290
+ if (!args.includes('--force') && state.get(containerKey(name))?.entry.State.Running) {
291
+ return failed(`Error response from daemon: cannot remove container "/${name}": container is running: stop the container before removing or force remove`);
292
+ }
293
+ return state.delete(containerKey(name)) ? ok(name) : failed(NOT_FOUND_TEXT.container(name));
294
+ }
295
+ if (first === 'stop' || first === 'start') {
296
+ const resource = state.get(containerKey(second));
297
+ if (!resource) return failed(NOT_FOUND_TEXT.container(second));
298
+ resource.entry.State.Running = first === 'start';
299
+ // A tmpfs is empty again after a stop, so the gatekeeper's program is gone with it.
300
+ if (first === 'stop') programs.delete(resource.name);
301
+ return ok(second);
302
+ }
303
+ if (first === 'rename') {
304
+ const [, from, to] = args;
305
+ const resource = state.get(`container:${from}`);
306
+ if (!resource) return failed(NOT_FOUND_TEXT.container(from));
307
+ if (state.has(`container:${to}`)) return nameConflict(to);
308
+ state.delete(`container:${from}`);
309
+ add('container', to, { ...resource.entry, Name: `/${to}` });
310
+ return ok();
311
+ }
312
+ if (first === 'exec') return execInside(args, stdin);
313
+ if (first === 'network' && second === 'connect') {
314
+ const [, , network, container] = args;
315
+ const resource = state.get(containerKey(container));
316
+ if (!resource) return failed(NOT_FOUND_TEXT.container(container));
317
+ if (!state.has(`network:${network}`)) return failed(NOT_FOUND_TEXT.network(network));
318
+ resource.entry.NetworkSettings.Networks[network] = {};
319
+ return ok();
320
+ }
321
+ if (first === 'network' || first === 'volume') {
322
+ const name = args[args.length - 1];
323
+ if (second === 'inspect') return inspect(first, args.slice(2));
324
+ if (second === 'ls') return ok(ofKind(first).filter((resource) => matchesFilters(resource, args)).map((resource) => resource.name).join('\n'));
325
+ if (second === 'rm') {
326
+ // Like the real engine: a volume that a container mounts cannot go.
327
+ const user = first === 'volume' && ofKind('container').find((resource) => resource.entry.Mounts.some((mount) => mount.Name === name));
328
+ if (user) return failed(`Error response from daemon: remove ${name}: volume is in use - [${user.name}]`);
329
+ filled.delete(name);
330
+ tokens.delete(name);
331
+ return state.delete(`${first}:${name}`) ? ok(name) : failed(NOT_FOUND_TEXT[first](name));
332
+ }
333
+ if (second === 'create') {
334
+ const labels = readPairs(args, '--label');
335
+ if (first === 'volume') {
336
+ add(first, name, { Name: name, Labels: labels });
337
+ } else {
338
+ add(first, name, args.includes('--internal')
339
+ ? internalNetworkEntry({ name, labels, options: readPairs(args, '--opt') })
340
+ : bridgeNetworkEntry({ name, labels }));
341
+ }
342
+ return ok(name);
343
+ }
344
+ }
345
+ throw new Error(`fake docker has no answer for: ${args.join(' ')}`);
346
+ };
347
+
348
+ const runCommand = async (file, args, options) => {
349
+ calls.push({ file, args, options });
350
+ if (timeoutAt(args)) {
351
+ late.push(args);
352
+ throw new SpaceError(interruptionCode, `docker ${args[0]} was stopped before it finished`);
353
+ }
354
+ if (args[0] === 'run' && roleOf(args) === 'tools-fill') await beforeFill();
355
+ return answer(args, { fails: failAt(args), stdin: options?.stdin });
356
+ };
357
+
358
+ /** Give this to the place as `wait`. Time passes, the daemon finishes the timed-out steps, and a one-shot stays running. */
359
+ const wait = async (milliseconds = 0) => {
360
+ clock += milliseconds;
361
+ for (const args of late.splice(0)) {
362
+ answer(args, { stuck: true });
363
+ }
364
+ };
365
+
366
+ return {
367
+ runCommand,
368
+ wait,
369
+ /** Give this to the place as `now`. */
370
+ now: () => new Date(clock),
371
+ calls,
372
+ names: () => Array.from(state.keys()),
373
+ token: (container) => tokens.get(homeOf(container)),
374
+ };
375
+ }
@@ -0,0 +1,103 @@
1
+ // Test support. A place that keeps spaces in a Map, for the contract suite and the manager tests.
2
+ //
3
+ // It models the two containers of a space, the space and its gatekeeper, because the contract
4
+ // suite is meant to fail a place that has no gatekeeper. A place that answered both targets from
5
+ // one container would pass nothing here.
6
+
7
+ import crypto from 'node:crypto';
8
+
9
+ import { SpaceError } from '../errors.js';
10
+ import { ROLE_GATEKEEPER, ROLE_SPACE, requireSpaceId } from '../labels.js';
11
+
12
+ const TARGETS = [ROLE_SPACE, ROLE_GATEKEEPER];
13
+
14
+ // What `execArgv` hands out: a one-line Node program that answers for one container, the way
15
+ // `exec` below does. The container's hostname is its first argument and the command follows.
16
+ // One line, because the argv may go into a git `ext::` URL, which cannot carry a newline.
17
+ const CONTAINER_PROGRAM = [
18
+ 'const [hostname, ...command] = process.argv.slice(1);',
19
+ 'const line = command.join(" ");',
20
+ 'if (line === "cat /etc/hostname") process.stdout.write(hostname + "\\n");',
21
+ 'else process.exitCode = 127;',
22
+ ].join(' ');
23
+
24
+ export function createMemoryPlace({ id = 'memory' } = {}) {
25
+ const spaces = new Map();
26
+ // Every container of every space that was ever made here, by `<space id>:<role>`.
27
+ const containers = new Map();
28
+
29
+ const requireSpace = (spaceId) => {
30
+ const space = spaces.get(requireSpaceId(spaceId));
31
+ if (!space) {
32
+ throw new SpaceError('space_not_found', `Space ${spaceId} does not exist`);
33
+ }
34
+ return space;
35
+ };
36
+
37
+ const requireContainer = (spaceId, target = ROLE_SPACE) => {
38
+ if (!TARGETS.includes(target)) {
39
+ throw new SpaceError('invalid_exec_target', `A command runs in the space or in its gatekeeper, not in '${target}'`);
40
+ }
41
+ const container = containers.get(`${requireSpaceId(spaceId)}:${target}`);
42
+ if (!container) {
43
+ throw new SpaceError(target === ROLE_GATEKEEPER ? 'gatekeeper_missing' : 'space_not_found', `Space ${spaceId} has no ${target} container`);
44
+ }
45
+ return container;
46
+ };
47
+
48
+ return {
49
+ id,
50
+ check: async () => ({ available: true, version: 'memory', os: 'none', arch: 'none', hostIsolation: true }),
51
+ create: async ({ id: spaceId, name, project, created }) => {
52
+ if (spaces.has(requireSpaceId(spaceId))) {
53
+ throw new SpaceError('space_name_taken', `Space ${spaceId} already exists`);
54
+ }
55
+ spaces.set(spaceId, { id: spaceId, name, project, created, state: 'running', orphans: [], damaged: false, missing: [] });
56
+ for (const target of TARGETS) {
57
+ // A container has a hostname of its own, as a real one does, so a caller can tell them apart.
58
+ containers.set(`${spaceId}:${target}`, { hostname: crypto.randomBytes(6).toString('hex') });
59
+ }
60
+ },
61
+ list: async () => Array.from(spaces.values(), (space) => ({ ...space })),
62
+ exec: async (spaceId, argv, options = {}) => {
63
+ // The container, and not the space record: a place that removed the space and left its
64
+ // gatekeeper running would still answer here, and the contract suite must see that.
65
+ const container = requireContainer(spaceId, options.target);
66
+ const command = argv.join(' ');
67
+ if (command === 'id -u') {
68
+ return { code: 0, stdout: '1000\n', stderr: '' };
69
+ }
70
+ if (command === 'cat /etc/hostname') {
71
+ return { code: 0, stdout: `${container.hostname}\n`, stderr: '' };
72
+ }
73
+ // The work directory of the space is in the space and not in its gatekeeper.
74
+ if (command === `test -d /spaces/${spaceId}`) {
75
+ return { code: options.target === ROLE_GATEKEEPER ? 1 : 0, stdout: '', stderr: '' };
76
+ }
77
+ return { code: 127, stdout: '', stderr: 'not found' };
78
+ },
79
+ // Added in stage 3a. The space container only, and refused like `exec` when it is gone.
80
+ execArgv: async (spaceId) => {
81
+ const { hostname } = requireContainer(spaceId);
82
+ if (requireSpace(spaceId).state !== 'running') {
83
+ throw new SpaceError('space_not_running', `Space ${spaceId} is stopped`);
84
+ }
85
+ return [process.execPath, '-e', CONTAINER_PROGRAM, hostname];
86
+ },
87
+ stop: async (spaceId) => { requireSpace(spaceId).state = 'exited'; },
88
+ start: async (spaceId) => { requireSpace(spaceId).state = 'running'; },
89
+ remove: async (spaceId) => {
90
+ const removed = [];
91
+ for (const target of TARGETS) {
92
+ if (containers.delete(`${requireSpaceId(spaceId)}:${target}`)) removed.push({ kind: 'container', name: `${spaceId}-${target}` });
93
+ }
94
+ if (spaces.delete(spaceId)) removed.push({ kind: 'space', name: spaceId });
95
+ return { removed, failed: [] };
96
+ },
97
+ verify: async (spaceId) => {
98
+ requireSpace(spaceId);
99
+ requireContainer(spaceId, ROLE_GATEKEEPER);
100
+ return [];
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,77 @@
1
+ // A development build inside a space: the local `web` and `sdk` packages, packed and installed
2
+ // by the filler. Runs only with OPENCHAMBER_TEST_DOCKER_PACKED=1, because it compiles the sdk
3
+ // and needs the workspace dependencies installed.
4
+
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+
9
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
10
+
11
+ import { createSpaceId, hashProjectDirectory } from '../labels.js';
12
+ import { TOOLS_MOUNT_PATH } from '../layout.js';
13
+ import { runCommand } from '../run-command.js';
14
+ import { createSpaceServerChannel } from '../space-server.js';
15
+ import { packLocalTools } from '../tools-pack.js';
16
+ import { createPackedToolsSource, readHostToolVersions } from '../tools.js';
17
+ import { createLiveDockerPlace } from './docker-live-support.js';
18
+
19
+ const PACKED_ENABLED = process.env.OPENCHAMBER_TEST_DOCKER_PACKED === '1';
20
+ const CREATE_TIMEOUT_MS = 25 * 60_000;
21
+
22
+ describe.skipIf(!PACKED_ENABLED)('development build inside a space: docker (live)', () => {
23
+ const spec = {
24
+ id: createSpaceId(),
25
+ name: 'Development build',
26
+ project: hashProjectDirectory('/development/build/project'),
27
+ created: new Date().toISOString(),
28
+ memoryBytes: 2 * 1024 * 1024 * 1024,
29
+ };
30
+ let directory;
31
+ let place;
32
+ let host;
33
+ let dispose = async () => {};
34
+
35
+ beforeAll(async () => {
36
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-packed-test-'));
37
+ const tarballs = await packLocalTools({ runCommand, outputDirectory: directory });
38
+ const toolsSource = createPackedToolsSource({ ...tarballs, openCodeVersion: readHostToolVersions().openCodeVersion });
39
+ ({ place, host, dispose } = createLiveDockerPlace({ toolsSource }));
40
+ await place.create(spec);
41
+ }, CREATE_TIMEOUT_MS);
42
+
43
+ afterAll(async () => {
44
+ await dispose();
45
+ if (directory) fs.rmSync(directory, { recursive: true, force: true });
46
+ });
47
+
48
+ it('is healthy and verifies clean', async () => {
49
+ const health = await createSpaceServerChannel({ exec: place.exec }).request(spec.id, { path: '/health' });
50
+ expect(JSON.parse(health.body)).toMatchObject({ isOpenCodeReady: true });
51
+ expect(await place.verify(spec.id)).toEqual([]);
52
+ });
53
+
54
+ it('labels the tools volume as a development build', async () => {
55
+ const [volume] = (await host.volumes()).filter((name) => name.startsWith('openchamber-tools-'));
56
+ const inspect = await runCommand('docker', ['volume', 'inspect', volume]);
57
+ expect(JSON.parse(inspect.stdout)[0].Labels['openchamber.space.tools.description']).toBe('development build');
58
+ });
59
+
60
+ it('runs the local sdk everywhere in the tree, not the published one with the same version', async () => {
61
+ const sdk = fs.readFileSync(new URL('../../../../../sdk/package.json', import.meta.url), 'utf8');
62
+ const copies = await place.exec(spec.id, ['sh', '-c', `find ${TOOLS_MOUNT_PATH}/node_modules -type d -path '*/@openchamber/sdk' -not -path '*/sdk/*'`]);
63
+ expect(copies.stdout).toBe(`${TOOLS_MOUNT_PATH}/node_modules/@openchamber/sdk\n`);
64
+
65
+ // The lock file names where each package came from: the sdk from the local tarball, never from the registry.
66
+ const lock = JSON.parse((await place.exec(spec.id, ['cat', `${TOOLS_MOUNT_PATH}/package-lock.json`])).stdout);
67
+ const installed = lock.packages['node_modules/@openchamber/sdk'];
68
+ expect(installed.version).toBe(JSON.parse(sdk).version);
69
+ expect(installed.resolved).toMatch(/^file:.*openchamber-sdk\.tgz$/);
70
+ expect(lock.packages['node_modules/@openchamber/web'].resolved).toMatch(/^file:.*openchamber-web\.tgz$/);
71
+ });
72
+
73
+ it('leaves the tarballs out of the volume', async () => {
74
+ const found = await place.exec(spec.id, ['sh', '-c', `find ${TOOLS_MOUNT_PATH} -maxdepth 1 -name '*.tgz'`]);
75
+ expect(found).toMatchObject({ code: 0, stdout: '' });
76
+ });
77
+ });
@@ -0,0 +1,43 @@
1
+ export const REQUIRED_PLACE_METHODS = ['check', 'create', 'list', 'exec', 'execArgv', 'stop', 'start', 'remove', 'verify'];
2
+
3
+ export function createPlaceRegistry(initialPlaces = []) {
4
+ const places = new Map();
5
+ let sealed = false;
6
+
7
+ const register = (place) => {
8
+ if (sealed) {
9
+ throw new Error('Place registry is sealed; no further registrations allowed');
10
+ }
11
+ const key = String(place?.id ?? '').trim().toLowerCase();
12
+ if (key.length === 0) {
13
+ throw new Error('Place must define a non-empty id');
14
+ }
15
+ for (const method of REQUIRED_PLACE_METHODS) {
16
+ if (!(place[method] instanceof Function)) {
17
+ throw new Error(`Place '${key}' must implement ${method}()`);
18
+ }
19
+ }
20
+ if (places.has(key)) {
21
+ throw new Error(`Place '${key}' is already registered`);
22
+ }
23
+ places.set(key, place);
24
+ return place;
25
+ };
26
+
27
+ const get = (placeId) => places.get(String(placeId ?? '').trim().toLowerCase()) ?? null;
28
+
29
+ const list = () => Array.from(places.values());
30
+
31
+ for (const place of initialPlaces) {
32
+ register(place);
33
+ }
34
+
35
+ const seal = () => { sealed = true; };
36
+
37
+ return {
38
+ register,
39
+ get,
40
+ list,
41
+ seal,
42
+ };
43
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { REQUIRED_PLACE_METHODS, createPlaceRegistry } from './registry.js';
4
+
5
+ const createPlace = (id) => ({
6
+ id,
7
+ ...Object.fromEntries(REQUIRED_PLACE_METHODS.map((method) => [method, async () => null])),
8
+ });
9
+
10
+ describe('createPlaceRegistry', () => {
11
+ it('registers places and finds them by id, ignoring case and spaces', () => {
12
+ const docker = createPlace('docker');
13
+ const registry = createPlaceRegistry([docker]);
14
+
15
+ expect(registry.get(' Docker ')).toBe(docker);
16
+ expect(registry.list()).toEqual([docker]);
17
+ });
18
+
19
+ it('returns null for an unknown or missing id', () => {
20
+ const registry = createPlaceRegistry([createPlace('docker')]);
21
+
22
+ expect(registry.get('cluster')).toBeNull();
23
+ expect(registry.get(undefined)).toBeNull();
24
+ });
25
+
26
+ it('rejects a place without an id', () => {
27
+ expect(() => createPlaceRegistry([createPlace(' ')])).toThrow('non-empty id');
28
+ expect(() => createPlaceRegistry([null])).toThrow('non-empty id');
29
+ });
30
+
31
+ it.each(REQUIRED_PLACE_METHODS)('rejects a place without %s()', (method) => {
32
+ const place = createPlace('docker');
33
+ delete place[method];
34
+
35
+ expect(() => createPlaceRegistry([place])).toThrow(`must implement ${method}()`);
36
+ });
37
+
38
+ it('rejects a second place with the same id', () => {
39
+ const registry = createPlaceRegistry([createPlace('docker')]);
40
+
41
+ expect(() => registry.register(createPlace('DOCKER'))).toThrow('already registered');
42
+ });
43
+
44
+ it('accepts nothing after it is sealed', () => {
45
+ const registry = createPlaceRegistry();
46
+ registry.seal();
47
+
48
+ expect(() => registry.register(createPlace('docker'))).toThrow('sealed');
49
+ });
50
+ });