@openchambery/web 2.1.7 → 2.1.8-beta.11
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.
- package/dist/assets/{AgentAvatar-CAR4F5Sn.js → AgentAvatar-mchFM3n-.js} +1 -1
- package/dist/assets/AssistantView-Buh5CnUM.js +3 -0
- package/dist/assets/{DiagramView-Dd_3w4Hk.js → DiagramView-CS-VbxSq.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CCVddfP5.js → FadeInOnReveal-BXDM9joC.js} +1 -1
- package/dist/assets/{Icon-DDB7S6Vz.js → Icon-BGp_ize2.js} +1 -1
- package/dist/assets/{ImageLoadingPlaceholder-C6fDEcU1.js → ImageLoadingPlaceholder-DmUxjyxq.js} +1 -1
- package/dist/assets/{LoadingImage-ByBH-5WW.js → LoadingImage-DGjlKW_O.js} +1 -1
- package/dist/assets/MarkdownRenderer-DTal450A.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-BXgUthom.js → MarkdownRendererImpl-BtNm_zUp.js} +2 -2
- package/dist/assets/{MarkstreamRendererImpl-5W9P9U-w.js → MarkstreamRendererImpl-XWeuflcN.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-Bk6C9i9l.js → MobileDetailNavigation-KY7OMEcd.js} +1 -1
- package/dist/assets/{MobileShareBridge-B5nF-gT6.js → MobileShareBridge-CniyrAC2.js} +1 -1
- package/dist/assets/MobileTabPageHeader-DS83O-io.js +2 -0
- package/dist/assets/{MultiRunWindow-CpuFaAyG.js → MultiRunWindow-DCKmDwMS.js} +1 -1
- package/dist/assets/{OnboardingScreen-DINUboLT.js → OnboardingScreen-DmJgU0Jc.js} +1 -1
- package/dist/assets/{OverlayScrollbar-BkxmoyYl.js → OverlayScrollbar-BkJCkZp3.js} +1 -1
- package/dist/assets/{SettingsView--C3eizlJ.js → SettingsView-DoWqNR1h.js} +10 -9
- package/dist/assets/{SettingsWindow-DYUvg92F.js → SettingsWindow-CPZKZbtW.js} +1 -1
- package/dist/assets/{TerminalView-BqAariXf.js → TerminalView-DTsI_jRx.js} +1 -1
- package/dist/assets/{ToolOutputDialog-D6lPUZrN.js → ToolOutputDialog-BPIcNeO8.js} +1 -1
- package/dist/assets/{appThemeRegistry-wWazoyp8.js → appThemeRegistry-BekiFLtH.js} +1 -1
- package/dist/assets/{button-DymteyMv.js → button-BKjGCkH3.js} +1 -1
- package/dist/assets/{collapsible-Dzp5rO0Q.js → collapsible-03FGLsF4.js} +1 -1
- package/dist/assets/{desktopHosts-DSBZ-9-L.js → desktopHosts-BZ7iYE-l.js} +1 -1
- package/dist/assets/{desktopRelayRestore-DK8TzHRx.js → desktopRelayRestore-S9wEiY2U.js} +1 -1
- package/dist/assets/{diagram-CQs9px6y.js → diagram-D4QHcaSQ.js} +1 -1
- package/dist/assets/{dialog-DfUeXqdE.js → dialog-nm_cQEUZ.js} +1 -1
- package/dist/assets/{dropdown-menu.styles-B6YMNOZz.js → dropdown-menu.styles-WkR8IcF2.js} +1 -1
- package/dist/assets/es-C9PRwAQm.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-CEIhM6Ov.js → fileMentionTouchSelection-D7wzKuXd.js} +1 -1
- package/dist/assets/{fileQueries-BGE5l3Jd.js → fileQueries-DZR-mK8h.js} +1 -1
- package/dist/assets/fr-2tfGTcme.js +13 -0
- package/dist/assets/{gitApi-BP68uLBQ.js → gitApi-7Eh0Gw65.js} +1 -1
- package/dist/assets/{image-loading-qNtUCy69.js → image-loading-CsxDGgek.js} +1 -1
- package/dist/assets/{input-BvpZiRHI.js → input-Buww8enN.js} +1 -1
- package/dist/assets/insertionBoundaries-CztRpooN.js +1 -0
- package/dist/assets/ja-D7NHmhJr.js +15 -0
- package/dist/assets/ko-DeGmXmv6.js +15 -0
- package/dist/assets/{main-DSY3-ZQZ.js → main-BOHCGdL_.js} +2 -2
- package/dist/assets/main-ed7J30DP.js +169 -0
- package/dist/assets/{markdownHeightCache-MW1NYOaG.js → markdownHeightCache-B6wZ-UwD.js} +1 -1
- package/dist/assets/{mcpQueries-BaQ2_bdm.js → mcpQueries-DP3Jdcpw.js} +1 -1
- package/dist/assets/miniChat-DwmYV7Hz.js +2 -0
- package/dist/assets/mobile-Bt8QvmfL.js +2 -0
- package/dist/assets/{multirun-Blywdd6A.js → multirun-CJbaII7D.js} +1 -1
- package/dist/assets/{persistence-B7ORJ371.js → persistence-B4Ni3Gmv.js} +1 -1
- package/dist/assets/{pl-BE8MC0XQ.js → pl-CyWLY1AT.js} +3 -3
- package/dist/assets/{platform-CiBU2bCp.js → platform-BQPsr8Zl.js} +1 -1
- package/dist/assets/{projectMeta-BwPtPeMh.js → projectMeta-CjpzTdsR.js} +1 -1
- package/dist/assets/pt-BR-D9VVxvts.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-BKJPbAmd.js +2 -0
- package/dist/assets/renderMobileApp-DbLhg1sk.js +28 -0
- package/dist/assets/{runtimeConfig-CqpbN2lM.js → runtimeConfig-HhNAmhW8.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-bWBhgTs_.js → runtimeEndpointReset-z8Oqvm5G.js} +15 -15
- package/dist/assets/{scheduledTaskNavigation-BnKGGUlX.js → scheduledTaskNavigation-C-HGgCQA.js} +1 -1
- package/dist/assets/sessionChangeSummary-B_MgrzNf.js +1 -0
- package/dist/assets/{sessionLookup-6gcNEZnB.js → sessionLookup-DovQh7Fb.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CWR8KhJB.js → sortable-tabs-strip-cHZM0f0Z.js} +1 -1
- package/dist/assets/speechSettingsQueries-8rvNYdbZ.js +4 -0
- package/dist/assets/{src-C9BNQK0O.css → src-OjoeiMbd.css} +1 -1
- package/dist/assets/store-Ds-7Em7C.js +15 -0
- package/dist/assets/{terminalApi-bN5IYQau.js → terminalApi-kRMttlcP.js} +1 -1
- package/dist/assets/{textarea-ClPlugKE.js → textarea-BZHD_GND.js} +1 -1
- package/dist/assets/{tooltip-D-1Pgmq1.js → tooltip-DCeWvAqN.js} +1 -1
- package/dist/assets/uk-Df44svOl.js +15 -0
- package/dist/assets/{url-nbXEti6c.js → url-Bd61gNgl.js} +1 -1
- package/dist/assets/{useAppFontEffects-8UJ5f0dG.js → useAppFontEffects-CP5PzBeS.js} +51 -53
- package/dist/assets/{useDesktopWindowControlsLayout-B_hNd3pD.js → useDesktopWindowControlsLayout-DPB0QqmV.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DISvoVLe.js → useEffectiveDirectory-d4hTR00J.js} +1 -1
- package/dist/assets/{useFontPreferences-6LD7d7hP.js → useFontPreferences-lAw0Cbza.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BboICP_n.js +417 -0
- package/dist/assets/{useWorkerHighlightedLines-BFygybD8.js → useWorkerHighlightedLines-BTe1KXFO.js} +1 -1
- package/dist/assets/vendor-codemirror-autocomplete-7c2L_4dM.js +12 -0
- package/dist/assets/vendor-codemirror-commands-B8KrBQ9B.js +1 -0
- package/dist/assets/vendor-codemirror-lang-angular-DXOgWEVj.js +23 -0
- package/dist/assets/{vendor-codemirror-lang-cpp-C3UKgJaQ.js → vendor-codemirror-lang-cpp-Dh8C3xqP.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-go-Cv5NYdGm.js → vendor-codemirror-lang-go-BXAJ1_dh.js} +5 -5
- package/dist/assets/{vendor-codemirror-lang-java-D6hBTIgD.js → vendor-codemirror-lang-java-DAyCHRhk.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-jinja-ehGXhAov.js → vendor-codemirror-lang-jinja-0TICrMfO.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-json-tHviF6YN.js → vendor-codemirror-lang-json-mLulNbwB.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-less-B-yDfSDo.js → vendor-codemirror-lang-less-CNvptX3P.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-liquid-BdoLH4KN.js → vendor-codemirror-lang-liquid-8g5seytq.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js +6 -0
- package/dist/assets/{vendor-codemirror-lang-php-Ch8GW4Xz.js → vendor-codemirror-lang-php-xP7rf28_.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-python-YM2WVLHF.js +9 -0
- package/dist/assets/{vendor-codemirror-lang-rust-BEUlElU6.js → vendor-codemirror-lang-rust-CtafR0gt.js} +1 -1
- package/dist/assets/vendor-codemirror-lang-sass-B4iav_Ps.js +1 -0
- package/dist/assets/{vendor-codemirror-lang-sql-BnZql454.js → vendor-codemirror-lang-sql-C6q09UUZ.js} +2 -2
- package/dist/assets/{vendor-codemirror-lang-vue-BCwQBvLR.js → vendor-codemirror-lang-vue-DIB78UsR.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-wast-mrtA2eML.js → vendor-codemirror-lang-wast-C_JVdOCF.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-xml-ClAh-G2D.js → vendor-codemirror-lang-xml-DIuzY_d4.js} +1 -1
- package/dist/assets/{vendor-codemirror-lang-yaml-CjL8XEf2.js → vendor-codemirror-lang-yaml-BO0hIfQM.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DGDYune4.js → vendor-codemirror-language-data-Ctd4WxgW.js} +2 -2
- package/dist/assets/vendor-codemirror-search-ZqtrPLjU.js +2 -0
- package/dist/assets/{vendor-replit-codemirror-vim-CEibx3EP.js → vendor-replit-codemirror-vim-DDj0dSVY.js} +6 -6
- package/dist/assets/zh-CN-wowfzo60.js +15 -0
- package/dist/assets/zh-TW-DwojcgA_.js +15 -0
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/lib/assistants/DOCUMENTATION.md +1 -1
- package/server/lib/assistants/harness.js +1 -1
- package/server/lib/assistants/harness.test.js +1 -1
- package/server/lib/config-sync/DOCUMENTATION.md +38 -11
- package/server/lib/config-sync/config-sync.test.js +11 -11
- package/server/lib/config-sync/constants.js +0 -3
- package/server/lib/config-sync/contract.js +8 -8
- package/server/lib/config-sync/credentials.js +101 -0
- package/server/lib/config-sync/credentials.test.js +98 -0
- package/server/lib/config-sync/engine.js +17 -11
- package/server/lib/config-sync/index.js +0 -3
- package/server/lib/config-sync/local-backup.js +0 -13
- package/server/lib/config-sync/magpie-auth.js +111 -0
- package/server/lib/config-sync/magpie-auth.test.js +79 -0
- package/server/lib/config-sync/magpie.js +276 -0
- package/server/lib/config-sync/magpie.test.js +265 -0
- package/server/lib/config-sync/plan.js +32 -37
- package/server/lib/config-sync/receiver.js +28 -25
- package/server/lib/config-sync/routes.js +23 -2
- package/server/lib/config-sync/routes.test.js +64 -5
- package/server/lib/config-sync/scripts.js +2 -30
- package/server/lib/config-sync/selections.js +14 -10
- package/server/lib/opencode/DOCUMENTATION.md +4 -1
- package/server/lib/opencode/auth.d.ts +1 -0
- package/server/lib/opencode/auth.js +1 -1
- package/server/lib/opencode/config-entities-layering.test.js +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +1 -0
- package/server/lib/opencode/lifecycle.js +4 -0
- package/server/lib/opencode/lifecycle.test.js +7 -2
- package/server/lib/opencode/plugin-routes.test.js +1 -1
- package/server/lib/opencode/plugins.js +31 -21
- package/server/lib/opencode/plugins.test.js +55 -10
- package/server/lib/session-folders/DOCUMENTATION.md +20 -0
- package/server/lib/session-folders/order-routes.js +69 -0
- package/server/lib/session-folders/routes.js +3 -0
- package/server/lib/session-folders/routes.test.js +49 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -0
- package/server/lib/session-index/sync-runtime.js +9 -4
- package/server/lib/session-index/sync-runtime.test.js +27 -0
- package/dist/assets/AssistantView-DgjRT4M4.js +0 -3
- package/dist/assets/MarkdownRenderer-YbG78e0Y.js +0 -2
- package/dist/assets/MobileTabPageHeader-D7r6pKco.js +0 -2
- package/dist/assets/es-2YvsazK2.js +0 -15
- package/dist/assets/fr-Cy0M48Gw.js +0 -13
- package/dist/assets/insertionBoundaries-BywXllQ9.js +0 -1
- package/dist/assets/ja-1zLWWJ2R.js +0 -15
- package/dist/assets/ko-BLk_5Dhc.js +0 -15
- package/dist/assets/main-DdyFxokk.js +0 -169
- package/dist/assets/miniChat-B43ASec3.js +0 -2
- package/dist/assets/mobile-DloCio4f.js +0 -2
- package/dist/assets/pt-BR-ubhNymyH.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-DCRctqrx.js +0 -2
- package/dist/assets/renderMobileApp-DuFdSBXi.js +0 -28
- package/dist/assets/sessionChangeSummary-BlR9MyHu.js +0 -2
- package/dist/assets/speechSettingsQueries-Bp5LgcR7.js +0 -4
- package/dist/assets/store-n-H5RvRn.js +0 -15
- package/dist/assets/uk-BNyyigDo.js +0 -15
- package/dist/assets/useMobileNavigationStore-BE_8Q8WJ.js +0 -417
- package/dist/assets/vendor-codemirror-autocomplete-DHomsvW7.js +0 -12
- package/dist/assets/vendor-codemirror-commands-C9DCaJ0C.js +0 -1
- package/dist/assets/vendor-codemirror-lang-angular-BDXnzWgq.js +0 -23
- package/dist/assets/vendor-codemirror-lang-markdown-Bt-vLz9k.js +0 -6
- package/dist/assets/vendor-codemirror-lang-python-B4naLXMo.js +0 -9
- package/dist/assets/vendor-codemirror-lang-sass-ibtp2iUB.js +0 -1
- package/dist/assets/vendor-codemirror-search-D2SOgzph.js +0 -2
- package/dist/assets/zh-CN-CNWzMPgL.js +0 -15
- 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-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-BOHCGdL_.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-
|
|
567
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
568
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
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-
|
|
572
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
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-
|
|
576
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
575
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-HhNAmhW8.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
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-DwmYV7Hz.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-
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
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-
|
|
94
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
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-
|
|
98
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-HhNAmhW8.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-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-Bt8QvmfL.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-
|
|
78
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-
|
|
79
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-
|
|
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-
|
|
83
|
-
<link rel="modulepreload" crossorigin href="/assets/terminalApi-
|
|
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-
|
|
87
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-HhNAmhW8.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
|
@@ -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(
|
|
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(
|
|
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,
|
|
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,
|
|
28
|
-
- **Selections**: `{ fileGroups: boolean[], singleFiles: boolean[], directories: boolean[], agentsRoot: 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
|
-
- `
|
|
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` / `
|
|
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
|
-
-
|
|
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=` |
|
|
63
|
-
| GET | `/download/:kind` |
|
|
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
|
-
`
|
|
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.
|
|
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,
|
|
96
|
-
const selections = buildDefaultSyncSelections({
|
|
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.
|
|
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
|
-
|
|
112
|
+
providerCredentials: { bytes: 9 },
|
|
113
113
|
}, {
|
|
114
114
|
direction: SYNC_DIRECTION_PULL,
|
|
115
|
-
selections: { ...selections,
|
|
116
|
-
|
|
115
|
+
selections: { ...selections, providerCredentials: true },
|
|
116
|
+
includeProviderCredentials: true,
|
|
117
117
|
});
|
|
118
118
|
expect(inventoryPlan.direction).toBe('pull');
|
|
119
|
-
expect(inventoryPlan.
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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' | '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
+
});
|