@runeya/runeya 2.0.69 → 2.0.71
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/agent/index.js +63 -10
- package/agent/index.js.map +1 -1
- package/agent-manager-UUSQK6S2.js +7 -0
- package/{chunk-YSJ6OJGN.js → chunk-Z3KAXI7V.js} +197 -12
- package/chunk-Z3KAXI7V.js.map +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/{src-MQWOIUB5.js → src-Q3PSCMS7.js} +1713 -1751
- package/src-Q3PSCMS7.js.map +1 -0
- package/web/assets/{AgentScalarChatInterface.vue-vU3z8Etw.js → AgentScalarChatInterface.vue-Cin_Rbay.js} +1 -1
- package/web/assets/AgentTerminalPage-BD2w4XRc.css +1 -0
- package/web/assets/AgentTerminalPage-YgCQj_Mj.js +1 -0
- package/web/assets/AiChatPage-cIE_ZdF6.js +1 -0
- package/web/assets/{AppLayout-CvkK_Pp3.js → AppLayout-dYfSESzv.js} +1 -1
- package/web/assets/{AuditPage-BZuVgUf1.js → AuditPage-Bw3-G8ZM.js} +1 -1
- package/web/assets/CloudAccountPage-CA4dMsIm.js +1 -0
- package/web/assets/{CloudAccountPage-B__2SGAw.js → CloudAccountPage-DjljrEI_.js} +1 -1
- package/web/assets/{CloudMessagingPage-C5pD6Qsd.js → CloudMessagingPage-CbfH2vPo.js} +1 -1
- package/web/assets/{DashboardPage-66q5K841.js → DashboardPage-Cn3bUyhR.js} +1 -1
- package/web/assets/{DirectoryPickerDialog-wZYtGEZ9.js → DirectoryPickerDialog-BmsAi5gR.js} +1 -1
- package/web/assets/{EditorIcon-DUDLTmUd.js → EditorIcon-JRryeySJ.js} +1 -1
- package/web/assets/{EnvironmentForm-C0Q1pRdW.js → EnvironmentForm-BfF9IFRB.js} +1 -1
- package/web/assets/{EnvironmentFormPage-LO-mwupn.js → EnvironmentFormPage-CebWv3I_.js} +1 -1
- package/web/assets/{EnvironmentListPage-CvknyN3O.js → EnvironmentListPage-DDJGOHvR.js} +1 -1
- package/web/assets/{KanbanBoardPage-DUsAyowy.js → KanbanBoardPage-BMXyvHOl.js} +3 -3
- package/web/assets/{KanbanListPage-9RBCauSd.js → KanbanListPage-YMm_7vzg.js} +1 -1
- package/web/assets/{LaunchProjectPage-obaNCuEY.js → LaunchProjectPage-Da21oFjw.js} +1 -1
- package/web/assets/{LaunchServicesPage-CiWzQIyC.js → LaunchServicesPage-8YJT1stQ.js} +1 -1
- package/web/assets/{LogViewer-BiSD_TNt.js → LogViewer-BYNRWqlP.js} +1 -1
- package/web/assets/{OrgKeyPrompt-zhMl_nTd.js → OrgKeyPrompt-De8D0zzy.js} +1 -1
- package/web/assets/{ProcessControls-DMxvJmDe.js → ProcessControls-BPQXGKib.js} +1 -1
- package/web/assets/{ProcessDetailPage-CsCqQh2V.js → ProcessDetailPage-CVwMbU6x.js} +1 -1
- package/web/assets/{ProcessMetrics-DK4Dgdb0.js → ProcessMetrics-D7L708Em.js} +1 -1
- package/web/assets/{ProjectDetailPage-B0vJySvH.js → ProjectDetailPage-BjBN3hnN.js} +1 -1
- package/web/assets/{ProjectListPage-srxBmrbO.js → ProjectListPage-BRXKT6op.js} +1 -1
- package/web/assets/{RandomJobTestPage-jGfX-6W9.js → RandomJobTestPage-dhDNam5v.js} +1 -1
- package/web/assets/{ScenarioEditorPage-CxLy6gs7.js → ScenarioEditorPage-yOp4ZF14.js} +1 -1
- package/web/assets/{SendToChatDialog-AHkuFQjt.js → SendToChatDialog-o4y1Q-OD.js} +1 -1
- package/web/assets/{ServiceControlsBar-C8P9_qWK.js → ServiceControlsBar-BH1ZEFkK.js} +1 -1
- package/web/assets/{ServiceDetailPage-DTBpQegV.js → ServiceDetailPage-CFb0blqq.js} +1 -1
- package/web/assets/{ServiceFormPage-CsPSMWdD.js → ServiceFormPage-DzFQ_M1-.js} +1 -1
- package/web/assets/SettingsPage-Bcs3tnUy.js +2063 -0
- package/web/assets/{SettingsPage-uCS5lY2z.css → SettingsPage-CScovMi5.css} +1 -1
- package/web/assets/{UnlockPage-Bkp_hR-F.js → UnlockPage-BAxKs_K9.js} +1 -1
- package/web/assets/{VariablesTable-jo61MhbL.js → VariablesTable-CSuAPeF-.js} +1 -1
- package/web/assets/{VaultMigrationPage-D-Dxg5xH.js → VaultMigrationPage-BJX9kunL.js} +1 -1
- package/web/assets/{VersionBadge-CwNZLgqN.js → VersionBadge-B9Wvyvf6.js} +1 -1
- package/web/assets/WikiPage-CBZFOPL_.js +1 -0
- package/web/assets/{WikiView-BpJECJ2Y.js → WikiView-DxPrGyFy.js} +1 -1
- package/web/assets/{add-scalar-classes-CqLAxAGv.js → add-scalar-classes-vtTXFTZ6.js} +3 -3
- package/web/assets/auth-errors-DpUvxKid.js +1 -0
- package/web/assets/{autocomplete-CrMQ2sCk.js → autocomplete-BakcEoGP.js} +1 -1
- package/web/assets/{basic-shape-diamond.svg-CacrbinE.js → basic-shape-diamond.svg-C8K6YjGj.js} +1 -1
- package/web/assets/{basic-shape-hexagon.svg-GGJ54oyX.js → basic-shape-hexagon.svg-C9PeXR6S.js} +1 -1
- package/web/assets/{basic-shape-primary-circle-ellipse-round.svg-CuWfwdJM.js → basic-shape-primary-circle-ellipse-round.svg-CfBLN6Yj.js} +1 -1
- package/web/assets/{basic-shape-primary-square-rectangle.svg-C2Bewjip.js → basic-shape-primary-square-rectangle.svg-D2QPBP5_.js} +1 -1
- package/web/assets/{basic-shape-shield.svg-DNtg0KuJ.js → basic-shape-shield.svg-ClsEFdja.js} +1 -1
- package/web/assets/{browser-XO2fm4BX.js → browser-CF5_9QqR.js} +1 -1
- package/web/assets/{browser-5NN8ieli.js → browser-D7ZckEFz.js} +1 -1
- package/web/assets/browser-DNbnH79q.js +6 -0
- package/web/assets/{card-CjcU4SfX.js → card-DcpoBJHQ.js} +1 -1
- package/web/assets/{checkbox-iq9mU5eP.js → checkbox-CfwNXlF_.js} +1 -1
- package/web/assets/{chip-BsccUnHb.js → chip-3tOoBrD7.js} +1 -1
- package/web/assets/{client-B9UJRQa-.js → client-CKBKBNVm.js} +1 -1
- package/web/assets/{cloud-chat.store-Dw2lzuBY.js → cloud-chat.store-Ci1DBP5j.js} +1 -1
- package/web/assets/{cloud-session-DIhbWyP5.js → cloud-session-CzYQ65k8.js} +1 -1
- package/web/assets/{column-BGKYk6b6.js → column-CVuKYYbZ.js} +1 -1
- package/web/assets/{computer-device-desktop-monitor.svg-DSwpF_ss.js → computer-device-desktop-monitor.svg-9f4DSNLj.js} +1 -1
- package/web/assets/{computer-device-desktop.svg-Caj_PZBV.js → computer-device-desktop.svg-BYoXiEOE.js} +1 -1
- package/web/assets/{computer-device-laptop.svg-Bv3ienI_.js → computer-device-laptop.svg-DR1sEMdL.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-android-samsung-back.svg-IWlhEKx_.js → computer-device-mobile-phone-android-samsung-back.svg-B-AOaBuq.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-android-samsung.svg-CglQAFq2.js → computer-device-mobile-phone-android-samsung.svg-Dw4z_H5H.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-iphone-x-back.svg-CgjNnTTw.js → computer-device-mobile-phone-iphone-x-back.svg-BtqMb2Wo.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-iphone-x.svg-CotZ-4M5.js → computer-device-mobile-phone-iphone-x.svg-eYEM_VXA.js} +1 -1
- package/web/assets/{computer-device-mobile-tablet-touch.svg-B2cemd61.js → computer-device-mobile-tablet-touch.svg-jLD41nvm.js} +1 -1
- package/web/assets/{computer-device-mobile-tablet.svg-8nPVs4u_.js → computer-device-mobile-tablet.svg-D_7_M1GK.js} +1 -1
- package/web/assets/{computer-device-network-ethernet-cat6.svg-Bl56Qw4v.js → computer-device-network-ethernet-cat6.svg-B_527QGY.js} +1 -1
- package/web/assets/{computer-device-network-lan-www.svg-C_x6Rf8N.js → computer-device-network-lan-www.svg-ClZIaUOH.js} +1 -1
- package/web/assets/{computer-device-network-wifi-connection.svg-oz6dsDQB.js → computer-device-network-wifi-connection.svg-DC4OCJY0.js} +1 -1
- package/web/assets/{computer-device-network-wifi-router.svg-BciHU6Nj.js → computer-device-network-wifi-router.svg-BWPpobCc.js} +1 -1
- package/web/assets/{cssMode-DSX5tYcm.js → cssMode-CXUrkdrQ.js} +1 -1
- package/web/assets/{divider-DnPOdDs3.js → divider-BrO5nkpd.js} +1 -1
- package/web/assets/docx-preview-tE714WSD.js +21 -0
- package/web/assets/{ecology-science-erlenmeyer-flask.svg-BzQP6y9L.js → ecology-science-erlenmeyer-flask.svg-o8D0ASmr.js} +1 -1
- package/web/assets/{fieldset-D9lkV70l.js → fieldset-LX5cdTvN.js} +1 -1
- package/web/assets/{freemarker2-D9yXX9Vw.js → freemarker2-DJ82s0fP.js} +1 -1
- package/web/assets/{handlebars-BmcjEksL.js → handlebars-BXFlY2Lu.js} +1 -1
- package/web/assets/{html-BQF9vZLL.js → html-CqDVypOC.js} +1 -1
- package/web/assets/{htmlMode-DxSRpgHE.js → htmlMode-DrHxdTQP.js} +1 -1
- package/web/assets/{image-flash-lightning.svg-DQX09uxo.js → image-flash-lightning.svg-BDPMOynq.js} +1 -1
- package/web/assets/{image-picture-flower.svg-CUBdrnoc.js → image-picture-flower.svg-D-AJaery.js} +1 -1
- package/web/assets/{index-enuBsJBa.js → index-a6DAapTb.js} +299 -299
- package/web/assets/{interface-alert-exclamation-diamond.svg-DXQdQSvN.js → interface-alert-exclamation-diamond.svg-CV7M0IVQ.js} +1 -1
- package/web/assets/{interface-alert-exclamation-triangle-warning.svg-BoCeNIlU.js → interface-alert-exclamation-triangle-warning.svg-B8NapiB7.js} +1 -1
- package/web/assets/{interface-alert-information-circle.svg-BCA57JL2.js → interface-alert-information-circle.svg-eQIFULjG.js} +1 -1
- package/web/assets/{interface-award-crown.svg-B1hTlsb0.js → interface-award-crown.svg-D-4fclUD.js} +1 -1
- package/web/assets/{interface-bookmark.svg-zf3cIGKM.js → interface-bookmark-tag.svg-Jy6EOdau.js} +1 -1
- package/web/assets/{interface-bookmark-tag.svg-AosfQ0Ip.js → interface-bookmark.svg-XdbYDqqk.js} +1 -1
- package/web/assets/{interface-calendar-date-one.svg-D58WqPie.js → interface-calendar-date-one.svg-BIoXVZrz.js} +1 -1
- package/web/assets/{interface-content-book-open-pages.svg-B74HWMlw.js → interface-content-book-open-pages.svg-DHwJbBvQ.js} +1 -1
- package/web/assets/{interface-content-book-page.svg-C2QWk62-.js → interface-content-book-page.svg-47msUBN_.js} +1 -1
- package/web/assets/{interface-content-file.svg-xKu0qAOQ.js → interface-content-file.svg-DXw9W8QM.js} +1 -1
- package/web/assets/interface-content-folder.svg-BikaY449.js +1 -0
- package/web/assets/{interface-content-folder.svg-ZkrY9WkQ.js → interface-content-folder.svg-BmK_IJv8.js} +1 -1
- package/web/assets/{interface-copy-clipboard.svg-BEkNG3vO.js → interface-copy-clipboard.svg-CQ1rQN0L.js} +1 -1
- package/web/assets/{interface-edit-attachment.svg-5ztQ3WTq.js → interface-edit-attachment.svg-C3aTaTh_.js} +1 -1
- package/web/assets/{interface-edit-binocular.svg-CC9U1Gnc.js → interface-edit-binocular.svg-B2Ue_tgs.js} +1 -1
- package/web/assets/{interface-edit-magic-wand.svg-CPl6ZoE9.js → interface-edit-magic-wand.svg-DDKse-Wy.js} +1 -1
- package/web/assets/{interface-edit-tool-paint-roller.svg-B85t7jG2.js → interface-edit-tool-paint-roller.svg-C2ABby1f.js} +1 -1
- package/web/assets/{interface-edit-tool-pencil.svg-BSTdqWUg.js → interface-edit-tool-pencil.svg-CNjYbknQ.js} +1 -1
- package/web/assets/{interface-favorite-award.svg-DbPr_WKt.js → interface-favorite-award.svg-DWqSHfIa.js} +1 -1
- package/web/assets/{interface-favorite-flag.svg-CgKYXc7k.js → interface-favorite-flag.svg-CNUsr98g.js} +1 -1
- package/web/assets/{interface-favorite-heart.svg-DfnXyJMg.js → interface-favorite-heart.svg-3Eb366FO.js} +1 -1
- package/web/assets/{interface-favorite-star.svg-0sT7jJMK.js → interface-favorite-star.svg-CVJ3aq1n.js} +1 -1
- package/web/assets/{interface-favorite-stars-sparkles.svg-BcM2EXgn.js → interface-favorite-stars-sparkles.svg-BfXhPBbs.js} +1 -1
- package/web/assets/{interface-hierarchy-flowchart.svg-BM62WcKt.js → interface-hierarchy-flowchart.svg-DzsqKrYq.js} +1 -1
- package/web/assets/{interface-home-house.svg-KqP7iMC4.js → interface-home-house.svg-BnXbuyZn.js} +1 -1
- package/web/assets/{interface-hyperlink.svg-DBKFbihR.js → interface-hyperlink.svg-BsmSmyP5.js} +1 -1
- package/web/assets/{interface-lighting-brightness.svg-DTkCS7PY.js → interface-lighting-brightness.svg-BywVQRo9.js} +1 -1
- package/web/assets/{interface-lock-closed.svg-Div5xlvI.js → interface-lock-closed.svg-BLPORc_h.js} +1 -1
- package/web/assets/{interface-lock-open-unlock.svg-Du5L_iMV.js → interface-lock-open-unlock.svg-CaYlbVmU.js} +1 -1
- package/web/assets/{interface-login-key.svg-28TYVVzH.js → interface-login-key.svg-D5F_9MJ0.js} +1 -1
- package/web/assets/{interface-search.svg-BS_owhIG.js → interface-search.svg-CF1D8GIV.js} +1 -1
- package/web/assets/{interface-setting-cog.svg-afYc0TQv.js → interface-setting-cog.svg-D4AOKG2g.js} +1 -1
- package/web/assets/{interface-share-megaphone-bullhorn.svg-BnW8HhjG.js → interface-share-megaphone-bullhorn.svg-BIfr2_va.js} +1 -1
- package/web/assets/{interface-share-rocket.svg-DvX_9178.js → interface-share-rocket.svg-NDd3dv1t.js} +1 -1
- package/web/assets/{interface-share-satellite.svg-B4nQqv6w.js → interface-share-satellite.svg-CVDJbmT-.js} +1 -1
- package/web/assets/{interface-share-space-ship.svg-Ciol-6JO.js → interface-share-space-ship.svg-D0ZLJNO2.js} +1 -1
- package/web/assets/{interface-share.svg-COm1OVr8.js → interface-share.svg-WpwbpOes.js} +1 -1
- package/web/assets/{interface-signal-square.svg-DXuF6sqG.js → interface-signal-square.svg-DnfzUw_B.js} +1 -1
- package/web/assets/{interface-time-clock-circle.svg-CWLe4mcs.js → interface-time-clock-circle.svg-BhxciNvX.js} +1 -1
- package/web/assets/{interface-time-hour-glass.svg-DXz9wBiv.js → interface-time-hour-glass.svg-DBzSaSHQ.js} +1 -1
- package/web/assets/{interface-users-multiple.svg-CXOw1J5l.js → interface-users-multiple.svg-BULh1giq.js} +1 -1
- package/web/assets/{interface-weather-moon.svg-B9H9xib3.js → interface-weather-moon.svg-D377V2os.js} +1 -1
- package/web/assets/{javascript-BY1URXQK.js → javascript-BTXoJ3Qy.js} +1 -1
- package/web/assets/{jsonMode-CxipJdzJ.js → jsonMode-D4a3Khim.js} +1 -1
- package/web/assets/{liquid-BbqI_ezD.js → liquid-DfO0R4Mk.js} +1 -1
- package/web/assets/{lspLanguageFeatures-BrbDxmfl.js → lspLanguageFeatures-BvPWuQFt.js} +1 -1
- package/web/assets/{mail-chat-bubble-square.svg-CAlBGAlW.js → mail-chat-bubble-square.svg-SK3IJ6QN.js} +1 -1
- package/web/assets/{mail-send-email-paper-airplane.svg-BoNc-FEY.js → mail-send-email-paper-airplane.svg-CJFA7B2E.js} +1 -1
- package/web/assets/{mail-send-envelope.svg-CnMOSib5.js → mail-send-envelope.svg-eO_-QNyE.js} +1 -1
- package/web/assets/{mdx-C4PcQ_oe.js → mdx-G_vwFSSe.js} +1 -1
- package/web/assets/{message-BWsbEnoG.js → message-CxPw04rG.js} +1 -1
- package/web/assets/{minus-BpDIA3FO.js → minus-Dp5ez1-1.js} +1 -1
- package/web/assets/{modal-DZYAJM4x.js → modal-CGT4d1lM.js} +1 -1
- package/web/assets/modal-D_N6NaGL.js +1 -0
- package/web/assets/{money-cashier-receipt.svg-DZQ9mrBZ.js → money-cashier-receipt.svg-EutWthy8.js} +1 -1
- package/web/assets/{money-currency-dollar-pay.svg-BCaw-GeJ.js → money-currency-dollar-pay.svg-MnLj-orw.js} +1 -1
- package/web/assets/{money-graph-arrow-increase.svg-DZXh-i2p.js → money-graph-arrow-increase.svg-CVCZ3TyJ.js} +1 -1
- package/web/assets/{money-graph-bar-chart-increase.svg-j_1tUlGM.js → money-graph-bar-chart-increase.svg-DiUmCuhj.js} +1 -1
- package/web/assets/{multiselect-CmjxW2sW.js → multiselect-C6uayHqj.js} +1 -1
- package/web/assets/{nature-ecology-leaf.svg-bajA57tM.js → nature-ecology-leaf.svg-ByaLs72Y.js} +1 -1
- package/web/assets/{password-W9W3PSE2.js → password-CxtOqagM.js} +1 -1
- package/web/assets/{phone-telephone.svg-DD5MGHqV.js → phone-telephone.svg-BKz6Jkwn.js} +1 -1
- package/web/assets/{plus-RuIcofM_.js → plus-BPT_7GGk.js} +1 -1
- package/web/assets/process.store-CMxuFjFg.js +1 -0
- package/web/assets/{programming-bug.svg-CjtqBc8Z.js → programming-bug.svg-BwSc8f0s.js} +1 -1
- package/web/assets/{programming-cloud.svg-BXQ_h3dK.js → programming-cloud.svg-CEIulNEp.js} +1 -1
- package/web/assets/{programming-computer-database-server.svg-D6TzOHsK.js → programming-computer-database-server.svg-Bc_F3NH0.js} +1 -1
- package/web/assets/{programming-computer-database.svg-Dmdh0lIx.js → programming-computer-database.svg-AYLkzJJE.js} +1 -1
- package/web/assets/{programming-module-four-layout.svg-CtGhpLhO.js → programming-module-four-layout.svg-DLpzPxNB.js} +1 -1
- package/web/assets/{programming-module-three.svg-hz_w8W7I.js → programming-module-three.svg-CcuZn8dz.js} +1 -1
- package/web/assets/{programming-module.svg-oDT_fQLn.js → programming-module.svg-BjbGpQeu.js} +1 -1
- package/web/assets/{programming-script-code.svg-CxHHTsaU.js → programming-script-code.svg-poyfodgC.js} +1 -1
- package/web/assets/{python-DH-AyaLR.js → python-DpMMlrWM.js} +1 -1
- package/web/assets/{razor-BUpT1gRX.js → razor-Ba_mg6U5.js} +1 -1
- package/web/assets/router-QZ5Z8Lh7.js +1 -0
- package/web/assets/settings-scope-D7umae8c.js +1 -0
- package/web/assets/{settings.store-Dj2nbYYG.js → settings.store-Dte8TdJb.js} +1 -1
- package/web/assets/{shopping-cart.svg-D1eK38-E.js → shopping-cart.svg-BX2KYVCw.js} +1 -1
- package/web/assets/{shopping-gift-present.svg-CxSfFNAS.js → shopping-gift-present.svg-BOl2vYLg.js} +1 -1
- package/web/assets/{shopping-shipping-box-parcel-package.svg-M5Njo2qJ.js → shopping-shipping-box-parcel-package.svg-gxfbOgnM.js} +1 -1
- package/web/assets/{src-CEK4EPEJ.js → src-D0LXRijj.js} +8 -8
- package/web/assets/{src-BTWvqafq.js → src-DWvNj-EG.js} +1 -1
- package/web/assets/{tabpanel-CWwOXePg.js → tabpanel-Bkt-f7d6.js} +1 -1
- package/web/assets/{tag-new-circle.svg-v_Go3fcT.js → tag-new-circle.svg-CTRMBJyc.js} +1 -1
- package/web/assets/{toggleswitch-C4A0_850.js → toggleswitch-DcDrXRKn.js} +1 -1
- package/web/assets/{travel-map-earth-globe.svg-Db2uRb3M.js → travel-map-earth-globe.svg-BPp4IapC.js} +1 -1
- package/web/assets/trpc-Dtgb4BMw.js +1 -0
- package/web/assets/{tsMode-CXB5XugS.js → tsMode-HfLOT-e5.js} +1 -1
- package/web/assets/{typescript-CHJkC9r6.js → typescript-DJAEsDnw.js} +1 -1
- package/web/assets/useAiChatDrawer-fzyQ-fyM.js +1 -0
- package/web/assets/useCloudOrg-BdpqnN19.css +1 -0
- package/web/assets/useCloudOrg-Bg7-icbW.js +97 -0
- package/web/assets/{useProjectCatalog-DDm0I6sX.js → useProjectCatalog-C95cDPIr.js} +1 -1
- package/web/assets/{xml-DB4EKWC9.js → xml-1frg7aol.js} +1 -1
- package/web/assets/{yaml-CqvLyuow.js → yaml-DXzDcMOS.js} +1 -1
- package/web/index.html +1 -1
- package/agent-manager-74WZPMVQ.js +0 -7
- package/chunk-YSJ6OJGN.js.map +0 -1
- package/src-MQWOIUB5.js.map +0 -1
- package/web/assets/AgentTerminalPage-A8GIX6dZ.css +0 -1
- package/web/assets/AgentTerminalPage-DE3GUoXn.js +0 -1
- package/web/assets/AiChatPage-BIJczUeS.js +0 -1
- package/web/assets/CloudAccountPage-MSWkg9Ke.js +0 -1
- package/web/assets/SettingsPage-CmtRn6VJ.js +0 -2063
- package/web/assets/WikiPage-DZLKTPi3.js +0 -1
- package/web/assets/auth-errors-BOjTx5eo.js +0 -1
- package/web/assets/interface-content-folder.svg-DjEfXjEF.js +0 -1
- package/web/assets/modal-Fe2dzlxp.js +0 -1
- package/web/assets/process.store-COWqXNUm.js +0 -1
- package/web/assets/router-BIJ4goD-.js +0 -1
- package/web/assets/settings-scope-DGzA7Be-.js +0 -1
- package/web/assets/trpc-DfyQNjZd.js +0 -1
- package/web/assets/useAiChatDrawer-Cuhlc2BI.js +0 -1
- package/web/assets/useCloudOrg-DYOv1Oak.js +0 -93
- package/web/assets/useCloudOrg-Dc6yRgro.css +0 -1
- /package/{agent-manager-74WZPMVQ.js.map → agent-manager-UUSQK6S2.js.map} +0 -0
- /package/web/assets/{abap-Q7FyqTce.js → abap-BvxTqXT2.js} +0 -0
- /package/web/assets/{apex-DBNDq2a_.js → apex-CIPCCkBr.js} +0 -0
- /package/web/assets/{azcli-BHW8ozYN.js → azcli-Rs0NIwkA.js} +0 -0
- /package/web/assets/{bat-BL-_3Nfo.js → bat-BxAhM6gR.js} +0 -0
- /package/web/assets/{bicep-DR2vgumW.js → bicep-oV_ehPcG.js} +0 -0
- /package/web/assets/{cameligo-BCjoEAZU.js → cameligo-CJLaUyQs.js} +0 -0
- /package/web/assets/{clojure-zw1Irdul.js → clojure-CjvMureh.js} +0 -0
- /package/web/assets/{coffee-BdEjkiiW.js → coffee-motWC5XQ.js} +0 -0
- /package/web/assets/{cpp-DKUlBIAG.js → cpp-DiWIfYNM.js} +0 -0
- /package/web/assets/{csharp-BsNC4zVq.js → csharp-GBlGBeai.js} +0 -0
- /package/web/assets/{csp-X4Chv5_9.js → csp-tSyK5eO1.js} +0 -0
- /package/web/assets/{css-Dh0Dlycs.js → css-BkFK9HG_.js} +0 -0
- /package/web/assets/{cypher-BL_DRjGX.js → cypher-Bcp8PXr2.js} +0 -0
- /package/web/assets/{dart-DaM4j487.js → dart-uz9Rxe0M.js} +0 -0
- /package/web/assets/{dockerfile-Df5WJXAt.js → dockerfile-iJizx3Zp.js} +0 -0
- /package/web/assets/{ecl-BjUo3uTs.js → ecl-BalaS36Z.js} +0 -0
- /package/web/assets/{elixir-DrLHYSxt.js → elixir-D6CLrLGy.js} +0 -0
- /package/web/assets/{flow9-DKiuX7rg.js → flow9-CFt3-vVY.js} +0 -0
- /package/web/assets/{fsharp-CAN78557.js → fsharp-DGa7raxa.js} +0 -0
- /package/web/assets/{go-DGjr9vA5.js → go-wlqInk4z.js} +0 -0
- /package/web/assets/{graphql-CcoU3Cj2.js → graphql-CP1guigM.js} +0 -0
- /package/web/assets/{hcl-udfyCcTm.js → hcl-CK0UkKD0.js} +0 -0
- /package/web/assets/{ini-BVbXatjh.js → ini-Ch56nrUj.js} +0 -0
- /package/web/assets/{java-Drpmh61g.js → java-C9_WIayl.js} +0 -0
- /package/web/assets/{julia-asFGk-ZV.js → julia-Can2_mhR.js} +0 -0
- /package/web/assets/{kotlin-HmkKz1TT.js → kotlin-DUvGf_BX.js} +0 -0
- /package/web/assets/{less-C5i2m2Dn.js → less-DoTbG-rc.js} +0 -0
- /package/web/assets/{lexon-B3Xh2kL6.js → lexon-BFMO2_kL.js} +0 -0
- /package/web/assets/{lua-DnOtgOOu.js → lua-CC-QtBo7.js} +0 -0
- /package/web/assets/{m3-Brj4HNKq.js → m3-C51MyH5Q.js} +0 -0
- /package/web/assets/{markdown-C3-4Ntpz.js → markdown-CDwLHhgZ.js} +0 -0
- /package/web/assets/{mips-vFQKPHzn.js → mips-YuxZtPGU.js} +0 -0
- /package/web/assets/{msdax-D5MYLS2R.js → msdax-CJjWhmte.js} +0 -0
- /package/web/assets/{mysql-CRQHhUON.js → mysql-B1riwLaD.js} +0 -0
- /package/web/assets/{objective-c-8cZEp0HR.js → objective-c-drXT-gmc.js} +0 -0
- /package/web/assets/{pascal-BPn_2jro.js → pascal-KUYgrN-Z.js} +0 -0
- /package/web/assets/{pascaligo-L7QnU_8B.js → pascaligo-DQmQJP-r.js} +0 -0
- /package/web/assets/{perl-B9zSPnp3.js → perl-09XLBmCS.js} +0 -0
- /package/web/assets/{pgsql-DiRJukCJ.js → pgsql-BFj9IFFu.js} +0 -0
- /package/web/assets/{php-Dv4g8Lhr.js → php-UnC5BqwY.js} +0 -0
- /package/web/assets/{pla-CKAoz6vp.js → pla-BFuYrPHG.js} +0 -0
- /package/web/assets/{postiats-B_PYgUTS.js → postiats-n4ft3jnO.js} +0 -0
- /package/web/assets/{powerquery-CAagSyo-.js → powerquery-D-s-oFSc.js} +0 -0
- /package/web/assets/{powershell-Cj9BzLhS.js → powershell-CTboTf93.js} +0 -0
- /package/web/assets/{protobuf-DTRY4FFo.js → protobuf-Dt62GuJP.js} +0 -0
- /package/web/assets/{pug-qADGmCS8.js → pug-CEFWkUzw.js} +0 -0
- /package/web/assets/{qsharp-xkZT0ZOm.js → qsharp-B0DUmgPZ.js} +0 -0
- /package/web/assets/{r-C7wfNC2Z.js → r-BVnF0c4T.js} +0 -0
- /package/web/assets/{redis-BoccK80D.js → redis-DLl3KQXi.js} +0 -0
- /package/web/assets/{redshift-EL8PjTts.js → redshift-cif1I7h1.js} +0 -0
- /package/web/assets/{restructuredtext-CA6rgR7C.js → restructuredtext-PNfql36m.js} +0 -0
- /package/web/assets/{ruby-BymrInur.js → ruby-j3jxDjUN.js} +0 -0
- /package/web/assets/{rust-BTbPL-fg.js → rust-BaIO-mAt.js} +0 -0
- /package/web/assets/{sb-BTkOICcb.js → sb-D8XNVNUQ.js} +0 -0
- /package/web/assets/{scala-CzaUHeQF.js → scala-DiK9Ps90.js} +0 -0
- /package/web/assets/{scheme-DZe2DRuY.js → scheme-DPKqXHmF.js} +0 -0
- /package/web/assets/{scss-D5oFt9cg.js → scss-DpMwfP2H.js} +0 -0
- /package/web/assets/{shell-DCkIF5jZ.js → shell-Dw9mKVg2.js} +0 -0
- /package/web/assets/{solidity-CfOO4gXi.js → solidity-DTxo-7fe.js} +0 -0
- /package/web/assets/{sophia-0-sROOHR.js → sophia-BjA3Q87y.js} +0 -0
- /package/web/assets/{sparql-Dzzr9hol.js → sparql-q1EpMUqb.js} +0 -0
- /package/web/assets/{sql-DbXHiHh_.js → sql-CjcQ4wIr.js} +0 -0
- /package/web/assets/{st-DEaYV8xH.js → st-AuOpxVl7.js} +0 -0
- /package/web/assets/{swift-BQItveav.js → swift-BUWnv6f7.js} +0 -0
- /package/web/assets/{systemverilog-nE6YfGlx.js → systemverilog-FhXn-3OZ.js} +0 -0
- /package/web/assets/{tcl-Do7Q0SM0.js → tcl-B0lqcTtS.js} +0 -0
- /package/web/assets/{twig-B2CLZN2j.js → twig-B0OeGPVa.js} +0 -0
- /package/web/assets/{typespec-r9R1AgVi.js → typespec-Bmh6VX11.js} +0 -0
- /package/web/assets/{vb-DGnD02qr.js → vb-bmgX6veu.js} +0 -0
- /package/web/assets/{wgsl-C2fD4koY.js → wgsl-FflLnFqI.js} +0 -0
|
@@ -60,9 +60,11 @@ import {
|
|
|
60
60
|
isVaultLocked,
|
|
61
61
|
keyRoot,
|
|
62
62
|
keyedFilePath,
|
|
63
|
+
killInstance,
|
|
63
64
|
legacyMachineFilePath,
|
|
64
65
|
legacyVaultRoot,
|
|
65
66
|
listCloudVaults,
|
|
67
|
+
listInstances,
|
|
66
68
|
listOrgVaults,
|
|
67
69
|
loadFromEnv,
|
|
68
70
|
localVaultRoot,
|
|
@@ -79,6 +81,7 @@ import {
|
|
|
79
81
|
readOrgNames,
|
|
80
82
|
refOfProjectAmong,
|
|
81
83
|
refsOfProjectId,
|
|
84
|
+
registerInstance,
|
|
82
85
|
registerProjectLoader,
|
|
83
86
|
rememberFile,
|
|
84
87
|
rememberOrgName,
|
|
@@ -93,11 +96,12 @@ import {
|
|
|
93
96
|
stableEncryptValue,
|
|
94
97
|
stampOrgCloud,
|
|
95
98
|
traefikManager,
|
|
99
|
+
unregisterInstance,
|
|
96
100
|
vaultFor,
|
|
97
101
|
vaultIdOfRoot,
|
|
98
102
|
wasRead,
|
|
99
103
|
writeEntity
|
|
100
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-Z3KAXI7V.js";
|
|
101
105
|
|
|
102
106
|
// ../server/src/create-server.ts
|
|
103
107
|
import express from "express";
|
|
@@ -2555,1440 +2559,1448 @@ var settingsManager = new SettingsManager();
|
|
|
2555
2559
|
import { isAbsolute as isAbsolute3 } from "path";
|
|
2556
2560
|
|
|
2557
2561
|
// ../server/src/services/workspace-cwd.ts
|
|
2558
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2562
|
+
import { isAbsolute as isAbsolute2, join as join10, dirname as dirname5 } from "path";
|
|
2563
|
+
|
|
2564
|
+
// ../server/src/services/conversation-store.ts
|
|
2565
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2566
|
+
import { readFile as readFile8, writeFile as writeFile8, rename as rename8, mkdir as mkdir8, chmod as chmod7 } from "fs/promises";
|
|
2567
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
2568
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
2569
|
+
var FILENAME4 = "conversations.private.json";
|
|
2570
|
+
async function localProjectId() {
|
|
2571
|
+
const projects = await projectStore.list().catch(() => []);
|
|
2572
|
+
return projects.find((p) => !p.orgId)?.id ?? null;
|
|
2563
2573
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2574
|
+
var ConversationStore = class {
|
|
2575
|
+
conversations = /* @__PURE__ */ new Map();
|
|
2576
|
+
/** Which vault each conversation was read from, until its project is known. */
|
|
2577
|
+
roots = /* @__PURE__ */ new Map();
|
|
2578
|
+
/**
|
|
2579
|
+
* Les conversations qu'on n'a pas su déchiffrer, par coffre, sous leur forme
|
|
2580
|
+
* de disque.
|
|
2581
|
+
*
|
|
2582
|
+
* Une entrée illisible était simplement ignorée. Tant qu'aucune de ses
|
|
2583
|
+
* voisines ne se chargeait, le fichier n'était jamais réécrit et la donnée
|
|
2584
|
+
* survivait par accident ; dès qu'une seule s'ouvrait, la sauvegarde suivante
|
|
2585
|
+
* réécrivait le fichier avec elle seule — et effaçait les autres. Elles sont
|
|
2586
|
+
* donc gardées ici, telles quelles, et réémises à l'identique.
|
|
2587
|
+
*/
|
|
2588
|
+
unreadable = /* @__PURE__ */ new Map();
|
|
2589
|
+
loaded = false;
|
|
2590
|
+
events = new EventEmitter2().setMaxListeners(0);
|
|
2591
|
+
saveQueue = Promise.resolve();
|
|
2592
|
+
snapshotConversation(conv) {
|
|
2593
|
+
return structuredClone(conv);
|
|
2575
2594
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2595
|
+
/**
|
|
2596
|
+
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
2597
|
+
* root, but a launch directory owning its own key keeps both together.
|
|
2598
|
+
*/
|
|
2599
|
+
getFilePath() {
|
|
2600
|
+
return keyedFilePath(FILENAME4);
|
|
2580
2601
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
if (!raw || raw === ".") return base;
|
|
2585
|
-
if (raw.startsWith("~") || raw.startsWith("$") || isAbsolute2(raw)) return raw;
|
|
2586
|
-
return join9(base, raw);
|
|
2587
|
-
}
|
|
2588
|
-
function localVaultRootParent() {
|
|
2589
|
-
return dirname4(localVaultRoot());
|
|
2590
|
-
}
|
|
2591
|
-
async function resolveAgentCwd(target) {
|
|
2592
|
-
if (target.serviceId) {
|
|
2593
|
-
const service = await serviceStore.get(target.serviceId);
|
|
2594
|
-
if (service) {
|
|
2595
|
-
const projects = await projectStore.list();
|
|
2596
|
-
const owner = projects.find((p) => p.serviceIds.includes(service.id));
|
|
2597
|
-
const root = owner ? await projectRoot(owner) : null;
|
|
2598
|
-
if (!root) return void 0;
|
|
2599
|
-
return anchor(root, (target.override ?? service.cwd ?? "").trim());
|
|
2600
|
-
}
|
|
2602
|
+
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
2603
|
+
getLegacyFilePath() {
|
|
2604
|
+
return legacyMachineFilePath(FILENAME4);
|
|
2601
2605
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
+
/**
|
|
2607
|
+
* Read the machine file, falling back to the launch directory of old.
|
|
2608
|
+
*
|
|
2609
|
+
* The legacy file is left in place: the next save writes the new location,
|
|
2610
|
+
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
2611
|
+
*/
|
|
2612
|
+
async readFromDisk() {
|
|
2613
|
+
try {
|
|
2614
|
+
return await readFile8(await this.getFilePath(), "utf-8");
|
|
2615
|
+
} catch {
|
|
2616
|
+
return readFile8(this.getLegacyFilePath(), "utf-8");
|
|
2617
|
+
}
|
|
2606
2618
|
}
|
|
2607
|
-
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
// ../server/src/trpc/routers/service.ts
|
|
2611
|
-
async function resolveEffectiveAgentId(service) {
|
|
2612
|
-
const known = /* @__PURE__ */ new Set([
|
|
2613
|
-
...agentManager.listAgents().map((a) => a.config.id),
|
|
2614
|
-
...(await agentStore.list()).map((a) => a.id)
|
|
2615
|
-
]);
|
|
2616
|
-
if (service.agentId && known.has(service.agentId)) return service.agentId;
|
|
2617
|
-
const projects = await projectStore.list();
|
|
2618
|
-
const project = projects.find((p) => p.serviceIds.includes(service.id));
|
|
2619
|
-
if (project?.activeEnvironmentId) {
|
|
2620
|
-
const activeEnv = await environmentStore.get(project.activeEnvironmentId);
|
|
2621
|
-
if (activeEnv?.agentId && known.has(activeEnv.agentId)) return activeEnv.agentId;
|
|
2619
|
+
async ensureDir() {
|
|
2620
|
+
await mkdir8(dirname4(await this.getFilePath()), { recursive: true });
|
|
2622
2621
|
}
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
async function resolveServiceVariables(service) {
|
|
2626
|
-
const projects = await projectStore.list();
|
|
2627
|
-
const project = projects.find((p) => p.serviceIds.includes(service.id));
|
|
2628
|
-
let resolvedService = service;
|
|
2629
|
-
const anchored = await resolveWorkspaceCwd({ serviceId: service.id });
|
|
2630
|
-
if (anchored !== (service.cwd ?? "").trim() && isAbsolute3(anchored)) {
|
|
2631
|
-
resolvedService = { ...resolvedService, resolvedCwd: anchored };
|
|
2622
|
+
encryptMessages(messages, vault) {
|
|
2623
|
+
return encryptValue(JSON.stringify(messages), vault);
|
|
2632
2624
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
const allParsers = [...nativeParsers, ...settings.logParsers || []];
|
|
2637
|
-
const resolvedParsers = service.parserIds.map((id) => allParsers.find((p) => p.id === id)).filter((p) => p !== void 0);
|
|
2638
|
-
resolvedService = { ...resolvedService, resolvedLogParsers: resolvedParsers };
|
|
2639
|
-
}
|
|
2640
|
-
} catch (err) {
|
|
2641
|
-
console.error("[service-router] Failed to resolve parsers for service", service.id + ":", err.message);
|
|
2625
|
+
decryptMessages(encrypted, vault) {
|
|
2626
|
+
const json = decryptValue(encrypted, vault);
|
|
2627
|
+
return JSON.parse(json);
|
|
2642
2628
|
}
|
|
2643
|
-
|
|
2644
|
-
|
|
2629
|
+
/**
|
|
2630
|
+
* Refuser d'écrire dans le coffre d'une organisation sans nommer sa clé.
|
|
2631
|
+
*
|
|
2632
|
+
* `getEncryptionKey(undefined)` rend la clé machine, et c'est le bon défaut
|
|
2633
|
+
* pour le coffre local. Appliqué à un fichier rangé sous `orgs/<org>/`, il
|
|
2634
|
+
* produit un chiffré que ce coffre n'emploie pas — lisible par personne à la
|
|
2635
|
+
* relecture, et impossible à distinguer d'une corruption.
|
|
2636
|
+
*
|
|
2637
|
+
* Le cas a été observé plusieurs fois sans qu'on parvienne à nommer le
|
|
2638
|
+
* chemin fautif : les garde-fous en place traitent le coffre fermé, le
|
|
2639
|
+
* coffre mal résolu et la copie périmée, aucun ne couvrait « coffre non
|
|
2640
|
+
* précisé ». Ces écritures-là n'ont jamais fonctionné — elles échouaient en
|
|
2641
|
+
* silence, ce qui est pire. On les saute désormais en le disant, avec la
|
|
2642
|
+
* pile d'appels qui manquait pour remonter au chemin fautif.
|
|
2643
|
+
*/
|
|
2644
|
+
isVaultNamed(root, vault) {
|
|
2645
|
+
const orgId = orgIdOfRoot(root);
|
|
2646
|
+
if (!orgId || vault) return true;
|
|
2647
|
+
console.error(
|
|
2648
|
+
`[conversation-store] \xC9criture ignor\xE9e dans le coffre de l'organisation ${orgId} (${root}) : aucune cl\xE9 d'organisation nomm\xE9e, le contenu serait chiffr\xE9 par la cl\xE9 machine et illisible \xE0 la relecture.
|
|
2649
|
+
${new Error("trace").stack}`
|
|
2650
|
+
);
|
|
2651
|
+
return false;
|
|
2645
2652
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2653
|
+
toDisk(conv, vault) {
|
|
2654
|
+
return {
|
|
2655
|
+
id: conv.id,
|
|
2656
|
+
projectId: conv.projectId,
|
|
2657
|
+
title: conv.title,
|
|
2658
|
+
encryptedMessages: this.encryptMessages(conv.messages, vault),
|
|
2659
|
+
isCli: conv.isCli,
|
|
2660
|
+
runner: conv.runner,
|
|
2661
|
+
providerId: conv.providerId,
|
|
2662
|
+
codexThreadId: conv.codexThreadId,
|
|
2663
|
+
contextUsage: conv.contextUsage,
|
|
2664
|
+
workflowId: conv.workflowId,
|
|
2665
|
+
workflowType: conv.workflowType,
|
|
2666
|
+
kanbanBoardId: conv.kanbanBoardId,
|
|
2667
|
+
kanbanCardId: conv.kanbanCardId,
|
|
2668
|
+
archived: conv.archived,
|
|
2669
|
+
createdAt: conv.createdAt,
|
|
2670
|
+
updatedAt: conv.updatedAt
|
|
2654
2671
|
};
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2672
|
+
}
|
|
2673
|
+
fromDisk(disk, vault) {
|
|
2674
|
+
const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
|
|
2675
|
+
return {
|
|
2676
|
+
id: disk.id,
|
|
2677
|
+
projectId: disk.projectId,
|
|
2678
|
+
title: disk.title,
|
|
2679
|
+
messages: this.decryptMessages(disk.encryptedMessages, vault),
|
|
2680
|
+
isCli: disk.isCli ?? disk.isClaudeCode ?? false,
|
|
2681
|
+
runner: disk.runner,
|
|
2682
|
+
providerId: disk.providerId,
|
|
2683
|
+
codexThreadId: disk.codexThreadId,
|
|
2684
|
+
contextUsage: disk.contextUsage,
|
|
2685
|
+
workflowId: disk.workflowId ?? disk.scenarioId,
|
|
2686
|
+
workflowType,
|
|
2687
|
+
kanbanBoardId: disk.kanbanBoardId,
|
|
2688
|
+
kanbanCardId: disk.kanbanCardId,
|
|
2689
|
+
archived: disk.archived,
|
|
2690
|
+
createdAt: disk.createdAt,
|
|
2691
|
+
updatedAt: disk.updatedAt
|
|
2666
2692
|
};
|
|
2667
|
-
} catch (err) {
|
|
2668
|
-
console.error("[service-router] Failed to resolve variables for service", service.id + ":", err.message);
|
|
2669
2693
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
}),
|
|
2683
|
-
get: protectedProcedure.meta({ openapi: { method: "GET", path: "/services/{id}", tags: ["services"], summary: "Get a service by ID", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.any()).query(async ({ input, ctx }) => {
|
|
2684
|
-
requireServiceAction(ctx, input.id, "service:read");
|
|
2685
|
-
const service = await serviceStore.get(input.id);
|
|
2686
|
-
if (!service) {
|
|
2687
|
-
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
2694
|
+
/**
|
|
2695
|
+
* Read every project vault, then the homes older versions used.
|
|
2696
|
+
*
|
|
2697
|
+
* A conversation found in a legacy file is attributed to the local project of
|
|
2698
|
+
* that directory — which is what it was about, back when one directory meant
|
|
2699
|
+
* one project. With no local project to point at, it is left unattributed
|
|
2700
|
+
* rather than filed under a project picked at random.
|
|
2701
|
+
*/
|
|
2702
|
+
async load() {
|
|
2703
|
+
if (this.loaded) return;
|
|
2704
|
+
if (!getEncryptionKey()) {
|
|
2705
|
+
return;
|
|
2688
2706
|
}
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
port: z4.number().int().min(1).max(65535)
|
|
2705
|
-
})).max(20).optional(),
|
|
2706
|
-
openapiUrl: z4.string().max(2048).optional(),
|
|
2707
|
-
git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).optional(),
|
|
2708
|
-
meta: z4.record(z4.string().max(256), z4.string().max(2e3)).optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
|
|
2709
|
-
runner: z4.enum(["native", "docker"]).optional().default("native"),
|
|
2710
|
-
dockerConfig: DockerConfigInput.optional(),
|
|
2711
|
-
healthCheck: HealthCheckSchema.optional(),
|
|
2712
|
-
autoRestart: z4.boolean().optional().default(false),
|
|
2713
|
-
restartStrategy: RestartStrategySchema.optional(),
|
|
2714
|
-
maxRestarts: z4.number().int().nonnegative().optional(),
|
|
2715
|
-
restartBackoffMs: z4.number().int().positive().optional().default(1e3),
|
|
2716
|
-
orgId: z4.string().max(128).nullable().optional(),
|
|
2717
|
-
shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
|
|
2718
|
-
parserIds: z4.array(z4.string().max(128)).max(50).optional()
|
|
2719
|
-
})
|
|
2720
|
-
).mutation(async ({ input }) => {
|
|
2721
|
-
if (input.agentId) {
|
|
2722
|
-
const status = agentManager.getStatus(input.agentId);
|
|
2723
|
-
if (!status) {
|
|
2724
|
-
throw new TRPCError4({
|
|
2725
|
-
code: "NOT_FOUND",
|
|
2726
|
-
message: `Agent ${input.agentId} not found`
|
|
2727
|
-
});
|
|
2707
|
+
this.loaded = true;
|
|
2708
|
+
this.unreadable.clear();
|
|
2709
|
+
await ensureProjectsLoaded();
|
|
2710
|
+
const vaults = await allVaultRoots();
|
|
2711
|
+
const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME4)];
|
|
2712
|
+
const sources = [
|
|
2713
|
+
...legacyPaths.map((path) => ({ path, legacy: true })),
|
|
2714
|
+
...vaults.map((root) => ({ path: join9(root, FILENAME4), legacy: false }))
|
|
2715
|
+
];
|
|
2716
|
+
for (const { path, legacy } of sources) {
|
|
2717
|
+
let data;
|
|
2718
|
+
try {
|
|
2719
|
+
data = JSON.parse(await readFile8(path, "utf-8"));
|
|
2720
|
+
} catch {
|
|
2721
|
+
continue;
|
|
2728
2722
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const service = await serviceStore.create({
|
|
2734
|
-
...input,
|
|
2735
|
-
agentId: input.agentId
|
|
2736
|
-
});
|
|
2737
|
-
await auditService.log({ userId: "local", action: "create", entityType: "service", entityId: service.id, metadata: { name: input.name } });
|
|
2738
|
-
return service;
|
|
2739
|
-
}),
|
|
2740
|
-
update: protectedProcedure.meta({ openapi: { method: "PATCH", path: "/services/{id}", tags: ["services"], summary: "Update a service", protect: true } }).output(z4.any()).input(
|
|
2741
|
-
z4.object({
|
|
2742
|
-
id: z4.string().max(128),
|
|
2743
|
-
agentId: z4.string().max(128).nullable().optional(),
|
|
2744
|
-
// null = clear (inherit from env)
|
|
2745
|
-
name: z4.string().min(1).max(200).optional(),
|
|
2746
|
-
commands: z4.array(ServiceCommandSchema).max(50).optional(),
|
|
2747
|
-
cwd: z4.string().max(4096).nullable().optional(),
|
|
2748
|
-
ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
|
|
2749
|
-
groups: z4.array(z4.string().max(200)).max(20).optional(),
|
|
2750
|
-
description: z4.string().max(2e3).optional(),
|
|
2751
|
-
docsPath: z4.string().max(4096).optional(),
|
|
2752
|
-
url: z4.string().max(2048).optional(),
|
|
2753
|
-
localUrls: z4.array(z4.object({
|
|
2754
|
-
url: z4.string().max(253),
|
|
2755
|
-
port: z4.number().int().min(1).max(65535)
|
|
2756
|
-
})).max(20).optional(),
|
|
2757
|
-
openapiUrl: z4.string().max(2048).optional(),
|
|
2758
|
-
git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).nullable().optional(),
|
|
2759
|
-
meta: z4.record(z4.string().max(256), z4.string().max(2e3)).nullable().optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
|
|
2760
|
-
runner: z4.enum(["native", "docker"]).optional(),
|
|
2761
|
-
dockerConfig: DockerConfigInput.optional(),
|
|
2762
|
-
healthCheck: HealthCheckSchema.nullable().optional(),
|
|
2763
|
-
autoRestart: z4.boolean().optional(),
|
|
2764
|
-
clearLogsOnStart: z4.boolean().optional(),
|
|
2765
|
-
restartStrategy: RestartStrategySchema.optional(),
|
|
2766
|
-
maxRestarts: z4.number().int().nonnegative().optional(),
|
|
2767
|
-
restartBackoffMs: z4.number().int().positive().optional(),
|
|
2768
|
-
orgId: z4.string().max(128).nullable().optional(),
|
|
2769
|
-
shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
|
|
2770
|
-
parserIds: z4.array(z4.string().max(128)).max(50).optional(),
|
|
2771
|
-
logSources: z4.array(LogSourceConfigSchema).max(20).optional()
|
|
2772
|
-
})
|
|
2773
|
-
).mutation(async ({ input }) => {
|
|
2774
|
-
const existing = await serviceStore.get(input.id);
|
|
2775
|
-
if (!existing) {
|
|
2776
|
-
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
2777
|
-
}
|
|
2778
|
-
let stoppedForAgentChange = false;
|
|
2779
|
-
if (input.agentId !== void 0 && input.agentId !== existing.agentId && existing.agentId) {
|
|
2780
|
-
const oldAgentStatus = agentManager.getStatus(existing.agentId);
|
|
2781
|
-
if (oldAgentStatus?.connected) {
|
|
2723
|
+
const inherited = legacy ? await localProjectId() : null;
|
|
2724
|
+
const root = dirname4(path);
|
|
2725
|
+
await rememberFile(path);
|
|
2726
|
+
for (const item of data) {
|
|
2782
2727
|
try {
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
stoppedForAgentChange = true;
|
|
2788
|
-
}
|
|
2728
|
+
const conv = this.fromDisk(item, vaultIdOfRoot(root));
|
|
2729
|
+
if (!conv.projectId && inherited) conv.projectId = inherited;
|
|
2730
|
+
this.conversations.set(item.id, conv);
|
|
2731
|
+
this.roots.set(item.id, root);
|
|
2789
2732
|
} catch {
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
const newAgentStatus = agentManager.getStatus(input.agentId);
|
|
2794
|
-
if (!newAgentStatus) {
|
|
2795
|
-
throw new TRPCError4({
|
|
2796
|
-
code: "NOT_FOUND",
|
|
2797
|
-
message: `Agent ${input.agentId} not found`
|
|
2798
|
-
});
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
const service = await serviceStore.update(input);
|
|
2803
|
-
if (!service) {
|
|
2804
|
-
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
2805
|
-
}
|
|
2806
|
-
if (input.name && input.name !== existing.name) {
|
|
2807
|
-
await environmentStore.renameByReference(input.id, input.name);
|
|
2808
|
-
}
|
|
2809
|
-
if (service.agentId) {
|
|
2810
|
-
const agentStatus = agentManager.getStatus(service.agentId);
|
|
2811
|
-
if (agentStatus?.connected) {
|
|
2812
|
-
try {
|
|
2813
|
-
const resolvedService = await resolveServiceVariables(service);
|
|
2814
|
-
await agentManager.agentMutation(service.agentId, "process.updateConfig", {
|
|
2815
|
-
serviceId: service.id,
|
|
2816
|
-
config: resolvedService
|
|
2817
|
-
});
|
|
2818
|
-
} catch (err) {
|
|
2819
|
-
console.debug("[service-router] Failed to propagate config to agent", service.agentId + ":", err.message);
|
|
2733
|
+
const bucket = this.unreadable.get(root) ?? [];
|
|
2734
|
+
bucket.push(item);
|
|
2735
|
+
this.unreadable.set(root, bucket);
|
|
2820
2736
|
}
|
|
2821
2737
|
}
|
|
2822
2738
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Oublier ce qui a été lu, pour tout relire sous la clé courante.
|
|
2742
|
+
*
|
|
2743
|
+
* Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
|
|
2744
|
+
* re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
|
|
2745
|
+
* qui n'existe plus. La laisser en place n'affichait pas seulement des
|
|
2746
|
+
* comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
|
|
2747
|
+
* par-dessus le nouveau, et défaisait l'adoption.
|
|
2748
|
+
*/
|
|
2749
|
+
resetCache() {
|
|
2750
|
+
this.conversations.clear();
|
|
2751
|
+
this.roots.clear();
|
|
2752
|
+
this.unreadable.clear();
|
|
2753
|
+
this.loaded = false;
|
|
2754
|
+
this.events.emit("changed", { type: "reloaded" });
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Combien de conversations ce coffre garde sans savoir les lire, par
|
|
2758
|
+
* organisation. La machine elle-même est rangée sous `null`.
|
|
2759
|
+
*/
|
|
2760
|
+
async unreadableByVault() {
|
|
2761
|
+
await this.load();
|
|
2762
|
+
const out = /* @__PURE__ */ new Map();
|
|
2763
|
+
for (const [root, items] of this.unreadable) {
|
|
2764
|
+
const vault = vaultIdOfRoot(root) ?? null;
|
|
2765
|
+
out.set(vault, (out.get(vault) ?? 0) + items.length);
|
|
2830
2766
|
}
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2767
|
+
return out;
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Write each vault that holds — or held — a conversation.
|
|
2771
|
+
*
|
|
2772
|
+
* A vault emptied by this save is still written, as an empty list: a
|
|
2773
|
+
* conversation that found its project must not survive in the file it was
|
|
2774
|
+
* read from.
|
|
2775
|
+
*/
|
|
2776
|
+
async saveInternal() {
|
|
2777
|
+
if (!getEncryptionKey()) return;
|
|
2778
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
2779
|
+
for (const root of this.roots.values()) byRoot.set(root, []);
|
|
2780
|
+
for (const conv of this.conversations.values()) {
|
|
2781
|
+
const root = await this.rootFor(conv);
|
|
2782
|
+
this.roots.set(conv.id, root);
|
|
2783
|
+
const bucket = byRoot.get(root) ?? [];
|
|
2784
|
+
const vault = vaultIdOfRoot(root);
|
|
2785
|
+
if (!this.isVaultNamed(root, vault)) continue;
|
|
2786
|
+
bucket.push(this.toDisk(conv, vault));
|
|
2787
|
+
byRoot.set(root, bucket);
|
|
2838
2788
|
}
|
|
2839
|
-
const
|
|
2840
|
-
|
|
2841
|
-
throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
|
|
2789
|
+
for (const [root, items] of this.unreadable) {
|
|
2790
|
+
byRoot.set(root, [...items, ...byRoot.get(root) ?? []]);
|
|
2842
2791
|
}
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
remote: service.git?.remote,
|
|
2848
|
-
checkReachability: input.checkReachability
|
|
2849
|
-
});
|
|
2850
|
-
const { exists: exists4, actualRemote, unreachable, resolvedCwd, unresolvedVars } = agentResult;
|
|
2851
|
-
if (unresolvedVars.length > 0) {
|
|
2852
|
-
return { exists: false, canClone: false, unresolvedVars, resolvedCwd, gitMismatch: null };
|
|
2792
|
+
const stale = /* @__PURE__ */ new Set();
|
|
2793
|
+
for (const root of byRoot.keys()) {
|
|
2794
|
+
const path = join9(root, FILENAME4);
|
|
2795
|
+
if (wasRead(path) && await hasChangedSinceRead(path)) stale.add(root);
|
|
2853
2796
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
} else if (unreachable) {
|
|
2862
|
-
gitMismatch = { actual: actualRemote, configured: service.git.remote, unreachable: true };
|
|
2797
|
+
for (const [root, data] of byRoot) {
|
|
2798
|
+
const filePath = join9(root, FILENAME4);
|
|
2799
|
+
if (stale.has(root)) {
|
|
2800
|
+
console.warn(
|
|
2801
|
+
`[conversation-store] ${filePath} a chang\xE9 depuis sa lecture \u2014 \xE9criture ignor\xE9e pour ne pas \xE9craser une r\xE9paration faite en dehors du serveur.`
|
|
2802
|
+
);
|
|
2803
|
+
continue;
|
|
2863
2804
|
}
|
|
2805
|
+
await mkdir8(root, { recursive: true });
|
|
2806
|
+
const tmpPath = `${filePath}.tmp`;
|
|
2807
|
+
await writeFile8(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
2808
|
+
await rename8(tmpPath, filePath);
|
|
2809
|
+
await chmod7(filePath, 384);
|
|
2810
|
+
await rememberFile(filePath);
|
|
2864
2811
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
}
|
|
2904
|
-
return
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* The vault a conversation is written to: its project's.
|
|
2815
|
+
*
|
|
2816
|
+
* One that names no project stays where it was read, falling back to the
|
|
2817
|
+
* key's own directory — never lost for want of an owner.
|
|
2818
|
+
*/
|
|
2819
|
+
async rootFor(conv) {
|
|
2820
|
+
const known = this.roots.get(conv.id) ?? null;
|
|
2821
|
+
if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);
|
|
2822
|
+
return known ?? dirname4(await keyedFilePath(FILENAME4));
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Le projet d'une conversation, organisation comprise.
|
|
2826
|
+
*
|
|
2827
|
+
* Ce que la conversation ne dit pas d'elle-même : elle ne nomme que l'id.
|
|
2828
|
+
* Le coffre d'où elle vient fournit l'organisation, et c'est ce couple qu'il
|
|
2829
|
+
* faut pour retrouver le répertoire de travail du projet.
|
|
2830
|
+
*/
|
|
2831
|
+
async projectRefOf(conv) {
|
|
2832
|
+
if (!conv.projectId) return null;
|
|
2833
|
+
const known = this.roots.get(conv.id) ?? null;
|
|
2834
|
+
const ref = refOfProjectAmong(conv.projectId, known);
|
|
2835
|
+
return { id: ref.id, orgId: ref.orgId ?? null };
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Réécrire chaque coffre, en déplaçant ce qui appartient désormais ailleurs.
|
|
2839
|
+
*
|
|
2840
|
+
* Appelé quand un projet a changé de coffre : ses conversations sont écrites
|
|
2841
|
+
* dans le nouveau, et le fichier d'où elles viennent est réécrit sans elles.
|
|
2842
|
+
*/
|
|
2843
|
+
async relocateAll() {
|
|
2844
|
+
await this.load();
|
|
2845
|
+
await this.save();
|
|
2846
|
+
}
|
|
2847
|
+
async save() {
|
|
2848
|
+
const write = this.saveQueue.then(() => this.saveInternal());
|
|
2849
|
+
this.saveQueue = write.catch(() => {
|
|
2850
|
+
});
|
|
2851
|
+
return write;
|
|
2852
|
+
}
|
|
2853
|
+
async list() {
|
|
2854
|
+
await this.load();
|
|
2855
|
+
return Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
2856
|
+
}
|
|
2857
|
+
/**
|
|
2858
|
+
* Les conversations d'un projet.
|
|
2859
|
+
*
|
|
2860
|
+
* Celle qui ne nomme aucun projet — écrite avant le champ, ou qu'une
|
|
2861
|
+
* migration n'a pas su attribuer — n'est rendue que dans les projets de son
|
|
2862
|
+
* propre coffre : elle reste atteignable là où elle a été écrite, sans se
|
|
2863
|
+
* déverser dans toutes les organisations de la machine.
|
|
2864
|
+
*/
|
|
2865
|
+
async listForProject(ref) {
|
|
2866
|
+
const belongs = await this.matcherForProject(ref);
|
|
2867
|
+
return (await this.list()).filter(belongs);
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Le test d'appartenance à un projet, préparé une fois.
|
|
2871
|
+
*
|
|
2872
|
+
* Le flux temps réel en a besoin autant que la liste : une conversation qui
|
|
2873
|
+
* arrive par événement doit passer le même filtre, sans quoi elle se
|
|
2874
|
+
* réinvite dans un projet qui ne l'a jamais demandée.
|
|
2875
|
+
*/
|
|
2876
|
+
async matcherForProject(ref) {
|
|
2877
|
+
await this.load();
|
|
2878
|
+
let projectRoot2 = null;
|
|
2931
2879
|
try {
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
});
|
|
2936
|
-
if (!res.ok) {
|
|
2937
|
-
return { success: false, error: `HTTP ${res.status} ${res.statusText}` };
|
|
2938
|
-
}
|
|
2939
|
-
const json = await res.json();
|
|
2940
|
-
if (json.error) {
|
|
2941
|
-
return { success: false, error: json.error.message ?? "Agent returned an error" };
|
|
2942
|
-
}
|
|
2943
|
-
return { success: true, uptime: json.result?.data?.uptime };
|
|
2944
|
-
} catch (err) {
|
|
2945
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
2946
|
-
return { success: false, error: msg };
|
|
2880
|
+
projectRoot2 = ref.orgId === void 0 ? rootOfProjectAmong(ref.id, null) : rootOfProjectRef({ id: ref.id, orgId: ref.orgId ?? null });
|
|
2881
|
+
} catch {
|
|
2882
|
+
projectRoot2 = null;
|
|
2947
2883
|
}
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
const
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
).mutation(async ({ input }) => {
|
|
2980
|
-
try {
|
|
2981
|
-
const { id, ...fields } = input;
|
|
2982
|
-
const config = await agentManager.update(id, fields);
|
|
2983
|
-
return { ...config, passphrase: "***" };
|
|
2984
|
-
} catch (err) {
|
|
2985
|
-
if (err instanceof Error) {
|
|
2986
|
-
if (err.message.includes("not found")) {
|
|
2987
|
-
throw new TRPCError5({ code: "NOT_FOUND", message: err.message });
|
|
2988
|
-
}
|
|
2989
|
-
if (err.message.includes("Cannot modify")) {
|
|
2990
|
-
throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
throw err;
|
|
2884
|
+
return (conv) => {
|
|
2885
|
+
if (conv.projectId) return conv.projectId === ref.id;
|
|
2886
|
+
if (!projectRoot2) return true;
|
|
2887
|
+
return this.roots.get(conv.id) === projectRoot2;
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
async get(id) {
|
|
2891
|
+
await this.load();
|
|
2892
|
+
return this.conversations.get(id) ?? null;
|
|
2893
|
+
}
|
|
2894
|
+
async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId, projectId, orgId) {
|
|
2895
|
+
await this.load();
|
|
2896
|
+
const now = Date.now();
|
|
2897
|
+
const conv = {
|
|
2898
|
+
id: randomUUID3(),
|
|
2899
|
+
projectId,
|
|
2900
|
+
title: title ?? "New conversation",
|
|
2901
|
+
messages: [],
|
|
2902
|
+
isCli,
|
|
2903
|
+
runner,
|
|
2904
|
+
providerId,
|
|
2905
|
+
workflowId,
|
|
2906
|
+
workflowType,
|
|
2907
|
+
kanbanBoardId,
|
|
2908
|
+
kanbanCardId,
|
|
2909
|
+
createdAt: now,
|
|
2910
|
+
updatedAt: now
|
|
2911
|
+
};
|
|
2912
|
+
this.conversations.set(conv.id, conv);
|
|
2913
|
+
if (projectId && orgId !== void 0) {
|
|
2914
|
+
this.roots.set(conv.id, vaultFor({ id: projectId, orgId }).root);
|
|
2994
2915
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
2916
|
+
await this.save();
|
|
2917
|
+
this.events.emit("changed", { type: "created", conversation: this.snapshotConversation(conv) });
|
|
2918
|
+
return conv;
|
|
2919
|
+
}
|
|
2920
|
+
async update(id, patch, options) {
|
|
2921
|
+
await this.load();
|
|
2922
|
+
const conv = this.conversations.get(id);
|
|
2923
|
+
if (!conv) return null;
|
|
2924
|
+
if (patch.title !== void 0) conv.title = patch.title;
|
|
2925
|
+
if (patch.messages !== void 0) conv.messages = patch.messages;
|
|
2926
|
+
if (patch.contextUsage !== void 0) conv.contextUsage = patch.contextUsage;
|
|
2927
|
+
if (patch.workflowId !== void 0) conv.workflowId = patch.workflowId;
|
|
2928
|
+
if (patch.archived !== void 0) conv.archived = patch.archived;
|
|
2929
|
+
if (patch.codexThreadId !== void 0) conv.codexThreadId = patch.codexThreadId;
|
|
2930
|
+
if (options?.touch !== false) conv.updatedAt = Date.now();
|
|
2931
|
+
await this.save();
|
|
2932
|
+
if (!options?.silent) {
|
|
2933
|
+
this.events.emit("changed", { type: "updated", conversation: this.snapshotConversation(conv) });
|
|
3002
2934
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
2935
|
+
return conv;
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Synchronously update the last assistant message content in memory, then schedule a save.
|
|
2939
|
+
* Used by activeSessionRegistry.persistNow() to avoid the async get() → .then() race.
|
|
2940
|
+
*/
|
|
2941
|
+
updateLastAssistantContent(id, content, parts) {
|
|
2942
|
+
const conv = this.conversations.get(id);
|
|
2943
|
+
if (!conv) return;
|
|
2944
|
+
const lastMsg = conv.messages[conv.messages.length - 1];
|
|
2945
|
+
if (!lastMsg || lastMsg.role !== "assistant") return;
|
|
2946
|
+
lastMsg.content = content;
|
|
2947
|
+
if (parts !== void 0) lastMsg.parts = parts;
|
|
2948
|
+
conv.updatedAt = Date.now();
|
|
2949
|
+
this.save().catch(() => {
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
async listByKanbanBoardId(boardId) {
|
|
2953
|
+
await this.load();
|
|
2954
|
+
return Array.from(this.conversations.values()).filter((c) => c.kanbanBoardId === boardId);
|
|
2955
|
+
}
|
|
2956
|
+
async listByKanbanCardId(cardId) {
|
|
2957
|
+
await this.load();
|
|
2958
|
+
return Array.from(this.conversations.values()).filter((c) => c.kanbanCardId === cardId);
|
|
2959
|
+
}
|
|
2960
|
+
async delete(id) {
|
|
2961
|
+
await this.load();
|
|
2962
|
+
const existed = this.conversations.delete(id);
|
|
2963
|
+
await this.save();
|
|
2964
|
+
if (existed) {
|
|
2965
|
+
this.events.emit("changed", { type: "deleted", id });
|
|
3017
2966
|
}
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
2967
|
+
}
|
|
2968
|
+
onChanged(handler) {
|
|
2969
|
+
this.events.on("changed", handler);
|
|
2970
|
+
}
|
|
2971
|
+
offChanged(handler) {
|
|
2972
|
+
this.events.off("changed", handler);
|
|
2973
|
+
}
|
|
2974
|
+
};
|
|
2975
|
+
var conversationStore = new ConversationStore();
|
|
2976
|
+
|
|
2977
|
+
// ../server/src/services/workspace-cwd.ts
|
|
2978
|
+
async function projectRoot(ref) {
|
|
2979
|
+
const project = await projectStore.get(ref);
|
|
2980
|
+
if (!project?.orgId) return null;
|
|
2981
|
+
return projectPathStore.get(project);
|
|
2982
|
+
}
|
|
2983
|
+
async function resolveWorkspaceCwd(target) {
|
|
2984
|
+
const launchDir = localVaultRootParent();
|
|
2985
|
+
if (target.serviceId) {
|
|
2986
|
+
const service = await serviceStore.get(target.serviceId);
|
|
2987
|
+
if (service) {
|
|
2988
|
+
const raw = (target.override ?? service.cwd ?? "").trim();
|
|
2989
|
+
const projects = await projectStore.list();
|
|
2990
|
+
const owner = projects.find((p) => p.serviceIds.includes(service.id));
|
|
2991
|
+
const root = owner ? await projectRoot(owner) : null;
|
|
2992
|
+
return root ? anchor(root, raw) : raw || ".";
|
|
3023
2993
|
}
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
2994
|
+
}
|
|
2995
|
+
if (target.projectId) {
|
|
2996
|
+
const root = await projectRoot({ id: target.projectId, orgId: target.orgId });
|
|
2997
|
+
const raw = (target.override ?? "").trim();
|
|
2998
|
+
return root ? anchor(root, raw) : raw || launchDir;
|
|
2999
|
+
}
|
|
3000
|
+
return anchor(launchDir, (target.override ?? "").trim());
|
|
3001
|
+
}
|
|
3002
|
+
function anchor(base, raw) {
|
|
3003
|
+
if (!raw || raw === ".") return base;
|
|
3004
|
+
if (raw.startsWith("~") || raw.startsWith("$") || isAbsolute2(raw)) return raw;
|
|
3005
|
+
return join10(base, raw);
|
|
3006
|
+
}
|
|
3007
|
+
function localVaultRootParent() {
|
|
3008
|
+
return dirname5(localVaultRoot());
|
|
3009
|
+
}
|
|
3010
|
+
async function resolveAgentCwd(target) {
|
|
3011
|
+
if (target.serviceId) {
|
|
3012
|
+
const service = await serviceStore.get(target.serviceId);
|
|
3013
|
+
if (service) {
|
|
3014
|
+
const projects = await projectStore.list();
|
|
3015
|
+
const owner = projects.find((p) => p.serviceIds.includes(service.id));
|
|
3016
|
+
const root = owner ? await projectRoot(owner) : null;
|
|
3017
|
+
if (!root) return void 0;
|
|
3018
|
+
return anchor(root, (target.override ?? service.cwd ?? "").trim());
|
|
3047
3019
|
}
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3020
|
+
}
|
|
3021
|
+
if (target.projectId) {
|
|
3022
|
+
const root = await projectRoot({ id: target.projectId, orgId: target.orgId });
|
|
3023
|
+
if (!root) return void 0;
|
|
3024
|
+
return anchor(root, (target.override ?? "").trim());
|
|
3025
|
+
}
|
|
3026
|
+
return void 0;
|
|
3027
|
+
}
|
|
3028
|
+
async function resolveConversationCwd(conversationId) {
|
|
3029
|
+
const conv = await conversationStore.get(conversationId);
|
|
3030
|
+
const ref = conv ? await conversationStore.projectRefOf(conv) : null;
|
|
3031
|
+
if (!ref) return resolveAgentCwd({});
|
|
3032
|
+
return resolveAgentCwd({ projectId: ref.id, orgId: ref.orgId });
|
|
3033
|
+
}
|
|
3050
3034
|
|
|
3051
|
-
// ../server/src/trpc/routers/
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
if (
|
|
3063
|
-
|
|
3064
|
-
|
|
3035
|
+
// ../server/src/trpc/routers/service.ts
|
|
3036
|
+
async function resolveEffectiveAgentId(service) {
|
|
3037
|
+
const known = /* @__PURE__ */ new Set([
|
|
3038
|
+
...agentManager.listAgents().map((a) => a.config.id),
|
|
3039
|
+
...(await agentStore.list()).map((a) => a.id)
|
|
3040
|
+
]);
|
|
3041
|
+
if (service.agentId && known.has(service.agentId)) return service.agentId;
|
|
3042
|
+
const projects = await projectStore.list();
|
|
3043
|
+
const project = projects.find((p) => p.serviceIds.includes(service.id));
|
|
3044
|
+
if (project?.activeEnvironmentId) {
|
|
3045
|
+
const activeEnv = await environmentStore.get(project.activeEnvironmentId);
|
|
3046
|
+
if (activeEnv?.agentId && known.has(activeEnv.agentId)) return activeEnv.agentId;
|
|
3047
|
+
}
|
|
3048
|
+
return agentManager.getDefaultAgentId();
|
|
3049
|
+
}
|
|
3050
|
+
async function resolveServiceVariables(service) {
|
|
3051
|
+
const projects = await projectStore.list();
|
|
3052
|
+
const project = projects.find((p) => p.serviceIds.includes(service.id));
|
|
3053
|
+
let resolvedService = service;
|
|
3054
|
+
const anchored = await resolveWorkspaceCwd({ serviceId: service.id });
|
|
3055
|
+
if (anchored !== (service.cwd ?? "").trim() && isAbsolute3(anchored)) {
|
|
3056
|
+
resolvedService = { ...resolvedService, resolvedCwd: anchored };
|
|
3057
|
+
}
|
|
3058
|
+
try {
|
|
3059
|
+
if (service.parserIds && service.parserIds.length > 0) {
|
|
3060
|
+
const settings = await settingsManager.getSettings();
|
|
3061
|
+
const allParsers = [...nativeParsers, ...settings.logParsers || []];
|
|
3062
|
+
const resolvedParsers = service.parserIds.map((id) => allParsers.find((p) => p.id === id)).filter((p) => p !== void 0);
|
|
3063
|
+
resolvedService = { ...resolvedService, resolvedLogParsers: resolvedParsers };
|
|
3065
3064
|
}
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3065
|
+
} catch (err) {
|
|
3066
|
+
console.error("[service-router] Failed to resolve parsers for service", service.id + ":", err.message);
|
|
3067
|
+
}
|
|
3068
|
+
if (!project || !project.activeEnvironmentId) {
|
|
3069
|
+
return resolvedService;
|
|
3070
|
+
}
|
|
3068
3071
|
try {
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3072
|
+
const resolved = await environmentResolver.resolveForService({
|
|
3073
|
+
serviceId: service.id,
|
|
3074
|
+
activeEnvironmentId: project.activeEnvironmentId
|
|
3075
|
+
});
|
|
3076
|
+
const serviceBuiltins = {
|
|
3077
|
+
HOST: env.HOST,
|
|
3078
|
+
HOST_LAN: "0.0.0.0"
|
|
3079
|
+
};
|
|
3080
|
+
const interpolatedEnv = {};
|
|
3081
|
+
for (const [key, value] of Object.entries(resolved.env)) {
|
|
3082
|
+
interpolatedEnv[key] = value.replace(
|
|
3083
|
+
/\{\{service\.([^}]+)\}\}/g,
|
|
3084
|
+
(match, k) => serviceBuiltins[k] !== void 0 ? serviceBuiltins[k] : match
|
|
3085
|
+
);
|
|
3077
3086
|
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3087
|
+
resolvedService = {
|
|
3088
|
+
...resolvedService,
|
|
3089
|
+
env: interpolatedEnv,
|
|
3090
|
+
envSecrets: [...resolved.envSecrets]
|
|
3091
|
+
};
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
console.error("[service-router] Failed to resolve variables for service", service.id + ":", err.message);
|
|
3080
3094
|
}
|
|
3095
|
+
return resolvedService;
|
|
3081
3096
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3097
|
+
var DockerConfigInput = DockerConfigSchema;
|
|
3098
|
+
var serviceRouter = router({
|
|
3099
|
+
list: protectedProcedure.meta({ openapi: { method: "GET", path: "/services", tags: ["services"], summary: "List all services", protect: true } }).output(z4.any()).query(async ({ ctx }) => {
|
|
3100
|
+
const all = await serviceStore.list();
|
|
3101
|
+
const allowedIds = getAllowedServiceIds(ctx);
|
|
3102
|
+
const filtered = allowedIds ? all.filter((s) => allowedIds.has(s.id)) : all;
|
|
3103
|
+
return Promise.all(filtered.map(async (s) => ({
|
|
3104
|
+
...s,
|
|
3105
|
+
effectiveAgentId: await resolveEffectiveAgentId(s)
|
|
3106
|
+
})));
|
|
3107
|
+
}),
|
|
3108
|
+
get: protectedProcedure.meta({ openapi: { method: "GET", path: "/services/{id}", tags: ["services"], summary: "Get a service by ID", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.any()).query(async ({ input, ctx }) => {
|
|
3109
|
+
requireServiceAction(ctx, input.id, "service:read");
|
|
3110
|
+
const service = await serviceStore.get(input.id);
|
|
3111
|
+
if (!service) {
|
|
3112
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
3096
3113
|
}
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3114
|
+
return service;
|
|
3115
|
+
}),
|
|
3116
|
+
create: protectedProcedure.meta({ openapi: { method: "POST", path: "/services", tags: ["services"], summary: "Create a new service", protect: true } }).output(z4.any()).input(
|
|
3117
|
+
z4.object({
|
|
3118
|
+
agentId: z4.string().max(128).optional(),
|
|
3119
|
+
name: z4.string().min(1).max(200),
|
|
3120
|
+
commands: z4.array(ServiceCommandSchema).max(50).optional(),
|
|
3121
|
+
cwd: z4.string().max(4096).optional(),
|
|
3122
|
+
ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
|
|
3123
|
+
groups: z4.array(z4.string().max(200)).max(20).optional(),
|
|
3124
|
+
description: z4.string().max(2e3).optional(),
|
|
3125
|
+
docsPath: z4.string().max(4096).optional(),
|
|
3126
|
+
url: z4.string().max(2048).optional(),
|
|
3127
|
+
localUrls: z4.array(z4.object({
|
|
3128
|
+
url: z4.string().max(253),
|
|
3129
|
+
port: z4.number().int().min(1).max(65535)
|
|
3130
|
+
})).max(20).optional(),
|
|
3131
|
+
openapiUrl: z4.string().max(2048).optional(),
|
|
3132
|
+
git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).optional(),
|
|
3133
|
+
meta: z4.record(z4.string().max(256), z4.string().max(2e3)).optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
|
|
3134
|
+
runner: z4.enum(["native", "docker"]).optional().default("native"),
|
|
3135
|
+
dockerConfig: DockerConfigInput.optional(),
|
|
3136
|
+
healthCheck: HealthCheckSchema.optional(),
|
|
3137
|
+
autoRestart: z4.boolean().optional().default(false),
|
|
3138
|
+
restartStrategy: RestartStrategySchema.optional(),
|
|
3139
|
+
maxRestarts: z4.number().int().nonnegative().optional(),
|
|
3140
|
+
restartBackoffMs: z4.number().int().positive().optional().default(1e3),
|
|
3141
|
+
orgId: z4.string().max(128).nullable().optional(),
|
|
3142
|
+
shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
|
|
3143
|
+
parserIds: z4.array(z4.string().max(128)).max(50).optional()
|
|
3144
|
+
})
|
|
3145
|
+
).mutation(async ({ input }) => {
|
|
3146
|
+
if (input.agentId) {
|
|
3147
|
+
const status = agentManager.getStatus(input.agentId);
|
|
3148
|
+
if (!status) {
|
|
3149
|
+
throw new TRPCError4({
|
|
3150
|
+
code: "NOT_FOUND",
|
|
3151
|
+
message: `Agent ${input.agentId} not found`
|
|
3106
3152
|
});
|
|
3107
3153
|
}
|
|
3108
3154
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3155
|
+
if (input.runner === "docker") {
|
|
3156
|
+
throw new TRPCError4({ code: "BAD_REQUEST", message: 'Docker runner is not yet supported. Use "native" runner.' });
|
|
3157
|
+
}
|
|
3158
|
+
const service = await serviceStore.create({
|
|
3159
|
+
...input,
|
|
3160
|
+
agentId: input.agentId
|
|
3161
|
+
});
|
|
3162
|
+
await auditService.log({ userId: "local", action: "create", entityType: "service", entityId: service.id, metadata: { name: input.name } });
|
|
3163
|
+
return service;
|
|
3164
|
+
}),
|
|
3165
|
+
update: protectedProcedure.meta({ openapi: { method: "PATCH", path: "/services/{id}", tags: ["services"], summary: "Update a service", protect: true } }).output(z4.any()).input(
|
|
3166
|
+
z4.object({
|
|
3167
|
+
id: z4.string().max(128),
|
|
3168
|
+
agentId: z4.string().max(128).nullable().optional(),
|
|
3169
|
+
// null = clear (inherit from env)
|
|
3170
|
+
name: z4.string().min(1).max(200).optional(),
|
|
3171
|
+
commands: z4.array(ServiceCommandSchema).max(50).optional(),
|
|
3172
|
+
cwd: z4.string().max(4096).nullable().optional(),
|
|
3173
|
+
ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
|
|
3174
|
+
groups: z4.array(z4.string().max(200)).max(20).optional(),
|
|
3175
|
+
description: z4.string().max(2e3).optional(),
|
|
3176
|
+
docsPath: z4.string().max(4096).optional(),
|
|
3177
|
+
url: z4.string().max(2048).optional(),
|
|
3178
|
+
localUrls: z4.array(z4.object({
|
|
3179
|
+
url: z4.string().max(253),
|
|
3180
|
+
port: z4.number().int().min(1).max(65535)
|
|
3181
|
+
})).max(20).optional(),
|
|
3182
|
+
openapiUrl: z4.string().max(2048).optional(),
|
|
3183
|
+
git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).nullable().optional(),
|
|
3184
|
+
meta: z4.record(z4.string().max(256), z4.string().max(2e3)).nullable().optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
|
|
3185
|
+
runner: z4.enum(["native", "docker"]).optional(),
|
|
3186
|
+
dockerConfig: DockerConfigInput.optional(),
|
|
3187
|
+
healthCheck: HealthCheckSchema.nullable().optional(),
|
|
3188
|
+
autoRestart: z4.boolean().optional(),
|
|
3189
|
+
clearLogsOnStart: z4.boolean().optional(),
|
|
3190
|
+
restartStrategy: RestartStrategySchema.optional(),
|
|
3191
|
+
maxRestarts: z4.number().int().nonnegative().optional(),
|
|
3192
|
+
restartBackoffMs: z4.number().int().positive().optional(),
|
|
3193
|
+
orgId: z4.string().max(128).nullable().optional(),
|
|
3194
|
+
shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
|
|
3195
|
+
parserIds: z4.array(z4.string().max(128)).max(50).optional(),
|
|
3196
|
+
logSources: z4.array(LogSourceConfigSchema).max(20).optional()
|
|
3197
|
+
})
|
|
3198
|
+
).mutation(async ({ input }) => {
|
|
3199
|
+
const existing = await serviceStore.get(input.id);
|
|
3200
|
+
if (!existing) {
|
|
3201
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
3202
|
+
}
|
|
3203
|
+
let stoppedForAgentChange = false;
|
|
3204
|
+
if (input.agentId !== void 0 && input.agentId !== existing.agentId && existing.agentId) {
|
|
3205
|
+
const oldAgentStatus = agentManager.getStatus(existing.agentId);
|
|
3206
|
+
if (oldAgentStatus?.connected) {
|
|
3120
3207
|
try {
|
|
3121
|
-
const processes = await agentManager.agentQuery(
|
|
3122
|
-
|
|
3123
|
-
|
|
3208
|
+
const processes = await agentManager.agentQuery(existing.agentId, "process.list");
|
|
3209
|
+
const proc = processes.find((p) => p.serviceId === input.id);
|
|
3210
|
+
if (proc && (proc.state === "running" || proc.state === "starting")) {
|
|
3211
|
+
await agentManager.agentMutation(existing.agentId, "process.stop", { id: input.id }, AGENT_STOP_FETCH_TIMEOUT);
|
|
3212
|
+
stoppedForAgentChange = true;
|
|
3124
3213
|
}
|
|
3125
3214
|
} catch {
|
|
3126
3215
|
}
|
|
3127
|
-
})
|
|
3128
|
-
);
|
|
3129
|
-
return results;
|
|
3130
|
-
}),
|
|
3131
|
-
/** Get a single process from a specific agent */
|
|
3132
|
-
get: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{id}", tags: ["processes"], summary: "Get a process by agent and ID", protect: true } }).input(z6.object({ agentId: z6.string().max(128), id: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
|
|
3133
|
-
requireServiceAction(ctx, input.id, "service:read");
|
|
3134
|
-
const status = agentManager.getStatus(input.agentId);
|
|
3135
|
-
if (!status) {
|
|
3136
|
-
throw new TRPCError6({ code: "NOT_FOUND", message: `Agent ${input.agentId} not found` });
|
|
3137
|
-
}
|
|
3138
|
-
const proc = await agentManager.agentQuery(input.agentId, "process.get", { id: input.id });
|
|
3139
|
-
return { ...proc, agentId: input.agentId };
|
|
3140
|
-
}),
|
|
3141
|
-
/** Start a process on an agent.
|
|
3142
|
-
* Lazy deploy: the agent only learns about a service when it is first started
|
|
3143
|
-
* (or restarted). This avoids pushing configs for every service at agent boot,
|
|
3144
|
-
* which would be wasteful when many services are never started.
|
|
3145
|
-
* The deploy call is idempotent — creates the process if missing, updates config if it exists. */
|
|
3146
|
-
start: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/start", tags: ["processes"], summary: "Start a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
|
|
3147
|
-
requireServiceAction(ctx, input.processId, "service:start");
|
|
3148
|
-
const service = await serviceStore.get(input.processId);
|
|
3149
|
-
const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
|
|
3150
|
-
if (!effectiveAgentId) {
|
|
3151
|
-
throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
|
|
3152
|
-
}
|
|
3153
|
-
if (service) {
|
|
3154
|
-
try {
|
|
3155
|
-
const resolved = await resolveServiceVariables(service);
|
|
3156
|
-
await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
|
|
3157
|
-
} catch (err) {
|
|
3158
|
-
console.debug("[process-router] Failed to deploy before start for", input.processId + ":", err.message);
|
|
3159
3216
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3217
|
+
if (input.agentId) {
|
|
3218
|
+
const newAgentStatus = agentManager.getStatus(input.agentId);
|
|
3219
|
+
if (!newAgentStatus) {
|
|
3220
|
+
throw new TRPCError4({
|
|
3221
|
+
code: "NOT_FOUND",
|
|
3222
|
+
message: `Agent ${input.agentId} not found`
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3167
3225
|
}
|
|
3168
3226
|
}
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
} catch (err) {
|
|
3173
|
-
const err_ = err;
|
|
3174
|
-
const isFetchError = err_?.cause?.code === "ECONNREFUSED" || err_?.message === "fetch failed" || err_?.name === "TypeError";
|
|
3175
|
-
throw new TRPCError6({
|
|
3176
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
3177
|
-
message: isFetchError ? `Cannot reach agent \u2014 make sure the agent is running` : err_?.message ?? "Failed to start process"
|
|
3178
|
-
});
|
|
3179
|
-
}
|
|
3180
|
-
await auditService.log({ userId: "local", action: "start", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
|
|
3181
|
-
return { ...result, agentId: effectiveAgentId };
|
|
3182
|
-
}),
|
|
3183
|
-
/** Stop a process on an agent */
|
|
3184
|
-
stop: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/stop", tags: ["processes"], summary: "Stop a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
|
|
3185
|
-
requireServiceAction(ctx, input.processId, "service:stop");
|
|
3186
|
-
const result = await agentManager.agentMutation(input.agentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
|
|
3187
|
-
await auditService.log({ userId: "local", action: "stop", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
|
|
3188
|
-
return { ...result, agentId: input.agentId };
|
|
3189
|
-
}),
|
|
3190
|
-
/** Restart a process on an agent.
|
|
3191
|
-
* Sends stop → deploy (latest resolved config) → start so that
|
|
3192
|
-
* config/variable changes take effect. Same lazy deploy pattern as start. */
|
|
3193
|
-
restart: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/restart", tags: ["processes"], summary: "Restart a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
|
|
3194
|
-
requireServiceAction(ctx, input.processId, "service:restart");
|
|
3195
|
-
const service = await serviceStore.get(input.processId);
|
|
3196
|
-
const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
|
|
3197
|
-
if (!effectiveAgentId) {
|
|
3198
|
-
throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
|
|
3199
|
-
}
|
|
3200
|
-
try {
|
|
3201
|
-
await agentManager.agentMutation(effectiveAgentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
|
|
3202
|
-
} catch (err) {
|
|
3203
|
-
console.debug("[process-router] Stop before restart ignored (process not found on agent?):", err.message);
|
|
3227
|
+
const service = await serviceStore.update(input);
|
|
3228
|
+
if (!service) {
|
|
3229
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
3204
3230
|
}
|
|
3205
|
-
if (
|
|
3206
|
-
|
|
3207
|
-
const resolved = await resolveServiceVariables(service);
|
|
3208
|
-
await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
|
|
3209
|
-
} catch (err) {
|
|
3210
|
-
console.debug("[process-router] Failed to deploy before restart for", input.processId + ":", err.message);
|
|
3211
|
-
}
|
|
3231
|
+
if (input.name && input.name !== existing.name) {
|
|
3232
|
+
await environmentStore.renameByReference(input.id, input.name);
|
|
3212
3233
|
}
|
|
3213
|
-
if (service
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3234
|
+
if (service.agentId) {
|
|
3235
|
+
const agentStatus = agentManager.getStatus(service.agentId);
|
|
3236
|
+
if (agentStatus?.connected) {
|
|
3237
|
+
try {
|
|
3238
|
+
const resolvedService = await resolveServiceVariables(service);
|
|
3239
|
+
await agentManager.agentMutation(service.agentId, "process.updateConfig", {
|
|
3240
|
+
serviceId: service.id,
|
|
3241
|
+
config: resolvedService
|
|
3242
|
+
});
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
console.debug("[service-router] Failed to propagate config to agent", service.agentId + ":", err.message);
|
|
3245
|
+
}
|
|
3219
3246
|
}
|
|
3220
3247
|
}
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
return { ...result, agentId: effectiveAgentId };
|
|
3224
|
-
}),
|
|
3225
|
-
/** Get buffered logs for a process from an agent */
|
|
3226
|
-
logs: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{processId}/logs", tags: ["processes"], summary: "Get buffered logs for a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
|
|
3227
|
-
requireServiceAction(ctx, input.processId, "logs:read");
|
|
3228
|
-
return agentManager.agentQuery(input.agentId, "process.logs", { id: input.processId });
|
|
3248
|
+
await auditService.log({ userId: "local", action: "update", entityType: "service", entityId: input.id });
|
|
3249
|
+
return { ...service, effectiveAgentId: await resolveEffectiveAgentId(service), stoppedForAgentChange };
|
|
3229
3250
|
}),
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3251
|
+
delete: protectedProcedure.meta({ openapi: { method: "DELETE", path: "/services/{id}", tags: ["services"], summary: "Delete a service", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.object({ success: z4.literal(true) })).mutation(async ({ input }) => {
|
|
3252
|
+
const deleted = await serviceStore.delete(input.id);
|
|
3253
|
+
if (!deleted) {
|
|
3254
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
3255
|
+
}
|
|
3256
|
+
await auditService.log({ userId: "local", action: "delete", entityType: "service", entityId: input.id });
|
|
3234
3257
|
return { success: true };
|
|
3235
3258
|
}),
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
shortcut: ServiceShortcutSchema
|
|
3241
|
-
})).mutation(async ({ input }) => {
|
|
3242
|
-
const service = await serviceStore.get(input.processId);
|
|
3243
|
-
if (service) {
|
|
3244
|
-
try {
|
|
3245
|
-
const resolved = await resolveServiceVariables(service);
|
|
3246
|
-
await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
|
|
3247
|
-
} catch (err) {
|
|
3248
|
-
console.debug("[process-router] Failed to deploy before executeShortcut for", input.processId + ":", err.message);
|
|
3249
|
-
}
|
|
3259
|
+
checkCwd: protectedProcedure.input(z4.object({ id: z4.string().max(128), checkReachability: z4.boolean().optional() })).query(async ({ input }) => {
|
|
3260
|
+
const service = await serviceStore.get(input.id);
|
|
3261
|
+
if (!service) {
|
|
3262
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
3250
3263
|
}
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
"
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
entityId: input.processId,
|
|
3262
|
-
metadata: { agentId: input.agentId, shortcutId: input.shortcut.id, shortcutLabel: input.shortcut.label }
|
|
3264
|
+
const effectiveAgentId = await resolveEffectiveAgentId(service);
|
|
3265
|
+
if (!effectiveAgentId) {
|
|
3266
|
+
throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
|
|
3267
|
+
}
|
|
3268
|
+
const agentResult = await agentManager.agentQuery(effectiveAgentId, "git.checkCwd", {
|
|
3269
|
+
// Anchored on the project, so the check answers about the directory the
|
|
3270
|
+
// service will actually run in.
|
|
3271
|
+
cwd: await resolveWorkspaceCwd({ serviceId: service.id }),
|
|
3272
|
+
remote: service.git?.remote,
|
|
3273
|
+
checkReachability: input.checkReachability
|
|
3263
3274
|
});
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
const resolved = await resolveServiceVariables(service);
|
|
3278
|
-
await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
|
|
3279
|
-
} catch (err) {
|
|
3280
|
-
console.debug("[process-router] Failed to deploy before executeAdHocCommand for", input.processId + ":", err.message);
|
|
3275
|
+
const { exists: exists4, actualRemote, unreachable, resolvedCwd, unresolvedVars } = agentResult;
|
|
3276
|
+
if (unresolvedVars.length > 0) {
|
|
3277
|
+
return { exists: false, canClone: false, unresolvedVars, resolvedCwd, gitMismatch: null };
|
|
3278
|
+
}
|
|
3279
|
+
const canClone = !!service.git?.remote && (!exists4 || actualRemote === null);
|
|
3280
|
+
let gitMismatch = null;
|
|
3281
|
+
if (exists4 && service.git?.remote && !canClone) {
|
|
3282
|
+
if (actualRemote === null) {
|
|
3283
|
+
gitMismatch = { actual: null, configured: service.git.remote };
|
|
3284
|
+
} else if (actualRemote !== service.git.remote) {
|
|
3285
|
+
gitMismatch = { actual: actualRemote, configured: service.git.remote };
|
|
3286
|
+
} else if (unreachable) {
|
|
3287
|
+
gitMismatch = { actual: actualRemote, configured: service.git.remote, unreachable: true };
|
|
3281
3288
|
}
|
|
3282
3289
|
}
|
|
3283
|
-
|
|
3284
|
-
input.agentId,
|
|
3285
|
-
"process.executeAdHocCommand",
|
|
3286
|
-
{
|
|
3287
|
-
id: input.processId,
|
|
3288
|
-
command: input.command,
|
|
3289
|
-
// An explicit cwd wins; otherwise the service's directory, anchored on
|
|
3290
|
-
// its project rather than on the server's launch directory.
|
|
3291
|
-
cwd: input.cwd ?? await resolveAgentCwd({ serviceId: input.processId }),
|
|
3292
|
-
commandId: input.commandId
|
|
3293
|
-
},
|
|
3294
|
-
AGENT_LONG_FETCH_TIMEOUT
|
|
3295
|
-
);
|
|
3296
|
-
await auditService.log({
|
|
3297
|
-
userId: "local",
|
|
3298
|
-
action: "executeAdHocCommand",
|
|
3299
|
-
entityType: "process",
|
|
3300
|
-
entityId: input.processId,
|
|
3301
|
-
metadata: { agentId: input.agentId, command: input.command }
|
|
3302
|
-
});
|
|
3303
|
-
return result;
|
|
3290
|
+
return { exists: exists4, canClone, unresolvedVars: [], resolvedCwd, gitMismatch };
|
|
3304
3291
|
}),
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3292
|
+
cloneRepository: protectedProcedure.input(z4.object({ id: z4.string().max(128) })).mutation(async ({ input }) => {
|
|
3293
|
+
const service = await serviceStore.get(input.id);
|
|
3294
|
+
if (!service) {
|
|
3295
|
+
throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
|
|
3296
|
+
}
|
|
3297
|
+
if (!service.git?.remote) {
|
|
3298
|
+
throw new TRPCError4({ code: "BAD_REQUEST", message: "Service has no git remote configured" });
|
|
3299
|
+
}
|
|
3300
|
+
const effectiveAgentId = await resolveEffectiveAgentId(service);
|
|
3301
|
+
if (!effectiveAgentId) {
|
|
3302
|
+
throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
|
|
3303
|
+
}
|
|
3304
|
+
await agentManager.agentMutation(effectiveAgentId, "git.clone", {
|
|
3305
|
+
remote: service.git.remote,
|
|
3306
|
+
// Clones WRITE: landing this in the launch directory instead of the
|
|
3307
|
+
// project's is how a repository ends up in the wrong place.
|
|
3308
|
+
cwd: await resolveWorkspaceCwd({ serviceId: service.id })
|
|
3309
|
+
}, 5 * 60 * 1e3);
|
|
3312
3310
|
return { success: true };
|
|
3311
|
+
})
|
|
3312
|
+
});
|
|
3313
|
+
|
|
3314
|
+
// ../server/src/trpc/routers/agent.ts
|
|
3315
|
+
import { z as z5 } from "zod";
|
|
3316
|
+
import { TRPCError as TRPCError5 } from "@trpc/server";
|
|
3317
|
+
var SUBSCRIPTION_QUEUE_MAX = LOG_BUFFER_SIZE;
|
|
3318
|
+
var agentRouter = router({
|
|
3319
|
+
/** List all registered agents with their connection status */
|
|
3320
|
+
list: protectedProcedure.query(() => {
|
|
3321
|
+
return agentManager.listAgents();
|
|
3313
3322
|
}),
|
|
3314
|
-
/**
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3323
|
+
/** Get a single agent's config and status */
|
|
3324
|
+
get: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).query(({ input }) => {
|
|
3325
|
+
const agent = agentManager.getAgent(input.id);
|
|
3326
|
+
if (!agent) {
|
|
3327
|
+
throw new TRPCError5({ code: "NOT_FOUND", message: `Agent ${input.id} not found` });
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
config: { ...agent.config, passphrase: "***" },
|
|
3331
|
+
status: agent.status
|
|
3332
|
+
};
|
|
3322
3333
|
}),
|
|
3323
|
-
/**
|
|
3324
|
-
|
|
3325
|
-
|
|
3334
|
+
/** Test connectivity to an agent given url + passphrase */
|
|
3335
|
+
testConnection: protectedProcedure.input(
|
|
3336
|
+
z5.object({
|
|
3337
|
+
url: z5.string().url().max(2048),
|
|
3338
|
+
passphrase: z5.string().min(1).max(1024).optional(),
|
|
3339
|
+
agentId: z5.string().max(128).optional()
|
|
3340
|
+
})
|
|
3341
|
+
).mutation(async ({ input }) => {
|
|
3342
|
+
let passphrase = input.passphrase;
|
|
3343
|
+
let targetUrl = input.url;
|
|
3344
|
+
if (!passphrase && input.agentId) {
|
|
3345
|
+
const agent = agentManager.getAgentConfig(input.agentId);
|
|
3346
|
+
if (!agent) {
|
|
3347
|
+
throw new TRPCError5({ code: "NOT_FOUND", message: "Agent not found" });
|
|
3348
|
+
}
|
|
3349
|
+
passphrase = agent.passphrase;
|
|
3350
|
+
targetUrl = agent.url;
|
|
3351
|
+
}
|
|
3352
|
+
if (!passphrase) {
|
|
3353
|
+
throw new TRPCError5({ code: "BAD_REQUEST", message: "Passphrase is required" });
|
|
3354
|
+
}
|
|
3355
|
+
const url = new URL("/api/trpc/health.check", targetUrl);
|
|
3356
|
+
try {
|
|
3357
|
+
const res = await fetch(url.toString(), {
|
|
3358
|
+
headers: { "Authorization": `Bearer ${passphrase}`, "Content-Type": "application/json" },
|
|
3359
|
+
signal: AbortSignal.timeout(5e3)
|
|
3360
|
+
});
|
|
3361
|
+
if (!res.ok) {
|
|
3362
|
+
return { success: false, error: `HTTP ${res.status} ${res.statusText}` };
|
|
3363
|
+
}
|
|
3364
|
+
const json = await res.json();
|
|
3365
|
+
if (json.error) {
|
|
3366
|
+
return { success: false, error: json.error.message ?? "Agent returned an error" };
|
|
3367
|
+
}
|
|
3368
|
+
return { success: true, uptime: json.result?.data?.uptime };
|
|
3369
|
+
} catch (err) {
|
|
3370
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
3371
|
+
return { success: false, error: msg };
|
|
3372
|
+
}
|
|
3326
3373
|
}),
|
|
3327
|
-
/**
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3374
|
+
/** Register a new agent */
|
|
3375
|
+
register: protectedProcedure.input(
|
|
3376
|
+
z5.object({
|
|
3377
|
+
name: z5.string().min(1).max(100),
|
|
3378
|
+
url: z5.string().url().max(2048),
|
|
3379
|
+
passphrase: z5.string().min(1).max(1024),
|
|
3380
|
+
// À quelle organisation rattacher l'agent. Absent = la machine, ce que
|
|
3381
|
+
// font l'agent local et tout client antérieur à ce champ.
|
|
3382
|
+
orgId: z5.string().min(1).max(128).nullable().optional()
|
|
3383
|
+
})
|
|
3384
|
+
).mutation(async ({ input }) => {
|
|
3385
|
+
const config = await agentManager.register(
|
|
3386
|
+
input.name,
|
|
3387
|
+
input.url,
|
|
3388
|
+
input.passphrase,
|
|
3389
|
+
input.orgId ?? null
|
|
3390
|
+
);
|
|
3391
|
+
return { ...config, passphrase: "***" };
|
|
3335
3392
|
}),
|
|
3336
|
-
/**
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3393
|
+
/** Update an existing agent */
|
|
3394
|
+
update: protectedProcedure.input(
|
|
3395
|
+
z5.object({
|
|
3396
|
+
id: z5.string().max(128),
|
|
3397
|
+
name: z5.string().min(1).max(100).optional(),
|
|
3398
|
+
url: z5.string().url().max(2048).optional(),
|
|
3399
|
+
passphrase: z5.string().min(1).max(1024).optional(),
|
|
3400
|
+
passthroughEnv: z5.array(
|
|
3401
|
+
z5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(256)
|
|
3402
|
+
).max(100).optional()
|
|
3403
|
+
})
|
|
3404
|
+
).mutation(async ({ input }) => {
|
|
3347
3405
|
try {
|
|
3348
|
-
const
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
}
|
|
3358
|
-
} catch {
|
|
3406
|
+
const { id, ...fields } = input;
|
|
3407
|
+
const config = await agentManager.update(id, fields);
|
|
3408
|
+
return { ...config, passphrase: "***" };
|
|
3409
|
+
} catch (err) {
|
|
3410
|
+
if (err instanceof Error) {
|
|
3411
|
+
if (err.message.includes("not found")) {
|
|
3412
|
+
throw new TRPCError5({ code: "NOT_FOUND", message: err.message });
|
|
3413
|
+
}
|
|
3414
|
+
if (err.message.includes("Cannot modify")) {
|
|
3415
|
+
throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
|
|
3359
3416
|
}
|
|
3360
3417
|
}
|
|
3361
|
-
|
|
3418
|
+
throw err;
|
|
3362
3419
|
}
|
|
3363
|
-
yield* forwardEvents("process:status", (d) => {
|
|
3364
|
-
if (input?.agentId && d.agentId !== input.agentId) return false;
|
|
3365
|
-
if (input?.processId && d.processId !== input.processId) return false;
|
|
3366
|
-
if (allowedIds && !allowedIds.has(d.processId)) return false;
|
|
3367
|
-
return true;
|
|
3368
|
-
});
|
|
3369
|
-
}),
|
|
3370
|
-
/** Subscribe to real-time metrics for a process. */
|
|
3371
|
-
onMetrics: protectedProcedure.input(z6.object({
|
|
3372
|
-
agentId: z6.string().max(128).optional(),
|
|
3373
|
-
processId: z6.string().max(128).optional()
|
|
3374
|
-
}).optional()).subscription(async function* ({ input, ctx }) {
|
|
3375
|
-
const allowedIds = getAllowedServiceIds(ctx);
|
|
3376
|
-
if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
|
|
3377
|
-
yield* forwardEvents("process:metrics", (d) => {
|
|
3378
|
-
if (input?.agentId && d.agentId !== input.agentId) return false;
|
|
3379
|
-
if (input?.processId && d.processId !== input.processId) return false;
|
|
3380
|
-
if (allowedIds && !allowedIds.has(d.processId)) return false;
|
|
3381
|
-
return true;
|
|
3382
|
-
});
|
|
3383
3420
|
}),
|
|
3384
|
-
/**
|
|
3385
|
-
|
|
3386
|
-
agentId: z6.string().max(128).optional(),
|
|
3387
|
-
processId: z6.string().max(128).optional()
|
|
3388
|
-
}).optional()).subscription(async function* ({ input, ctx }) {
|
|
3389
|
-
const allowedIds = getAllowedServiceIds(ctx);
|
|
3390
|
-
if (input?.processId) requireServiceAction(ctx, input.processId, "service:healthcheck");
|
|
3391
|
-
yield* forwardEvents("process:health", (d) => {
|
|
3392
|
-
if (input?.agentId && d.agentId !== input.agentId) return false;
|
|
3393
|
-
if (input?.processId && d.processId !== input.processId) return false;
|
|
3394
|
-
if (allowedIds && !allowedIds.has(d.processId)) return false;
|
|
3395
|
-
return true;
|
|
3396
|
-
});
|
|
3397
|
-
})
|
|
3398
|
-
});
|
|
3399
|
-
|
|
3400
|
-
// ../server/src/trpc/routers/audit.ts
|
|
3401
|
-
import { z as z7 } from "zod";
|
|
3402
|
-
var auditRouter = router({
|
|
3403
|
-
list: protectedProcedure.input(z7.object({
|
|
3404
|
-
action: z7.string().max(50).optional(),
|
|
3405
|
-
entityType: z7.string().max(50).optional(),
|
|
3406
|
-
entityId: z7.string().max(100).optional(),
|
|
3407
|
-
limit: z7.number().int().positive().max(100).default(50),
|
|
3408
|
-
offset: z7.number().int().nonnegative().default(0)
|
|
3409
|
-
}).optional()).query(async ({ input }) => {
|
|
3410
|
-
return auditService.list(input ?? {});
|
|
3411
|
-
})
|
|
3412
|
-
});
|
|
3413
|
-
|
|
3414
|
-
// ../server/src/trpc/routers/setup.ts
|
|
3415
|
-
import { z as z8 } from "zod";
|
|
3416
|
-
|
|
3417
|
-
// ../server/src/services/cloud-session-store.ts
|
|
3418
|
-
import { readFile as readFile8, writeFile as writeFile8, rename as rename8, mkdir as mkdir8, chmod as chmod7, unlink as unlink2 } from "fs/promises";
|
|
3419
|
-
import { dirname as dirname5 } from "path";
|
|
3420
|
-
import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3421
|
-
var FILENAME4 = "cloud-session.json";
|
|
3422
|
-
var STATE_TTL_MS = 5 * 60 * 1e3;
|
|
3423
|
-
var CloudSessionStore = class {
|
|
3424
|
-
session = null;
|
|
3425
|
-
loaded = false;
|
|
3426
|
-
/** Pending login states (CSRF binding between `start` and the callback). */
|
|
3427
|
-
pending = /* @__PURE__ */ new Map();
|
|
3428
|
-
/**
|
|
3429
|
-
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
3430
|
-
* root, but a launch directory owning its own key keeps both together.
|
|
3431
|
-
*/
|
|
3432
|
-
getFilePath() {
|
|
3433
|
-
return keyedFilePath(FILENAME4);
|
|
3434
|
-
}
|
|
3435
|
-
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
3436
|
-
getLegacyFilePath() {
|
|
3437
|
-
return legacyMachineFilePath(FILENAME4);
|
|
3438
|
-
}
|
|
3439
|
-
/**
|
|
3440
|
-
* Read the machine file, falling back to the launch directory of old.
|
|
3441
|
-
*
|
|
3442
|
-
* The legacy file is left in place: the next save writes the new location,
|
|
3443
|
-
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
3444
|
-
*/
|
|
3445
|
-
async readFromDisk() {
|
|
3421
|
+
/** Get resolved passthrough env values from an agent */
|
|
3422
|
+
getPassthroughEnv: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).query(async ({ input }) => {
|
|
3446
3423
|
try {
|
|
3447
|
-
return await
|
|
3424
|
+
return await agentManager.agentQuery(input.id, "config.getPassthroughEnv");
|
|
3448
3425
|
} catch {
|
|
3449
|
-
return
|
|
3426
|
+
return {};
|
|
3450
3427
|
}
|
|
3451
|
-
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3428
|
+
}),
|
|
3429
|
+
/** Remove a registered agent */
|
|
3430
|
+
remove: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).mutation(async ({ input }) => {
|
|
3454
3431
|
try {
|
|
3455
|
-
const
|
|
3456
|
-
|
|
3457
|
-
|
|
3432
|
+
const removed = await agentManager.remove(input.id);
|
|
3433
|
+
if (!removed) {
|
|
3434
|
+
throw new TRPCError5({ code: "NOT_FOUND", message: `Agent ${input.id} not found` });
|
|
3435
|
+
}
|
|
3436
|
+
return { success: true };
|
|
3437
|
+
} catch (err) {
|
|
3438
|
+
if (err instanceof Error && err.message === "Cannot remove the local agent") {
|
|
3439
|
+
throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
|
|
3440
|
+
}
|
|
3441
|
+
throw err;
|
|
3458
3442
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3443
|
+
}),
|
|
3444
|
+
/** Subscribe to real-time agent status changes */
|
|
3445
|
+
onStatus: protectedProcedure.subscription(async function* () {
|
|
3446
|
+
for (const agent of agentManager.listAgents()) {
|
|
3447
|
+
yield { agentId: agent.config.id, status: agent.status };
|
|
3448
|
+
}
|
|
3449
|
+
const queue = [];
|
|
3450
|
+
let resolve7 = null;
|
|
3451
|
+
const handler = (event) => {
|
|
3452
|
+
if (queue.length >= SUBSCRIPTION_QUEUE_MAX) queue.shift();
|
|
3453
|
+
queue.push(event);
|
|
3454
|
+
if (resolve7) {
|
|
3455
|
+
resolve7();
|
|
3456
|
+
resolve7 = null;
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
agentManager.on("status", handler);
|
|
3460
|
+
try {
|
|
3461
|
+
while (true) {
|
|
3462
|
+
if (queue.length > 0) {
|
|
3463
|
+
yield queue.shift();
|
|
3464
|
+
} else {
|
|
3465
|
+
await new Promise((r) => {
|
|
3466
|
+
resolve7 = r;
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
} finally {
|
|
3471
|
+
agentManager.off("status", handler);
|
|
3472
|
+
}
|
|
3473
|
+
})
|
|
3474
|
+
});
|
|
3475
|
+
|
|
3476
|
+
// ../server/src/trpc/routers/process.ts
|
|
3477
|
+
import { z as z6 } from "zod";
|
|
3478
|
+
import { TRPCError as TRPCError6 } from "@trpc/server";
|
|
3479
|
+
var SUBSCRIPTION_QUEUE_MAX2 = LOG_BUFFER_SIZE;
|
|
3480
|
+
async function* forwardEvents(eventName, filter) {
|
|
3481
|
+
const queue = [];
|
|
3482
|
+
let resolve7 = null;
|
|
3483
|
+
const handler = (data) => {
|
|
3484
|
+
if (filter && !filter(data)) return;
|
|
3485
|
+
if (queue.length >= SUBSCRIPTION_QUEUE_MAX2) queue.shift();
|
|
3486
|
+
queue.push(data);
|
|
3487
|
+
if (resolve7) {
|
|
3488
|
+
resolve7();
|
|
3489
|
+
resolve7 = null;
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
agentManager.on(eventName, handler);
|
|
3493
|
+
try {
|
|
3494
|
+
while (true) {
|
|
3495
|
+
if (queue.length > 0) {
|
|
3496
|
+
yield queue.shift();
|
|
3497
|
+
} else {
|
|
3498
|
+
await new Promise((r) => {
|
|
3499
|
+
resolve7 = r;
|
|
3500
|
+
});
|
|
3485
3501
|
}
|
|
3486
3502
|
}
|
|
3487
|
-
|
|
3503
|
+
} finally {
|
|
3504
|
+
agentManager.off(eventName, handler);
|
|
3488
3505
|
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3506
|
+
}
|
|
3507
|
+
async function* forwardLogBatches(agentId, processId) {
|
|
3508
|
+
const queue = [];
|
|
3509
|
+
let resolve7 = null;
|
|
3510
|
+
const handler = (data) => {
|
|
3511
|
+
if (data.agentId !== agentId) return;
|
|
3512
|
+
const batch = data.batch;
|
|
3513
|
+
if (!Array.isArray(batch)) return;
|
|
3514
|
+
const filtered = batch.filter((log) => log.processId === processId);
|
|
3515
|
+
if (filtered.length === 0) return;
|
|
3516
|
+
if (queue.length >= SUBSCRIPTION_QUEUE_MAX2) queue.shift();
|
|
3517
|
+
queue.push(filtered);
|
|
3518
|
+
if (resolve7) {
|
|
3519
|
+
resolve7();
|
|
3520
|
+
resolve7 = null;
|
|
3493
3521
|
}
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
await this.load();
|
|
3506
|
-
this.session = { cloudUrl, encryptedToken: encryptValue(token) };
|
|
3507
|
-
await this.save();
|
|
3508
|
-
}
|
|
3509
|
-
/** The decrypted session token, or null when not logged in. */
|
|
3510
|
-
async getToken() {
|
|
3511
|
-
await this.load();
|
|
3512
|
-
if (!this.session) return null;
|
|
3513
|
-
try {
|
|
3514
|
-
return decryptValue(this.session.encryptedToken);
|
|
3515
|
-
} catch {
|
|
3516
|
-
return null;
|
|
3522
|
+
};
|
|
3523
|
+
agentManager.on("process:log", handler);
|
|
3524
|
+
try {
|
|
3525
|
+
while (true) {
|
|
3526
|
+
if (queue.length > 0) {
|
|
3527
|
+
yield queue.shift();
|
|
3528
|
+
} else {
|
|
3529
|
+
await new Promise((r) => {
|
|
3530
|
+
resolve7 = r;
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3517
3533
|
}
|
|
3534
|
+
} finally {
|
|
3535
|
+
agentManager.off("process:log", handler);
|
|
3518
3536
|
}
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3537
|
+
}
|
|
3538
|
+
var processRouter = router({
|
|
3539
|
+
/** List all processes across all connected agents */
|
|
3540
|
+
list: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes", tags: ["processes"], summary: "List all processes", protect: true } }).output(z6.any()).query(async () => {
|
|
3541
|
+
const agents = agentManager.listAgents();
|
|
3542
|
+
const results = [];
|
|
3543
|
+
await Promise.allSettled(
|
|
3544
|
+
agents.filter((a) => a.status.connected).map(async (a) => {
|
|
3545
|
+
try {
|
|
3546
|
+
const processes = await agentManager.agentQuery(a.config.id, "process.list");
|
|
3547
|
+
for (const proc of processes) {
|
|
3548
|
+
results.push({ ...proc, agentId: a.config.id });
|
|
3549
|
+
}
|
|
3550
|
+
} catch {
|
|
3551
|
+
}
|
|
3552
|
+
})
|
|
3553
|
+
);
|
|
3554
|
+
return results;
|
|
3555
|
+
}),
|
|
3556
|
+
/** Get a single process from a specific agent */
|
|
3557
|
+
get: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{id}", tags: ["processes"], summary: "Get a process by agent and ID", protect: true } }).input(z6.object({ agentId: z6.string().max(128), id: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
|
|
3558
|
+
requireServiceAction(ctx, input.id, "service:read");
|
|
3559
|
+
const status = agentManager.getStatus(input.agentId);
|
|
3560
|
+
if (!status) {
|
|
3561
|
+
throw new TRPCError6({ code: "NOT_FOUND", message: `Agent ${input.agentId} not found` });
|
|
3562
|
+
}
|
|
3563
|
+
const proc = await agentManager.agentQuery(input.agentId, "process.get", { id: input.id });
|
|
3564
|
+
return { ...proc, agentId: input.agentId };
|
|
3565
|
+
}),
|
|
3566
|
+
/** Start a process on an agent.
|
|
3567
|
+
* Lazy deploy: the agent only learns about a service when it is first started
|
|
3568
|
+
* (or restarted). This avoids pushing configs for every service at agent boot,
|
|
3569
|
+
* which would be wasteful when many services are never started.
|
|
3570
|
+
* The deploy call is idempotent — creates the process if missing, updates config if it exists. */
|
|
3571
|
+
start: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/start", tags: ["processes"], summary: "Start a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
|
|
3572
|
+
requireServiceAction(ctx, input.processId, "service:start");
|
|
3573
|
+
const service = await serviceStore.get(input.processId);
|
|
3574
|
+
const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
|
|
3575
|
+
if (!effectiveAgentId) {
|
|
3576
|
+
throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
|
|
3577
|
+
}
|
|
3578
|
+
if (service) {
|
|
3579
|
+
try {
|
|
3580
|
+
const resolved = await resolveServiceVariables(service);
|
|
3581
|
+
await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
|
|
3582
|
+
} catch (err) {
|
|
3583
|
+
console.debug("[process-router] Failed to deploy before start for", input.processId + ":", err.message);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
if (service?.clearLogsOnStart) {
|
|
3587
|
+
try {
|
|
3588
|
+
await agentManager.agentMutation(effectiveAgentId, "process.clearLogBuffer", { id: input.processId });
|
|
3589
|
+
agentManager.emit("process:log", { agentId: effectiveAgentId, batch: [{ processId: input.processId, __clearLogBuffer: true }] });
|
|
3590
|
+
} catch (err) {
|
|
3591
|
+
console.debug("[process-router] Failed to clear logs before start for", input.processId + ":", err.message);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
let result;
|
|
3539
3595
|
try {
|
|
3540
|
-
await
|
|
3541
|
-
} catch {
|
|
3596
|
+
result = await agentManager.agentMutation(effectiveAgentId, "process.start", { id: input.processId });
|
|
3597
|
+
} catch (err) {
|
|
3598
|
+
const err_ = err;
|
|
3599
|
+
const isFetchError = err_?.cause?.code === "ECONNREFUSED" || err_?.message === "fetch failed" || err_?.name === "TypeError";
|
|
3600
|
+
throw new TRPCError6({
|
|
3601
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
3602
|
+
message: isFetchError ? `Cannot reach agent \u2014 make sure the agent is running` : err_?.message ?? "Failed to start process"
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
await auditService.log({ userId: "local", action: "start", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
|
|
3606
|
+
return { ...result, agentId: effectiveAgentId };
|
|
3607
|
+
}),
|
|
3608
|
+
/** Stop a process on an agent */
|
|
3609
|
+
stop: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/stop", tags: ["processes"], summary: "Stop a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
|
|
3610
|
+
requireServiceAction(ctx, input.processId, "service:stop");
|
|
3611
|
+
const result = await agentManager.agentMutation(input.agentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
|
|
3612
|
+
await auditService.log({ userId: "local", action: "stop", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
|
|
3613
|
+
return { ...result, agentId: input.agentId };
|
|
3614
|
+
}),
|
|
3615
|
+
/** Restart a process on an agent.
|
|
3616
|
+
* Sends stop → deploy (latest resolved config) → start so that
|
|
3617
|
+
* config/variable changes take effect. Same lazy deploy pattern as start. */
|
|
3618
|
+
restart: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/restart", tags: ["processes"], summary: "Restart a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
|
|
3619
|
+
requireServiceAction(ctx, input.processId, "service:restart");
|
|
3620
|
+
const service = await serviceStore.get(input.processId);
|
|
3621
|
+
const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
|
|
3622
|
+
if (!effectiveAgentId) {
|
|
3623
|
+
throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
|
|
3542
3624
|
}
|
|
3543
|
-
}
|
|
3544
|
-
};
|
|
3545
|
-
var cloudSessionStore = new CloudSessionStore();
|
|
3546
|
-
|
|
3547
|
-
// ../server/src/services/cloud-api.ts
|
|
3548
|
-
var DEFAULT_CLOUD_API_URL = "https://api.runeya.dev";
|
|
3549
|
-
var DEFAULT_CLOUD_APP_URL = "https://runeya.dev";
|
|
3550
|
-
function getCloudApiUrl() {
|
|
3551
|
-
return (env.CLOUD_URL ?? DEFAULT_CLOUD_API_URL).replace(/\/+$/, "");
|
|
3552
|
-
}
|
|
3553
|
-
function getCloudAppUrl() {
|
|
3554
|
-
return (env.CLOUD_APP_URL ?? DEFAULT_CLOUD_APP_URL).replace(/\/+$/, "");
|
|
3555
|
-
}
|
|
3556
|
-
var CloudNotLinkedError = class extends Error {
|
|
3557
|
-
code = "CLOUD_NOT_LINKED";
|
|
3558
|
-
constructor() {
|
|
3559
|
-
super("Not linked to the Runeya cloud.");
|
|
3560
|
-
this.name = "CloudNotLinkedError";
|
|
3561
|
-
}
|
|
3562
|
-
};
|
|
3563
|
-
async function cloudFetch(path, init = {}) {
|
|
3564
|
-
const token = await cloudSessionStore.getToken();
|
|
3565
|
-
if (!token) throw new CloudNotLinkedError();
|
|
3566
|
-
return fetch(`${getCloudApiUrl()}${path}`, {
|
|
3567
|
-
method: init.method ?? "GET",
|
|
3568
|
-
headers: {
|
|
3569
|
-
Authorization: `Bearer ${token}`,
|
|
3570
|
-
// Le cloud refuse (426) les apps trop anciennes pour son schéma courant.
|
|
3571
|
-
[APP_VERSION_HEADER]: appVersion,
|
|
3572
|
-
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
3573
|
-
},
|
|
3574
|
-
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
3575
|
-
// 15s suits a JSON call; attachment downloads pass a longer budget.
|
|
3576
|
-
signal: AbortSignal.timeout(init.timeoutMs ?? 15e3)
|
|
3577
|
-
});
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
// ../server/src/services/conversation-store.ts
|
|
3581
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
3582
|
-
import { readFile as readFile9, writeFile as writeFile9, rename as rename9, mkdir as mkdir9, chmod as chmod8 } from "fs/promises";
|
|
3583
|
-
import { dirname as dirname6, join as join10 } from "path";
|
|
3584
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
3585
|
-
var FILENAME5 = "conversations.private.json";
|
|
3586
|
-
async function localProjectId() {
|
|
3587
|
-
const projects = await projectStore.list().catch(() => []);
|
|
3588
|
-
return projects.find((p) => !p.orgId)?.id ?? null;
|
|
3589
|
-
}
|
|
3590
|
-
var ConversationStore = class {
|
|
3591
|
-
conversations = /* @__PURE__ */ new Map();
|
|
3592
|
-
/** Which vault each conversation was read from, until its project is known. */
|
|
3593
|
-
roots = /* @__PURE__ */ new Map();
|
|
3594
|
-
/**
|
|
3595
|
-
* Les conversations qu'on n'a pas su déchiffrer, par coffre, sous leur forme
|
|
3596
|
-
* de disque.
|
|
3597
|
-
*
|
|
3598
|
-
* Une entrée illisible était simplement ignorée. Tant qu'aucune de ses
|
|
3599
|
-
* voisines ne se chargeait, le fichier n'était jamais réécrit et la donnée
|
|
3600
|
-
* survivait par accident ; dès qu'une seule s'ouvrait, la sauvegarde suivante
|
|
3601
|
-
* réécrivait le fichier avec elle seule — et effaçait les autres. Elles sont
|
|
3602
|
-
* donc gardées ici, telles quelles, et réémises à l'identique.
|
|
3603
|
-
*/
|
|
3604
|
-
unreadable = /* @__PURE__ */ new Map();
|
|
3605
|
-
loaded = false;
|
|
3606
|
-
events = new EventEmitter2().setMaxListeners(0);
|
|
3607
|
-
saveQueue = Promise.resolve();
|
|
3608
|
-
snapshotConversation(conv) {
|
|
3609
|
-
return structuredClone(conv);
|
|
3610
|
-
}
|
|
3611
|
-
/**
|
|
3612
|
-
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
3613
|
-
* root, but a launch directory owning its own key keeps both together.
|
|
3614
|
-
*/
|
|
3615
|
-
getFilePath() {
|
|
3616
|
-
return keyedFilePath(FILENAME5);
|
|
3617
|
-
}
|
|
3618
|
-
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
3619
|
-
getLegacyFilePath() {
|
|
3620
|
-
return legacyMachineFilePath(FILENAME5);
|
|
3621
|
-
}
|
|
3622
|
-
/**
|
|
3623
|
-
* Read the machine file, falling back to the launch directory of old.
|
|
3624
|
-
*
|
|
3625
|
-
* The legacy file is left in place: the next save writes the new location,
|
|
3626
|
-
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
3627
|
-
*/
|
|
3628
|
-
async readFromDisk() {
|
|
3629
3625
|
try {
|
|
3630
|
-
|
|
3631
|
-
} catch {
|
|
3632
|
-
|
|
3626
|
+
await agentManager.agentMutation(effectiveAgentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
|
|
3627
|
+
} catch (err) {
|
|
3628
|
+
console.debug("[process-router] Stop before restart ignored (process not found on agent?):", err.message);
|
|
3633
3629
|
}
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3630
|
+
if (service) {
|
|
3631
|
+
try {
|
|
3632
|
+
const resolved = await resolveServiceVariables(service);
|
|
3633
|
+
await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
|
|
3634
|
+
} catch (err) {
|
|
3635
|
+
console.debug("[process-router] Failed to deploy before restart for", input.processId + ":", err.message);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
if (service?.clearLogsOnStart) {
|
|
3639
|
+
try {
|
|
3640
|
+
await agentManager.agentMutation(effectiveAgentId, "process.clearLogBuffer", { id: input.processId });
|
|
3641
|
+
agentManager.emit("process:log", { agentId: effectiveAgentId, batch: [{ processId: input.processId, __clearLogBuffer: true }] });
|
|
3642
|
+
} catch (err) {
|
|
3643
|
+
console.debug("[process-router] Failed to clear logs before restart for", input.processId + ":", err.message);
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
const result = await agentManager.agentMutation(effectiveAgentId, "process.start", { id: input.processId });
|
|
3647
|
+
await auditService.log({ userId: "local", action: "restart", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
|
|
3648
|
+
return { ...result, agentId: effectiveAgentId };
|
|
3649
|
+
}),
|
|
3650
|
+
/** Get buffered logs for a process from an agent */
|
|
3651
|
+
logs: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{processId}/logs", tags: ["processes"], summary: "Get buffered logs for a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
|
|
3652
|
+
requireServiceAction(ctx, input.processId, "logs:read");
|
|
3653
|
+
return agentManager.agentQuery(input.agentId, "process.logs", { id: input.processId });
|
|
3654
|
+
}),
|
|
3655
|
+
/** Clear buffered logs for a process on an agent */
|
|
3656
|
+
clearLogBuffer: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).mutation(async ({ input }) => {
|
|
3657
|
+
await agentManager.agentMutation(input.agentId, "process.clearLogBuffer", { id: input.processId });
|
|
3658
|
+
await auditService.log({ userId: "local", action: "clearLogBuffer", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
|
|
3659
|
+
return { success: true };
|
|
3660
|
+
}),
|
|
3661
|
+
/** Execute a shortcut command on an agent, streaming output into the process log */
|
|
3662
|
+
executeShortcut: protectedProcedure.input(z6.object({
|
|
3663
|
+
agentId: z6.string().max(128),
|
|
3664
|
+
processId: z6.string().max(128),
|
|
3665
|
+
shortcut: ServiceShortcutSchema
|
|
3666
|
+
})).mutation(async ({ input }) => {
|
|
3667
|
+
const service = await serviceStore.get(input.processId);
|
|
3668
|
+
if (service) {
|
|
3669
|
+
try {
|
|
3670
|
+
const resolved = await resolveServiceVariables(service);
|
|
3671
|
+
await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
|
|
3672
|
+
} catch (err) {
|
|
3673
|
+
console.debug("[process-router] Failed to deploy before executeShortcut for", input.processId + ":", err.message);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
const result = await agentManager.agentMutation(
|
|
3677
|
+
input.agentId,
|
|
3678
|
+
"process.executeShortcut",
|
|
3679
|
+
{ id: input.processId, shortcut: input.shortcut },
|
|
3680
|
+
AGENT_LONG_FETCH_TIMEOUT
|
|
3681
|
+
);
|
|
3682
|
+
await auditService.log({
|
|
3683
|
+
userId: "local",
|
|
3684
|
+
action: "executeShortcut",
|
|
3685
|
+
entityType: "process",
|
|
3686
|
+
entityId: input.processId,
|
|
3687
|
+
metadata: { agentId: input.agentId, shortcutId: input.shortcut.id, shortcutLabel: input.shortcut.label }
|
|
3688
|
+
});
|
|
3689
|
+
return result;
|
|
3690
|
+
}),
|
|
3691
|
+
/** Execute an ad-hoc command on an agent, streaming output into the process log */
|
|
3692
|
+
executeAdHocCommand: protectedProcedure.input(z6.object({
|
|
3693
|
+
agentId: z6.string().max(128),
|
|
3694
|
+
processId: z6.string().max(128),
|
|
3695
|
+
command: z6.string().min(1).max(4096),
|
|
3696
|
+
cwd: z6.string().max(4096).optional(),
|
|
3697
|
+
commandId: z6.string().max(128).optional()
|
|
3698
|
+
})).mutation(async ({ input }) => {
|
|
3699
|
+
const service = await serviceStore.get(input.processId);
|
|
3700
|
+
if (service) {
|
|
3701
|
+
try {
|
|
3702
|
+
const resolved = await resolveServiceVariables(service);
|
|
3703
|
+
await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
|
|
3704
|
+
} catch (err) {
|
|
3705
|
+
console.debug("[process-router] Failed to deploy before executeAdHocCommand for", input.processId + ":", err.message);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
const result = await agentManager.agentMutation(
|
|
3709
|
+
input.agentId,
|
|
3710
|
+
"process.executeAdHocCommand",
|
|
3711
|
+
{
|
|
3712
|
+
id: input.processId,
|
|
3713
|
+
command: input.command,
|
|
3714
|
+
// An explicit cwd wins; otherwise the service's directory, anchored on
|
|
3715
|
+
// its project rather than on the server's launch directory.
|
|
3716
|
+
cwd: input.cwd ?? await resolveAgentCwd({ serviceId: input.processId }),
|
|
3717
|
+
commandId: input.commandId
|
|
3718
|
+
},
|
|
3719
|
+
AGENT_LONG_FETCH_TIMEOUT
|
|
3720
|
+
);
|
|
3721
|
+
await auditService.log({
|
|
3722
|
+
userId: "local",
|
|
3723
|
+
action: "executeAdHocCommand",
|
|
3724
|
+
entityType: "process",
|
|
3725
|
+
entityId: input.processId,
|
|
3726
|
+
metadata: { agentId: input.agentId, command: input.command }
|
|
3727
|
+
});
|
|
3728
|
+
return result;
|
|
3729
|
+
}),
|
|
3730
|
+
/** Cancel a running shortcut command on an agent */
|
|
3731
|
+
cancelShortcut: protectedProcedure.input(z6.object({
|
|
3732
|
+
agentId: z6.string().max(128),
|
|
3733
|
+
processId: z6.string().max(128),
|
|
3734
|
+
shortcutId: z6.string().max(128)
|
|
3735
|
+
})).mutation(async ({ input }) => {
|
|
3736
|
+
await agentManager.agentMutation(input.agentId, "process.cancelShortcut", { id: input.processId, shortcutId: input.shortcutId });
|
|
3737
|
+
return { success: true };
|
|
3738
|
+
}),
|
|
3739
|
+
/** Cancel a running ad-hoc command on an agent */
|
|
3740
|
+
cancelAdHocCommand: protectedProcedure.input(z6.object({
|
|
3741
|
+
agentId: z6.string().max(128),
|
|
3742
|
+
processId: z6.string().max(128),
|
|
3743
|
+
commandId: z6.string().max(128).optional()
|
|
3744
|
+
})).mutation(async ({ input }) => {
|
|
3745
|
+
await agentManager.agentMutation(input.agentId, "process.cancelAdHocCommand", { id: input.processId, commandId: input.commandId });
|
|
3746
|
+
return { success: true };
|
|
3747
|
+
}),
|
|
3748
|
+
/** Get current metrics for a process from an agent */
|
|
3749
|
+
metrics: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).query(async ({ input }) => {
|
|
3750
|
+
return agentManager.agentQuery(input.agentId, "process.metrics", { id: input.processId });
|
|
3751
|
+
}),
|
|
3645
3752
|
/**
|
|
3646
|
-
*
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
* pour le coffre local. Appliqué à un fichier rangé sous `orgs/<org>/`, il
|
|
3650
|
-
* produit un chiffré que ce coffre n'emploie pas — lisible par personne à la
|
|
3651
|
-
* relecture, et impossible à distinguer d'une corruption.
|
|
3652
|
-
*
|
|
3653
|
-
* Le cas a été observé plusieurs fois sans qu'on parvienne à nommer le
|
|
3654
|
-
* chemin fautif : les garde-fous en place traitent le coffre fermé, le
|
|
3655
|
-
* coffre mal résolu et la copie périmée, aucun ne couvrait « coffre non
|
|
3656
|
-
* précisé ». Ces écritures-là n'ont jamais fonctionné — elles échouaient en
|
|
3657
|
-
* silence, ce qui est pire. On les saute désormais en le disant, avec la
|
|
3658
|
-
* pile d'appels qui manquait pour remonter au chemin fautif.
|
|
3753
|
+
* Subscribe to real-time log stream for a process.
|
|
3754
|
+
* Forwards batched events from the AgentWSBridge in real-time.
|
|
3755
|
+
* Historical logs are fetched separately via process.logs query.
|
|
3659
3756
|
*/
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
`[conversation-store] \xC9criture ignor\xE9e dans le coffre de l'organisation ${orgId} (${root}) : aucune cl\xE9 d'organisation nomm\xE9e, le contenu serait chiffr\xE9 par la cl\xE9 machine et illisible \xE0 la relecture.
|
|
3665
|
-
${new Error("trace").stack}`
|
|
3666
|
-
);
|
|
3667
|
-
return false;
|
|
3668
|
-
}
|
|
3669
|
-
toDisk(conv, vault) {
|
|
3670
|
-
return {
|
|
3671
|
-
id: conv.id,
|
|
3672
|
-
projectId: conv.projectId,
|
|
3673
|
-
title: conv.title,
|
|
3674
|
-
encryptedMessages: this.encryptMessages(conv.messages, vault),
|
|
3675
|
-
isCli: conv.isCli,
|
|
3676
|
-
runner: conv.runner,
|
|
3677
|
-
providerId: conv.providerId,
|
|
3678
|
-
codexThreadId: conv.codexThreadId,
|
|
3679
|
-
contextUsage: conv.contextUsage,
|
|
3680
|
-
workflowId: conv.workflowId,
|
|
3681
|
-
workflowType: conv.workflowType,
|
|
3682
|
-
kanbanBoardId: conv.kanbanBoardId,
|
|
3683
|
-
kanbanCardId: conv.kanbanCardId,
|
|
3684
|
-
archived: conv.archived,
|
|
3685
|
-
createdAt: conv.createdAt,
|
|
3686
|
-
updatedAt: conv.updatedAt
|
|
3687
|
-
};
|
|
3688
|
-
}
|
|
3689
|
-
fromDisk(disk, vault) {
|
|
3690
|
-
const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
|
|
3691
|
-
return {
|
|
3692
|
-
id: disk.id,
|
|
3693
|
-
projectId: disk.projectId,
|
|
3694
|
-
title: disk.title,
|
|
3695
|
-
messages: this.decryptMessages(disk.encryptedMessages, vault),
|
|
3696
|
-
isCli: disk.isCli ?? disk.isClaudeCode ?? false,
|
|
3697
|
-
runner: disk.runner,
|
|
3698
|
-
providerId: disk.providerId,
|
|
3699
|
-
codexThreadId: disk.codexThreadId,
|
|
3700
|
-
contextUsage: disk.contextUsage,
|
|
3701
|
-
workflowId: disk.workflowId ?? disk.scenarioId,
|
|
3702
|
-
workflowType,
|
|
3703
|
-
kanbanBoardId: disk.kanbanBoardId,
|
|
3704
|
-
kanbanCardId: disk.kanbanCardId,
|
|
3705
|
-
archived: disk.archived,
|
|
3706
|
-
createdAt: disk.createdAt,
|
|
3707
|
-
updatedAt: disk.updatedAt
|
|
3708
|
-
};
|
|
3709
|
-
}
|
|
3757
|
+
onLog: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).subscription(async function* ({ input, ctx }) {
|
|
3758
|
+
requireServiceAction(ctx, input.processId, "logs:read");
|
|
3759
|
+
yield* forwardLogBatches(input.agentId, input.processId);
|
|
3760
|
+
}),
|
|
3710
3761
|
/**
|
|
3711
|
-
*
|
|
3712
|
-
*
|
|
3713
|
-
*
|
|
3714
|
-
* that directory — which is what it was about, back when one directory meant
|
|
3715
|
-
* one project. With no local project to point at, it is left unattributed
|
|
3716
|
-
* rather than filed under a project picked at random.
|
|
3762
|
+
* Subscribe to real-time status changes for processes.
|
|
3763
|
+
* Input is optional — omit agentId/processId for global monitoring (dashboard).
|
|
3764
|
+
* Forwards events from the AgentWSBridge in real-time.
|
|
3717
3765
|
*/
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
const sources = [
|
|
3729
|
-
...legacyPaths.map((path) => ({ path, legacy: true })),
|
|
3730
|
-
...vaults.map((root) => ({ path: join10(root, FILENAME5), legacy: false }))
|
|
3731
|
-
];
|
|
3732
|
-
for (const { path, legacy } of sources) {
|
|
3733
|
-
let data;
|
|
3734
|
-
try {
|
|
3735
|
-
data = JSON.parse(await readFile9(path, "utf-8"));
|
|
3736
|
-
} catch {
|
|
3737
|
-
continue;
|
|
3738
|
-
}
|
|
3739
|
-
const inherited = legacy ? await localProjectId() : null;
|
|
3740
|
-
const root = dirname6(path);
|
|
3741
|
-
await rememberFile(path);
|
|
3742
|
-
for (const item of data) {
|
|
3766
|
+
onStatus: protectedProcedure.input(z6.object({
|
|
3767
|
+
agentId: z6.string().max(128).optional(),
|
|
3768
|
+
processId: z6.string().max(128).optional()
|
|
3769
|
+
}).optional()).subscription(async function* ({ input, ctx }) {
|
|
3770
|
+
const allowedIds = getAllowedServiceIds(ctx);
|
|
3771
|
+
if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
|
|
3772
|
+
try {
|
|
3773
|
+
const agents = agentManager.listAgents();
|
|
3774
|
+
for (const agent of agents.filter((a) => a.status.connected)) {
|
|
3775
|
+
if (input?.agentId && agent.config.id !== input.agentId) continue;
|
|
3743
3776
|
try {
|
|
3744
|
-
const
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3777
|
+
const processes = await agentManager.agentQuery(agent.config.id, "process.list");
|
|
3778
|
+
for (const proc of processes) {
|
|
3779
|
+
if (input?.processId && proc.id !== input.processId) continue;
|
|
3780
|
+
if (allowedIds && !allowedIds.has(proc.id)) continue;
|
|
3781
|
+
yield { ...proc, agentId: agent.config.id };
|
|
3782
|
+
}
|
|
3748
3783
|
} catch {
|
|
3749
|
-
const bucket = this.unreadable.get(root) ?? [];
|
|
3750
|
-
bucket.push(item);
|
|
3751
|
-
this.unreadable.set(root, bucket);
|
|
3752
3784
|
}
|
|
3753
3785
|
}
|
|
3786
|
+
} catch {
|
|
3754
3787
|
}
|
|
3755
|
-
|
|
3788
|
+
yield* forwardEvents("process:status", (d) => {
|
|
3789
|
+
if (input?.agentId && d.agentId !== input.agentId) return false;
|
|
3790
|
+
if (input?.processId && d.processId !== input.processId) return false;
|
|
3791
|
+
if (allowedIds && !allowedIds.has(d.processId)) return false;
|
|
3792
|
+
return true;
|
|
3793
|
+
});
|
|
3794
|
+
}),
|
|
3795
|
+
/** Subscribe to real-time metrics for a process. */
|
|
3796
|
+
onMetrics: protectedProcedure.input(z6.object({
|
|
3797
|
+
agentId: z6.string().max(128).optional(),
|
|
3798
|
+
processId: z6.string().max(128).optional()
|
|
3799
|
+
}).optional()).subscription(async function* ({ input, ctx }) {
|
|
3800
|
+
const allowedIds = getAllowedServiceIds(ctx);
|
|
3801
|
+
if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
|
|
3802
|
+
yield* forwardEvents("process:metrics", (d) => {
|
|
3803
|
+
if (input?.agentId && d.agentId !== input.agentId) return false;
|
|
3804
|
+
if (input?.processId && d.processId !== input.processId) return false;
|
|
3805
|
+
if (allowedIds && !allowedIds.has(d.processId)) return false;
|
|
3806
|
+
return true;
|
|
3807
|
+
});
|
|
3808
|
+
}),
|
|
3809
|
+
/** Subscribe to real-time health status changes. */
|
|
3810
|
+
onHealth: protectedProcedure.input(z6.object({
|
|
3811
|
+
agentId: z6.string().max(128).optional(),
|
|
3812
|
+
processId: z6.string().max(128).optional()
|
|
3813
|
+
}).optional()).subscription(async function* ({ input, ctx }) {
|
|
3814
|
+
const allowedIds = getAllowedServiceIds(ctx);
|
|
3815
|
+
if (input?.processId) requireServiceAction(ctx, input.processId, "service:healthcheck");
|
|
3816
|
+
yield* forwardEvents("process:health", (d) => {
|
|
3817
|
+
if (input?.agentId && d.agentId !== input.agentId) return false;
|
|
3818
|
+
if (input?.processId && d.processId !== input.processId) return false;
|
|
3819
|
+
if (allowedIds && !allowedIds.has(d.processId)) return false;
|
|
3820
|
+
return true;
|
|
3821
|
+
});
|
|
3822
|
+
})
|
|
3823
|
+
});
|
|
3824
|
+
|
|
3825
|
+
// ../server/src/trpc/routers/audit.ts
|
|
3826
|
+
import { z as z7 } from "zod";
|
|
3827
|
+
var auditRouter = router({
|
|
3828
|
+
list: protectedProcedure.input(z7.object({
|
|
3829
|
+
action: z7.string().max(50).optional(),
|
|
3830
|
+
entityType: z7.string().max(50).optional(),
|
|
3831
|
+
entityId: z7.string().max(100).optional(),
|
|
3832
|
+
limit: z7.number().int().positive().max(100).default(50),
|
|
3833
|
+
offset: z7.number().int().nonnegative().default(0)
|
|
3834
|
+
}).optional()).query(async ({ input }) => {
|
|
3835
|
+
return auditService.list(input ?? {});
|
|
3836
|
+
})
|
|
3837
|
+
});
|
|
3838
|
+
|
|
3839
|
+
// ../server/src/trpc/routers/setup.ts
|
|
3840
|
+
import { z as z8 } from "zod";
|
|
3841
|
+
|
|
3842
|
+
// ../server/src/services/cloud-session-store.ts
|
|
3843
|
+
import { readFile as readFile9, writeFile as writeFile9, rename as rename9, mkdir as mkdir9, chmod as chmod8, unlink as unlink2 } from "fs/promises";
|
|
3844
|
+
import { dirname as dirname6 } from "path";
|
|
3845
|
+
import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3846
|
+
var FILENAME5 = "cloud-session.json";
|
|
3847
|
+
var STATE_TTL_MS = 5 * 60 * 1e3;
|
|
3848
|
+
var CloudSessionStore = class {
|
|
3849
|
+
session = null;
|
|
3850
|
+
loaded = false;
|
|
3851
|
+
/** Pending login states (CSRF binding between `start` and the callback). */
|
|
3852
|
+
pending = /* @__PURE__ */ new Map();
|
|
3756
3853
|
/**
|
|
3757
|
-
*
|
|
3758
|
-
*
|
|
3759
|
-
* Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
|
|
3760
|
-
* re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
|
|
3761
|
-
* qui n'existe plus. La laisser en place n'affichait pas seulement des
|
|
3762
|
-
* comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
|
|
3763
|
-
* par-dessus le nouveau, et défaisait l'adoption.
|
|
3854
|
+
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
3855
|
+
* root, but a launch directory owning its own key keeps both together.
|
|
3764
3856
|
*/
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
this.roots.clear();
|
|
3768
|
-
this.unreadable.clear();
|
|
3769
|
-
this.loaded = false;
|
|
3770
|
-
this.events.emit("changed", { type: "reloaded" });
|
|
3857
|
+
getFilePath() {
|
|
3858
|
+
return keyedFilePath(FILENAME5);
|
|
3771
3859
|
}
|
|
3772
|
-
/**
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
*/
|
|
3776
|
-
async unreadableByVault() {
|
|
3777
|
-
await this.load();
|
|
3778
|
-
const out = /* @__PURE__ */ new Map();
|
|
3779
|
-
for (const [root, items] of this.unreadable) {
|
|
3780
|
-
const vault = vaultIdOfRoot(root) ?? null;
|
|
3781
|
-
out.set(vault, (out.get(vault) ?? 0) + items.length);
|
|
3782
|
-
}
|
|
3783
|
-
return out;
|
|
3860
|
+
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
3861
|
+
getLegacyFilePath() {
|
|
3862
|
+
return legacyMachineFilePath(FILENAME5);
|
|
3784
3863
|
}
|
|
3785
3864
|
/**
|
|
3786
|
-
*
|
|
3865
|
+
* Read the machine file, falling back to the launch directory of old.
|
|
3787
3866
|
*
|
|
3788
|
-
*
|
|
3789
|
-
*
|
|
3790
|
-
* read from.
|
|
3867
|
+
* The legacy file is left in place: the next save writes the new location,
|
|
3868
|
+
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
3791
3869
|
*/
|
|
3792
|
-
async
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
const root = await this.rootFor(conv);
|
|
3798
|
-
this.roots.set(conv.id, root);
|
|
3799
|
-
const bucket = byRoot.get(root) ?? [];
|
|
3800
|
-
const vault = vaultIdOfRoot(root);
|
|
3801
|
-
if (!this.isVaultNamed(root, vault)) continue;
|
|
3802
|
-
bucket.push(this.toDisk(conv, vault));
|
|
3803
|
-
byRoot.set(root, bucket);
|
|
3804
|
-
}
|
|
3805
|
-
for (const [root, items] of this.unreadable) {
|
|
3806
|
-
byRoot.set(root, [...items, ...byRoot.get(root) ?? []]);
|
|
3807
|
-
}
|
|
3808
|
-
const stale = /* @__PURE__ */ new Set();
|
|
3809
|
-
for (const root of byRoot.keys()) {
|
|
3810
|
-
const path = join10(root, FILENAME5);
|
|
3811
|
-
if (wasRead(path) && await hasChangedSinceRead(path)) stale.add(root);
|
|
3812
|
-
}
|
|
3813
|
-
for (const [root, data] of byRoot) {
|
|
3814
|
-
const filePath = join10(root, FILENAME5);
|
|
3815
|
-
if (stale.has(root)) {
|
|
3816
|
-
console.warn(
|
|
3817
|
-
`[conversation-store] ${filePath} a chang\xE9 depuis sa lecture \u2014 \xE9criture ignor\xE9e pour ne pas \xE9craser une r\xE9paration faite en dehors du serveur.`
|
|
3818
|
-
);
|
|
3819
|
-
continue;
|
|
3820
|
-
}
|
|
3821
|
-
await mkdir9(root, { recursive: true });
|
|
3822
|
-
const tmpPath = `${filePath}.tmp`;
|
|
3823
|
-
await writeFile9(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
3824
|
-
await rename9(tmpPath, filePath);
|
|
3825
|
-
await chmod8(filePath, 384);
|
|
3826
|
-
await rememberFile(filePath);
|
|
3870
|
+
async readFromDisk() {
|
|
3871
|
+
try {
|
|
3872
|
+
return await readFile9(await this.getFilePath(), "utf-8");
|
|
3873
|
+
} catch {
|
|
3874
|
+
return readFile9(this.getLegacyFilePath(), "utf-8");
|
|
3827
3875
|
}
|
|
3828
3876
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);
|
|
3838
|
-
return known ?? dirname6(await keyedFilePath(FILENAME5));
|
|
3839
|
-
}
|
|
3840
|
-
/**
|
|
3841
|
-
* Le projet d'une conversation, organisation comprise.
|
|
3842
|
-
*
|
|
3843
|
-
* Ce que la conversation ne dit pas d'elle-même : elle ne nomme que l'id.
|
|
3844
|
-
* Le coffre d'où elle vient fournit l'organisation, et c'est ce couple qu'il
|
|
3845
|
-
* faut pour retrouver le répertoire de travail du projet.
|
|
3846
|
-
*/
|
|
3847
|
-
async projectRefOf(conv) {
|
|
3848
|
-
if (!conv.projectId) return null;
|
|
3849
|
-
const known = this.roots.get(conv.id) ?? null;
|
|
3850
|
-
const ref = refOfProjectAmong(conv.projectId, known);
|
|
3851
|
-
return { id: ref.id, orgId: ref.orgId ?? null };
|
|
3877
|
+
async load() {
|
|
3878
|
+
if (this.loaded) return;
|
|
3879
|
+
try {
|
|
3880
|
+
const raw = await this.readFromDisk();
|
|
3881
|
+
this.session = JSON.parse(raw);
|
|
3882
|
+
} catch {
|
|
3883
|
+
}
|
|
3884
|
+
this.loaded = true;
|
|
3852
3885
|
}
|
|
3853
3886
|
/**
|
|
3854
|
-
*
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3857
|
-
* dans le nouveau, et le fichier d'où elles viennent est réécrit sans elles.
|
|
3887
|
+
* Mint a state value binding a login attempt to this server. Returned to the
|
|
3888
|
+
* cloud page and echoed back on the callback, so a redirect we never
|
|
3889
|
+
* initiated cannot install a token.
|
|
3858
3890
|
*/
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
const write = this.saveQueue.then(() => this.saveInternal());
|
|
3865
|
-
this.saveQueue = write.catch(() => {
|
|
3866
|
-
});
|
|
3867
|
-
return write;
|
|
3868
|
-
}
|
|
3869
|
-
async list() {
|
|
3870
|
-
await this.load();
|
|
3871
|
-
return Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3891
|
+
createState(returnTo) {
|
|
3892
|
+
this.sweepStates();
|
|
3893
|
+
const state = randomBytes4(32).toString("hex");
|
|
3894
|
+
this.pending.set(state, { expiresAt: Date.now() + STATE_TTL_MS, returnTo });
|
|
3895
|
+
return state;
|
|
3872
3896
|
}
|
|
3873
3897
|
/**
|
|
3874
|
-
*
|
|
3875
|
-
*
|
|
3876
|
-
*
|
|
3877
|
-
* migration n'a pas su attribuer — n'est rendue que dans les projets de son
|
|
3878
|
-
* propre coffre : elle reste atteignable là où elle a été écrite, sans se
|
|
3879
|
-
* déverser dans toutes les organisations de la machine.
|
|
3898
|
+
* Consume a state — single use, constant-time compare, TTL enforced. Returns
|
|
3899
|
+
* the URL to send the browser back to, or null when the state is unknown or
|
|
3900
|
+
* expired (i.e. a callback this server never initiated).
|
|
3880
3901
|
*/
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3902
|
+
consumeState(candidate) {
|
|
3903
|
+
this.sweepStates();
|
|
3904
|
+
for (const [state, entry] of this.pending) {
|
|
3905
|
+
const a = Buffer.from(state);
|
|
3906
|
+
const b = Buffer.from(candidate);
|
|
3907
|
+
if (a.length === b.length && timingSafeEqual2(a, b)) {
|
|
3908
|
+
this.pending.delete(state);
|
|
3909
|
+
return entry.expiresAt > Date.now() ? entry.returnTo : null;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
return null;
|
|
3884
3913
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
* arrive par événement doit passer le même filtre, sans quoi elle se
|
|
3890
|
-
* réinvite dans un projet qui ne l'a jamais demandée.
|
|
3891
|
-
*/
|
|
3892
|
-
async matcherForProject(ref) {
|
|
3893
|
-
await this.load();
|
|
3894
|
-
let projectRoot2 = null;
|
|
3895
|
-
try {
|
|
3896
|
-
projectRoot2 = ref.orgId === void 0 ? rootOfProjectAmong(ref.id, null) : rootOfProjectRef({ id: ref.id, orgId: ref.orgId ?? null });
|
|
3897
|
-
} catch {
|
|
3898
|
-
projectRoot2 = null;
|
|
3914
|
+
sweepStates() {
|
|
3915
|
+
const now = Date.now();
|
|
3916
|
+
for (const [state, entry] of this.pending) {
|
|
3917
|
+
if (entry.expiresAt <= now) this.pending.delete(state);
|
|
3899
3918
|
}
|
|
3900
|
-
return (conv) => {
|
|
3901
|
-
if (conv.projectId) return conv.projectId === ref.id;
|
|
3902
|
-
if (!projectRoot2) return true;
|
|
3903
|
-
return this.roots.get(conv.id) === projectRoot2;
|
|
3904
|
-
};
|
|
3905
3919
|
}
|
|
3906
|
-
async
|
|
3907
|
-
await this.
|
|
3908
|
-
|
|
3920
|
+
async save() {
|
|
3921
|
+
await mkdir9(dirname6(await this.getFilePath()), { recursive: true });
|
|
3922
|
+
const filePath = await this.getFilePath();
|
|
3923
|
+
const tmpPath = filePath + ".tmp";
|
|
3924
|
+
await writeFile9(tmpPath, JSON.stringify(this.session, null, 2), "utf-8");
|
|
3925
|
+
await rename9(tmpPath, filePath);
|
|
3926
|
+
await chmod8(filePath, 384);
|
|
3909
3927
|
}
|
|
3910
|
-
|
|
3928
|
+
/** Store the session token obtained from the one-time-token exchange. */
|
|
3929
|
+
async set(cloudUrl, token) {
|
|
3911
3930
|
await this.load();
|
|
3912
|
-
|
|
3913
|
-
const conv = {
|
|
3914
|
-
id: randomUUID3(),
|
|
3915
|
-
projectId,
|
|
3916
|
-
title: title ?? "New conversation",
|
|
3917
|
-
messages: [],
|
|
3918
|
-
isCli,
|
|
3919
|
-
runner,
|
|
3920
|
-
providerId,
|
|
3921
|
-
workflowId,
|
|
3922
|
-
workflowType,
|
|
3923
|
-
kanbanBoardId,
|
|
3924
|
-
kanbanCardId,
|
|
3925
|
-
createdAt: now,
|
|
3926
|
-
updatedAt: now
|
|
3927
|
-
};
|
|
3928
|
-
this.conversations.set(conv.id, conv);
|
|
3929
|
-
if (projectId && orgId !== void 0) {
|
|
3930
|
-
this.roots.set(conv.id, vaultFor({ id: projectId, orgId }).root);
|
|
3931
|
-
}
|
|
3931
|
+
this.session = { cloudUrl, encryptedToken: encryptValue(token) };
|
|
3932
3932
|
await this.save();
|
|
3933
|
-
this.events.emit("changed", { type: "created", conversation: this.snapshotConversation(conv) });
|
|
3934
|
-
return conv;
|
|
3935
3933
|
}
|
|
3936
|
-
|
|
3934
|
+
/** The decrypted session token, or null when not logged in. */
|
|
3935
|
+
async getToken() {
|
|
3937
3936
|
await this.load();
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
if (patch.workflowId !== void 0) conv.workflowId = patch.workflowId;
|
|
3944
|
-
if (patch.archived !== void 0) conv.archived = patch.archived;
|
|
3945
|
-
if (patch.codexThreadId !== void 0) conv.codexThreadId = patch.codexThreadId;
|
|
3946
|
-
if (options?.touch !== false) conv.updatedAt = Date.now();
|
|
3947
|
-
await this.save();
|
|
3948
|
-
if (!options?.silent) {
|
|
3949
|
-
this.events.emit("changed", { type: "updated", conversation: this.snapshotConversation(conv) });
|
|
3937
|
+
if (!this.session) return null;
|
|
3938
|
+
try {
|
|
3939
|
+
return decryptValue(this.session.encryptedToken);
|
|
3940
|
+
} catch {
|
|
3941
|
+
return null;
|
|
3950
3942
|
}
|
|
3951
|
-
return conv;
|
|
3952
3943
|
}
|
|
3953
3944
|
/**
|
|
3954
|
-
*
|
|
3955
|
-
*
|
|
3945
|
+
* The cloud this machine is signed into, or null when not linked.
|
|
3946
|
+
*
|
|
3947
|
+
* Needed to tell "an org I am not a member of" apart from "an org of another
|
|
3948
|
+
* cloud instance entirely" — the ids of two clouds share no namespace, so
|
|
3949
|
+
* comparing them without this would call every foreign-cloud project a
|
|
3950
|
+
* permission problem.
|
|
3956
3951
|
*/
|
|
3957
|
-
|
|
3958
|
-
const conv = this.conversations.get(id);
|
|
3959
|
-
if (!conv) return;
|
|
3960
|
-
const lastMsg = conv.messages[conv.messages.length - 1];
|
|
3961
|
-
if (!lastMsg || lastMsg.role !== "assistant") return;
|
|
3962
|
-
lastMsg.content = content;
|
|
3963
|
-
if (parts !== void 0) lastMsg.parts = parts;
|
|
3964
|
-
conv.updatedAt = Date.now();
|
|
3965
|
-
this.save().catch(() => {
|
|
3966
|
-
});
|
|
3967
|
-
}
|
|
3968
|
-
async listByKanbanBoardId(boardId) {
|
|
3952
|
+
async getCloudUrl() {
|
|
3969
3953
|
await this.load();
|
|
3970
|
-
return
|
|
3954
|
+
return this.session?.cloudUrl ?? null;
|
|
3971
3955
|
}
|
|
3972
|
-
async
|
|
3973
|
-
await this.
|
|
3974
|
-
return Array.from(this.conversations.values()).filter((c) => c.kanbanCardId === cardId);
|
|
3956
|
+
async isLinked() {
|
|
3957
|
+
return await this.getToken() !== null;
|
|
3975
3958
|
}
|
|
3976
|
-
|
|
3959
|
+
/** Forget the session (sign-out). */
|
|
3960
|
+
async clear() {
|
|
3977
3961
|
await this.load();
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
this.
|
|
3962
|
+
this.session = null;
|
|
3963
|
+
this.pending.clear();
|
|
3964
|
+
try {
|
|
3965
|
+
await unlink2(await this.getFilePath());
|
|
3966
|
+
} catch {
|
|
3982
3967
|
}
|
|
3983
3968
|
}
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3969
|
+
};
|
|
3970
|
+
var cloudSessionStore = new CloudSessionStore();
|
|
3971
|
+
|
|
3972
|
+
// ../server/src/services/cloud-api.ts
|
|
3973
|
+
var DEFAULT_CLOUD_API_URL = "https://api.runeya.dev";
|
|
3974
|
+
var DEFAULT_CLOUD_APP_URL = "https://runeya.dev";
|
|
3975
|
+
function getCloudApiUrl() {
|
|
3976
|
+
return (env.CLOUD_URL ?? DEFAULT_CLOUD_API_URL).replace(/\/+$/, "");
|
|
3977
|
+
}
|
|
3978
|
+
function getCloudAppUrl() {
|
|
3979
|
+
return (env.CLOUD_APP_URL ?? DEFAULT_CLOUD_APP_URL).replace(/\/+$/, "");
|
|
3980
|
+
}
|
|
3981
|
+
var CloudNotLinkedError = class extends Error {
|
|
3982
|
+
code = "CLOUD_NOT_LINKED";
|
|
3983
|
+
constructor() {
|
|
3984
|
+
super("Not linked to the Runeya cloud.");
|
|
3985
|
+
this.name = "CloudNotLinkedError";
|
|
3989
3986
|
}
|
|
3990
3987
|
};
|
|
3991
|
-
|
|
3988
|
+
async function cloudFetch(path, init = {}) {
|
|
3989
|
+
const token = await cloudSessionStore.getToken();
|
|
3990
|
+
if (!token) throw new CloudNotLinkedError();
|
|
3991
|
+
return fetch(`${getCloudApiUrl()}${path}`, {
|
|
3992
|
+
method: init.method ?? "GET",
|
|
3993
|
+
headers: {
|
|
3994
|
+
Authorization: `Bearer ${token}`,
|
|
3995
|
+
// Le cloud refuse (426) les apps trop anciennes pour son schéma courant.
|
|
3996
|
+
[APP_VERSION_HEADER]: appVersion,
|
|
3997
|
+
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
3998
|
+
},
|
|
3999
|
+
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
4000
|
+
// 15s suits a JSON call; attachment downloads pass a longer budget.
|
|
4001
|
+
signal: AbortSignal.timeout(init.timeoutMs ?? 15e3)
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
3992
4004
|
|
|
3993
4005
|
// ../server/src/trpc/routers/setup.ts
|
|
3994
4006
|
var setupRouter = router({
|
|
@@ -4844,20 +4856,20 @@ var settingsRouter = router({
|
|
|
4844
4856
|
});
|
|
4845
4857
|
if (config?.wildcardDomain?.trim()) {
|
|
4846
4858
|
const sanitisedDomain = config.wildcardDomain.replace(/^\*\./, "").replace(/[^a-z0-9.-]/gi, "");
|
|
4847
|
-
const
|
|
4859
|
+
const probe = `probe.${sanitisedDomain}`;
|
|
4848
4860
|
try {
|
|
4849
|
-
const addresses = await dns.resolve4(
|
|
4861
|
+
const addresses = await dns.resolve4(probe);
|
|
4850
4862
|
const pointsToLocalhost = addresses.some((a) => a === "127.0.0.1");
|
|
4851
4863
|
checks.push({
|
|
4852
4864
|
id: "dns_resolution",
|
|
4853
4865
|
ok: pointsToLocalhost,
|
|
4854
|
-
message: pointsToLocalhost ? `${
|
|
4866
|
+
message: pointsToLocalhost ? `${probe} \u2192 127.0.0.1 \u2713` : `${probe} r\xE9sout vers ${addresses.join(", ")} (attendu : 127.0.0.1)`
|
|
4855
4867
|
});
|
|
4856
4868
|
} catch {
|
|
4857
4869
|
checks.push({
|
|
4858
4870
|
id: "dns_resolution",
|
|
4859
4871
|
ok: false,
|
|
4860
|
-
message: `${
|
|
4872
|
+
message: `${probe} ne r\xE9sout pas \u2014 v\xE9rifiez le DNS wildcard`
|
|
4861
4873
|
});
|
|
4862
4874
|
}
|
|
4863
4875
|
}
|
|
@@ -7951,7 +7963,7 @@ function tailTranscript(opts) {
|
|
|
7951
7963
|
|
|
7952
7964
|
// ../server/src/services/ai-runners/claude-pty-runner.ts
|
|
7953
7965
|
async function getAgentManager() {
|
|
7954
|
-
const { agentManager: agentManager2 } = await import("./agent-manager-
|
|
7966
|
+
const { agentManager: agentManager2 } = await import("./agent-manager-UUSQK6S2.js");
|
|
7955
7967
|
return agentManager2;
|
|
7956
7968
|
}
|
|
7957
7969
|
function buildPtyArgs(params, sessionId, opts) {
|
|
@@ -8076,6 +8088,15 @@ async function runClaudeViaPty(params) {
|
|
|
8076
8088
|
});
|
|
8077
8089
|
} catch {
|
|
8078
8090
|
}
|
|
8091
|
+
if (skipTokenInstructions && entry.type === "assistant" && entry.message?.stop_reason === "end_turn") {
|
|
8092
|
+
settle({
|
|
8093
|
+
output: state.assistantText,
|
|
8094
|
+
tokenStatus: "none",
|
|
8095
|
+
tokenMessage: "",
|
|
8096
|
+
actualSessionId: sessionId
|
|
8097
|
+
});
|
|
8098
|
+
return;
|
|
8099
|
+
}
|
|
8079
8100
|
if (!skipTokenInstructions) {
|
|
8080
8101
|
const { status, message, branchChoice } = parseEndToken(state.assistantText);
|
|
8081
8102
|
if (status !== "none") {
|
|
@@ -8094,11 +8115,21 @@ async function runClaudeViaPty(params) {
|
|
|
8094
8115
|
cli: "claude",
|
|
8095
8116
|
args,
|
|
8096
8117
|
env: params.injectedEnv && Object.keys(params.injectedEnv).length > 0 ? params.injectedEnv : void 0,
|
|
8118
|
+
// Le répertoire que l'appelant a résolu — celui du projet de la
|
|
8119
|
+
// conversation. Sans lui, le PTY naissait dans le répertoire de l'agent,
|
|
8120
|
+
// c'est-à-dire là où Runeya a été lancé : une carte kanban du projet A
|
|
8121
|
+
// faisait travailler le CLI dans le code de Runeya.
|
|
8122
|
+
cwd: params.cwd || void 0,
|
|
8097
8123
|
cols: 120,
|
|
8098
8124
|
rows: 30,
|
|
8099
8125
|
sessionId,
|
|
8100
8126
|
enableMcp: !!params.mcpConfig,
|
|
8101
|
-
mcpApiBaseUrl: params.apiBaseUrl || void 0
|
|
8127
|
+
mcpApiBaseUrl: params.apiBaseUrl || void 0,
|
|
8128
|
+
// Sans lui, l'agent lance le CLI et son serveur MCP sans RUNEYA_API_TOKEN :
|
|
8129
|
+
// toutes les tools répondent « No auth token available », et `$RUNEYA_API_TOKEN`
|
|
8130
|
+
// ne résout à rien dans le shell du CLI. Le jeton est déjà là — la file le
|
|
8131
|
+
// signe elle-même, faute de requête HTTP d'où le tirer.
|
|
8132
|
+
mcpApiToken: params.apiToken || void 0
|
|
8102
8133
|
}).then(({ spawnId: id }) => {
|
|
8103
8134
|
spawnId = id;
|
|
8104
8135
|
if (signal.aborted) {
|
|
@@ -8673,7 +8704,7 @@ async function shutdownCodexAppServer() {
|
|
|
8673
8704
|
}
|
|
8674
8705
|
}
|
|
8675
8706
|
async function getAgentManager2() {
|
|
8676
|
-
const { agentManager: agentManager2 } = await import("./agent-manager-
|
|
8707
|
+
const { agentManager: agentManager2 } = await import("./agent-manager-UUSQK6S2.js");
|
|
8677
8708
|
return agentManager2;
|
|
8678
8709
|
}
|
|
8679
8710
|
function parseRuneyaCurlCommand(command) {
|
|
@@ -9359,7 +9390,10 @@ async function* drainSession(session, signal, isReconnect) {
|
|
|
9359
9390
|
}
|
|
9360
9391
|
async function buildCliSystemContext(conv, jwt) {
|
|
9361
9392
|
const allServices = await serviceStore.list();
|
|
9362
|
-
const
|
|
9393
|
+
const ref = conv ? await conversationStore.projectRefOf(conv) : null;
|
|
9394
|
+
const project = ref ? await projectStore.get(ref) : null;
|
|
9395
|
+
const ownServices = project ? allServices.filter((svc) => project.serviceIds.includes(svc.id)) : allServices;
|
|
9396
|
+
const resolvedServices = await Promise.all(ownServices.map(resolveServiceVariables));
|
|
9363
9397
|
let systemContext = generateAdminLlmsTxt(resolvedServices, jwt);
|
|
9364
9398
|
if (conv?.kanbanBoardId) {
|
|
9365
9399
|
const kanbanBoard = await kanbanStore.get(conv.kanbanBoardId);
|
|
@@ -9529,9 +9563,7 @@ var StoredMessageSchema = z16.object({
|
|
|
9529
9563
|
parts: z16.array(MessagePartSchema).max(200).optional()
|
|
9530
9564
|
});
|
|
9531
9565
|
async function conversationCwd(conv) {
|
|
9532
|
-
|
|
9533
|
-
if (!ref) return resolveAgentCwd({});
|
|
9534
|
-
return resolveAgentCwd({ projectId: ref.id, orgId: ref.orgId });
|
|
9566
|
+
return resolveConversationCwd(conv.id);
|
|
9535
9567
|
}
|
|
9536
9568
|
var chatRouter = router({
|
|
9537
9569
|
conversations: router({
|
|
@@ -10219,9 +10251,13 @@ ${apiDocs}` : systemContext;
|
|
|
10219
10251
|
cli,
|
|
10220
10252
|
args,
|
|
10221
10253
|
env: injectedEnv,
|
|
10222
|
-
// An explicit cwd wins;
|
|
10223
|
-
//
|
|
10224
|
-
|
|
10254
|
+
// An explicit cwd wins; then le projet de la conversation, s'il y en
|
|
10255
|
+
// a une : c'est elle que ce terminal poursuit, et son projet n'est
|
|
10256
|
+
// pas forcément celui ouvert dans l'onglet — ouvrir le terminal
|
|
10257
|
+
// d'une carte kanban depuis ailleurs lançait le CLI dans le
|
|
10258
|
+
// répertoire de lancement de Runeya. Sinon le service, puis le
|
|
10259
|
+
// projet actif — jamais le répertoire du serveur.
|
|
10260
|
+
cwd: input.cwd ?? (input.sessionId ? await resolveConversationCwd(input.sessionId) : void 0) ?? await resolveAgentCwd({
|
|
10225
10261
|
serviceId: input.serviceId,
|
|
10226
10262
|
projectId: input.projectId,
|
|
10227
10263
|
orgId: input.orgId
|
|
@@ -11588,9 +11624,11 @@ async function runNodeCall(conversationId, node, systemContext, userMessage, run
|
|
|
11588
11624
|
const result = await state.runner.run({
|
|
11589
11625
|
agentId: runOverrides?.agentId ?? agentManager.getDefaultAgentId() ?? void 0,
|
|
11590
11626
|
// Without this the CLI would run wherever the server was launched, whatever
|
|
11591
|
-
// project the scenario belongs to.
|
|
11592
|
-
//
|
|
11593
|
-
|
|
11627
|
+
// project the scenario belongs to. Read from the conversation, so un lancement
|
|
11628
|
+
// sans requête HTTP — la file kanban — est ancré comme les autres. No project:
|
|
11629
|
+
// left unset, so the runner keeps its own default rather than being sent to
|
|
11630
|
+
// this machine's path.
|
|
11631
|
+
cwd: await resolveConversationCwd(conversationId),
|
|
11594
11632
|
sessionId: state.sessionId,
|
|
11595
11633
|
prompt: currentPrompt,
|
|
11596
11634
|
isResume: state.isResume,
|
|
@@ -12468,7 +12506,6 @@ var scenarioRouter = router({
|
|
|
12468
12506
|
runnerOverride: input.runnerOverride,
|
|
12469
12507
|
modelOverride: input.modelOverride,
|
|
12470
12508
|
agentId: input.agentId,
|
|
12471
|
-
projectId: input.projectId,
|
|
12472
12509
|
orgId: input.orgId
|
|
12473
12510
|
}, !!input.resume);
|
|
12474
12511
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -12664,7 +12701,8 @@ var debugRouter = router({
|
|
|
12664
12701
|
});
|
|
12665
12702
|
|
|
12666
12703
|
// ../server/src/trpc/routers/diagnostics.ts
|
|
12667
|
-
import { z as
|
|
12704
|
+
import { z as z20 } from "zod";
|
|
12705
|
+
import { TRPCError as TRPCError18 } from "@trpc/server";
|
|
12668
12706
|
|
|
12669
12707
|
// ../server/src/services/log-buffer.ts
|
|
12670
12708
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
@@ -12732,132 +12770,10 @@ function onEntry(listener) {
|
|
|
12732
12770
|
return () => emitter.off("entry", listener);
|
|
12733
12771
|
}
|
|
12734
12772
|
|
|
12735
|
-
// ../server/src/services/instance-registry.ts
|
|
12736
|
-
import { mkdir as mkdir16, readdir as readdir5, readFile as readFile15, writeFile as writeFile14, rm as rm3 } from "fs/promises";
|
|
12737
|
-
import { join as join19 } from "path";
|
|
12738
|
-
import { z as z20 } from "zod";
|
|
12739
|
-
var InstanceFileSchema = z20.object({
|
|
12740
|
-
pid: z20.number().int().positive(),
|
|
12741
|
-
port: z20.number().int().min(1).max(65535),
|
|
12742
|
-
agentPort: z20.number().int().min(1).max(65535),
|
|
12743
|
-
cwd: z20.string().max(4096),
|
|
12744
|
-
dataDir: z20.string().max(4096),
|
|
12745
|
-
version: z20.string().max(64),
|
|
12746
|
-
fromDesktop: z20.boolean(),
|
|
12747
|
-
startedAt: z20.number().int().nonnegative()
|
|
12748
|
-
});
|
|
12749
|
-
function registryDir() {
|
|
12750
|
-
return join19(machineRoot(), "instances");
|
|
12751
|
-
}
|
|
12752
|
-
function fileFor(pid) {
|
|
12753
|
-
return join19(registryDir(), `${pid}.json`);
|
|
12754
|
-
}
|
|
12755
|
-
function selfRecord() {
|
|
12756
|
-
return {
|
|
12757
|
-
pid: process.pid,
|
|
12758
|
-
port: env.PORT,
|
|
12759
|
-
agentPort: env.RUNEYA_AGENT_PORT,
|
|
12760
|
-
cwd: process.cwd(),
|
|
12761
|
-
dataDir: env.DATA_DIR,
|
|
12762
|
-
version: process.env["RUNEYA_VERSION"] ?? "unknown",
|
|
12763
|
-
fromDesktop: env.FROM_DESKTOP,
|
|
12764
|
-
startedAt: Date.now()
|
|
12765
|
-
};
|
|
12766
|
-
}
|
|
12767
|
-
async function registerInstance() {
|
|
12768
|
-
try {
|
|
12769
|
-
await mkdir16(registryDir(), { recursive: true });
|
|
12770
|
-
await writeFile14(fileFor(process.pid), JSON.stringify(selfRecord(), null, 2));
|
|
12771
|
-
} catch (err) {
|
|
12772
|
-
console.log("[instances] Could not register this instance:", err.message);
|
|
12773
|
-
}
|
|
12774
|
-
}
|
|
12775
|
-
async function unregisterInstance() {
|
|
12776
|
-
await rm3(fileFor(process.pid), { force: true }).catch(() => {
|
|
12777
|
-
});
|
|
12778
|
-
}
|
|
12779
|
-
function isProcessAlive(pid) {
|
|
12780
|
-
try {
|
|
12781
|
-
process.kill(pid, 0);
|
|
12782
|
-
return true;
|
|
12783
|
-
} catch (err) {
|
|
12784
|
-
return err.code === "EPERM";
|
|
12785
|
-
}
|
|
12786
|
-
}
|
|
12787
|
-
async function probe(port) {
|
|
12788
|
-
try {
|
|
12789
|
-
const res = await fetch(`http://127.0.0.1:${port}/api/trpc/health.check`, {
|
|
12790
|
-
signal: AbortSignal.timeout(1500)
|
|
12791
|
-
});
|
|
12792
|
-
if (!res.ok) return { ok: false, uptimeMs: null, version: null };
|
|
12793
|
-
const body = await res.json();
|
|
12794
|
-
const data = body.result?.data;
|
|
12795
|
-
return { ok: true, uptimeMs: data?.uptime ?? null, version: data?.version ?? null };
|
|
12796
|
-
} catch {
|
|
12797
|
-
return { ok: false, uptimeMs: null, version: null };
|
|
12798
|
-
}
|
|
12799
|
-
}
|
|
12800
|
-
async function listInstances() {
|
|
12801
|
-
let files;
|
|
12802
|
-
try {
|
|
12803
|
-
files = await readdir5(registryDir());
|
|
12804
|
-
} catch {
|
|
12805
|
-
return [];
|
|
12806
|
-
}
|
|
12807
|
-
const found = await Promise.all(
|
|
12808
|
-
files.filter((f) => f.endsWith(".json")).map(async (name) => {
|
|
12809
|
-
const path = join19(registryDir(), name);
|
|
12810
|
-
let record;
|
|
12811
|
-
try {
|
|
12812
|
-
const parsed = InstanceFileSchema.safeParse(JSON.parse(await readFile15(path, "utf8")));
|
|
12813
|
-
if (!parsed.success) {
|
|
12814
|
-
await rm3(path, { force: true }).catch(() => {
|
|
12815
|
-
});
|
|
12816
|
-
return null;
|
|
12817
|
-
}
|
|
12818
|
-
record = parsed.data;
|
|
12819
|
-
} catch {
|
|
12820
|
-
return null;
|
|
12821
|
-
}
|
|
12822
|
-
if (!isProcessAlive(record.pid)) {
|
|
12823
|
-
await rm3(path, { force: true }).catch(() => {
|
|
12824
|
-
});
|
|
12825
|
-
return null;
|
|
12826
|
-
}
|
|
12827
|
-
const { ok, uptimeMs, version } = await probe(record.port);
|
|
12828
|
-
if (!ok) {
|
|
12829
|
-
await rm3(path, { force: true }).catch(() => {
|
|
12830
|
-
});
|
|
12831
|
-
return null;
|
|
12832
|
-
}
|
|
12833
|
-
return {
|
|
12834
|
-
...record,
|
|
12835
|
-
// Trust the live answer over the file: the file was written at boot
|
|
12836
|
-
// and an upgraded instance would keep claiming its old version.
|
|
12837
|
-
version: version ?? record.version,
|
|
12838
|
-
reachable: true,
|
|
12839
|
-
current: record.pid === process.pid,
|
|
12840
|
-
uptimeMs
|
|
12841
|
-
};
|
|
12842
|
-
})
|
|
12843
|
-
);
|
|
12844
|
-
return dedupeByPort(found.filter((i) => i !== null)).sort((a, b) => Number(b.current) - Number(a.current) || a.port - b.port);
|
|
12845
|
-
}
|
|
12846
|
-
function dedupeByPort(instances) {
|
|
12847
|
-
const byPort = /* @__PURE__ */ new Map();
|
|
12848
|
-
for (const instance of instances) {
|
|
12849
|
-
const kept = byPort.get(instance.port);
|
|
12850
|
-
if (!kept || instance.current || !kept.current && instance.startedAt > kept.startedAt) {
|
|
12851
|
-
byPort.set(instance.port, instance);
|
|
12852
|
-
}
|
|
12853
|
-
}
|
|
12854
|
-
return [...byPort.values()];
|
|
12855
|
-
}
|
|
12856
|
-
|
|
12857
12773
|
// ../server/src/trpc/routers/diagnostics.ts
|
|
12858
12774
|
var diagnosticsRouter = router({
|
|
12859
12775
|
/** Backlog for a client that just opened the panel. */
|
|
12860
|
-
recentLogs: protectedProcedure.input(
|
|
12776
|
+
recentLogs: protectedProcedure.input(z20.object({ limit: z20.number().int().min(1).max(2e3).default(500) }).optional()).query(({ input }) => recentEntries(input?.limit ?? 500)),
|
|
12861
12777
|
/**
|
|
12862
12778
|
* Live log stream.
|
|
12863
12779
|
*
|
|
@@ -12866,7 +12782,7 @@ var diagnosticsRouter = router({
|
|
|
12866
12782
|
* silently loses lines, which is precisely what someone watching a boot
|
|
12867
12783
|
* failure cannot afford.
|
|
12868
12784
|
*/
|
|
12869
|
-
streamLogs: protectedProcedure.input(
|
|
12785
|
+
streamLogs: protectedProcedure.input(z20.object({ afterSeq: z20.number().int().nonnegative().default(0) }).optional()).subscription(async function* ({ input, signal }) {
|
|
12870
12786
|
for (const entry of entriesSince(input?.afterSeq ?? 0)) yield entry;
|
|
12871
12787
|
const queue = [];
|
|
12872
12788
|
let wake = null;
|
|
@@ -12891,11 +12807,33 @@ var diagnosticsRouter = router({
|
|
|
12891
12807
|
}
|
|
12892
12808
|
}),
|
|
12893
12809
|
/** Every Runeya answering on this machine, this one included. */
|
|
12894
|
-
instances: protectedProcedure.query(() => listInstances())
|
|
12810
|
+
instances: protectedProcedure.query(() => listInstances()),
|
|
12811
|
+
/**
|
|
12812
|
+
* Stop another Runeya on this machine — its agent first, then its server.
|
|
12813
|
+
*
|
|
12814
|
+
* The pid is a *selector*, not an instruction: it is looked up in the list
|
|
12815
|
+
* `listInstances()` has just vouched for, and anything absent from it is
|
|
12816
|
+
* refused. That is what keeps this from being a way to signal arbitrary
|
|
12817
|
+
* processes with the server's privileges.
|
|
12818
|
+
*/
|
|
12819
|
+
killInstance: protectedProcedure.input(z20.object({ pid: z20.number().int().positive() })).mutation(async ({ input }) => {
|
|
12820
|
+
try {
|
|
12821
|
+
return await killInstance(input.pid);
|
|
12822
|
+
} catch (err) {
|
|
12823
|
+
const reason = err.message;
|
|
12824
|
+
if (reason === "instance-not-found") {
|
|
12825
|
+
throw new TRPCError18({ code: "NOT_FOUND", message: "No live Runeya instance with this pid." });
|
|
12826
|
+
}
|
|
12827
|
+
if (reason === "instance-is-current") {
|
|
12828
|
+
throw new TRPCError18({ code: "BAD_REQUEST", message: "This instance cannot stop itself." });
|
|
12829
|
+
}
|
|
12830
|
+
throw new TRPCError18({ code: "INTERNAL_SERVER_ERROR", message: reason });
|
|
12831
|
+
}
|
|
12832
|
+
})
|
|
12895
12833
|
});
|
|
12896
12834
|
|
|
12897
12835
|
// ../server/src/trpc/routers/kanban-queue.ts
|
|
12898
|
-
import { z as
|
|
12836
|
+
import { z as z21 } from "zod";
|
|
12899
12837
|
|
|
12900
12838
|
// ../server/src/services/kanban-queue.ts
|
|
12901
12839
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
@@ -13083,36 +13021,36 @@ var kanbanQueue = new KanbanQueueService();
|
|
|
13083
13021
|
// ../server/src/trpc/routers/kanban-queue.ts
|
|
13084
13022
|
var kanbanQueueRouter = router({
|
|
13085
13023
|
getState: protectedProcedure.query(() => kanbanQueue.getState()),
|
|
13086
|
-
add: protectedProcedure.input(
|
|
13087
|
-
boardId:
|
|
13088
|
-
cardId:
|
|
13089
|
-
runner:
|
|
13090
|
-
model:
|
|
13091
|
-
scenarioId:
|
|
13024
|
+
add: protectedProcedure.input(z21.object({
|
|
13025
|
+
boardId: z21.string().max(128),
|
|
13026
|
+
cardId: z21.string().max(128),
|
|
13027
|
+
runner: z21.enum(["claude-code", "codex"]).optional(),
|
|
13028
|
+
model: z21.string().max(128).optional(),
|
|
13029
|
+
scenarioId: z21.string().max(128).optional(),
|
|
13092
13030
|
// Le projet ouvert quand la carte a été mise en file : la conversation
|
|
13093
13031
|
// qui en naîtra lui appartient, plutôt que de rester orpheline et de
|
|
13094
13032
|
// s'afficher dans tous les projets.
|
|
13095
|
-
projectId:
|
|
13096
|
-
orgId:
|
|
13033
|
+
projectId: z21.string().max(128).optional(),
|
|
13034
|
+
orgId: z21.string().max(128).nullable().optional()
|
|
13097
13035
|
})).mutation(({ input }) => kanbanQueue.add(input)),
|
|
13098
|
-
addColumn: protectedProcedure.input(
|
|
13099
|
-
boardId:
|
|
13100
|
-
columnId:
|
|
13101
|
-
runner:
|
|
13102
|
-
model:
|
|
13103
|
-
scenarioId:
|
|
13104
|
-
projectId:
|
|
13105
|
-
orgId:
|
|
13036
|
+
addColumn: protectedProcedure.input(z21.object({
|
|
13037
|
+
boardId: z21.string().max(128),
|
|
13038
|
+
columnId: z21.string().max(128),
|
|
13039
|
+
runner: z21.enum(["claude-code", "codex"]).optional(),
|
|
13040
|
+
model: z21.string().max(128).optional(),
|
|
13041
|
+
scenarioId: z21.string().max(128).optional(),
|
|
13042
|
+
projectId: z21.string().max(128).optional(),
|
|
13043
|
+
orgId: z21.string().max(128).nullable().optional()
|
|
13106
13044
|
})).mutation(async ({ input }) => {
|
|
13107
13045
|
const { boardId, columnId, ...options } = input;
|
|
13108
13046
|
return kanbanQueue.addColumn(boardId, columnId, options);
|
|
13109
13047
|
}),
|
|
13110
|
-
remove: protectedProcedure.input(
|
|
13048
|
+
remove: protectedProcedure.input(z21.object({ itemId: z21.string().max(128) })).mutation(({ input }) => ({ removed: kanbanQueue.remove(input.itemId) })),
|
|
13111
13049
|
stopAll: protectedProcedure.mutation(() => {
|
|
13112
13050
|
kanbanQueue.stopAll();
|
|
13113
13051
|
return { ok: true };
|
|
13114
13052
|
}),
|
|
13115
|
-
setMaxConcurrent: protectedProcedure.input(
|
|
13053
|
+
setMaxConcurrent: protectedProcedure.input(z21.object({ max: z21.number().int().min(1).max(20) })).mutation(({ input }) => {
|
|
13116
13054
|
kanbanQueue.setMaxConcurrent(input.max);
|
|
13117
13055
|
return kanbanQueue.getState();
|
|
13118
13056
|
}),
|
|
@@ -13146,12 +13084,12 @@ var kanbanQueueRouter = router({
|
|
|
13146
13084
|
});
|
|
13147
13085
|
|
|
13148
13086
|
// ../server/src/trpc/routers/kanban.ts
|
|
13149
|
-
import { mkdir as
|
|
13087
|
+
import { mkdir as mkdir16, readdir as readdir5, rm as rm3, stat as stat5, writeFile as writeFile14 } from "fs/promises";
|
|
13150
13088
|
import { createHash } from "crypto";
|
|
13151
13089
|
import { tmpdir } from "os";
|
|
13152
|
-
import { join as
|
|
13153
|
-
import { z as
|
|
13154
|
-
import { TRPCError as
|
|
13090
|
+
import { join as join19 } from "path";
|
|
13091
|
+
import { z as z22 } from "zod";
|
|
13092
|
+
import { TRPCError as TRPCError19 } from "@trpc/server";
|
|
13155
13093
|
function summarizeCard(card, boardId, columnTitle) {
|
|
13156
13094
|
return {
|
|
13157
13095
|
boardId,
|
|
@@ -13200,37 +13138,37 @@ function safeDecode(value) {
|
|
|
13200
13138
|
}
|
|
13201
13139
|
var MAX_ATTACHMENT_BYTES = 55 * 1024 * 1024;
|
|
13202
13140
|
function attachmentCacheDir() {
|
|
13203
|
-
return
|
|
13141
|
+
return join19(tmpdir(), "runeya-kanban-attachments");
|
|
13204
13142
|
}
|
|
13205
13143
|
var ATTACHMENT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13206
13144
|
async function purgeStaleAttachments(dir, ttlMs = ATTACHMENT_CACHE_TTL_MS) {
|
|
13207
13145
|
try {
|
|
13208
13146
|
const cutoff = Date.now() - ttlMs;
|
|
13209
|
-
for (const name of await
|
|
13210
|
-
const path =
|
|
13147
|
+
for (const name of await readdir5(dir)) {
|
|
13148
|
+
const path = join19(dir, name);
|
|
13211
13149
|
const info = await stat5(path).catch(() => null);
|
|
13212
|
-
if (info && info.mtimeMs < cutoff) await
|
|
13150
|
+
if (info && info.mtimeMs < cutoff) await rm3(path, { force: true });
|
|
13213
13151
|
}
|
|
13214
13152
|
} catch {
|
|
13215
13153
|
}
|
|
13216
13154
|
}
|
|
13217
13155
|
function attachmentCacheDirs() {
|
|
13218
|
-
return [attachmentCacheDir(),
|
|
13156
|
+
return [attachmentCacheDir(), join19(machineRoot(), "kanban-attachments")];
|
|
13219
13157
|
}
|
|
13220
13158
|
async function storeAttachment(id, name, body, dirs = attachmentCacheDirs()) {
|
|
13221
13159
|
const refusals = [];
|
|
13222
13160
|
for (const dir of dirs) {
|
|
13223
13161
|
try {
|
|
13224
|
-
await
|
|
13225
|
-
const path =
|
|
13226
|
-
await
|
|
13162
|
+
await mkdir16(dir, { recursive: true });
|
|
13163
|
+
const path = join19(dir, safeLocalName(id, name));
|
|
13164
|
+
await writeFile14(path, body);
|
|
13227
13165
|
void purgeStaleAttachments(dir);
|
|
13228
13166
|
return path;
|
|
13229
13167
|
} catch (err) {
|
|
13230
13168
|
refusals.push(`${dir} (${err instanceof Error ? err.message : String(err)})`);
|
|
13231
13169
|
}
|
|
13232
13170
|
}
|
|
13233
|
-
throw new
|
|
13171
|
+
throw new TRPCError19({
|
|
13234
13172
|
code: "INTERNAL_SERVER_ERROR",
|
|
13235
13173
|
message: `Impossible d'\xE9crire la pi\xE8ce jointe sur cette machine. Emplacements essay\xE9s : ${refusals.join(" ; ")}. V\xE9rifiez les droits d'\xE9criture, ou l'espace disque.`
|
|
13236
13174
|
});
|
|
@@ -13246,9 +13184,9 @@ var kanbanRouter = router({
|
|
|
13246
13184
|
* card graph, so listing them in full would be megabytes to answer "which
|
|
13247
13185
|
* boards exist?". Use `getBoard` for the cards of one.
|
|
13248
13186
|
*/
|
|
13249
|
-
listBoards: protectedProcedure.input(
|
|
13250
|
-
includeArchived:
|
|
13251
|
-
organizationId:
|
|
13187
|
+
listBoards: protectedProcedure.input(z22.object({
|
|
13188
|
+
includeArchived: z22.boolean().optional(),
|
|
13189
|
+
organizationId: z22.string().max(128).optional()
|
|
13252
13190
|
}).optional()).query(async ({ input }) => {
|
|
13253
13191
|
const { organizationId, boards } = await kanbanStore.list(
|
|
13254
13192
|
input?.includeArchived ?? false,
|
|
@@ -13274,7 +13212,7 @@ var kanbanRouter = router({
|
|
|
13274
13212
|
};
|
|
13275
13213
|
}),
|
|
13276
13214
|
/** One board: its columns, and the cards in each. */
|
|
13277
|
-
getBoard: protectedProcedure.input(
|
|
13215
|
+
getBoard: protectedProcedure.input(z22.object({ boardId: z22.string().max(128) })).query(async ({ input }) => {
|
|
13278
13216
|
const board = await kanbanStore.get(input.boardId);
|
|
13279
13217
|
if (!board) return { board: null };
|
|
13280
13218
|
const { links } = await kanbanStore.cardLinks(input.boardId);
|
|
@@ -13313,11 +13251,11 @@ var kanbanRouter = router({
|
|
|
13313
13251
|
* organization — or one board when `boardId` is given. Archived boards are
|
|
13314
13252
|
* left out: they are what the team decided to stop looking at.
|
|
13315
13253
|
*/
|
|
13316
|
-
searchCards: protectedProcedure.input(
|
|
13317
|
-
query:
|
|
13318
|
-
boardId:
|
|
13319
|
-
limit:
|
|
13320
|
-
organizationId:
|
|
13254
|
+
searchCards: protectedProcedure.input(z22.object({
|
|
13255
|
+
query: z22.string().min(1).max(256),
|
|
13256
|
+
boardId: z22.string().max(128).optional(),
|
|
13257
|
+
limit: z22.number().int().min(1).max(100).optional(),
|
|
13258
|
+
organizationId: z22.string().max(128).optional()
|
|
13321
13259
|
})).query(async ({ input }) => {
|
|
13322
13260
|
const needle = normalize(input.query);
|
|
13323
13261
|
const ids = input.boardId ? [input.boardId] : (await kanbanStore.list(false, input.organizationId)).boards.map((b) => b.id);
|
|
@@ -13336,10 +13274,10 @@ var kanbanRouter = router({
|
|
|
13336
13274
|
* qu'un `commentCount`, et rien n'en donnait le texte. Cette route est la
|
|
13337
13275
|
* relecture, pour une seule carte : c'est le journal qui pèse, pas le reste.
|
|
13338
13276
|
*/
|
|
13339
|
-
getCardComments: protectedProcedure.input(
|
|
13340
|
-
boardId:
|
|
13341
|
-
cardId:
|
|
13342
|
-
limit:
|
|
13277
|
+
getCardComments: protectedProcedure.input(z22.object({
|
|
13278
|
+
boardId: z22.string().max(128),
|
|
13279
|
+
cardId: z22.string().max(128),
|
|
13280
|
+
limit: z22.number().int().min(1).max(200).optional()
|
|
13343
13281
|
})).query(async ({ input }) => {
|
|
13344
13282
|
const history = await kanbanStore.cardHistory(input.boardId, input.cardId);
|
|
13345
13283
|
const comments = history.filter((e) => e.type === "comment");
|
|
@@ -13368,18 +13306,18 @@ var kanbanRouter = router({
|
|
|
13368
13306
|
* `getBoard` en donne déjà l'essentiel carte par carte ; cette route sert
|
|
13369
13307
|
* quand c'est l'arbre lui-même qu'on veut, dates et auteurs compris.
|
|
13370
13308
|
*/
|
|
13371
|
-
getCardLinks: protectedProcedure.input(
|
|
13309
|
+
getCardLinks: protectedProcedure.input(z22.object({ boardId: z22.string().max(128) })).query(async ({ input }) => kanbanStore.cardLinks(input.boardId)),
|
|
13372
13310
|
/**
|
|
13373
13311
|
* Rattacher une carte à une carte parente, ou l'en détacher.
|
|
13374
13312
|
*
|
|
13375
13313
|
* Sans `parentCardId`, le lien est retiré. Le parent peut vivre sur un autre
|
|
13376
13314
|
* tableau : `parentBoardId` le désigne alors, sinon c'est celui de la carte.
|
|
13377
13315
|
*/
|
|
13378
|
-
setCardParent: protectedProcedure.input(
|
|
13379
|
-
boardId:
|
|
13380
|
-
cardId:
|
|
13381
|
-
parentCardId:
|
|
13382
|
-
parentBoardId:
|
|
13316
|
+
setCardParent: protectedProcedure.input(z22.object({
|
|
13317
|
+
boardId: z22.string().max(128),
|
|
13318
|
+
cardId: z22.string().max(128),
|
|
13319
|
+
parentCardId: z22.string().max(128).optional(),
|
|
13320
|
+
parentBoardId: z22.string().max(128).optional()
|
|
13383
13321
|
})).mutation(async ({ input }) => {
|
|
13384
13322
|
const ok = await kanbanStore.setCardParent(
|
|
13385
13323
|
input.boardId,
|
|
@@ -13387,35 +13325,35 @@ var kanbanRouter = router({
|
|
|
13387
13325
|
input.parentCardId ? { boardId: input.parentBoardId ?? input.boardId, cardId: input.parentCardId } : null
|
|
13388
13326
|
);
|
|
13389
13327
|
if (!ok) {
|
|
13390
|
-
throw new
|
|
13328
|
+
throw new TRPCError19({ code: "BAD_REQUEST", message: "The cloud refused the link (unknown card, or a cycle)." });
|
|
13391
13329
|
}
|
|
13392
13330
|
return { ok: true };
|
|
13393
13331
|
}),
|
|
13394
|
-
moveCard: protectedProcedure.input(
|
|
13395
|
-
boardId:
|
|
13396
|
-
cardId:
|
|
13397
|
-
targetColumn:
|
|
13332
|
+
moveCard: protectedProcedure.input(z22.object({
|
|
13333
|
+
boardId: z22.string().max(128),
|
|
13334
|
+
cardId: z22.string().max(128),
|
|
13335
|
+
targetColumn: z22.string().max(256)
|
|
13398
13336
|
})).mutation(async ({ input }) => {
|
|
13399
13337
|
await executeKanbanCommands(input.boardId, input.cardId, [
|
|
13400
13338
|
{ type: "move_card", columnTitle: input.targetColumn }
|
|
13401
13339
|
]);
|
|
13402
13340
|
return { ok: true };
|
|
13403
13341
|
}),
|
|
13404
|
-
addComment: protectedProcedure.input(
|
|
13405
|
-
boardId:
|
|
13406
|
-
cardId:
|
|
13407
|
-
comment:
|
|
13342
|
+
addComment: protectedProcedure.input(z22.object({
|
|
13343
|
+
boardId: z22.string().max(128),
|
|
13344
|
+
cardId: z22.string().max(128),
|
|
13345
|
+
comment: z22.string().max(2e4)
|
|
13408
13346
|
})).mutation(async ({ input }) => {
|
|
13409
13347
|
await executeKanbanCommands(input.boardId, input.cardId, [
|
|
13410
13348
|
{ type: "add_comment", comment: input.comment }
|
|
13411
13349
|
]);
|
|
13412
13350
|
return { ok: true };
|
|
13413
13351
|
}),
|
|
13414
|
-
addCard: protectedProcedure.input(
|
|
13415
|
-
boardId:
|
|
13416
|
-
column:
|
|
13417
|
-
title:
|
|
13418
|
-
content:
|
|
13352
|
+
addCard: protectedProcedure.input(z22.object({
|
|
13353
|
+
boardId: z22.string().max(128),
|
|
13354
|
+
column: z22.string().max(256),
|
|
13355
|
+
title: z22.string().max(512),
|
|
13356
|
+
content: z22.string().max(2e4).optional()
|
|
13419
13357
|
})).mutation(async ({ input }) => {
|
|
13420
13358
|
await executeKanbanBoardCommands(input.boardId, [
|
|
13421
13359
|
{ type: "add_card", columnTitle: input.column, cardTitle: input.title, cardContent: input.content }
|
|
@@ -13427,28 +13365,28 @@ var kanbanRouter = router({
|
|
|
13427
13365
|
* agent can open it with its own file tools. The bytes never go through the
|
|
13428
13366
|
* prompt: the card context lists the attachments, this returns a path.
|
|
13429
13367
|
*/
|
|
13430
|
-
downloadAttachment: protectedProcedure.input(
|
|
13368
|
+
downloadAttachment: protectedProcedure.input(z22.object({ attachmentId: z22.string().max(128) })).mutation(async ({ input }) => {
|
|
13431
13369
|
const id = input.attachmentId;
|
|
13432
13370
|
if (!ATTACHMENT_ID_REGEX.test(id)) {
|
|
13433
|
-
throw new
|
|
13371
|
+
throw new TRPCError19({ code: "BAD_REQUEST", message: "Invalid attachment id" });
|
|
13434
13372
|
}
|
|
13435
13373
|
const res = await cloudFetch2(`/api/kanban/file/${id}`, { timeoutMs: 12e4 });
|
|
13436
13374
|
if (res.status === 404) {
|
|
13437
|
-
throw new
|
|
13375
|
+
throw new TRPCError19({ code: "NOT_FOUND", message: `Attachment ${id} not found` });
|
|
13438
13376
|
}
|
|
13439
13377
|
if (!res.ok) {
|
|
13440
|
-
throw new
|
|
13378
|
+
throw new TRPCError19({
|
|
13441
13379
|
code: res.status === 403 ? "FORBIDDEN" : "INTERNAL_SERVER_ERROR",
|
|
13442
13380
|
message: `Cloud refused the attachment (HTTP ${res.status})`
|
|
13443
13381
|
});
|
|
13444
13382
|
}
|
|
13445
13383
|
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
13446
13384
|
if (declared > MAX_ATTACHMENT_BYTES) {
|
|
13447
|
-
throw new
|
|
13385
|
+
throw new TRPCError19({ code: "PAYLOAD_TOO_LARGE", message: "Attachment too large to download" });
|
|
13448
13386
|
}
|
|
13449
13387
|
const body = Buffer.from(await res.arrayBuffer());
|
|
13450
13388
|
if (body.length > MAX_ATTACHMENT_BYTES) {
|
|
13451
|
-
throw new
|
|
13389
|
+
throw new TRPCError19({ code: "PAYLOAD_TOO_LARGE", message: "Attachment too large to download" });
|
|
13452
13390
|
}
|
|
13453
13391
|
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
13454
13392
|
const disposition = res.headers.get("content-disposition") ?? "";
|
|
@@ -13460,20 +13398,21 @@ var kanbanRouter = router({
|
|
|
13460
13398
|
});
|
|
13461
13399
|
|
|
13462
13400
|
// ../server/src/trpc/routers/terminal.ts
|
|
13463
|
-
import { z as
|
|
13464
|
-
import { TRPCError as
|
|
13401
|
+
import { z as z23 } from "zod";
|
|
13402
|
+
import { TRPCError as TRPCError20 } from "@trpc/server";
|
|
13465
13403
|
var terminalRouter = router({
|
|
13466
13404
|
/** Create a PTY shell session on the target agent */
|
|
13467
|
-
create: protectedProcedure.input(
|
|
13468
|
-
agentId:
|
|
13469
|
-
cwd:
|
|
13405
|
+
create: protectedProcedure.input(z23.object({
|
|
13406
|
+
agentId: z23.string().max(128),
|
|
13407
|
+
cwd: z23.string().max(512).optional(),
|
|
13470
13408
|
/** Anchors the terminal when no explicit cwd is given. */
|
|
13471
|
-
serviceId:
|
|
13472
|
-
projectId:
|
|
13409
|
+
serviceId: z23.string().max(128).optional(),
|
|
13410
|
+
projectId: z23.string().max(128).optional(),
|
|
13473
13411
|
/** L'organisation du projet : un id seul peut en nommer un par organisation. */
|
|
13474
|
-
orgId:
|
|
13475
|
-
cols:
|
|
13476
|
-
rows:
|
|
13412
|
+
orgId: z23.string().max(128).nullable().optional(),
|
|
13413
|
+
cols: z23.number().int().min(1).max(512).optional(),
|
|
13414
|
+
rows: z23.number().int().min(1).max(256).optional(),
|
|
13415
|
+
title: z23.string().max(128).optional()
|
|
13477
13416
|
})).mutation(async ({ input }) => {
|
|
13478
13417
|
const result = await agentManager.agentMutation(
|
|
13479
13418
|
input.agentId,
|
|
@@ -13487,16 +13426,37 @@ var terminalRouter = router({
|
|
|
13487
13426
|
orgId: input.orgId
|
|
13488
13427
|
}),
|
|
13489
13428
|
cols: input.cols,
|
|
13490
|
-
rows: input.rows
|
|
13429
|
+
rows: input.rows,
|
|
13430
|
+
title: input.title
|
|
13491
13431
|
}
|
|
13492
13432
|
);
|
|
13493
13433
|
return result;
|
|
13494
13434
|
}),
|
|
13435
|
+
/** Les sessions PTY encore vivantes sur l'agent (onglets à retrouver) */
|
|
13436
|
+
list: protectedProcedure.input(z23.object({ agentId: z23.string().max(128) })).query(async ({ input }) => {
|
|
13437
|
+
return agentManager.agentQuery(
|
|
13438
|
+
input.agentId,
|
|
13439
|
+
"terminal.list"
|
|
13440
|
+
);
|
|
13441
|
+
}),
|
|
13442
|
+
/** Renommer un onglet */
|
|
13443
|
+
rename: protectedProcedure.input(z23.object({
|
|
13444
|
+
agentId: z23.string().max(128),
|
|
13445
|
+
sessionId: z23.string().max(128),
|
|
13446
|
+
title: z23.string().max(128)
|
|
13447
|
+
})).mutation(async ({ input }) => {
|
|
13448
|
+
await agentManager.agentMutation(
|
|
13449
|
+
input.agentId,
|
|
13450
|
+
"terminal.rename",
|
|
13451
|
+
{ sessionId: input.sessionId, title: input.title }
|
|
13452
|
+
);
|
|
13453
|
+
return { ok: true };
|
|
13454
|
+
}),
|
|
13495
13455
|
/** Send keystrokes / data to a PTY session */
|
|
13496
|
-
write: protectedProcedure.input(
|
|
13497
|
-
agentId:
|
|
13498
|
-
sessionId:
|
|
13499
|
-
data:
|
|
13456
|
+
write: protectedProcedure.input(z23.object({
|
|
13457
|
+
agentId: z23.string().max(128),
|
|
13458
|
+
sessionId: z23.string().max(128),
|
|
13459
|
+
data: z23.string().max(65536)
|
|
13500
13460
|
})).mutation(async ({ input }) => {
|
|
13501
13461
|
await agentManager.agentMutation(
|
|
13502
13462
|
input.agentId,
|
|
@@ -13506,11 +13466,11 @@ var terminalRouter = router({
|
|
|
13506
13466
|
return { ok: true };
|
|
13507
13467
|
}),
|
|
13508
13468
|
/** Resize the PTY */
|
|
13509
|
-
resize: protectedProcedure.input(
|
|
13510
|
-
agentId:
|
|
13511
|
-
sessionId:
|
|
13512
|
-
cols:
|
|
13513
|
-
rows:
|
|
13469
|
+
resize: protectedProcedure.input(z23.object({
|
|
13470
|
+
agentId: z23.string().max(128),
|
|
13471
|
+
sessionId: z23.string().max(128),
|
|
13472
|
+
cols: z23.number().int().min(1).max(512),
|
|
13473
|
+
rows: z23.number().int().min(1).max(256)
|
|
13514
13474
|
})).mutation(async ({ input }) => {
|
|
13515
13475
|
await agentManager.agentMutation(
|
|
13516
13476
|
input.agentId,
|
|
@@ -13520,9 +13480,9 @@ var terminalRouter = router({
|
|
|
13520
13480
|
return { ok: true };
|
|
13521
13481
|
}),
|
|
13522
13482
|
/** Destroy a PTY session */
|
|
13523
|
-
destroy: protectedProcedure.input(
|
|
13524
|
-
agentId:
|
|
13525
|
-
sessionId:
|
|
13483
|
+
destroy: protectedProcedure.input(z23.object({
|
|
13484
|
+
agentId: z23.string().max(128),
|
|
13485
|
+
sessionId: z23.string().max(128)
|
|
13526
13486
|
})).mutation(async ({ input }) => {
|
|
13527
13487
|
await agentManager.agentMutation(
|
|
13528
13488
|
input.agentId,
|
|
@@ -13532,9 +13492,11 @@ var terminalRouter = router({
|
|
|
13532
13492
|
return { ok: true };
|
|
13533
13493
|
}),
|
|
13534
13494
|
/** Stream PTY output — proxied via AgentWSBridge dynamic subscription */
|
|
13535
|
-
onData: protectedProcedure.input(
|
|
13536
|
-
agentId:
|
|
13537
|
-
sessionId:
|
|
13495
|
+
onData: protectedProcedure.input(z23.object({
|
|
13496
|
+
agentId: z23.string().max(128),
|
|
13497
|
+
sessionId: z23.string().max(128),
|
|
13498
|
+
/** Rejouer l'historique de la session avant le flux temps réel. */
|
|
13499
|
+
replay: z23.boolean().optional()
|
|
13538
13500
|
})).subscription(async function* ({ input }) {
|
|
13539
13501
|
const queue = [];
|
|
13540
13502
|
let resolve7 = null;
|
|
@@ -13544,7 +13506,7 @@ var terminalRouter = router({
|
|
|
13544
13506
|
unsubscribe = agentManager.agentSubscribe(
|
|
13545
13507
|
input.agentId,
|
|
13546
13508
|
"terminal.onData",
|
|
13547
|
-
{ sessionId: input.sessionId },
|
|
13509
|
+
{ sessionId: input.sessionId, replay: input.replay },
|
|
13548
13510
|
(data) => {
|
|
13549
13511
|
if (typeof data === "string") queue.push(data);
|
|
13550
13512
|
if (resolve7) {
|
|
@@ -13561,7 +13523,7 @@ var terminalRouter = router({
|
|
|
13561
13523
|
}
|
|
13562
13524
|
);
|
|
13563
13525
|
} catch (err) {
|
|
13564
|
-
throw new
|
|
13526
|
+
throw new TRPCError20({
|
|
13565
13527
|
code: "BAD_REQUEST",
|
|
13566
13528
|
message: `Cannot subscribe to terminal: ${err.message}`
|
|
13567
13529
|
});
|
|
@@ -13583,11 +13545,11 @@ var terminalRouter = router({
|
|
|
13583
13545
|
});
|
|
13584
13546
|
|
|
13585
13547
|
// ../server/src/trpc/routers/utility.ts
|
|
13586
|
-
import { z as
|
|
13548
|
+
import { z as z24 } from "zod";
|
|
13587
13549
|
var MAX_WAIT_MS = 5 * 60 * 1e3;
|
|
13588
13550
|
var utilityRouter = router({
|
|
13589
|
-
wait: protectedProcedure.input(
|
|
13590
|
-
ms:
|
|
13551
|
+
wait: protectedProcedure.input(z24.object({
|
|
13552
|
+
ms: z24.number().int().min(0).max(MAX_WAIT_MS)
|
|
13591
13553
|
})).mutation(async ({ input }) => {
|
|
13592
13554
|
await new Promise((resolve7) => setTimeout(resolve7, input.ms));
|
|
13593
13555
|
return { waited: input.ms };
|
|
@@ -13595,15 +13557,15 @@ var utilityRouter = router({
|
|
|
13595
13557
|
});
|
|
13596
13558
|
|
|
13597
13559
|
// ../server/src/trpc/routers/wiki.ts
|
|
13598
|
-
import { z as
|
|
13599
|
-
import { TRPCError as
|
|
13600
|
-
import { readFile as
|
|
13601
|
-
import { resolve as resolvePath, join as
|
|
13602
|
-
var SCOPE_SCHEMA =
|
|
13603
|
-
|
|
13604
|
-
|
|
13560
|
+
import { z as z25 } from "zod";
|
|
13561
|
+
import { TRPCError as TRPCError21 } from "@trpc/server";
|
|
13562
|
+
import { readFile as readFile15, readdir as readdir6, stat as stat6, realpath, writeFile as writeFile15, mkdir as mkdir17, rename as rename12, rm as rm4 } from "fs/promises";
|
|
13563
|
+
import { resolve as resolvePath, join as join20, relative, sep as sep3, dirname as dirname9 } from "path";
|
|
13564
|
+
var SCOPE_SCHEMA = z25.union([
|
|
13565
|
+
z25.literal("global"),
|
|
13566
|
+
z25.string().regex(/^service:[A-Za-z0-9_-]{1,128}$/, "Invalid service scope")
|
|
13605
13567
|
]);
|
|
13606
|
-
var REL_PATH_SCHEMA =
|
|
13568
|
+
var REL_PATH_SCHEMA = z25.string().min(1).max(512).regex(/^[A-Za-z0-9_./-]+$/).refine((p) => !p.includes("..") && !p.startsWith("/"), {
|
|
13607
13569
|
message: "Path must be a relative path without traversal"
|
|
13608
13570
|
});
|
|
13609
13571
|
var MAX_FILES = 500;
|
|
@@ -13623,36 +13585,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
13623
13585
|
"target",
|
|
13624
13586
|
"out"
|
|
13625
13587
|
]);
|
|
13626
|
-
var DocsEntrySchema =
|
|
13627
|
-
|
|
13628
|
-
|
|
13588
|
+
var DocsEntrySchema = z25.lazy(() => z25.union([
|
|
13589
|
+
z25.object({ type: z25.literal("file"), name: z25.string(), path: z25.string() }),
|
|
13590
|
+
z25.object({ type: z25.literal("dir"), name: z25.string(), path: z25.string(), children: z25.array(DocsEntrySchema) })
|
|
13629
13591
|
]));
|
|
13630
13592
|
async function resolveScope(scope) {
|
|
13631
13593
|
if (scope === "global") {
|
|
13632
13594
|
const settings = await settingsManager.getSettings();
|
|
13633
13595
|
const sub = (settings.globalDocsPath?.trim() || "wiki").replace(/^[\\/]+|[\\/]+$/g, "");
|
|
13634
13596
|
if (sub.includes("..") || sub.includes("\0")) {
|
|
13635
|
-
throw new
|
|
13597
|
+
throw new TRPCError21({ code: "BAD_REQUEST", message: "Sous-chemin invalide" });
|
|
13636
13598
|
}
|
|
13637
13599
|
const dataDirAbs = resolvePath(env.DATA_DIR);
|
|
13638
|
-
await
|
|
13600
|
+
await mkdir17(dataDirAbs, { recursive: true });
|
|
13639
13601
|
const base = dirname9(await realpath(dataDirAbs));
|
|
13640
13602
|
const target = resolvePath(base, sub);
|
|
13641
13603
|
if (target !== base && !(target + sep3).startsWith(base + sep3)) {
|
|
13642
|
-
throw new
|
|
13604
|
+
throw new TRPCError21({ code: "FORBIDDEN", message: "Hors du dossier de donn\xE9es du serveur" });
|
|
13643
13605
|
}
|
|
13644
|
-
await
|
|
13606
|
+
await mkdir17(target, { recursive: true });
|
|
13645
13607
|
const root = await realpath(target);
|
|
13646
13608
|
if (root !== base && !(root + sep3).startsWith(base + sep3)) {
|
|
13647
|
-
throw new
|
|
13609
|
+
throw new TRPCError21({ code: "FORBIDDEN", message: "Le dossier wiki r\xE9sout hors du dossier de donn\xE9es" });
|
|
13648
13610
|
}
|
|
13649
13611
|
return { mode: "local", root };
|
|
13650
13612
|
}
|
|
13651
13613
|
const serviceId = scope.slice("service:".length);
|
|
13652
13614
|
const service = await serviceStore.get(serviceId);
|
|
13653
|
-
if (!service) throw new
|
|
13615
|
+
if (!service) throw new TRPCError21({ code: "NOT_FOUND", message: "Service introuvable" });
|
|
13654
13616
|
const agentId = await resolveEffectiveAgentId(service);
|
|
13655
|
-
if (!agentId) throw new
|
|
13617
|
+
if (!agentId) throw new TRPCError21({ code: "BAD_REQUEST", message: "Aucun agent configur\xE9 pour ce service" });
|
|
13656
13618
|
const cwd = await resolveWorkspaceCwd({ serviceId, override: service.docsPath?.trim() || void 0 });
|
|
13657
13619
|
return { mode: "agent", agentId, cwd };
|
|
13658
13620
|
}
|
|
@@ -13660,7 +13622,7 @@ async function walkLocal(root, dir, depth, counter) {
|
|
|
13660
13622
|
if (depth > MAX_DEPTH || counter.n >= MAX_FILES) return [];
|
|
13661
13623
|
let entries2;
|
|
13662
13624
|
try {
|
|
13663
|
-
entries2 = await
|
|
13625
|
+
entries2 = await readdir6(dir, { withFileTypes: true });
|
|
13664
13626
|
} catch {
|
|
13665
13627
|
return [];
|
|
13666
13628
|
}
|
|
@@ -13668,7 +13630,7 @@ async function walkLocal(root, dir, depth, counter) {
|
|
|
13668
13630
|
for (const e of entries2) {
|
|
13669
13631
|
if (counter.n >= MAX_FILES) break;
|
|
13670
13632
|
if (e.name.startsWith(".") && e.name !== ".") continue;
|
|
13671
|
-
const abs =
|
|
13633
|
+
const abs = join20(dir, e.name);
|
|
13672
13634
|
const rel = relative(root, abs).split(sep3).join("/");
|
|
13673
13635
|
if (e.isDirectory()) {
|
|
13674
13636
|
if (IGNORED_DIRS.has(e.name)) continue;
|
|
@@ -13687,7 +13649,7 @@ async function walkLocal(root, dir, depth, counter) {
|
|
|
13687
13649
|
}
|
|
13688
13650
|
async function resolveSafeLocal(root, relPath, requireExist) {
|
|
13689
13651
|
if (relPath.includes("..") || relPath.startsWith("/")) {
|
|
13690
|
-
throw new
|
|
13652
|
+
throw new TRPCError21({ code: "BAD_REQUEST", message: "Chemin invalide" });
|
|
13691
13653
|
}
|
|
13692
13654
|
const target = resolvePath(root, relPath);
|
|
13693
13655
|
if (requireExist) {
|
|
@@ -13695,10 +13657,10 @@ async function resolveSafeLocal(root, relPath, requireExist) {
|
|
|
13695
13657
|
try {
|
|
13696
13658
|
real = await realpath(target);
|
|
13697
13659
|
} catch {
|
|
13698
|
-
throw new
|
|
13660
|
+
throw new TRPCError21({ code: "NOT_FOUND", message: "Chemin introuvable" });
|
|
13699
13661
|
}
|
|
13700
13662
|
if (real !== root && !(real + sep3).startsWith(root + sep3)) {
|
|
13701
|
-
throw new
|
|
13663
|
+
throw new TRPCError21({ code: "FORBIDDEN", message: "Hors du dossier autoris\xE9" });
|
|
13702
13664
|
}
|
|
13703
13665
|
return real;
|
|
13704
13666
|
}
|
|
@@ -13710,12 +13672,12 @@ async function resolveSafeLocal(root, relPath, requireExist) {
|
|
|
13710
13672
|
realParent = parent;
|
|
13711
13673
|
}
|
|
13712
13674
|
if (realParent !== root && !(realParent + sep3).startsWith(root + sep3)) {
|
|
13713
|
-
throw new
|
|
13675
|
+
throw new TRPCError21({ code: "FORBIDDEN", message: "Hors du dossier autoris\xE9" });
|
|
13714
13676
|
}
|
|
13715
13677
|
return resolvePath(realParent, target.slice(parent.length).replace(/^[\\/]+/, ""));
|
|
13716
13678
|
}
|
|
13717
13679
|
var wikiRouter = router({
|
|
13718
|
-
list: protectedProcedure.input(
|
|
13680
|
+
list: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA })).output(z25.object({ root: z25.string(), entries: z25.array(DocsEntrySchema), truncated: z25.boolean() })).query(async ({ input }) => {
|
|
13719
13681
|
const target = await resolveScope(input.scope);
|
|
13720
13682
|
if (target.mode === "agent") {
|
|
13721
13683
|
return agentManager.agentQuery(target.agentId, "wiki.listRepoMarkdown", { cwd: target.cwd });
|
|
@@ -13724,9 +13686,9 @@ var wikiRouter = router({
|
|
|
13724
13686
|
const entries2 = await walkLocal(target.root, target.root, 0, counter);
|
|
13725
13687
|
return { root: target.root, entries: entries2, truncated: counter.n >= MAX_FILES };
|
|
13726
13688
|
}),
|
|
13727
|
-
read: protectedProcedure.input(
|
|
13689
|
+
read: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).output(z25.object({ path: z25.string(), content: z25.string() })).query(async ({ input }) => {
|
|
13728
13690
|
if (!/\.(md|markdown)$/i.test(input.path)) {
|
|
13729
|
-
throw new
|
|
13691
|
+
throw new TRPCError21({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
|
|
13730
13692
|
}
|
|
13731
13693
|
const target = await resolveScope(input.scope);
|
|
13732
13694
|
if (target.mode === "agent") {
|
|
@@ -13734,27 +13696,27 @@ var wikiRouter = router({
|
|
|
13734
13696
|
}
|
|
13735
13697
|
const file = await resolveSafeLocal(target.root, input.path, true);
|
|
13736
13698
|
const s = await stat6(file);
|
|
13737
|
-
if (!s.isFile()) throw new
|
|
13738
|
-
if (s.size > MAX_FILE_BYTES) throw new
|
|
13739
|
-
const content = await
|
|
13699
|
+
if (!s.isFile()) throw new TRPCError21({ code: "BAD_REQUEST", message: "Pas un fichier" });
|
|
13700
|
+
if (s.size > MAX_FILE_BYTES) throw new TRPCError21({ code: "PAYLOAD_TOO_LARGE", message: "Fichier trop volumineux" });
|
|
13701
|
+
const content = await readFile15(file, "utf-8");
|
|
13740
13702
|
return { path: input.path, content };
|
|
13741
13703
|
}),
|
|
13742
|
-
write: protectedProcedure.input(
|
|
13704
|
+
write: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA, content: z25.string().max(5e6) })).mutation(async ({ input }) => {
|
|
13743
13705
|
if (!/\.(md|markdown)$/i.test(input.path)) {
|
|
13744
|
-
throw new
|
|
13706
|
+
throw new TRPCError21({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
|
|
13745
13707
|
}
|
|
13746
13708
|
const target = await resolveScope(input.scope);
|
|
13747
13709
|
if (target.mode === "agent") {
|
|
13748
13710
|
return agentManager.agentMutation(target.agentId, "wiki.writeMarkdown", { cwd: target.cwd, path: input.path, content: input.content });
|
|
13749
13711
|
}
|
|
13750
13712
|
const file = await resolveSafeLocal(target.root, input.path, false);
|
|
13751
|
-
await
|
|
13752
|
-
await
|
|
13713
|
+
await mkdir17(dirname9(file), { recursive: true });
|
|
13714
|
+
await writeFile15(file, input.content, "utf-8");
|
|
13753
13715
|
return { path: input.path, ok: true };
|
|
13754
13716
|
}),
|
|
13755
|
-
createFile: protectedProcedure.input(
|
|
13717
|
+
createFile: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
|
|
13756
13718
|
if (!/\.(md|markdown)$/i.test(input.path)) {
|
|
13757
|
-
throw new
|
|
13719
|
+
throw new TRPCError21({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
|
|
13758
13720
|
}
|
|
13759
13721
|
const target = await resolveScope(input.scope);
|
|
13760
13722
|
if (target.mode === "agent") {
|
|
@@ -13763,24 +13725,24 @@ var wikiRouter = router({
|
|
|
13763
13725
|
const file = await resolveSafeLocal(target.root, input.path, false);
|
|
13764
13726
|
try {
|
|
13765
13727
|
await stat6(file);
|
|
13766
|
-
throw new
|
|
13728
|
+
throw new TRPCError21({ code: "CONFLICT", message: "Fichier d\xE9j\xE0 existant" });
|
|
13767
13729
|
} catch (err) {
|
|
13768
|
-
if (err instanceof
|
|
13730
|
+
if (err instanceof TRPCError21) throw err;
|
|
13769
13731
|
}
|
|
13770
|
-
await
|
|
13771
|
-
await
|
|
13732
|
+
await mkdir17(dirname9(file), { recursive: true });
|
|
13733
|
+
await writeFile15(file, "", "utf-8");
|
|
13772
13734
|
return { path: input.path, ok: true };
|
|
13773
13735
|
}),
|
|
13774
|
-
createDir: protectedProcedure.input(
|
|
13736
|
+
createDir: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
|
|
13775
13737
|
const target = await resolveScope(input.scope);
|
|
13776
13738
|
if (target.mode === "agent") {
|
|
13777
13739
|
return agentManager.agentMutation(target.agentId, "wiki.createDir", { cwd: target.cwd, path: input.path });
|
|
13778
13740
|
}
|
|
13779
13741
|
const dir = await resolveSafeLocal(target.root, input.path, false);
|
|
13780
|
-
await
|
|
13742
|
+
await mkdir17(dir, { recursive: true });
|
|
13781
13743
|
return { path: input.path, ok: true };
|
|
13782
13744
|
}),
|
|
13783
|
-
rename: protectedProcedure.input(
|
|
13745
|
+
rename: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, fromPath: REL_PATH_SCHEMA, toPath: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
|
|
13784
13746
|
const target = await resolveScope(input.scope);
|
|
13785
13747
|
if (target.mode === "agent") {
|
|
13786
13748
|
return agentManager.agentMutation(target.agentId, "wiki.renamePath", { cwd: target.cwd, fromPath: input.fromPath, toPath: input.toPath });
|
|
@@ -13789,35 +13751,35 @@ var wikiRouter = router({
|
|
|
13789
13751
|
const to = await resolveSafeLocal(target.root, input.toPath, false);
|
|
13790
13752
|
try {
|
|
13791
13753
|
await stat6(to);
|
|
13792
|
-
throw new
|
|
13754
|
+
throw new TRPCError21({ code: "CONFLICT", message: "La cible existe d\xE9j\xE0" });
|
|
13793
13755
|
} catch (err) {
|
|
13794
|
-
if (err instanceof
|
|
13756
|
+
if (err instanceof TRPCError21) throw err;
|
|
13795
13757
|
}
|
|
13796
|
-
await
|
|
13758
|
+
await mkdir17(dirname9(to), { recursive: true });
|
|
13797
13759
|
await rename12(from, to);
|
|
13798
13760
|
return { fromPath: input.fromPath, toPath: input.toPath, ok: true };
|
|
13799
13761
|
}),
|
|
13800
|
-
ensureRoot: protectedProcedure.input(
|
|
13762
|
+
ensureRoot: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA })).mutation(async ({ input }) => {
|
|
13801
13763
|
if (input.scope === "global") {
|
|
13802
13764
|
await resolveScope("global");
|
|
13803
13765
|
return { ok: true };
|
|
13804
13766
|
}
|
|
13805
13767
|
const serviceId = input.scope.slice("service:".length);
|
|
13806
13768
|
const service = await serviceStore.get(serviceId);
|
|
13807
|
-
if (!service) throw new
|
|
13769
|
+
if (!service) throw new TRPCError21({ code: "NOT_FOUND", message: "Service introuvable" });
|
|
13808
13770
|
const agentId = await resolveEffectiveAgentId(service);
|
|
13809
|
-
if (!agentId) throw new
|
|
13771
|
+
if (!agentId) throw new TRPCError21({ code: "BAD_REQUEST", message: "Aucun agent configur\xE9" });
|
|
13810
13772
|
const cwd = await resolveWorkspaceCwd({ serviceId, override: service.docsPath?.trim() || void 0 });
|
|
13811
13773
|
return agentManager.agentMutation(agentId, "wiki.ensureRoot", { cwd });
|
|
13812
13774
|
}),
|
|
13813
|
-
delete: protectedProcedure.input(
|
|
13775
|
+
delete: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
|
|
13814
13776
|
const target = await resolveScope(input.scope);
|
|
13815
13777
|
if (target.mode === "agent") {
|
|
13816
13778
|
return agentManager.agentMutation(target.agentId, "wiki.deletePath", { cwd: target.cwd, path: input.path });
|
|
13817
13779
|
}
|
|
13818
13780
|
const fp = await resolveSafeLocal(target.root, input.path, true);
|
|
13819
|
-
if (fp === target.root) throw new
|
|
13820
|
-
await
|
|
13781
|
+
if (fp === target.root) throw new TRPCError21({ code: "FORBIDDEN", message: "Impossible de supprimer la racine" });
|
|
13782
|
+
await rm4(fp, { recursive: true, force: true });
|
|
13821
13783
|
return { path: input.path, ok: true };
|
|
13822
13784
|
})
|
|
13823
13785
|
});
|
|
@@ -13836,14 +13798,14 @@ var configRouter = router({
|
|
|
13836
13798
|
});
|
|
13837
13799
|
|
|
13838
13800
|
// ../server/src/trpc/routers/cloud-sync.ts
|
|
13839
|
-
import { z as
|
|
13840
|
-
import { TRPCError as
|
|
13801
|
+
import { z as z26 } from "zod";
|
|
13802
|
+
import { TRPCError as TRPCError22 } from "@trpc/server";
|
|
13841
13803
|
|
|
13842
13804
|
// ../server/src/services/org-sync-scope.ts
|
|
13843
13805
|
import { access as access2 } from "fs/promises";
|
|
13844
|
-
import { join as
|
|
13806
|
+
import { join as join21 } from "path";
|
|
13845
13807
|
async function isMaterialized(root) {
|
|
13846
|
-
return access2(
|
|
13808
|
+
return access2(join21(root, "project.json")).then(() => true, () => false);
|
|
13847
13809
|
}
|
|
13848
13810
|
async function orgsToSync() {
|
|
13849
13811
|
const cloudUrl = getCloudApiUrl();
|
|
@@ -13905,16 +13867,16 @@ async function reconcileOrgCanary(orgId) {
|
|
|
13905
13867
|
|
|
13906
13868
|
// ../server/src/services/cloud-sync-bridge.ts
|
|
13907
13869
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
13908
|
-
import { mkdir as
|
|
13870
|
+
import { mkdir as mkdir19, rm as rm5 } from "fs/promises";
|
|
13909
13871
|
import * as Y from "yjs";
|
|
13910
13872
|
|
|
13911
13873
|
// ../server/src/services/org-lock.ts
|
|
13912
|
-
import { open as open2, readFile as
|
|
13874
|
+
import { open as open2, readFile as readFile16, unlink as unlink4, mkdir as mkdir18 } from "fs/promises";
|
|
13913
13875
|
import { readFileSync, unlinkSync } from "fs";
|
|
13914
|
-
import { join as
|
|
13876
|
+
import { join as join22 } from "path";
|
|
13915
13877
|
var held = /* @__PURE__ */ new Set();
|
|
13916
13878
|
function lockPath(orgId) {
|
|
13917
|
-
return
|
|
13879
|
+
return join22(orgVaultRoot(orgId), "org.lock");
|
|
13918
13880
|
}
|
|
13919
13881
|
function isRunning(pid) {
|
|
13920
13882
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -13927,7 +13889,7 @@ function isRunning(pid) {
|
|
|
13927
13889
|
}
|
|
13928
13890
|
async function readLock(path) {
|
|
13929
13891
|
try {
|
|
13930
|
-
const parsed = JSON.parse(await
|
|
13892
|
+
const parsed = JSON.parse(await readFile16(path, "utf-8"));
|
|
13931
13893
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
13932
13894
|
} catch {
|
|
13933
13895
|
return null;
|
|
@@ -13935,7 +13897,7 @@ async function readLock(path) {
|
|
|
13935
13897
|
}
|
|
13936
13898
|
async function acquireOrgLock(orgId) {
|
|
13937
13899
|
const path = lockPath(orgId);
|
|
13938
|
-
await
|
|
13900
|
+
await mkdir18(orgVaultRoot(orgId), { recursive: true });
|
|
13939
13901
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
13940
13902
|
try {
|
|
13941
13903
|
const handle = await open2(path, "wx");
|
|
@@ -14346,7 +14308,7 @@ var OrgSyncConnection = class {
|
|
|
14346
14308
|
}
|
|
14347
14309
|
const isLocal = existing?.orgId === this.link.orgId;
|
|
14348
14310
|
if (!inDoc && !isLocal) return false;
|
|
14349
|
-
await
|
|
14311
|
+
await mkdir19(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
|
|
14350
14312
|
this.tracked.add(projectId);
|
|
14351
14313
|
if (inDoc) await this.pullFromDoc([projectId]);
|
|
14352
14314
|
else await this.reconcileLocalToDoc();
|
|
@@ -14362,7 +14324,7 @@ var OrgSyncConnection = class {
|
|
|
14362
14324
|
async unfollowProject(projectId) {
|
|
14363
14325
|
this.tracked.delete(projectId);
|
|
14364
14326
|
await projectStore.forgetLocally({ id: projectId, orgId: this.link.orgId });
|
|
14365
|
-
await
|
|
14327
|
+
await rm5(projectVaultRoot(this.link.orgId, projectId), { recursive: true, force: true }).catch(() => {
|
|
14366
14328
|
});
|
|
14367
14329
|
}
|
|
14368
14330
|
/**
|
|
@@ -14722,8 +14684,8 @@ var CloudSyncBridge = class {
|
|
|
14722
14684
|
var cloudSyncBridge = new CloudSyncBridge();
|
|
14723
14685
|
|
|
14724
14686
|
// ../server/src/services/key-adoption.ts
|
|
14725
|
-
import { readdir as
|
|
14726
|
-
import { join as
|
|
14687
|
+
import { readdir as readdir7, readFile as readFile17, writeFile as writeFile16, mkdir as mkdir20, copyFile, rename as rename13, stat as stat7 } from "fs/promises";
|
|
14688
|
+
import { join as join23 } from "path";
|
|
14727
14689
|
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes5 } from "crypto";
|
|
14728
14690
|
var ALGORITHM = "aes-256-gcm";
|
|
14729
14691
|
function isAgentTriplet(value) {
|
|
@@ -14780,7 +14742,7 @@ async function rootsOfOrg(orgId) {
|
|
|
14780
14742
|
}
|
|
14781
14743
|
async function jsonFilesIn(root) {
|
|
14782
14744
|
try {
|
|
14783
|
-
const entries2 = await
|
|
14745
|
+
const entries2 = await readdir7(root, { withFileTypes: true });
|
|
14784
14746
|
return entries2.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name);
|
|
14785
14747
|
} catch {
|
|
14786
14748
|
return [];
|
|
@@ -14803,10 +14765,10 @@ async function adoptOrgKey(orgId, newKey, masterPassword, now) {
|
|
|
14803
14765
|
const touchedRoots = /* @__PURE__ */ new Set();
|
|
14804
14766
|
for (const vaultRoot of await rootsOfOrg(orgId)) {
|
|
14805
14767
|
for (const name of await jsonFilesIn(vaultRoot)) {
|
|
14806
|
-
const path =
|
|
14768
|
+
const path = join23(vaultRoot, name);
|
|
14807
14769
|
let parsed;
|
|
14808
14770
|
try {
|
|
14809
|
-
parsed = JSON.parse(await
|
|
14771
|
+
parsed = JSON.parse(await readFile17(path, "utf-8"));
|
|
14810
14772
|
} catch {
|
|
14811
14773
|
continue;
|
|
14812
14774
|
}
|
|
@@ -14820,21 +14782,21 @@ async function adoptOrgKey(orgId, newKey, masterPassword, now) {
|
|
|
14820
14782
|
const stamp = now.toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
14821
14783
|
const backups = [];
|
|
14822
14784
|
for (const vaultRoot of touchedRoots) {
|
|
14823
|
-
const backupDir =
|
|
14824
|
-
await
|
|
14785
|
+
const backupDir = join23(vaultRoot, `backup-key-${stamp}`);
|
|
14786
|
+
await mkdir20(backupDir, { recursive: true });
|
|
14825
14787
|
for (const name of await jsonFilesIn(vaultRoot)) {
|
|
14826
|
-
await copyFile(
|
|
14788
|
+
await copyFile(join23(vaultRoot, name), join23(backupDir, name));
|
|
14827
14789
|
}
|
|
14828
14790
|
for (const name of [".encryption.key.enc", ".runeya-verify"]) {
|
|
14829
|
-
if (await exists(
|
|
14830
|
-
await copyFile(
|
|
14791
|
+
if (await exists(join23(vaultRoot, name))) {
|
|
14792
|
+
await copyFile(join23(vaultRoot, name), join23(backupDir, name));
|
|
14831
14793
|
}
|
|
14832
14794
|
}
|
|
14833
14795
|
backups.push(backupDir);
|
|
14834
14796
|
}
|
|
14835
14797
|
for (const item of pending) {
|
|
14836
14798
|
const tmp = item.path + ".tmp";
|
|
14837
|
-
await
|
|
14799
|
+
await writeFile16(tmp, item.content, "utf-8");
|
|
14838
14800
|
await rename13(tmp, item.path);
|
|
14839
14801
|
}
|
|
14840
14802
|
await installOrgKey(orgId, newKey, masterPassword);
|
|
@@ -14866,7 +14828,7 @@ async function assertOrgReadable(orgId) {
|
|
|
14866
14828
|
await assertOrgVaultReadable(orgId);
|
|
14867
14829
|
} catch (err) {
|
|
14868
14830
|
if (err instanceof OrgVaultUnreadableError) {
|
|
14869
|
-
throw new
|
|
14831
|
+
throw new TRPCError22({ code: "FORBIDDEN", message: err.message });
|
|
14870
14832
|
}
|
|
14871
14833
|
throw err;
|
|
14872
14834
|
}
|
|
@@ -14876,10 +14838,10 @@ async function assertOrgKey(orgId) {
|
|
|
14876
14838
|
await reconcileOrgCanary(orgId);
|
|
14877
14839
|
} catch (err) {
|
|
14878
14840
|
if (err instanceof OrgKeyMismatchError) {
|
|
14879
|
-
throw new
|
|
14841
|
+
throw new TRPCError22({ code: "FORBIDDEN", message: err.message });
|
|
14880
14842
|
}
|
|
14881
14843
|
if (err instanceof CanaryUnavailableError) {
|
|
14882
|
-
throw new
|
|
14844
|
+
throw new TRPCError22({ code: "PRECONDITION_FAILED", message: err.message });
|
|
14883
14845
|
}
|
|
14884
14846
|
throw err;
|
|
14885
14847
|
}
|
|
@@ -14896,10 +14858,10 @@ var cloudSyncRouter = router({
|
|
|
14896
14858
|
* "open this project" is the whole gesture.
|
|
14897
14859
|
*/
|
|
14898
14860
|
trackProject: protectedProcedure.input(
|
|
14899
|
-
|
|
14900
|
-
orgId:
|
|
14901
|
-
cloudUrl:
|
|
14902
|
-
projectId:
|
|
14861
|
+
z26.object({
|
|
14862
|
+
orgId: z26.string().min(1).max(128),
|
|
14863
|
+
cloudUrl: z26.string().url().max(2048),
|
|
14864
|
+
projectId: z26.string().min(1).max(128)
|
|
14903
14865
|
})
|
|
14904
14866
|
).mutation(async ({ input }) => {
|
|
14905
14867
|
await assertOrgReadable(input.orgId);
|
|
@@ -14914,7 +14876,7 @@ var cloudSyncRouter = router({
|
|
|
14914
14876
|
return { pulled, state: syncState(input.orgId) };
|
|
14915
14877
|
} catch (err) {
|
|
14916
14878
|
if (err instanceof ProjectHeldElsewhereError) {
|
|
14917
|
-
throw new
|
|
14879
|
+
throw new TRPCError22({ code: "CONFLICT", message: err.code, cause: err });
|
|
14918
14880
|
}
|
|
14919
14881
|
throw err;
|
|
14920
14882
|
}
|
|
@@ -14933,18 +14895,18 @@ var cloudSyncRouter = router({
|
|
|
14933
14895
|
* fusionné.
|
|
14934
14896
|
*/
|
|
14935
14897
|
publishProject: protectedProcedure.input(
|
|
14936
|
-
|
|
14937
|
-
orgId:
|
|
14938
|
-
cloudUrl:
|
|
14939
|
-
projectId:
|
|
14898
|
+
z26.object({
|
|
14899
|
+
orgId: z26.string().min(1).max(128),
|
|
14900
|
+
cloudUrl: z26.string().url().max(2048),
|
|
14901
|
+
projectId: z26.string().min(1).max(128)
|
|
14940
14902
|
})
|
|
14941
14903
|
).mutation(async ({ input }) => {
|
|
14942
14904
|
const local = await projectStore.get({ id: input.projectId, orgId: null });
|
|
14943
14905
|
if (!local) {
|
|
14944
|
-
throw new
|
|
14906
|
+
throw new TRPCError22({ code: "NOT_FOUND", message: "PROJECT_NOT_LOCAL" });
|
|
14945
14907
|
}
|
|
14946
14908
|
if (await projectStore.get({ id: input.projectId, orgId: input.orgId })) {
|
|
14947
|
-
throw new
|
|
14909
|
+
throw new TRPCError22({ code: "CONFLICT", message: "PROJECT_ALREADY_IN_ORG" });
|
|
14948
14910
|
}
|
|
14949
14911
|
await assertOrgReadable(input.orgId);
|
|
14950
14912
|
const connected = cloudSyncBridge.states().some((s) => s.orgId === input.orgId);
|
|
@@ -14963,7 +14925,7 @@ var cloudSyncRouter = router({
|
|
|
14963
14925
|
await cloudSyncBridge.followProject(input.orgId, input.projectId);
|
|
14964
14926
|
} catch (err) {
|
|
14965
14927
|
if (err instanceof ProjectHeldElsewhereError) {
|
|
14966
|
-
throw new
|
|
14928
|
+
throw new TRPCError22({ code: "CONFLICT", message: err.code, cause: err });
|
|
14967
14929
|
}
|
|
14968
14930
|
throw err;
|
|
14969
14931
|
}
|
|
@@ -14977,16 +14939,16 @@ var cloudSyncRouter = router({
|
|
|
14977
14939
|
* publié. Le cloud, lui, ne peut pas répondre — il n'a que du chiffré sous
|
|
14978
14940
|
* les yeux — donc le canari est rapatrié et jugé ici.
|
|
14979
14941
|
*/
|
|
14980
|
-
checkOrgKey: protectedProcedure.input(
|
|
14981
|
-
orgId:
|
|
14982
|
-
keyHex:
|
|
14942
|
+
checkOrgKey: protectedProcedure.input(z26.object({
|
|
14943
|
+
orgId: z26.string().min(1).max(128),
|
|
14944
|
+
keyHex: z26.string().length(64).regex(/^[0-9a-f]+$/i)
|
|
14983
14945
|
})).query(async ({ input }) => {
|
|
14984
14946
|
const res = await cloudFetch2(
|
|
14985
14947
|
`/api/auth/organization/canary?organizationId=${encodeURIComponent(input.orgId)}`,
|
|
14986
14948
|
{ method: "GET" }
|
|
14987
14949
|
);
|
|
14988
14950
|
if (!res.ok) {
|
|
14989
|
-
throw new
|
|
14951
|
+
throw new TRPCError22({ code: "BAD_GATEWAY", message: await cloudMessage(res) || `CANARY_READ_FAILED (${res.status})` });
|
|
14990
14952
|
}
|
|
14991
14953
|
const { canary } = await res.json();
|
|
14992
14954
|
if (typeof canary !== "string") return { matches: false, published: false };
|
|
@@ -15002,10 +14964,10 @@ var cloudSyncRouter = router({
|
|
|
15002
14964
|
* Bornée au coffre de l'organisation et à ceux de ses projets : les autres
|
|
15003
14965
|
* organisations et la machine gardent leur clé.
|
|
15004
14966
|
*/
|
|
15005
|
-
adoptOrgKey: protectedProcedure.input(
|
|
15006
|
-
orgId:
|
|
15007
|
-
keyHex:
|
|
15008
|
-
masterPassword:
|
|
14967
|
+
adoptOrgKey: protectedProcedure.input(z26.object({
|
|
14968
|
+
orgId: z26.string().min(1).max(128),
|
|
14969
|
+
keyHex: z26.string().length(64).regex(/^[0-9a-f]+$/i),
|
|
14970
|
+
masterPassword: z26.string().min(1).max(1024)
|
|
15009
14971
|
})).mutation(async ({ input }) => {
|
|
15010
14972
|
const report = await adoptOrgKey(
|
|
15011
14973
|
input.orgId,
|
|
@@ -15029,10 +14991,10 @@ var cloudSyncRouter = router({
|
|
|
15029
14991
|
* Elle ne re-chiffre rien : ce qui a déjà été synchronisé sous l'ancienne clé
|
|
15030
14992
|
* reste illisible. L'interface le dit avant de la déclencher.
|
|
15031
14993
|
*/
|
|
15032
|
-
resetOrgKey: protectedProcedure.input(
|
|
14994
|
+
resetOrgKey: protectedProcedure.input(z26.object({ orgId: z26.string().min(1).max(128) })).mutation(async ({ input }) => {
|
|
15033
14995
|
const canary = await readVerifyFile(await keyRoot());
|
|
15034
14996
|
if (!canary) {
|
|
15035
|
-
throw new
|
|
14997
|
+
throw new TRPCError22({
|
|
15036
14998
|
code: "PRECONDITION_FAILED",
|
|
15037
14999
|
message: "Cette machine n'a pas encore de cl\xE9 de chiffrement."
|
|
15038
15000
|
});
|
|
@@ -15043,7 +15005,7 @@ var cloudSyncRouter = router({
|
|
|
15043
15005
|
});
|
|
15044
15006
|
if (!res.ok) {
|
|
15045
15007
|
const detail = await cloudMessage(res);
|
|
15046
|
-
throw new
|
|
15008
|
+
throw new TRPCError22({
|
|
15047
15009
|
code: res.status === 403 ? "FORBIDDEN" : "BAD_GATEWAY",
|
|
15048
15010
|
message: detail || `ORG_KEY_RESET_FAILED (${res.status})`
|
|
15049
15011
|
});
|
|
@@ -15059,9 +15021,9 @@ var cloudSyncRouter = router({
|
|
|
15059
15021
|
* every other member and for this one on another machine.
|
|
15060
15022
|
*/
|
|
15061
15023
|
untrackProject: protectedProcedure.input(
|
|
15062
|
-
|
|
15063
|
-
orgId:
|
|
15064
|
-
projectId:
|
|
15024
|
+
z26.object({
|
|
15025
|
+
orgId: z26.string().min(1).max(128),
|
|
15026
|
+
projectId: z26.string().min(1).max(128)
|
|
15065
15027
|
})
|
|
15066
15028
|
).mutation(async ({ input }) => {
|
|
15067
15029
|
await cloudSyncBridge.unfollowProject(input.orgId, input.projectId);
|
|
@@ -15077,9 +15039,9 @@ var cloudSyncRouter = router({
|
|
|
15077
15039
|
* membre, ce qui évite d'effacer les fichiers si le cloud a refusé l'ordre.
|
|
15078
15040
|
*/
|
|
15079
15041
|
deleteProjectFromOrg: protectedProcedure.input(
|
|
15080
|
-
|
|
15081
|
-
orgId:
|
|
15082
|
-
projectId:
|
|
15042
|
+
z26.object({
|
|
15043
|
+
orgId: z26.string().min(1).max(128),
|
|
15044
|
+
projectId: z26.string().min(1).max(128)
|
|
15083
15045
|
})
|
|
15084
15046
|
).mutation(async ({ input }) => {
|
|
15085
15047
|
let res;
|
|
@@ -15089,8 +15051,8 @@ var cloudSyncRouter = router({
|
|
|
15089
15051
|
body: { organizationId: input.orgId, projectId: input.projectId }
|
|
15090
15052
|
});
|
|
15091
15053
|
} catch (err) {
|
|
15092
|
-
if (err instanceof
|
|
15093
|
-
throw new
|
|
15054
|
+
if (err instanceof TRPCError22 && err.code === "PRECONDITION_FAILED") {
|
|
15055
|
+
throw new TRPCError22({ code: "PRECONDITION_FAILED", message: "CLOUD_NOT_LINKED" });
|
|
15094
15056
|
}
|
|
15095
15057
|
throw err;
|
|
15096
15058
|
}
|
|
@@ -15098,12 +15060,12 @@ var cloudSyncRouter = router({
|
|
|
15098
15060
|
const detail = await cloudMessage(res);
|
|
15099
15061
|
if (res.status === 403) {
|
|
15100
15062
|
const code = /not a member/i.test(detail) ? "PROJECT_NOT_MEMBER" : "PROJECT_DELETE_FORBIDDEN";
|
|
15101
|
-
throw new
|
|
15063
|
+
throw new TRPCError22({ code: "FORBIDDEN", message: code, cause: new Error(detail) });
|
|
15102
15064
|
}
|
|
15103
15065
|
if (res.status === 404) {
|
|
15104
|
-
throw new
|
|
15066
|
+
throw new TRPCError22({ code: "NOT_FOUND", message: "PROJECT_NOT_IN_ORG", cause: new Error(detail) });
|
|
15105
15067
|
}
|
|
15106
|
-
throw new
|
|
15068
|
+
throw new TRPCError22({
|
|
15107
15069
|
code: "BAD_GATEWAY",
|
|
15108
15070
|
message: detail || `PROJECT_DELETE_FAILED (${res.status})`
|
|
15109
15071
|
});
|
|
@@ -15123,10 +15085,10 @@ var cloudSyncRouter = router({
|
|
|
15123
15085
|
* afficher. Le nom est donc recopié dans le tampon du coffre au passage, là
|
|
15124
15086
|
* où il est déjà lu au démarrage suivant.
|
|
15125
15087
|
*/
|
|
15126
|
-
rememberOrgNames: protectedProcedure.input(
|
|
15127
|
-
orgs:
|
|
15128
|
-
orgId:
|
|
15129
|
-
name:
|
|
15088
|
+
rememberOrgNames: protectedProcedure.input(z26.object({
|
|
15089
|
+
orgs: z26.array(z26.object({
|
|
15090
|
+
orgId: z26.string().min(1).max(128),
|
|
15091
|
+
name: z26.string().min(1).max(100)
|
|
15130
15092
|
})).max(200)
|
|
15131
15093
|
})).mutation(async ({ input }) => {
|
|
15132
15094
|
for (const { orgId, name } of input.orgs) await rememberOrgName(orgId, name);
|
|
@@ -15160,15 +15122,15 @@ var cloudSyncRouter = router({
|
|
|
15160
15122
|
});
|
|
15161
15123
|
|
|
15162
15124
|
// ../server/src/trpc/routers/filesystem.ts
|
|
15163
|
-
import { readdir as
|
|
15125
|
+
import { readdir as readdir8, stat as stat8 } from "fs/promises";
|
|
15164
15126
|
import { homedir as homedir5 } from "os";
|
|
15165
|
-
import { isAbsolute as isAbsolute4, join as
|
|
15166
|
-
import { z as
|
|
15127
|
+
import { isAbsolute as isAbsolute4, join as join24, resolve as resolve5, dirname as dirname10, basename as basename2 } from "path";
|
|
15128
|
+
import { z as z27 } from "zod";
|
|
15167
15129
|
var MAX_ENTRIES2 = 500;
|
|
15168
|
-
var PATH_SCHEMA =
|
|
15130
|
+
var PATH_SCHEMA = z27.string().min(1).max(4096);
|
|
15169
15131
|
function safeResolve(input) {
|
|
15170
15132
|
if (input.includes("\0")) throw new Error("Chemin invalide");
|
|
15171
|
-
const expanded = input.startsWith("~") ?
|
|
15133
|
+
const expanded = input.startsWith("~") ? join24(homedir5(), input.slice(1)) : input;
|
|
15172
15134
|
if (!isAbsolute4(expanded)) throw new Error("Le chemin doit \xEAtre absolu");
|
|
15173
15135
|
return resolve5(expanded);
|
|
15174
15136
|
}
|
|
@@ -15189,7 +15151,7 @@ var filesystemRouter = router({
|
|
|
15189
15151
|
* Hidden entries are skipped: they are noise for choosing a workspace, and
|
|
15190
15152
|
* `.runeya` itself would invite placing a project inside another's data.
|
|
15191
15153
|
*/
|
|
15192
|
-
listDirectories: protectedProcedure.input(
|
|
15154
|
+
listDirectories: protectedProcedure.input(z27.object({ path: PATH_SCHEMA })).query(async ({ input }) => {
|
|
15193
15155
|
const dir = safeResolve(input.path);
|
|
15194
15156
|
const info = await stat8(dir).catch(() => null);
|
|
15195
15157
|
if (!info?.isDirectory()) {
|
|
@@ -15197,14 +15159,14 @@ var filesystemRouter = router({
|
|
|
15197
15159
|
}
|
|
15198
15160
|
let names;
|
|
15199
15161
|
try {
|
|
15200
|
-
names = (await
|
|
15162
|
+
names = (await readdir8(dir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
15201
15163
|
} catch {
|
|
15202
15164
|
throw new Error(`R\xE9pertoire illisible : ${dir}`);
|
|
15203
15165
|
}
|
|
15204
15166
|
const truncated = names.length > MAX_ENTRIES2;
|
|
15205
15167
|
const entries2 = [];
|
|
15206
15168
|
for (const name of names.slice(0, MAX_ENTRIES2)) {
|
|
15207
|
-
const path =
|
|
15169
|
+
const path = join24(dir, name);
|
|
15208
15170
|
entries2.push({ name, path, hasRuneyaProject: await holdsRuneyaProject(path) });
|
|
15209
15171
|
}
|
|
15210
15172
|
return {
|
|
@@ -15223,10 +15185,10 @@ var filesystemRouter = router({
|
|
|
15223
15185
|
* Two projects in one directory is what makes services run against the wrong
|
|
15224
15186
|
* code, so a directory that already holds one is refused outright.
|
|
15225
15187
|
*/
|
|
15226
|
-
checkProjectTarget: protectedProcedure.input(
|
|
15188
|
+
checkProjectTarget: protectedProcedure.input(z27.object({
|
|
15227
15189
|
path: PATH_SCHEMA,
|
|
15228
|
-
projectId:
|
|
15229
|
-
orgId:
|
|
15190
|
+
projectId: z27.string().min(1).max(128),
|
|
15191
|
+
orgId: z27.string().max(128).nullable().optional()
|
|
15230
15192
|
})).query(async ({ input }) => {
|
|
15231
15193
|
const dir = safeResolve(input.path);
|
|
15232
15194
|
const info = await stat8(dir).catch(() => null);
|
|
@@ -15336,8 +15298,8 @@ async function ensureFilePermissions() {
|
|
|
15336
15298
|
}
|
|
15337
15299
|
|
|
15338
15300
|
// ../server/src/services/vault-migration.ts
|
|
15339
|
-
import { readdir as
|
|
15340
|
-
import { join as
|
|
15301
|
+
import { readdir as readdir9, readFile as readFile18, writeFile as writeFile17, mkdir as mkdir21, copyFile as copyFile2, unlink as unlink5, rename as rename14, stat as stat10 } from "fs/promises";
|
|
15302
|
+
import { join as join25 } from "path";
|
|
15341
15303
|
var KEY_FILES = [".encryption.key.enc", ".runeya-verify"];
|
|
15342
15304
|
var MACHINE_FILES = [
|
|
15343
15305
|
"agents.json",
|
|
@@ -15349,7 +15311,7 @@ var MACHINE_FILES = [
|
|
|
15349
15311
|
];
|
|
15350
15312
|
var rekeyValue = rekeyJsonValue;
|
|
15351
15313
|
async function vaultJsonFiles(root) {
|
|
15352
|
-
const entries2 = await
|
|
15314
|
+
const entries2 = await readdir9(root, { withFileTypes: true });
|
|
15353
15315
|
return entries2.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name);
|
|
15354
15316
|
}
|
|
15355
15317
|
async function exists2(path) {
|
|
@@ -15367,17 +15329,17 @@ async function openLegacyVault(legacyRoot, masterPassword) {
|
|
|
15367
15329
|
}
|
|
15368
15330
|
async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machineKey, now) {
|
|
15369
15331
|
const stamp = now.toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
15370
|
-
const backupDir =
|
|
15371
|
-
await
|
|
15332
|
+
const backupDir = join25(legacyRoot, `backup-${stamp}`);
|
|
15333
|
+
await mkdir21(backupDir, { recursive: true });
|
|
15372
15334
|
const jsonFiles = await vaultJsonFiles(legacyRoot);
|
|
15373
15335
|
for (const name of [...jsonFiles, ...KEY_FILES]) {
|
|
15374
|
-
if (await exists2(
|
|
15375
|
-
await copyFile2(
|
|
15336
|
+
if (await exists2(join25(legacyRoot, name))) {
|
|
15337
|
+
await copyFile2(join25(legacyRoot, name), join25(backupDir, name));
|
|
15376
15338
|
}
|
|
15377
15339
|
}
|
|
15378
15340
|
const rekeyed = [];
|
|
15379
15341
|
for (const name of jsonFiles) {
|
|
15380
|
-
const raw = await
|
|
15342
|
+
const raw = await readFile18(join25(legacyRoot, name), "utf-8");
|
|
15381
15343
|
let parsed;
|
|
15382
15344
|
try {
|
|
15383
15345
|
parsed = JSON.parse(raw);
|
|
@@ -15387,13 +15349,13 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
|
|
|
15387
15349
|
const counter = { n: 0 };
|
|
15388
15350
|
const converted = rekeyValue(parsed, legacyKey, machineKey, counter);
|
|
15389
15351
|
if (counter.n === 0) continue;
|
|
15390
|
-
const tmp =
|
|
15391
|
-
await
|
|
15392
|
-
await rename14(tmp,
|
|
15352
|
+
const tmp = join25(legacyRoot, `${name}.tmp`);
|
|
15353
|
+
await writeFile17(tmp, JSON.stringify(converted, null, 2), "utf-8");
|
|
15354
|
+
await rename14(tmp, join25(legacyRoot, name));
|
|
15393
15355
|
rekeyed.push({ file: name, values: counter.n });
|
|
15394
15356
|
}
|
|
15395
15357
|
for (const { file, values } of rekeyed) {
|
|
15396
|
-
const parsed = JSON.parse(await
|
|
15358
|
+
const parsed = JSON.parse(await readFile18(join25(legacyRoot, file), "utf-8"));
|
|
15397
15359
|
const check = { n: 0 };
|
|
15398
15360
|
rekeyValue(parsed, machineKey, machineKey, check);
|
|
15399
15361
|
if (check.n !== values) {
|
|
@@ -15402,13 +15364,13 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
|
|
|
15402
15364
|
);
|
|
15403
15365
|
}
|
|
15404
15366
|
}
|
|
15405
|
-
await
|
|
15367
|
+
await mkdir21(machineRootDir, { recursive: true });
|
|
15406
15368
|
const moved = [];
|
|
15407
15369
|
const skipped = [];
|
|
15408
15370
|
for (const name of MACHINE_FILES) {
|
|
15409
|
-
const from =
|
|
15371
|
+
const from = join25(legacyRoot, name);
|
|
15410
15372
|
if (!await exists2(from)) continue;
|
|
15411
|
-
const to =
|
|
15373
|
+
const to = join25(machineRootDir, name);
|
|
15412
15374
|
if (await exists2(to)) {
|
|
15413
15375
|
skipped.push(name);
|
|
15414
15376
|
continue;
|
|
@@ -15418,15 +15380,15 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
|
|
|
15418
15380
|
moved.push(name);
|
|
15419
15381
|
}
|
|
15420
15382
|
for (const name of KEY_FILES) {
|
|
15421
|
-
const path =
|
|
15383
|
+
const path = join25(legacyRoot, name);
|
|
15422
15384
|
if (await exists2(path)) await unlink5(path);
|
|
15423
15385
|
}
|
|
15424
15386
|
return { backupDir, rekeyed, moved, skipped };
|
|
15425
15387
|
}
|
|
15426
15388
|
|
|
15427
15389
|
// ../server/src/services/settings-migration.ts
|
|
15428
|
-
import { readFile as
|
|
15429
|
-
import { join as
|
|
15390
|
+
import { readFile as readFile19, writeFile as writeFile18, rename as rename15, unlink as unlink6, mkdir as mkdir22, chmod as chmod12, stat as stat11 } from "fs/promises";
|
|
15391
|
+
import { join as join26 } from "path";
|
|
15430
15392
|
var SETTINGS_FILE2 = "settings.json";
|
|
15431
15393
|
var SETTINGS_PRIVATE_FILE2 = "settings.private.json";
|
|
15432
15394
|
async function exists3(path) {
|
|
@@ -15439,11 +15401,11 @@ async function legacySource(filename) {
|
|
|
15439
15401
|
return null;
|
|
15440
15402
|
}
|
|
15441
15403
|
async function moveInto(root, filename, from) {
|
|
15442
|
-
await
|
|
15443
|
-
const to =
|
|
15444
|
-
const contents = await
|
|
15404
|
+
await mkdir22(root, { recursive: true });
|
|
15405
|
+
const to = join26(root, filename);
|
|
15406
|
+
const contents = await readFile19(from, "utf-8");
|
|
15445
15407
|
const tmp = to + ".tmp";
|
|
15446
|
-
await
|
|
15408
|
+
await writeFile18(tmp, contents, "utf-8");
|
|
15447
15409
|
await rename15(tmp, to);
|
|
15448
15410
|
await chmod12(to, 384);
|
|
15449
15411
|
await unlink6(from);
|
|
@@ -15465,7 +15427,7 @@ async function migrateSettingsIntoVaults() {
|
|
|
15465
15427
|
const orgId = orgs[0];
|
|
15466
15428
|
const root = orgVaultRoot(orgId);
|
|
15467
15429
|
for (const { filename, from } of sources) {
|
|
15468
|
-
if (await exists3(
|
|
15430
|
+
if (await exists3(join26(root, filename))) continue;
|
|
15469
15431
|
await moveInto(root, filename, from);
|
|
15470
15432
|
report.files.push(filename);
|
|
15471
15433
|
}
|
|
@@ -15481,14 +15443,14 @@ import { createRequire as createRequire2 } from "module";
|
|
|
15481
15443
|
import { randomBytes as randomBytes6, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3 } from "crypto";
|
|
15482
15444
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
15483
15445
|
import {
|
|
15484
|
-
readFile as
|
|
15485
|
-
writeFile as
|
|
15446
|
+
readFile as readFile20,
|
|
15447
|
+
writeFile as writeFile19,
|
|
15486
15448
|
rename as rename16,
|
|
15487
|
-
mkdir as
|
|
15449
|
+
mkdir as mkdir23,
|
|
15488
15450
|
access as access3,
|
|
15489
|
-
readdir as
|
|
15451
|
+
readdir as readdir10
|
|
15490
15452
|
} from "fs/promises";
|
|
15491
|
-
import { join as
|
|
15453
|
+
import { join as join27, dirname as dirname11 } from "path";
|
|
15492
15454
|
var _require = createRequire2(import.meta.url);
|
|
15493
15455
|
var PARSER_ID_MAP = {
|
|
15494
15456
|
"stack-monitor-parser-jsons": "native:json",
|
|
@@ -15597,7 +15559,7 @@ async function getSodium() {
|
|
|
15597
15559
|
return _sodium;
|
|
15598
15560
|
}
|
|
15599
15561
|
async function decryptLegacyFile(filePath, key, sodium) {
|
|
15600
|
-
const raw = (await
|
|
15562
|
+
const raw = (await readFile20(filePath, "utf-8")).trim();
|
|
15601
15563
|
if (!raw || raw === "[]" || raw === "{}") {
|
|
15602
15564
|
try {
|
|
15603
15565
|
return JSON.parse(raw);
|
|
@@ -15617,12 +15579,12 @@ async function decryptLegacyFile(filePath, key, sodium) {
|
|
|
15617
15579
|
}
|
|
15618
15580
|
async function atomicWrite(path, data) {
|
|
15619
15581
|
const tmp = path + ".tmp";
|
|
15620
|
-
await
|
|
15582
|
+
await writeFile19(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
15621
15583
|
await rename16(tmp, path);
|
|
15622
15584
|
}
|
|
15623
15585
|
async function runLegacyMigration(dataDir) {
|
|
15624
15586
|
const legacyRoot = env.DATA_DIR;
|
|
15625
|
-
const legacyKeyPath =
|
|
15587
|
+
const legacyKeyPath = join27(legacyRoot, "dbs", "encryption-key.json");
|
|
15626
15588
|
try {
|
|
15627
15589
|
await access3(legacyKeyPath);
|
|
15628
15590
|
} catch {
|
|
@@ -15630,20 +15592,20 @@ async function runLegacyMigration(dataDir) {
|
|
|
15630
15592
|
}
|
|
15631
15593
|
console.log("[legacy-migration] Legacy Runeya data detected, starting migration\u2026");
|
|
15632
15594
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
15633
|
-
const legacyTmp =
|
|
15595
|
+
const legacyTmp = join27(abCwd, ".runeya-tmp");
|
|
15634
15596
|
await rename16(legacyRoot, legacyTmp);
|
|
15635
|
-
await
|
|
15636
|
-
const legacyOld =
|
|
15597
|
+
await mkdir23(dataDir, { recursive: true });
|
|
15598
|
+
const legacyOld = join27(dataDir, `backup-${timestamp}`);
|
|
15637
15599
|
await rename16(legacyTmp, legacyOld);
|
|
15638
15600
|
console.log(`[legacy-migration] Legacy data backed up to ${legacyOld}`);
|
|
15639
|
-
const keyJson = JSON.parse(await
|
|
15601
|
+
const keyJson = JSON.parse(await readFile20(join27(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
|
|
15640
15602
|
const sodium = await getSodium();
|
|
15641
15603
|
const sodiumKey = sodium.from_base64(keyJson.encryptionKey);
|
|
15642
15604
|
const keyBuffer = Buffer.from(sodiumKey);
|
|
15643
15605
|
const runyeaKeyHex = keyBuffer.toString("hex");
|
|
15644
15606
|
await setEncryptionKey(runyeaKeyHex);
|
|
15645
15607
|
console.log("[legacy-migration] Encryption key loaded in memory (temporary)");
|
|
15646
|
-
const dbsDir =
|
|
15608
|
+
const dbsDir = join27(legacyOld, "dbs");
|
|
15647
15609
|
async function readEncrypted(filePath) {
|
|
15648
15610
|
try {
|
|
15649
15611
|
return await decryptLegacyFile(filePath, sodiumKey, sodium);
|
|
@@ -15652,29 +15614,29 @@ async function runLegacyMigration(dataDir) {
|
|
|
15652
15614
|
return null;
|
|
15653
15615
|
}
|
|
15654
15616
|
}
|
|
15655
|
-
const envsDir =
|
|
15656
|
-
const envFiles = (await
|
|
15617
|
+
const envsDir = join27(dbsDir, "envs");
|
|
15618
|
+
const envFiles = (await readdir10(envsDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
15657
15619
|
const legacyEnvs = {};
|
|
15658
15620
|
for (const file of envFiles) {
|
|
15659
15621
|
const name = file.replace(".encrypted.json", "");
|
|
15660
|
-
legacyEnvs[name] = await readEncrypted(
|
|
15622
|
+
legacyEnvs[name] = await readEncrypted(join27(envsDir, file));
|
|
15661
15623
|
}
|
|
15662
|
-
const servicesDir =
|
|
15663
|
-
const serviceFiles = (await
|
|
15624
|
+
const servicesDir = join27(dbsDir, "services");
|
|
15625
|
+
const serviceFiles = (await readdir10(servicesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
15664
15626
|
const legacyServices = {};
|
|
15665
15627
|
for (const file of serviceFiles) {
|
|
15666
15628
|
const name = file.replace(".encrypted.json", "");
|
|
15667
15629
|
if (!name) continue;
|
|
15668
|
-
const data = await readEncrypted(
|
|
15630
|
+
const data = await readEncrypted(join27(servicesDir, file));
|
|
15669
15631
|
if (data?.label) legacyServices[name] = data;
|
|
15670
15632
|
}
|
|
15671
|
-
const overridesDir =
|
|
15672
|
-
const overrideFiles = (await
|
|
15633
|
+
const overridesDir = join27(dbsDir, "overrides");
|
|
15634
|
+
const overrideFiles = (await readdir10(overridesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
15673
15635
|
const legacyServiceOverrides = {};
|
|
15674
15636
|
const legacyEnvOverrides = {};
|
|
15675
15637
|
for (const file of overrideFiles) {
|
|
15676
15638
|
const baseName = file.replace(".encrypted.json", "");
|
|
15677
|
-
const data = await readEncrypted(
|
|
15639
|
+
const data = await readEncrypted(join27(overridesDir, file));
|
|
15678
15640
|
if (!data) continue;
|
|
15679
15641
|
if (baseName.endsWith("-envs")) {
|
|
15680
15642
|
legacyServiceOverrides[baseName.slice(0, -5)] = data;
|
|
@@ -15682,11 +15644,11 @@ async function runLegacyMigration(dataDir) {
|
|
|
15682
15644
|
legacyEnvOverrides[baseName.slice(0, -12)] = data;
|
|
15683
15645
|
}
|
|
15684
15646
|
}
|
|
15685
|
-
const parsersDir =
|
|
15686
|
-
const parserFiles = (await
|
|
15647
|
+
const parsersDir = join27(dbsDir, "parsers");
|
|
15648
|
+
const parserFiles = (await readdir10(parsersDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
15687
15649
|
const legacyParsers = [];
|
|
15688
15650
|
for (const file of parserFiles) {
|
|
15689
|
-
const data = await readEncrypted(
|
|
15651
|
+
const data = await readEncrypted(join27(parsersDir, file));
|
|
15690
15652
|
if (data?.id) legacyParsers.push(data);
|
|
15691
15653
|
}
|
|
15692
15654
|
const projectMonoId = randomUUID12();
|
|
@@ -16027,7 +15989,7 @@ async function runLegacyMigration(dataDir) {
|
|
|
16027
15989
|
}
|
|
16028
15990
|
let existingSettings = {};
|
|
16029
15991
|
try {
|
|
16030
|
-
existingSettings = JSON.parse(await
|
|
15992
|
+
existingSettings = JSON.parse(await readFile20(join27(dataDir, "settings.json"), "utf-8"));
|
|
16031
15993
|
} catch {
|
|
16032
15994
|
}
|
|
16033
15995
|
const migratedParsers = legacyParsers.map((p) => ({
|
|
@@ -16048,11 +16010,11 @@ ${p.transform ?? ""}`,
|
|
|
16048
16010
|
...migratedParsers
|
|
16049
16011
|
]
|
|
16050
16012
|
};
|
|
16051
|
-
await atomicWrite(
|
|
16052
|
-
await atomicWrite(
|
|
16053
|
-
await atomicWrite(
|
|
16054
|
-
await atomicWrite(
|
|
16055
|
-
await atomicWrite(
|
|
16013
|
+
await atomicWrite(join27(dataDir, "projects.json"), runeProjects);
|
|
16014
|
+
await atomicWrite(join27(dataDir, "services.json"), runeServices);
|
|
16015
|
+
await atomicWrite(join27(dataDir, "environments.json"), runeEnvs);
|
|
16016
|
+
await atomicWrite(join27(dataDir, "environment-overrides.json"), flatOverrides);
|
|
16017
|
+
await atomicWrite(join27(dataDir, "settings.json"), newSettings);
|
|
16056
16018
|
const svcEnvCount = runeEnvs.filter((e) => e.scope === "service").length;
|
|
16057
16019
|
console.log(`[legacy-migration] Written: ${runeProjects.length} projects, ${runeServices.length} services, ${runeEnvs.length} environments (${svcEnvCount} service-scoped)`);
|
|
16058
16020
|
console.log(`[legacy-migration] Overrides: ${Object.keys(flatOverrides).length}, custom parsers: ${migratedParsers.length} (all disabled \u2014 rewrite required)`);
|
|
@@ -16063,12 +16025,12 @@ ${p.transform ?? ""}`,
|
|
|
16063
16025
|
}
|
|
16064
16026
|
async function findLegacyKeyInBackup(dataDir) {
|
|
16065
16027
|
try {
|
|
16066
|
-
const entries2 = await
|
|
16028
|
+
const entries2 = await readdir10(dataDir);
|
|
16067
16029
|
const backupDirs = entries2.filter((e) => e.startsWith("backup-")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).reverse();
|
|
16068
16030
|
for (const dir of backupDirs) {
|
|
16069
|
-
const keyJsonPath =
|
|
16031
|
+
const keyJsonPath = join27(dataDir, dir, "dbs", "encryption-key.json");
|
|
16070
16032
|
try {
|
|
16071
|
-
const raw = await
|
|
16033
|
+
const raw = await readFile20(keyJsonPath, "utf-8");
|
|
16072
16034
|
const parsed = JSON.parse(raw);
|
|
16073
16035
|
if (!parsed.encryptionKey) continue;
|
|
16074
16036
|
const sodium = await getSodium();
|
|
@@ -16089,14 +16051,14 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16089
16051
|
const { resolve: resolve7 } = await import("path");
|
|
16090
16052
|
const abDataDir = resolve7(dataDir);
|
|
16091
16053
|
const candidateDirs = [
|
|
16092
|
-
|
|
16093
|
-
|
|
16054
|
+
join27(dirname11(env.DATA_DIR), ".runeya", "dbs", "overrides"),
|
|
16055
|
+
join27(abDataDir, "dbs", "overrides")
|
|
16094
16056
|
];
|
|
16095
16057
|
let overridesDir = null;
|
|
16096
16058
|
let overrideFiles = [];
|
|
16097
16059
|
for (const dir of candidateDirs) {
|
|
16098
16060
|
try {
|
|
16099
|
-
const files = (await
|
|
16061
|
+
const files = (await readdir10(dir)).filter((f) => f.endsWith(".encrypted.json"));
|
|
16100
16062
|
if (files.length > 0) {
|
|
16101
16063
|
overridesDir = dir;
|
|
16102
16064
|
overrideFiles = files;
|
|
@@ -16106,7 +16068,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16106
16068
|
}
|
|
16107
16069
|
}
|
|
16108
16070
|
if (!overridesDir || overrideFiles.length === 0) return false;
|
|
16109
|
-
const legacyKeyPath =
|
|
16071
|
+
const legacyKeyPath = join27(overridesDir, "..", "encryption-key.json");
|
|
16110
16072
|
try {
|
|
16111
16073
|
await access3(legacyKeyPath);
|
|
16112
16074
|
return false;
|
|
@@ -16124,7 +16086,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16124
16086
|
const envNameToId = {};
|
|
16125
16087
|
let envsRaw;
|
|
16126
16088
|
try {
|
|
16127
|
-
envsRaw = JSON.parse(await
|
|
16089
|
+
envsRaw = JSON.parse(await readFile20(join27(abDataDir, "environments.json"), "utf-8"));
|
|
16128
16090
|
for (const _env of envsRaw) {
|
|
16129
16091
|
const env2 = _env;
|
|
16130
16092
|
if (env2["name"] && env2["id"]) {
|
|
@@ -16143,7 +16105,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16143
16105
|
let servicesRaw = [];
|
|
16144
16106
|
const serviceNameToId = {};
|
|
16145
16107
|
try {
|
|
16146
|
-
servicesRaw = JSON.parse(await
|
|
16108
|
+
servicesRaw = JSON.parse(await readFile20(join27(abDataDir, "services.json"), "utf-8"));
|
|
16147
16109
|
for (const _svc of servicesRaw) {
|
|
16148
16110
|
const svc = _svc;
|
|
16149
16111
|
if (svc["name"] && svc["id"]) serviceNameToId[svc["name"]] = svc["id"];
|
|
@@ -16160,7 +16122,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16160
16122
|
const envName = baseName.slice(0, -"-environment".length);
|
|
16161
16123
|
let data;
|
|
16162
16124
|
try {
|
|
16163
|
-
data = await decryptLegacyFile(
|
|
16125
|
+
data = await decryptLegacyFile(join27(overridesDir, file), sodiumKey, sodium);
|
|
16164
16126
|
} catch (err) {
|
|
16165
16127
|
console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
|
|
16166
16128
|
continue;
|
|
@@ -16187,7 +16149,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16187
16149
|
}
|
|
16188
16150
|
}
|
|
16189
16151
|
if (Object.keys(flatOverrides).length > 0) {
|
|
16190
|
-
await atomicWrite(
|
|
16152
|
+
await atomicWrite(join27(abDataDir, "environment-overrides.json"), flatOverrides);
|
|
16191
16153
|
console.log(`[override-migration] Written ${Object.keys(flatOverrides).length} override(s) to environment-overrides.json`);
|
|
16192
16154
|
}
|
|
16193
16155
|
let envsModified = false;
|
|
@@ -16197,7 +16159,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16197
16159
|
const serviceName = baseName.slice(0, -"-envs".length);
|
|
16198
16160
|
let data;
|
|
16199
16161
|
try {
|
|
16200
|
-
data = await decryptLegacyFile(
|
|
16162
|
+
data = await decryptLegacyFile(join27(overridesDir, file), sodiumKey, sodium);
|
|
16201
16163
|
} catch (err) {
|
|
16202
16164
|
console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
|
|
16203
16165
|
continue;
|
|
@@ -16256,7 +16218,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16256
16218
|
}
|
|
16257
16219
|
}
|
|
16258
16220
|
if (envsModified) {
|
|
16259
|
-
await atomicWrite(
|
|
16221
|
+
await atomicWrite(join27(abDataDir, "environments.json"), envsRaw);
|
|
16260
16222
|
console.log("[override-migration] Merged service overrides into environments.json");
|
|
16261
16223
|
}
|
|
16262
16224
|
const migratedDir = overridesDir.replace(/overrides$/, "overrides-migrated");
|
|
@@ -16271,7 +16233,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
16271
16233
|
}
|
|
16272
16234
|
|
|
16273
16235
|
// ../server/src/services/server-state.ts
|
|
16274
|
-
import { join as
|
|
16236
|
+
import { join as join28 } from "path";
|
|
16275
16237
|
import { stat as stat12 } from "fs/promises";
|
|
16276
16238
|
var ENCRYPTED_KEY_FILENAME2 = ".encryption.key.enc";
|
|
16277
16239
|
var VERIFY_FILENAME2 = ".runeya-verify";
|
|
@@ -16281,22 +16243,22 @@ async function getServerState() {
|
|
|
16281
16243
|
let hasVerify = false;
|
|
16282
16244
|
let hasData = false;
|
|
16283
16245
|
try {
|
|
16284
|
-
await stat12(
|
|
16246
|
+
await stat12(join28(dataDir, ENCRYPTED_KEY_FILENAME2));
|
|
16285
16247
|
hasEncFile = true;
|
|
16286
16248
|
} catch {
|
|
16287
16249
|
}
|
|
16288
16250
|
try {
|
|
16289
|
-
await stat12(
|
|
16251
|
+
await stat12(join28(dataDir, VERIFY_FILENAME2));
|
|
16290
16252
|
hasVerify = true;
|
|
16291
16253
|
} catch {
|
|
16292
16254
|
}
|
|
16293
16255
|
try {
|
|
16294
|
-
await stat12(
|
|
16256
|
+
await stat12(join28(dataDir, "projects.json"));
|
|
16295
16257
|
hasData = true;
|
|
16296
16258
|
} catch {
|
|
16297
16259
|
}
|
|
16298
16260
|
try {
|
|
16299
|
-
await stat12(
|
|
16261
|
+
await stat12(join28(dataDir, "project.json"));
|
|
16300
16262
|
hasData = true;
|
|
16301
16263
|
} catch {
|
|
16302
16264
|
}
|
|
@@ -17556,4 +17518,4 @@ export {
|
|
|
17556
17518
|
pullEnv,
|
|
17557
17519
|
registerInstance
|
|
17558
17520
|
};
|
|
17559
|
-
//# sourceMappingURL=src-
|
|
17521
|
+
//# sourceMappingURL=src-Q3PSCMS7.js.map
|