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
@@ -121,6 +121,11 @@ export async function createWsApprovalGate({
121
121
  }),
122
122
  hasConfirmer: () => gate.hasConfirmer(),
123
123
  setConfirmer: (fn) => gate.setConfirmer(fn),
124
+ setAuthorizationConsumer: (fn) => gate.setAuthorizationConsumer?.(fn),
125
+ hasAuthorizationConsumer: () =>
126
+ gate.hasAuthorizationConsumer?.() === true,
127
+ consumeAuthorization: (authorization, ctx) =>
128
+ gate.consumeAuthorization(authorization, ctx),
124
129
  getSessionPolicy: (sid) => inner.getSessionPolicy(sid || sessionId),
125
130
  setSessionPolicy: (sid, policy) =>
126
131
  inner.setSessionPolicy(sid || sessionId, policy),
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { EventEmitter } from "node:events";
14
- import { createHash, timingSafeEqual } from "node:crypto";
14
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { dirname, join } from "node:path";
17
17
  import { WebSocketServer } from "ws";
@@ -29,6 +29,7 @@ import { createWsMessageDispatcher } from "./message-dispatcher.js";
29
29
  import {
30
30
  RemoteSessionRegistry,
31
31
  RemoteSessionPolicy,
32
+ REMOTE_APPROVAL_BINDING_CAPABILITY,
32
33
  } from "../../harness/remote-session-registry.js";
33
34
  import { RemoteSessionAuditLog } from "../../harness/remote-session-audit.js";
34
35
  import { RemoteSessionAuditFileSink } from "../../harness/remote-session-audit-sink.js";
@@ -38,6 +39,8 @@ import {
38
39
  } from "../../harness/remote-session-push.js";
39
40
  import { createRemoteSessionPushSender } from "../../harness/remote-session-push-senders.js";
40
41
  import { DurableRemoteMembershipAuthority } from "../../lib/remote-membership-authority.js";
42
+ import { DurableRemoteMembershipCoordinator } from "../../lib/remote-membership-coordinator.js";
43
+ import { remoteSessionPairingTokenDigest } from "../../harness/remote-session-crypto.js";
41
44
  import { RemoteSessionRelay } from "../remote-session-relay.js";
42
45
  import { handleRemoteSessionPublish } from "./remote-session-protocol.js";
43
46
  import { handleTaskDetail, handleTaskHistory } from "./task-protocol.js";
@@ -271,11 +274,26 @@ export class ChainlessChainWSServer extends EventEmitter {
271
274
  /** Org-policy constraints for Remote Sessions (scopes, device cap, TTL). */
272
275
  this.remoteSessionPolicy =
273
276
  options.remoteSessionPolicy || RemoteSessionPolicy.fromEnv(process.env);
277
+ /**
278
+ * Server-authoritative durable membership/lease coordinator. Constructed
279
+ * lazily so ordinary WS deployments that never enable Remote Approval do
280
+ * not create a machine-security root as a constructor side effect.
281
+ */
282
+ this._remoteMembershipCoordinatorOption =
283
+ options.remoteMembershipCoordinator === null
284
+ ? null
285
+ : options.remoteMembershipCoordinator || undefined;
286
+ this._remoteMembershipCoordinatorOptions =
287
+ options.remoteMembershipCoordinatorOptions || {};
274
288
  /** Local authorization state for multi-device Remote Sessions. */
275
289
  this.remoteSessions =
276
290
  options.remoteSessionRegistry ||
277
291
  new RemoteSessionRegistry({
278
292
  policy: this.remoteSessionPolicy,
293
+ membershipCoordinator:
294
+ options.remoteMembershipCoordinator === null
295
+ ? null
296
+ : () => this._requireRemoteMembershipCoordinator(),
279
297
  membershipAuthority:
280
298
  options.remoteMembershipAuthority === null
281
299
  ? null
@@ -343,6 +361,20 @@ export class ChainlessChainWSServer extends EventEmitter {
343
361
  this._clientCounter = 0;
344
362
  }
345
363
 
364
+ _requireRemoteMembershipCoordinator() {
365
+ if (typeof this._remoteMembershipCoordinatorOption === "function") {
366
+ this._remoteMembershipCoordinatorOption =
367
+ this._remoteMembershipCoordinatorOption();
368
+ }
369
+ if (!this._remoteMembershipCoordinatorOption) {
370
+ this._remoteMembershipCoordinatorOption =
371
+ new DurableRemoteMembershipCoordinator(
372
+ this._remoteMembershipCoordinatorOptions,
373
+ );
374
+ }
375
+ return this._remoteMembershipCoordinatorOption;
376
+ }
377
+
346
378
  /** Start the WebSocket server */
347
379
  start() {
348
380
  return new Promise((resolve, reject) => {
@@ -465,6 +497,7 @@ export class ChainlessChainWSServer extends EventEmitter {
465
497
  ip: clientIp,
466
498
  alive: true,
467
499
  authTimer: null,
500
+ membershipConnectionNonce: randomBytes(32).toString("base64url"),
468
501
  };
469
502
  this.clients.set(clientId, client);
470
503
 
@@ -1329,10 +1362,17 @@ export class ChainlessChainWSServer extends EventEmitter {
1329
1362
  data.sessionId,
1330
1363
  hostClientId,
1331
1364
  )) {
1332
- for (const member of remoteSession.members.values()) {
1365
+ for (const memberView of this.remoteSessions.members(
1366
+ remoteSession.sessionId,
1367
+ )) {
1368
+ const memberTransportId =
1369
+ memberView.transportClientId || memberView.clientId;
1370
+ if (remoteSession.hostPrincipalId && !memberView.transportClientId) {
1371
+ continue;
1372
+ }
1333
1373
  if (
1334
- member.clientId === hostClientId ||
1335
- !member.scopes.includes("observe")
1374
+ memberTransportId === hostClientId ||
1375
+ !memberView.scopes.includes("observe")
1336
1376
  ) {
1337
1377
  continue;
1338
1378
  }
@@ -1340,27 +1380,47 @@ export class ChainlessChainWSServer extends EventEmitter {
1340
1380
  // Independent of the WS mirror below — a relay-paired mobile app may
1341
1381
  // report a live socket while its process is suspended and will never
1342
1382
  // read the mirrored event until the user taps the push.
1343
- if (member.pushToken && isApprovalRequestEvent(data.type)) {
1344
- this._dispatchApprovalPush(remoteSession, member, data);
1383
+ if (memberView.pushToken && isApprovalRequestEvent(data.type)) {
1384
+ this._dispatchApprovalPush(remoteSession, memberView, data);
1345
1385
  }
1346
- const target = this.clients.get(member.clientId);
1386
+ const target = this.clients.get(memberTransportId);
1347
1387
  if (!target && this.remoteSessionRelay) {
1348
1388
  const crypto = this.remoteSessionCrypto.get(remoteSession.sessionId);
1349
1389
  if (crypto) {
1350
- this.remoteSessionRelay.sendEncrypted(
1351
- member.clientId,
1352
- crypto.encrypt(member.clientId, data),
1353
- );
1390
+ try {
1391
+ this.remoteSessions.authorize(
1392
+ remoteSession.sessionId,
1393
+ memberTransportId,
1394
+ "observe",
1395
+ );
1396
+ this.remoteSessionRelay.sendEncrypted(
1397
+ memberTransportId,
1398
+ crypto.encrypt(memberTransportId, data),
1399
+ );
1400
+ } catch {
1401
+ // Revoked/closed between enumeration and the physical send.
1402
+ }
1354
1403
  }
1355
1404
  continue;
1356
1405
  }
1357
1406
  if (!target) continue;
1358
- this._send(target.ws, {
1359
- type: "remote-session-event",
1360
- remoteSessionId: remoteSession.sessionId,
1361
- agentSessionId: remoteSession.agentSessionId,
1362
- event: data,
1363
- });
1407
+ try {
1408
+ this.remoteSessions.authorize(
1409
+ remoteSession.sessionId,
1410
+ memberTransportId,
1411
+ "observe",
1412
+ );
1413
+ const currentTarget = this.clients.get(memberTransportId);
1414
+ if (!currentTarget || currentTarget.ws !== target.ws) continue;
1415
+ this._send(currentTarget.ws, {
1416
+ type: "remote-session-event",
1417
+ remoteSessionId: remoteSession.sessionId,
1418
+ agentSessionId: remoteSession.agentSessionId,
1419
+ event: data,
1420
+ });
1421
+ } catch {
1422
+ // Current durable membership is the send-time authority.
1423
+ }
1364
1424
  }
1365
1425
  }
1366
1426
  }
@@ -1373,14 +1433,26 @@ export class ChainlessChainWSServer extends EventEmitter {
1373
1433
  _dispatchApprovalPush(remoteSession, member, data) {
1374
1434
  const dispatcher = this.remoteSessionPush;
1375
1435
  if (!dispatcher || !dispatcher.enabled) return;
1436
+ let authorized;
1437
+ try {
1438
+ authorized = this.remoteSessions.authorize(
1439
+ remoteSession.sessionId,
1440
+ member.transportClientId || member.clientId,
1441
+ "observe",
1442
+ );
1443
+ } catch {
1444
+ return;
1445
+ }
1446
+ const currentMember = authorized.member;
1447
+ if (!currentMember.pushToken) return;
1376
1448
  const requestId =
1377
1449
  data.requestId || data.approvalId || data.id || `approval:${data.type}`;
1378
1450
  dispatcher
1379
1451
  .dispatch({
1380
- token: member.pushToken,
1381
- provider: member.pushProvider,
1452
+ token: currentMember.pushToken,
1453
+ provider: currentMember.pushProvider,
1382
1454
  sessionId: remoteSession.sessionId,
1383
- clientId: member.clientId,
1455
+ clientId: currentMember.principalId || currentMember.clientId,
1384
1456
  dedupeKey: requestId,
1385
1457
  notification: {
1386
1458
  title: "Approval requested",
@@ -1394,7 +1466,7 @@ export class ChainlessChainWSServer extends EventEmitter {
1394
1466
  try {
1395
1467
  this.remoteSessions.registerPush(
1396
1468
  remoteSession.sessionId,
1397
- member.clientId,
1469
+ currentMember.principalId || currentMember.clientId,
1398
1470
  { token: null },
1399
1471
  );
1400
1472
  } catch {
@@ -1402,15 +1474,15 @@ export class ChainlessChainWSServer extends EventEmitter {
1402
1474
  }
1403
1475
  this.remoteSessionAudit?.record({
1404
1476
  sessionId: remoteSession.sessionId,
1405
- actor: member.clientId,
1477
+ actor: currentMember.principalId || currentMember.clientId,
1406
1478
  action: "push.unregistered",
1407
- detail: { provider: member.pushProvider || null },
1479
+ detail: { provider: currentMember.pushProvider || null },
1408
1480
  });
1409
1481
  return;
1410
1482
  }
1411
1483
  this.remoteSessionAudit?.record({
1412
1484
  sessionId: remoteSession.sessionId,
1413
- actor: member.clientId,
1485
+ actor: currentMember.principalId || currentMember.clientId,
1414
1486
  action:
1415
1487
  outcome.status === "sent"
1416
1488
  ? "push.sent"
@@ -1418,7 +1490,7 @@ export class ChainlessChainWSServer extends EventEmitter {
1418
1490
  ? "push.failed"
1419
1491
  : "push.skipped",
1420
1492
  detail: {
1421
- provider: member.pushProvider || null,
1493
+ provider: currentMember.pushProvider || null,
1422
1494
  reason: outcome.reason || null,
1423
1495
  error: outcome.error || null,
1424
1496
  },
@@ -1447,6 +1519,52 @@ export class ChainlessChainWSServer extends EventEmitter {
1447
1519
  });
1448
1520
  }
1449
1521
 
1522
+ _assertExactRemoteRelayMembership(sessionId, peerId, receipt) {
1523
+ if (
1524
+ receipt?.sessionId !== sessionId ||
1525
+ receipt.mobilePeerId !== peerId ||
1526
+ !Array.isArray(receipt.scopes) ||
1527
+ receipt.scopes.length === 0
1528
+ ) {
1529
+ throw new Error("Remote relay membership reconciliation changed");
1530
+ }
1531
+ const authorized = this.remoteSessions.authorize(
1532
+ sessionId,
1533
+ peerId,
1534
+ receipt.scopes[0],
1535
+ );
1536
+ const expectedScopes = JSON.stringify([...receipt.scopes].sort());
1537
+ if (
1538
+ authorized.session.sessionId !== receipt.sessionId ||
1539
+ authorized.session.sessionEpoch !== receipt.sessionEpoch ||
1540
+ authorized.member.principalId !== receipt.principalId ||
1541
+ authorized.member.membershipEpoch !== receipt.membershipEpoch ||
1542
+ authorized.member.transportClientId !== peerId ||
1543
+ JSON.stringify([...authorized.member.scopes].sort()) !== expectedScopes ||
1544
+ authorized.membership?.sessionId !== receipt.sessionId ||
1545
+ authorized.membership?.sessionEpoch !== receipt.sessionEpoch ||
1546
+ authorized.membership?.principalId !== receipt.principalId ||
1547
+ authorized.membership?.membershipEpoch !== receipt.membershipEpoch ||
1548
+ JSON.stringify([...(authorized.membership?.scopes || [])].sort()) !==
1549
+ expectedScopes
1550
+ ) {
1551
+ throw new Error("Remote relay membership reconciliation changed");
1552
+ }
1553
+ return authorized;
1554
+ }
1555
+
1556
+ _deleteRemoteSessionPairingSecretIfMatches(sessionId, tokenDigest) {
1557
+ const current = this.remoteSessionPairingSecrets.get(sessionId);
1558
+ if (
1559
+ typeof current === "string" &&
1560
+ remoteSessionPairingTokenDigest(current) === tokenDigest
1561
+ ) {
1562
+ this.remoteSessionPairingSecrets.delete(sessionId);
1563
+ return true;
1564
+ }
1565
+ return false;
1566
+ }
1567
+
1450
1568
  async _handleRemoteRelayMessage(message) {
1451
1569
  if (
1452
1570
  message?.type !== "message" ||
@@ -1457,21 +1575,312 @@ export class ChainlessChainWSServer extends EventEmitter {
1457
1575
  const sessionId = payload.envelope?.sessionId;
1458
1576
  const crypto = this.remoteSessionCrypto.get(sessionId);
1459
1577
  const token = this.remoteSessionPairingSecrets.get(sessionId);
1460
- if (!crypto || !token)
1578
+ if (!crypto)
1461
1579
  throw new Error("Remote Session pairing is unavailable or expired");
1462
- crypto.pair(payload.mobilePeerId, payload.mobilePublicKey, token);
1463
- const join = crypto.decrypt(payload.envelope);
1464
- if (join.type !== "pair.join" || join.token !== token) {
1580
+ if (
1581
+ typeof payload.mobilePeerId !== "string" ||
1582
+ payload.mobilePeerId !== message.from ||
1583
+ payload.envelope?.senderId !== payload.mobilePeerId
1584
+ ) {
1585
+ throw new Error("Remote Session relay peer binding is invalid");
1586
+ }
1587
+
1588
+ // An already-authenticated receipt wins even if the host has meanwhile
1589
+ // issued a fresh token for another device. Trying the committed key first
1590
+ // lets an ACK-lost peer reconcile without letting the new invitation erase
1591
+ // its key/receipt. Authentication failure is side-effect free; only then
1592
+ // may this frame be considered as a prospective current-token pairing.
1593
+ let acceptedRetry = null;
1594
+ const hasPendingCommittedAcceptance =
1595
+ crypto.hasPendingCommittedRelayAcceptance(payload.mobilePeerId);
1596
+ if (crypto.hasRelayMembershipAcceptance(payload.mobilePeerId)) {
1597
+ try {
1598
+ acceptedRetry = crypto.decrypt(payload.envelope);
1599
+ } catch (error) {
1600
+ if (!token && !hasPendingCommittedAcceptance) throw error;
1601
+ }
1602
+ }
1603
+ if (acceptedRetry) {
1604
+ const join = acceptedRetry;
1605
+ if (
1606
+ join.type !== "pair.join" ||
1607
+ join.remoteSessionId !== sessionId ||
1608
+ typeof join.token !== "string"
1609
+ ) {
1610
+ throw new Error("Invalid encrypted Remote Session pairing retry");
1611
+ }
1612
+ const receipt = crypto.readRelayMembershipAcceptance(
1613
+ payload.mobilePeerId,
1614
+ {
1615
+ mobilePublicKey: payload.mobilePublicKey,
1616
+ pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
1617
+ capabilities: join.capabilities,
1618
+ },
1619
+ );
1620
+ this._assertExactRemoteRelayMembership(
1621
+ sessionId,
1622
+ payload.mobilePeerId,
1623
+ receipt,
1624
+ );
1625
+ this._deleteRemoteSessionPairingSecretIfMatches(
1626
+ sessionId,
1627
+ receipt.pairingTokenDigest,
1628
+ );
1629
+ this.remoteSessionAudit?.record({
1630
+ sessionId,
1631
+ actor: payload.mobilePeerId,
1632
+ action: "device.join-reconciled",
1633
+ detail: {
1634
+ via: "relay",
1635
+ principalId: receipt.principalId,
1636
+ sessionEpoch: receipt.sessionEpoch,
1637
+ membershipEpoch: receipt.membershipEpoch,
1638
+ },
1639
+ });
1640
+ this.remoteSessionRelay.sendEncrypted(
1641
+ payload.mobilePeerId,
1642
+ crypto.encrypt(payload.mobilePeerId, {
1643
+ type: "pair.accepted",
1644
+ remoteSessionId: sessionId,
1645
+ principalId: receipt.principalId,
1646
+ sessionEpoch: receipt.sessionEpoch,
1647
+ membershipEpoch: receipt.membershipEpoch,
1648
+ scopes: [...receipt.scopes],
1649
+ capabilities: [...receipt.capabilities],
1650
+ reconciled: true,
1651
+ }),
1652
+ );
1653
+ return;
1654
+ }
1655
+
1656
+ let pendingRetry = null;
1657
+ if (hasPendingCommittedAcceptance) {
1658
+ try {
1659
+ pendingRetry = crypto.decryptPendingCommittedRelayAcceptance(
1660
+ payload.mobilePeerId,
1661
+ payload.envelope,
1662
+ );
1663
+ } catch (error) {
1664
+ if (!token) throw error;
1665
+ }
1666
+ }
1667
+ if (pendingRetry) {
1668
+ const join = pendingRetry;
1669
+ if (
1670
+ join.type !== "pair.join" ||
1671
+ join.remoteSessionId !== sessionId ||
1672
+ typeof join.token !== "string"
1673
+ ) {
1674
+ throw new Error("Invalid encrypted Remote Session pairing retry");
1675
+ }
1676
+ crypto.readPendingRelayMembershipRequest(payload.mobilePeerId, {
1677
+ mobilePublicKey: payload.mobilePublicKey,
1678
+ pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
1679
+ capabilities: join.capabilities,
1680
+ });
1681
+ const membershipCoordinator = this._requireRemoteMembershipCoordinator();
1682
+ const authorityRead = membershipCoordinator.getSessionSnapshot(sessionId);
1683
+ const resolution = crypto.resolvePendingRelayMembershipCommit(
1684
+ payload.mobilePeerId,
1685
+ authorityRead,
1686
+ membershipCoordinator.relayAuthorityDescriptor(),
1687
+ );
1688
+ if (resolution.status === "not-committed") {
1689
+ crypto.discardPendingRelayMembershipCommit(payload.mobilePeerId);
1690
+ } else if (resolution.status !== "committed") {
1691
+ crypto.clearRelayPeer(payload.mobilePeerId);
1692
+ throw new Error(
1693
+ "Remote relay committed membership is no longer active",
1694
+ );
1695
+ } else {
1696
+ const receipt = crypto.readPendingCommittedRelayAcceptance(
1697
+ payload.mobilePeerId,
1698
+ {
1699
+ mobilePublicKey: payload.mobilePublicKey,
1700
+ pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
1701
+ capabilities: join.capabilities,
1702
+ },
1703
+ );
1704
+ this._assertExactRemoteRelayMembership(
1705
+ sessionId,
1706
+ payload.mobilePeerId,
1707
+ receipt,
1708
+ );
1709
+ crypto.finalizeCommittedRelayAcceptance(payload.mobilePeerId);
1710
+ this._deleteRemoteSessionPairingSecretIfMatches(
1711
+ sessionId,
1712
+ receipt.pairingTokenDigest,
1713
+ );
1714
+ this.remoteSessionAudit?.record({
1715
+ sessionId,
1716
+ actor: payload.mobilePeerId,
1717
+ action: "device.join-reconciled",
1718
+ detail: {
1719
+ via: "relay",
1720
+ principalId: receipt.principalId,
1721
+ sessionEpoch: receipt.sessionEpoch,
1722
+ membershipEpoch: receipt.membershipEpoch,
1723
+ },
1724
+ });
1725
+ this.remoteSessionRelay.sendEncrypted(
1726
+ payload.mobilePeerId,
1727
+ crypto.encrypt(payload.mobilePeerId, {
1728
+ type: "pair.accepted",
1729
+ remoteSessionId: sessionId,
1730
+ principalId: receipt.principalId,
1731
+ sessionEpoch: receipt.sessionEpoch,
1732
+ membershipEpoch: receipt.membershipEpoch,
1733
+ scopes: [...receipt.scopes],
1734
+ capabilities: [...receipt.capabilities],
1735
+ reconciled: true,
1736
+ }),
1737
+ );
1738
+ return;
1739
+ }
1740
+ }
1741
+ if (!token) {
1742
+ throw new Error("Remote Session pairing is unavailable or expired");
1743
+ }
1744
+ const invitation = this.remoteSessions.pairingInvitation(sessionId, token);
1745
+ const durableMembership = Boolean(invitation.session.hostPrincipalId);
1746
+ // A relay peer id is the transport identity for the life of its active
1747
+ // membership. If an existing receipt could not authenticate this frame,
1748
+ // do not let possession of a newer invitation replace that transport with
1749
+ // a different key/principal. In particular, joinRelayMember() commits the
1750
+ // durable coordinator transition before attaching the transport; detecting
1751
+ // the collision there would consume the token and leave an orphan member.
1752
+ // members() refreshes durable state and returns only active memberships, so
1753
+ // a revoked/inactive member whose binding was cleared does not block a
1754
+ // legitimate fresh join.
1755
+ const activeTransportMember = this.remoteSessions
1756
+ .members(sessionId)
1757
+ .find((member) => member.transportClientId === payload.mobilePeerId);
1758
+ if (activeTransportMember) {
1759
+ throw new Error(
1760
+ "Remote relay transport is already bound to an active membership",
1761
+ );
1762
+ }
1763
+ // Derive and authenticate against a candidate key without touching the
1764
+ // live key, replay high-water, or accepted receipt.
1765
+ const membershipCoordinator = durableMembership
1766
+ ? this._requireRemoteMembershipCoordinator()
1767
+ : null;
1768
+ const relayAuthority = membershipCoordinator
1769
+ ? membershipCoordinator.relayAuthorityDescriptor()
1770
+ : null;
1771
+ const stagedPairing = crypto.stageRelayPairing(
1772
+ payload.mobilePeerId,
1773
+ payload.mobilePublicKey,
1774
+ token,
1775
+ payload.envelope,
1776
+ durableMembership
1777
+ ? {
1778
+ authorizedScopes: invitation.scopes,
1779
+ expiresAtMs: invitation.expiresAt,
1780
+ ...relayAuthority,
1781
+ }
1782
+ : null,
1783
+ );
1784
+ const join = stagedPairing.message;
1785
+ if (
1786
+ join.type !== "pair.join" ||
1787
+ join.remoteSessionId !== sessionId ||
1788
+ join.token !== token
1789
+ ) {
1465
1790
  throw new Error("Invalid encrypted Remote Session pairing request");
1466
1791
  }
1467
- const relayMember = this.remoteSessions.join({
1468
- sessionId,
1469
- clientId: payload.mobilePeerId,
1470
- token: join.token,
1471
- via: "relay",
1472
- pushToken: join.pushToken,
1473
- pushProvider: join.pushProvider,
1474
- });
1792
+ const relayPossessionCapability = durableMembership
1793
+ ? stagedPairing.possessionCapability
1794
+ : null;
1795
+ let relayMember;
1796
+ let committedReceipt = null;
1797
+ if (durableMembership) {
1798
+ const expectedScopes = invitation.scopes.filter(
1799
+ (scope) =>
1800
+ scope !== "approve" ||
1801
+ (Array.isArray(join.capabilities) &&
1802
+ join.capabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY)),
1803
+ );
1804
+ const authorityBefore =
1805
+ membershipCoordinator.getSessionSnapshot(sessionId);
1806
+ if (!authorityBefore) {
1807
+ throw new Error("Remote relay membership authority is unavailable");
1808
+ }
1809
+ // The prepared record exists before the durable call. If the coordinator
1810
+ // commits but the registry return path or the very next instruction
1811
+ // fails, a later authenticated retry can adjudicate against a fresh
1812
+ // authority snapshot without replaying the one-time join.
1813
+ crypto.armPendingRelayMembershipCommit(stagedPairing.stage, {
1814
+ authoritySnapshot: authorityBefore.session,
1815
+ scopes: expectedScopes,
1816
+ capabilities: join.capabilities,
1817
+ });
1818
+ try {
1819
+ relayMember = this.remoteSessions.joinRelayMember({
1820
+ sessionId,
1821
+ transportClientId: payload.mobilePeerId,
1822
+ token: join.token,
1823
+ // Mobile/web clients send a raw 32-byte X25519 key. The authenticated
1824
+ // relay transcript canonicalizes that key to SPKI; use the bound proof
1825
+ // value rather than reparsing the untrusted transport representation.
1826
+ mobilePublicKey: relayPossessionCapability.mobilePublicKey,
1827
+ pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
1828
+ possessionCapability: relayPossessionCapability,
1829
+ capabilities: join.capabilities,
1830
+ pushToken: join.pushToken,
1831
+ pushProvider: join.pushProvider,
1832
+ });
1833
+ committedReceipt = crypto.completePendingCommittedRelayAcceptance(
1834
+ payload.mobilePeerId,
1835
+ {
1836
+ mobilePublicKey: relayPossessionCapability.mobilePublicKey,
1837
+ pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
1838
+ principalId: relayMember.member.principalId,
1839
+ sessionEpoch: relayMember.session.sessionEpoch,
1840
+ membershipEpoch: relayMember.member.membershipEpoch,
1841
+ scopes: relayMember.member.scopes,
1842
+ capabilities: relayMember.capabilities,
1843
+ statement: relayMember.statement,
1844
+ },
1845
+ );
1846
+ } catch (error) {
1847
+ try {
1848
+ const freshAuthority =
1849
+ membershipCoordinator.getSessionSnapshot(sessionId);
1850
+ const resolution = crypto.resolvePendingRelayMembershipCommit(
1851
+ payload.mobilePeerId,
1852
+ freshAuthority,
1853
+ relayAuthority,
1854
+ );
1855
+ if (resolution.status === "not-committed") {
1856
+ crypto.discardPendingRelayMembershipCommit(payload.mobilePeerId);
1857
+ } else if (resolution.status === "fenced") {
1858
+ crypto.clearRelayPeer(payload.mobilePeerId);
1859
+ }
1860
+ } catch {
1861
+ // Keep the prepared record if the fresh authority read itself is
1862
+ // unavailable. Guessing non-commit here could erase the only
1863
+ // in-process evidence for an already-durable membership.
1864
+ }
1865
+ throw error;
1866
+ }
1867
+ this._assertExactRemoteRelayMembership(
1868
+ sessionId,
1869
+ payload.mobilePeerId,
1870
+ committedReceipt,
1871
+ );
1872
+ crypto.finalizeCommittedRelayAcceptance(payload.mobilePeerId);
1873
+ } else {
1874
+ relayMember = this.remoteSessions.join({
1875
+ sessionId,
1876
+ clientId: payload.mobilePeerId,
1877
+ token: join.token,
1878
+ via: "relay",
1879
+ pushToken: join.pushToken,
1880
+ pushProvider: join.pushProvider,
1881
+ });
1882
+ crypto.commitStagedRelayPairing(stagedPairing.stage);
1883
+ }
1475
1884
  this.remoteSessionAudit?.record({
1476
1885
  sessionId,
1477
1886
  actor: payload.mobilePeerId,
@@ -1479,14 +1888,24 @@ export class ChainlessChainWSServer extends EventEmitter {
1479
1888
  detail: {
1480
1889
  via: "relay",
1481
1890
  hasPush: relayMember?.member?.pushToken ? true : false,
1891
+ scopes: relayMember?.member?.scopes || null,
1892
+ capabilities: relayMember?.capabilities || [],
1482
1893
  },
1483
1894
  });
1484
- this.remoteSessionPairingSecrets.delete(sessionId);
1895
+ this._deleteRemoteSessionPairingSecretIfMatches(
1896
+ sessionId,
1897
+ remoteSessionPairingTokenDigest(join.token),
1898
+ );
1485
1899
  this.remoteSessionRelay.sendEncrypted(
1486
1900
  payload.mobilePeerId,
1487
1901
  crypto.encrypt(payload.mobilePeerId, {
1488
1902
  type: "pair.accepted",
1489
1903
  remoteSessionId: sessionId,
1904
+ principalId: relayMember?.member?.principalId || null,
1905
+ sessionEpoch: relayMember?.session?.sessionEpoch || null,
1906
+ membershipEpoch: relayMember?.member?.membershipEpoch || null,
1907
+ scopes: relayMember?.member?.scopes || null,
1908
+ capabilities: relayMember?.capabilities || [],
1490
1909
  }),
1491
1910
  );
1492
1911
  }
@@ -1500,6 +1919,11 @@ export class ChainlessChainWSServer extends EventEmitter {
1500
1919
  readyState: 1,
1501
1920
  send: (raw) => {
1502
1921
  const response = JSON.parse(raw);
1922
+ try {
1923
+ this.remoteSessions.authorize(envelope.sessionId, from, "observe");
1924
+ } catch {
1925
+ return;
1926
+ }
1503
1927
  this.remoteSessionRelay.sendEncrypted(
1504
1928
  from,
1505
1929
  crypto.encrypt(from, response),