termdock 1.4.298 → 1.4.300
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/README.en.md +1 -1
- package/README.md +2 -1
- package/dist/client/assets/App-7k2I_09f.js +179 -0
- package/dist/client/assets/App-7k2I_09f.js.br +0 -0
- package/dist/client/assets/App-7k2I_09f.js.gz +0 -0
- package/dist/client/assets/ArchitecturePanel-BrfM7rGb.js +12 -0
- package/dist/client/assets/ArchitecturePanel-BrfM7rGb.js.br +0 -0
- package/dist/client/assets/ArchitecturePanel-BrfM7rGb.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-BrAwRa20.js → ChangeWalkthroughPanel-RoMt6giT.js} +2 -2
- package/dist/client/assets/ChangeWalkthroughPanel-RoMt6giT.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-RoMt6giT.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-CK2BKKnn.js +1 -0
- package/dist/client/assets/CollaborationMainWorkspace-CK2BKKnn.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-CK2BKKnn.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-FlTbV9e1.js +2 -0
- package/dist/client/assets/ComputerControlView-AdjmvNUa.js +5 -0
- package/dist/client/assets/ComputerControlView-AdjmvNUa.js.br +0 -0
- package/dist/client/assets/ComputerControlView-AdjmvNUa.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-DiAEIEvj.js → DagPlayground-x8-mp6wC.js} +1 -1
- package/dist/client/assets/DagPlayground-x8-mp6wC.js.br +0 -0
- package/dist/client/assets/DagPlayground-x8-mp6wC.js.gz +0 -0
- package/dist/client/assets/{DiffLab-BOXARpIG.js → DiffLab-B9KLZ--H.js} +1 -1
- package/dist/client/assets/DiffLab-B9KLZ--H.js.br +0 -0
- package/dist/client/assets/DiffLab-B9KLZ--H.js.gz +0 -0
- package/dist/client/assets/{DiffReview-C90Qt0Gx.js → DiffReview-IEcDVREB.js} +1 -1
- package/dist/client/assets/DiffReview-IEcDVREB.js.br +0 -0
- package/dist/client/assets/DiffReview-IEcDVREB.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-CrVXGE1A.js → DiffReviewLab-lWra5pTJ.js} +2 -2
- package/dist/client/assets/DiffReviewLab-lWra5pTJ.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-lWra5pTJ.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-BA_9bZxs.js → DiffViewer-B2Rue9PA.js} +8 -8
- package/dist/client/assets/DiffViewer-B2Rue9PA.js.br +0 -0
- package/dist/client/assets/DiffViewer-B2Rue9PA.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-BX75IksJ.js → ModelPreview-NMQsfqAe.js} +1 -1
- package/dist/client/assets/ModelPreview-NMQsfqAe.js.br +0 -0
- package/dist/client/assets/ModelPreview-NMQsfqAe.js.gz +0 -0
- package/dist/client/assets/RightSidebar-BOBbegdP.js +1677 -0
- package/dist/client/assets/RightSidebar-BOBbegdP.js.br +0 -0
- package/dist/client/assets/RightSidebar-BOBbegdP.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-BGF7DKjR.js → abnfDiagram-VCTEODGH-BIVuPQbA.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-BIVuPQbA.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BIVuPQbA.js.gz +0 -0
- package/dist/client/assets/app-CHoOHnjf.js +35 -0
- package/dist/client/assets/app-CHoOHnjf.js.br +0 -0
- package/dist/client/assets/app-CHoOHnjf.js.gz +0 -0
- package/dist/client/assets/{arc-DZUR8G8M.js → arc-CNTT1p1A.js} +1 -1
- package/dist/client/assets/arc-CNTT1p1A.js.br +0 -0
- package/dist/client/assets/arc-CNTT1p1A.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-CWXE4hoZ.js → architectureDiagram-5GKGNRK7-Cl3gg8ZE.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Cl3gg8ZE.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Cl3gg8ZE.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-BZdUQzE2.js → blockDiagram-I7D4REHJ-Sxmfy_3_.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-Sxmfy_3_.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Sxmfy_3_.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DA7m-AbK.js → c4Diagram-7LVT6UL2-DaTnrOn7.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-DaTnrOn7.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DaTnrOn7.js.gz +0 -0
- package/dist/client/assets/channel-CEvBVvt3.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-Dg-zs5cw.js → chunk-2Q5K7J3B-BoiPA32P.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-DKMA2xz_.js → chunk-5VM5RSS4-G5G1YBLk.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-ByyS2RgO.js → chunk-F27PBJKO-BNHv_EnL.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-BNHv_EnL.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BNHv_EnL.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-DTTmE4mJ.js → chunk-IMKFNOWR-CxgtyKdq.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-CxgtyKdq.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-CxgtyKdq.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-D9nr-n4D.js → chunk-JWPE2WC7-UO-0iK-G.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-BPVZ3XrQ.js → chunk-POPQ4Y6H-Cox9z-e2.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-Ctn5SKJf.js → chunk-SVP7TREG-B2W8-Lxh.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-B2W8-Lxh.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-B2W8-Lxh.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-BCNSq9V4.js → chunk-TICWLB2K-BYDr9nkD.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-BYDr9nkD.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BYDr9nkD.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-BlJ1FL7I.js → chunk-XXDRQBXY-atArreox.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-nSmGM9Fl.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-nSmGM9Fl.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-Cv4gLm7L.js → cose-bilkent-JH36ORCC-CvfJj195.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-CvfJj195.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-CvfJj195.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-BNbPniZY.js → cynefin-OW5HDTMX-ByWE_fUn.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-ByWE_fUn.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-ByWE_fUn.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-DFR2fyKM.js → cynefinDiagram-5FMLGOSQ-BRT0d0MN.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BRT0d0MN.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BRT0d0MN.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-DM9QOAp3.js → dagre-GXQ25YYZ-Twtyv7ud.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-Twtyv7ud.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Twtyv7ud.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-DqeCdIIK.js → diagram-S7CK7UJ4-Vl06hZtd.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-Vl06hZtd.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-Vl06hZtd.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-Cc1KiE_N.js → diagram-UQ7AKVKN-Cp39Mpse.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-Cp39Mpse.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Cp39Mpse.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-I_0NpClr.js → diagram-VSXAHHWV-Drchji0N.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-Drchji0N.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Drchji0N.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-DLjDAEEa.js → diagram-VX7I27RA-rp8n_lct.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-rp8n_lct.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-rp8n_lct.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-DHo74-rP.js → diagram-Z3DM3KII-BEGDaMBm.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-BEGDaMBm.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-BEGDaMBm.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-S9dCP2Z9.js → ebnfDiagram-PWID7BFC-kvgdD4A7.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-kvgdD4A7.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-kvgdD4A7.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-CR99Mjq9.js → erDiagram-RLTQ6QDP-DcYHv9hK.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-DcYHv9hK.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DcYHv9hK.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-CIqCbTFC.js → flowDiagram-HODETNUW-T9FQKn1F.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-T9FQKn1F.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-T9FQKn1F.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-CSpUeAKu.js → ganttDiagram-EL5Y4UJY-BEB_X-y5.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BEB_X-y5.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BEB_X-y5.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-C3GqyqzH.js → gestureArbiter-Do0TIdkh.js} +1 -1
- package/dist/client/assets/gestureArbiter-Do0TIdkh.js.br +0 -0
- package/dist/client/assets/gestureArbiter-Do0TIdkh.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-DBICHCGp.js → gitGraphDiagram-WWUBYQGX-_0Y7kKA2.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-_0Y7kKA2.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-_0Y7kKA2.js.gz +0 -0
- package/dist/client/assets/{index-C6ftfFlG.css → index-D2_-ypCL.css} +1 -1
- package/dist/client/assets/index-D2_-ypCL.css.br +0 -0
- package/dist/client/assets/index-D2_-ypCL.css.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-CjPrG37z.js → infoDiagram-27XIBGKW-B6y0nIx8.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-C718heAs.js → ishikawaDiagram-5VMMS53U-ebGuykGn.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-ebGuykGn.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-ebGuykGn.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-fu76FPSY.js → journeyDiagram-3NMN7TZE-DJDkcIq5.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DJDkcIq5.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DJDkcIq5.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-BVj6v9-z.js → kanban-definition-UXKFOSKX-pTBbviRZ.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-pTBbviRZ.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-pTBbviRZ.js.gz +0 -0
- package/dist/client/assets/{layout-Dspjm0zn.js → layout-Dw9FB4Uc.js} +1 -1
- package/dist/client/assets/layout-Dw9FB4Uc.js.br +0 -0
- package/dist/client/assets/layout-Dw9FB4Uc.js.gz +0 -0
- package/dist/client/assets/{linear-BktGv6ei.js → linear-CyLfVGgB.js} +1 -1
- package/dist/client/assets/linear-CyLfVGgB.js.br +0 -0
- package/dist/client/assets/linear-CyLfVGgB.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-Dt2TVLfQ.js → mermaid.core-xRkirK0l.js} +5 -5
- package/dist/client/assets/mermaid.core-xRkirK0l.js.br +0 -0
- package/dist/client/assets/mermaid.core-xRkirK0l.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-DwxtrCyv.js → mindmap-definition-YA3MSWOX-BxvaDEGQ.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BxvaDEGQ.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BxvaDEGQ.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-B5Inu7kn.js → passwordBootstrap-CaYbIQDC.js} +1 -1
- package/dist/client/assets/passwordBootstrap-CaYbIQDC.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-CaYbIQDC.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-eDDhkygS.js → pegDiagram-XKGWAZYB-Cxh-Bbay.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-Cxh-Bbay.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-Cxh-Bbay.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-C-A_a53h.js → pieDiagram-E7YTZNPT-CgOsk0Ai.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-CgOsk0Ai.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CgOsk0Ai.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-BRmQ1U56.js → quadrantDiagram-AXDQQJYC-D1S_gKcT.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-D1S_gKcT.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-D1S_gKcT.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-BRmkfIUn.js → railroadDiagram-O6MQD6OU-BG5e87Np.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BG5e87Np.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BG5e87Np.js.gz +0 -0
- package/dist/client/assets/rdpSession-DL1rZ49m.js +1 -0
- package/dist/client/assets/rdpSession-DL1rZ49m.js.br +0 -0
- package/dist/client/assets/rdpSession-DL1rZ49m.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-DB039WbW.js +1 -0
- package/dist/client/assets/referenceLongPress-DB039WbW.js.br +0 -0
- package/dist/client/assets/referenceLongPress-DB039WbW.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-C95pjpXU.js → requirementDiagram-BXWQKSXE-C37DsQfu.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C37DsQfu.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C37DsQfu.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BD7SJZ8y.js → sankeyDiagram-P5KCCOFB-DLzhCYoj.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-DLzhCYoj.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-DLzhCYoj.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D0egOILU.js → sequenceDiagram-WJ2MYXX4-DeVpSvB0.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DeVpSvB0.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DeVpSvB0.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-BaJGHebj.js → sizeCapture-INFHLROL-CE03E66U.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-CE03E66U.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CE03E66U.js.gz +0 -0
- package/dist/client/assets/{sparkles-D4t6BWgT.js → sparkles-ButxQzs4.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-DEZo4cxr.js → stateDiagram-D77RDMKH-DuqhHT3k.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-DuqhHT3k.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DuqhHT3k.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CxfzEwAH.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-DSZuZ910.js → swimlanes-42K2YHIH-DxkSdJ8g.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-DxkSdJ8g.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-DxkSdJ8g.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-B_muKJLi.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-Bw0gUCrJ.js → timeline-definition-24CTP7MA-DVpcJ6y5.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-DVpcJ6y5.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DVpcJ6y5.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-Cf9mn5Ug.js → triangle-alert-BRy562K_.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-B0gzw-En.js → vennDiagram-4TSXK5OY-BEKXLWGa.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-BEKXLWGa.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BEKXLWGa.js.gz +0 -0
- package/dist/client/assets/{videoCompression-CyTyZG2b.js → videoCompression-CoVXZ8qw.js} +1 -1
- package/dist/client/assets/videoCompression-CoVXZ8qw.js.br +0 -0
- package/dist/client/assets/videoCompression-CoVXZ8qw.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-vaiRXtUT.js → wardleyDiagram-VM6X3IG4-Cj30GTa9.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-Cj30GTa9.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-Cj30GTa9.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-Dosv-6JB.js → xychartDiagram-S5SC5T6Z-Cm7NEdCl.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Cm7NEdCl.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Cm7NEdCl.js.gz +0 -0
- package/dist/client/index.html +2 -2
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/agent/collaborationCli.js +45 -6
- package/dist/server/agent/collaborationRoutes.js +4 -2
- package/dist/server/agent/collaborationStore.js +9 -0
- package/dist/server/agent/collaborationTaskCli.js +11 -3
- package/dist/server/agent/collaborationTaskService.js +14 -8
- package/dist/server/agent/collaborationTaskStore.js +67 -25
- package/dist/server/agent/integrationCli.js +227 -0
- package/dist/server/agent/integrationServer.js +326 -0
- package/dist/server/agent/integrationSessions.js +296 -0
- package/dist/server/agent/integrationStore.js +199 -0
- package/dist/server/cli.js +29 -0
- package/dist/server/entry.js +26 -7
- package/dist/server/routes/filesystem.js +120 -87
- package/dist/server/routes/terminal.js +158 -4
- package/dist/server/utils/localAccess.js +27 -7
- package/dist/server/utils/sessionDisplayName.js +53 -0
- package/docs/integrations.md +239 -0
- package/package.json +3 -2
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-D7FKaW3e.js +0 -179
- package/dist/client/assets/App-D7FKaW3e.js.br +0 -0
- package/dist/client/assets/App-D7FKaW3e.js.gz +0 -0
- package/dist/client/assets/ArchitecturePanel-CU5vv8Fm.js +0 -7
- package/dist/client/assets/ArchitecturePanel-CU5vv8Fm.js.br +0 -0
- package/dist/client/assets/ArchitecturePanel-CU5vv8Fm.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-BrAwRa20.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-BrAwRa20.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-CH6mfIDe.js +0 -1
- package/dist/client/assets/CollaborationMainWorkspace-CH6mfIDe.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-CH6mfIDe.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-DkhB6WXO.js +0 -2
- package/dist/client/assets/ComputerControlView-CI-TMTRg.js +0 -5
- package/dist/client/assets/ComputerControlView-CI-TMTRg.js.br +0 -0
- package/dist/client/assets/ComputerControlView-CI-TMTRg.js.gz +0 -0
- package/dist/client/assets/DagPlayground-DiAEIEvj.js.br +0 -0
- package/dist/client/assets/DagPlayground-DiAEIEvj.js.gz +0 -0
- package/dist/client/assets/DiffLab-BOXARpIG.js.br +0 -0
- package/dist/client/assets/DiffLab-BOXARpIG.js.gz +0 -0
- package/dist/client/assets/DiffReview-C90Qt0Gx.js.br +0 -0
- package/dist/client/assets/DiffReview-C90Qt0Gx.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-CrVXGE1A.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CrVXGE1A.js.gz +0 -0
- package/dist/client/assets/DiffViewer-BA_9bZxs.js.br +0 -0
- package/dist/client/assets/DiffViewer-BA_9bZxs.js.gz +0 -0
- package/dist/client/assets/ModelPreview-BX75IksJ.js.br +0 -0
- package/dist/client/assets/ModelPreview-BX75IksJ.js.gz +0 -0
- package/dist/client/assets/RightSidebar-B_dO0nU5.js +0 -1678
- package/dist/client/assets/RightSidebar-B_dO0nU5.js.br +0 -0
- package/dist/client/assets/RightSidebar-B_dO0nU5.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BGF7DKjR.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BGF7DKjR.js.gz +0 -0
- package/dist/client/assets/app-BJP8Xgmi.js +0 -35
- package/dist/client/assets/app-BJP8Xgmi.js.br +0 -0
- package/dist/client/assets/app-BJP8Xgmi.js.gz +0 -0
- package/dist/client/assets/arc-DZUR8G8M.js.br +0 -0
- package/dist/client/assets/arc-DZUR8G8M.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CWXE4hoZ.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CWXE4hoZ.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-BZdUQzE2.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-BZdUQzE2.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DA7m-AbK.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DA7m-AbK.js.gz +0 -0
- package/dist/client/assets/channel-DD06PSxm.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-ByyS2RgO.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-ByyS2RgO.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DTTmE4mJ.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DTTmE4mJ.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Ctn5SKJf.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Ctn5SKJf.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BCNSq9V4.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BCNSq9V4.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-DkVrFG7y.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-DkVrFG7y.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-Cv4gLm7L.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-Cv4gLm7L.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-BNbPniZY.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-BNbPniZY.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DFR2fyKM.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DFR2fyKM.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DM9QOAp3.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DM9QOAp3.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DqeCdIIK.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DqeCdIIK.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Cc1KiE_N.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Cc1KiE_N.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-I_0NpClr.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-I_0NpClr.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DLjDAEEa.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DLjDAEEa.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-DHo74-rP.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-DHo74-rP.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-S9dCP2Z9.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-S9dCP2Z9.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-CR99Mjq9.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-CR99Mjq9.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CIqCbTFC.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CIqCbTFC.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CSpUeAKu.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CSpUeAKu.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-C3GqyqzH.js.br +0 -0
- package/dist/client/assets/gestureArbiter-C3GqyqzH.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DBICHCGp.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DBICHCGp.js.gz +0 -0
- package/dist/client/assets/index-C6ftfFlG.css.br +0 -0
- package/dist/client/assets/index-C6ftfFlG.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-C718heAs.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-C718heAs.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-fu76FPSY.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-fu76FPSY.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BVj6v9-z.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BVj6v9-z.js.gz +0 -0
- package/dist/client/assets/layout-Dspjm0zn.js.br +0 -0
- package/dist/client/assets/layout-Dspjm0zn.js.gz +0 -0
- package/dist/client/assets/linear-BktGv6ei.js.br +0 -0
- package/dist/client/assets/linear-BktGv6ei.js.gz +0 -0
- package/dist/client/assets/mermaid.core-Dt2TVLfQ.js.br +0 -0
- package/dist/client/assets/mermaid.core-Dt2TVLfQ.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DwxtrCyv.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DwxtrCyv.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-B5Inu7kn.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-B5Inu7kn.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-eDDhkygS.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-eDDhkygS.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-C-A_a53h.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-C-A_a53h.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BRmQ1U56.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BRmQ1U56.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BRmkfIUn.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BRmkfIUn.js.gz +0 -0
- package/dist/client/assets/rdpSession-DknTu7rD.js +0 -1
- package/dist/client/assets/rdpSession-DknTu7rD.js.br +0 -0
- package/dist/client/assets/rdpSession-DknTu7rD.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-CnGp4Gdi.js +0 -1
- package/dist/client/assets/referenceLongPress-CnGp4Gdi.js.br +0 -0
- package/dist/client/assets/referenceLongPress-CnGp4Gdi.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C95pjpXU.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C95pjpXU.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BD7SJZ8y.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BD7SJZ8y.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D0egOILU.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D0egOILU.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-BaJGHebj.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-BaJGHebj.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DEZo4cxr.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DEZo4cxr.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CJg7AUmk.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-DSZuZ910.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-DSZuZ910.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-CW58IimW.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-Bw0gUCrJ.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-Bw0gUCrJ.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-B0gzw-En.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-B0gzw-En.js.gz +0 -0
- package/dist/client/assets/videoCompression-CyTyZG2b.js.br +0 -0
- package/dist/client/assets/videoCompression-CyTyZG2b.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-vaiRXtUT.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-vaiRXtUT.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Dosv-6JB.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Dosv-6JB.js.gz +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import { CollaborationError } from './collaborationProtocol.js';
|
|
8
|
+
import { terminalMessage } from './collaborationStore.js';
|
|
9
|
+
import { IntegrationStore, INTEGRATION_PROTOCOL, integrationError } from './integrationStore.js';
|
|
10
|
+
import { IntegrationSessions, publicIntegrationSession } from './integrationSessions.js';
|
|
11
|
+
import { getTermdockVersion } from '../utils/version.js';
|
|
12
|
+
export function integrationSocketPath(port) { return path.join(homedir(), '.termdock', `integration-${port}.sock`); }
|
|
13
|
+
/** A local authenticated API and push stream. It is never mounted on the
|
|
14
|
+
* public HTTP application and does not reuse an integration's admin token. */
|
|
15
|
+
export class IntegrationRuntime {
|
|
16
|
+
options;
|
|
17
|
+
store;
|
|
18
|
+
sessions;
|
|
19
|
+
server;
|
|
20
|
+
stops = [];
|
|
21
|
+
streams = new Set();
|
|
22
|
+
sent = new Map();
|
|
23
|
+
syncing = false;
|
|
24
|
+
retry;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.store = new IntegrationStore(path.join(options.directory, 'integrations.json'));
|
|
28
|
+
this.sessions = new IntegrationSessions(path.join(options.directory, 'integration-sessions.json'), options.sessions);
|
|
29
|
+
const changed = () => { try {
|
|
30
|
+
this.sync();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.failStreams(error);
|
|
34
|
+
this.scheduleRetry();
|
|
35
|
+
} };
|
|
36
|
+
this.stops.push(options.messages.subscribe(changed), options.taskStore.subscribe(changed), this.sessions.subscribe(changed));
|
|
37
|
+
const app = express();
|
|
38
|
+
app.use(express.json({ limit: '1mb' }));
|
|
39
|
+
app.use((req, res, next) => {
|
|
40
|
+
if (req.headers['x-termdock-integration-protocol'] !== String(INTEGRATION_PROTOCOL)) {
|
|
41
|
+
res.status(409).json({ ...this.error(new CollaborationError('INCOMPATIBLE_INTEGRATION_PROTOCOL', 'Unsupported integration protocol', 409)), protocol: INTEGRATION_PROTOCOL });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
const run = (handler) => async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const value = await handler(req, res);
|
|
49
|
+
if (!res.headersSent)
|
|
50
|
+
res.json(value);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
res.status(error instanceof CollaborationError ? error.httpStatus : 500).json(this.error(error));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const principal = (req) => this.store.authenticate(String(req.headers['x-termdock-integration-id'] ?? ''), String(req.headers.authorization ?? '').replace(/^Bearer /, ''));
|
|
57
|
+
const admin = (req) => {
|
|
58
|
+
const given = Buffer.from(String(req.headers['x-termdock-local-token'] ?? '')), expected = Buffer.from(options.adminToken ?? '');
|
|
59
|
+
if (!expected.length || given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
60
|
+
integrationError('INTEGRATION_ADMIN_REQUIRED', 'Local administration credential is required', 401);
|
|
61
|
+
};
|
|
62
|
+
app.get('/admin/groups', run(req => { admin(req); return { groups: options.messages.list().filter(g => !g.federated).map(g => ({ id: g.id, name: g.name })) }; }));
|
|
63
|
+
app.get('/admin/principals', run(req => { admin(req); return { integrations: this.store.policies() }; }));
|
|
64
|
+
app.post('/admin/principals', run(req => { admin(req); this.group(req.body?.groupId); const provisioned = this.store.provision(req.body); try {
|
|
65
|
+
this.sync();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
this.scheduleRetry();
|
|
69
|
+
} return { id: provisioned.principal.id, token: provisioned.token, protocol: INTEGRATION_PROTOCOL }; }));
|
|
70
|
+
app.post('/admin/principals/:id/revoke', run(req => { admin(req); this.store.revoke(req.params.id); return { ok: true }; }));
|
|
71
|
+
app.get('/capabilities', run(req => { const p = principal(req); return { ...this.capabilities(), principal: { id: p.id, group_id: p.groupId, permissions: p.permissions, launch_profiles: p.launchProfiles.map(profile => ({ id: profile.id, agent_slug: profile.agentSlug, cwd_roots: profile.cwdRoots })) } }; }));
|
|
72
|
+
app.get('/tasks', run(req => { const p = principal(req); this.authorize(p, 'task.read', req.query.group); return { tasks: options.tasks.list(p.groupId, null) }; }));
|
|
73
|
+
app.get('/tasks/:id', run(async (req) => { const p = principal(req); this.taskPermission(p, req.params.id, 'task.read'); return { task: await options.tasks.get(req.params.id, null) }; }));
|
|
74
|
+
app.post('/tasks', run(async (req) => {
|
|
75
|
+
const p = principal(req), raw = req.body?.input ?? {};
|
|
76
|
+
this.authorize(p, 'task.create', raw.groupId);
|
|
77
|
+
if (raw.managed || raw.parentTaskId || raw.dependsOn?.length || raw.coordinatorSessionId || raw.reviewerSessionIds?.length || raw.integration)
|
|
78
|
+
integrationError('INTEGRATION_OPERATION_DENIED', 'Integration creates ordinary standalone tasks only', 403);
|
|
79
|
+
const input = { idempotencyKey: raw.idempotencyKey, groupId: p.groupId, title: raw.title, spec: raw.spec,
|
|
80
|
+
constraints: raw.constraints, acceptance: raw.acceptance, assignee: raw.assigneeSessionId ? options.peers.taskMember(String(raw.assigneeSessionId)) : undefined };
|
|
81
|
+
if (input.assignee)
|
|
82
|
+
this.authorize(p, 'task.assign');
|
|
83
|
+
const task = await options.tasks.create(input, null, this.origin(p, req.body?.origin));
|
|
84
|
+
this.sync();
|
|
85
|
+
return { task };
|
|
86
|
+
}));
|
|
87
|
+
app.post('/tasks/:id', run(async (req) => {
|
|
88
|
+
const p = principal(req), raw = req.body?.input ?? {};
|
|
89
|
+
const permissions = { assign: 'task.assign', comment: 'task.comment', revise: 'task.revise', answer: 'task.answer' };
|
|
90
|
+
const permission = permissions[raw.kind];
|
|
91
|
+
if (!permission)
|
|
92
|
+
integrationError('INTEGRATION_OPERATION_DENIED', 'Operation requires a user or terminal member', 403);
|
|
93
|
+
this.taskPermission(p, req.params.id, permission);
|
|
94
|
+
const input = { kind: raw.kind, idempotencyKey: raw.idempotencyKey, content: raw.content,
|
|
95
|
+
expectedRevision: raw.expectedRevision, decisionId: raw.decisionId,
|
|
96
|
+
assignee: raw.assigneeSessionId ? options.peers.taskMember(String(raw.assigneeSessionId)) : undefined };
|
|
97
|
+
const task = await options.tasks.apply(req.params.id, input, null, this.origin(p, req.body?.origin));
|
|
98
|
+
this.sync();
|
|
99
|
+
const event = options.taskStore.integrationRequestEvent(p.id, input.idempotencyKey, task.id);
|
|
100
|
+
return { task, event_id: event?.id ?? null, attempt_id: event?.attemptId ?? null, delivery_id: event?.deliveryId ?? null };
|
|
101
|
+
}));
|
|
102
|
+
app.get('/message/:id', run(req => {
|
|
103
|
+
const p = principal(req), message = options.messages.getMessage(req.params.id);
|
|
104
|
+
if (!message)
|
|
105
|
+
integrationError('MESSAGE_NOT_FOUND', 'Message does not exist', 404);
|
|
106
|
+
this.authorize(p, 'task.read', message.groupId);
|
|
107
|
+
const receipt = options.messages.receipt(message.id);
|
|
108
|
+
const { snapshot: _, ...diagnostic } = receipt;
|
|
109
|
+
return { ok: true, ...diagnostic, ...(req.query.receipt_only === 'true' ? {} : { message: terminalMessage(message) }) };
|
|
110
|
+
}));
|
|
111
|
+
app.post('/sessions', run(async (req) => {
|
|
112
|
+
const p = principal(req);
|
|
113
|
+
this.authorize(p, 'session.create', req.body?.group_id);
|
|
114
|
+
const session = await this.sessions.create(p, { profile: req.body?.launch_profile, cwd: req.body?.cwd, idempotencyKey: req.body?.idempotency_key });
|
|
115
|
+
this.sync();
|
|
116
|
+
return { session: publicIntegrationSession(session) };
|
|
117
|
+
}));
|
|
118
|
+
app.get('/sessions/:id', run(async (req) => { const p = principal(req); this.authorize(p, 'session.read'); return { session: publicIntegrationSession(await this.sessions.get(p, req.params.id)) }; }));
|
|
119
|
+
app.post('/sessions/:id/restore', run(async (req) => { const p = principal(req); this.authorize(p, 'session.restore'); const session = await this.sessions.restore(p, req.params.id, req.body?.idempotency_key); this.sync(); return { session: publicIntegrationSession(session) }; }));
|
|
120
|
+
app.get('/events', run((req, res) => {
|
|
121
|
+
const p = principal(req);
|
|
122
|
+
this.authorize(p, 'events.read', req.query.group);
|
|
123
|
+
this.sync();
|
|
124
|
+
this.stream(p, String(req.query.consumer ?? 'default'), req, res);
|
|
125
|
+
}));
|
|
126
|
+
app.post('/events/ack', run(req => {
|
|
127
|
+
const p = principal(req);
|
|
128
|
+
this.authorize(p, 'events.read');
|
|
129
|
+
const cursor = String(req.body?.cursor ?? ''), consumer = String(req.body?.consumer ?? 'default');
|
|
130
|
+
const sequence = this.store.cursorSequence(p.groupId, cursor);
|
|
131
|
+
if (sequence > (this.sent.get(`${p.id}:${p.groupId}:${consumer}`) ?? -1))
|
|
132
|
+
integrationError('EVENT_ACK_NOT_SENT', 'Subscribe and durably receive the continuous prefix before acknowledging', 409);
|
|
133
|
+
return this.store.ack(p, consumer, cursor);
|
|
134
|
+
}));
|
|
135
|
+
app.use((_error, _req, res, _next) => { res.status(400).json(this.error(new CollaborationError('INVALID_REQUEST', 'Invalid integration request', 400))); });
|
|
136
|
+
this.server = createServer(app);
|
|
137
|
+
}
|
|
138
|
+
capabilities() {
|
|
139
|
+
return { integration_protocol: INTEGRATION_PROTOCOL, server_version: getTermdockVersion(), local_only: true,
|
|
140
|
+
integration_principal: true, group_scoped_permissions: true, durable_event_push: true,
|
|
141
|
+
event_ack_semantics: 'durable_received', correlated_task_responses: true, accepted_task_comments: true,
|
|
142
|
+
idempotent_session_create: true, exact_session_restore: true, external_validation: false,
|
|
143
|
+
permissions: ['task.read', 'task.create', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore'] };
|
|
144
|
+
}
|
|
145
|
+
error(error) {
|
|
146
|
+
return { type: 'error', ok: false, code: error instanceof CollaborationError ? error.code : 'INTEGRATION_INTERNAL_ERROR',
|
|
147
|
+
error: error instanceof CollaborationError ? error.message : 'Integration operation failed', retryable: !(error instanceof CollaborationError) || error.httpStatus >= 500 };
|
|
148
|
+
}
|
|
149
|
+
group(id) {
|
|
150
|
+
const group = typeof id === 'string' ? this.options.messages.getGroup(id) : null;
|
|
151
|
+
if (!group || group.deleted)
|
|
152
|
+
integrationError('TASK_GROUP_NOT_FOUND', 'A live group is required', 404);
|
|
153
|
+
if (group.federated)
|
|
154
|
+
integrationError('INTEGRATION_LOCAL_ONLY', 'First integration protocol uses a local collaboration group', 409);
|
|
155
|
+
return group;
|
|
156
|
+
}
|
|
157
|
+
authorize(p, operation, group) { this.store.authorize(p, operation, group === undefined ? p.groupId : String(group)); this.group(p.groupId); }
|
|
158
|
+
taskPermission(p, id, operation) {
|
|
159
|
+
const task = this.options.taskStore.get(id);
|
|
160
|
+
if (!task || task.groupId !== p.groupId)
|
|
161
|
+
integrationError('TASK_NOT_FOUND', 'Task does not exist within this integration grant', 404);
|
|
162
|
+
this.authorize(p, operation, task.groupId);
|
|
163
|
+
}
|
|
164
|
+
origin(p, input) {
|
|
165
|
+
const value = input && typeof input === 'object' ? input : {};
|
|
166
|
+
if (Buffer.byteLength(JSON.stringify(value)) > 16384)
|
|
167
|
+
integrationError('INVALID_SOURCE_METADATA', 'Source metadata exceeds 16 KiB');
|
|
168
|
+
if (value.externalActor !== undefined && (!value.externalActor || typeof value.externalActor !== 'object' || Array.isArray(value.externalActor)))
|
|
169
|
+
integrationError('INVALID_SOURCE_METADATA', 'externalActor must be an object');
|
|
170
|
+
if (value.metadata !== undefined && (!value.metadata || typeof value.metadata !== 'object' || Array.isArray(value.metadata)))
|
|
171
|
+
integrationError('INVALID_SOURCE_METADATA', 'metadata must be an object');
|
|
172
|
+
return { integrationId: p.id, source: typeof value.source === 'string' ? value.source.slice(0, 128) : 'integration',
|
|
173
|
+
...(value.externalActor ? { externalActor: value.externalActor } : {}),
|
|
174
|
+
...(typeof value.externalMessageId === 'string' ? { externalMessageId: value.externalMessageId.slice(0, 512) } : {}),
|
|
175
|
+
...(value.metadata ? { metadata: value.metadata } : {}) };
|
|
176
|
+
}
|
|
177
|
+
sync() {
|
|
178
|
+
if (this.syncing)
|
|
179
|
+
return;
|
|
180
|
+
this.syncing = true;
|
|
181
|
+
try {
|
|
182
|
+
const groups = new Set(this.store.policies().filter(p => !p.revoked).map(p => p.groupId)), updates = [];
|
|
183
|
+
if (!groups.size)
|
|
184
|
+
return;
|
|
185
|
+
for (const task of this.options.taskStore.snapshot())
|
|
186
|
+
if (groups.has(task.groupId)) {
|
|
187
|
+
for (const event of task.events)
|
|
188
|
+
updates.push({ sourceKey: `task:${task.id}:${event.id}`, sourceVersion: '1', eventId: event.id, event: {
|
|
189
|
+
group_id: task.groupId, kind: `task.${event.kind}`, created_at: event.createdAt, task_id: task.id, attempt_id: event.attemptId,
|
|
190
|
+
artifact_id: event.artifactId ?? null, reply_to_event_id: event.replyToEventId ?? null,
|
|
191
|
+
source: event.source ?? (event.actor ? 'member' : 'user'), actor: event.actor, external_actor: event.origin?.externalActor ?? null, payload: { event }
|
|
192
|
+
} });
|
|
193
|
+
for (const artifact of task.artifacts)
|
|
194
|
+
if (artifact.kind === 'result')
|
|
195
|
+
updates.push({ sourceKey: `artifact:${artifact.id}`, sourceVersion: '1', event: {
|
|
196
|
+
group_id: task.groupId, kind: 'task.result', created_at: artifact.createdAt, task_id: task.id, attempt_id: artifact.attemptId,
|
|
197
|
+
artifact_id: artifact.id, source: 'member', actor: artifact.actor, payload: { artifact }
|
|
198
|
+
} });
|
|
199
|
+
for (const delivery of task.deliveries)
|
|
200
|
+
updates.push({ sourceKey: `task-delivery:${delivery.id}`, sourceVersion: JSON.stringify(delivery), event: {
|
|
201
|
+
group_id: task.groupId, kind: 'task.delivery', created_at: delivery.deliveredAt ?? task.updatedAt,
|
|
202
|
+
task_id: task.id, attempt_id: delivery.attemptId, message_id: delivery.messageId, source: 'system', payload: { delivery }
|
|
203
|
+
} });
|
|
204
|
+
}
|
|
205
|
+
for (const message of this.options.messages.snapshotMessages())
|
|
206
|
+
if (groups.has(message.groupId)) {
|
|
207
|
+
const link = message.metadata?.termdockTask;
|
|
208
|
+
const context = { group_id: message.groupId, task_id: link?.taskId ?? null, attempt_id: link?.attemptId ?? null,
|
|
209
|
+
reply_to_event_id: link?.replyToEventId ?? null, message_id: message.id, source: message.fromSessionId ? 'member' : 'system' };
|
|
210
|
+
updates.push({ sourceKey: `message:${message.id}`, sourceVersion: '1', event: { ...context,
|
|
211
|
+
kind: message.kind === 'reply' ? 'message.reply' : 'message.queued', created_at: message.createdAt,
|
|
212
|
+
payload: { message_id: message.id, thread_id: message.threadId, reply_to: message.replyTo ?? null,
|
|
213
|
+
from_session_id: message.fromSessionId, to_session_id: message.toSessionId, content: message.content, metadata: message.metadata ?? null } } });
|
|
214
|
+
const { snapshot: _, ...receipt } = this.options.messages.receipt(message.id);
|
|
215
|
+
updates.push({ sourceKey: `message-status:${message.id}`, sourceVersion: JSON.stringify(receipt), event: {
|
|
216
|
+
...context, kind: 'message.delivery', created_at: message.deliveredAt ?? message.createdAt, payload: { receipt }
|
|
217
|
+
} });
|
|
218
|
+
}
|
|
219
|
+
for (const session of this.sessions.snapshot())
|
|
220
|
+
if (groups.has(session.group_id))
|
|
221
|
+
updates.push({ sourceKey: `session:${session.session_id}`, sourceVersion: JSON.stringify(publicIntegrationSession(session)), event: {
|
|
222
|
+
group_id: session.group_id, kind: `session.${session.state}`, created_at: session.updated_at, source: 'system', actor: { integration_id: session.principal_id }, payload: { session: publicIntegrationSession(session) }
|
|
223
|
+
} });
|
|
224
|
+
updates.sort((a, b) => a.event.created_at - b.event.created_at);
|
|
225
|
+
this.store.append(updates);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.syncing = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
failStreams(error) {
|
|
232
|
+
for (const stream of this.streams)
|
|
233
|
+
stream.end(`${JSON.stringify(this.error(error))}\n`);
|
|
234
|
+
}
|
|
235
|
+
scheduleRetry() {
|
|
236
|
+
if (this.retry)
|
|
237
|
+
return;
|
|
238
|
+
this.retry = setTimeout(() => { this.retry = undefined; try {
|
|
239
|
+
this.sync();
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
this.scheduleRetry();
|
|
243
|
+
} }, 1000);
|
|
244
|
+
this.retry.unref();
|
|
245
|
+
}
|
|
246
|
+
stream(p, consumer, req, res) {
|
|
247
|
+
let cursor, pumping = false, closed = false;
|
|
248
|
+
const key = `${p.id}:${p.groupId}:${consumer}`;
|
|
249
|
+
// Resolve the initial cursor before sending headers so retention gaps
|
|
250
|
+
// and bad consumer names are ordinary machine-readable HTTP errors.
|
|
251
|
+
const initial = this.store.page(p, consumer);
|
|
252
|
+
cursor = initial.cursor;
|
|
253
|
+
res.setHeader('Content-Type', 'application/x-ndjson');
|
|
254
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
255
|
+
res.flushHeaders();
|
|
256
|
+
this.streams.add(res);
|
|
257
|
+
const stop = this.store.subscribe(() => pump());
|
|
258
|
+
const cleanup = () => { closed = true; stop(); clearInterval(heartbeat); this.streams.delete(res); };
|
|
259
|
+
const pump = () => {
|
|
260
|
+
if (pumping || closed)
|
|
261
|
+
return;
|
|
262
|
+
pumping = true;
|
|
263
|
+
try {
|
|
264
|
+
this.authorize(p, 'events.read');
|
|
265
|
+
for (;;) {
|
|
266
|
+
const page = this.store.page(p, consumer, cursor);
|
|
267
|
+
if (!page.events.length)
|
|
268
|
+
break;
|
|
269
|
+
for (const event of page.events) {
|
|
270
|
+
if (res.writableLength > 1024 * 1024)
|
|
271
|
+
integrationError('EVENT_CONSUMER_SLOW', 'Reconnect from the last acknowledged cursor', 503);
|
|
272
|
+
res.write(`${JSON.stringify({ type: 'event', ...event })}\n`);
|
|
273
|
+
cursor = event.cursor;
|
|
274
|
+
this.sent.set(key, Math.max(this.sent.get(key) ?? 0, event.sequence));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
res.end(`${JSON.stringify(this.error(error))}\n`);
|
|
280
|
+
cleanup();
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
pumping = false;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const heartbeat = setInterval(() => { pump(); if (!closed)
|
|
287
|
+
res.write(`${JSON.stringify({ type: 'heartbeat', ack_semantics: 'durable_received' })}\n`); }, 15000);
|
|
288
|
+
heartbeat.unref();
|
|
289
|
+
res.once('close', cleanup);
|
|
290
|
+
req.once('aborted', cleanup);
|
|
291
|
+
pump();
|
|
292
|
+
}
|
|
293
|
+
async listen() {
|
|
294
|
+
fs.mkdirSync(path.dirname(this.options.socketPath), { recursive: true, mode: 0o700 });
|
|
295
|
+
// A stale socket may be removed only after proving no listener exists.
|
|
296
|
+
if (fs.existsSync(this.options.socketPath)) {
|
|
297
|
+
const { createConnection } = await import('node:net');
|
|
298
|
+
const active = await new Promise((resolve, reject) => {
|
|
299
|
+
const socket = createConnection(this.options.socketPath);
|
|
300
|
+
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
|
301
|
+
socket.once('error', (error) => { socket.destroy(); if (['ECONNREFUSED', 'ENOENT'].includes(error.code ?? ''))
|
|
302
|
+
resolve(false);
|
|
303
|
+
else
|
|
304
|
+
reject(error); });
|
|
305
|
+
});
|
|
306
|
+
if (active)
|
|
307
|
+
integrationError('INTEGRATION_SOCKET_IN_USE', 'Another integration service owns this socket', 409);
|
|
308
|
+
if (!fs.lstatSync(this.options.socketPath).isSocket())
|
|
309
|
+
integrationError('INTEGRATION_SOCKET_INVALID', 'Refusing to replace a non-socket file');
|
|
310
|
+
fs.unlinkSync(this.options.socketPath);
|
|
311
|
+
}
|
|
312
|
+
await new Promise((resolve, reject) => { this.server.once('error', reject); this.server.listen(this.options.socketPath, () => { this.server.removeListener('error', reject); fs.chmodSync(this.options.socketPath, 0o600); resolve(); }); });
|
|
313
|
+
this.sync();
|
|
314
|
+
await this.sessions.refreshAll();
|
|
315
|
+
}
|
|
316
|
+
async close() {
|
|
317
|
+
for (const stop of this.stops)
|
|
318
|
+
stop();
|
|
319
|
+
if (this.retry)
|
|
320
|
+
clearTimeout(this.retry);
|
|
321
|
+
this.sessions.close();
|
|
322
|
+
for (const stream of this.streams)
|
|
323
|
+
stream.destroy();
|
|
324
|
+
await new Promise(resolve => this.server.close(() => resolve()));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
4
|
+
import { atomicJson, integrationError } from './integrationStore.js';
|
|
5
|
+
/** Quote each configured argument as literal shell data, including paths. */
|
|
6
|
+
export function integrationLaunchCommand(record, restore) {
|
|
7
|
+
const launchArgs = record.profile.argv.map(arg => arg === '{cwd}' ? record.cwd : arg);
|
|
8
|
+
const args = restore ? record.profile.resumeArgv.flatMap(arg => arg === '{launchArgs}' ? launchArgs
|
|
9
|
+
: [arg === '{sessionId}' ? record.agent_native_session_id : arg === '{cwd}' ? record.cwd : arg]) : launchArgs;
|
|
10
|
+
const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
11
|
+
return `cd -- ${quote(record.cwd)} && ${[record.profile.executable, ...args].map(quote).join(' ')}`;
|
|
12
|
+
}
|
|
13
|
+
export function publicIntegrationSession(record) {
|
|
14
|
+
const { profile: _, deadline: _deadline, launch_submitted: _submitted, ...publicRecord } = record;
|
|
15
|
+
return publicRecord;
|
|
16
|
+
}
|
|
17
|
+
export function permittedCwd(profile, cwd) {
|
|
18
|
+
if (typeof cwd !== 'string' || !path.isAbsolute(cwd) || /[\x00-\x1f\x7f]/.test(cwd))
|
|
19
|
+
integrationError('SESSION_CWD_DENIED', 'cwd must be absolute', 403);
|
|
20
|
+
let real;
|
|
21
|
+
try {
|
|
22
|
+
real = fs.realpathSync(cwd);
|
|
23
|
+
if (/[\x00-\x1f\x7f]/.test(real) || !fs.statSync(real).isDirectory())
|
|
24
|
+
throw new Error();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return integrationError('SESSION_CWD_MISSING', 'Task working directory must exist');
|
|
28
|
+
}
|
|
29
|
+
if (!profile.cwdRoots.some(root => { try {
|
|
30
|
+
const relative = path.relative(fs.realpathSync(root), real);
|
|
31
|
+
return !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
} }))
|
|
36
|
+
integrationError('SESSION_CWD_DENIED', 'Working directory is outside the configured roots', 403);
|
|
37
|
+
return real;
|
|
38
|
+
}
|
|
39
|
+
/** Creation intent and exact launch configuration are durable before any input
|
|
40
|
+
* enters a PTY. An ambiguous crash never starts another Agent automatically. */
|
|
41
|
+
export class IntegrationSessions {
|
|
42
|
+
file;
|
|
43
|
+
adapter;
|
|
44
|
+
bindingTimeout;
|
|
45
|
+
doc = { version: 1, sessions: [], requests: {} };
|
|
46
|
+
pending = new Map();
|
|
47
|
+
listeners = new Set();
|
|
48
|
+
restoring = new Set();
|
|
49
|
+
timers = new Set();
|
|
50
|
+
stopObservation;
|
|
51
|
+
observing = false;
|
|
52
|
+
observeAgain = false;
|
|
53
|
+
constructor(file, adapter, bindingTimeout = 120000) {
|
|
54
|
+
this.file = file;
|
|
55
|
+
this.adapter = adapter;
|
|
56
|
+
this.bindingTimeout = bindingTimeout;
|
|
57
|
+
try {
|
|
58
|
+
const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
59
|
+
if (doc.version !== 1 || !Array.isArray(doc.sessions) || !doc.requests)
|
|
60
|
+
throw new Error('Invalid integration session store');
|
|
61
|
+
this.doc = doc;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== 'ENOENT')
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
for (const record of this.doc.sessions)
|
|
68
|
+
if (['starting', 'restoring', 'binding_pending'].includes(record.state))
|
|
69
|
+
this.scheduleRefresh(record);
|
|
70
|
+
this.stopObservation = adapter.subscribe(() => { void this.refreshAll().catch(() => { }); });
|
|
71
|
+
}
|
|
72
|
+
scheduleRefresh(record) {
|
|
73
|
+
const timer = setTimeout(() => { this.timers.delete(timer); void this.refresh(record.session_id).catch(() => { }); }, Math.max(1, record.deadline - Date.now() + 5));
|
|
74
|
+
timer.unref();
|
|
75
|
+
this.timers.add(timer);
|
|
76
|
+
}
|
|
77
|
+
subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
|
|
78
|
+
snapshot() { return structuredClone(this.doc.sessions); }
|
|
79
|
+
save() { atomicJson(this.file, this.doc); for (const listener of this.listeners) {
|
|
80
|
+
try {
|
|
81
|
+
listener();
|
|
82
|
+
}
|
|
83
|
+
catch { /* Durable session state is reconciled on restart. */ }
|
|
84
|
+
} }
|
|
85
|
+
saveRecord(record) {
|
|
86
|
+
const before = this.doc.sessions;
|
|
87
|
+
this.doc.sessions = [...before.filter(s => s.session_id !== record.session_id), structuredClone(record)];
|
|
88
|
+
try {
|
|
89
|
+
this.save();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.doc.sessions = before;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
lookup(principal, id) {
|
|
97
|
+
const record = this.doc.sessions.find(s => s.session_id === id && s.group_id === principal.groupId && s.principal_id === principal.id);
|
|
98
|
+
if (!record)
|
|
99
|
+
integrationError('INTEGRATION_SESSION_NOT_FOUND', 'Session is not owned by this integration', 404);
|
|
100
|
+
return structuredClone(record);
|
|
101
|
+
}
|
|
102
|
+
request(principal, key, input) {
|
|
103
|
+
if (!key || typeof key !== 'string' || key.length > 256)
|
|
104
|
+
integrationError('INVALID_IDEMPOTENCY_KEY', 'An idempotency key is required');
|
|
105
|
+
const scoped = `${principal.id}:${key}`, hash = createHash('sha256').update(JSON.stringify(input)).digest('hex');
|
|
106
|
+
const previous = this.doc.requests[scoped];
|
|
107
|
+
if (previous && previous.hash !== hash)
|
|
108
|
+
integrationError('IDEMPOTENCY_CONFLICT', 'Key was already used for different session parameters', 409);
|
|
109
|
+
return { scoped, hash, previous };
|
|
110
|
+
}
|
|
111
|
+
async create(principal, input) {
|
|
112
|
+
const request = this.request(principal, input.idempotencyKey, ['create', input.profile, input.cwd]);
|
|
113
|
+
if (request.previous)
|
|
114
|
+
return this.pending.get(request.previous.sessionId) ?? this.get(principal, request.previous.sessionId);
|
|
115
|
+
const profile = principal.launchProfiles.find(p => p.id === input.profile);
|
|
116
|
+
if (!profile)
|
|
117
|
+
integrationError('LAUNCH_PROFILE_DENIED', 'Profile is not allowed by this integration', 403);
|
|
118
|
+
const cwd = permittedCwd(profile, input.cwd);
|
|
119
|
+
try {
|
|
120
|
+
fs.accessSync(profile.executable, fs.constants.X_OK);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return integrationError('LAUNCHER_UNAVAILABLE', 'Configured executable is not available');
|
|
124
|
+
}
|
|
125
|
+
if (this.doc.sessions.length >= 2000)
|
|
126
|
+
integrationError('INTEGRATION_SESSION_LIMIT', 'Integration session storage is full', 409);
|
|
127
|
+
const now = Date.now(), record = { operation_id: randomUUID(), session_id: randomBytes(6).toString('hex'),
|
|
128
|
+
group_id: principal.groupId, principal_id: principal.id, agent_slug: profile.agentSlug, agent_native_session_id: null,
|
|
129
|
+
state: 'starting', error_code: null, cwd, launch_profile: profile.id, profile: structuredClone(profile),
|
|
130
|
+
created_at: now, updated_at: now, deadline: now + this.bindingTimeout, launch_submitted: false };
|
|
131
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: record.session_id };
|
|
132
|
+
try {
|
|
133
|
+
this.saveRecord(record);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
delete this.doc.requests[request.scoped];
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
return this.launch(record, false);
|
|
140
|
+
}
|
|
141
|
+
launch(record, restoring) {
|
|
142
|
+
const promise = (async () => {
|
|
143
|
+
try {
|
|
144
|
+
// Store intent before launching. A failed persistence cannot execute.
|
|
145
|
+
record.launch_submitted = true;
|
|
146
|
+
this.saveRecord(record);
|
|
147
|
+
const prepared = (binding) => { record.terminal_binding = binding; this.saveRecord(record); };
|
|
148
|
+
if (restoring)
|
|
149
|
+
await this.adapter.restore(record, prepared);
|
|
150
|
+
else
|
|
151
|
+
await this.adapter.create(record, prepared);
|
|
152
|
+
await this.refresh(record.session_id);
|
|
153
|
+
this.scheduleRefresh(record);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const current = this.doc.sessions.find(s => s.session_id === record.session_id);
|
|
157
|
+
const code = error.code;
|
|
158
|
+
this.saveRecord({ ...current, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_LAUNCH_FAILED', updated_at: Date.now() });
|
|
159
|
+
}
|
|
160
|
+
return structuredClone(this.doc.sessions.find(s => s.session_id === record.session_id));
|
|
161
|
+
})().finally(() => this.pending.delete(record.session_id));
|
|
162
|
+
this.pending.set(record.session_id, promise);
|
|
163
|
+
return promise;
|
|
164
|
+
}
|
|
165
|
+
async get(principal, id) { this.lookup(principal, id); await this.refresh(id); return this.lookup(principal, id); }
|
|
166
|
+
async restore(principal, id, idempotencyKey) {
|
|
167
|
+
const request = this.request(principal, idempotencyKey, ['restore', id]);
|
|
168
|
+
if (request.previous)
|
|
169
|
+
return this.pending.get(id) ?? this.get(principal, id);
|
|
170
|
+
if (this.pending.has(id) || this.restoring.has(id))
|
|
171
|
+
integrationError('SESSION_OPERATION_IN_PROGRESS', 'Another session operation is in progress', 409);
|
|
172
|
+
this.restoring.add(id);
|
|
173
|
+
try {
|
|
174
|
+
const record = await this.get(principal, id);
|
|
175
|
+
if (!record.agent_native_session_id)
|
|
176
|
+
integrationError('NATIVE_SESSION_ID_MISSING', 'An exact native session binding is required', 409);
|
|
177
|
+
// Recheck the current grant and actual directories; restore uses the
|
|
178
|
+
// original profile snapshot, never changed model or wrapper defaults.
|
|
179
|
+
if (!principal.launchProfiles.some(p => p.id === record.launch_profile))
|
|
180
|
+
integrationError('LAUNCH_PROFILE_DENIED', 'Original launch profile is no longer authorized', 403);
|
|
181
|
+
permittedCwd(record.profile, record.cwd);
|
|
182
|
+
const observed = await this.adapter.inspect(record);
|
|
183
|
+
if (observed.running) {
|
|
184
|
+
if (observed.agentSlug !== record.agent_slug || observed.nativeId !== record.agent_native_session_id)
|
|
185
|
+
integrationError('SESSION_IDENTITY_MISMATCH', 'Existing process does not prove the exact native binding', 409);
|
|
186
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id };
|
|
187
|
+
try {
|
|
188
|
+
this.save();
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
delete this.doc.requests[request.scoped];
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
return record;
|
|
195
|
+
}
|
|
196
|
+
if (['starting', 'restoring', 'binding_pending'].includes(record.state))
|
|
197
|
+
integrationError('SESSION_OPERATION_IN_PROGRESS', 'Prior launch outcome is not yet resolved; inspect the original session', 409);
|
|
198
|
+
if (observed.exists && !observed.shell)
|
|
199
|
+
integrationError('SESSION_TARGET_NOT_SHELL', 'Original pane is not a verified shell', 409);
|
|
200
|
+
record.state = 'restoring';
|
|
201
|
+
record.error_code = null;
|
|
202
|
+
record.operation_id = randomUUID();
|
|
203
|
+
record.deadline = Date.now() + this.bindingTimeout;
|
|
204
|
+
record.updated_at = Date.now();
|
|
205
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id };
|
|
206
|
+
try {
|
|
207
|
+
this.saveRecord(record);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
delete this.doc.requests[request.scoped];
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
return await this.launch(record, true);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.restoring.delete(id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async refresh(id) {
|
|
220
|
+
const record = structuredClone(this.doc.sessions.find(s => s.session_id === id));
|
|
221
|
+
if (!record)
|
|
222
|
+
return;
|
|
223
|
+
let observed;
|
|
224
|
+
try {
|
|
225
|
+
observed = await this.adapter.inspect(record);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const current = this.doc.sessions.find(s => s.session_id === id);
|
|
229
|
+
if (JSON.stringify(current) !== JSON.stringify(record))
|
|
230
|
+
return;
|
|
231
|
+
const code = error.code;
|
|
232
|
+
if (record.state !== 'failed' || record.error_code !== code)
|
|
233
|
+
this.saveRecord({ ...record, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_OBSERVATION_FAILED', updated_at: Date.now() });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const next = { ...record };
|
|
237
|
+
if (observed.nativeId && (typeof observed.nativeId !== 'string' || observed.nativeId.length > 256 || /[\x00-\x1f\x7f]/.test(observed.nativeId))) {
|
|
238
|
+
next.state = 'failed';
|
|
239
|
+
next.error_code = 'NATIVE_SESSION_ID_INVALID';
|
|
240
|
+
}
|
|
241
|
+
else if (observed.running && observed.agentSlug !== record.agent_slug) {
|
|
242
|
+
next.state = 'failed';
|
|
243
|
+
next.error_code = 'SESSION_IDENTITY_MISMATCH';
|
|
244
|
+
}
|
|
245
|
+
else if (observed.running && observed.nativeId) {
|
|
246
|
+
if (record.agent_native_session_id && record.agent_native_session_id !== observed.nativeId) {
|
|
247
|
+
next.state = 'failed';
|
|
248
|
+
next.error_code = 'NATIVE_SESSION_ID_MISMATCH';
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
next.agent_native_session_id = observed.nativeId;
|
|
252
|
+
if (next.terminal_binding)
|
|
253
|
+
next.terminal_binding = { ...next.terminal_binding, nativeSessionId: observed.nativeId };
|
|
254
|
+
next.state = 'ready';
|
|
255
|
+
next.error_code = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (observed.running) {
|
|
259
|
+
next.state = Date.now() > record.deadline ? 'failed' : 'binding_pending';
|
|
260
|
+
next.error_code = next.state === 'failed' ? 'SESSION_BINDING_TIMEOUT' : null;
|
|
261
|
+
}
|
|
262
|
+
else if (record.state === 'ready') {
|
|
263
|
+
next.state = 'failed';
|
|
264
|
+
next.error_code = 'AGENT_NOT_RUNNING';
|
|
265
|
+
}
|
|
266
|
+
else if (Date.now() > record.deadline && record.state !== 'failed') {
|
|
267
|
+
next.state = 'failed';
|
|
268
|
+
next.error_code = 'SESSION_BINDING_TIMEOUT';
|
|
269
|
+
}
|
|
270
|
+
if (JSON.stringify(this.doc.sessions.find(s => s.session_id === id)) !== JSON.stringify(record))
|
|
271
|
+
return;
|
|
272
|
+
if (next.state !== record.state || next.error_code !== record.error_code || next.agent_native_session_id !== record.agent_native_session_id) {
|
|
273
|
+
next.updated_at = Date.now();
|
|
274
|
+
this.saveRecord(next);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async refreshAll() {
|
|
278
|
+
if (this.observing) {
|
|
279
|
+
this.observeAgain = true;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.observing = true;
|
|
283
|
+
try {
|
|
284
|
+
do {
|
|
285
|
+
this.observeAgain = false;
|
|
286
|
+
for (const record of this.snapshot())
|
|
287
|
+
await this.refresh(record.session_id);
|
|
288
|
+
} while (this.observeAgain);
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
this.observing = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
close() { this.stopObservation(); for (const timer of this.timers)
|
|
295
|
+
clearTimeout(timer); }
|
|
296
|
+
}
|