chainlesschain 0.163.8 → 0.165.1

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 (240) hide show
  1. package/package.json +7 -4
  2. package/scripts/macos-mcp-launcher-build.mjs +317 -0
  3. package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
  4. package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
  5. package/src/assets/web-panel/.build-hash +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
  10. package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
  51. package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
  52. package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
  56. package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
  62. package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
  63. package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
  67. package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
  68. package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
  91. package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
  107. package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
  136. package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
  137. package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +3 -3
  155. package/src/commands/cowork.js +418 -41
  156. package/src/commands/permissions.js +240 -16
  157. package/src/commands/plugin.js +1293 -56
  158. package/src/commands/session-location.js +276 -0
  159. package/src/commands/session.js +2 -0
  160. package/src/data/changelog.json +28 -2
  161. package/src/gateways/ws/action-protocol.js +316 -17
  162. package/src/gateways/ws/message-dispatcher.js +30 -0
  163. package/src/gateways/ws/remote-session-protocol.js +565 -35
  164. package/src/gateways/ws/ws-agent-handler.js +12 -0
  165. package/src/gateways/ws/ws-approval-gate.js +5 -0
  166. package/src/gateways/ws/ws-server.js +510 -50
  167. package/src/harness/jsonl-session-store.js +341 -24
  168. package/src/harness/mcp-client.js +8 -1
  169. package/src/harness/remote-session-audit.js +2 -0
  170. package/src/harness/remote-session-crypto.js +929 -21
  171. package/src/harness/remote-session-registry.js +945 -24
  172. package/src/harness/session-list-index.js +233 -14
  173. package/src/lib/agent-authority.js +6 -0
  174. package/src/lib/auto-mode-config.js +98 -3
  175. package/src/lib/background-agent-keeper-client.js +248 -0
  176. package/src/lib/background-agent-keeper-protocol.js +291 -0
  177. package/src/lib/background-agent-supervisor.js +1357 -85
  178. package/src/lib/background-interaction-resolver.js +12 -0
  179. package/src/lib/background-session-transport.js +23 -6
  180. package/src/lib/background-turn-bootstrap-protocol.js +190 -0
  181. package/src/lib/cowork-workflow.js +1188 -76
  182. package/src/lib/delivery-production-adapter.js +38 -2
  183. package/src/lib/dynamic-workflow-facade.js +1736 -0
  184. package/src/lib/execution-location-contract.js +551 -0
  185. package/src/lib/execution-location-runtime.js +130 -0
  186. package/src/lib/ide-context.js +94 -6
  187. package/src/lib/mcp-stdio-package-materialization.js +166 -14
  188. package/src/lib/packer/pkg-config-generator.js +2 -0
  189. package/src/lib/permission-authority.js +77 -0
  190. package/src/lib/permission-side-effect-center.js +495 -0
  191. package/src/lib/plugin-runtime/install.js +514 -13
  192. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
  193. package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
  194. package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
  195. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
  196. package/src/lib/plugin-security.js +60 -3
  197. package/src/lib/process-execution-broker/index.js +1095 -57
  198. package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
  199. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
  200. package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
  201. package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
  202. package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
  203. package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
  204. package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
  205. package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
  206. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  207. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  208. package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
  209. package/src/lib/public-ide-capability-manifest.js +3 -0
  210. package/src/lib/remote-approval-bridge.js +777 -34
  211. package/src/lib/remote-control.js +2 -0
  212. package/src/lib/remote-membership-authority.js +632 -0
  213. package/src/lib/remote-membership-coordinator.js +2973 -0
  214. package/src/lib/remote-membership-host-store.js +1799 -0
  215. package/src/lib/runtime-provenance-ledger.js +171 -46
  216. package/src/lib/scoped-permission-store.js +369 -0
  217. package/src/lib/session-anti-rollback-anchor.js +262 -5
  218. package/src/lib/shell-approval.js +169 -6
  219. package/src/lib/sub-agent-context.js +4 -0
  220. package/src/lib/with-file-lock.js +73 -38
  221. package/src/lib/workflow-definition-contract.js +149 -0
  222. package/src/repl/agent-repl.js +111 -36
  223. package/src/repl/chat-repl.js +2 -0
  224. package/src/repl/clipboard-image.js +735 -16
  225. package/src/repl/ide-status.js +1 -1
  226. package/src/repl/prompt-interactions.js +59 -6
  227. package/src/repl/remote-approval.js +29 -7
  228. package/src/runtime/agent-core.js +1182 -155
  229. package/src/runtime/headless-runner.js +52 -30
  230. package/src/runtime/headless-stream.js +51 -28
  231. package/src/workers/background-agent-keeper.js +689 -0
  232. package/src/workers/background-agent-worker.js +365 -13
  233. package/src/workers/background-turn-bootstrap.js +170 -0
  234. package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
  235. package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
  236. package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
  237. package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
  238. package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
  239. package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
  240. package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
@@ -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";
@@ -37,6 +38,9 @@ import {
37
38
  isApprovalRequestEvent,
38
39
  } from "../../harness/remote-session-push.js";
39
40
  import { createRemoteSessionPushSender } from "../../harness/remote-session-push-senders.js";
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";
40
44
  import { RemoteSessionRelay } from "../remote-session-relay.js";
41
45
  import { handleRemoteSessionPublish } from "./remote-session-protocol.js";
42
46
  import { handleTaskDetail, handleTaskHistory } from "./task-protocol.js";
@@ -270,10 +274,37 @@ export class ChainlessChainWSServer extends EventEmitter {
270
274
  /** Org-policy constraints for Remote Sessions (scopes, device cap, TTL). */
271
275
  this.remoteSessionPolicy =
272
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 || {};
273
288
  /** Local authorization state for multi-device Remote Sessions. */
274
289
  this.remoteSessions =
275
290
  options.remoteSessionRegistry ||
276
- new RemoteSessionRegistry({ policy: this.remoteSessionPolicy });
291
+ new RemoteSessionRegistry({
292
+ policy: this.remoteSessionPolicy,
293
+ membershipCoordinator:
294
+ options.remoteMembershipCoordinator === null
295
+ ? null
296
+ : () => this._requireRemoteMembershipCoordinator(),
297
+ membershipAuthority:
298
+ options.remoteMembershipAuthority === null
299
+ ? null
300
+ : () =>
301
+ options.remoteMembershipAuthority ||
302
+ new DurableRemoteMembershipAuthority(
303
+ options.remoteMembershipStateFile
304
+ ? { filePath: options.remoteMembershipStateFile }
305
+ : {},
306
+ ),
307
+ });
277
308
  /**
278
309
  * Optional durable sink (JSONL) for the audit trail. Enabled only when a
279
310
  * sink is injected or CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE is set; the
@@ -330,6 +361,20 @@ export class ChainlessChainWSServer extends EventEmitter {
330
361
  this._clientCounter = 0;
331
362
  }
332
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
+
333
378
  /** Start the WebSocket server */
334
379
  start() {
335
380
  return new Promise((resolve, reject) => {
@@ -452,6 +497,7 @@ export class ChainlessChainWSServer extends EventEmitter {
452
497
  ip: clientIp,
453
498
  alive: true,
454
499
  authTimer: null,
500
+ membershipConnectionNonce: randomBytes(32).toString("base64url"),
455
501
  };
456
502
  this.clients.set(clientId, client);
457
503
 
@@ -502,16 +548,36 @@ export class ChainlessChainWSServer extends EventEmitter {
502
548
  // remaining clients finalizes itself).
503
549
  cleanupBgAttachments(client);
504
550
  this.clients.delete(clientId);
505
- for (const affected of this.remoteSessions.removeClient(clientId)) {
506
- if (affected.closed)
507
- this.remoteSessionCrypto.delete(affected.sessionId);
508
- if (affected.closed)
509
- this.remoteSessionPairingSecrets.delete(affected.sessionId);
551
+ try {
552
+ for (const affected of this.remoteSessions.removeClient(clientId)) {
553
+ if (affected.closed)
554
+ this.remoteSessionCrypto.delete(affected.sessionId);
555
+ if (affected.closed)
556
+ this.remoteSessionPairingSecrets.delete(affected.sessionId);
557
+ this.remoteSessionAudit?.record({
558
+ sessionId: affected.sessionId,
559
+ actor: clientId,
560
+ action: affected.closed ? "session.closed" : "device.disconnected",
561
+ detail: { reason: "disconnect" },
562
+ });
563
+ }
564
+ } catch (error) {
565
+ // A durable membership transition that cannot be committed must never
566
+ // be represented as a successful revoke/close. The socket is already
567
+ // gone (so this principal cannot send another frame), and every future
568
+ // durable authorization still re-checks the authority store.
510
569
  this.remoteSessionAudit?.record({
511
- sessionId: affected.sessionId,
570
+ sessionId: null,
512
571
  actor: clientId,
513
- action: affected.closed ? "session.closed" : "device.disconnected",
514
- detail: { reason: "disconnect" },
572
+ action: "membership.state-error",
573
+ detail: {
574
+ reason: "disconnect-transition-failed",
575
+ errorCode: error?.code || null,
576
+ },
577
+ });
578
+ this.emit("client-error", {
579
+ clientId,
580
+ error: error?.message || String(error),
515
581
  });
516
582
  }
517
583
  this.emit("disconnection", { clientId });
@@ -539,11 +605,12 @@ export class ChainlessChainWSServer extends EventEmitter {
539
605
  return this._dispatcher.dispatch(clientId, ws, message);
540
606
  }
541
607
 
542
- /** Read the same .claude settings rules used by the Agent Core. */
608
+ /** Read the same static + scoped authority used by the Agent Core. */
543
609
  async _handlePermissionRulesGet(id, ws) {
544
610
  try {
545
- const { loadSettings } = await import("../../lib/settings-loader.cjs");
546
- const loaded = loadSettings({ cwd: this.projectRoot });
611
+ const { loadPermissionAuthority } =
612
+ await import("../../lib/permission-authority.js");
613
+ const loaded = loadPermissionAuthority({ cwd: this.projectRoot });
547
614
  this._send(ws, {
548
615
  id,
549
616
  type: "permission-rules",
@@ -551,6 +618,8 @@ export class ChainlessChainWSServer extends EventEmitter {
551
618
  sources: loaded.sources,
552
619
  files: loaded.files,
553
620
  managed: loaded.managed || null,
621
+ managedFile: loaded.managedFile || null,
622
+ scoped: loaded.scoped || null,
554
623
  });
555
624
  } catch (error) {
556
625
  this._send(ws, {
@@ -1293,10 +1362,17 @@ export class ChainlessChainWSServer extends EventEmitter {
1293
1362
  data.sessionId,
1294
1363
  hostClientId,
1295
1364
  )) {
1296
- 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
+ }
1297
1373
  if (
1298
- member.clientId === hostClientId ||
1299
- !member.scopes.includes("observe")
1374
+ memberTransportId === hostClientId ||
1375
+ !memberView.scopes.includes("observe")
1300
1376
  ) {
1301
1377
  continue;
1302
1378
  }
@@ -1304,27 +1380,47 @@ export class ChainlessChainWSServer extends EventEmitter {
1304
1380
  // Independent of the WS mirror below — a relay-paired mobile app may
1305
1381
  // report a live socket while its process is suspended and will never
1306
1382
  // read the mirrored event until the user taps the push.
1307
- if (member.pushToken && isApprovalRequestEvent(data.type)) {
1308
- this._dispatchApprovalPush(remoteSession, member, data);
1383
+ if (memberView.pushToken && isApprovalRequestEvent(data.type)) {
1384
+ this._dispatchApprovalPush(remoteSession, memberView, data);
1309
1385
  }
1310
- const target = this.clients.get(member.clientId);
1386
+ const target = this.clients.get(memberTransportId);
1311
1387
  if (!target && this.remoteSessionRelay) {
1312
1388
  const crypto = this.remoteSessionCrypto.get(remoteSession.sessionId);
1313
1389
  if (crypto) {
1314
- this.remoteSessionRelay.sendEncrypted(
1315
- member.clientId,
1316
- crypto.encrypt(member.clientId, data),
1317
- );
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
+ }
1318
1403
  }
1319
1404
  continue;
1320
1405
  }
1321
1406
  if (!target) continue;
1322
- this._send(target.ws, {
1323
- type: "remote-session-event",
1324
- remoteSessionId: remoteSession.sessionId,
1325
- agentSessionId: remoteSession.agentSessionId,
1326
- event: data,
1327
- });
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
+ }
1328
1424
  }
1329
1425
  }
1330
1426
  }
@@ -1337,14 +1433,26 @@ export class ChainlessChainWSServer extends EventEmitter {
1337
1433
  _dispatchApprovalPush(remoteSession, member, data) {
1338
1434
  const dispatcher = this.remoteSessionPush;
1339
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;
1340
1448
  const requestId =
1341
1449
  data.requestId || data.approvalId || data.id || `approval:${data.type}`;
1342
1450
  dispatcher
1343
1451
  .dispatch({
1344
- token: member.pushToken,
1345
- provider: member.pushProvider,
1452
+ token: currentMember.pushToken,
1453
+ provider: currentMember.pushProvider,
1346
1454
  sessionId: remoteSession.sessionId,
1347
- clientId: member.clientId,
1455
+ clientId: currentMember.principalId || currentMember.clientId,
1348
1456
  dedupeKey: requestId,
1349
1457
  notification: {
1350
1458
  title: "Approval requested",
@@ -1358,7 +1466,7 @@ export class ChainlessChainWSServer extends EventEmitter {
1358
1466
  try {
1359
1467
  this.remoteSessions.registerPush(
1360
1468
  remoteSession.sessionId,
1361
- member.clientId,
1469
+ currentMember.principalId || currentMember.clientId,
1362
1470
  { token: null },
1363
1471
  );
1364
1472
  } catch {
@@ -1366,15 +1474,15 @@ export class ChainlessChainWSServer extends EventEmitter {
1366
1474
  }
1367
1475
  this.remoteSessionAudit?.record({
1368
1476
  sessionId: remoteSession.sessionId,
1369
- actor: member.clientId,
1477
+ actor: currentMember.principalId || currentMember.clientId,
1370
1478
  action: "push.unregistered",
1371
- detail: { provider: member.pushProvider || null },
1479
+ detail: { provider: currentMember.pushProvider || null },
1372
1480
  });
1373
1481
  return;
1374
1482
  }
1375
1483
  this.remoteSessionAudit?.record({
1376
1484
  sessionId: remoteSession.sessionId,
1377
- actor: member.clientId,
1485
+ actor: currentMember.principalId || currentMember.clientId,
1378
1486
  action:
1379
1487
  outcome.status === "sent"
1380
1488
  ? "push.sent"
@@ -1382,7 +1490,7 @@ export class ChainlessChainWSServer extends EventEmitter {
1382
1490
  ? "push.failed"
1383
1491
  : "push.skipped",
1384
1492
  detail: {
1385
- provider: member.pushProvider || null,
1493
+ provider: currentMember.pushProvider || null,
1386
1494
  reason: outcome.reason || null,
1387
1495
  error: outcome.error || null,
1388
1496
  },
@@ -1411,6 +1519,52 @@ export class ChainlessChainWSServer extends EventEmitter {
1411
1519
  });
1412
1520
  }
1413
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
+
1414
1568
  async _handleRemoteRelayMessage(message) {
1415
1569
  if (
1416
1570
  message?.type !== "message" ||
@@ -1421,21 +1575,312 @@ export class ChainlessChainWSServer extends EventEmitter {
1421
1575
  const sessionId = payload.envelope?.sessionId;
1422
1576
  const crypto = this.remoteSessionCrypto.get(sessionId);
1423
1577
  const token = this.remoteSessionPairingSecrets.get(sessionId);
1424
- if (!crypto || !token)
1578
+ if (!crypto)
1579
+ throw new Error("Remote Session pairing is unavailable or expired");
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) {
1425
1742
  throw new Error("Remote Session pairing is unavailable or expired");
1426
- crypto.pair(payload.mobilePeerId, payload.mobilePublicKey, token);
1427
- const join = crypto.decrypt(payload.envelope);
1428
- if (join.type !== "pair.join" || join.token !== token) {
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
+ ) {
1429
1790
  throw new Error("Invalid encrypted Remote Session pairing request");
1430
1791
  }
1431
- const relayMember = this.remoteSessions.join({
1432
- sessionId,
1433
- clientId: payload.mobilePeerId,
1434
- token: join.token,
1435
- via: "relay",
1436
- pushToken: join.pushToken,
1437
- pushProvider: join.pushProvider,
1438
- });
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
+ }
1439
1884
  this.remoteSessionAudit?.record({
1440
1885
  sessionId,
1441
1886
  actor: payload.mobilePeerId,
@@ -1443,14 +1888,24 @@ export class ChainlessChainWSServer extends EventEmitter {
1443
1888
  detail: {
1444
1889
  via: "relay",
1445
1890
  hasPush: relayMember?.member?.pushToken ? true : false,
1891
+ scopes: relayMember?.member?.scopes || null,
1892
+ capabilities: relayMember?.capabilities || [],
1446
1893
  },
1447
1894
  });
1448
- this.remoteSessionPairingSecrets.delete(sessionId);
1895
+ this._deleteRemoteSessionPairingSecretIfMatches(
1896
+ sessionId,
1897
+ remoteSessionPairingTokenDigest(join.token),
1898
+ );
1449
1899
  this.remoteSessionRelay.sendEncrypted(
1450
1900
  payload.mobilePeerId,
1451
1901
  crypto.encrypt(payload.mobilePeerId, {
1452
1902
  type: "pair.accepted",
1453
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 || [],
1454
1909
  }),
1455
1910
  );
1456
1911
  }
@@ -1464,6 +1919,11 @@ export class ChainlessChainWSServer extends EventEmitter {
1464
1919
  readyState: 1,
1465
1920
  send: (raw) => {
1466
1921
  const response = JSON.parse(raw);
1922
+ try {
1923
+ this.remoteSessions.authorize(envelope.sessionId, from, "observe");
1924
+ } catch {
1925
+ return;
1926
+ }
1467
1927
  this.remoteSessionRelay.sendEncrypted(
1468
1928
  from,
1469
1929
  crypto.encrypt(from, response),