@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,336 @@
1
+ import fsSync from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+
6
+ import { GUEST_CAPABILITIES } from '@openchamber/sdk';
7
+
8
+ const GUEST_SOURCES = ['path', 'zip', 'git'];
9
+
10
+ const storeSchema = z.object({
11
+ paths: z.array(z.string().min(1).refine((entry) => !entry.includes('\0'))),
12
+ sources: z.record(z.string(), z.enum(GUEST_SOURCES)).optional(),
13
+ // Where a git install came from, keyed by install path. Entries are checked
14
+ // one at a time on read so a malformed one drops out instead of hiding the
15
+ // whole catalog.
16
+ gitOrigins: z.record(z.string(), z.unknown()).optional(),
17
+ // Grants are stored as plain strings and filtered on read: a capability
18
+ // that this build no longer knows (renamed, removed) must not invalidate the
19
+ // whole store and hide every installed extension.
20
+ capabilityGrants: z.record(z.string(), z.array(z.string())).optional(),
21
+ // What each grant covered when the user approved it: the filesystem
22
+ // patterns, the API origin, the service's exec names and socket ids. A
23
+ // newer package that widens any of these is treated as not approved for
24
+ // that capability. Entries are checked one at a time on read.
25
+ capabilityScopes: z.record(z.string(), z.unknown()).optional(),
26
+ disabledGuests: z.record(z.string(), z.literal(true)).optional(),
27
+ serviceSocketOverrides: z.record(
28
+ z.string(),
29
+ z.record(z.string(), z.string().min(1).refine((entry) => !entry.includes('\0'))),
30
+ ).optional(),
31
+ });
32
+
33
+ const capabilityScopeSchema = z.object({
34
+ filesystem: z.array(z.string().min(1)).optional(),
35
+ apiOrigin: z.string().min(1).optional(),
36
+ oauth: z.object({
37
+ authorizeUrl: z.string().min(1),
38
+ tokenUrl: z.string().min(1),
39
+ }).optional(),
40
+ service: z.object({
41
+ exec: z.array(z.string().min(1)),
42
+ sockets: z.array(z.string().min(1)),
43
+ }).optional(),
44
+ });
45
+
46
+ /** @returns {Record<string, { filesystem?: string[], apiOrigin?: string, service?: { exec: string[], sockets: string[] } }>} */
47
+ const knownScopesOnly = (scopes) => {
48
+ const cleaned = {};
49
+ for (const [guestId, raw] of Object.entries(scopes)) {
50
+ const parsed = capabilityScopeSchema.safeParse(raw);
51
+ if (parsed.success) {
52
+ cleaned[guestId] = parsed.data;
53
+ }
54
+ }
55
+ return cleaned;
56
+ };
57
+
58
+ const gitOriginSchema = z.object({
59
+ url: z.string().min(1).refine((entry) => !entry.includes('\0')),
60
+ ref: z.string().min(1).max(256).refine((entry) => !entry.includes('\0')).optional(),
61
+ gitIdentityId: z.string().trim().min(1).max(128).optional(),
62
+ });
63
+
64
+ /** @returns {Record<string, { url: string, ref?: string, gitIdentityId?: string }>} */
65
+ const knownGitOriginsOnly = (origins) => {
66
+ /** @type {Record<string, { url: string, ref?: string, gitIdentityId?: string }>} */
67
+ const cleaned = {};
68
+ for (const [installPath, raw] of Object.entries(origins)) {
69
+ const parsed = gitOriginSchema.safeParse(raw);
70
+ if (!parsed.success) {
71
+ continue;
72
+ }
73
+ cleaned[installPath] = parsed.data;
74
+ }
75
+ return cleaned;
76
+ };
77
+
78
+ const parseStore = (raw) => {
79
+ try {
80
+ const parsed = storeSchema.safeParse(JSON.parse(raw));
81
+ return parsed.success ? parsed.data : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ };
86
+
87
+ const dataDirSchema = z.string().min(1).refine((entry) => !entry.includes('\0') && path.isAbsolute(entry));
88
+
89
+ /** `{openchamberDataDir}/extensions.json`. One catalog per OpenChamber instance. */
90
+ export const extensionsPersistPath = (dataDir) => {
91
+ const parsed = dataDirSchema.safeParse(dataDir);
92
+ if (!parsed.success) {
93
+ throw new Error('Guest persist needs an absolute OpenChamber data dir');
94
+ }
95
+ return path.join(parsed.data, 'extensions.json');
96
+ };
97
+
98
+ /** Host-owned zip/git copies. Lives next to `extensions.json` as `{dataDir}/extensions/{id}`. */
99
+ export const guestCopiesDir = (persistPath) => path.join(path.dirname(persistPath), 'extensions');
100
+
101
+ const realOrResolved = (value) => {
102
+ try {
103
+ return fsSync.realpathSync(value);
104
+ } catch {
105
+ return path.resolve(value);
106
+ }
107
+ };
108
+
109
+ export const isCopiedGuestRoot = (root, persistPath) => {
110
+ const copies = realOrResolved(guestCopiesDir(persistPath));
111
+ const resolved = realOrResolved(root);
112
+ return resolved === copies || resolved.startsWith(`${copies}${path.sep}`);
113
+ };
114
+
115
+ const emptyStore = () => ({
116
+ paths: [],
117
+ sources: {},
118
+ gitOrigins: {},
119
+ capabilityGrants: {},
120
+ capabilityScopes: {},
121
+ disabledGuests: {},
122
+ serviceSocketOverrides: {},
123
+ });
124
+
125
+ const knownCapabilities = new Set(GUEST_CAPABILITIES);
126
+
127
+ const knownGrantsOnly = (grants) => Object.fromEntries(
128
+ Object.entries(grants).map(([guestId, list]) => [guestId, list.filter((capability) => knownCapabilities.has(capability))]),
129
+ );
130
+
131
+ export const readExtensionStore = async (persistPath) => {
132
+ try {
133
+ const raw = await fs.readFile(persistPath, 'utf8');
134
+ const parsed = parseStore(raw);
135
+ if (!parsed) {
136
+ throw new Error('Invalid extensions store');
137
+ }
138
+ return {
139
+ paths: parsed.paths,
140
+ sources: parsed.sources ?? {},
141
+ gitOrigins: knownGitOriginsOnly(parsed.gitOrigins ?? {}),
142
+ capabilityGrants: knownGrantsOnly(parsed.capabilityGrants ?? {}),
143
+ capabilityScopes: knownScopesOnly(parsed.capabilityScopes ?? {}),
144
+ disabledGuests: parsed.disabledGuests ?? {},
145
+ serviceSocketOverrides: parsed.serviceSocketOverrides ?? {},
146
+ };
147
+ } catch (error) {
148
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
149
+ return emptyStore();
150
+ }
151
+ throw error;
152
+ }
153
+ };
154
+
155
+ const writeExtensionStoreUnlocked = async (
156
+ persistPath,
157
+ {
158
+ paths,
159
+ sources = {},
160
+ gitOrigins = {},
161
+ capabilityGrants = {},
162
+ capabilityScopes = {},
163
+ disabledGuests = {},
164
+ serviceSocketOverrides = {},
165
+ },
166
+ ) => {
167
+ const cleaned = {};
168
+ /** @type {Record<string, { url: string, ref?: string }>} */
169
+ const origins = {};
170
+ for (const entry of paths) {
171
+ const source = sources[entry];
172
+ if (source && source !== 'path') {
173
+ cleaned[entry] = source;
174
+ }
175
+ // An origin only means something for a git copy that is still installed.
176
+ const origin = source === 'git' ? gitOrigins[entry] : undefined;
177
+ const parsedOrigin = gitOriginSchema.safeParse(origin);
178
+ if (parsedOrigin.success) {
179
+ origins[entry] = parsedOrigin.data;
180
+ }
181
+ }
182
+ const grants = {};
183
+ for (const [id, granted] of Object.entries(capabilityGrants)) {
184
+ if (Array.isArray(granted) && granted.length > 0) {
185
+ grants[id] = [...granted];
186
+ }
187
+ }
188
+ const scopes = {};
189
+ for (const [id, scope] of Object.entries(capabilityScopes)) {
190
+ // A scope only means something next to a grant.
191
+ if (grants[id] && capabilityScopeSchema.safeParse(scope).success) {
192
+ scopes[id] = scope;
193
+ }
194
+ }
195
+ const disabled = {};
196
+ for (const [id, isDisabled] of Object.entries(disabledGuests)) {
197
+ if (isDisabled) {
198
+ disabled[id] = true;
199
+ }
200
+ }
201
+ /** @type {Record<string, Record<string, string>>} */
202
+ const socketOverrides = {};
203
+ for (const [guestId, bySocket] of Object.entries(serviceSocketOverrides)) {
204
+ /** @type {Record<string, string>} */
205
+ const cleanedSockets = {};
206
+ for (const [socketId, socketPath] of Object.entries(bySocket ?? {})) {
207
+ if (typeof socketPath === 'string' && socketPath.trim() && !socketPath.includes('\0')) {
208
+ cleanedSockets[socketId] = socketPath.trim();
209
+ }
210
+ }
211
+ if (Object.keys(cleanedSockets).length > 0) {
212
+ socketOverrides[guestId] = cleanedSockets;
213
+ }
214
+ }
215
+ const payload = { paths };
216
+ if (Object.keys(cleaned).length > 0) {
217
+ payload.sources = cleaned;
218
+ }
219
+ if (Object.keys(origins).length > 0) {
220
+ payload.gitOrigins = origins;
221
+ }
222
+ if (Object.keys(grants).length > 0) {
223
+ payload.capabilityGrants = grants;
224
+ }
225
+ if (Object.keys(scopes).length > 0) {
226
+ payload.capabilityScopes = scopes;
227
+ }
228
+ if (Object.keys(disabled).length > 0) {
229
+ payload.disabledGuests = disabled;
230
+ }
231
+ if (Object.keys(socketOverrides).length > 0) {
232
+ payload.serviceSocketOverrides = socketOverrides;
233
+ }
234
+ // Listeners hear about a write twice: before, so a cached catalog is
235
+ // dropped, and after, so a read that started in between (and saw the old
236
+ // file) cannot be cached as current.
237
+ for (const listener of writeListeners) listener(persistPath);
238
+ await fs.mkdir(path.dirname(persistPath), { recursive: true });
239
+ const tmp = `${persistPath}.tmp-${process.pid}-${Date.now()}-${(writeSequence += 1)}`;
240
+ await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
241
+ await fs.rename(tmp, persistPath);
242
+ for (const listener of writeListeners) listener(persistPath);
243
+ };
244
+
245
+ /** Replace the whole store. Prefer `updateExtensionStore` for a change based on the current contents. */
246
+ export const writeExtensionStore = (persistPath, store) => (
247
+ withStoreWriteLock(persistPath, () => writeExtensionStoreUnlocked(persistPath, store))
248
+ );
249
+
250
+ /**
251
+ * Read, change, and write the store as one step under the write lock, so two
252
+ * changes made at the same time (Allow in one window, Pause in another) both
253
+ * land instead of the second one writing over a stale copy of the first.
254
+ * `mutate` returns the next store, or `null` to leave the file alone.
255
+ * @param {string} persistPath
256
+ * @param {(current: Awaited<ReturnType<typeof readExtensionStore>>) => Promise<object | null> | object | null} mutate
257
+ */
258
+ export const updateExtensionStore = (persistPath, mutate) => (
259
+ withStoreWriteLock(persistPath, async () => {
260
+ const current = await readExtensionStore(persistPath);
261
+ const next = await mutate(current);
262
+ if (next) {
263
+ await writeExtensionStoreUnlocked(persistPath, next);
264
+ }
265
+ })
266
+ );
267
+
268
+ let writeSequence = 0;
269
+ /** @type {Set<(persistPath: string) => void>} */
270
+ const writeListeners = new Set();
271
+
272
+ /** Called before and after every store write with the path that changes. */
273
+ export const onExtensionStoreWrite = (listener) => {
274
+ writeListeners.add(listener);
275
+ return () => writeListeners.delete(listener);
276
+ };
277
+ /** @type {Map<string, Promise<void>>} */
278
+ const writeChains = new Map();
279
+
280
+ // Two writers in one process (say, Enable and Allow clicked back to back)
281
+ // used to share one temp file: the second truncated it under the first's
282
+ // rename and the store came back as half a JSON document. Writes to one path
283
+ // now run one after another, each on its own temp file.
284
+ const withStoreWriteLock = (persistPath, write) => {
285
+ const previous = writeChains.get(persistPath) ?? Promise.resolve();
286
+ const next = previous.catch(() => undefined).then(write);
287
+ writeChains.set(persistPath, next.finally(() => {
288
+ if (writeChains.get(persistPath) === chained) writeChains.delete(persistPath);
289
+ }));
290
+ const chained = writeChains.get(persistPath);
291
+ return next;
292
+ };
293
+
294
+ export const readExtensionPaths = async (persistPath) => {
295
+ const store = await readExtensionStore(persistPath);
296
+ return store.paths;
297
+ };
298
+
299
+ export const writeExtensionPaths = (paths, persistPath) => updateExtensionStore(persistPath, (current) => {
300
+ const sources = {};
301
+ const gitOrigins = {};
302
+ for (const entry of paths) {
303
+ if (current.sources[entry]) {
304
+ sources[entry] = current.sources[entry];
305
+ }
306
+ if (current.gitOrigins[entry]) {
307
+ gitOrigins[entry] = current.gitOrigins[entry];
308
+ }
309
+ }
310
+ return { ...current, paths, sources, gitOrigins };
311
+ });
312
+
313
+ /**
314
+ * Record the user's approval for one guest. `granted` replaces the previous
315
+ * list; an empty list withdraws approval. Callers pass the requested list
316
+ * verbatim, so approval is all-or-nothing per install.
317
+ * @param {string} guestId
318
+ * @param {string} persistPath
319
+ * @param {string[]} granted
320
+ */
321
+ export const setCapabilityGrants = (guestId, persistPath, granted, scope = null) => updateExtensionStore(persistPath, (current) => {
322
+ const capabilityGrants = { ...current.capabilityGrants };
323
+ const capabilityScopes = { ...current.capabilityScopes };
324
+ if (granted.length > 0) {
325
+ capabilityGrants[guestId] = [...granted];
326
+ if (scope) {
327
+ capabilityScopes[guestId] = scope;
328
+ } else {
329
+ delete capabilityScopes[guestId];
330
+ }
331
+ } else {
332
+ delete capabilityGrants[guestId];
333
+ delete capabilityScopes[guestId];
334
+ }
335
+ return { ...current, capabilityGrants, capabilityScopes };
336
+ });
@@ -0,0 +1,288 @@
1
+ import { describe, 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
+
6
+ import {
7
+ extensionsPersistPath,
8
+ guestCopiesDir,
9
+ isCopiedGuestRoot,
10
+ readExtensionPaths,
11
+ setCapabilityGrants,
12
+ readExtensionStore,
13
+ writeExtensionPaths,
14
+ writeExtensionStore,
15
+ } from './persist.js';
16
+
17
+ describe('extensionsPersistPath', () => {
18
+ test('joins the instance data dir and refuses a relative path', () => {
19
+ const dataDir = path.join(os.tmpdir(), 'oc-instance-a');
20
+ expect(extensionsPersistPath(dataDir)).toBe(path.join(dataDir, 'extensions.json'));
21
+ expect(() => extensionsPersistPath('relative')).toThrow('absolute OpenChamber data dir');
22
+ expect(() => extensionsPersistPath('')).toThrow('absolute OpenChamber data dir');
23
+ });
24
+ });
25
+
26
+ describe('extension persist', () => {
27
+ test('round-trips paths and treats a missing file as empty', async () => {
28
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
29
+ const file = extensionsPersistPath(dir);
30
+ expect(await readExtensionPaths(file)).toEqual([]);
31
+ await writeExtensionPaths(['/one', '/two'], file);
32
+ expect(await readExtensionPaths(file)).toEqual(['/one', '/two']);
33
+ await fs.rm(dir, { recursive: true, force: true });
34
+ });
35
+
36
+ test('keeps two instance stores apart', async () => {
37
+ const parent = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
38
+ const a = extensionsPersistPath(path.join(parent, 'a'));
39
+ const b = extensionsPersistPath(path.join(parent, 'b'));
40
+ await writeExtensionPaths(['/one'], a);
41
+ await writeExtensionPaths(['/two'], b);
42
+ expect(await readExtensionPaths(a)).toEqual(['/one']);
43
+ expect(await readExtensionPaths(b)).toEqual(['/two']);
44
+ await fs.rm(parent, { recursive: true, force: true });
45
+ });
46
+
47
+ test('reads a path-only store and keeps zip sources', async () => {
48
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
49
+ const file = extensionsPersistPath(dir);
50
+ await fs.writeFile(file, `${JSON.stringify({ paths: ['/one'] })}\n`, 'utf8');
51
+ expect(await readExtensionStore(file)).toEqual({
52
+ paths: ['/one'],
53
+ sources: {},
54
+ gitOrigins: {},
55
+ capabilityGrants: {},
56
+ capabilityScopes: {},
57
+ disabledGuests: {},
58
+ serviceSocketOverrides: {},
59
+ });
60
+ await writeExtensionStore(file, {
61
+ paths: ['/one', '/two'],
62
+ sources: { '/one': 'path', '/two': 'zip' },
63
+ });
64
+ expect(await readExtensionStore(file)).toEqual({
65
+ paths: ['/one', '/two'],
66
+ sources: { '/two': 'zip' },
67
+ gitOrigins: {},
68
+ capabilityGrants: {},
69
+ capabilityScopes: {},
70
+ disabledGuests: {},
71
+ serviceSocketOverrides: {},
72
+ });
73
+ await writeExtensionPaths(['/two'], file);
74
+ expect(await readExtensionStore(file)).toEqual({
75
+ paths: ['/two'],
76
+ sources: { '/two': 'zip' },
77
+ gitOrigins: {},
78
+ capabilityGrants: {},
79
+ capabilityScopes: {},
80
+ disabledGuests: {},
81
+ serviceSocketOverrides: {},
82
+ });
83
+ expect(guestCopiesDir(file)).toBe(path.join(dir, 'extensions'));
84
+ expect(isCopiedGuestRoot(path.join(dir, 'extensions', 'hello'), file)).toBe(true);
85
+ expect(isCopiedGuestRoot(path.join(dir, 'other', 'hello'), file)).toBe(false);
86
+ await fs.rm(dir, { recursive: true, force: true });
87
+ });
88
+
89
+ test('round-trips service grants', async () => {
90
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
91
+ const file = extensionsPersistPath(dir);
92
+ await writeExtensionStore(file, {
93
+ paths: ['/one'],
94
+ sources: {},
95
+ gitOrigins: {},
96
+ capabilityGrants: { docker: ['service'] },
97
+ });
98
+ expect(await readExtensionStore(file)).toEqual({
99
+ paths: ['/one'],
100
+ sources: {},
101
+ gitOrigins: {},
102
+ capabilityGrants: { docker: ['service'] },
103
+ capabilityScopes: {},
104
+ disabledGuests: {},
105
+ serviceSocketOverrides: {},
106
+ });
107
+ await fs.rm(dir, { recursive: true, force: true });
108
+ });
109
+
110
+ test('round-trips service socket overrides', async () => {
111
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
112
+ const file = extensionsPersistPath(dir);
113
+ await writeExtensionStore(file, {
114
+ paths: ['/one'],
115
+ sources: {},
116
+ gitOrigins: {},
117
+ capabilityGrants: { docker: ['service'] },
118
+ serviceSocketOverrides: { docker: { docker: '/custom/docker.sock' } },
119
+ });
120
+ expect(await readExtensionStore(file)).toEqual({
121
+ paths: ['/one'],
122
+ sources: {},
123
+ gitOrigins: {},
124
+ capabilityGrants: { docker: ['service'] },
125
+ capabilityScopes: {},
126
+ disabledGuests: {},
127
+ serviceSocketOverrides: { docker: { docker: '/custom/docker.sock' } },
128
+ });
129
+ await writeExtensionPaths(['/one'], file);
130
+ expect(await readExtensionStore(file)).toEqual({
131
+ paths: ['/one'],
132
+ sources: {},
133
+ gitOrigins: {},
134
+ capabilityGrants: { docker: ['service'] },
135
+ capabilityScopes: {},
136
+ disabledGuests: {},
137
+ serviceSocketOverrides: { docker: { docker: '/custom/docker.sock' } },
138
+ });
139
+ await fs.rm(dir, { recursive: true, force: true });
140
+ });
141
+
142
+ test('round-trips disabled guests', async () => {
143
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
144
+ const file = extensionsPersistPath(dir);
145
+ await writeExtensionStore(file, {
146
+ paths: ['/one'],
147
+ sources: {},
148
+ disabledGuests: { docker: true },
149
+ });
150
+ expect(await readExtensionStore(file)).toEqual({
151
+ paths: ['/one'],
152
+ sources: {},
153
+ gitOrigins: {},
154
+ capabilityGrants: {},
155
+ capabilityScopes: {},
156
+ disabledGuests: { docker: true },
157
+ serviceSocketOverrides: {},
158
+ });
159
+ await fs.rm(dir, { recursive: true, force: true });
160
+ });
161
+
162
+ test('keeps the store whole under concurrent writes', async () => {
163
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
164
+ const file = extensionsPersistPath(dir);
165
+ try {
166
+ await Promise.all(Array.from({ length: 12 }, (_, index) => (
167
+ writeExtensionStore(file, { paths: [`/guests/${index}`] })
168
+ )));
169
+ const store = await readExtensionStore(file);
170
+ expect(store.paths).toHaveLength(1);
171
+ } finally {
172
+ await fs.rm(dir, { recursive: true, force: true });
173
+ }
174
+ });
175
+
176
+ test('drops grants this build no longer knows instead of refusing the store', async () => {
177
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
178
+ const persistPath = extensionsPersistPath(dir);
179
+ await fs.writeFile(persistPath, JSON.stringify({
180
+ paths: ['/tmp/old-echo', '/tmp/tasks'],
181
+ capabilityGrants: { 'old-echo': ['agent'], tasks: ['prompt', 'made-up', 'sessions'] },
182
+ }));
183
+ try {
184
+ const store = await readExtensionStore(persistPath);
185
+ expect(store.paths).toEqual(['/tmp/old-echo', '/tmp/tasks']);
186
+ expect(store.capabilityGrants).toEqual({ 'old-echo': [], tasks: ['prompt', 'sessions'] });
187
+ } finally {
188
+ await fs.rm(dir, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ test('refuses a corrupt store', async () => {
193
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
194
+ const file = extensionsPersistPath(dir);
195
+ await fs.writeFile(file, '{"paths":[1]}', 'utf8');
196
+ try {
197
+ await readExtensionPaths(file);
198
+ throw new Error('should have thrown');
199
+ } catch (error) {
200
+ expect(error).toBeInstanceOf(Error);
201
+ expect(String(error)).toContain('Invalid extensions store');
202
+ }
203
+ await fs.rm(dir, { recursive: true, force: true });
204
+ });
205
+
206
+ test('keeps git origins for installed git copies and drops malformed ones', async () => {
207
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
208
+ const file = extensionsPersistPath(dir);
209
+ await writeExtensionStore(file, {
210
+ paths: ['/git', '/zip', '/folder'],
211
+ sources: { '/git': 'git', '/zip': 'zip' },
212
+ gitOrigins: {
213
+ '/git': { url: 'https://github.com/acme/panel.git', ref: 'v1' },
214
+ '/zip': { url: 'https://example.com/ignored.git' },
215
+ '/folder': { url: 'https://example.com/ignored.git' },
216
+ '/gone': { url: 'https://example.com/ignored.git' },
217
+ },
218
+ });
219
+ expect((await readExtensionStore(file)).gitOrigins).toEqual({
220
+ '/git': { url: 'https://github.com/acme/panel.git', ref: 'v1' },
221
+ });
222
+
223
+ await writeExtensionPaths(['/zip'], file);
224
+ expect((await readExtensionStore(file)).gitOrigins).toEqual({});
225
+
226
+ await fs.writeFile(file, `${JSON.stringify({
227
+ paths: ['/a', '/b'],
228
+ sources: { '/a': 'git', '/b': 'git' },
229
+ gitOrigins: { '/a': { url: 'https://github.com/acme/a.git' }, '/b': { ref: 'main' }, '/c': 'nope' },
230
+ })}\n`, 'utf8');
231
+ const tolerant = await readExtensionStore(file);
232
+ expect(tolerant.paths).toEqual(['/a', '/b']);
233
+ expect(tolerant.gitOrigins).toEqual({ '/a': { url: 'https://github.com/acme/a.git' } });
234
+
235
+ await fs.rm(dir, { recursive: true, force: true });
236
+ });
237
+ });
238
+
239
+ describe('concurrent store changes', () => {
240
+ test('two approvals at once both land', async () => {
241
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
242
+ const file = extensionsPersistPath(dir);
243
+ try {
244
+ await writeExtensionStore(file, { paths: ['/a', '/b'] });
245
+ await Promise.all([
246
+ setCapabilityGrants('alpha', file, ['prompt'], null),
247
+ setCapabilityGrants('beta', file, ['filesystem'], { filesystem: ['~/notes/**'] }),
248
+ ]);
249
+ const store = await readExtensionStore(file);
250
+ expect(store.capabilityGrants).toEqual({ alpha: ['prompt'], beta: ['filesystem'] });
251
+ expect(store.capabilityScopes).toEqual({ beta: { filesystem: ['~/notes/**'] } });
252
+ } finally {
253
+ await fs.rm(dir, { recursive: true, force: true });
254
+ }
255
+ });
256
+
257
+ test('withdrawing approval also drops the recorded scope', async () => {
258
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
259
+ const file = extensionsPersistPath(dir);
260
+ try {
261
+ await writeExtensionStore(file, { paths: ['/a'] });
262
+ await setCapabilityGrants('alpha', file, ['network'], { apiOrigin: 'https://api.example' });
263
+ expect((await readExtensionStore(file)).capabilityScopes).toEqual({ alpha: { apiOrigin: 'https://api.example' } });
264
+ await setCapabilityGrants('alpha', file, []);
265
+ const store = await readExtensionStore(file);
266
+ expect(store.capabilityGrants).toEqual({});
267
+ expect(store.capabilityScopes).toEqual({});
268
+ } finally {
269
+ await fs.rm(dir, { recursive: true, force: true });
270
+ }
271
+ });
272
+
273
+ test('drops a malformed scope entry on read instead of refusing the store', async () => {
274
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
275
+ const file = extensionsPersistPath(dir);
276
+ try {
277
+ await fs.writeFile(file, JSON.stringify({
278
+ paths: ['/a'],
279
+ capabilityGrants: { alpha: ['filesystem'], beta: ['network'] },
280
+ capabilityScopes: { alpha: { filesystem: 'not-a-list' }, beta: { apiOrigin: 'https://api.example' } },
281
+ }));
282
+ const store = await readExtensionStore(file);
283
+ expect(store.capabilityScopes).toEqual({ beta: { apiOrigin: 'https://api.example' } });
284
+ } finally {
285
+ await fs.rm(dir, { recursive: true, force: true });
286
+ }
287
+ });
288
+ });