@openchambery/web 2.1.7 → 2.1.8-beta.10

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 (168) hide show
  1. package/dist/assets/{AgentAvatar-CAR4F5Sn.js → AgentAvatar-mchFM3n-.js} +1 -1
  2. package/dist/assets/AssistantView-pLVk20s1.js +3 -0
  3. package/dist/assets/{DiagramView-Dd_3w4Hk.js → DiagramView-DmaKTHol.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-CCVddfP5.js → FadeInOnReveal-BXDM9joC.js} +1 -1
  5. package/dist/assets/{Icon-DDB7S6Vz.js → Icon-BGp_ize2.js} +1 -1
  6. package/dist/assets/{ImageLoadingPlaceholder-C6fDEcU1.js → ImageLoadingPlaceholder-DmUxjyxq.js} +1 -1
  7. package/dist/assets/{LoadingImage-ByBH-5WW.js → LoadingImage-DGjlKW_O.js} +1 -1
  8. package/dist/assets/MarkdownRenderer-VYYHEQHc.js +2 -0
  9. package/dist/assets/{MarkdownRendererImpl-BXgUthom.js → MarkdownRendererImpl-BQPOlcOs.js} +2 -2
  10. package/dist/assets/{MarkstreamRendererImpl-5W9P9U-w.js → MarkstreamRendererImpl-B9DtJ2K2.js} +1 -1
  11. package/dist/assets/{MobileDetailNavigation-Bk6C9i9l.js → MobileDetailNavigation-9MnryETM.js} +1 -1
  12. package/dist/assets/{MobileShareBridge-B5nF-gT6.js → MobileShareBridge-e3WTrRks.js} +1 -1
  13. package/dist/assets/MobileTabPageHeader-Fh7QCZc7.js +2 -0
  14. package/dist/assets/{MultiRunWindow-CpuFaAyG.js → MultiRunWindow-sfoabcvx.js} +1 -1
  15. package/dist/assets/{OnboardingScreen-DINUboLT.js → OnboardingScreen-DmJgU0Jc.js} +1 -1
  16. package/dist/assets/{OverlayScrollbar-BkxmoyYl.js → OverlayScrollbar-BkJCkZp3.js} +1 -1
  17. package/dist/assets/{SettingsView--C3eizlJ.js → SettingsView-DbNglFY1.js} +10 -9
  18. package/dist/assets/{SettingsWindow-DYUvg92F.js → SettingsWindow-BGzCAKgh.js} +1 -1
  19. package/dist/assets/{TerminalView-BqAariXf.js → TerminalView-oT_BTx_W.js} +1 -1
  20. package/dist/assets/{ToolOutputDialog-D6lPUZrN.js → ToolOutputDialog-D85_cJ2B.js} +1 -1
  21. package/dist/assets/{appThemeRegistry-wWazoyp8.js → appThemeRegistry-CGjHffdt.js} +1 -1
  22. package/dist/assets/{button-DymteyMv.js → button-BKjGCkH3.js} +1 -1
  23. package/dist/assets/{collapsible-Dzp5rO0Q.js → collapsible-03FGLsF4.js} +1 -1
  24. package/dist/assets/{desktopHosts-DSBZ-9-L.js → desktopHosts-BZ7iYE-l.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-DK8TzHRx.js → desktopRelayRestore-S9wEiY2U.js} +1 -1
  26. package/dist/assets/{diagram-CQs9px6y.js → diagram-D4QHcaSQ.js} +1 -1
  27. package/dist/assets/{dialog-DfUeXqdE.js → dialog-nm_cQEUZ.js} +1 -1
  28. package/dist/assets/{dropdown-menu.styles-B6YMNOZz.js → dropdown-menu.styles-WkR8IcF2.js} +1 -1
  29. package/dist/assets/es-C9PRwAQm.js +15 -0
  30. package/dist/assets/{fileMentionTouchSelection-CEIhM6Ov.js → fileMentionTouchSelection-DujPmFPX.js} +1 -1
  31. package/dist/assets/{fileQueries-BGE5l3Jd.js → fileQueries-DZR-mK8h.js} +1 -1
  32. package/dist/assets/fr-2tfGTcme.js +13 -0
  33. package/dist/assets/{gitApi-BP68uLBQ.js → gitApi-CrqGdlbS.js} +1 -1
  34. package/dist/assets/{image-loading-qNtUCy69.js → image-loading-CsxDGgek.js} +1 -1
  35. package/dist/assets/{input-BvpZiRHI.js → input-Buww8enN.js} +1 -1
  36. package/dist/assets/insertionBoundaries-o5RDEXaZ.js +1 -0
  37. package/dist/assets/ja-D7NHmhJr.js +15 -0
  38. package/dist/assets/ko-DeGmXmv6.js +15 -0
  39. package/dist/assets/main-6vgvFImW.js +169 -0
  40. package/dist/assets/{main-DSY3-ZQZ.js → main-Zix8q1Qb.js} +2 -2
  41. package/dist/assets/{markdownHeightCache-MW1NYOaG.js → markdownHeightCache-B6wZ-UwD.js} +1 -1
  42. package/dist/assets/{mcpQueries-BaQ2_bdm.js → mcpQueries-BVPwR92u.js} +1 -1
  43. package/dist/assets/miniChat-BYZJ4yQd.js +2 -0
  44. package/dist/assets/mobile-CvQVwDyf.js +2 -0
  45. package/dist/assets/{multirun-Blywdd6A.js → multirun-BoFWk8S5.js} +1 -1
  46. package/dist/assets/{persistence-B7ORJ371.js → persistence-B4Ni3Gmv.js} +1 -1
  47. package/dist/assets/{pl-BE8MC0XQ.js → pl-CyWLY1AT.js} +3 -3
  48. package/dist/assets/{platform-CiBU2bCp.js → platform-BQPsr8Zl.js} +1 -1
  49. package/dist/assets/{projectMeta-BwPtPeMh.js → projectMeta-B073H1YC.js} +1 -1
  50. package/dist/assets/pt-BR-D9VVxvts.js +15 -0
  51. package/dist/assets/renderElectronMiniChatApp-Bu1k2_XW.js +2 -0
  52. package/dist/assets/renderMobileApp-wEjrkNAR.js +28 -0
  53. package/dist/assets/{runtimeConfig-CqpbN2lM.js → runtimeConfig-DKN9Jyz-.js} +1 -1
  54. package/dist/assets/{runtimeEndpointReset-bWBhgTs_.js → runtimeEndpointReset-Cqf421q-.js} +15 -15
  55. package/dist/assets/{scheduledTaskNavigation-BnKGGUlX.js → scheduledTaskNavigation-C-HGgCQA.js} +1 -1
  56. package/dist/assets/sessionChangeSummary-COHW-iNe.js +1 -0
  57. package/dist/assets/{sessionLookup-6gcNEZnB.js → sessionLookup-ByvikQSd.js} +1 -1
  58. package/dist/assets/{sortable-tabs-strip-CWR8KhJB.js → sortable-tabs-strip-cHZM0f0Z.js} +1 -1
  59. package/dist/assets/speechSettingsQueries-BJ7hAvj0.js +4 -0
  60. package/dist/assets/{src-C9BNQK0O.css → src-OjoeiMbd.css} +1 -1
  61. package/dist/assets/store-Ds-7Em7C.js +15 -0
  62. package/dist/assets/{terminalApi-bN5IYQau.js → terminalApi-kRMttlcP.js} +1 -1
  63. package/dist/assets/{textarea-ClPlugKE.js → textarea-BZHD_GND.js} +1 -1
  64. package/dist/assets/{tooltip-D-1Pgmq1.js → tooltip-DCeWvAqN.js} +1 -1
  65. package/dist/assets/uk-Df44svOl.js +15 -0
  66. package/dist/assets/{url-nbXEti6c.js → url-Bd61gNgl.js} +1 -1
  67. package/dist/assets/{useAppFontEffects-8UJ5f0dG.js → useAppFontEffects-BqrJlSC2.js} +51 -53
  68. package/dist/assets/{useDesktopWindowControlsLayout-B_hNd3pD.js → useDesktopWindowControlsLayout-DgB-LNHC.js} +1 -1
  69. package/dist/assets/{useEffectiveDirectory-DISvoVLe.js → useEffectiveDirectory-axifbbuL.js} +1 -1
  70. package/dist/assets/{useFontPreferences-6LD7d7hP.js → useFontPreferences-lAw0Cbza.js} +1 -1
  71. package/dist/assets/useMobileNavigationStore-DnmY3KH3.js +417 -0
  72. package/dist/assets/{useWorkerHighlightedLines-BFygybD8.js → useWorkerHighlightedLines-D2YpPANs.js} +1 -1
  73. package/dist/assets/vendor-codemirror-autocomplete-7c2L_4dM.js +12 -0
  74. package/dist/assets/vendor-codemirror-commands-B8KrBQ9B.js +1 -0
  75. package/dist/assets/vendor-codemirror-lang-angular-DXOgWEVj.js +23 -0
  76. package/dist/assets/{vendor-codemirror-lang-cpp-C3UKgJaQ.js → vendor-codemirror-lang-cpp-Dh8C3xqP.js} +1 -1
  77. package/dist/assets/{vendor-codemirror-lang-go-Cv5NYdGm.js → vendor-codemirror-lang-go-BXAJ1_dh.js} +5 -5
  78. package/dist/assets/{vendor-codemirror-lang-java-D6hBTIgD.js → vendor-codemirror-lang-java-DAyCHRhk.js} +1 -1
  79. package/dist/assets/{vendor-codemirror-lang-jinja-ehGXhAov.js → vendor-codemirror-lang-jinja-0TICrMfO.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-lang-json-tHviF6YN.js → vendor-codemirror-lang-json-mLulNbwB.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-lang-less-B-yDfSDo.js → vendor-codemirror-lang-less-CNvptX3P.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-liquid-BdoLH4KN.js → vendor-codemirror-lang-liquid-8g5seytq.js} +1 -1
  83. package/dist/assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js +6 -0
  84. package/dist/assets/{vendor-codemirror-lang-php-Ch8GW4Xz.js → vendor-codemirror-lang-php-xP7rf28_.js} +1 -1
  85. package/dist/assets/vendor-codemirror-lang-python-YM2WVLHF.js +9 -0
  86. package/dist/assets/{vendor-codemirror-lang-rust-BEUlElU6.js → vendor-codemirror-lang-rust-CtafR0gt.js} +1 -1
  87. package/dist/assets/vendor-codemirror-lang-sass-B4iav_Ps.js +1 -0
  88. package/dist/assets/{vendor-codemirror-lang-sql-BnZql454.js → vendor-codemirror-lang-sql-C6q09UUZ.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-vue-BCwQBvLR.js → vendor-codemirror-lang-vue-DIB78UsR.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-wast-mrtA2eML.js → vendor-codemirror-lang-wast-C_JVdOCF.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-xml-ClAh-G2D.js → vendor-codemirror-lang-xml-DIuzY_d4.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-yaml-CjL8XEf2.js → vendor-codemirror-lang-yaml-BO0hIfQM.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-language-data-DGDYune4.js → vendor-codemirror-language-data-Ctd4WxgW.js} +2 -2
  94. package/dist/assets/vendor-codemirror-search-ZqtrPLjU.js +2 -0
  95. package/dist/assets/{vendor-replit-codemirror-vim-CEibx3EP.js → vendor-replit-codemirror-vim-DDj0dSVY.js} +6 -6
  96. package/dist/assets/zh-CN-wowfzo60.js +15 -0
  97. package/dist/assets/zh-TW-DwojcgA_.js +15 -0
  98. package/dist/index.html +8 -8
  99. package/dist/mini-chat.html +8 -8
  100. package/dist/mobile.html +8 -8
  101. package/package.json +1 -1
  102. package/server/lib/assistants/DOCUMENTATION.md +1 -1
  103. package/server/lib/assistants/harness.js +1 -1
  104. package/server/lib/assistants/harness.test.js +1 -1
  105. package/server/lib/config-sync/DOCUMENTATION.md +38 -11
  106. package/server/lib/config-sync/config-sync.test.js +11 -11
  107. package/server/lib/config-sync/constants.js +0 -3
  108. package/server/lib/config-sync/contract.js +8 -8
  109. package/server/lib/config-sync/credentials.js +101 -0
  110. package/server/lib/config-sync/credentials.test.js +98 -0
  111. package/server/lib/config-sync/engine.js +17 -11
  112. package/server/lib/config-sync/index.js +0 -3
  113. package/server/lib/config-sync/local-backup.js +0 -13
  114. package/server/lib/config-sync/magpie-auth.js +111 -0
  115. package/server/lib/config-sync/magpie-auth.test.js +79 -0
  116. package/server/lib/config-sync/magpie.js +276 -0
  117. package/server/lib/config-sync/magpie.test.js +265 -0
  118. package/server/lib/config-sync/plan.js +32 -37
  119. package/server/lib/config-sync/receiver.js +28 -25
  120. package/server/lib/config-sync/routes.js +23 -2
  121. package/server/lib/config-sync/routes.test.js +64 -5
  122. package/server/lib/config-sync/scripts.js +2 -30
  123. package/server/lib/config-sync/selections.js +14 -10
  124. package/server/lib/opencode/DOCUMENTATION.md +4 -1
  125. package/server/lib/opencode/auth.d.ts +1 -0
  126. package/server/lib/opencode/auth.js +1 -1
  127. package/server/lib/opencode/config-entities-layering.test.js +2 -1
  128. package/server/lib/opencode/feature-routes-runtime.js +1 -0
  129. package/server/lib/opencode/lifecycle.js +4 -0
  130. package/server/lib/opencode/lifecycle.test.js +7 -2
  131. package/server/lib/opencode/plugin-routes.test.js +1 -1
  132. package/server/lib/opencode/plugins.js +31 -21
  133. package/server/lib/opencode/plugins.test.js +55 -10
  134. package/server/lib/session-folders/DOCUMENTATION.md +20 -0
  135. package/server/lib/session-folders/order-routes.js +69 -0
  136. package/server/lib/session-folders/routes.js +3 -0
  137. package/server/lib/session-folders/routes.test.js +49 -0
  138. package/server/lib/session-index/DOCUMENTATION.md +3 -0
  139. package/server/lib/session-index/sync-runtime.js +9 -4
  140. package/server/lib/session-index/sync-runtime.test.js +27 -0
  141. package/dist/assets/AssistantView-DgjRT4M4.js +0 -3
  142. package/dist/assets/MarkdownRenderer-YbG78e0Y.js +0 -2
  143. package/dist/assets/MobileTabPageHeader-D7r6pKco.js +0 -2
  144. package/dist/assets/es-2YvsazK2.js +0 -15
  145. package/dist/assets/fr-Cy0M48Gw.js +0 -13
  146. package/dist/assets/insertionBoundaries-BywXllQ9.js +0 -1
  147. package/dist/assets/ja-1zLWWJ2R.js +0 -15
  148. package/dist/assets/ko-BLk_5Dhc.js +0 -15
  149. package/dist/assets/main-DdyFxokk.js +0 -169
  150. package/dist/assets/miniChat-B43ASec3.js +0 -2
  151. package/dist/assets/mobile-DloCio4f.js +0 -2
  152. package/dist/assets/pt-BR-ubhNymyH.js +0 -15
  153. package/dist/assets/renderElectronMiniChatApp-DCRctqrx.js +0 -2
  154. package/dist/assets/renderMobileApp-DuFdSBXi.js +0 -28
  155. package/dist/assets/sessionChangeSummary-BlR9MyHu.js +0 -2
  156. package/dist/assets/speechSettingsQueries-Bp5LgcR7.js +0 -4
  157. package/dist/assets/store-n-H5RvRn.js +0 -15
  158. package/dist/assets/uk-BNyyigDo.js +0 -15
  159. package/dist/assets/useMobileNavigationStore-BE_8Q8WJ.js +0 -417
  160. package/dist/assets/vendor-codemirror-autocomplete-DHomsvW7.js +0 -12
  161. package/dist/assets/vendor-codemirror-commands-C9DCaJ0C.js +0 -1
  162. package/dist/assets/vendor-codemirror-lang-angular-BDXnzWgq.js +0 -23
  163. package/dist/assets/vendor-codemirror-lang-markdown-Bt-vLz9k.js +0 -6
  164. package/dist/assets/vendor-codemirror-lang-python-B4naLXMo.js +0 -9
  165. package/dist/assets/vendor-codemirror-lang-sass-ibtp2iUB.js +0 -1
  166. package/dist/assets/vendor-codemirror-search-D2SOgzph.js +0 -2
  167. package/dist/assets/zh-CN-CNWzMPgL.js +0 -15
  168. package/dist/assets/zh-TW-DrZ9S1yn.js +0 -15
package/dist/index.html CHANGED
@@ -552,7 +552,7 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-DSY3-ZQZ.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-Zix8q1Qb.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -563,17 +563,17 @@
563
563
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BP2cRVqX.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-B7ORJ371.js">
567
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-DSBZ-9-L.js">
568
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DK8TzHRx.js">
566
+ <link rel="modulepreload" crossorigin href="/assets/persistence-B4Ni3Gmv.js">
567
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-BZ7iYE-l.js">
568
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-S9wEiY2U.js">
569
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-xol_6BsB.js">
571
- <link rel="modulepreload" crossorigin href="/assets/platform-CiBU2bCp.js">
572
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
571
+ <link rel="modulepreload" crossorigin href="/assets/platform-BQPsr8Zl.js">
572
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-kRMttlcP.js">
573
573
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
574
574
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
575
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-CqpbN2lM.js">
576
- <link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DKN9Jyz-.js">
576
+ <link rel="stylesheet" crossorigin href="/assets/src-OjoeiMbd.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,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-B43ASec3.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-BYZJ4yQd.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -85,17 +85,17 @@
85
85
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BP2cRVqX.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-B7ORJ371.js">
89
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-DSBZ-9-L.js">
90
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DK8TzHRx.js">
88
+ <link rel="modulepreload" crossorigin href="/assets/persistence-B4Ni3Gmv.js">
89
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-BZ7iYE-l.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-S9wEiY2U.js">
91
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-xol_6BsB.js">
93
- <link rel="modulepreload" crossorigin href="/assets/platform-CiBU2bCp.js">
94
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
93
+ <link rel="modulepreload" crossorigin href="/assets/platform-BQPsr8Zl.js">
94
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-kRMttlcP.js">
95
95
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
96
96
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
97
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-CqpbN2lM.js">
98
- <link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DKN9Jyz-.js">
98
+ <link rel="stylesheet" crossorigin href="/assets/src-OjoeiMbd.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,7 +63,7 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-DloCio4f.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-CvQVwDyf.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -74,17 +74,17 @@
74
74
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BP2cRVqX.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-B7ORJ371.js">
78
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-DSBZ-9-L.js">
79
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DK8TzHRx.js">
77
+ <link rel="modulepreload" crossorigin href="/assets/persistence-B4Ni3Gmv.js">
78
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-BZ7iYE-l.js">
79
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-S9wEiY2U.js">
80
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-xol_6BsB.js">
82
- <link rel="modulepreload" crossorigin href="/assets/platform-CiBU2bCp.js">
83
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
82
+ <link rel="modulepreload" crossorigin href="/assets/platform-BQPsr8Zl.js">
83
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-kRMttlcP.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
86
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-CqpbN2lM.js">
87
- <link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DKN9Jyz-.js">
87
+ <link rel="stylesheet" crossorigin href="/assets/src-OjoeiMbd.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.1.7",
3
+ "version": "2.1.8-beta.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -146,7 +146,7 @@ and keep their existing error result handling.
146
146
  - Protocol correction never replays rejected assistant prose into model history: simulated User/Assistant turns and tool-result records are rejected as fabricated evidence. Known rejected coding calls cannot be skipped with a subsequent `complete` declaration; the model must issue actual calls or report a blocker. Corrections remain bounded and exhaustion is an error, not a completion.
147
147
 
148
148
  - A workspace-backed contact keeps running until it emits coding calls or an explicit `openchamber-final` JSON fence with `{ "status": "complete" | "blocked", "text": "..." }`. Final declarations contain the user-facing answer only; missing input/questions use `blocked`. Ordinary conversation can finish without tools using the same declaration. Reading a skill loads instructions and does not itself complete the user's task. Contacts without coding tools retain their plain-text reply contract.
149
- - Plain progress text is not completion. The stream adapter requests at most two protocol corrections for a rejected response, retaining actual prior calls/results. Rejected ordinary prose and tool calls stay private/unexecuted; independently validated public prefix messages already delivered remain visible. Exhaustion becomes an upstream error. This guard enforces the response protocol; a model's explicit completion claim is not independent verification of arbitrary task semantics.
149
+ - Plain progress text is not completion. The stream adapter requests at most two protocol corrections for a rejected response, retaining actual prior calls/results. Rejected ordinary prose and tool calls stay private/unexecuted; independently validated public prefix messages already delivered remain visible. Exhaustion becomes an upstream error containing the last validation reason, without including rejected model output or tool arguments. This guard enforces the response protocol; a model's explicit completion claim is not independent verification of arbitrary task semantics.
150
150
  - `parseContactToolCalls` returns an ordered `toolCalls` array. It accepts application fences, complete generic JSON fences, and bare/embedded call objects. Nested argument objects and quoted markdown fences remain data. An invalid/unavailable/unclosed explicit application call rejects the entire response before any prefix can execute.
151
151
  - Multiple coding calls run sequentially through pi (`toolExecution: 'sequential'`), preserving dependencies such as write then read. OpenChamber operation tools must be sent one at a time: mixed/multiple-operation batches are rejected before execution so terminal assign/reset/watch semantics and side-effect gates remain intact.
152
152
  - Completion history retains every assistant tool call (name, arguments, generated call ID) and its real tool result, including empty content and `isError`. Results still use gateway-supported user messages labeled `OpenChamber tool result name=... call=...` with a JSON body. Model-written result lookalikes are never executed or admitted as actual tool results. Final JSON text is opaque: quoted tool examples in a final answer cannot execute.
@@ -495,7 +495,7 @@ export function createContactStreamFn(createChatCompletion, {
495
495
  || (malformedFinal ? 'The final declaration must be a single valid openchamber-final JSON fence with status and text, without other content.' : '')
496
496
  || (requiresFinal && parsed.toolCalls.length === 0 && finalText === null ? 'A workspace response needs an actual tool call or an explicit final declaration.' : '');
497
497
  if (!protocolError) break;
498
- if (attempt >= 2) throw new Error('Assistant response protocol failed after two corrections; the task was not confirmed complete.');
498
+ if (attempt >= 2) throw new Error(`Assistant response protocol failed after two corrections; the task was not confirmed complete. Reason: ${protocolError}`);
499
499
  const rejected = parsed.toolCalls.filter((call) => isPiCodingToolName(call.name));
500
500
  if (rejected.length > 0) rejectedCodingCalls = rejected.map(({ name, arguments: args }) => ({ name, arguments: args }));
501
501
  if (publicBubbles.length > publicStart) {
@@ -613,7 +613,7 @@ describe('runContactTurn', () => {
613
613
  await expect(runContactTurn({
614
614
  assistant: { providerID: 'p', modelID: 'm', effectiveWorkspacePath: workspace },
615
615
  history: [], userText: '请写入笔记。', skillHomeDir: path.join(workspace, 'home'), createChatCompletion, onBubbleDelta,
616
- })).rejects.toThrow(/protocol/i)
616
+ })).rejects.toThrow('Assistant response protocol failed after two corrections; the task was not confirmed complete. Reason: A workspace response needs an actual tool call or an explicit final declaration.')
617
617
  expect(createChatCompletion).toHaveBeenCalledTimes(3)
618
618
  expect(onBubbleDelta).not.toHaveBeenCalled()
619
619
  } finally {
@@ -17,17 +17,20 @@ Own the direction-agnostic OpenCode config sync contract: allowlist planning on
17
17
  | `local-backup.js` | Local (pull destination) generational backup + tar extract |
18
18
  | `tar.js` | `collectLocalTarBuffer` (buffered) |
19
19
  | `engine.js` | `applyConfigSyncPlan` orchestration for **push** (prepare → putTar* → finalize) |
20
+ | `credentials.js` | V2 credential API transfer, validation, account merge and in-memory rollback |
21
+ | `magpie.js` | Host-local Magpie discovery, verified CLI provisioning, native backup/restore |
22
+ | `magpie-auth.js` | Bounded portable authorization export, plugin-store merge and account verification |
20
23
  | `target-id.js` | `ssh:<instanceId>` namespace helper |
21
24
 
22
25
  ## Contract shape
23
26
 
24
- - **SyncTarget**: `{ id, kind, capabilities: { posixShell, tarExtract, authFileWrite } }`
27
+ - **SyncTarget**: `{ id, kind, capabilities: { posixShell, tarExtract, providerCredentialsWrite } }`
25
28
  - **TargetExecutor**: `{ probe(plan), prepare(plan, { syncRunId }), putTar({ kind, payload }), finalize(plan, { syncRunId }) }`
26
29
  - `payload`: `Buffer | Uint8Array` — all executors currently buffer; reintroduce streaming only when a transport needs it.
27
- - **Plan** (source-computed): `{ direction: 'push' | 'pull', syncRunId?, sourceTargetId?, targetId?, files, directories, agentsRoot, authFile, deletes, totalBytes, selections? }`
28
- - **Selections**: `{ fileGroups: boolean[], singleFiles: boolean[], directories: boolean[], agentsRoot: boolean, authFile: boolean }`
30
+ - **Plan** (source-computed): `{ direction: 'push' | 'pull', syncRunId?, sourceTargetId?, targetId?, files, directories, agentsRoot, providerCredentials, deletes, totalBytes, selections? }`
31
+ - **Selections**: `{ fileGroups: boolean[], singleFiles: boolean[], directories: boolean[], agentsRoot: boolean, providerCredentials: boolean, magpie?: boolean }`
29
32
  - Preview and apply must share one selections snapshot so the confirmed scope cannot drift.
30
- - `authFile` defaults unchecked like an opt-in ordinary selection; once checked it syncs the same way as other allowlisted files (no extra grant).
33
+ - `providerCredentials` is the selection/metadata key; the UI calls it **Provider keys**. It defaults unchecked and contains only `{ bytes }`, never credential values or a filesystem path. The protocol has no legacy credential-file fields, aliases, archive kinds, probe markers or backup roots.
31
34
 
32
35
  ## Direction flow
33
36
 
@@ -45,11 +48,12 @@ Switching direction in the wizard **must** discard the previous preview and re-c
45
48
  at every depth, including followed symlinks, matching the inventory walk. Keep
46
49
  these exclusions in `OPENCODE_CONFIG_SYNC_TAR_EXCLUDES`; bypassing the shared
47
50
  archive builder can silently transfer dependency trees omitted by the preview.
48
- - Managed SSH readiness is gated by `posixShell` (and `tarExtract` / `authFileWrite` as needed).
51
+ - Managed SSH readiness is gated by `posixShell` (and `tarExtract` / `providerCredentialsWrite` as needed).
49
52
  - Prepare (remote or local) writes generational backups under `<backupRoot>/<syncRunId>/` and prunes older than `OPENCODE_CONFIG_SYNC_BACKUP_GENERATIONS`. Failed runs keep their scene until pruned.
50
53
  - Finalize confirms the run's backup directory exists.
51
54
  - Run records (Electron `sync-run-store`) persist `direction` alongside `syncRunId` / summary; they are not stored in `settings.json`.
52
- - **`auth.json` sync:** treated like any other selected allowlist item. Default selection leaves it unchecked; when selected, push/pull transfer it under the dedicated auth backup directory with no separate credential grant.
55
+ - **Provider keys:** read/write the active OpenCode service through the official V2 credential SDK. API keys and OAuth accounts retain IDs, labels, metadata and active selection. Matching IDs are replaced; other destination accounts remain. No `auth.json`, database copying, environment-key discovery, or credential backups on disk. Failed imports attempt in-memory rollback and report incomplete rollback explicitly. Already-applied config/skill files remain when credentials fail; retry is idempotent for credentials. Process termination cannot guarantee rollback.
56
+ - Credential API failures reject rather than becoming empty inventory. Empty successful inventory is `null`. Preview/run records contain metadata only. Transfer envelopes are versioned `opencode-credentials-v1`; only the current protocol is supported.
53
57
 
54
58
  ## HTTP receive protocol (direct / later relay)
55
59
 
@@ -59,15 +63,38 @@ Routes under `/api/openchamber/config-sync/*` (global `/api` auth gate: session
59
63
  |--------|------|------|
60
64
  | POST | `/probe` | Inventory + existence vs plan |
61
65
  | POST | `/prepare` | Generational local backup + deletes; one inflight `syncRunId` |
62
- | PUT | `/put/:kind?syncRunId=` | Stream tar.gz (`config` \| `agents` \| `auth`) |
63
- | GET | `/download/:kind` | Stream tar.gz for pull source |
66
+ | PUT | `/put/:kind?syncRunId=` | tar.gz for `config` / `agents`; versioned credential JSON bytes for selected `credentials` |
67
+ | GET | `/download/:kind` | Matching binary payload; `credentials` is never cached |
68
+ | GET | `/credentials/inventory` | Current-service credential byte count, without secrets |
69
+ | PUT | `/credentials` | Bounded credential import for local pull and SSH-forwarded transfer |
70
+ | GET | `/magpie/inventory` | `{ magpie: { available, installed } }`, metadata only; never installs |
71
+ | PUT | `/magpie` | Magpie config/authorization restore and verification for local pull and SSH-forwarded transfer; installs missing CLI |
64
72
  | POST | `/finalize` | Confirm backup generation + success receipt |
65
73
  | POST | `/abort` | Clear inflight |
66
74
 
67
- `auth` put/download follows the same selection/plan rules as other kinds. Concurrent prepare → `sync_in_progress`.
75
+ `credentials` put requires a selected plan; finalize rejects a missing credential transfer. Concurrent prepare → `sync_in_progress`; credential imports are separately serialized by rejection. Credential routes use the existing global authenticated API gate and sanitize upstream errors.
68
76
 
69
77
  ## Composition
70
78
 
71
- - Electron SSH: `TargetExecutor` push + inventory/tar pull.
79
+ - Electron SSH: `TargetExecutor` push + inventory/tar pull for files; credentials use authenticated HTTP through the established SSH forward.
72
80
  - Electron direct hosts: `direct-config-sync.mjs` calls these HTTP routes.
73
- - **Relay hosts (ticket 06):** UI opens an E2EE tunnel (`packages/ui/src/lib/relay/relay-config-sync.ts`) and calls the same HTTP routes via `tunnel.fetch`. Target id is `relay:<serverId>` (signing-key fingerprint). Preview/apply refresh candidates and refuse on `serverId` mismatch (`relay_identity_changed`) with no fallback identity. Auth remains the global `/api` bearer gate; selected `auth.json` transfers without a separate credential grant.
81
+ - **Relay hosts (ticket 06):** UI opens an E2EE tunnel (`packages/ui/src/lib/relay/relay-config-sync.ts`) and calls the same HTTP routes via `tunnel.fetch`. Target id is `relay:<serverId>` (signing-key fingerprint). Preview/apply refresh candidates and refuse on `serverId` mismatch (`relay_identity_changed`) with no fallback identity. Only explicit sync transports carry credential payloads; ordinary provider UI never receives them.
82
+ - Desktop local credential operations go through the local host HTTP routes, resolving its URL/token at call time. This also works when the active backend is an OTA module distinct from installer-owned config-sync code. Web/mobile hosts can receive through the same server; initiating the two-instance wizard remains desktop-only, including for VS Code.
83
+
84
+ ## Magpie discovery and transfer
85
+
86
+ The existing sync wizard discovers Magpie on the **source** for every preview. A push uses the local host's inventory; a pull uses the selected remote host's inventory (HTTP through the SSH forward, direct, or E2EE Relay). `magpieAvailable` survives selection filtering. The optional `magpie` plan entry is `{ bytes: 0 }` when selected: backup size is unknown until export, not counted as a zero-byte archive. Switching direction clears the Magpie selection and discovers the new source. A selected source disappearing is a failure, never an empty successful sync. This is discovery during the existing explicit sync flow, not a background file watcher.
87
+
88
+ Discovery checks `MAGPIE_BINARY`, PATH, the user's `.local/bin`, macOS application bundles and Homebrew paths, the standard Windows user installation path, then OpenChamber's managed executable. Symlinks resolve before locating portable data. Installed config honors absolute `XDG_CONFIG_HOME`, otherwise `~/.config/magpie`; portable config follows `.portable` / adjacent `data`, including the macOS app parent and an applicable AppImage directory. A shared Applications/home/download directory's `data` is not adopted over installed config. The source is available only when a verified CLI is installed and providers, settings, profiles, library, plugin config or authorization stores exist. Leftover config without an installed CLI does not expose the sync option. Magpie's checkbox and explanatory copy form a separate conditional group, default unchecked; general OpenCode sync copy remains independent. Metadata does not expose paths or secrets. Read errors fail discovery.
89
+
90
+ Selecting Magpie uses its native `backup` / `restore` with configuration, API keys, profiles, agent models, library and portable plugin declarations. Native provider/group restore merges by ID and does not propagate deletions; settings and library follow Magpie's own restore semantics, retaining machine-local settings. This is a full native bundle, not a provider-only restore.
91
+
92
+ The v2 envelope additionally carries recognized plugin API/OAuth accounts and self-contained saved Codex credentials. The source CLI account inventory limits export to recognized providers; hidden and orphaned plugin sign-ins are excluded. Plugin accounts merge by their native account key into `plugin-auth.json` under Magpie's exclusive-create lock, using a private atomic replacement. Destination-only accounts remain; repeated transfers do not append duplicates. Codex uses native `accounts import codex --yes`, which owns account identity/deduplication and the OS-level `logins.json` lock. OpenChamber never writes `logins.json` or copies machine-local account homes. Other subscription formats and accounts discovered only from external clients are not inferred or silently treated as transferred: unsupported active source accounts produce `sign_in_required`. An import re-reads `accounts --json` and requires all transferred plugin account counts and Codex identities to be recognized before returning success. This checks discovery, not successful model inference or future token renewal; revoked credentials still require login.
93
+
94
+ Missing destination executables are installed only when Magpie is explicitly selected for import, never in preview or ordinary OpenCode sync. The host selects the official pure CLI asset for macOS/Linux/Windows x64/arm64 from `https://usemagpie.ai/api/latest`, verifies SHA-256, checks `--version`, then atomically publishes it under `<OPENCHAMBER_DATA_DIR>/tools/magpie/` (default `~/.config/openchamber`). Existing desktop clients and CLIs are reused. Installation is process-local single-flight, needs no global PATH change or desktop environment, and fails explicitly for unsupported platforms or unverifiable downloads.
95
+
96
+ Selected sync also publishes `~/.local/bin/magpie` (`magpie.exe` on Windows), a path searched explicitly by `opencode-magpie-plugin`. POSIX uses a symlink, Windows a hard link for installed-mode data and a symlink for portable data; unsupported linking fails explicitly. Existing valid user commands using the same data directory are retained; conflicting/broken entries are not overwritten. Shared-service and private OpenCode startup repair the command entry for an already managed CLI before launching OpenCode; they never download Magpie just because OpenChamber starts. SSH/direct/Relay push and pull apply Magpie before OpenCode config archives, preventing a newly synced plugin from loading before its dependency is installed. Native agent settings skipped before sign-in are retried after authorization succeeds.
97
+
98
+ The `magpie` transfer kind exports a bounded `magpie-sync-v2` JSON envelope containing the encrypted native backup, a random per-transfer passphrase and portable authorizations. The envelope is sensitive and stays in memory on authenticated sync transports; Relay adds its existing E2EE. New receivers accept old `magpie-sync-v1` config-only envelopes with the original warning behavior; old receivers reject v2 rather than silently discarding its accounts. Upgrade both hosts for authorization sync. Validate the entire envelope before installation or restore. Passphrases go to child stdin, never argv, run logs or disk. Private temporary directories hold native encrypted backups and, during native Codex import only, a mode-0600 credential file; both are removed on success/failure. Raw child output is sanitized before reporting. A zero exit with genuinely missing/unsupported sign-ins or unavailable agent settings returns `magpieWarnings` (`sign_in_required`, `agent_settings_skipped`). The sign-in warning is recomputed after authorization transfer, not copied from the pre-transfer native restore. Receipts carry these codes through SSH/direct/Relay and desktop IPC to localized completion warnings. Provider/API-key, plugin-install, library, account import and verification failures remain errors. CLI failure can leave configuration and earlier accounts applied; retry merges idempotently. There is no cross-application rollback, and process termination can interrupt cleanup.
99
+
100
+ `/put/magpie` requires a selected prepared plan and finalize requires its receipt. Standalone `/magpie` is the corresponding authenticated host operation for SSH and local pull. Both use one host-local import/export lock. Desktop main only moves bytes and resolves the local host at call time; the web host owns process execution and installation, including under backend OTA. Web/mobile hosts can receive this operation; the initiating wizard remains desktop-only. No credential values enter previews or sync-run summaries.
@@ -92,8 +92,8 @@ describe('config-sync contract', () => {
92
92
  await fsp.writeFile(path.join(configDir, 'AGENTS.md'), 'hi\n');
93
93
  await fsp.writeFile(path.join(configDir, 'skills', 'a.md'), 'a\n');
94
94
 
95
- const full = planOpenCodeConfigSync(home, { direction: SYNC_DIRECTION_PUSH, includeAuthFile: false });
96
- const selections = buildDefaultSyncSelections({ includeAuthFile: false });
95
+ const full = planOpenCodeConfigSync(home, { direction: SYNC_DIRECTION_PUSH, includeProviderCredentials: false });
96
+ const selections = buildDefaultSyncSelections({ includeProviderCredentials: false });
97
97
  selections.fileGroups = selections.fileGroups.map((_, index) => index === 0);
98
98
  selections.singleFiles = selections.singleFiles.map(() => false);
99
99
  selections.directories = selections.directories.map((__, index) => index === 2); // skills
@@ -103,20 +103,20 @@ describe('config-sync contract', () => {
103
103
  expect(filtered.files.map((entry) => entry.path)).toEqual(['opencode.jsonc']);
104
104
  expect(filtered.directories.map((entry) => entry.path)).toEqual(['skills']);
105
105
  expect(filtered.agentsRoot).toBeNull();
106
- expect(filtered.authFile).toBeNull();
106
+ expect(filtered.providerCredentials).toBeNull();
107
107
 
108
108
  const inventoryPlan = planOpenCodeConfigSyncFromInventory({
109
109
  files: [{ path: 'opencode.jsonc', bytes: 3 }],
110
110
  directories: [{ path: 'skills', fileCount: 1, bytes: 2 }],
111
111
  agentsRoot: null,
112
- authFile: { bytes: 9 },
112
+ providerCredentials: { bytes: 9 },
113
113
  }, {
114
114
  direction: SYNC_DIRECTION_PULL,
115
- selections: { ...selections, authFile: true },
116
- includeAuthFile: true,
115
+ selections: { ...selections, providerCredentials: true },
116
+ includeProviderCredentials: true,
117
117
  });
118
118
  expect(inventoryPlan.direction).toBe('pull');
119
- expect(inventoryPlan.authFile).toEqual({ bytes: 9 });
119
+ expect(inventoryPlan.providerCredentials).toEqual({ bytes: 9 });
120
120
  expect(inventoryPlan.files.map((entry) => entry.path)).toEqual(['opencode.jsonc']);
121
121
  });
122
122
  });
@@ -132,7 +132,7 @@ describe('local pull destination backups', () => {
132
132
  directories: [],
133
133
  deletes: ['config.json', 'opencode.json'],
134
134
  agentsRoot: null,
135
- authFile: null,
135
+ providerCredentials: null,
136
136
  };
137
137
 
138
138
  await prepareLocalSyncDestination(home, plan, { syncRunId: 'pull-01', generations: 2 });
@@ -206,7 +206,7 @@ describe('remote inventory script', () => {
206
206
  await fsp.writeFile(path.join(skill, '.venv-asc', 'lib', 'binary.so'), 'x'.repeat(4096));
207
207
  await fsp.writeFile(path.join(skill, 'SKILL.md'), 'keep');
208
208
  }
209
- const local = planOpenCodeConfigSync(home, { includeAuthFile: false });
209
+ const local = planOpenCodeConfigSync(home, { includeProviderCredentials: false });
210
210
  expect(local.agentsRoot).toEqual({ fileCount: 1, bytes: 4 });
211
211
  expect(local.directories.find((entry) => entry.path === 'skills')).toMatchObject({ fileCount: 1, bytes: 4 });
212
212
  const stdout = execFileSync('sh', ['-c', buildRemoteSyncInventoryScript()], {
@@ -241,7 +241,7 @@ describe('remote inventory script', () => {
241
241
  expect(files).toContain('AGENTS.md');
242
242
  const directories = inventory.directories.map((entry) => entry.path);
243
243
  expect(directories).toContain('agents');
244
- expect(inventory.authFile).toEqual({ bytes: 3 });
244
+ expect(inventory).not.toHaveProperty('providerCredentials');
245
245
  for (const entry of inventory.files) {
246
246
  expect(typeof entry.path).toBe('string');
247
247
  expect(typeof entry.bytes).toBe('number');
@@ -263,6 +263,6 @@ describe('remote inventory script', () => {
263
263
  expect(inventory.files).toEqual([]);
264
264
  expect(inventory.directories).toEqual([]);
265
265
  expect(inventory.agentsRoot).toBeNull();
266
- expect(inventory.authFile).toBeNull();
266
+ expect(inventory).not.toHaveProperty('providerCredentials');
267
267
  });
268
268
  });
@@ -21,11 +21,8 @@ export const OPENCODE_CONFIG_SYNC_ALLOWLIST = {
21
21
  export const OPENCODE_CONFIG_SYNC_BACKUP_DIR = '.openchamber.sync-backup';
22
22
  /** Backup root for `$HOME/.agents` (outside `~/.agents` so rm -rf does not eat it). */
23
23
  export const OPENCODE_AGENTS_SYNC_BACKUP_DIR = '.openchamber.sync-backup-agents';
24
- /** Backup root for provider `auth.json` (outside `~/.local/share`). */
25
- export const OPENCODE_AUTH_SYNC_BACKUP_DIR = '.openchamber.sync-backup-auth';
26
24
 
27
25
  export const OPENCODE_AGENTS_ROOT_PROBE_MARKER = '__AGENTS_ROOT__';
28
- export const OPENCODE_AUTH_FILE_PROBE_MARKER = '__AUTH_FILE__';
29
26
 
30
27
  export const OPENCODE_CONFIG_SYNC_MAX_BYTES = 512 * 1024 * 1024;
31
28
  export const OPENCODE_CONFIG_SYNC_MAX_FILES = 20000;
@@ -8,7 +8,7 @@ import {
8
8
  * @typedef {{
9
9
  * posixShell: boolean,
10
10
  * tarExtract: boolean,
11
- * authFileWrite: boolean,
11
+ * providerCredentialsWrite: boolean,
12
12
  * ocHttp?: boolean,
13
13
  * }} SyncTargetCapabilities
14
14
  *
@@ -21,7 +21,7 @@ import {
21
21
  * @typedef {{
22
22
  * remoteExisting: string[],
23
23
  * remoteAgentsRootExists: boolean,
24
- * remoteAuthFileExists: boolean,
24
+ * remoteProviderCredentialsExists: boolean,
25
25
  * }} SyncProbeResult
26
26
  *
27
27
  * Payload for putTar. Callers always hand a fully buffered archive
@@ -31,7 +31,7 @@ import {
31
31
  * @typedef {{
32
32
  * probe: (plan: object) => Promise<SyncProbeResult>,
33
33
  * prepare: (plan: object, ctx: { syncRunId: string }) => Promise<void>,
34
- * putTar: (args: { kind: 'config' | 'agents' | 'auth', payload: SyncTarPayload }) => Promise<void>,
34
+ * putTar: (args: { kind: 'config' | 'agents' | 'credentials' | 'magpie', payload: SyncTarPayload }) => Promise<void>,
35
35
  * finalize: (plan: object, ctx: { syncRunId: string }) => Promise<{ ok: true }>,
36
36
  * }} TargetExecutor
37
37
  */
@@ -39,34 +39,34 @@ import {
39
39
  export const EMPTY_SYNC_CAPABILITIES = Object.freeze({
40
40
  posixShell: false,
41
41
  tarExtract: false,
42
- authFileWrite: false,
42
+ providerCredentialsWrite: false,
43
43
  ocHttp: false,
44
44
  });
45
45
 
46
46
  export const MANAGED_SSH_SYNC_CAPABILITIES = Object.freeze({
47
47
  posixShell: true,
48
48
  tarExtract: true,
49
- authFileWrite: true,
49
+ providerCredentialsWrite: true,
50
50
  ocHttp: false,
51
51
  });
52
52
 
53
53
  export const DIRECT_HOST_SYNC_CAPABILITIES = Object.freeze({
54
54
  posixShell: false,
55
55
  tarExtract: false,
56
- authFileWrite: true,
56
+ providerCredentialsWrite: true,
57
57
  ocHttp: true,
58
58
  });
59
59
 
60
60
  export const RELAY_HOST_SYNC_CAPABILITIES = Object.freeze({
61
61
  posixShell: false,
62
62
  tarExtract: false,
63
- authFileWrite: true,
63
+ providerCredentialsWrite: true,
64
64
  ocHttp: true,
65
65
  });
66
66
 
67
67
  /**
68
68
  * Build a namespaced SyncTarget for an SSH instance.
69
- * Managed remotes expose posixShell/tarExtract/authFileWrite; others expose none
69
+ * Managed remotes expose posixShell/tarExtract/providerCredentialsWrite; others expose none
70
70
  * (semantic equivalent of the former mode==='managed' hard gate).
71
71
  *
72
72
  * @param {string} instanceId
@@ -0,0 +1,101 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { currentClient } from '../opencode/auth.js';
3
+
4
+ const invalid = () => new Error('Invalid provider credentials sync payload');
5
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
6
+
7
+ function validate(entries) {
8
+ if (!Array.isArray(entries)) throw invalid();
9
+ const ids = new Set();
10
+ const active = new Set();
11
+ for (const entry of entries) {
12
+ if (!record(entry) || typeof entry.id !== 'string' || !entry.id || ids.has(entry.id)
13
+ || typeof entry.integrationID !== 'string' || !entry.integrationID
14
+ || typeof entry.label !== 'string' || typeof entry.active !== 'boolean' || !record(entry.value)) throw invalid();
15
+ ids.add(entry.id);
16
+ if (entry.active && active.has(entry.integrationID)) throw invalid();
17
+ if (entry.active) active.add(entry.integrationID);
18
+ const value = entry.value;
19
+ if (value.type === 'key') {
20
+ if (typeof value.key !== 'string') throw invalid();
21
+ } else if (value.type === 'oauth') {
22
+ if (typeof value.methodID !== 'string' || typeof value.access !== 'string'
23
+ || typeof value.refresh !== 'string' || !Number.isInteger(value.expires) || value.expires < 0) throw invalid();
24
+ } else if (value.type === 'external') {
25
+ if (typeof value.methodID !== 'string') throw invalid();
26
+ } else throw invalid();
27
+ if (value.metadata !== undefined && !record(value.metadata)) throw invalid();
28
+ }
29
+ return entries;
30
+ }
31
+
32
+ // Keep secrets in memory. Preview and run records receive only byte counts.
33
+ export function createCredentialSync(resolveClient = currentClient) {
34
+ let importing = false;
35
+ const request = () => ({ signal: AbortSignal.timeout(15000) });
36
+ const read = async (client) => validate(await client.credential.list(request()));
37
+ const encode = (entries) => Buffer.from(JSON.stringify({ format: 'opencode-credentials-v1', entries }));
38
+ return {
39
+ async inventory() {
40
+ try {
41
+ const entries = await read(resolveClient());
42
+ return entries.length ? { bytes: encode(entries).byteLength } : null;
43
+ } catch { throw new Error('Unable to read provider credentials'); }
44
+ },
45
+ async download() {
46
+ try { return encode(await read(resolveClient())); }
47
+ catch { throw new Error('Unable to read provider credentials'); }
48
+ },
49
+ async import(payload) {
50
+ if (importing) throw new Error('Provider credentials sync already in progress');
51
+ importing = true;
52
+ try {
53
+ let entries;
54
+ try {
55
+ const decoded = JSON.parse(Buffer.from(payload).toString('utf8'));
56
+ if (decoded.format !== 'opencode-credentials-v1') throw invalid();
57
+ entries = validate(decoded.entries);
58
+ } catch { throw invalid(); }
59
+ const client = resolveClient();
60
+ let before;
61
+ try { before = await read(client); }
62
+ catch { throw new Error('Unable to read destination provider credentials'); }
63
+ const touched = [];
64
+ const create = (entry) => client.credential.create({
65
+ id: entry.id, integrationID: entry.integrationID, label: entry.label,
66
+ value: entry.value, activate: false,
67
+ }, request());
68
+ try {
69
+ for (const entry of entries) {
70
+ const previous = before.find((item) => item.id === entry.id);
71
+ if (previous && previous.integrationID !== entry.integrationID) throw invalid();
72
+ if (previous && previous.label === entry.label && isDeepStrictEqual(previous.value, entry.value)) continue;
73
+ touched.push({ entry, previous });
74
+ if (previous) await client.credential.remove({ credentialID: entry.id }, request());
75
+ await create(entry);
76
+ }
77
+ for (const entry of entries.filter((item) => item.active)) {
78
+ await client.credential.activate({ credentialID: entry.id }, request());
79
+ }
80
+ } catch {
81
+ let restored = true;
82
+ for (const { entry, previous } of touched.reverse()) {
83
+ try {
84
+ const current = await read(client);
85
+ if (current.some((item) => item.id === entry.id)) await client.credential.remove({ credentialID: entry.id }, request());
86
+ if (previous) await create(previous);
87
+ } catch { restored = false; }
88
+ }
89
+ for (const entry of before.filter((item) => item.active && entries.some((source) => source.integrationID === item.integrationID))) {
90
+ try { await client.credential.activate({ credentialID: entry.id }, request()); }
91
+ catch { restored = false; }
92
+ }
93
+ throw new Error(restored
94
+ ? 'Provider credentials sync failed; previous credentials restored'
95
+ : 'Provider credentials sync failed; rollback incomplete. Retry the sync.');
96
+ }
97
+ return { ok: true };
98
+ } finally { importing = false; }
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OpenCode } from '@opencode/client';
3
+ import { createCredentialSync } from './credentials.js';
4
+
5
+ const key = (id, integrationID = 'openai', active = true) => ({
6
+ id, integrationID, label: id, active, value: { type: 'key', key: `fixture-${id}` },
7
+ });
8
+
9
+ // Exercise the installed SDK's real HTTP serialization, not a second SDK shape.
10
+ function service(initial = []) {
11
+ let entries = structuredClone(initial);
12
+ let failCreate = false;
13
+ let failList = false;
14
+ const client = OpenCode.make({ baseUrl: 'http://credentials.invalid', fetch: async (input, init) => {
15
+ const req = new Request(input, init);
16
+ const pathname = new URL(req.url).pathname;
17
+ const parts = pathname.split('/');
18
+ const id = parts[3];
19
+ if (req.method === 'GET') {
20
+ if (failList) return new Response('fixture-private-upstream-error', { status: 500 });
21
+ return Response.json({ data: entries });
22
+ }
23
+ if (req.method === 'DELETE') {
24
+ entries = entries.filter((entry) => entry.id !== id);
25
+ return new Response(null, { status: 204 });
26
+ }
27
+ if (pathname.endsWith('/activate')) {
28
+ const selected = entries.find((entry) => entry.id === id);
29
+ for (const entry of entries) if (entry.integrationID === selected.integrationID) entry.active = entry.id === id;
30
+ return new Response(null, { status: 204 });
31
+ }
32
+ const body = await req.json();
33
+ expect(body.integrationID).toBeTypeOf('string');
34
+ expect(body.value).toBeTruthy();
35
+ if (failCreate) {
36
+ failCreate = false;
37
+ return new Response('fixture-private-upstream-error', { status: 400 });
38
+ }
39
+ if (entries.some((entry) => entry.id === body.id)) return new Response(null, { status: 409 });
40
+ const { activate, ...value } = body;
41
+ const entry = { ...value, active: activate || !entries.some((item) => item.integrationID === body.integrationID && item.active) };
42
+ entries.push(entry);
43
+ return Response.json({ data: entry });
44
+ } });
45
+ return {
46
+ sync: createCredentialSync(() => client),
47
+ entries: () => structuredClone(entries),
48
+ failNextCreate: () => { failCreate = true; },
49
+ failReads: () => { failList = true; },
50
+ };
51
+ }
52
+
53
+ describe('provider credential sync', () => {
54
+ it('round-trips keys, OAuth accounts, metadata and active selection without deleting destination accounts', async () => {
55
+ const oauth = { ...key('oauth'), value: {
56
+ type: 'oauth', methodID: 'oauth', access: 'fixture-access', refresh: 'fixture-refresh', expires: 1000,
57
+ metadata: { accountID: 'fixture-account' },
58
+ } };
59
+ const source = service([key('key', 'openai', false), oauth]);
60
+ const other = key('other', 'anthropic');
61
+ const destination = service([other]);
62
+ const payload = await source.sync.download();
63
+ expect(await source.sync.inventory()).toEqual({ bytes: payload.byteLength });
64
+ expect(JSON.stringify(await source.sync.inventory())).not.toContain('fixture-');
65
+ await destination.sync.import(payload);
66
+ expect(destination.entries()).toEqual([other, key('key', 'openai', false), oauth]);
67
+ await destination.sync.import(payload);
68
+ expect(destination.entries()).toHaveLength(3);
69
+ });
70
+
71
+ it('updates matching IDs and rolls back failed replacement without leaking upstream errors', async () => {
72
+ const original = key('same');
73
+ const updated = { ...original, value: { type: 'key', key: 'fixture-updated' } };
74
+ const source = service([updated]);
75
+ const target = service([original, key('other', 'anthropic')]);
76
+ target.failNextCreate();
77
+ await expect(target.sync.import(await source.sync.download())).rejects.toThrow('previous credentials restored');
78
+ expect(target.entries()).toContainEqual(original);
79
+ expect(target.entries()).toContainEqual(key('other', 'anthropic'));
80
+ await target.sync.import(await source.sync.download());
81
+ expect(target.entries()).toContainEqual(updated);
82
+ });
83
+
84
+ it('rejects legacy archives and malformed input before changing destination state', async () => {
85
+ const target = service([key('existing')]);
86
+ for (const payload of ['legacy tar', '{"openai":{"key":"fixture"}}', JSON.stringify({ format: 'opencode-credentials-v1', entries: [{}] })]) {
87
+ await expect(target.sync.import(Buffer.from(payload))).rejects.toThrow('Invalid provider credentials');
88
+ expect(target.entries()).toEqual([key('existing')]);
89
+ }
90
+ });
91
+
92
+ it('does not turn unavailable credentials into empty inventory', async () => {
93
+ const source = service();
94
+ expect(await source.sync.inventory()).toBeNull();
95
+ source.failReads();
96
+ await expect(source.sync.inventory()).rejects.toThrow('Unable to read provider credentials');
97
+ });
98
+ });