chainlesschain 0.164.0 → 0.165.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +248 -2
  2. package/package.json +7 -4
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
  40. package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
  56. package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
  75. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
  76. package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
  104. package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
  109. package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
  134. package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
  135. package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/cowork.js +559 -62
  154. package/src/commands/plugin.js +819 -64
  155. package/src/commands/session-location.js +318 -7
  156. package/src/constants.js +2 -2
  157. package/src/data/changelog.json +28 -2
  158. package/src/gateways/ws/action-protocol.js +293 -19
  159. package/src/gateways/ws/message-dispatcher.js +30 -0
  160. package/src/gateways/ws/remote-session-protocol.js +542 -35
  161. package/src/gateways/ws/ws-approval-gate.js +5 -0
  162. package/src/gateways/ws/ws-server.js +462 -38
  163. package/src/harness/jsonl-session-store.js +638 -2
  164. package/src/harness/prompt-compressor.js +4 -0
  165. package/src/harness/remote-session-audit.js +1 -0
  166. package/src/harness/remote-session-crypto.js +750 -50
  167. package/src/harness/remote-session-registry.js +795 -27
  168. package/src/lib/auto-mode-config.js +98 -3
  169. package/src/lib/background-agent-keeper-client.js +29 -0
  170. package/src/lib/background-agent-keeper-protocol.js +158 -10
  171. package/src/lib/background-agent-supervisor.js +870 -48
  172. package/src/lib/background-session-transport.js +23 -6
  173. package/src/lib/background-turn-bootstrap-protocol.js +64 -0
  174. package/src/lib/cowork-task-runner.js +70 -0
  175. package/src/lib/cowork-workflow.js +887 -62
  176. package/src/lib/delivery-production-adapter.js +38 -2
  177. package/src/lib/doctor-checkup.js +34 -10
  178. package/src/lib/dynamic-workflow-draft.js +535 -0
  179. package/src/lib/dynamic-workflow-facade.js +1105 -17
  180. package/src/lib/dynamic-workflow-runtime.js +2974 -0
  181. package/src/lib/execution-location-contract.js +111 -7
  182. package/src/lib/execution-location-runtime.js +3 -1
  183. package/src/lib/execution-location-target.js +910 -0
  184. package/src/lib/llm-providers.js +1 -0
  185. package/src/lib/mcp-call-ledger-store.js +35 -0
  186. package/src/lib/mcp-call-ledger.js +30 -0
  187. package/src/lib/mcp-stdio-package-materialization.js +1 -1
  188. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
  189. package/src/lib/plugin-runtime/install.js +4956 -657
  190. package/src/lib/plugin-runtime/manifest.js +3 -1
  191. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
  192. package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
  193. package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
  194. package/src/lib/plugin-runtime/marketplace-network.js +317 -0
  195. package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
  196. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
  197. package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
  198. package/src/lib/plugin-runtime/policy.js +21 -6
  199. package/src/lib/plugin-runtime/publisher-trust.js +332 -0
  200. package/src/lib/plugin-runtime/remote-source.js +323 -27
  201. package/src/lib/plugin-runtime/scopes.js +317 -41
  202. package/src/lib/plugin-runtime/transaction-journal.js +806 -0
  203. package/src/lib/public-ide-capability-manifest.js +1 -1
  204. package/src/lib/remote-approval-bridge.js +745 -108
  205. package/src/lib/remote-control.js +2 -0
  206. package/src/lib/remote-membership-coordinator.js +612 -46
  207. package/src/lib/remote-membership-host-store.js +59 -10
  208. package/src/lib/runtime-provenance-ledger.js +171 -46
  209. package/src/lib/shell-approval.js +169 -6
  210. package/src/lib/sub-agent-context.js +268 -2
  211. package/src/lib/task-model-selector.js +1 -1
  212. package/src/lib/with-file-lock.js +167 -42
  213. package/src/repl/agent-repl.js +70 -29
  214. package/src/repl/remote-approval.js +29 -7
  215. package/src/runtime/agent-core.js +1695 -194
  216. package/src/runtime/headless-runner.js +22 -5
  217. package/src/runtime/headless-stream.js +19 -3
  218. package/src/workers/background-agent-keeper.js +279 -58
  219. package/src/workers/background-agent-worker.js +7 -0
  220. package/src/workers/background-turn-bootstrap.js +29 -2
  221. package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
  222. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
  223. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
  224. package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
  225. package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
  226. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
  227. package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
  228. package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
@@ -7,6 +7,7 @@ export const REMOTE_SESSION_SCOPES = Object.freeze([
7
7
  "approve",
8
8
  "interrupt",
9
9
  ]);
10
+ export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
10
11
 
11
12
  const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
12
13
  const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -36,6 +37,25 @@ function normalizeScopes(scopes) {
36
37
  return unique;
37
38
  }
38
39
 
40
+ function normalizeCapabilities(capabilities) {
41
+ if (capabilities == null) return [];
42
+ if (!Array.isArray(capabilities) || capabilities.length > 32) {
43
+ throw new TypeError("Remote session capabilities must be a bounded array");
44
+ }
45
+ const normalized = [...new Set(capabilities)];
46
+ for (const capability of normalized) {
47
+ if (
48
+ typeof capability !== "string" ||
49
+ capability.length === 0 ||
50
+ capability.length > 128 ||
51
+ capability.includes("\0")
52
+ ) {
53
+ throw new TypeError("Remote session capability is invalid");
54
+ }
55
+ }
56
+ return normalized.sort();
57
+ }
58
+
39
59
  /**
40
60
  * Org-level constraints an administrator can impose on Remote Sessions. Lives
41
61
  * next to the registry (its single enforcement point) so both the direct WS
@@ -49,6 +69,7 @@ export class RemoteSessionPolicy {
49
69
  maxSessionTtlMs = null,
50
70
  maxTokenTtlMs = null,
51
71
  allowRelayPairing = true,
72
+ policyVersion = null,
52
73
  } = {}) {
53
74
  this.allowedScopes = allowedScopes ? [...new Set(allowedScopes)] : null;
54
75
  if (this.allowedScopes) {
@@ -68,6 +89,24 @@ export class RemoteSessionPolicy {
68
89
  this.maxTokenTtlMs =
69
90
  maxTokenTtlMs == null ? null : Math.max(1, Math.floor(maxTokenTtlMs));
70
91
  this.allowRelayPairing = allowRelayPairing !== false;
92
+ const authorityMaterial = JSON.stringify({
93
+ allowedScopes: this.allowedScopes ? [...this.allowedScopes].sort() : null,
94
+ allowRelayPairing: this.allowRelayPairing,
95
+ maxDevices: this.maxDevices,
96
+ });
97
+ this.policyVersion =
98
+ policyVersion == null
99
+ ? `remote-session-policy:sha256:${createHash("sha256")
100
+ .update(authorityMaterial, "utf8")
101
+ .digest("hex")}`
102
+ : String(policyVersion);
103
+ if (
104
+ this.policyVersion.length === 0 ||
105
+ this.policyVersion.length > 512 ||
106
+ this.policyVersion.includes("\0")
107
+ ) {
108
+ throw new TypeError("policyVersion must be a non-empty bounded string");
109
+ }
71
110
  }
72
111
 
73
112
  /**
@@ -119,9 +158,22 @@ export class RemoteSessionPolicy {
119
158
  maxSessionTtlMs: this.maxSessionTtlMs,
120
159
  maxTokenTtlMs: this.maxTokenTtlMs,
121
160
  allowRelayPairing: this.allowRelayPairing,
161
+ policyVersion: this.policyVersion,
122
162
  };
123
163
  }
124
164
 
165
+ authorityDescriptor() {
166
+ return Object.freeze({
167
+ allowedScopes:
168
+ this.allowedScopes === null
169
+ ? null
170
+ : Object.freeze([...this.allowedScopes].sort()),
171
+ maxDevices: this.maxDevices,
172
+ allowRelayPairing: this.allowRelayPairing,
173
+ policyVersion: this.policyVersion,
174
+ });
175
+ }
176
+
125
177
  static fromEnv(env = {}) {
126
178
  const options = {};
127
179
  const scopes = env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOWED_SCOPES;
@@ -163,7 +215,13 @@ function publicSession(session) {
163
215
  sessionId: session.sessionId,
164
216
  agentSessionId: session.agentSessionId,
165
217
  name: session.name,
166
- hostClientId: session.hostClientId,
218
+ hostClientId: session.hostPrincipalId || session.hostClientId,
219
+ ...(session.hostPrincipalId
220
+ ? {
221
+ hostPrincipalId: session.hostPrincipalId,
222
+ hostConnected: Boolean(session.hostClientId),
223
+ }
224
+ : {}),
167
225
  createdAt: session.createdAt,
168
226
  expiresAt: session.expiresAt,
169
227
  memberCount: session.members.size,
@@ -176,6 +234,28 @@ function publicSession(session) {
176
234
  };
177
235
  }
178
236
 
237
+ function transportBindingKey(sessionId, transportClientId) {
238
+ return JSON.stringify([String(sessionId), String(transportClientId)]);
239
+ }
240
+
241
+ function publicMember(member, session) {
242
+ return {
243
+ clientId: member.principalId || member.clientId,
244
+ ...(member.principalId ? { principalId: member.principalId } : {}),
245
+ scopes: [...member.scopes],
246
+ joinedAt: member.joinedAt,
247
+ pushToken: member.pushToken || null,
248
+ pushProvider: member.pushToken ? member.pushProvider || null : null,
249
+ ...(member.membershipEpoch
250
+ ? { membershipEpoch: member.membershipEpoch }
251
+ : {}),
252
+ ...(member.membershipQuarantined ? { membershipQuarantined: true } : {}),
253
+ ...(session?.hostPrincipalId
254
+ ? { connected: Boolean(member.transportClientId) }
255
+ : {}),
256
+ };
257
+ }
258
+
179
259
  /** In-memory authorization state for one local CLI process. */
180
260
  export class RemoteSessionRegistry {
181
261
  constructor(options = {}) {
@@ -187,8 +267,201 @@ export class RemoteSessionRegistry {
187
267
  // from touching the machine security store. Client-hosted approval bridges
188
268
  // explicitly require this authority and trigger its lazy construction.
189
269
  this._membershipAuthority = options.membershipAuthority ?? null;
270
+ this._membershipCoordinator = options.membershipCoordinator ?? null;
190
271
  this.sessions = new Map();
191
272
  this.tokens = new Map();
273
+ this._transportBindings = new Map();
274
+ this._pendingAuthentication = new Map();
275
+ this._coordinatorHydrated = false;
276
+ this._coordinatorHydrationError = null;
277
+ if (
278
+ this._membershipCoordinator &&
279
+ typeof this._membershipCoordinator !== "function"
280
+ ) {
281
+ try {
282
+ this._hydrateCoordinatorSessions(this._membershipCoordinator);
283
+ } catch (error) {
284
+ // Construction remains side-effect safe/retryable. Every durable use
285
+ // retries the authoritative read and still fails closed if it remains
286
+ // unavailable.
287
+ this._coordinatorHydrated = false;
288
+ this._coordinatorHydrationError = error;
289
+ }
290
+ }
291
+ }
292
+
293
+ _resolveMembershipCoordinator() {
294
+ if (typeof this._membershipCoordinator === "function") {
295
+ this._membershipCoordinator = this._membershipCoordinator();
296
+ }
297
+ if (
298
+ !this._membershipCoordinator ||
299
+ typeof this._membershipCoordinator.listSessionSnapshots !== "function" ||
300
+ typeof this._membershipCoordinator.getSessionSnapshot !== "function" ||
301
+ typeof this._membershipCoordinator.trustDescriptor !== "function"
302
+ ) {
303
+ throw new Error("Durable Remote membership coordinator is unavailable");
304
+ }
305
+ return this._membershipCoordinator;
306
+ }
307
+
308
+ _requireMembershipCoordinator() {
309
+ const coordinator = this._resolveMembershipCoordinator();
310
+ if (!this._coordinatorHydrated) {
311
+ this._hydrateCoordinatorSessions(coordinator);
312
+ }
313
+ return coordinator;
314
+ }
315
+
316
+ _hydrateCoordinatorSessions(coordinator) {
317
+ try {
318
+ const snapshots = coordinator.listSessionSnapshots({ activeOnly: false });
319
+ const live = new Set();
320
+ for (const snapshot of snapshots) {
321
+ if (snapshot.expiresAt <= this.now() || snapshot.status !== "active") {
322
+ this._clearCoordinatorSession(snapshot.sessionId);
323
+ continue;
324
+ }
325
+ live.add(snapshot.sessionId);
326
+ this._syncCoordinatorSnapshot(snapshot, coordinator);
327
+ }
328
+ for (const [sessionId, session] of this.sessions) {
329
+ if (session.hostPrincipalId && !live.has(sessionId)) {
330
+ this._clearCoordinatorSession(sessionId);
331
+ }
332
+ }
333
+ this._coordinatorHydrated = true;
334
+ this._coordinatorHydrationError = null;
335
+ } catch (error) {
336
+ this._coordinatorHydrated = false;
337
+ this._coordinatorHydrationError = error;
338
+ throw error;
339
+ }
340
+ }
341
+
342
+ _clearCoordinatorSession(sessionId) {
343
+ for (const [key, binding] of this._transportBindings) {
344
+ if (binding.sessionId === sessionId) this._transportBindings.delete(key);
345
+ }
346
+ for (const [challengeId, pending] of this._pendingAuthentication) {
347
+ if (pending.sessionId === sessionId) {
348
+ this._pendingAuthentication.delete(challengeId);
349
+ }
350
+ }
351
+ this.sessions.delete(sessionId);
352
+ this.tokens.delete(sessionId);
353
+ }
354
+
355
+ _refreshCoordinatorSession(sessionId) {
356
+ const coordinator = this._requireMembershipCoordinator();
357
+ const current = coordinator.getSessionSnapshot(sessionId);
358
+ if (
359
+ !current ||
360
+ current.session.status !== "active" ||
361
+ current.session.expiresAt <= this.now()
362
+ ) {
363
+ this._clearCoordinatorSession(sessionId);
364
+ return current?.session || null;
365
+ }
366
+ return this._syncCoordinatorSnapshot(current.session, coordinator);
367
+ }
368
+
369
+ _syncCoordinatorSnapshot(snapshot, coordinator = null) {
370
+ const authority = coordinator || this._resolveMembershipCoordinator();
371
+ const previous = this.sessions.get(snapshot.sessionId) || null;
372
+ const priorMembers = previous?.members || new Map();
373
+ for (const [key, binding] of this._transportBindings) {
374
+ if (binding.sessionId === snapshot.sessionId) {
375
+ this._transportBindings.delete(key);
376
+ }
377
+ }
378
+ const members = new Map();
379
+ for (const durableMember of snapshot.members) {
380
+ const prior = priorMembers.get(durableMember.principalId) || null;
381
+ const sameActiveMembership =
382
+ prior?.status === "active" &&
383
+ durableMember.status === "active" &&
384
+ prior.membershipEpoch === durableMember.membershipEpoch;
385
+ const member = {
386
+ principalId: durableMember.principalId,
387
+ clientId: durableMember.principalId,
388
+ scopes: [...durableMember.scopes],
389
+ joinedAt: prior?.joinedAt || snapshot.createdAt,
390
+ pushToken: sameActiveMembership ? prior.pushToken || null : null,
391
+ pushProvider:
392
+ sameActiveMembership && prior.pushToken
393
+ ? prior.pushProvider || null
394
+ : null,
395
+ membershipEpoch: durableMember.membershipEpoch,
396
+ status: durableMember.status,
397
+ transportClientId:
398
+ durableMember.status === "active"
399
+ ? sameActiveMembership
400
+ ? prior.transportClientId || null
401
+ : null
402
+ : null,
403
+ };
404
+ members.set(member.principalId, member);
405
+ if (member.transportClientId) {
406
+ this._transportBindings.set(
407
+ transportBindingKey(snapshot.sessionId, member.transportClientId),
408
+ { sessionId: snapshot.sessionId, principalId: member.principalId },
409
+ );
410
+ }
411
+ }
412
+ const host = members.get(snapshot.hostPrincipalId) || null;
413
+ const session = {
414
+ sessionId: snapshot.sessionId,
415
+ agentSessionId: snapshot.agentSessionId,
416
+ name: previous?.name || snapshot.agentSessionId,
417
+ hostPrincipalId: snapshot.hostPrincipalId,
418
+ hostClientId: host?.transportClientId || null,
419
+ createdAt: snapshot.createdAt,
420
+ expiresAt: snapshot.expiresAt,
421
+ status: snapshot.status,
422
+ members,
423
+ membershipAuthority: authority.trustDescriptor().authorityVersion,
424
+ sessionEpoch: snapshot.sessionEpoch,
425
+ authorityGeneration: snapshot.authorityGeneration,
426
+ leases: new Map(snapshot.leases.map((lease) => [lease.leaseId, lease])),
427
+ };
428
+ this.sessions.set(session.sessionId, session);
429
+ return session;
430
+ }
431
+
432
+ _attachTransport(session, principalId, transportClientId) {
433
+ const member = session.members.get(principalId);
434
+ if (!member || member.status !== "active") {
435
+ throw new Error("Remote membership principal is not active");
436
+ }
437
+ const occupied = this._transportBindings.get(
438
+ transportBindingKey(session.sessionId, transportClientId),
439
+ );
440
+ if (occupied && occupied.principalId !== principalId) {
441
+ throw new Error("Remote transport is already bound to another principal");
442
+ }
443
+ if (member.transportClientId) {
444
+ this._transportBindings.delete(
445
+ transportBindingKey(session.sessionId, member.transportClientId),
446
+ );
447
+ }
448
+ member.transportClientId = transportClientId;
449
+ this._transportBindings.set(
450
+ transportBindingKey(session.sessionId, transportClientId),
451
+ { sessionId: session.sessionId, principalId },
452
+ );
453
+ if (principalId === session.hostPrincipalId) {
454
+ session.hostClientId = transportClientId;
455
+ }
456
+ return member;
457
+ }
458
+
459
+ _principalForTransport(session, transportClientId) {
460
+ if (!session.hostPrincipalId) return transportClientId;
461
+ const binding = this._transportBindings.get(
462
+ transportBindingKey(session.sessionId, transportClientId),
463
+ );
464
+ return binding?.principalId || null;
192
465
  }
193
466
 
194
467
  _requireMembershipAuthority() {
@@ -209,21 +482,50 @@ export class RemoteSessionRegistry {
209
482
  name,
210
483
  scopes,
211
484
  durableMembership = false,
485
+ hostCredentialPublicKeySpki = null,
212
486
  } = {}) {
213
487
  if (!hostClientId) throw new Error("hostClientId is required");
214
488
  if (!agentSessionId) throw new Error("agentSessionId is required");
215
489
  const now = this.now();
216
490
  const expiresAt = now + this.policy.capSessionTtl(this.sessionTtlMs);
217
491
  const sessionId = randomUUID();
218
- const durable = durableMembership
219
- ? this._requireMembershipAuthority().createSession({
492
+ const coordinator =
493
+ durableMembership && this._membershipCoordinator
494
+ ? this._requireMembershipCoordinator()
495
+ : null;
496
+ const durable = coordinator
497
+ ? coordinator.createSession({
220
498
  sessionId,
221
499
  agentSessionId,
222
- hostPrincipalId: hostClientId,
223
500
  scopes: REMOTE_SESSION_SCOPES,
224
501
  expiresAt,
502
+ hostCredentialPublicKeySpki,
503
+ joinPolicy: this.policy.authorityDescriptor(),
225
504
  })
226
- : null;
505
+ : durableMembership
506
+ ? this._requireMembershipAuthority().createSession({
507
+ sessionId,
508
+ agentSessionId,
509
+ hostPrincipalId: hostClientId,
510
+ scopes: REMOTE_SESSION_SCOPES,
511
+ expiresAt,
512
+ })
513
+ : null;
514
+ if (coordinator) {
515
+ const session = this._syncCoordinatorSnapshot(durable.statement.payload);
516
+ session.name = name || agentSessionId;
517
+ return {
518
+ session: publicSession(session),
519
+ pairing: this.issuePairingToken(session.sessionId, { scopes }),
520
+ bootstrap: Object.freeze({
521
+ trust: durable.trust,
522
+ statement: durable.statement,
523
+ hostPrincipalId: durable.hostPrincipalId,
524
+ membershipEpoch: durable.membershipEpoch,
525
+ hostCredentialPublicKeySpki: durable.hostCredentialPublicKeySpki,
526
+ }),
527
+ };
528
+ }
227
529
  const session = {
228
530
  sessionId,
229
531
  agentSessionId,
@@ -256,6 +558,312 @@ export class RemoteSessionRegistry {
256
558
  };
257
559
  }
258
560
 
561
+ pairingInvitation(sessionId, token) {
562
+ const session = this.requireSession(sessionId);
563
+ const pairing = this.tokens.get(sessionId);
564
+ if (!pairing || pairing.expiresAt <= this.now()) {
565
+ this.tokens.delete(sessionId);
566
+ throw new Error("Pairing token is missing or expired");
567
+ }
568
+ if (!tokensMatch(token, pairing.digest)) {
569
+ throw new Error("Invalid pairing token");
570
+ }
571
+ return Object.freeze({
572
+ session,
573
+ scopes: Object.freeze([...pairing.scopes]),
574
+ expiresAt: pairing.expiresAt,
575
+ });
576
+ }
577
+
578
+ issueMemberJoinChallenge({
579
+ sessionId,
580
+ transportClientId,
581
+ token,
582
+ credentialPublicKey,
583
+ connectionNonce,
584
+ capabilities = null,
585
+ } = {}) {
586
+ const invitation = this.pairingInvitation(sessionId, token);
587
+ const session = invitation.session;
588
+ if (!session.hostPrincipalId) {
589
+ throw new Error("Durable direct membership challenge is unavailable");
590
+ }
591
+ const deviceCount = [...session.members.values()].filter(
592
+ (member) =>
593
+ member.principalId !== session.hostPrincipalId &&
594
+ member.status === "active",
595
+ ).length;
596
+ this.policy.enforceJoin({ deviceCount, via: "direct" });
597
+ const challenge =
598
+ this._requireMembershipCoordinator().issueMemberJoinChallenge({
599
+ sessionId,
600
+ expectedSessionEpoch: session.sessionEpoch,
601
+ scopes: invitation.scopes,
602
+ credentialPublicKey,
603
+ connectionNonce,
604
+ ttlMs: Math.max(1, invitation.expiresAt - this.now()),
605
+ capabilities,
606
+ joinPolicy: this.policy.authorityDescriptor(),
607
+ });
608
+ this._pendingAuthentication.set(challenge.challengeId, {
609
+ purpose: "member.join",
610
+ sessionId,
611
+ transportClientId,
612
+ pairingDigest: Buffer.from(this.tokens.get(sessionId).digest),
613
+ pairingExpiresAt: invitation.expiresAt,
614
+ });
615
+ return challenge;
616
+ }
617
+
618
+ completeMemberJoin({
619
+ challengeId,
620
+ transportClientId,
621
+ connectionNonce,
622
+ signature,
623
+ pushToken = null,
624
+ pushProvider = null,
625
+ } = {}) {
626
+ const pending = this._pendingAuthentication.get(challengeId);
627
+ this._pendingAuthentication.delete(challengeId);
628
+ if (
629
+ !pending ||
630
+ pending.purpose !== "member.join" ||
631
+ pending.transportClientId !== transportClientId ||
632
+ pending.pairingExpiresAt <= this.now()
633
+ ) {
634
+ throw new Error("Remote membership join proof is stale");
635
+ }
636
+ const currentPairing = this.tokens.get(pending.sessionId);
637
+ if (
638
+ !currentPairing ||
639
+ currentPairing.digest.length !== pending.pairingDigest.length ||
640
+ !timingSafeEqual(currentPairing.digest, pending.pairingDigest)
641
+ ) {
642
+ throw new Error("Remote membership pairing invitation changed");
643
+ }
644
+ const joined = this._requireMembershipCoordinator().joinMember({
645
+ challengeId,
646
+ connectionNonce,
647
+ signature,
648
+ });
649
+ this.tokens.delete(pending.sessionId);
650
+ const session = this._syncCoordinatorSnapshot(joined.statement.payload);
651
+ const member = this._attachTransport(
652
+ session,
653
+ joined.principalId,
654
+ transportClientId,
655
+ );
656
+ member.pushToken = pushToken || null;
657
+ member.pushProvider = pushToken ? pushProvider || null : null;
658
+ return {
659
+ session: publicSession(session),
660
+ member: publicMember(member, session),
661
+ nextConnectionNonce: joined.nextConnectionNonce,
662
+ statement: joined.statement,
663
+ };
664
+ }
665
+
666
+ issueSessionResumeChallenge({
667
+ sessionId,
668
+ principalId,
669
+ transportClientId,
670
+ connectionNonce,
671
+ } = {}) {
672
+ const challenge =
673
+ this._requireMembershipCoordinator().issueSessionResumeChallenge({
674
+ sessionId,
675
+ principalId,
676
+ connectionNonce,
677
+ });
678
+ this._pendingAuthentication.set(challenge.challengeId, {
679
+ purpose: "session.resume",
680
+ sessionId,
681
+ principalId,
682
+ transportClientId,
683
+ });
684
+ return challenge;
685
+ }
686
+
687
+ completeSessionResume({
688
+ challengeId,
689
+ transportClientId,
690
+ connectionNonce,
691
+ signature,
692
+ } = {}) {
693
+ const pending = this._pendingAuthentication.get(challengeId);
694
+ this._pendingAuthentication.delete(challengeId);
695
+ if (
696
+ !pending ||
697
+ pending.purpose !== "session.resume" ||
698
+ pending.transportClientId !== transportClientId
699
+ ) {
700
+ throw new Error("Remote membership resume proof is stale");
701
+ }
702
+ const resumed = this._requireMembershipCoordinator().resumeSession({
703
+ challengeId,
704
+ connectionNonce,
705
+ signature,
706
+ });
707
+ if (
708
+ resumed.session.sessionId !== pending.sessionId ||
709
+ resumed.principalId !== pending.principalId
710
+ ) {
711
+ throw new Error("Remote membership resume binding changed");
712
+ }
713
+ const session = this._syncCoordinatorSnapshot(resumed.session);
714
+ const member = this._attachTransport(
715
+ session,
716
+ resumed.principalId,
717
+ transportClientId,
718
+ );
719
+ return {
720
+ session: publicSession(session),
721
+ member: publicMember(member, session),
722
+ nextConnectionNonce: resumed.nextConnectionNonce,
723
+ statement: resumed.statement,
724
+ trust: resumed.trust,
725
+ };
726
+ }
727
+
728
+ issueSessionReenableChallenge({
729
+ sessionId,
730
+ principalId,
731
+ transportClientId,
732
+ connectionNonce,
733
+ newHostCredentialPublicKeySpki = null,
734
+ } = {}) {
735
+ const coordinator = this._requireMembershipCoordinator();
736
+ const challenge = coordinator.issueSessionReenableChallenge({
737
+ sessionId,
738
+ principalId,
739
+ connectionNonce,
740
+ newHostCredentialPublicKeySpki,
741
+ scopes: REMOTE_SESSION_SCOPES,
742
+ expiresAt: this.now() + this.policy.capSessionTtl(this.sessionTtlMs),
743
+ joinPolicy: this.policy.authorityDescriptor(),
744
+ });
745
+ this._pendingAuthentication.set(challenge.challengeId, {
746
+ purpose: "session.reenable",
747
+ sessionId,
748
+ principalId,
749
+ nextPrincipalId: challenge.nextPrincipalId,
750
+ transportClientId,
751
+ });
752
+ return challenge;
753
+ }
754
+
755
+ completeSessionReenable({
756
+ challengeId,
757
+ transportClientId,
758
+ connectionNonce,
759
+ signature,
760
+ } = {}) {
761
+ const pending = this._pendingAuthentication.get(challengeId);
762
+ this._pendingAuthentication.delete(challengeId);
763
+ if (
764
+ !pending ||
765
+ pending.purpose !== "session.reenable" ||
766
+ pending.transportClientId !== transportClientId
767
+ ) {
768
+ throw new Error("Remote membership re-enable proof is stale");
769
+ }
770
+ const reenabled = this._requireMembershipCoordinator().reenableSession({
771
+ challengeId,
772
+ connectionNonce,
773
+ signature,
774
+ });
775
+ if (
776
+ reenabled.sessionId !== pending.sessionId ||
777
+ reenabled.hostPrincipalId !== pending.nextPrincipalId
778
+ ) {
779
+ throw new Error("Remote membership re-enable binding changed");
780
+ }
781
+ const session = this._syncCoordinatorSnapshot(reenabled.statement.payload);
782
+ const member = this._attachTransport(
783
+ session,
784
+ reenabled.hostPrincipalId,
785
+ transportClientId,
786
+ );
787
+ return {
788
+ session: publicSession(session),
789
+ member: publicMember(member, session),
790
+ nextConnectionNonce: reenabled.nextConnectionNonce,
791
+ statement: reenabled.statement,
792
+ trust: reenabled.trust,
793
+ };
794
+ }
795
+
796
+ terminalSnapshot(sessionId) {
797
+ const current =
798
+ this._requireMembershipCoordinator().getSessionSnapshot(sessionId);
799
+ if (!current || current.session.status !== "closed") {
800
+ throw new Error("Remote membership session has no terminal tombstone");
801
+ }
802
+ this._clearCoordinatorSession(sessionId);
803
+ return current;
804
+ }
805
+
806
+ joinRelayMember({
807
+ sessionId,
808
+ transportClientId,
809
+ token,
810
+ mobilePublicKey,
811
+ pairingTokenDigest,
812
+ possessionCapability,
813
+ capabilities = null,
814
+ pushToken = null,
815
+ pushProvider = null,
816
+ } = {}) {
817
+ const invitation = this.pairingInvitation(sessionId, token);
818
+ const session = invitation.session;
819
+ if (!session.hostPrincipalId) {
820
+ throw new Error("Durable relay membership is unavailable");
821
+ }
822
+ const deviceCount = [...session.members.values()].filter(
823
+ (member) =>
824
+ member.principalId !== session.hostPrincipalId &&
825
+ member.status === "active",
826
+ ).length;
827
+ this.policy.enforceJoin({ deviceCount, via: "relay" });
828
+ const negotiatedCapabilities = normalizeCapabilities(capabilities);
829
+ const negotiatedScopes = invitation.scopes.filter(
830
+ (scope) =>
831
+ scope !== "approve" ||
832
+ negotiatedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY),
833
+ );
834
+ if (negotiatedScopes.length === 0) {
835
+ throw new Error(
836
+ "Remote relay capabilities do not authorize any requested scope",
837
+ );
838
+ }
839
+ const joined = this._requireMembershipCoordinator().joinRelayMember({
840
+ sessionId,
841
+ expectedSessionEpoch: session.sessionEpoch,
842
+ scopes: negotiatedScopes,
843
+ mobilePeerId: transportClientId,
844
+ mobilePublicKey,
845
+ pairingTokenDigest,
846
+ possessionCapability,
847
+ capabilities: negotiatedCapabilities,
848
+ joinPolicy: this.policy.authorityDescriptor(),
849
+ });
850
+ this.tokens.delete(sessionId);
851
+ const updated = this._syncCoordinatorSnapshot(joined.statement.payload);
852
+ const member = this._attachTransport(
853
+ updated,
854
+ joined.principalId,
855
+ transportClientId,
856
+ );
857
+ member.pushToken = pushToken || null;
858
+ member.pushProvider = pushToken ? pushProvider || null : null;
859
+ return {
860
+ session: publicSession(updated),
861
+ member: publicMember(member, updated),
862
+ statement: joined.statement,
863
+ capabilities: Object.freeze([...negotiatedCapabilities]),
864
+ };
865
+ }
866
+
259
867
  issuePairingToken(sessionId, { scopes } = {}) {
260
868
  const session = this.requireSession(sessionId);
261
869
  const token = randomBytes(32).toString("base64url");
@@ -289,6 +897,11 @@ export class RemoteSessionRegistry {
289
897
  } = {}) {
290
898
  if (!clientId) throw new Error("clientId is required");
291
899
  const session = this.requireSession(sessionId);
900
+ if (session.hostPrincipalId) {
901
+ throw new Error(
902
+ "Durable membership requires a possession-proof join protocol",
903
+ );
904
+ }
292
905
  const pairing = this.tokens.get(sessionId);
293
906
  if (!pairing || pairing.expiresAt <= this.now()) {
294
907
  this.tokens.delete(sessionId);
@@ -338,8 +951,9 @@ export class RemoteSessionRegistry {
338
951
  */
339
952
  registerPush(sessionId, clientId, { token = null, provider = null } = {}) {
340
953
  const session = this.requireSession(sessionId);
341
- const member = session.members.get(clientId);
342
- if (!member) {
954
+ const principalId = this._principalForTransport(session, clientId);
955
+ const member = session.members.get(principalId || clientId);
956
+ if (!member || (member.status && member.status !== "active")) {
343
957
  throw new Error("Device is not paired with this remote session");
344
958
  }
345
959
  member.pushToken = token || null;
@@ -356,11 +970,25 @@ export class RemoteSessionRegistry {
356
970
  const session = this.requireSession(sessionId);
357
971
  const targets = [];
358
972
  for (const member of session.members.values()) {
359
- if (member.clientId === session.hostClientId) continue;
360
- if (member.clientId === excludeClientId) continue;
973
+ if (member.status && member.status !== "active") continue;
974
+ if (
975
+ (session.hostPrincipalId &&
976
+ member.principalId === session.hostPrincipalId) ||
977
+ (!session.hostPrincipalId && member.clientId === session.hostClientId)
978
+ )
979
+ continue;
980
+ if (
981
+ excludeClientId &&
982
+ (member.clientId === excludeClientId ||
983
+ member.transportClientId === excludeClientId)
984
+ )
985
+ continue;
361
986
  if (!member.pushToken) continue;
362
987
  targets.push({
363
988
  clientId: member.clientId,
989
+ ...(session.hostPrincipalId
990
+ ? { transportClientId: member.transportClientId || null }
991
+ : {}),
364
992
  pushToken: member.pushToken,
365
993
  pushProvider: member.pushProvider,
366
994
  });
@@ -375,9 +1003,13 @@ export class RemoteSessionRegistry {
375
1003
  "Remote membership session is quarantined after an unknown durable transition",
376
1004
  );
377
1005
  }
378
- const member = session.members.get(clientId);
1006
+ const principalId = this._principalForTransport(session, clientId);
1007
+ const member = session.members.get(principalId || clientId);
379
1008
  if (!member)
380
1009
  throw new Error("Client is not paired with this remote session");
1010
+ if (member.status && member.status !== "active") {
1011
+ throw new Error("Remote membership principal is not active");
1012
+ }
381
1013
  if (member.membershipQuarantined) {
382
1014
  throw new Error(
383
1015
  "Remote membership is quarantined after an unknown durable transition",
@@ -387,7 +1019,23 @@ export class RemoteSessionRegistry {
387
1019
  throw new Error(`Remote session scope required: ${scope}`);
388
1020
  }
389
1021
  let membership = null;
390
- if (session.membershipAuthority) {
1022
+ if (session.hostPrincipalId) {
1023
+ const verdict = this._requireMembershipCoordinator().readMembership(
1024
+ {
1025
+ sessionId,
1026
+ principalId: member.principalId,
1027
+ sessionEpoch: session.sessionEpoch,
1028
+ membershipEpoch: member.membershipEpoch,
1029
+ },
1030
+ scope,
1031
+ );
1032
+ if (!verdict.ok) {
1033
+ throw new Error(
1034
+ `Remote membership coordinator denied: ${verdict.reason}`,
1035
+ );
1036
+ }
1037
+ membership = verdict.binding;
1038
+ } else if (session.membershipAuthority) {
391
1039
  const verdict = this._requireMembershipAuthority().readMembership(
392
1040
  {
393
1041
  sessionId,
@@ -409,7 +1057,12 @@ export class RemoteSessionRegistry {
409
1057
 
410
1058
  members(sessionId) {
411
1059
  const session = this.requireSession(sessionId);
412
- return [...session.members.values()].map((member) => ({ ...member }));
1060
+ return [...session.members.values()]
1061
+ .filter((member) => !member.status || member.status === "active")
1062
+ .map((member) => ({
1063
+ ...publicMember(member, session),
1064
+ transportClientId: member.transportClientId || member.clientId,
1065
+ }));
413
1066
  }
414
1067
 
415
1068
  /**
@@ -418,18 +1071,33 @@ export class RemoteSessionRegistry {
418
1071
  */
419
1072
  listDevices(sessionId, hostClientId) {
420
1073
  const session = this.requireSession(sessionId);
421
- if (hostClientId && session.hostClientId !== hostClientId) {
1074
+ const hostPrincipal = hostClientId
1075
+ ? this._principalForTransport(session, hostClientId)
1076
+ : null;
1077
+ if (
1078
+ hostClientId &&
1079
+ (session.hostPrincipalId
1080
+ ? hostPrincipal !== session.hostPrincipalId
1081
+ : session.hostClientId !== hostClientId)
1082
+ ) {
422
1083
  throw new Error("Only the host can list paired devices");
423
1084
  }
424
1085
  return {
425
1086
  session: publicSession(session),
426
1087
  devices: [...session.members.values()].map((member) => ({
427
- clientId: member.clientId,
1088
+ clientId: member.principalId || member.clientId,
1089
+ ...(member.principalId ? { principalId: member.principalId } : {}),
428
1090
  scopes: [...member.scopes],
429
1091
  joinedAt: member.joinedAt,
430
- isHost: member.clientId === session.hostClientId,
1092
+ isHost: session.hostPrincipalId
1093
+ ? member.principalId === session.hostPrincipalId
1094
+ : member.clientId === session.hostClientId,
1095
+ ...(session.hostPrincipalId
1096
+ ? { connected: Boolean(member.transportClientId) }
1097
+ : {}),
431
1098
  hasPush: Boolean(member.pushToken),
432
1099
  pushProvider: member.pushProvider || null,
1100
+ ...(member.status ? { status: member.status } : {}),
433
1101
  })),
434
1102
  };
435
1103
  }
@@ -442,16 +1110,45 @@ export class RemoteSessionRegistry {
442
1110
  revokeMember(sessionId, hostClientId, clientId) {
443
1111
  if (!clientId) throw new Error("clientId is required");
444
1112
  const session = this.requireSession(sessionId);
445
- if (session.hostClientId !== hostClientId) {
1113
+ const hostPrincipal = this._principalForTransport(session, hostClientId);
1114
+ if (
1115
+ session.hostPrincipalId
1116
+ ? hostPrincipal !== session.hostPrincipalId
1117
+ : session.hostClientId !== hostClientId
1118
+ ) {
446
1119
  throw new Error("Only the host can revoke devices");
447
1120
  }
448
- if (clientId === session.hostClientId) {
1121
+ if (clientId === (session.hostPrincipalId || session.hostClientId)) {
449
1122
  throw new Error("Cannot revoke the host device");
450
1123
  }
451
1124
  const member = session.members.get(clientId);
452
1125
  if (!member)
453
1126
  throw new Error("Device is not paired with this remote session");
454
- if (session.membershipAuthority) {
1127
+ const notificationTransportId = member.transportClientId || member.clientId;
1128
+ let resultSession = session;
1129
+ let resultMember = member;
1130
+ let membershipStatement = null;
1131
+ if (session.hostPrincipalId) {
1132
+ try {
1133
+ const host = session.members.get(session.hostPrincipalId);
1134
+ const revoked = this._requireMembershipCoordinator().revokeMember({
1135
+ sessionId,
1136
+ principalId: clientId,
1137
+ hostPrincipalId: session.hostPrincipalId,
1138
+ expectedSessionEpoch: session.sessionEpoch,
1139
+ expectedMembershipEpoch: member.membershipEpoch,
1140
+ expectedHostMembershipEpoch: host.membershipEpoch,
1141
+ });
1142
+ resultSession = this._syncCoordinatorSnapshot(
1143
+ revoked.statement.payload,
1144
+ );
1145
+ resultMember = resultSession.members.get(clientId);
1146
+ membershipStatement = revoked.statement;
1147
+ } catch (error) {
1148
+ member.membershipQuarantined = true;
1149
+ throw error;
1150
+ }
1151
+ } else if (session.membershipAuthority) {
455
1152
  try {
456
1153
  this._requireMembershipAuthority().revokeMember({
457
1154
  sessionId,
@@ -466,13 +1163,33 @@ export class RemoteSessionRegistry {
466
1163
  throw error;
467
1164
  }
468
1165
  }
469
- session.members.delete(clientId);
470
- return { session: publicSession(session), member: { ...member } };
1166
+ if (!session.hostPrincipalId) session.members.delete(clientId);
1167
+ return {
1168
+ session: publicSession(resultSession),
1169
+ member: {
1170
+ ...publicMember(resultMember, resultSession),
1171
+ transportClientId: notificationTransportId,
1172
+ },
1173
+ ...(membershipStatement ? { statement: membershipStatement } : {}),
1174
+ };
471
1175
  }
472
1176
 
473
1177
  findHosted(agentSessionId, hostClientId) {
1178
+ if (this._membershipCoordinator) {
1179
+ // Enumeration cannot rely on the one-time startup hydration: another
1180
+ // server may have created, closed, or re-enabled a durable session since
1181
+ // this process started. Refresh the coordinator index before matching
1182
+ // local transport attachments.
1183
+ this._hydrateCoordinatorSessions(this._resolveMembershipCoordinator());
1184
+ }
474
1185
  const matches = [];
475
- for (const session of this.sessions.values()) {
1186
+ for (const sessionId of [...this.sessions.keys()]) {
1187
+ let session;
1188
+ try {
1189
+ session = this.requireSession(sessionId);
1190
+ } catch {
1191
+ continue;
1192
+ }
476
1193
  if (
477
1194
  session.agentSessionId === agentSessionId &&
478
1195
  session.hostClientId === hostClientId &&
@@ -487,8 +1204,25 @@ export class RemoteSessionRegistry {
487
1204
  removeClient(clientId) {
488
1205
  const affected = [];
489
1206
  for (const [sessionId, session] of this.sessions) {
490
- const member = session.members.get(clientId);
1207
+ const principalId = this._principalForTransport(session, clientId);
1208
+ const member = session.members.get(principalId || clientId);
491
1209
  if (!member) continue;
1210
+ if (session.hostPrincipalId) {
1211
+ this._transportBindings.delete(
1212
+ transportBindingKey(sessionId, clientId),
1213
+ );
1214
+ member.transportClientId = null;
1215
+ if (member.principalId === session.hostPrincipalId) {
1216
+ session.hostClientId = null;
1217
+ }
1218
+ affected.push({
1219
+ sessionId,
1220
+ closed: false,
1221
+ detached: true,
1222
+ principalId: member.principalId,
1223
+ });
1224
+ continue;
1225
+ }
492
1226
  if (session.hostClientId === clientId) {
493
1227
  if (session.membershipAuthority) {
494
1228
  try {
@@ -528,10 +1262,29 @@ export class RemoteSessionRegistry {
528
1262
 
529
1263
  close(sessionId, clientId) {
530
1264
  const session = this.requireSession(sessionId);
531
- if (session.hostClientId !== clientId) {
1265
+ const hostPrincipal = this._principalForTransport(session, clientId);
1266
+ if (
1267
+ session.hostPrincipalId
1268
+ ? hostPrincipal !== session.hostPrincipalId
1269
+ : session.hostClientId !== clientId
1270
+ ) {
532
1271
  throw new Error("Only the host can close a Remote Session");
533
1272
  }
534
- if (session.membershipAuthority) {
1273
+ let durableClose = null;
1274
+ if (session.hostPrincipalId) {
1275
+ try {
1276
+ const host = session.members.get(session.hostPrincipalId);
1277
+ durableClose = this._requireMembershipCoordinator().closeSession({
1278
+ sessionId,
1279
+ hostPrincipalId: session.hostPrincipalId,
1280
+ expectedSessionEpoch: session.sessionEpoch,
1281
+ expectedHostMembershipEpoch: host.membershipEpoch,
1282
+ });
1283
+ } catch (error) {
1284
+ session.membershipQuarantined = true;
1285
+ throw error;
1286
+ }
1287
+ } else if (session.membershipAuthority) {
535
1288
  try {
536
1289
  this._requireMembershipAuthority().closeSession({
537
1290
  sessionId,
@@ -545,14 +1298,29 @@ export class RemoteSessionRegistry {
545
1298
  }
546
1299
  this.sessions.delete(sessionId);
547
1300
  this.tokens.delete(sessionId);
548
- return publicSession(session);
1301
+ const closedSession = publicSession(session);
1302
+ return durableClose
1303
+ ? {
1304
+ session: closedSession,
1305
+ statement: durableClose.statement,
1306
+ terminal: durableClose.terminal === true,
1307
+ alreadyClosed: durableClose.alreadyClosed === true,
1308
+ }
1309
+ : closedSession;
549
1310
  }
550
1311
 
551
1312
  requireSession(sessionId) {
552
- const session = this.sessions.get(sessionId);
1313
+ let session = this.sessions.get(sessionId) || null;
1314
+ if ((session?.hostPrincipalId || !session) && this._membershipCoordinator) {
1315
+ const durable = this._refreshCoordinatorSession(sessionId);
1316
+ if (durable?.status && durable.status !== "active") {
1317
+ throw new Error("Remote session is durably closed");
1318
+ }
1319
+ session = this.sessions.get(sessionId) || null;
1320
+ }
553
1321
  if (!session) throw new Error("Remote session not found");
554
1322
  if (session.expiresAt <= this.now()) {
555
- if (session.membershipAuthority) {
1323
+ if (session.membershipAuthority && !session.hostPrincipalId) {
556
1324
  try {
557
1325
  this._requireMembershipAuthority().closeSession({
558
1326
  sessionId,