@openchambery/web 2.0.0-beta.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-CzTtVMnI.js → AgentAvatar-C1MGZ7qC.js} +1 -1
  3. package/dist/assets/AssistantView-vqCFvVo0.js +3 -0
  4. package/dist/assets/{DiagramView-oeXj4YHn.js → DiagramView-CYO5iVVm.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-DCqrQkkz.js → FadeInOnReveal-BMSk_tY-.js} +1 -1
  6. package/dist/assets/Icon-BdzPRCSy.js +1 -0
  7. package/dist/assets/MarkdownRenderer-FMynjkGR.js +2 -0
  8. package/dist/assets/{MarkdownRendererImpl-CahUctxC.js → MarkdownRendererImpl-DoWvw5jj.js} +3 -3
  9. package/dist/assets/{MarkstreamRendererImpl-5JUaTSdl.js → MarkstreamRendererImpl-DoeYajmw.js} +1 -1
  10. package/dist/assets/{MobileDetailNavigation-agQ-Yhpd.js → MobileDetailNavigation-BzBz-co_.js} +1 -1
  11. package/dist/assets/{MobileShareBridge-yCN95dYg.js → MobileShareBridge-D1Uoj6l_.js} +3 -3
  12. package/dist/assets/MobileSurface-8YpZ51Qo.js +2 -0
  13. package/dist/assets/{MultiRunWindow-C0HqMFpp.js → MultiRunWindow-BxcYUPta.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-O4cGdz5p.js → OnboardingScreen-yRdQTR1M.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-CRfGtcmG.js → OverlayScrollbar-BUjtqnxn.js} +1 -1
  16. package/dist/assets/SettingsView-D7KrvqvH.js +25 -0
  17. package/dist/assets/{SettingsWindow-B_d5Aqq3.js → SettingsWindow-DX2w-cDc.js} +1 -1
  18. package/dist/assets/{TerminalView-DdNQQb8W.js → TerminalView-CiUcCQPm.js} +1 -1
  19. package/dist/assets/{ToolOutputDialog-4DC5OE1J.js → ToolOutputDialog-DQvXtK71.js} +1 -1
  20. package/dist/assets/appThemeRegistry-BXC6cIDE.js +1 -0
  21. package/dist/assets/{assistant-attachment-cache-DphJleHr.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
  22. package/dist/assets/{button-DIIrsMjW.js → button-CUwjpv-i.js} +1 -1
  23. package/dist/assets/{collapsible-CnvUeYst.js → collapsible-paItOH3A.js} +1 -1
  24. package/dist/assets/{context-menu-BGBUpZEQ.js → context-menu-K6zNumqN.js} +1 -1
  25. package/dist/assets/{desktopHosts-JGFqhlaM.js → desktopHosts-DuftEj8B.js} +1 -1
  26. package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
  27. package/dist/assets/{diagram-rvn7_qBM.js → diagram-D6nY5hbD.js} +1 -1
  28. package/dist/assets/dialog-PjKWayIU.js +1 -0
  29. package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
  30. package/dist/assets/{es-Dsmc0A_3.js → es-BBbon7SL.js} +4 -4
  31. package/dist/assets/fileMentionTouchSelection-78v5mNTE.js +2 -0
  32. package/dist/assets/{fileQueries-DN6gbLhM.js → fileQueries-DuF89nFO.js} +1 -1
  33. package/dist/assets/{fr-C_ZlZDI2.js → fr-BsKj2CTi.js} +4 -4
  34. package/dist/assets/{gitApi-da8etTTe.js → gitApi-eoKSJcvW.js} +4 -4
  35. package/dist/assets/{imageSource-BMztRu0c.js → imageSource-BpUsiwFd.js} +1 -1
  36. package/dist/assets/{input-BVu-iykl.js → input-BeVw4D0W.js} +1 -1
  37. package/dist/assets/insertionBoundaries-BI3RHOpf.js +1 -0
  38. package/dist/assets/{ja-BqqTaWTi.js → ja-1mgxunoM.js} +4 -4
  39. package/dist/assets/{ko-DP2XPM37.js → ko-Ra9znPcL.js} +4 -4
  40. package/dist/assets/main-CQpUPjPK.js +169 -0
  41. package/dist/assets/{main-KwlAomA8.js → main-MN0YKJ0_.js} +2 -2
  42. package/dist/assets/main-qSeeJRW1.css +1 -0
  43. package/dist/assets/{markdownHeightCache-D9-B1wdY.js → markdownHeightCache-CGCWmHgq.js} +1 -1
  44. package/dist/assets/{mcpQueries-3GTWeMU-.js → mcpQueries-X424Iq2z.js} +1 -1
  45. package/dist/assets/miniChat-DFIl76ku.js +2 -0
  46. package/dist/assets/mobile-C3fWXGQJ.js +2 -0
  47. package/dist/assets/multirun-DuSX3YRs.js +3 -0
  48. package/dist/assets/persistence-Dnj1QOdX.js +3 -0
  49. package/dist/assets/pl-B714sdql.js +15 -0
  50. package/dist/assets/platform-jCZ3KfMQ.js +6 -0
  51. package/dist/assets/projectMeta-Cn08pTNi.js +1 -0
  52. package/dist/assets/{pt-BR-W09Q2pvn.js → pt-BR-DVoT_I7_.js} +4 -4
  53. package/dist/assets/{relay-image-stream-DrdVj99c.js → relay-image-stream-D2XooXKZ.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-CRPdctIn.js +2 -0
  55. package/dist/assets/renderMobileApp-CVURQbeh.js +28 -0
  56. package/dist/assets/{runtime-fetch-Fs9ZV2V8.js → runtime-fetch-CiqQd__Y.js} +1 -1
  57. package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
  58. package/dist/assets/runtimeConfig-xivE1wxC.js +2 -0
  59. package/dist/assets/runtimeEndpointReset-B8DzKzwg.js +148 -0
  60. package/dist/assets/runtimeEndpointReset-D_vfNOQz.css +1 -0
  61. package/dist/assets/sessionChangeSummary-yBsR2pyd.js +2 -0
  62. package/dist/assets/{sessionLookup-0Z4e-J1g.js → sessionLookup-1Z1FWZRB.js} +1 -1
  63. package/dist/assets/{sortable-tabs-strip-CE7W7lJ9.js → sortable-tabs-strip-D611LPyd.js} +1 -1
  64. package/dist/assets/src-TBM7scxy.css +2 -0
  65. package/dist/assets/{store-jaP79mqI.js → store-l_X_jQNz.js} +4 -4
  66. package/dist/assets/{terminalApi-mDR2jXTo.js → terminalApi-BZW_rAc3.js} +1 -1
  67. package/dist/assets/{textarea-XD2oX_Ee.js → textarea-QtZjf4L8.js} +1 -1
  68. package/dist/assets/{tooltip-DYeQ_Hvy.js → tooltip-DDNnmcXI.js} +1 -1
  69. package/dist/assets/{uk-BgFzz8m2.js → uk-ChNnTgKP.js} +4 -4
  70. package/dist/assets/{url-CGbJbiJq.js → url-DNRMq_0R.js} +1 -1
  71. package/dist/assets/{useAppFontEffects-y_GTVXhJ.js → useAppFontEffects-DJy8P5By.js} +55 -55
  72. package/dist/assets/{useAssistantUIStore-Bz4tRflF.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
  73. package/dist/assets/useDesktopWindowControlsLayout-DlwSomVK.js +1 -0
  74. package/dist/assets/{useEffectiveDirectory-BW9jNyuK.js → useEffectiveDirectory-eJorVTT9.js} +1 -1
  75. package/dist/assets/{useFontPreferences-Bjx8zkGM.js → useFontPreferences-Bjgt_lzl.js} +1 -1
  76. package/dist/assets/useMobileNavigationStore-A_ZNTfpl.js +415 -0
  77. package/dist/assets/useSessionAutoCleanup-CqC3dEou.js +2 -0
  78. package/dist/assets/{useUIStore-syVmbuDA.js → useUIStore-Dfx6TKvi.js} +1 -1
  79. package/dist/assets/{useWorkerHighlightedLines-BUcSLKlL.js → useWorkerHighlightedLines-B0vSL6jr.js} +1 -1
  80. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  81. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-DzRqcymP.js} +1 -1
  82. package/dist/assets/{zh-CN-BhqSLgUY.js → zh-CN-DHEFf4u1.js} +4 -4
  83. package/dist/assets/{zh-TW-BsaBgY-p.js → zh-TW-rdSa8atW.js} +4 -4
  84. package/dist/index.html +13 -13
  85. package/dist/mini-chat.html +13 -13
  86. package/dist/mobile.html +13 -13
  87. package/package.json +3 -3
  88. package/server/index.js +8 -20
  89. package/server/lib/client-auth/remote-clients.js +15 -0
  90. package/server/lib/client-auth/remote-clients.test.js +42 -1
  91. package/server/lib/git/DOCUMENTATION.md +1 -0
  92. package/server/lib/git/routes.js +7 -4
  93. package/server/lib/git/routes.test.js +34 -0
  94. package/server/lib/git/service.js +3 -1
  95. package/server/lib/llm/DOCUMENTATION.md +6 -4
  96. package/server/lib/llm/generate.js +8 -9
  97. package/server/lib/llm/generate.test.js +33 -6
  98. package/server/lib/message-queue/service.js +1 -1
  99. package/server/lib/opencode/DOCUMENTATION.md +33 -22
  100. package/server/lib/opencode/agents.js +13 -22
  101. package/server/lib/opencode/auth-state-runtime.js +3 -3
  102. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  103. package/server/lib/opencode/auth.d.ts +13 -0
  104. package/server/lib/opencode/auth.js +74 -64
  105. package/server/lib/opencode/auth.test.js +57 -0
  106. package/server/lib/opencode/commands.js +1 -1
  107. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  108. package/server/lib/opencode/config-entity-routes.js +10 -123
  109. package/server/lib/opencode/config-entity-routes.test.js +23 -11
  110. package/server/lib/opencode/config-files.d.ts +17 -0
  111. package/server/lib/opencode/config-files.js +187 -0
  112. package/server/lib/opencode/config-files.test.js +201 -0
  113. package/server/lib/opencode/core-routes.js +17 -1
  114. package/server/lib/opencode/core-routes.test.js +43 -15
  115. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  116. package/server/lib/opencode/credential-boundary.js +11 -0
  117. package/server/lib/opencode/ensure-cli.js +4 -1
  118. package/server/lib/opencode/env-config.js +4 -1
  119. package/server/lib/opencode/env-config.test.js +28 -0
  120. package/server/lib/opencode/env-runtime.test.js +18 -1
  121. package/server/lib/opencode/feature-routes-runtime.js +9 -7
  122. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  123. package/server/lib/opencode/lifecycle.js +1 -49
  124. package/server/lib/opencode/lifecycle.test.js +30 -4
  125. package/server/lib/opencode/location-release.d.ts +8 -0
  126. package/server/lib/opencode/location-release.js +32 -0
  127. package/server/lib/opencode/location-release.test.js +43 -0
  128. package/server/lib/opencode/mcp.js +1 -1
  129. package/server/lib/opencode/opencode2-pin.js +2 -1
  130. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  131. package/server/lib/opencode/plugin-routes.test.js +4 -2
  132. package/server/lib/opencode/plugins.js +12 -60
  133. package/server/lib/opencode/plugins.test.js +5 -3
  134. package/server/lib/opencode/providers.js +6 -3
  135. package/server/lib/opencode/proxy.js +7 -0
  136. package/server/lib/opencode/proxy.test.js +13 -0
  137. package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
  138. package/server/lib/opencode/routes.js +10 -8
  139. package/server/lib/opencode/routes.test.js +51 -10
  140. package/server/lib/opencode/runtime-contract.js +3 -2
  141. package/server/lib/opencode/runtime-contract.test.js +12 -12
  142. package/server/lib/opencode/settings-normalization-runtime.js +3 -1
  143. package/server/lib/opencode/settings-normalization-runtime.test.js +36 -0
  144. package/server/lib/opencode/settings-runtime.test.js +58 -1
  145. package/server/lib/opencode/shared.js +6 -175
  146. package/server/lib/opencode/update-discovery.js +39 -0
  147. package/server/lib/opencode/update-discovery.test.js +39 -0
  148. package/server/lib/opencode/upgrade-screen.js +4 -11
  149. package/server/lib/opencode/upgrade-screen.test.js +25 -25
  150. package/server/lib/opencode/version-alignment.test.js +40 -0
  151. package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
  152. package/server/lib/question-auto-delegate/core.d.ts +5 -1
  153. package/server/lib/question-auto-delegate/core.js +21 -13
  154. package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
  155. package/server/lib/question-auto-delegate/form-io.js +39 -0
  156. package/server/lib/question-auto-delegate/runtime.js +4 -24
  157. package/server/lib/question-auto-delegate/runtime.test.js +138 -0
  158. package/server/lib/quota/DOCUMENTATION.md +9 -0
  159. package/server/lib/quota/provider-credentials.test.js +29 -0
  160. package/server/lib/quota/providers/claude.js +4 -4
  161. package/server/lib/quota/providers/codex.js +4 -4
  162. package/server/lib/quota/providers/codex.test.js +1 -1
  163. package/server/lib/quota/providers/copilot.js +5 -5
  164. package/server/lib/quota/providers/google/auth.js +3 -3
  165. package/server/lib/quota/providers/google/index.js +2 -2
  166. package/server/lib/quota/providers/index.js +3 -7
  167. package/server/lib/quota/providers/index.test.js +2 -2
  168. package/server/lib/quota/providers/kimi.js +4 -4
  169. package/server/lib/quota/providers/minimax-shared.js +4 -4
  170. package/server/lib/quota/providers/nanogpt.js +4 -4
  171. package/server/lib/quota/providers/openai.js +4 -4
  172. package/server/lib/quota/providers/openrouter.js +4 -4
  173. package/server/lib/quota/providers/wafer.js +4 -4
  174. package/server/lib/quota/providers/zai.js +4 -4
  175. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  176. package/server/lib/quota/routes.js +1 -1
  177. package/server/lib/relay/DOCUMENTATION.md +3 -1
  178. package/server/lib/relay/service.js +1 -0
  179. package/server/lib/relay/service.test.js +13 -0
  180. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
  181. package/server/lib/scheduled-tasks/runtime.js +12 -4
  182. package/server/lib/scheduled-tasks/runtime.test.js +81 -6
  183. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  184. package/server/lib/session-goal/runtime.js +4 -4
  185. package/server/lib/session-goal/runtime.test.js +32 -3
  186. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  187. package/server/lib/session-index/event-ingest.js +13 -0
  188. package/server/lib/session-index/event-ingest.test.js +7 -0
  189. package/server/lib/session-index/service.js +45 -0
  190. package/server/lib/session-index/service.test.js +47 -0
  191. package/server/lib/session-metadata/DOCUMENTATION.md +6 -0
  192. package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
  193. package/server/lib/session-metadata/session-projection.d.ts +1 -0
  194. package/server/lib/session-metadata/session-projection.js +23 -0
  195. package/server/lib/session-metadata/session-projection.test.js +35 -0
  196. package/server/lib/session-title/DOCUMENTATION.md +52 -14
  197. package/server/lib/session-title/native-runtime.test.js +59 -5
  198. package/server/lib/session-title/runtime.js +63 -17
  199. package/server/lib/session-title/session-access.js +108 -3
  200. package/server/lib/session-title/session-access.test.js +125 -7
  201. package/server/lib/tts/DOCUMENTATION.md +7 -5
  202. package/server/lib/tts/credential-source.test.js +23 -0
  203. package/server/lib/tts/routes.js +2 -2
  204. package/server/lib/tts/service.js +16 -37
  205. package/dist/assets/AssistantView-CLFPj5Ki.js +0 -3
  206. package/dist/assets/Icon-B8fSTLdP.js +0 -1
  207. package/dist/assets/MarkdownRenderer-BK69MEHQ.js +0 -2
  208. package/dist/assets/MobileSurface-D0HP_lTS.js +0 -2
  209. package/dist/assets/SettingsView-xLYb22wV.js +0 -25
  210. package/dist/assets/appThemeRegistry-LkVH62uF.js +0 -1
  211. package/dist/assets/desktopRelayRestore-CHEwrI93.js +0 -1
  212. package/dist/assets/dialog-CQ63sc9E.js +0 -1
  213. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
  214. package/dist/assets/fileMentionTouchSelection-CBzDuQxP.js +0 -3
  215. package/dist/assets/insertionBoundaries-DQrE5qG2.js +0 -1
  216. package/dist/assets/main-D2BzpnUt.css +0 -1
  217. package/dist/assets/main-DYISNvWY.js +0 -169
  218. package/dist/assets/miniChat-DYIqdWi7.js +0 -2
  219. package/dist/assets/mobile-CwiCcmWt.js +0 -2
  220. package/dist/assets/multirun-C3snjTFs.js +0 -4
  221. package/dist/assets/persistence-D0y7226-.js +0 -3
  222. package/dist/assets/pl-BCf5uLlK.js +0 -15
  223. package/dist/assets/platform-CurqO28D.js +0 -6
  224. package/dist/assets/projectMeta-BkhYFMaM.js +0 -3
  225. package/dist/assets/renderElectronMiniChatApp-7iyh_419.js +0 -2
  226. package/dist/assets/renderMobileApp-y0_NeckR.js +0 -28
  227. package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
  228. package/dist/assets/runtimeConfig-12C5j02f.js +0 -2
  229. package/dist/assets/runtimeEndpointReset-DTY2ZXXb.js +0 -149
  230. package/dist/assets/sessionChangeSummary-CwPBIb3Z.js +0 -2
  231. package/dist/assets/src-lS1keDVV.css +0 -2
  232. package/dist/assets/useDesktopWindowControlsLayout-CR_DcY77.js +0 -1
  233. package/dist/assets/useMobileNavigationStore-DwupvLSA.js +0 -414
  234. package/dist/assets/useSessionAutoCleanup-DkGO_usj.js +0 -2
  235. package/dist/assets/vendor-opencode-client-C12gfjag.js +0 -12
  236. package/server/lib/opencode/snippets.js +0 -242
  237. package/server/lib/opencode/snippets.test.js +0 -97
package/dist/index.html CHANGED
@@ -552,28 +552,28 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-KwlAomA8.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-MN0YKJ0_.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
558
- <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
559
- <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-Fs9ZV2V8.js">
558
+ <link rel="modulepreload" crossorigin href="/assets/runtime-switch-BJM2k_uH.js">
559
+ <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-CiqQd__Y.js">
560
560
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
561
- <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
562
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-syVmbuDA.js">
561
+ <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-DzRqcymP.js">
562
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-Dfx6TKvi.js">
563
563
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
564
564
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
565
565
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
566
- <link rel="modulepreload" crossorigin href="/assets/persistence-D0y7226-.js">
567
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-JGFqhlaM.js">
568
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CHEwrI93.js">
569
- <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-C12gfjag.js">
566
+ <link rel="modulepreload" crossorigin href="/assets/persistence-Dnj1QOdX.js">
567
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-DuftEj8B.js">
568
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-g378BdZf.js">
569
+ <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-MF2GBrFx.js">
570
570
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
571
- <link rel="modulepreload" crossorigin href="/assets/platform-CurqO28D.js">
572
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-mDR2jXTo.js">
571
+ <link rel="modulepreload" crossorigin href="/assets/platform-jCZ3KfMQ.js">
572
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-BZW_rAc3.js">
573
573
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
574
574
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
575
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-12C5j02f.js">
576
- <link rel="stylesheet" crossorigin href="/assets/src-lS1keDVV.css">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-xivE1wxC.js">
576
+ <link rel="stylesheet" crossorigin href="/assets/src-TBM7scxy.css">
577
577
  </head>
578
578
  <body class="h-full bg-background text-foreground">
579
579
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -74,28 +74,28 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-DYIqdWi7.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-DFIl76ku.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
80
- <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
81
- <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-Fs9ZV2V8.js">
80
+ <link rel="modulepreload" crossorigin href="/assets/runtime-switch-BJM2k_uH.js">
81
+ <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-CiqQd__Y.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
83
- <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
84
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-syVmbuDA.js">
83
+ <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-DzRqcymP.js">
84
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-Dfx6TKvi.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
86
86
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
87
87
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
88
- <link rel="modulepreload" crossorigin href="/assets/persistence-D0y7226-.js">
89
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-JGFqhlaM.js">
90
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CHEwrI93.js">
91
- <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-C12gfjag.js">
88
+ <link rel="modulepreload" crossorigin href="/assets/persistence-Dnj1QOdX.js">
89
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-DuftEj8B.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-g378BdZf.js">
91
+ <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-MF2GBrFx.js">
92
92
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
93
- <link rel="modulepreload" crossorigin href="/assets/platform-CurqO28D.js">
94
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-mDR2jXTo.js">
93
+ <link rel="modulepreload" crossorigin href="/assets/platform-jCZ3KfMQ.js">
94
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-BZW_rAc3.js">
95
95
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
96
96
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
97
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-12C5j02f.js">
98
- <link rel="stylesheet" crossorigin href="/assets/src-lS1keDVV.css">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-xivE1wxC.js">
98
+ <link rel="stylesheet" crossorigin href="/assets/src-TBM7scxy.css">
99
99
  </head>
100
100
  <body class="h-full bg-background text-foreground">
101
101
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,28 +63,28 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-CwiCcmWt.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-C3fWXGQJ.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
69
- <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
70
- <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-Fs9ZV2V8.js">
69
+ <link rel="modulepreload" crossorigin href="/assets/runtime-switch-BJM2k_uH.js">
70
+ <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-CiqQd__Y.js">
71
71
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
72
- <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
73
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-syVmbuDA.js">
72
+ <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-DzRqcymP.js">
73
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-Dfx6TKvi.js">
74
74
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
75
75
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
76
76
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
77
- <link rel="modulepreload" crossorigin href="/assets/persistence-D0y7226-.js">
78
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-JGFqhlaM.js">
79
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-CHEwrI93.js">
80
- <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-C12gfjag.js">
77
+ <link rel="modulepreload" crossorigin href="/assets/persistence-Dnj1QOdX.js">
78
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-DuftEj8B.js">
79
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-g378BdZf.js">
80
+ <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-MF2GBrFx.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
82
- <link rel="modulepreload" crossorigin href="/assets/platform-CurqO28D.js">
83
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-mDR2jXTo.js">
82
+ <link rel="modulepreload" crossorigin href="/assets/platform-jCZ3KfMQ.js">
83
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-BZW_rAc3.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
86
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-12C5j02f.js">
87
- <link rel="stylesheet" crossorigin href="/assets/src-lS1keDVV.css">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-xivE1wxC.js">
87
+ <link rel="stylesheet" crossorigin href="/assets/src-TBM7scxy.css">
88
88
  </head>
89
89
  <body class="h-full bg-background text-foreground">
90
90
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "2.0.0-beta.9",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -27,8 +27,8 @@
27
27
  "@clack/prompts": "^1.1.0",
28
28
  "@earendil-works/pi-agent-core": "0.84.4",
29
29
  "@octokit/rest": "^22.0.1",
30
- "@opencode/client": "2.0.15",
31
- "@opencode/schema": "2.0.15",
30
+ "@opencode/client": "2.0.23",
31
+ "@opencode/schema": "2.0.23",
32
32
  "@simplewebauthn/server": "13.3.1",
33
33
  "adm-zip": "^0.5.16",
34
34
  "better-sqlite3": "^12.10.0",
package/server/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import express from 'express';
3
+ import { configureOpenCodeCredentials, openCodeCredentialSource } from './lib/opencode/auth.js';
3
4
  import compression from 'compression';
4
5
  import path from 'path';
5
6
  import { spawn, spawnSync } from 'child_process';
@@ -601,6 +602,7 @@ const waitForReady = (...args) => openCodeNetworkRuntime.waitForReady(...args);
601
602
  const normalizeApiPrefix = (...args) => openCodeNetworkRuntime.normalizeApiPrefix(...args);
602
603
  const setDetectedOpenCodeApiPrefix = (...args) => openCodeNetworkRuntime.setDetectedOpenCodeApiPrefix(...args);
603
604
  const buildOpenCodeUrl = (...args) => openCodeNetworkRuntime.buildOpenCodeUrl(...args);
605
+ configureOpenCodeCredentials(openCodeCredentialSource({ buildOpenCodeUrl, getOpenCodeAuthHeaders }));
604
606
  const ensureOpenCodeApiPrefix = (...args) => openCodeNetworkRuntime.ensureOpenCodeApiPrefix(...args);
605
607
  const scheduleOpenCodeApiDetection = (...args) => openCodeNetworkRuntime.scheduleOpenCodeApiDetection(...args);
606
608
 
@@ -755,6 +757,8 @@ const sessionTitleRuntime = createSessionTitleRuntime({
755
757
  buildOpenCodeUrl,
756
758
  getOpenCodeAuthHeaders,
757
759
  readSessionMetadata: (sessionID) => sessionMetadataStore.get(sessionID),
760
+ mutateSessionMetadata: (sessionID, decide) => sessionMetadataStore.mutateSessionMetadata(sessionID, decide),
761
+ listSessionMetadata: () => sessionMetadataStore.getAll(),
758
762
  persistSessionMetadata: (sessionID, patch) => persistSessionMetadataToStore(sessionID, patch),
759
763
  publishSession: (session, directory) => {
760
764
  const payload = { type: 'session.updated', properties: { info: session, directory } };
@@ -1043,6 +1047,9 @@ const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
1043
1047
  // envelope's directory to route their own OpenCode calls to the right instance.
1044
1048
  console.log('[session-title] listening for session events');
1045
1049
  console.log('[session-goal] listening for session events');
1050
+ globalMessageStreamHub.subscribeStatus((status) => {
1051
+ if (status.type === 'connect') sessionTitleRuntime.processPayload({ type: 'server.connected' });
1052
+ });
1046
1053
  globalMessageStreamHub.subscribeEvent((event) => {
1047
1054
  const raw = event?.payload;
1048
1055
  const payload = raw?.payload && typeof raw.payload === 'object' ? raw.payload : raw;
@@ -1293,26 +1300,6 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
1293
1300
  getManagedOpenCodeShellEnvSnapshot: getLoginShellEnvSnapshot,
1294
1301
  managedCapabilitiesRuntime,
1295
1302
  getActiveSessionCount,
1296
- // Most-recently-used directories first: OpenCode initializes each directory
1297
- // lazily on first request (seconds on large session stores), so the
1298
- // lifecycle warms these right after readiness — before the UI's first
1299
- // interactive request would otherwise pay that cost.
1300
- getWarmupDirectories: async () => {
1301
- const settings = await readSettingsFromDiskMigrated().catch(() => null);
1302
- if (!settings) return [];
1303
- const directories = [];
1304
- if (typeof settings.lastDirectory === 'string' && settings.lastDirectory) {
1305
- directories.push(settings.lastDirectory);
1306
- }
1307
- const projects = Array.isArray(settings.projects) ? [...settings.projects] : [];
1308
- projects.sort((a, b) => (b?.lastOpenedAt ?? 0) - (a?.lastOpenedAt ?? 0));
1309
- for (const project of projects) {
1310
- if (typeof project?.path === 'string' && project.path) {
1311
- directories.push(project.path);
1312
- }
1313
- }
1314
- return [...new Set(directories)];
1315
- },
1316
1303
  });
1317
1304
 
1318
1305
  const restartOpenCode = (...args) => openCodeLifecycleRuntime.restartOpenCode(...args);
@@ -2188,6 +2175,7 @@ async function main(options = {}) {
2188
2175
  };
2189
2176
  },
2190
2177
  stop: async (shutdownOptions = {}) => {
2178
+ sessionTitleRuntime.stop();
2191
2179
  try {
2192
2180
  unsubscribeSessionIndexEvents();
2193
2181
  unsubscribeMessageQueueEvents();
@@ -204,6 +204,20 @@ export const createRemoteClientAuthRuntime = ({ fsPromises, path, crypto, storeP
204
204
  });
205
205
  };
206
206
 
207
+ const renameClient = async (id, label) => {
208
+ if (typeof label !== 'string' || !label.trim() || label.trim().length > MAX_LABEL_LENGTH) {
209
+ throw Object.assign(new Error('Client label must contain 1–80 characters'), { status: 400 });
210
+ }
211
+ return withStoreMutation(async () => {
212
+ const store = await readStore();
213
+ const client = store.clients.find((entry) => entry.id === id);
214
+ if (!client) return null;
215
+ client.label = label.trim();
216
+ await writeStore(store);
217
+ return publicClient(client);
218
+ });
219
+ };
220
+
207
221
  const revokeClient = async (id) => {
208
222
  if (typeof id !== 'string' || id.trim().length === 0) {
209
223
  return { revoked: false };
@@ -264,6 +278,7 @@ export const createRemoteClientAuthRuntime = ({ fsPromises, path, crypto, storeP
264
278
  listClients,
265
279
  hasActiveRelayClients,
266
280
  purgeRevokedClients,
281
+ renameClient,
267
282
  revokeClient,
268
283
  };
269
284
  };
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'bun:test';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
@@ -17,6 +17,47 @@ const createRuntime = async () => {
17
17
  };
18
18
 
19
19
  describe('remote client auth runtime', () => {
20
+ it('persists renamed labels without changing credentials, metadata or revocation', async () => {
21
+ const { dir, runtime } = await createRuntime();
22
+ try {
23
+ const created = await runtime.createClient({ label: 'Phone', devicePlatform: 'iOS', usesRelay: true });
24
+ const renamed = await runtime.renameClient(created.client.id, ' Work phone ');
25
+ expect(renamed).toEqual({ ...created.client, label: 'Work phone' });
26
+ expect('tokenHash' in renamed).toBe(false);
27
+ const reopened = createRemoteClientAuthRuntime({ fsPromises: fs, path, crypto, storePath: path.join(dir, 'remote-clients.json') });
28
+ expect(await reopened.listClients()).toEqual([renamed]);
29
+ expect((await reopened.authenticateBearerToken(created.token))?.clientId).toBe(created.client.id);
30
+ await Promise.all([
31
+ runtime.renameClient(created.client.id, 'Archived phone'),
32
+ runtime.revokeClient(created.client.id),
33
+ ]);
34
+ expect(await runtime.authenticateBearerToken(created.token)).toBe(null);
35
+ expect((await runtime.listClients())[0]).toMatchObject({ label: 'Archived phone', revokedAt: expect.any(String) });
36
+ } finally {
37
+ await fs.rm(dir, { recursive: true, force: true });
38
+ }
39
+ });
40
+
41
+ it('rejects invalid labels and failed writes without committing a new name', async () => {
42
+ const { dir, runtime } = await createRuntime();
43
+ try {
44
+ const created = await runtime.createClient({ label: 'Phone' });
45
+ for (const label of [undefined, null, 42, '', ' ', 'x'.repeat(81)]) {
46
+ await expect(runtime.renameClient(created.client.id, label)).rejects.toMatchObject({ status: 400 });
47
+ }
48
+ expect(await runtime.renameClient('missing', 'Name')).toBe(null);
49
+ const failing = createRemoteClientAuthRuntime({
50
+ fsPromises: { ...fs, writeFile: async () => { throw new Error('Write failed'); } },
51
+ path, crypto, storePath: path.join(dir, 'remote-clients.json'),
52
+ });
53
+ await expect(failing.renameClient(created.client.id, 'New name')).rejects.toThrow('Write failed');
54
+ expect(await runtime.listClients()).toEqual([created.client]);
55
+ expect((await runtime.authenticateBearerToken(created.token))?.ok).toBe(true);
56
+ } finally {
57
+ await fs.rm(dir, { recursive: true, force: true });
58
+ }
59
+ });
60
+
20
61
  it('creates, authenticates, lists, and revokes client tokens', async () => {
21
62
  const { dir, runtime } = await createRuntime();
22
63
  try {
@@ -54,6 +54,7 @@ The following functions are exported and used by the web server:
54
54
  - `validateWorktreeCreate(directory, input)`: Validate worktree creation parameters (mode, branchName, startRef, upstream config).
55
55
  - `createWorktree(directory, input)`: Create a new worktree (supports 'new' and 'existing' modes, upstream setup).
56
56
  - `removeWorktree(directory, input)`: Remove a worktree (optionally delete local branch).
57
+ - `removeWorktree(directory, input, { beforeRemove })`: optional host hook after primary-workspace/catalog validation, immediately before removal of a matched worktree or managed orphan. The hook never runs for unrelated orphan paths. Production hosts use the official v2 `debug.location.evict` with the validated target and current upstream URL/auth; the 2s budget returns `released`, `failed`, `timeout`, or `unavailable`. Eviction is best effort and never counts as successful Git removal. DELETE responses include `locationRelease` when attempted. Shared-service explicit removal retains the existing force-delete semantics for that target; it is not automatic idle cleanup and does not interrupt unrelated sessions. An eviction acknowledgement is cache detachment, not a guarantee that active borrowers released OS handles. Queue rollback and topology publication remain tied to the Git result.
57
58
  - `isLinkedWorktree(directory)`: Check if directory is a linked worktree (not primary).
58
59
 
59
60
  ### Commit and Remote Operations
@@ -1,4 +1,4 @@
1
- export function registerGitRoutes(app, { messageQueueService = null, createRequestID = () => globalThis.crypto.randomUUID(), broadcastWorktreeTopologyChanged = () => {} } = {}) {
1
+ export function registerGitRoutes(app, { messageQueueService = null, createRequestID = () => globalThis.crypto.randomUUID(), broadcastWorktreeTopologyChanged = () => {}, releaseWorktreeLocation } = {}) {
2
2
  const normalizeWorktreePath = (value) => typeof value === 'string' ? value.trim().replace(/\\/g, '/').replace(/\/+$/, '') : '';
3
3
  const lifecycleOperations = new Map();
4
4
  const acquireWorktreeLifecycleOperation = (runtimeKey, directory) => {
@@ -1282,11 +1282,14 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
1282
1282
  }
1283
1283
 
1284
1284
  let result;
1285
+ let locationRelease;
1285
1286
  try {
1286
1287
  result = await removeWorktree(directory, {
1287
1288
  directory: worktreeDirectory,
1288
1289
  deleteLocalBranch: req.body?.deleteLocalBranch === true,
1289
- });
1290
+ }, ...(releaseWorktreeLocation ? [{ beforeRemove: async (target) => {
1291
+ locationRelease = await releaseWorktreeLocation(target);
1292
+ } }] : []));
1290
1293
  } catch (error) {
1291
1294
  if (messageQueueService) {
1292
1295
  try {
@@ -1328,7 +1331,7 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
1328
1331
  console.warn('Failed to roll back message queue worktree deletion');
1329
1332
  }
1330
1333
  }
1331
- return res.json({ success: false });
1334
+ return res.json({ success: false, ...(locationRelease ? { locationRelease } : {}) });
1332
1335
  }
1333
1336
 
1334
1337
  notifyWorktreeTopologyChanged(directory, worktreeDirectory, 'removed');
@@ -1346,7 +1349,7 @@ export function registerGitRoutes(app, { messageQueueService = null, createReque
1346
1349
  return res.status(500).json({ error: 'Failed to commit worktree queue deletion' });
1347
1350
  }
1348
1351
  }
1349
- res.json({ success: true });
1352
+ res.json({ success: true, ...(locationRelease ? { locationRelease } : {}) });
1350
1353
  } finally {
1351
1354
  releaseWorktreeLifecycleOperation(operation);
1352
1355
  }
@@ -69,6 +69,40 @@ const createMockResponse = () => {
69
69
  };
70
70
  };
71
71
 
72
+ describe('worktree eviction best effort', () => {
73
+ it.each(['released', 'failed', 'timeout', 'unavailable'])('preserves successful deletion with release outcome %s', async (state) => {
74
+ const { app, getRoute } = createRouteRegistry();
75
+ const releaseWorktreeLocation = vi.fn(async () => ({ state }));
76
+ const broadcastWorktreeTopologyChanged = vi.fn();
77
+ registerGitRoutes(app, { releaseWorktreeLocation, broadcastWorktreeTopologyChanged });
78
+ gitLibraries.removeWorktree.mockImplementation(async (_root, _input, options) => {
79
+ await options.beforeRemove('/validated/worktree');
80
+ return true;
81
+ });
82
+ const response = createMockResponse();
83
+ await getRoute('DELETE', '/api/git/worktrees')({ query: { directory: '/repo' }, body: { directory: '/requested/worktree' } }, response);
84
+ expect(releaseWorktreeLocation).toHaveBeenCalledWith('/validated/worktree');
85
+ expect(response.body).toEqual({ success: true, locationRelease: { state } });
86
+ expect(broadcastWorktreeTopologyChanged).toHaveBeenCalledTimes(1);
87
+ });
88
+ it('does not publish topology when removal fails after successful release', async () => {
89
+ const { app, getRoute } = createRouteRegistry();
90
+ const broadcastWorktreeTopologyChanged = vi.fn();
91
+ registerGitRoutes(app, { releaseWorktreeLocation: async () => ({ state: 'released' }), broadcastWorktreeTopologyChanged });
92
+ gitLibraries.removeWorktree.mockImplementation(async (_root, _input, options) => {
93
+ await options.beforeRemove('/repo/worktree');
94
+ throw new Error('deletion failed');
95
+ });
96
+ const response = createMockResponse();
97
+ const log = vi.spyOn(console, 'error').mockImplementation(() => {});
98
+ try {
99
+ await getRoute('DELETE', '/api/git/worktrees')({ query: { directory: '/repo' }, body: { directory: '/repo/worktree' } }, response);
100
+ } finally { log.mockRestore(); }
101
+ expect(response.statusCode).toBe(500);
102
+ expect(broadcastWorktreeTopologyChanged).not.toHaveBeenCalled();
103
+ });
104
+ });
105
+
72
106
  describe('git routes batch discover', () => {
73
107
  beforeEach(() => {
74
108
  gitLibraries.isGitRepository.mockReset();
@@ -3851,7 +3851,7 @@ export async function getWorktreeBootstrapStatus(directory) {
3851
3851
  };
3852
3852
  }
3853
3853
 
3854
- export async function removeWorktree(directory, input = {}) {
3854
+ export async function removeWorktree(directory, input = {}, { beforeRemove } = {}) {
3855
3855
  const targetDirectory = normalizeDirectoryPath(input?.directory);
3856
3856
  if (!targetDirectory) {
3857
3857
  throw new Error('Worktree directory is required');
@@ -3887,6 +3887,7 @@ export async function removeWorktree(directory, input = {}) {
3887
3887
 
3888
3888
  const targetExists = await checkPathExists(targetDirectory);
3889
3889
  if (targetExists && isManagedOrphan) {
3890
+ await beforeRemove?.(targetDirectory);
3890
3891
  await fsp.rm(targetDirectory, { recursive: true, force: true });
3891
3892
  }
3892
3893
 
@@ -3901,6 +3902,7 @@ export async function removeWorktree(directory, input = {}) {
3901
3902
  return true;
3902
3903
  }
3903
3904
 
3905
+ await beforeRemove?.(matchedEntry.worktree);
3904
3906
  await runGitCommandOrThrow(
3905
3907
  context.primaryWorktree,
3906
3908
  ['worktree', 'remove', '--force', matchedEntry.worktree],
@@ -39,8 +39,8 @@ call Anthropic/OpenAI/plugin SDKs, and does not use the `openai` npm package.
39
39
  `openchamber-text` agent (same temp root, same deny-all verification and
40
40
  `session.create` / `session.remove` lifecycle as the attachment path below):
41
41
 
42
- 1. Ensure `.opencode/agent/openchamber-text.md` and verify its final
43
- permission rule is deny-all before creating the session.
42
+ 1. Ensure `.opencode/agent/openchamber-text.md` and verify its last catch-all
43
+ permission rule is deny-all, with only deny rules after it, before creating the session.
44
44
  2. `session.create({ location, agent: 'openchamber-text', model, metadata })`.
45
45
  3. `session.generate({ sessionID, prompt })` → `{ text }`. One model call with
46
46
  the session's provider context, no tool loop, no messages written. System
@@ -62,8 +62,10 @@ data-URL file parts:
62
62
 
63
63
  1. Ensure the throwaway temp directory agent markdown (`openchamber-llm`,
64
64
  permissions array ending in `action:* resource:* effect:deny`).
65
- 2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **final**
66
- permissions rule to be deny-all. Failure → `llm_attachment_generation_unavailable`
65
+ 2. `agent.get({ agentID: 'openchamber-llm', location })` and require the **last catch-all**
66
+ permissions rule to be deny-all, with only deny rules after it. This accepts
67
+ OpenCode's appended `browser` deny while rejecting trailing allow/ask rules.
68
+ Failure → `llm_attachment_generation_unavailable`
67
69
  **before** any `session.prompt`. Title is never treated as permission isolation.
68
70
  3. `session.create({ location, agent, model: { id, providerID, variant? },
69
71
  metadata: { openchamber: { llm: { purpose: 'chat-completions' } } } })`
@@ -18,7 +18,7 @@ const positiveMs = (value, fallback) => (
18
18
 
19
19
  /**
20
20
  * Agent frontmatter for official client agent permissions (action/resource/effect).
21
- * The final rule must be deny-all; assertLlmAgentDenyAll verifies that before prompt.
21
+ * The final catch-all must deny, with only denies after it; verified before prompt.
22
22
  */
23
23
  const AGENT_MARKDOWN = `---
24
24
  mode: primary
@@ -308,7 +308,7 @@ const readGeneratorAgent = async ({ client, location, signal, agentID, waitMs, p
308
308
  };
309
309
 
310
310
  /**
311
- * Verify the generator agent ends with deny-all permissions.
311
+ * Verify the generator agent has a deny-all rule followed only by denies.
312
312
  * Title alone is never treated as isolation. Failure must happen before prompt.
313
313
  */
314
314
  export async function assertLlmAgentDenyAll({
@@ -336,13 +336,12 @@ export async function assertLlmAgentDenyAll({
336
336
  'llm_attachment_generation_unavailable',
337
337
  );
338
338
  }
339
- const last = permissions[permissions.length - 1];
340
- if (
341
- !last
342
- || last.action !== '*'
343
- || last.resource !== '*'
344
- || last.effect !== 'deny'
345
- ) {
339
+ const lastCatchAll = permissions.findLastIndex(
340
+ (rule) => rule?.action === '*' && rule?.resource === '*',
341
+ );
342
+ const isolated = lastCatchAll > -1
343
+ && permissions.slice(lastCatchAll).every((rule) => rule?.effect === 'deny');
344
+ if (!isolated) {
346
345
  failGenerate(
347
346
  'OpenCode LLM agent final permission is not deny-all; generation blocked',
348
347
  'llm_attachment_generation_unavailable',
@@ -127,8 +127,16 @@ function textSessionClient({ generate = vi.fn(async () => ({ text: 'reply' })),
127
127
  }
128
128
 
129
129
  describe('generateOpenCodeText — text path', () => {
130
- it('runs session.generate in a verified deny-all throwaway session and removes it', async () => {
131
- const { client, agentGet, create, generate, remove, prompt } = textSessionClient()
130
+ it.each([
131
+ { name: 'deny-all alone', suffix: [] },
132
+ { name: 'appended browser deny', suffix: [{ action: 'browser', resource: '*', effect: 'deny' }] },
133
+ ])('runs session.generate in a verified throwaway session ($name) and removes it', async ({ suffix }) => {
134
+ const { client, agentGet, create, generate, remove, prompt } = textSessionClient({
135
+ agentGet: vi.fn(async () => ({
136
+ ...denyAllTextAgent,
137
+ data: { ...denyAllTextAgent.data, permissions: [...denyAllTextAgent.data.permissions, ...suffix] },
138
+ })),
139
+ })
132
140
  const ensureTempDirectory = vi.fn(async ({ agentName, agentMarkdown }) => {
133
141
  expect(agentName).toBe(_test.TEXT_AGENT_NAME)
134
142
  expect(agentMarkdown).toBe(_test.TEXT_AGENT_MARKDOWN)
@@ -188,10 +196,23 @@ describe('generateOpenCodeText — text path', () => {
188
196
  }
189
197
  })
190
198
 
191
- it('blocks before creating a session when the text agent is not deny-all', async () => {
199
+ it.each([
200
+ { name: 'no catch-all', permissions: [{ action: 'browser', resource: '*', effect: 'deny' }] },
201
+ ...['allow', 'ask'].flatMap((effect) => ['*', 'edit'].map((action) => ({
202
+ name: `trailing ${action} ${effect} before browser deny`,
203
+ permissions: [
204
+ ...denyAllTextAgent.data.permissions,
205
+ { action, resource: '*', effect },
206
+ { action: 'browser', resource: '*', effect: 'deny' },
207
+ ],
208
+ }))),
209
+ { name: 'missing permissions', permissions: undefined },
210
+ { name: 'empty permissions', permissions: [] },
211
+ { name: 'invalid trailing rule', permissions: [...denyAllTextAgent.data.permissions, null] },
212
+ ])('blocks before creating a session: $name', async ({ permissions }) => {
192
213
  const { client, create, generate } = textSessionClient({
193
214
  agentGet: vi.fn(async () => ({
194
- data: { id: 'openchamber-text', permissions: [{ action: 'bash', resource: '*', effect: 'allow' }] },
215
+ data: { id: 'openchamber-text', permissions },
195
216
  })),
196
217
  })
197
218
  await expect(generateOpenCodeText({
@@ -316,8 +337,14 @@ describe('generateOpenCodeText — text path', () => {
316
337
  })
317
338
 
318
339
  describe('generateOpenCodeText — attachment session path', () => {
319
- it('verifies deny-all before prompt, prompts with files, waits, lists, and removes', async () => {
320
- const agentGet = vi.fn(async () => denyAllAgent)
340
+ it.each([
341
+ { name: 'deny-all alone', suffix: [] },
342
+ { name: 'appended browser deny', suffix: [{ action: 'browser', resource: '*', effect: 'deny' }] },
343
+ ])('verifies deny-all ($name) before prompt, prompts with files, waits, lists, and removes', async ({ suffix }) => {
344
+ const agentGet = vi.fn(async () => ({
345
+ ...denyAllAgent,
346
+ data: { ...denyAllAgent.data, permissions: [...denyAllAgent.data.permissions, ...suffix] },
347
+ }))
321
348
  const create = vi.fn(async () => ({ id: 'ses_tmp' }))
322
349
  const put = vi.fn(async () => undefined)
323
350
  const prompt = vi.fn(async () => ({ id: 'inbox_1', type: 'user' }))
@@ -246,7 +246,7 @@ export const createMessageQueueService = ({ dbPath, getRuntimeConfig = () => nul
246
246
  totalPasteLength += reference.text.length;
247
247
  if (totalPasteLength > MAX_COMPOSER_PASTE_TOTAL_LENGTH) fail('validation_error');
248
248
  canonical += reference.text;
249
- } else if (reference.kind === 'skill' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'skillName'].includes(key)) && nonEmptyString(reference.skillName, MAX_COMPOSER_SKILL_NAME) && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(reference.skillName) && (reference.display === `/${reference.skillName}` || reference.display === `/\u2003${reference.skillName}`)) canonical += `[skill:${reference.skillName}]`;
249
+ } else if (reference.kind === 'skill' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'skillName'].includes(key)) && nonEmptyString(reference.skillName, MAX_COMPOSER_SKILL_NAME) && /^[^\s/\\[\]\r\n]+$/u.test(reference.skillName) && [`/${reference.skillName}`, `/\u2003${reference.skillName}`, `@${reference.skillName}`, `@\u2003${reference.skillName}`].includes(reference.display)) canonical += `[skill:${reference.skillName}]`;
250
250
  else if (reference.kind === 'command' && Object.keys(reference).every((key) => ['id', 'kind', 'start', 'end', 'display', 'commandName', 'reference'].includes(key)) && nonEmptyString(reference.commandName, MAX_COMPOSER_COMMAND_NAME) && /^[\p{L}\p{N}._/-]+$/u.test(reference.commandName) && nonEmptyString(reference.reference, MAX_COMPOSER_COMMAND_REFERENCE) && !/[\]\r\n]/u.test(reference.reference) && (reference.display === `/${reference.commandName}` || reference.display === `/\u2003${reference.commandName}`)) canonical += `[command:${reference.reference}]`;
251
251
  else fail('validation_error');
252
252
  previousEnd = reference.end;