@vantran-se/openchamber-web 1.23.0 → 1.24.3

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 (362) hide show
  1. package/bin/cli.test.js +29 -6
  2. package/bin/lib/cli-http.js +5 -1
  3. package/bin/lib/commands-status.js +5 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-DFg2wnpo.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
  5. package/dist/assets/DiffView-COVQhWGd.js +4 -0
  6. package/dist/assets/FilesView-B1V5JxFX.js +13 -0
  7. package/dist/assets/GitView-CDOJy5Ay.js +6 -0
  8. package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
  9. package/dist/assets/{JsonTreeView-BKdsjg5B.js → JsonTreeView-D0C9nU7K.js} +1 -1
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
  72. package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
  73. package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
  75. package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
  76. package/dist/assets/PluginPane-B4tKno2h.js +3 -0
  77. package/dist/assets/SettingsView-DEaMulN9.js +29 -0
  78. package/dist/assets/{SettingsWindow-DVTdDSol.js → SettingsWindow-FLDJXOgQ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
  80. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
  83. package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
  84. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
  85. package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
  87. package/dist/assets/de-DgxgFyGG.js +10 -0
  88. package/dist/assets/es-BFsZj5F_.js +16 -0
  89. package/dist/assets/fr-jdZ5KKQi.js +14 -0
  90. package/dist/assets/{index-BB7ryz8y.js → index-CKHjPKbq.js} +1 -1
  91. package/dist/assets/index-Cbci4i1W.js +40 -0
  92. package/dist/assets/index-DqaMWNrC.css +1 -0
  93. package/dist/assets/{isDiffPlainText-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
  94. package/dist/assets/{isFilePlainText-Cc4KFSYi.js → isFilePlainText-CL2vsrIZ.js} +1 -1
  95. package/dist/assets/ja-C5kKrhO4.js +16 -0
  96. package/dist/assets/ko-DXDTT_0O.js +16 -0
  97. package/dist/assets/main-CZALtVek.js +896 -0
  98. package/dist/assets/main-CgXMBNvG.js +2 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
  101. package/dist/assets/miniChat-CwTmDixi.js +2 -0
  102. package/dist/assets/mobile-CLf2vCtO.js +2 -0
  103. package/dist/assets/patchFileDiff-Bsba8KFp.js +13 -0
  104. package/dist/assets/pl-D-7x4Y8Q.js +16 -0
  105. package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
  107. package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
  109. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +1 -0
  110. package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
  111. package/dist/assets/surface-B6n7wJtt.js +1 -0
  112. package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
  113. package/dist/assets/tr-Dr-oJ22L.js +7 -0
  114. package/dist/assets/uk-9Nx83UcI.js +16 -0
  115. package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
  116. package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
  117. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
  118. package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  119. package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
  120. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  121. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  122. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  123. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  124. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  125. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  126. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  127. package/dist/assets/vendor-zod-CgAdXWZO.js +39 -0
  128. package/dist/assets/{web-components-CdGfR2XJ.js → web-components-B8IcVQAA.js} +2 -2
  129. package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
  130. package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
  131. package/dist/index.html +6 -4
  132. package/dist/mini-chat.html +6 -4
  133. package/dist/mobile.html +6 -4
  134. package/package.json +12 -9
  135. package/server/built-in-extensions/registry.json +4 -0
  136. package/server/index.d.ts +2 -0
  137. package/server/index.js +47 -23
  138. package/server/lib/agent-memory/runtime.js +7 -2
  139. package/server/lib/agent-memory/runtime.test.js +22 -0
  140. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  141. package/server/lib/agent-tool/runtime.js +62 -15
  142. package/server/lib/agent-tool/runtime.test.js +88 -2
  143. package/server/lib/changelog/update-notes.js +2 -1
  144. package/server/lib/changelog/update-notes.test.js +19 -0
  145. package/server/lib/event-stream/DOCUMENTATION.md +26 -0
  146. package/server/lib/event-stream/delta-coalescer.js +0 -0
  147. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  148. package/server/lib/event-stream/global-hub.js +68 -13
  149. package/server/lib/event-stream/global-hub.test.js +182 -0
  150. package/server/lib/event-stream/global-ws-bridge.js +16 -18
  151. package/server/lib/event-stream/index.js +4 -0
  152. package/server/lib/event-stream/protocol.js +21 -7
  153. package/server/lib/event-stream/runtime.js +9 -6
  154. package/server/lib/event-stream/runtime.test.js +60 -0
  155. package/server/lib/fs/DOCUMENTATION.md +10 -1
  156. package/server/lib/fs/git-process.test.js +77 -0
  157. package/server/lib/fs/routes.js +58 -5
  158. package/server/lib/fs/routes.test.js +116 -0
  159. package/server/lib/fs/search.js +2 -1
  160. package/server/lib/git/DOCUMENTATION.md +30 -6
  161. package/server/lib/git/routes.js +37 -11
  162. package/server/lib/git/routes.test.js +78 -0
  163. package/server/lib/git/serial-refresh.js +93 -0
  164. package/server/lib/git/serial-refresh.test.js +112 -0
  165. package/server/lib/git/service.js +588 -74
  166. package/server/lib/git/service.test.js +492 -0
  167. package/server/lib/github/DOCUMENTATION.md +5 -0
  168. package/server/lib/github/pr-status.js +5 -5
  169. package/server/lib/github/pr-status.test.js +1 -1
  170. package/server/lib/github/routes.js +6 -6
  171. package/server/lib/guests/DOCUMENTATION.md +68 -0
  172. package/server/lib/guests/auth-store.js +184 -0
  173. package/server/lib/guests/auth-store.test.js +112 -0
  174. package/server/lib/guests/background.test.js +83 -0
  175. package/server/lib/guests/builtins.js +34 -0
  176. package/server/lib/guests/builtins.test.js +174 -0
  177. package/server/lib/guests/catalog.js +477 -0
  178. package/server/lib/guests/catalog.test.js +390 -0
  179. package/server/lib/guests/clone.js +297 -0
  180. package/server/lib/guests/extract-zip.js +136 -0
  181. package/server/lib/guests/extract-zip.test.js +109 -0
  182. package/server/lib/guests/files.js +308 -0
  183. package/server/lib/guests/files.test.js +262 -0
  184. package/server/lib/guests/grant-scope.js +81 -0
  185. package/server/lib/guests/grant-scope.test.js +75 -0
  186. package/server/lib/guests/host-session.js +73 -0
  187. package/server/lib/guests/host-session.test.js +30 -0
  188. package/server/lib/guests/html-styles.js +6 -0
  189. package/server/lib/guests/html-styles.test.js +45 -0
  190. package/server/lib/guests/html-tokens.js +73 -0
  191. package/server/lib/guests/html-tokens.test.js +53 -0
  192. package/server/lib/guests/install.js +406 -0
  193. package/server/lib/guests/install.test.js +338 -0
  194. package/server/lib/guests/oauth.js +501 -0
  195. package/server/lib/guests/oauth.test.js +481 -0
  196. package/server/lib/guests/persist.js +336 -0
  197. package/server/lib/guests/persist.test.js +288 -0
  198. package/server/lib/guests/request.js +112 -0
  199. package/server/lib/guests/request.test.js +206 -0
  200. package/server/lib/guests/routes.js +838 -0
  201. package/server/lib/guests/service.js +538 -0
  202. package/server/lib/guests/service.test.js +277 -0
  203. package/server/lib/guests/sockets.js +129 -0
  204. package/server/lib/guests/sockets.test.js +110 -0
  205. package/server/lib/guests/ssh-install.test.js +188 -0
  206. package/server/lib/guests/storage.js +68 -0
  207. package/server/lib/guests/storage.test.js +59 -0
  208. package/server/lib/guests/updates.js +294 -0
  209. package/server/lib/guests/updates.test.js +334 -0
  210. package/server/lib/guests/upload.js +50 -0
  211. package/server/lib/guests/upload.test.js +131 -0
  212. package/server/lib/linear/DOCUMENTATION.md +1 -1
  213. package/server/lib/message-queue/DOCUMENTATION.md +17 -1
  214. package/server/lib/message-queue/runtime.js +34 -1
  215. package/server/lib/message-queue/runtime.test.js +83 -6
  216. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  217. package/server/lib/notifications/emitter-runtime.js +4 -3
  218. package/server/lib/notifications/routes.js +5 -0
  219. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  220. package/server/lib/openchamber-sessions/routes.js +62 -29
  221. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  222. package/server/lib/opencode/DOCUMENTATION.md +73 -5
  223. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  224. package/server/lib/opencode/core-routes.js +39 -1
  225. package/server/lib/opencode/env-runtime.js +5 -1
  226. package/server/lib/opencode/env-runtime.test.js +72 -0
  227. package/server/lib/opencode/feature-routes-runtime.js +30 -1
  228. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  229. package/server/lib/opencode/lifecycle.js +58 -70
  230. package/server/lib/opencode/lifecycle.test.js +29 -0
  231. package/server/lib/opencode/managed-plugin-config.js +1 -1
  232. package/server/lib/opencode/openchamber-routes.js +72 -14
  233. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  234. package/server/lib/opencode/proxy.js +33 -0
  235. package/server/lib/opencode/session-runtime.js +68 -0
  236. package/server/lib/opencode/session-runtime.test.js +43 -0
  237. package/server/lib/opencode/settings-helpers.js +11 -0
  238. package/server/lib/opencode/settings-helpers.test.js +24 -2
  239. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  240. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  241. package/server/lib/opencode/settings-registry.json +18 -0
  242. package/server/lib/opencode/settings-runtime.js +36 -9
  243. package/server/lib/opencode/settings-runtime.test.js +158 -2
  244. package/server/lib/opencode/shutdown-runtime.js +3 -0
  245. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  246. package/server/lib/opencode/theme-archive.js +53 -0
  247. package/server/lib/opencode/theme-catalog.js +171 -0
  248. package/server/lib/opencode/theme-catalog.test.js +125 -0
  249. package/server/lib/opencode/theme-import.test.js +144 -0
  250. package/server/lib/opencode/theme-runtime.js +100 -28
  251. package/server/lib/opencode/theme-runtime.test.js +15 -0
  252. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  253. package/server/lib/permission-auto-accept/runtime.js +13 -0
  254. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  255. package/server/lib/project-context/DOCUMENTATION.md +13 -3
  256. package/server/lib/project-context/runtime.js +8 -2
  257. package/server/lib/project-context/runtime.test.js +54 -0
  258. package/server/lib/projects/DOCUMENTATION.md +2 -1
  259. package/server/lib/projects/project-config.js +40 -11
  260. package/server/lib/projects/project-config.test.js +97 -0
  261. package/server/lib/projects/project-id.js +26 -0
  262. package/server/lib/quota/DOCUMENTATION.md +0 -1
  263. package/server/lib/quota/providers/index.js +0 -7
  264. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  265. package/server/lib/realtime-proxy.js +2 -0
  266. package/server/lib/relay/DOCUMENTATION.md +20 -0
  267. package/server/lib/relay/host-client.js +7 -0
  268. package/server/lib/relay/host-client.test.js +14 -0
  269. package/server/lib/routing/DOCUMENTATION.md +83 -0
  270. package/server/lib/routing/defaults.js +98 -0
  271. package/server/lib/routing/feature-flag.js +11 -0
  272. package/server/lib/routing/history.js +34 -0
  273. package/server/lib/routing/jev.js +98 -0
  274. package/server/lib/routing/routes.http.test.js +100 -0
  275. package/server/lib/routing/routes.js +81 -0
  276. package/server/lib/routing/runtime.js +217 -0
  277. package/server/lib/routing/runtime.test.js +188 -0
  278. package/server/lib/routing/store.js +249 -0
  279. package/server/lib/routing/store.test.js +96 -0
  280. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  281. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  282. package/server/lib/scheduled-tasks/runtime.js +14 -4
  283. package/server/lib/scheduled-tasks/runtime.test.js +67 -0
  284. package/server/lib/security/request-security.js +9 -5
  285. package/server/lib/security/request-security.test.js +37 -0
  286. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  287. package/server/lib/session-assist/context.js +127 -0
  288. package/server/lib/session-assist/context.test.js +132 -0
  289. package/server/lib/session-assist/prompt.js +54 -0
  290. package/server/lib/session-assist/runtime.js +111 -257
  291. package/server/lib/session-assist/runtime.test.js +203 -0
  292. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  293. package/server/lib/small-model/call.js +37 -6
  294. package/server/lib/small-model/call.test.js +182 -0
  295. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  296. package/server/lib/terminal/runtime.js +27 -5
  297. package/server/lib/terminal/runtime.test.js +67 -0
  298. package/server/lib/terminal/shutdown.js +106 -0
  299. package/server/lib/terminal/shutdown.test.js +84 -0
  300. package/server/lib/tts/service.test.js +78 -0
  301. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  302. package/server/lib/ui-auth/session-cookie.js +51 -0
  303. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  304. package/server/lib/ui-auth/ui-auth.js +56 -17
  305. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  306. package/server/lib/walkthrough/DOCUMENTATION.md +15 -5
  307. package/server/lib/walkthrough/pr-source.test.js +21 -0
  308. package/server/lib/walkthrough/pull-request.js +7 -4
  309. package/server/lib/walkthrough/pull-request.test.js +16 -0
  310. package/server/lib/walkthrough/routes.js +19 -0
  311. package/server/lib/walkthrough/routes.test.js +26 -0
  312. package/server/lib/walkthrough/sources.js +13 -2
  313. package/server/opencode-worktree-readiness.test.js +130 -0
  314. package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
  315. package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
  316. package/dist/assets/FilesView-BdTOpew9.js +0 -17
  317. package/dist/assets/GitView-Dqf9vHJh.js +0 -6
  318. package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
  320. package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
  321. package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
  322. package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
  323. package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
  324. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  325. package/dist/assets/de-C8jV7J9y.js +0 -10
  326. package/dist/assets/es-BI6J1M5R.js +0 -16
  327. package/dist/assets/fr-D6oDFt7E.js +0 -14
  328. package/dist/assets/index-BqhiCmYS.js +0 -39
  329. package/dist/assets/index-FqSOBz0J.css +0 -1
  330. package/dist/assets/ja-C_4i5EQu.js +0 -16
  331. package/dist/assets/ko-BFbOTpbq.js +0 -16
  332. package/dist/assets/main-C9ETLKFc.js +0 -2
  333. package/dist/assets/main-CJmb2dF8.js +0 -930
  334. package/dist/assets/markdownCore-DVuZpnli.js +0 -5
  335. package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
  336. package/dist/assets/mobile-BUT6-lo1.js +0 -2
  337. package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
  338. package/dist/assets/pl-COFvT56l.js +0 -16
  339. package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
  340. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  341. package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
  342. package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
  343. package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
  344. package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
  345. package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
  346. package/dist/assets/surface-BvRzS58U.js +0 -1
  347. package/dist/assets/tr-B8Q0Go2l.js +0 -7
  348. package/dist/assets/uk-Dk4a-Fn-.js +0 -16
  349. package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
  350. package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
  351. package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
  352. package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
  353. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  354. package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
  355. package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
  356. package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
  357. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  358. package/server/lib/mcp-reconnect/runtime.js +0 -122
  359. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
  360. package/server/lib/quota/providers/crof.js +0 -96
  361. package/server/lib/quota/providers/crof.test.js +0 -98
  362. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -0,0 +1,68 @@
1
+ # Guest panels
2
+
3
+ ## Purpose
4
+
5
+ OpenChamber-owned install and static serve for iframe guests. This is not the OpenCode plugin manager (`plugin-routes.js` / Settings → Plugins).
6
+
7
+ ## Routes
8
+
9
+ - `POST /api/guests/:id/storage` accepts the SDK's `GuestStorageRequest` and returns `GuestStorageResult`. It requires an installed, enabled, fully approved guest with either a panel or background entry, but no additional capability. The UI host supplies the frame's own identity. Guest-scoped asset tokens cannot call this route. Data lives beside `extensions.json` in `guest-storage/<id>.json`. Each key is 1 to 128 characters, each JSON value at most 64 KiB UTF-8, the whole namespace at most 2 MiB and 2,000 keys. `storage.js` owns serialized authorization/read/write/removal; writes are atomic and failures preserve prior bytes. `uninstallGuest` removes storage for every install source. Missing keys return `found: false`, including after removal; stored JSON null returns `found: true`.
10
+ - `GET /api/guests` also exposes `pageEntry` and optional `pageTitle` for `contributes.page`. Full-screen page HTML and its built scripts pass the same install checks as attach HTML. No new serving/auth path is required.
11
+
12
+ - `GET /api/guests` returns installed guests. `attach` is `"panel"` or `"dialog"` when declared, and `attachEntry` is the dialog's separate HTML. `version` is the package semver. `integration` is the public slice: name, description, `auth`, and settings; OAuth URLs and tokens stay on the server. `service` includes `runtime`, optional `permissions`, optional `socketBindings`, and `granted`. `capabilities` carries `{ requested, granted }`, derived through the SDK's `requestedGuestCapabilities`. `actions`, `commands`, `tools`, and `filesystem` retain their parsed declarations; UI owns menu placement, command collisions, and tool precedence. `entry` is the optional visible panel and `backgroundEntry` is independent on-demand execution HTML. Background-only extensions get no rail surface; tools-only extensions with neither entry mount no frame. A guest without every requested grant is inactive until approval. `enabled: false` pauses it. A failed listing is 500, never an empty list.
13
+ - `GET /api/guests/:id/oauth/status` — `{ connected, account, hasClient, settings, redirectUri }`. No secrets. Host Linear merges first-party Linear status. `redirectUri` is empty unless `auth` is `oauth`.
14
+ - `PUT /api/guests/:id/oauth/client` — client id and optional secret from the Integrations form. OAuth guests only.
15
+ - `PUT /api/guests/:id/token` — pasted API token. Token guests only. The host probes the declared account path. The token never goes to the iframe.
16
+ - `PUT /api/guests/:id/settings` — declared keys only. The Integrations card calls it when a declared field loses focus with a changed value; there is no Save button. Connect saves the token (and username for `basic`) or the OAuth client id and then the declared fields.
17
+ - `POST /api/guests/:id/oauth/start` — PKCE S256 for OAuth guests. Host Linear starts the first-party Linear authorize URL. Token guests get `NO_INTEGRATION`.
18
+ - `GET /api/guests/:id/oauth/callback` — public HTML page. The provider redirects here with no UI bearer. State and verifier stay on the server. A callback without a matching `state` is refused; nothing is inferred from a pending exchange. Host Linear uses the first-party Linear callback, not this URL.
19
+ - `DELETE /api/guests/:id/oauth` — drops tokens for that guest. Client and settings stay. Host Linear disconnects the first-party Linear workspace.
20
+ - `POST /api/guests/:id/request` — authenticated UI session. Attaches Authorization. Path must stay on that guest's `apiOrigin`. Token guests send the pasted token as is, `Bearer <token>` when the manifest sets `token.scheme: "bearer"`, or `Basic <pair>` for `"basic"`, where `PUT /api/guests/:id/token` took `{ token, username }` and stored `base64(username:token)` as the access token (`USERNAME_MISSING` without a username). Host Linear sends the Linear bearer to `https://api.linear.app` only. Linear issue file URLs stay on `GET /api/linear/issues/get`. The guest proxy does not add Linear headers. A paused extension (`enabled: false`) is 400 `DISABLED`; tokens stay. A guest without the `network` capability granted is 400 `NOT_GRANTED`.
21
+ - `POST /api/guests/:id/service/request` — authenticated UI session. Proxies to that guest's host-spawned service on `127.0.0.1`. Spawns on first use after the grant; every service needs one. Missing grant, missing service, or bad path is 400 with `NO_SERVICE` / `BAD_PATH`. Paused extension is 400 `DISABLED` (service stopped). Ready failure is 502 `SERVICE_FAILED`.
22
+ - `POST /api/guests/:id/files` — authenticated UI session. `{ op: 'read' | 'write' | 'list' | 'stat', path, content? }` (`content` required for `write`, at most `GUEST_FILE_CONTENT_MAX` characters; `path` at most `GUEST_FILE_PATH_MAX`). The open project comes from `resolveOptionalProjectDirectory(req)` (`x-opencode-directory` header or `directory` query). `files.js` resolves the path: relative → joined to the project's realpath, and the deepest existing ancestor is realpath'd so a symlink out of the project is `BAD_PATH`; a relative path with no project is `NO_DIRECTORY`. `/…` or `~/…` → home expanded, normalized, canonicalized the same way, then matched against the package's `contributes.filesystem` globs whose literal prefixes were canonicalized too (`/tmp` → `/private/tmp` on macOS); no match is `BAD_PATH`. Any `..` segment, NUL, or backslash is `BAD_PATH`. Then the grant: project scope needs `files`, outside needs `filesystem`, else `NOT_GRANTED`. Then the operation: read is UTF-8 and refuses a file over the cap with `FILE_TOO_LARGE`; write makes parent directories and lands through a temp file plus rename (a reader never sees a truncated file); list returns `{ name, kind }` sorted by name and capped at `GUEST_FILE_LIST_MAX`, with symlinks as `other`; stat answers `kind: 'missing'` for nothing there. Answers are `{ ok: true, result }`, or `{ error: code, message }` with 400 (404 for `NOT_FOUND`). Paused extension is 400 `DISABLED`. `ENOENT` is `NOT_FOUND`, `EACCES` / `EPERM` is `DENIED`, `EISDIR` / `ENOTDIR` is `BAD_PATH`. Neither the path nor the content is ever logged.
23
+ - `POST /api/guests/:id/generate` — authenticated UI session. `{ prompt, system?, maxOutputTokens? }` (prompt at most `GUEST_GENERATE_PROMPT_MAX`, system `GUEST_GENERATE_SYSTEM_MAX`, tokens `GUEST_GENERATE_OUTPUT_TOKENS_MAX`). Paused extension is 400 `DISABLED`; missing `model` grant is 400 `NOT_GRANTED`. The call goes to `generateSmallModelText` in `lib/small-model`, which resolves and authenticates the Small Model the same way session titles and notes do (settings override → OpenCode config → signed-in provider); the optional `x-opencode-directory` header only picks which OpenCode config is read. No model (404) or Claude Code as the only candidate (422) is 400 `NO_MODEL`; a provider failure is 502 `MODEL_FAILED`. Answer is `{ ok: true, result: { text } }` capped at `GUEST_GENERATE_TEXT_MAX`. Prompt and answer are never logged.
24
+ - `GET /api/guests/:id/service/status` — `{ status }` for that guest's service runtime (`stopped` / `starting` / `ready` / `failed`).
25
+ - `PUT /api/guests/:id/capabilities` — `{ granted: GuestCapability[] }`. The list must equal the package's requested list, or be empty to withdraw approval (which also stops a running service). Anything else is 400 `invalid-request`. `service.granted` mirrors whether `service` is in the granted list. Approval also records the grant's scope (`grant-scope.js` `guestGrantScope`: the sorted `filesystem` patterns, the integration's `apiOrigin` and OAuth `authorizeUrl` / `tokenUrl`, the service's exec names and socket ids) as `capabilityScopes[id]`. When the recorded origin or OAuth endpoints differ from the new ones (`sameCredentialTarget`) the guest's stored tokens are dropped: a token pasted for one API, or a refresh token and client secret minted for one token endpoint, must not follow the integration to another.
26
+ - `PUT /api/guests/:id/service/sockets` — `{ id, path }` for a declared socket binding. Empty or null `path` clears the override. Stops a running service for that guest. Catalog `service.socketBindings` shows `resolved` and `override` for this host.
27
+ - `PUT /api/guests/:id/enabled` — `{ enabled }`. False is a full pause: catalog `enabled: false`, rail/attach/Integrations hide the guest, open panels close, cloud `request` and `service/request` return `DISABLED`, and a running service stops. Tokens and grants stay until Remove.
28
+ - `POST /api/guests` accepts exactly one of `{ path }` or `{ url }`, optional `replace`, and optional `gitIdentityId` for Git installs. `path` is an absolute folder or local ZIP. `url` is HTTPS Git/ZIP, `ssh://` Git, or scp-style SSH. Folder installs persist their realpath; Git and ZIP installs copy into `{openchamberDataDir}/extensions/{id}`. Success is 201, or 200 after replacement. Invalid input is `invalid-path`/`invalid-url`; clone and extraction failures are `clone-failed`/`extract-failed`. A missing selected identity fails cloning rather than falling back to global auth. Missing built scripts are `missing-build`, invalid package/version is `invalid-manifest`, and a newer required host is `host-too-old`. Duplicate installs are 409 `id-taken`/`already-installed`. Other install failures are 400. Clone uses the host's resolved Git binary; orphaned copy folders from interrupted installs can be replaced on retry. Each OpenChamber data directory owns its own catalog.
29
+ - `POST /api/guests/upload` — the archive itself as the body, `Content-Type: application/octet-stream`, for a browser or a remote instance that cannot name a path on the host. Query `replace=true` mirrors the JSON route's `replace`; `name` (the original filename) is accepted and unused, never logged. A wrong content type is 415 `unsupported-media-type`. A `Content-Length` over the cap is refused with 413 `too-large` before the body is read, and a body that grows past the cap without a declared length stops with the same answer. The cap is `OPENCHAMBER_GUEST_UPLOAD_MAX_BYTES`, default 50 MB (`upload.js` `guestUploadMaxBytes`). The bytes go to `installGuestFromZipBuffer`, the same function a local `.zip` path and an https `.zip` URL end in, so results and errors are those of `POST /api/guests` with a zip: 201 / 200 after replace with `{ guest }` (`source: "zip"`), 409 `already-installed` / `id-taken` with `id`, 400 `invalid-manifest`, `missing-build`, `host-too-old`, `extract-failed`. Registered before the `:id` routes because `upload` is a valid panel id shape. Archive contents are never logged.
30
+ - `POST /api/guests/updates/check` — `{ force?: boolean }` → `{ updates: Record<guestId, { version }> }` for every git install whose origin now carries a newer `package.json` `version`. The check runs `git fetch --depth 1 origin <ref|HEAD>` inside the installed clone and reads `FETCH_HEAD:package.json` through the SDK manifest parser; the only signal is semver (`updates.js` `compareSemver`, prerelease below release), no commit hashes. Results sit in a memory cache per store file and guest id for `UPDATE_CHECK_TTL_MS` (one hour); `force` asks the network again. That cache is separate from the catalog cache, so a store write does not drop it. One guest's failed fetch never hides another's update. Folder and zip installs are never checked.
31
+ - `POST /api/guests/:id/update` — replace the installed git copy with a fresh clone of its stored origin. Answers `{ guest }` (the refreshed catalog row) or 400 `{ error }` with `not-git` (folder, zip, or no stored origin), `clone-failed`, `invalid-manifest` (also when the clone declares a different panel id), `missing-build`, `host-too-old` (body may include `required`), or `swap-failed`. Unknown id is 404. Grants, tokens, settings, and the paused flag are keyed by guest id and stay as they were; a version that asks for more capabilities shows up as needs-approval in the catalog and goes through the usual review dialog.
32
+ - `DELETE /api/guests/:id` — drops the store row, any service grant, and socket overrides; stops a running service. Path-install does not delete the user's folder. Zip and git copies under `{openchamberDataDir}/extensions/` are deleted. Unknown id is 404.
33
+ - `GET /api/guests/:id/{*filePath}` serves package-local files. Unknown IDs, escapes, missing files, and unknown types return 404. HTML and JS require either `entry` or `backgroundEntry`, expressed by `resolveGuestServedFile`'s `hasRuntime` option. Tools-only extensions still serve their SVG icons but no executable content. Guest HTML propagates the scoped `oc_url_token` onto relative script/link/image URLs. The token is minted for `guest:<id>` and works only under that guest's asset route, never `/api/fs/raw` or event streams. Every response carries `Content-Security-Policy: sandbox allow-scripts`, including direct document navigation. The host serves built files from disk and never compiles guest sources.
34
+
35
+ ## Install
36
+
37
+ `contributes.background.entry` is optional package-local `.html`, separate from `panel.entry`. Install validates its HTML and every referenced built script using the same path confinement as visible panels. `backgroundEntry` is exposed on the catalog independently of `entry`; adding it does not create a rail icon or a visible page. Capabilities, services, integrations, and storage work with either execution entry. The SDK refuses visible contributions without `panel.entry` and all runtime contributions when neither entry exists. See `packages/sdk/DOCUMENTATION.md` for on-demand invocation ownership.
38
+
39
+ `html-styles.js` appends the SDK's shared scrollbar stylesheet to served HTML, including requests without an asset token. This applies to existing panel, page, and attach bundles in direct and relay mode; the relay loader retains the inline style in `srcDoc`. Appending preserves the doctype and avoids rewriting tag-like text inside scripts or comments. Authored CSP stays intact and can block inline styling. Package files on disk and non-HTML responses are unchanged. Scrollbar colors follow the SDK theme variables inside the frame.
40
+
41
+ Built-ins ship with the app; user extensions are installed separately. Add a folder, local ZIP, HTTPS Git/ZIP URL, or SSH Git address from Settings → Extensions. The ZIP picker and drop zone upload the archive on web and remote desktop instances. A guest with a page must ship built classic-IIFE JavaScript, for example from `bunx openchamber-guest-bundle panel/main.ts panel/main.js`; the sandbox does not support ESM imports. `package.json` needs a semver version. A missing panel SVG or invalid manifest fails installation; page-less tools-only packages skip HTML/script checks. Optional `engines.openchamber` is checked against this host. Provider accounts are connected separately in Settings → Integrations.
42
+
43
+ Folder installs store their realpath with `source: "path"`. ZIP and Git copies live in `{openchamberDataDir}/extensions/{id}`. Git URLs may end in `#branch-or-tag`; clones use `--branch <ref> --depth 1`. `gitOrigins` stores `{ url, ref?, gitIdentityId? }` per install path and is removed on uninstall. Legacy origins without an identity retain global authentication. Malformed origins are dropped individually. The public catalog exposes the origin URL/ref, never the key path. Named profiles are re-resolved on each clone or update fetch so key rotation applies; missing profiles fail closed. Global auth preserves the server's SSH agent and Git/SSH configuration. ZIP limits remain 20MB for path/URL installs, 500 files and 40MB expanded, store/deflate only, no encryption, zip64 or parent-path escapes. Uninstall deletes copies and leaves folder-install sources intact.
44
+
45
+ OAuth and pasted tokens live in `{openchamberDataDir}/guest-auth.json`, each entry with the `target` it was minted for (`apiOrigin`, and for OAuth `authorizeUrl` and `tokenUrl`). `takeUsableGuestAuth` runs before every use (proxy, refresh, the status card): tokens whose target differs from what the package declares now, or that carry no target, are dropped on the spot, whatever the grants say. A pending authorization carries the same target; the callback refuses the exchange (`TARGET_CHANGED`) when the package changed its endpoints in between, and the exchange itself goes to the pending target's `tokenUrl`, never to the current manifest's. The client id and secret carry their own `clientTarget`, written by `PUT /api/guests/:id/oauth/client` (`saveGuestOAuthClient`: an empty secret field keeps the stored secret only for the same client id and the same endpoints, decided under the store lock; a new id or moved endpoints drop it); `usableClientCredentials` treats a client entered for other endpoints as absent (`CLIENT_MISSING` on Connect, `hasClient: false` on the card), so a moved package gets no old secret and the user enters new credentials. Every token write that follows a network round trip (the code exchange, a refresh) goes through `patchGuestAuth` with an `expect` predicate checked under the store lock: the entry must still hold the tokens and target the round trip started from, else the answer is dropped and the entry left as it is. The proxy's cleanup after a 401 is scoped the same way: `dropGuestTokens` with an `expect` on the access token the request was refused with, so a refresh that came back empty because the user reconnected meanwhile never erases the new connection. Host Linear tokens stay in `linear-auth.json`. Writes are atomic and `0o600`. The file is per OpenChamber instance, same as `extensions.json`. Web and desktop share it. VS Code and mobile set the guest catalog to `unsupported`. That is not an empty ready list. Those cards stay hidden.
46
+
47
+ A missing folder is omitted from the list. Other guests stay. A corrupt `extensions.json` is 500, not an empty catalog. A missing or blank `guest-auth.json` is an empty store. Corrupt JSON there is 500, not a disconnected card. This is not Settings → Plugins.
48
+
49
+ ## Invariants
50
+
51
+ - Built-in packages come from the app-owned registry bound by `registerBuiltInGuests` at server startup. Source/build/packaging rules are in `packages/extensions/DOCUMENTATION.md`. They receive only their declared grants automatically; ordinary runtime and enabled checks still apply. `openchamber-builtin-` IDs are reserved from user installs, including replacement. DELETE, Git update and grant-edit routes refuse built-ins. Disable preserves account/storage data and stops their services.
52
+
53
+ - Grants are narrowed on read, never trusted as stored. `listInstalledGuestsUncached` passes each guest's stored list through `effectiveGrants(granted, storedScope, currentScope)`: `filesystem`, `network`, and `service` only count while the scope recorded at approval equals what the package declares now, and never count without a recorded scope. A newer version that widens its patterns, moves its API origin, or asks for more exec names or sockets shows up as needs-approval and is refused by every proxy meanwhile. Routes and `proxyGuestServiceRequest` take the grant list from the catalog row (`guest.capabilityGrants`), not from the store.
54
+ - Store changes are read-modify-write under one lock per file (`updateExtensionStore`): Allow, Pause, a socket override, an install, and an uninstall landing at the same time all keep each other's result. `guest-auth.json` has the same lock (`withAuthStoreLock`) and every temp file name is unique.
55
+ - ZIP downloads validate and pin each HTTPS hop, at most five. Git clones and update fetches share `prepareGuestGitNetwork`. HTTPS retains `http.followRedirects=false` and `http.curloptResolve` pinning. SSH validates the username/host/path, rejects password-bearing URLs and private DNS answers, pins `Hostname` to a resolved public address, and retains the original host for host-key checks through `HostKeyAlias`. Default SSH options use batch mode and `StrictHostKeyChecking=accept-new`, with ProxyCommand/ProxyJump disabled. Identity key paths are shell-quoted; keys and SSH agent sockets stay on the server. Public Git hosts are supported, not local SSH aliases or private-network targets. Existing user-configured Git/SSH commands remain trusted server configuration.
56
+ - The catalog cache is version-guarded: `invalidateGuestCatalog` bumps a per-path version, the store notifies before and after each write, and a listing only caches its result when the version did not move while it read. A read that straddled a withdrawal therefore never serves the old grants for the TTL.
57
+ - Stopping a service also cancels a request in flight: `stopGuestService` (Pause, Remove, withdrawn approval, socket change, host quit) bumps a per-guest epoch. `proxyGuestServiceRequest` reads the epoch before its first store read, hands it to the start, and checks it again after the service is up; `startGuestService` checks it after every await, right before `spawn`, and while polling readiness. Pause anywhere between the request's enabled check and the proxied call ends with no process and `NO_SERVICE`. Internal cleanup of a dead or failed runtime before a restart is `discardRuntime`, which does not bump the epoch: a crash is not a Pause, so the request that triggers the restart goes through.
58
+
59
+ - Paths stay inside the package root (`realpath`). `..`, absolute paths, and URLs do not serve.
60
+ - Only an allowlisted content type is served.
61
+ - Host and guest speak `@openchamber/sdk`. Do not copy those types into `packages/ui`.
62
+ - `contributes.actions`, `contributes.commands`, and `contributes.tools` are validated by the SDK parse at install (`invalid-actions` / `invalid-commands` / `invalid-tools` surface as `invalid-manifest`) and copied to the catalog row unchanged. Tool presentations are applied in the UI only (`packages/ui/src/lib/guests/tool-presentation.ts`); no server route reads them. No server route runs an action or a command: the message or session item is built in the UI from the same records Export as Markdown uses, and a slash command is answered by the guest frame over `postMessage`. The rail badge is UI memory only.
63
+ - `contributes.attach` is copied as `"panel"` or `"dialog"`. The composer + menu and New Worktree read that field. Omitted guests stay off those menus. The object form's `entry` (dialog only) is checked at install like `panel.entry`: missing HTML is `invalid-manifest`, an unbuilt script it loads is `missing-build`; the catalog exposes it as `attachEntry` and the attach window loads that page instead of `panel.entry`. The optional attach `data` payload lives only in the UI's chip, queued context part, and session metadata snapshot; no server route parses it.
64
+ - `request` never leaves the declared `apiOrigin`. A 401 refreshes once. If that fails, tokens for that guest are dropped. Host Linear does not drop Linear auth on a failed GraphQL query. A paused guest never proxies `request` or `serviceRequest`.
65
+ - `serviceRequest` never leaves that guest's loopback port. Auth to the service is `Authorization: Bearer` with a host-minted token. The service env is an allowlist (PATH, HOME, temp, locale, Windows system variables) plus `OPENCHAMBER_SERVICE_*`; host secrets are not inherited. There is no sandbox: an allowed service has the user's full access, and the grant card says so. Ready is `GET /health` → 200. Parallel first requests share one spawn; they must not kill each other mid-start. Host quit stops every service. Disabling a guest stops its service without clearing OAuth or grants.
66
+ - File access is decided in `files.js` on canonical paths, never on the string the guest sent. `matchesFilesystemPattern`, `expandHome`, and `globToRegExp` are pure; `resolveGuestFilePath` and `runGuestFileOperation` take `fsPromises` / `nodePath` so tests can stub the OS. The project directory is whatever `resolveOptionalProjectDirectory` validated for this request, so a guest cannot name a project the UI did not open.
67
+ - Update swap (`updates.js` `updateGuest`): the new clone lands in `{dataDir}/extensions/.tmp-<id>-<random>` and is inspected exactly like an install before anything else moves. Only then: stop the guest's service, rename current → `.old-<id>`, rename temp → final, delete `.old-<id>`. Any failure before the swap deletes the temp dir and leaves the install untouched; a failed second rename moves `.old-<id>` back. The store row is not rewritten (same path, same id), the catalog cache and that guest's update cache entry are dropped.
68
+ - The OAuth callback skips UI auth. Everything else under `/api/guests` stays behind the usual session.
@@ -0,0 +1,184 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+
5
+ const PANEL_ID = /^[a-z][a-z0-9-]*$/;
6
+ const SETTING_KEY = /^[a-z][a-z0-9-]*$/;
7
+
8
+ const guestAuthEntrySchema = z.object({
9
+ clientId: z.string().min(1).optional(),
10
+ clientSecret: z.string().min(1).optional(),
11
+ accessToken: z.string().min(1).optional(),
12
+ refreshToken: z.string().min(1).nullable().optional(),
13
+ tokenType: z.string().min(1).optional(),
14
+ expiresAt: z.number().int().positive().nullable().optional(),
15
+ account: z.string().max(200).optional(),
16
+ settings: z.record(z.string().regex(SETTING_KEY), z.string().max(2000)).optional(),
17
+ authorizedAt: z.number().int().positive().optional(),
18
+ // Where the tokens were minted for. Checked before every use, so a
19
+ // package that moved its API origin or OAuth endpoints cannot receive
20
+ // credentials the user connected for the old ones.
21
+ target: z.object({
22
+ apiOrigin: z.string().min(1),
23
+ authorizeUrl: z.string().min(1).optional(),
24
+ tokenUrl: z.string().min(1).optional(),
25
+ }).optional(),
26
+ // Where the client id and secret were entered for. A package that moved
27
+ // its endpoints gets no client credentials until the user enters them
28
+ // again; they are never carried over.
29
+ clientTarget: z.object({
30
+ apiOrigin: z.string().min(1),
31
+ authorizeUrl: z.string().min(1).optional(),
32
+ tokenUrl: z.string().min(1).optional(),
33
+ }).optional(),
34
+ });
35
+
36
+ const storeSchema = z.object({
37
+ guests: z.record(z.string().regex(PANEL_ID), guestAuthEntrySchema),
38
+ });
39
+
40
+ const dataDirSchema = z.string().min(1).refine((entry) => !entry.includes('\0') && path.isAbsolute(entry));
41
+
42
+ export const guestAuthPersistPath = (dataDir) => {
43
+ const parsed = dataDirSchema.safeParse(dataDir);
44
+ if (!parsed.success) {
45
+ throw new Error('Guest auth needs an absolute OpenChamber data dir');
46
+ }
47
+ return path.join(parsed.data, 'guest-auth.json');
48
+ };
49
+
50
+ const parseStore = (raw) => {
51
+ try {
52
+ const parsed = storeSchema.safeParse(JSON.parse(raw));
53
+ return parsed.success ? parsed.data : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ };
58
+
59
+ export const readGuestAuthStore = async (persistPath) => {
60
+ try {
61
+ const raw = await fs.readFile(persistPath, 'utf8');
62
+ const text = raw.replace(/^\uFEFF/, '').trim();
63
+ if (text === '') {
64
+ return { guests: {} };
65
+ }
66
+ const parsed = parseStore(text);
67
+ if (!parsed) {
68
+ throw new Error('Invalid guest auth store');
69
+ }
70
+ return parsed;
71
+ } catch (error) {
72
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
73
+ return { guests: {} };
74
+ }
75
+ throw error;
76
+ }
77
+ };
78
+
79
+ /** @type {Map<string, Promise<unknown>>} */
80
+ const writeChains = new Map();
81
+ let writeSequence = 0;
82
+
83
+ // Two writers at once (a token refresh while a settings field saves) used to
84
+ // share one temp file and a stale read: one of them lost. Every change to one
85
+ // store file now runs read → change → write as a single step, one after
86
+ // another, each on its own temp file.
87
+ const withAuthStoreLock = (persistPath, run) => {
88
+ const previous = writeChains.get(persistPath) ?? Promise.resolve();
89
+ const next = previous.catch(() => undefined).then(run);
90
+ const chained = next.finally(() => {
91
+ if (writeChains.get(persistPath) === chained) writeChains.delete(persistPath);
92
+ });
93
+ writeChains.set(persistPath, chained);
94
+ return next;
95
+ };
96
+
97
+ const writeGuestAuthStoreUnlocked = async (store, persistPath) => {
98
+ const parsed = storeSchema.safeParse(store);
99
+ if (!parsed.success) {
100
+ throw new Error('Invalid guest auth store');
101
+ }
102
+ await fs.mkdir(path.dirname(persistPath), { recursive: true });
103
+ const tmp = `${persistPath}.tmp-${process.pid}-${Date.now()}-${(writeSequence += 1)}`;
104
+ await fs.writeFile(tmp, `${JSON.stringify(parsed.data, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
105
+ await fs.rename(tmp, persistPath);
106
+ await fs.chmod(persistPath, 0o600);
107
+ };
108
+
109
+
110
+ export const getGuestAuth = async (guestId, persistPath) => {
111
+ const store = await readGuestAuthStore(persistPath);
112
+ return store.guests[guestId] ?? null;
113
+ };
114
+
115
+ /**
116
+ * Apply `patch` to one guest's entry, read and written under the store lock.
117
+ * With `expect`, the patch only lands when the entry read under the lock
118
+ * satisfies it; a caller that spent a network round trip on the old entry
119
+ * (a token refresh) uses this to drop its result when the entry moved on.
120
+ * `patch` may be a function of the entry read under the lock, for a change
121
+ * that depends on what is there (a client id replacing another one).
122
+ * Returns the entry as stored afterwards, or `null` when it is gone.
123
+ */
124
+ export const patchGuestAuth = (guestId, patch, persistPath, expect = null) => withAuthStoreLock(persistPath, async () => {
125
+ const store = await readGuestAuthStore(persistPath);
126
+ const current = store.guests[guestId] ?? null;
127
+ if (expect && !expect(current)) {
128
+ return current;
129
+ }
130
+ const next = { ...(current ?? {}) };
131
+ const resolvedPatch = typeof patch === 'function' ? patch(current) : patch;
132
+ for (const [key, value] of Object.entries(resolvedPatch)) {
133
+ if (value === undefined) {
134
+ delete next[key];
135
+ } else {
136
+ next[key] = value;
137
+ }
138
+ }
139
+ const guests = { ...store.guests };
140
+ const empty = !next.clientId
141
+ && !next.clientSecret
142
+ && !next.accessToken
143
+ && !next.refreshToken
144
+ && (!next.settings || Object.keys(next.settings).length === 0);
145
+ if (empty) {
146
+ delete guests[guestId];
147
+ } else {
148
+ guests[guestId] = next;
149
+ }
150
+ await writeGuestAuthStoreUnlocked({ guests }, persistPath);
151
+ return guests[guestId] ?? null;
152
+ });
153
+
154
+ /**
155
+ * Forget the tokens. With `expect`, only when the entry read under the lock
156
+ * still holds the tokens the caller is reacting to; a caller that saw a 401
157
+ * on one access token must not erase a newer connection made meanwhile.
158
+ */
159
+ export const dropGuestTokens = async (guestId, persistPath, expect = null) => {
160
+ const current = await getGuestAuth(guestId, persistPath);
161
+ if (!current) {
162
+ return null;
163
+ }
164
+ return patchGuestAuth(guestId, {
165
+ accessToken: undefined,
166
+ refreshToken: undefined,
167
+ target: undefined,
168
+ tokenType: undefined,
169
+ expiresAt: undefined,
170
+ account: undefined,
171
+ authorizedAt: undefined,
172
+ }, persistPath, expect);
173
+ };
174
+
175
+ /** Drop everything stored for a guest: tokens, client credentials, and settings. Used when the package is removed. */
176
+ export const forgetGuestAuth = (guestId, persistPath) => withAuthStoreLock(persistPath, async () => {
177
+ const store = await readGuestAuthStore(persistPath);
178
+ if (!(guestId in store.guests)) {
179
+ return;
180
+ }
181
+ const guests = { ...store.guests };
182
+ delete guests[guestId];
183
+ await writeGuestAuthStoreUnlocked({ ...store, guests }, persistPath);
184
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import {
7
+ dropGuestTokens,
8
+ forgetGuestAuth,
9
+ getGuestAuth,
10
+ guestAuthPersistPath,
11
+ patchGuestAuth,
12
+ readGuestAuthStore,
13
+ } from './auth-store.js';
14
+
15
+ describe('guestAuthPersistPath', () => {
16
+ test('joins the instance data dir and refuses a relative path', () => {
17
+ const dataDir = path.join(os.tmpdir(), 'oc-guest-auth-a');
18
+ expect(guestAuthPersistPath(dataDir)).toBe(path.join(dataDir, 'guest-auth.json'));
19
+ expect(() => guestAuthPersistPath('relative')).toThrow('absolute OpenChamber data dir');
20
+ });
21
+ });
22
+
23
+ describe('guest auth store', () => {
24
+ test('round-trips tokens and writes 0o600', async () => {
25
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'oc-guest-auth-'));
26
+ const file = guestAuthPersistPath(dir);
27
+ expect(await readGuestAuthStore(file)).toEqual({ guests: {} });
28
+ await patchGuestAuth('clickup', {
29
+ clientId: 'id',
30
+ clientSecret: 'secret',
31
+ accessToken: 'tok',
32
+ account: 'ada',
33
+ settings: { 'list-id': '123' },
34
+ }, file);
35
+ expect(await getGuestAuth('clickup', file)).toMatchObject({
36
+ clientId: 'id',
37
+ accessToken: 'tok',
38
+ account: 'ada',
39
+ settings: { 'list-id': '123' },
40
+ });
41
+ expect(fs.statSync(file).mode & 0o777).toBe(0o600);
42
+ await dropGuestTokens('clickup', file);
43
+ const after = await getGuestAuth('clickup', file);
44
+ expect(after?.accessToken).toBeUndefined();
45
+ expect(after?.clientId).toBe('id');
46
+ expect(after?.settings).toEqual({ 'list-id': '123' });
47
+ await fs.promises.rm(dir, { recursive: true, force: true });
48
+ });
49
+
50
+ test('forgets a guest entirely when its package is removed', async () => {
51
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'oc-guest-auth-'));
52
+ const file = guestAuthPersistPath(dir);
53
+ try {
54
+ await patchGuestAuth('clickup', { clientId: 'app', clientSecret: 'secret', accessToken: 'token' }, file);
55
+ await patchGuestAuth('other', { accessToken: 'keep' }, file);
56
+ await forgetGuestAuth('clickup', file);
57
+ expect(await getGuestAuth('clickup', file)).toBeNull();
58
+ expect((await getGuestAuth('other', file))?.accessToken).toBe('keep');
59
+ } finally {
60
+ await fs.promises.rm(dir, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ test('treats a blank file as an empty store', async () => {
65
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'oc-guest-auth-'));
66
+ const file = guestAuthPersistPath(dir);
67
+ await fs.promises.writeFile(file, '', 'utf8');
68
+ expect(await readGuestAuthStore(file)).toEqual({ guests: {} });
69
+ await fs.promises.writeFile(file, '\uFEFF\n', 'utf8');
70
+ expect(await readGuestAuthStore(file)).toEqual({ guests: {} });
71
+ await fs.promises.rm(dir, { recursive: true, force: true });
72
+ });
73
+
74
+ test('refuses a corrupt store', async () => {
75
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'oc-guest-auth-'));
76
+ const file = guestAuthPersistPath(dir);
77
+ await fs.promises.writeFile(file, '{"guests":[]}', 'utf8');
78
+ try {
79
+ await readGuestAuthStore(file);
80
+ throw new Error('should have thrown');
81
+ } catch (error) {
82
+ expect(String(error)).toContain('Invalid guest auth store');
83
+ }
84
+ await fs.promises.writeFile(file, '{', 'utf8');
85
+ try {
86
+ await readGuestAuthStore(file);
87
+ throw new Error('should have thrown');
88
+ } catch (error) {
89
+ expect(String(error)).toContain('Invalid guest auth store');
90
+ }
91
+ await fs.promises.rm(dir, { recursive: true, force: true });
92
+ });
93
+ });
94
+
95
+ describe('concurrent auth changes', () => {
96
+ test('two patches at once both land', async () => {
97
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'oc-guest-auth-'));
98
+ const file = guestAuthPersistPath(dir);
99
+ try {
100
+ await Promise.all([
101
+ patchGuestAuth('alpha', { accessToken: 'tok-a' }, file),
102
+ patchGuestAuth('beta', { accessToken: 'tok-b' }, file),
103
+ patchGuestAuth('alpha', { settings: { 'list-id': '7' } }, file),
104
+ ]);
105
+ expect(await getGuestAuth('alpha', file)).toMatchObject({ accessToken: 'tok-a', settings: { 'list-id': '7' } });
106
+ expect(await getGuestAuth('beta', file)).toMatchObject({ accessToken: 'tok-b' });
107
+ expect(fs.readdirSync(dir).filter((name) => name.includes('.tmp-'))).toEqual([]);
108
+ } finally {
109
+ await fs.promises.rm(dir, { recursive: true, force: true });
110
+ }
111
+ });
112
+ });
@@ -0,0 +1,83 @@
1
+ import { afterEach, expect, test } from 'bun:test';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import express from 'express';
6
+ import request from 'supertest';
7
+ import { inspectGuestPackage, toPublicGuest } from './catalog.js';
8
+ import { registerGuestRoutes } from './routes.js';
9
+
10
+ const roots = [];
11
+ afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); });
12
+ const fixture = async () => {
13
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-background-')); roots.push(root);
14
+ const packageRoot = path.join(root, 'package');
15
+ await fs.mkdir(packageRoot);
16
+ await fs.writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: 'background-test', version: '1.0.0', openchamber: {
17
+ apiVersion: 1, contributes: {
18
+ panel: { id: 'background-test', name: 'Background Test', icon: 'apps' },
19
+ background: { entry: 'index.html' },
20
+ capabilities: ['files'], commands: [{ name: 'test-background' }],
21
+ actions: [{ id: 'count', label: 'Count', where: 'message', mode: 'background' }],
22
+ },
23
+ } }));
24
+ return { root, packageRoot };
25
+ };
26
+
27
+ test('background-only packages validate HTML and built scripts and expose a separate catalog entry', async () => {
28
+ const { packageRoot } = await fixture();
29
+ expect(await inspectGuestPackage(packageRoot)).toMatchObject({ ok: false, code: 'invalid-manifest' });
30
+ await fs.writeFile(path.join(packageRoot, 'index.html'), '<script src="main.js"></script>');
31
+ await fs.writeFile(path.join(packageRoot, 'main.ts'), 'export {};');
32
+ expect(await inspectGuestPackage(packageRoot)).toMatchObject({ ok: false, code: 'missing-build' });
33
+ await fs.writeFile(path.join(packageRoot, 'main.js'), 'void 0;');
34
+ const result = await inspectGuestPackage(packageRoot);
35
+ expect(result.ok).toBe(true);
36
+ if (!result.ok) throw new Error(result.code);
37
+ const row = toPublicGuest(result.guest);
38
+ expect(row.entry).toBeUndefined();
39
+ expect(row.backgroundEntry).toBe('index.html');
40
+ expect(row.attach).toBeUndefined();
41
+ expect(row.actions).toHaveLength(1);
42
+ expect(row.commands).toHaveLength(1);
43
+ expect(row.capabilities).toEqual({ requested: ['files'], granted: [] });
44
+ });
45
+
46
+ test('background entry and its scripts cannot escape the installed package through symlinks', async () => {
47
+ const { root, packageRoot } = await fixture();
48
+ const outside = path.join(root, 'outside.html');
49
+ await fs.writeFile(outside, '<p>Outside</p>');
50
+ await fs.symlink(outside, path.join(packageRoot, 'index.html'));
51
+ expect(await inspectGuestPackage(packageRoot)).toMatchObject({ ok: false, code: 'invalid-manifest' });
52
+ await fs.unlink(path.join(packageRoot, 'index.html'));
53
+ await fs.writeFile(path.join(packageRoot, 'index.html'), '<script src="main.js"></script>');
54
+ await fs.writeFile(path.join(root, 'outside.js'), 'void 0;');
55
+ await fs.symlink(path.join(root, 'outside.js'), path.join(packageRoot, 'main.js'));
56
+ expect(await inspectGuestPackage(packageRoot)).toMatchObject({ ok: false, code: 'missing-build' });
57
+ });
58
+
59
+ test('background install serves sandboxed assets and storage obeys approval and enabled state', async () => {
60
+ const { root, packageRoot } = await fixture();
61
+ await fs.writeFile(path.join(packageRoot, 'index.html'), '<script src="main.js"></script>');
62
+ await fs.writeFile(path.join(packageRoot, 'main.js'), 'void 0;');
63
+ const app = express(); registerGuestRoutes(app, { openchamberDataDir: root, resolveGitBinaryForSpawn: () => 'git' });
64
+ const installed = await request(app).post('/api/guests').send({ path: packageRoot }).expect(201);
65
+ expect(installed.body.guest.backgroundEntry).toBe('index.html');
66
+ expect(installed.body.guest.entry).toBeUndefined();
67
+ const catalog = await request(app).get('/api/guests').expect(200);
68
+ expect(catalog.body.guests[0].backgroundEntry).toBe('index.html');
69
+ const html = await request(app).get('/api/guests/background-test/index.html?oc_url_token=test-scope').expect(200);
70
+ expect(html.headers['content-security-policy']).toBe('sandbox allow-scripts');
71
+ expect(html.text).toContain('main.js?oc_url_token=test-scope');
72
+ expect((await request(app).get('/api/guests/background-test/main.js').expect(200)).text).toBe('void 0;');
73
+ await request(app).post('/api/guests/background-test/storage').send({ op: 'set', key: 'count', value: 1 }).expect(400);
74
+ await request(app).put('/api/guests/background-test/capabilities').send({ granted: ['files'] }).expect(200);
75
+ await request(app).post('/api/guests/background-test/storage').send({ op: 'set', key: 'count', value: 1 }).expect(200);
76
+ expect((await request(app).post('/api/guests/background-test/storage').send({ op: 'get', key: 'count' }).expect(200)).body).toMatchObject({ found: true, value: 1 });
77
+ await request(app).put('/api/guests/background-test/enabled').send({ enabled: false }).expect(200);
78
+ await request(app).post('/api/guests/background-test/storage').send({ op: 'set', key: 'count', value: 2 }).expect(400);
79
+ await request(app).put('/api/guests/background-test/enabled').send({ enabled: true }).expect(200);
80
+ expect((await request(app).post('/api/guests/background-test/storage').send({ op: 'get', key: 'count' }).expect(200)).body).toMatchObject({ value: 1 });
81
+ await request(app).put('/api/guests/background-test/capabilities').send({ granted: [] }).expect(200);
82
+ await request(app).post('/api/guests/background-test/storage').send({ op: 'get', key: 'count' }).expect(400);
83
+ });
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { z } from 'zod';
5
+
6
+ const BUILTIN_PREFIX = 'openchamber-builtin-';
7
+ export const DEFAULT_BUILTIN_ROOT = fileURLToPath(new URL('../../built-in-extensions/', import.meta.url));
8
+ export const builtInExtensionSchema = z.object({
9
+ id: z.string().regex(/^openchamber-builtin-[a-z][a-z0-9-]*$/),
10
+ directory: z.string().regex(/^[a-z][a-z0-9-]*$/),
11
+ });
12
+ export const builtInRegistrySchema = z.object({
13
+ version: z.literal(1),
14
+ extensions: z.array(builtInExtensionSchema),
15
+ }).superRefine(({ extensions }, context) => {
16
+ const ids = new Set();
17
+ const directories = new Set();
18
+ for (const entry of extensions) {
19
+ if (ids.has(entry.id) || directories.has(entry.directory)) {
20
+ context.addIssue({ code: 'custom', message: 'Built-in extension IDs and directories must be unique' });
21
+ }
22
+ ids.add(entry.id);
23
+ directories.add(entry.directory);
24
+ }
25
+ });
26
+
27
+ export const isReservedBuiltInId = (id) => id.startsWith(BUILTIN_PREFIX);
28
+
29
+ /** Only application startup supplies this root; manifests and HTTP input cannot select it. */
30
+ export const readBuiltInRegistry = async (root = DEFAULT_BUILTIN_ROOT) => {
31
+ const realRoot = await fs.realpath(root);
32
+ const registry = builtInRegistrySchema.parse(JSON.parse(await fs.readFile(path.join(realRoot, 'registry.json'), 'utf8')));
33
+ return { root: realRoot, extensions: registry.extensions };
34
+ };