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
@@ -7,6 +7,7 @@ export const REMOTE_SESSION_SCOPES = Object.freeze([
7
7
  "approve",
8
8
  "interrupt",
9
9
  ]);
10
+ export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
10
11
 
11
12
  const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
12
13
  const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -36,6 +37,25 @@ function normalizeScopes(scopes) {
36
37
  return unique;
37
38
  }
38
39
 
40
+ function normalizeCapabilities(capabilities) {
41
+ if (capabilities == null) return [];
42
+ if (!Array.isArray(capabilities) || capabilities.length > 32) {
43
+ throw new TypeError("Remote session capabilities must be a bounded array");
44
+ }
45
+ const normalized = [...new Set(capabilities)];
46
+ for (const capability of normalized) {
47
+ if (
48
+ typeof capability !== "string" ||
49
+ capability.length === 0 ||
50
+ capability.length > 128 ||
51
+ capability.includes("\0")
52
+ ) {
53
+ throw new TypeError("Remote session capability is invalid");
54
+ }
55
+ }
56
+ return normalized.sort();
57
+ }
58
+
39
59
  /**
40
60
  * Org-level constraints an administrator can impose on Remote Sessions. Lives
41
61
  * next to the registry (its single enforcement point) so both the direct WS
@@ -49,6 +69,7 @@ export class RemoteSessionPolicy {
49
69
  maxSessionTtlMs = null,
50
70
  maxTokenTtlMs = null,
51
71
  allowRelayPairing = true,
72
+ policyVersion = null,
52
73
  } = {}) {
53
74
  this.allowedScopes = allowedScopes ? [...new Set(allowedScopes)] : null;
54
75
  if (this.allowedScopes) {
@@ -68,6 +89,24 @@ export class RemoteSessionPolicy {
68
89
  this.maxTokenTtlMs =
69
90
  maxTokenTtlMs == null ? null : Math.max(1, Math.floor(maxTokenTtlMs));
70
91
  this.allowRelayPairing = allowRelayPairing !== false;
92
+ const authorityMaterial = JSON.stringify({
93
+ allowedScopes: this.allowedScopes ? [...this.allowedScopes].sort() : null,
94
+ allowRelayPairing: this.allowRelayPairing,
95
+ maxDevices: this.maxDevices,
96
+ });
97
+ this.policyVersion =
98
+ policyVersion == null
99
+ ? `remote-session-policy:sha256:${createHash("sha256")
100
+ .update(authorityMaterial, "utf8")
101
+ .digest("hex")}`
102
+ : String(policyVersion);
103
+ if (
104
+ this.policyVersion.length === 0 ||
105
+ this.policyVersion.length > 512 ||
106
+ this.policyVersion.includes("\0")
107
+ ) {
108
+ throw new TypeError("policyVersion must be a non-empty bounded string");
109
+ }
71
110
  }
72
111
 
73
112
  /**
@@ -119,9 +158,22 @@ export class RemoteSessionPolicy {
119
158
  maxSessionTtlMs: this.maxSessionTtlMs,
120
159
  maxTokenTtlMs: this.maxTokenTtlMs,
121
160
  allowRelayPairing: this.allowRelayPairing,
161
+ policyVersion: this.policyVersion,
122
162
  };
123
163
  }
124
164
 
165
+ authorityDescriptor() {
166
+ return Object.freeze({
167
+ allowedScopes:
168
+ this.allowedScopes === null
169
+ ? null
170
+ : Object.freeze([...this.allowedScopes].sort()),
171
+ maxDevices: this.maxDevices,
172
+ allowRelayPairing: this.allowRelayPairing,
173
+ policyVersion: this.policyVersion,
174
+ });
175
+ }
176
+
125
177
  static fromEnv(env = {}) {
126
178
  const options = {};
127
179
  const scopes = env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOWED_SCOPES;
@@ -163,10 +215,44 @@ function publicSession(session) {
163
215
  sessionId: session.sessionId,
164
216
  agentSessionId: session.agentSessionId,
165
217
  name: session.name,
166
- hostClientId: session.hostClientId,
218
+ hostClientId: session.hostPrincipalId || session.hostClientId,
219
+ ...(session.hostPrincipalId
220
+ ? {
221
+ hostPrincipalId: session.hostPrincipalId,
222
+ hostConnected: Boolean(session.hostClientId),
223
+ }
224
+ : {}),
167
225
  createdAt: session.createdAt,
168
226
  expiresAt: session.expiresAt,
169
227
  memberCount: session.members.size,
228
+ ...(session.membershipAuthority
229
+ ? {
230
+ membershipAuthority: session.membershipAuthority,
231
+ sessionEpoch: session.sessionEpoch,
232
+ }
233
+ : {}),
234
+ };
235
+ }
236
+
237
+ function transportBindingKey(sessionId, transportClientId) {
238
+ return JSON.stringify([String(sessionId), String(transportClientId)]);
239
+ }
240
+
241
+ function publicMember(member, session) {
242
+ return {
243
+ clientId: member.principalId || member.clientId,
244
+ ...(member.principalId ? { principalId: member.principalId } : {}),
245
+ scopes: [...member.scopes],
246
+ joinedAt: member.joinedAt,
247
+ pushToken: member.pushToken || null,
248
+ pushProvider: member.pushToken ? member.pushProvider || null : null,
249
+ ...(member.membershipEpoch
250
+ ? { membershipEpoch: member.membershipEpoch }
251
+ : {}),
252
+ ...(member.membershipQuarantined ? { membershipQuarantined: true } : {}),
253
+ ...(session?.hostPrincipalId
254
+ ? { connected: Boolean(member.transportClientId) }
255
+ : {}),
170
256
  };
171
257
  }
172
258
 
@@ -177,21 +263,276 @@ export class RemoteSessionRegistry {
177
263
  this.tokenTtlMs = options.tokenTtlMs || DEFAULT_TOKEN_TTL_MS;
178
264
  this.sessionTtlMs = options.sessionTtlMs || DEFAULT_SESSION_TTL_MS;
179
265
  this.policy = options.policy || new RemoteSessionPolicy();
266
+ // A factory keeps ordinary/in-memory Remote Session tests and deployments
267
+ // from touching the machine security store. Client-hosted approval bridges
268
+ // explicitly require this authority and trigger its lazy construction.
269
+ this._membershipAuthority = options.membershipAuthority ?? null;
270
+ this._membershipCoordinator = options.membershipCoordinator ?? null;
180
271
  this.sessions = new Map();
181
272
  this.tokens = new Map();
273
+ this._transportBindings = new Map();
274
+ this._pendingAuthentication = new Map();
275
+ this._coordinatorHydrated = false;
276
+ this._coordinatorHydrationError = null;
277
+ if (
278
+ this._membershipCoordinator &&
279
+ typeof this._membershipCoordinator !== "function"
280
+ ) {
281
+ try {
282
+ this._hydrateCoordinatorSessions(this._membershipCoordinator);
283
+ } catch (error) {
284
+ // Construction remains side-effect safe/retryable. Every durable use
285
+ // retries the authoritative read and still fails closed if it remains
286
+ // unavailable.
287
+ this._coordinatorHydrated = false;
288
+ this._coordinatorHydrationError = error;
289
+ }
290
+ }
291
+ }
292
+
293
+ _resolveMembershipCoordinator() {
294
+ if (typeof this._membershipCoordinator === "function") {
295
+ this._membershipCoordinator = this._membershipCoordinator();
296
+ }
297
+ if (
298
+ !this._membershipCoordinator ||
299
+ typeof this._membershipCoordinator.listSessionSnapshots !== "function" ||
300
+ typeof this._membershipCoordinator.getSessionSnapshot !== "function" ||
301
+ typeof this._membershipCoordinator.trustDescriptor !== "function"
302
+ ) {
303
+ throw new Error("Durable Remote membership coordinator is unavailable");
304
+ }
305
+ return this._membershipCoordinator;
306
+ }
307
+
308
+ _requireMembershipCoordinator() {
309
+ const coordinator = this._resolveMembershipCoordinator();
310
+ if (!this._coordinatorHydrated) {
311
+ this._hydrateCoordinatorSessions(coordinator);
312
+ }
313
+ return coordinator;
314
+ }
315
+
316
+ _hydrateCoordinatorSessions(coordinator) {
317
+ try {
318
+ const snapshots = coordinator.listSessionSnapshots({ activeOnly: false });
319
+ const live = new Set();
320
+ for (const snapshot of snapshots) {
321
+ if (snapshot.expiresAt <= this.now() || snapshot.status !== "active") {
322
+ this._clearCoordinatorSession(snapshot.sessionId);
323
+ continue;
324
+ }
325
+ live.add(snapshot.sessionId);
326
+ this._syncCoordinatorSnapshot(snapshot, coordinator);
327
+ }
328
+ for (const [sessionId, session] of this.sessions) {
329
+ if (session.hostPrincipalId && !live.has(sessionId)) {
330
+ this._clearCoordinatorSession(sessionId);
331
+ }
332
+ }
333
+ this._coordinatorHydrated = true;
334
+ this._coordinatorHydrationError = null;
335
+ } catch (error) {
336
+ this._coordinatorHydrated = false;
337
+ this._coordinatorHydrationError = error;
338
+ throw error;
339
+ }
340
+ }
341
+
342
+ _clearCoordinatorSession(sessionId) {
343
+ for (const [key, binding] of this._transportBindings) {
344
+ if (binding.sessionId === sessionId) this._transportBindings.delete(key);
345
+ }
346
+ for (const [challengeId, pending] of this._pendingAuthentication) {
347
+ if (pending.sessionId === sessionId) {
348
+ this._pendingAuthentication.delete(challengeId);
349
+ }
350
+ }
351
+ this.sessions.delete(sessionId);
352
+ this.tokens.delete(sessionId);
182
353
  }
183
354
 
184
- create({ hostClientId, agentSessionId, name, scopes } = {}) {
355
+ _refreshCoordinatorSession(sessionId) {
356
+ const coordinator = this._requireMembershipCoordinator();
357
+ const current = coordinator.getSessionSnapshot(sessionId);
358
+ if (
359
+ !current ||
360
+ current.session.status !== "active" ||
361
+ current.session.expiresAt <= this.now()
362
+ ) {
363
+ this._clearCoordinatorSession(sessionId);
364
+ return current?.session || null;
365
+ }
366
+ return this._syncCoordinatorSnapshot(current.session, coordinator);
367
+ }
368
+
369
+ _syncCoordinatorSnapshot(snapshot, coordinator = null) {
370
+ const authority = coordinator || this._resolveMembershipCoordinator();
371
+ const previous = this.sessions.get(snapshot.sessionId) || null;
372
+ const priorMembers = previous?.members || new Map();
373
+ for (const [key, binding] of this._transportBindings) {
374
+ if (binding.sessionId === snapshot.sessionId) {
375
+ this._transportBindings.delete(key);
376
+ }
377
+ }
378
+ const members = new Map();
379
+ for (const durableMember of snapshot.members) {
380
+ const prior = priorMembers.get(durableMember.principalId) || null;
381
+ const sameActiveMembership =
382
+ prior?.status === "active" &&
383
+ durableMember.status === "active" &&
384
+ prior.membershipEpoch === durableMember.membershipEpoch;
385
+ const member = {
386
+ principalId: durableMember.principalId,
387
+ clientId: durableMember.principalId,
388
+ scopes: [...durableMember.scopes],
389
+ joinedAt: prior?.joinedAt || snapshot.createdAt,
390
+ pushToken: sameActiveMembership ? prior.pushToken || null : null,
391
+ pushProvider:
392
+ sameActiveMembership && prior.pushToken
393
+ ? prior.pushProvider || null
394
+ : null,
395
+ membershipEpoch: durableMember.membershipEpoch,
396
+ status: durableMember.status,
397
+ transportClientId:
398
+ durableMember.status === "active"
399
+ ? sameActiveMembership
400
+ ? prior.transportClientId || null
401
+ : null
402
+ : null,
403
+ };
404
+ members.set(member.principalId, member);
405
+ if (member.transportClientId) {
406
+ this._transportBindings.set(
407
+ transportBindingKey(snapshot.sessionId, member.transportClientId),
408
+ { sessionId: snapshot.sessionId, principalId: member.principalId },
409
+ );
410
+ }
411
+ }
412
+ const host = members.get(snapshot.hostPrincipalId) || null;
413
+ const session = {
414
+ sessionId: snapshot.sessionId,
415
+ agentSessionId: snapshot.agentSessionId,
416
+ name: previous?.name || snapshot.agentSessionId,
417
+ hostPrincipalId: snapshot.hostPrincipalId,
418
+ hostClientId: host?.transportClientId || null,
419
+ createdAt: snapshot.createdAt,
420
+ expiresAt: snapshot.expiresAt,
421
+ status: snapshot.status,
422
+ members,
423
+ membershipAuthority: authority.trustDescriptor().authorityVersion,
424
+ sessionEpoch: snapshot.sessionEpoch,
425
+ authorityGeneration: snapshot.authorityGeneration,
426
+ leases: new Map(snapshot.leases.map((lease) => [lease.leaseId, lease])),
427
+ };
428
+ this.sessions.set(session.sessionId, session);
429
+ return session;
430
+ }
431
+
432
+ _attachTransport(session, principalId, transportClientId) {
433
+ const member = session.members.get(principalId);
434
+ if (!member || member.status !== "active") {
435
+ throw new Error("Remote membership principal is not active");
436
+ }
437
+ const occupied = this._transportBindings.get(
438
+ transportBindingKey(session.sessionId, transportClientId),
439
+ );
440
+ if (occupied && occupied.principalId !== principalId) {
441
+ throw new Error("Remote transport is already bound to another principal");
442
+ }
443
+ if (member.transportClientId) {
444
+ this._transportBindings.delete(
445
+ transportBindingKey(session.sessionId, member.transportClientId),
446
+ );
447
+ }
448
+ member.transportClientId = transportClientId;
449
+ this._transportBindings.set(
450
+ transportBindingKey(session.sessionId, transportClientId),
451
+ { sessionId: session.sessionId, principalId },
452
+ );
453
+ if (principalId === session.hostPrincipalId) {
454
+ session.hostClientId = transportClientId;
455
+ }
456
+ return member;
457
+ }
458
+
459
+ _principalForTransport(session, transportClientId) {
460
+ if (!session.hostPrincipalId) return transportClientId;
461
+ const binding = this._transportBindings.get(
462
+ transportBindingKey(session.sessionId, transportClientId),
463
+ );
464
+ return binding?.principalId || null;
465
+ }
466
+
467
+ _requireMembershipAuthority() {
468
+ if (typeof this._membershipAuthority === "function") {
469
+ this._membershipAuthority = this._membershipAuthority();
470
+ }
471
+ if (!this._membershipAuthority) {
472
+ throw new Error(
473
+ "Durable Remote membership authority is required for client-hosted approvals",
474
+ );
475
+ }
476
+ return this._membershipAuthority;
477
+ }
478
+
479
+ create({
480
+ hostClientId,
481
+ agentSessionId,
482
+ name,
483
+ scopes,
484
+ durableMembership = false,
485
+ hostCredentialPublicKeySpki = null,
486
+ } = {}) {
185
487
  if (!hostClientId) throw new Error("hostClientId is required");
186
488
  if (!agentSessionId) throw new Error("agentSessionId is required");
187
489
  const now = this.now();
490
+ const expiresAt = now + this.policy.capSessionTtl(this.sessionTtlMs);
491
+ const sessionId = randomUUID();
492
+ const coordinator =
493
+ durableMembership && this._membershipCoordinator
494
+ ? this._requireMembershipCoordinator()
495
+ : null;
496
+ const durable = coordinator
497
+ ? coordinator.createSession({
498
+ sessionId,
499
+ agentSessionId,
500
+ scopes: REMOTE_SESSION_SCOPES,
501
+ expiresAt,
502
+ hostCredentialPublicKeySpki,
503
+ joinPolicy: this.policy.authorityDescriptor(),
504
+ })
505
+ : durableMembership
506
+ ? this._requireMembershipAuthority().createSession({
507
+ sessionId,
508
+ agentSessionId,
509
+ hostPrincipalId: hostClientId,
510
+ scopes: REMOTE_SESSION_SCOPES,
511
+ expiresAt,
512
+ })
513
+ : null;
514
+ if (coordinator) {
515
+ const session = this._syncCoordinatorSnapshot(durable.statement.payload);
516
+ session.name = name || agentSessionId;
517
+ return {
518
+ session: publicSession(session),
519
+ pairing: this.issuePairingToken(session.sessionId, { scopes }),
520
+ bootstrap: Object.freeze({
521
+ trust: durable.trust,
522
+ statement: durable.statement,
523
+ hostPrincipalId: durable.hostPrincipalId,
524
+ membershipEpoch: durable.membershipEpoch,
525
+ hostCredentialPublicKeySpki: durable.hostCredentialPublicKeySpki,
526
+ }),
527
+ };
528
+ }
188
529
  const session = {
189
- sessionId: randomUUID(),
530
+ sessionId,
190
531
  agentSessionId,
191
532
  name: name || agentSessionId,
192
533
  hostClientId,
193
534
  createdAt: now,
194
- expiresAt: now + this.policy.capSessionTtl(this.sessionTtlMs),
535
+ expiresAt,
195
536
  members: new Map([
196
537
  [
197
538
  hostClientId,
@@ -199,9 +540,16 @@ export class RemoteSessionRegistry {
199
540
  clientId: hostClientId,
200
541
  scopes: REMOTE_SESSION_SCOPES,
201
542
  joinedAt: now,
543
+ ...(durable ? { membershipEpoch: durable.membershipEpoch } : {}),
202
544
  },
203
545
  ],
204
546
  ]),
547
+ ...(durable
548
+ ? {
549
+ membershipAuthority: durable.authorityVersion,
550
+ sessionEpoch: durable.sessionEpoch,
551
+ }
552
+ : {}),
205
553
  };
206
554
  this.sessions.set(session.sessionId, session);
207
555
  return {
@@ -210,6 +558,312 @@ export class RemoteSessionRegistry {
210
558
  };
211
559
  }
212
560
 
561
+ pairingInvitation(sessionId, token) {
562
+ const session = this.requireSession(sessionId);
563
+ const pairing = this.tokens.get(sessionId);
564
+ if (!pairing || pairing.expiresAt <= this.now()) {
565
+ this.tokens.delete(sessionId);
566
+ throw new Error("Pairing token is missing or expired");
567
+ }
568
+ if (!tokensMatch(token, pairing.digest)) {
569
+ throw new Error("Invalid pairing token");
570
+ }
571
+ return Object.freeze({
572
+ session,
573
+ scopes: Object.freeze([...pairing.scopes]),
574
+ expiresAt: pairing.expiresAt,
575
+ });
576
+ }
577
+
578
+ issueMemberJoinChallenge({
579
+ sessionId,
580
+ transportClientId,
581
+ token,
582
+ credentialPublicKey,
583
+ connectionNonce,
584
+ capabilities = null,
585
+ } = {}) {
586
+ const invitation = this.pairingInvitation(sessionId, token);
587
+ const session = invitation.session;
588
+ if (!session.hostPrincipalId) {
589
+ throw new Error("Durable direct membership challenge is unavailable");
590
+ }
591
+ const deviceCount = [...session.members.values()].filter(
592
+ (member) =>
593
+ member.principalId !== session.hostPrincipalId &&
594
+ member.status === "active",
595
+ ).length;
596
+ this.policy.enforceJoin({ deviceCount, via: "direct" });
597
+ const challenge =
598
+ this._requireMembershipCoordinator().issueMemberJoinChallenge({
599
+ sessionId,
600
+ expectedSessionEpoch: session.sessionEpoch,
601
+ scopes: invitation.scopes,
602
+ credentialPublicKey,
603
+ connectionNonce,
604
+ ttlMs: Math.max(1, invitation.expiresAt - this.now()),
605
+ capabilities,
606
+ joinPolicy: this.policy.authorityDescriptor(),
607
+ });
608
+ this._pendingAuthentication.set(challenge.challengeId, {
609
+ purpose: "member.join",
610
+ sessionId,
611
+ transportClientId,
612
+ pairingDigest: Buffer.from(this.tokens.get(sessionId).digest),
613
+ pairingExpiresAt: invitation.expiresAt,
614
+ });
615
+ return challenge;
616
+ }
617
+
618
+ completeMemberJoin({
619
+ challengeId,
620
+ transportClientId,
621
+ connectionNonce,
622
+ signature,
623
+ pushToken = null,
624
+ pushProvider = null,
625
+ } = {}) {
626
+ const pending = this._pendingAuthentication.get(challengeId);
627
+ this._pendingAuthentication.delete(challengeId);
628
+ if (
629
+ !pending ||
630
+ pending.purpose !== "member.join" ||
631
+ pending.transportClientId !== transportClientId ||
632
+ pending.pairingExpiresAt <= this.now()
633
+ ) {
634
+ throw new Error("Remote membership join proof is stale");
635
+ }
636
+ const currentPairing = this.tokens.get(pending.sessionId);
637
+ if (
638
+ !currentPairing ||
639
+ currentPairing.digest.length !== pending.pairingDigest.length ||
640
+ !timingSafeEqual(currentPairing.digest, pending.pairingDigest)
641
+ ) {
642
+ throw new Error("Remote membership pairing invitation changed");
643
+ }
644
+ const joined = this._requireMembershipCoordinator().joinMember({
645
+ challengeId,
646
+ connectionNonce,
647
+ signature,
648
+ });
649
+ this.tokens.delete(pending.sessionId);
650
+ const session = this._syncCoordinatorSnapshot(joined.statement.payload);
651
+ const member = this._attachTransport(
652
+ session,
653
+ joined.principalId,
654
+ transportClientId,
655
+ );
656
+ member.pushToken = pushToken || null;
657
+ member.pushProvider = pushToken ? pushProvider || null : null;
658
+ return {
659
+ session: publicSession(session),
660
+ member: publicMember(member, session),
661
+ nextConnectionNonce: joined.nextConnectionNonce,
662
+ statement: joined.statement,
663
+ };
664
+ }
665
+
666
+ issueSessionResumeChallenge({
667
+ sessionId,
668
+ principalId,
669
+ transportClientId,
670
+ connectionNonce,
671
+ } = {}) {
672
+ const challenge =
673
+ this._requireMembershipCoordinator().issueSessionResumeChallenge({
674
+ sessionId,
675
+ principalId,
676
+ connectionNonce,
677
+ });
678
+ this._pendingAuthentication.set(challenge.challengeId, {
679
+ purpose: "session.resume",
680
+ sessionId,
681
+ principalId,
682
+ transportClientId,
683
+ });
684
+ return challenge;
685
+ }
686
+
687
+ completeSessionResume({
688
+ challengeId,
689
+ transportClientId,
690
+ connectionNonce,
691
+ signature,
692
+ } = {}) {
693
+ const pending = this._pendingAuthentication.get(challengeId);
694
+ this._pendingAuthentication.delete(challengeId);
695
+ if (
696
+ !pending ||
697
+ pending.purpose !== "session.resume" ||
698
+ pending.transportClientId !== transportClientId
699
+ ) {
700
+ throw new Error("Remote membership resume proof is stale");
701
+ }
702
+ const resumed = this._requireMembershipCoordinator().resumeSession({
703
+ challengeId,
704
+ connectionNonce,
705
+ signature,
706
+ });
707
+ if (
708
+ resumed.session.sessionId !== pending.sessionId ||
709
+ resumed.principalId !== pending.principalId
710
+ ) {
711
+ throw new Error("Remote membership resume binding changed");
712
+ }
713
+ const session = this._syncCoordinatorSnapshot(resumed.session);
714
+ const member = this._attachTransport(
715
+ session,
716
+ resumed.principalId,
717
+ transportClientId,
718
+ );
719
+ return {
720
+ session: publicSession(session),
721
+ member: publicMember(member, session),
722
+ nextConnectionNonce: resumed.nextConnectionNonce,
723
+ statement: resumed.statement,
724
+ trust: resumed.trust,
725
+ };
726
+ }
727
+
728
+ issueSessionReenableChallenge({
729
+ sessionId,
730
+ principalId,
731
+ transportClientId,
732
+ connectionNonce,
733
+ newHostCredentialPublicKeySpki = null,
734
+ } = {}) {
735
+ const coordinator = this._requireMembershipCoordinator();
736
+ const challenge = coordinator.issueSessionReenableChallenge({
737
+ sessionId,
738
+ principalId,
739
+ connectionNonce,
740
+ newHostCredentialPublicKeySpki,
741
+ scopes: REMOTE_SESSION_SCOPES,
742
+ expiresAt: this.now() + this.policy.capSessionTtl(this.sessionTtlMs),
743
+ joinPolicy: this.policy.authorityDescriptor(),
744
+ });
745
+ this._pendingAuthentication.set(challenge.challengeId, {
746
+ purpose: "session.reenable",
747
+ sessionId,
748
+ principalId,
749
+ nextPrincipalId: challenge.nextPrincipalId,
750
+ transportClientId,
751
+ });
752
+ return challenge;
753
+ }
754
+
755
+ completeSessionReenable({
756
+ challengeId,
757
+ transportClientId,
758
+ connectionNonce,
759
+ signature,
760
+ } = {}) {
761
+ const pending = this._pendingAuthentication.get(challengeId);
762
+ this._pendingAuthentication.delete(challengeId);
763
+ if (
764
+ !pending ||
765
+ pending.purpose !== "session.reenable" ||
766
+ pending.transportClientId !== transportClientId
767
+ ) {
768
+ throw new Error("Remote membership re-enable proof is stale");
769
+ }
770
+ const reenabled = this._requireMembershipCoordinator().reenableSession({
771
+ challengeId,
772
+ connectionNonce,
773
+ signature,
774
+ });
775
+ if (
776
+ reenabled.sessionId !== pending.sessionId ||
777
+ reenabled.hostPrincipalId !== pending.nextPrincipalId
778
+ ) {
779
+ throw new Error("Remote membership re-enable binding changed");
780
+ }
781
+ const session = this._syncCoordinatorSnapshot(reenabled.statement.payload);
782
+ const member = this._attachTransport(
783
+ session,
784
+ reenabled.hostPrincipalId,
785
+ transportClientId,
786
+ );
787
+ return {
788
+ session: publicSession(session),
789
+ member: publicMember(member, session),
790
+ nextConnectionNonce: reenabled.nextConnectionNonce,
791
+ statement: reenabled.statement,
792
+ trust: reenabled.trust,
793
+ };
794
+ }
795
+
796
+ terminalSnapshot(sessionId) {
797
+ const current =
798
+ this._requireMembershipCoordinator().getSessionSnapshot(sessionId);
799
+ if (!current || current.session.status !== "closed") {
800
+ throw new Error("Remote membership session has no terminal tombstone");
801
+ }
802
+ this._clearCoordinatorSession(sessionId);
803
+ return current;
804
+ }
805
+
806
+ joinRelayMember({
807
+ sessionId,
808
+ transportClientId,
809
+ token,
810
+ mobilePublicKey,
811
+ pairingTokenDigest,
812
+ possessionCapability,
813
+ capabilities = null,
814
+ pushToken = null,
815
+ pushProvider = null,
816
+ } = {}) {
817
+ const invitation = this.pairingInvitation(sessionId, token);
818
+ const session = invitation.session;
819
+ if (!session.hostPrincipalId) {
820
+ throw new Error("Durable relay membership is unavailable");
821
+ }
822
+ const deviceCount = [...session.members.values()].filter(
823
+ (member) =>
824
+ member.principalId !== session.hostPrincipalId &&
825
+ member.status === "active",
826
+ ).length;
827
+ this.policy.enforceJoin({ deviceCount, via: "relay" });
828
+ const negotiatedCapabilities = normalizeCapabilities(capabilities);
829
+ const negotiatedScopes = invitation.scopes.filter(
830
+ (scope) =>
831
+ scope !== "approve" ||
832
+ negotiatedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY),
833
+ );
834
+ if (negotiatedScopes.length === 0) {
835
+ throw new Error(
836
+ "Remote relay capabilities do not authorize any requested scope",
837
+ );
838
+ }
839
+ const joined = this._requireMembershipCoordinator().joinRelayMember({
840
+ sessionId,
841
+ expectedSessionEpoch: session.sessionEpoch,
842
+ scopes: negotiatedScopes,
843
+ mobilePeerId: transportClientId,
844
+ mobilePublicKey,
845
+ pairingTokenDigest,
846
+ possessionCapability,
847
+ capabilities: negotiatedCapabilities,
848
+ joinPolicy: this.policy.authorityDescriptor(),
849
+ });
850
+ this.tokens.delete(sessionId);
851
+ const updated = this._syncCoordinatorSnapshot(joined.statement.payload);
852
+ const member = this._attachTransport(
853
+ updated,
854
+ joined.principalId,
855
+ transportClientId,
856
+ );
857
+ member.pushToken = pushToken || null;
858
+ member.pushProvider = pushToken ? pushProvider || null : null;
859
+ return {
860
+ session: publicSession(updated),
861
+ member: publicMember(member, updated),
862
+ statement: joined.statement,
863
+ capabilities: Object.freeze([...negotiatedCapabilities]),
864
+ };
865
+ }
866
+
213
867
  issuePairingToken(sessionId, { scopes } = {}) {
214
868
  const session = this.requireSession(sessionId);
215
869
  const token = randomBytes(32).toString("base64url");
@@ -243,6 +897,11 @@ export class RemoteSessionRegistry {
243
897
  } = {}) {
244
898
  if (!clientId) throw new Error("clientId is required");
245
899
  const session = this.requireSession(sessionId);
900
+ if (session.hostPrincipalId) {
901
+ throw new Error(
902
+ "Durable membership requires a possession-proof join protocol",
903
+ );
904
+ }
246
905
  const pairing = this.tokens.get(sessionId);
247
906
  if (!pairing || pairing.expiresAt <= this.now()) {
248
907
  this.tokens.delete(sessionId);
@@ -251,6 +910,9 @@ export class RemoteSessionRegistry {
251
910
  if (!tokensMatch(token, pairing.digest)) {
252
911
  throw new Error("Invalid pairing token");
253
912
  }
913
+ if (clientId === session.hostClientId) {
914
+ throw new Error("Cannot replace the host membership through pairing");
915
+ }
254
916
  // Org policy is enforced only after the token proves the caller is invited,
255
917
  // so a device-limit / relay-disabled message never leaks to unauthenticated
256
918
  // probes. Existing (non-host) devices count against the limit.
@@ -258,8 +920,17 @@ export class RemoteSessionRegistry {
258
920
  (member) => member.clientId !== session.hostClientId,
259
921
  ).length;
260
922
  this.policy.enforceJoin({ deviceCount, via });
923
+ const durable = session.membershipAuthority
924
+ ? this._requireMembershipAuthority().joinMember({
925
+ sessionId,
926
+ principalId: clientId,
927
+ scopes: pairing.scopes,
928
+ expectedSessionEpoch: session.sessionEpoch,
929
+ })
930
+ : null;
261
931
  // Pairing credentials are deliberately one-time. The host must explicitly
262
- // issue another token for each additional device.
932
+ // issue another token for each additional device. For durable sessions the
933
+ // authority commit happens first, so a persistence failure stays retryable.
263
934
  this.tokens.delete(sessionId);
264
935
  const member = {
265
936
  clientId,
@@ -267,6 +938,7 @@ export class RemoteSessionRegistry {
267
938
  joinedAt: this.now(),
268
939
  pushToken: pushToken || null,
269
940
  pushProvider: pushToken ? pushProvider || null : null,
941
+ ...(durable ? { membershipEpoch: durable.membershipEpoch } : {}),
270
942
  };
271
943
  session.members.set(clientId, member);
272
944
  return { session: publicSession(session), member: { ...member } };
@@ -279,8 +951,9 @@ export class RemoteSessionRegistry {
279
951
  */
280
952
  registerPush(sessionId, clientId, { token = null, provider = null } = {}) {
281
953
  const session = this.requireSession(sessionId);
282
- const member = session.members.get(clientId);
283
- if (!member) {
954
+ const principalId = this._principalForTransport(session, clientId);
955
+ const member = session.members.get(principalId || clientId);
956
+ if (!member || (member.status && member.status !== "active")) {
284
957
  throw new Error("Device is not paired with this remote session");
285
958
  }
286
959
  member.pushToken = token || null;
@@ -297,11 +970,25 @@ export class RemoteSessionRegistry {
297
970
  const session = this.requireSession(sessionId);
298
971
  const targets = [];
299
972
  for (const member of session.members.values()) {
300
- if (member.clientId === session.hostClientId) continue;
301
- if (member.clientId === excludeClientId) continue;
973
+ if (member.status && member.status !== "active") continue;
974
+ if (
975
+ (session.hostPrincipalId &&
976
+ member.principalId === session.hostPrincipalId) ||
977
+ (!session.hostPrincipalId && member.clientId === session.hostClientId)
978
+ )
979
+ continue;
980
+ if (
981
+ excludeClientId &&
982
+ (member.clientId === excludeClientId ||
983
+ member.transportClientId === excludeClientId)
984
+ )
985
+ continue;
302
986
  if (!member.pushToken) continue;
303
987
  targets.push({
304
988
  clientId: member.clientId,
989
+ ...(session.hostPrincipalId
990
+ ? { transportClientId: member.transportClientId || null }
991
+ : {}),
305
992
  pushToken: member.pushToken,
306
993
  pushProvider: member.pushProvider,
307
994
  });
@@ -311,18 +998,71 @@ export class RemoteSessionRegistry {
311
998
 
312
999
  authorize(sessionId, clientId, scope) {
313
1000
  const session = this.requireSession(sessionId);
314
- const member = session.members.get(clientId);
1001
+ if (session.membershipQuarantined) {
1002
+ throw new Error(
1003
+ "Remote membership session is quarantined after an unknown durable transition",
1004
+ );
1005
+ }
1006
+ const principalId = this._principalForTransport(session, clientId);
1007
+ const member = session.members.get(principalId || clientId);
315
1008
  if (!member)
316
1009
  throw new Error("Client is not paired with this remote session");
1010
+ if (member.status && member.status !== "active") {
1011
+ throw new Error("Remote membership principal is not active");
1012
+ }
1013
+ if (member.membershipQuarantined) {
1014
+ throw new Error(
1015
+ "Remote membership is quarantined after an unknown durable transition",
1016
+ );
1017
+ }
317
1018
  if (!member.scopes.includes(scope)) {
318
1019
  throw new Error(`Remote session scope required: ${scope}`);
319
1020
  }
320
- return { session, member };
1021
+ let membership = null;
1022
+ if (session.hostPrincipalId) {
1023
+ const verdict = this._requireMembershipCoordinator().readMembership(
1024
+ {
1025
+ sessionId,
1026
+ principalId: member.principalId,
1027
+ sessionEpoch: session.sessionEpoch,
1028
+ membershipEpoch: member.membershipEpoch,
1029
+ },
1030
+ scope,
1031
+ );
1032
+ if (!verdict.ok) {
1033
+ throw new Error(
1034
+ `Remote membership coordinator denied: ${verdict.reason}`,
1035
+ );
1036
+ }
1037
+ membership = verdict.binding;
1038
+ } else if (session.membershipAuthority) {
1039
+ const verdict = this._requireMembershipAuthority().readMembership(
1040
+ {
1041
+ sessionId,
1042
+ principalId: clientId,
1043
+ sessionEpoch: session.sessionEpoch,
1044
+ membershipEpoch: member.membershipEpoch,
1045
+ },
1046
+ scope,
1047
+ );
1048
+ if (!verdict.ok) {
1049
+ throw new Error(
1050
+ `Remote membership authority denied: ${verdict.reason}`,
1051
+ );
1052
+ }
1053
+ membership = verdict.binding;
1054
+ }
1055
+ return { session, member, membership };
321
1056
  }
322
1057
 
323
1058
  members(sessionId) {
324
1059
  const session = this.requireSession(sessionId);
325
- return [...session.members.values()].map((member) => ({ ...member }));
1060
+ return [...session.members.values()]
1061
+ .filter((member) => !member.status || member.status === "active")
1062
+ .map((member) => ({
1063
+ ...publicMember(member, session),
1064
+ transportClientId: member.transportClientId || member.clientId,
1065
+ }));
326
1066
  }
327
1067
 
328
1068
  /**
@@ -331,18 +1071,33 @@ export class RemoteSessionRegistry {
331
1071
  */
332
1072
  listDevices(sessionId, hostClientId) {
333
1073
  const session = this.requireSession(sessionId);
334
- if (hostClientId && session.hostClientId !== hostClientId) {
1074
+ const hostPrincipal = hostClientId
1075
+ ? this._principalForTransport(session, hostClientId)
1076
+ : null;
1077
+ if (
1078
+ hostClientId &&
1079
+ (session.hostPrincipalId
1080
+ ? hostPrincipal !== session.hostPrincipalId
1081
+ : session.hostClientId !== hostClientId)
1082
+ ) {
335
1083
  throw new Error("Only the host can list paired devices");
336
1084
  }
337
1085
  return {
338
1086
  session: publicSession(session),
339
1087
  devices: [...session.members.values()].map((member) => ({
340
- clientId: member.clientId,
1088
+ clientId: member.principalId || member.clientId,
1089
+ ...(member.principalId ? { principalId: member.principalId } : {}),
341
1090
  scopes: [...member.scopes],
342
1091
  joinedAt: member.joinedAt,
343
- isHost: member.clientId === session.hostClientId,
1092
+ isHost: session.hostPrincipalId
1093
+ ? member.principalId === session.hostPrincipalId
1094
+ : member.clientId === session.hostClientId,
1095
+ ...(session.hostPrincipalId
1096
+ ? { connected: Boolean(member.transportClientId) }
1097
+ : {}),
344
1098
  hasPush: Boolean(member.pushToken),
345
1099
  pushProvider: member.pushProvider || null,
1100
+ ...(member.status ? { status: member.status } : {}),
346
1101
  })),
347
1102
  };
348
1103
  }
@@ -355,22 +1110,86 @@ export class RemoteSessionRegistry {
355
1110
  revokeMember(sessionId, hostClientId, clientId) {
356
1111
  if (!clientId) throw new Error("clientId is required");
357
1112
  const session = this.requireSession(sessionId);
358
- if (session.hostClientId !== hostClientId) {
1113
+ const hostPrincipal = this._principalForTransport(session, hostClientId);
1114
+ if (
1115
+ session.hostPrincipalId
1116
+ ? hostPrincipal !== session.hostPrincipalId
1117
+ : session.hostClientId !== hostClientId
1118
+ ) {
359
1119
  throw new Error("Only the host can revoke devices");
360
1120
  }
361
- if (clientId === session.hostClientId) {
1121
+ if (clientId === (session.hostPrincipalId || session.hostClientId)) {
362
1122
  throw new Error("Cannot revoke the host device");
363
1123
  }
364
1124
  const member = session.members.get(clientId);
365
1125
  if (!member)
366
1126
  throw new Error("Device is not paired with this remote session");
367
- session.members.delete(clientId);
368
- return { session: publicSession(session), member: { ...member } };
1127
+ const notificationTransportId = member.transportClientId || member.clientId;
1128
+ let resultSession = session;
1129
+ let resultMember = member;
1130
+ let membershipStatement = null;
1131
+ if (session.hostPrincipalId) {
1132
+ try {
1133
+ const host = session.members.get(session.hostPrincipalId);
1134
+ const revoked = this._requireMembershipCoordinator().revokeMember({
1135
+ sessionId,
1136
+ principalId: clientId,
1137
+ hostPrincipalId: session.hostPrincipalId,
1138
+ expectedSessionEpoch: session.sessionEpoch,
1139
+ expectedMembershipEpoch: member.membershipEpoch,
1140
+ expectedHostMembershipEpoch: host.membershipEpoch,
1141
+ });
1142
+ resultSession = this._syncCoordinatorSnapshot(
1143
+ revoked.statement.payload,
1144
+ );
1145
+ resultMember = resultSession.members.get(clientId);
1146
+ membershipStatement = revoked.statement;
1147
+ } catch (error) {
1148
+ member.membershipQuarantined = true;
1149
+ throw error;
1150
+ }
1151
+ } else if (session.membershipAuthority) {
1152
+ try {
1153
+ this._requireMembershipAuthority().revokeMember({
1154
+ sessionId,
1155
+ principalId: clientId,
1156
+ expectedSessionEpoch: session.sessionEpoch,
1157
+ expectedMembershipEpoch: member.membershipEpoch,
1158
+ });
1159
+ } catch (error) {
1160
+ // The durable outcome may be unknown. Keep the record for diagnostics,
1161
+ // but locally quarantine it so no later frame can authorize.
1162
+ member.membershipQuarantined = true;
1163
+ throw error;
1164
+ }
1165
+ }
1166
+ if (!session.hostPrincipalId) session.members.delete(clientId);
1167
+ return {
1168
+ session: publicSession(resultSession),
1169
+ member: {
1170
+ ...publicMember(resultMember, resultSession),
1171
+ transportClientId: notificationTransportId,
1172
+ },
1173
+ ...(membershipStatement ? { statement: membershipStatement } : {}),
1174
+ };
369
1175
  }
370
1176
 
371
1177
  findHosted(agentSessionId, hostClientId) {
1178
+ if (this._membershipCoordinator) {
1179
+ // Enumeration cannot rely on the one-time startup hydration: another
1180
+ // server may have created, closed, or re-enabled a durable session since
1181
+ // this process started. Refresh the coordinator index before matching
1182
+ // local transport attachments.
1183
+ this._hydrateCoordinatorSessions(this._resolveMembershipCoordinator());
1184
+ }
372
1185
  const matches = [];
373
- for (const session of this.sessions.values()) {
1186
+ for (const sessionId of [...this.sessions.keys()]) {
1187
+ let session;
1188
+ try {
1189
+ session = this.requireSession(sessionId);
1190
+ } catch {
1191
+ continue;
1192
+ }
374
1193
  if (
375
1194
  session.agentSessionId === agentSessionId &&
376
1195
  session.hostClientId === hostClientId &&
@@ -385,12 +1204,56 @@ export class RemoteSessionRegistry {
385
1204
  removeClient(clientId) {
386
1205
  const affected = [];
387
1206
  for (const [sessionId, session] of this.sessions) {
388
- if (!session.members.delete(clientId)) continue;
1207
+ const principalId = this._principalForTransport(session, clientId);
1208
+ const member = session.members.get(principalId || clientId);
1209
+ if (!member) continue;
1210
+ if (session.hostPrincipalId) {
1211
+ this._transportBindings.delete(
1212
+ transportBindingKey(sessionId, clientId),
1213
+ );
1214
+ member.transportClientId = null;
1215
+ if (member.principalId === session.hostPrincipalId) {
1216
+ session.hostClientId = null;
1217
+ }
1218
+ affected.push({
1219
+ sessionId,
1220
+ closed: false,
1221
+ detached: true,
1222
+ principalId: member.principalId,
1223
+ });
1224
+ continue;
1225
+ }
389
1226
  if (session.hostClientId === clientId) {
1227
+ if (session.membershipAuthority) {
1228
+ try {
1229
+ this._requireMembershipAuthority().closeSession({
1230
+ sessionId,
1231
+ hostPrincipalId: clientId,
1232
+ expectedSessionEpoch: session.sessionEpoch,
1233
+ });
1234
+ } catch (error) {
1235
+ session.membershipQuarantined = true;
1236
+ throw error;
1237
+ }
1238
+ }
390
1239
  this.sessions.delete(sessionId);
391
1240
  this.tokens.delete(sessionId);
392
1241
  affected.push({ sessionId, closed: true });
393
1242
  } else {
1243
+ if (session.membershipAuthority) {
1244
+ try {
1245
+ this._requireMembershipAuthority().revokeMember({
1246
+ sessionId,
1247
+ principalId: clientId,
1248
+ expectedSessionEpoch: session.sessionEpoch,
1249
+ expectedMembershipEpoch: member.membershipEpoch,
1250
+ });
1251
+ } catch (error) {
1252
+ member.membershipQuarantined = true;
1253
+ throw error;
1254
+ }
1255
+ }
1256
+ session.members.delete(clientId);
394
1257
  affected.push({ sessionId, closed: false });
395
1258
  }
396
1259
  }
@@ -399,18 +1262,76 @@ export class RemoteSessionRegistry {
399
1262
 
400
1263
  close(sessionId, clientId) {
401
1264
  const session = this.requireSession(sessionId);
402
- if (session.hostClientId !== clientId) {
1265
+ const hostPrincipal = this._principalForTransport(session, clientId);
1266
+ if (
1267
+ session.hostPrincipalId
1268
+ ? hostPrincipal !== session.hostPrincipalId
1269
+ : session.hostClientId !== clientId
1270
+ ) {
403
1271
  throw new Error("Only the host can close a Remote Session");
404
1272
  }
1273
+ let durableClose = null;
1274
+ if (session.hostPrincipalId) {
1275
+ try {
1276
+ const host = session.members.get(session.hostPrincipalId);
1277
+ durableClose = this._requireMembershipCoordinator().closeSession({
1278
+ sessionId,
1279
+ hostPrincipalId: session.hostPrincipalId,
1280
+ expectedSessionEpoch: session.sessionEpoch,
1281
+ expectedHostMembershipEpoch: host.membershipEpoch,
1282
+ });
1283
+ } catch (error) {
1284
+ session.membershipQuarantined = true;
1285
+ throw error;
1286
+ }
1287
+ } else if (session.membershipAuthority) {
1288
+ try {
1289
+ this._requireMembershipAuthority().closeSession({
1290
+ sessionId,
1291
+ hostPrincipalId: clientId,
1292
+ expectedSessionEpoch: session.sessionEpoch,
1293
+ });
1294
+ } catch (error) {
1295
+ session.membershipQuarantined = true;
1296
+ throw error;
1297
+ }
1298
+ }
405
1299
  this.sessions.delete(sessionId);
406
1300
  this.tokens.delete(sessionId);
407
- return publicSession(session);
1301
+ const closedSession = publicSession(session);
1302
+ return durableClose
1303
+ ? {
1304
+ session: closedSession,
1305
+ statement: durableClose.statement,
1306
+ terminal: durableClose.terminal === true,
1307
+ alreadyClosed: durableClose.alreadyClosed === true,
1308
+ }
1309
+ : closedSession;
408
1310
  }
409
1311
 
410
1312
  requireSession(sessionId) {
411
- const session = this.sessions.get(sessionId);
1313
+ let session = this.sessions.get(sessionId) || null;
1314
+ if ((session?.hostPrincipalId || !session) && this._membershipCoordinator) {
1315
+ const durable = this._refreshCoordinatorSession(sessionId);
1316
+ if (durable?.status && durable.status !== "active") {
1317
+ throw new Error("Remote session is durably closed");
1318
+ }
1319
+ session = this.sessions.get(sessionId) || null;
1320
+ }
412
1321
  if (!session) throw new Error("Remote session not found");
413
1322
  if (session.expiresAt <= this.now()) {
1323
+ if (session.membershipAuthority && !session.hostPrincipalId) {
1324
+ try {
1325
+ this._requireMembershipAuthority().closeSession({
1326
+ sessionId,
1327
+ hostPrincipalId: session.hostClientId,
1328
+ expectedSessionEpoch: session.sessionEpoch,
1329
+ });
1330
+ } catch (error) {
1331
+ session.membershipQuarantined = true;
1332
+ throw error;
1333
+ }
1334
+ }
414
1335
  this.sessions.delete(sessionId);
415
1336
  this.tokens.delete(sessionId);
416
1337
  throw new Error("Remote session expired");