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
@@ -12,6 +12,16 @@ import {
12
12
 
13
13
  const PROTOCOL = "chainlesschain.remote-session.e2ee.v1";
14
14
  const PAIRING_SCHEME = "chainlesschain://remote-session/pair#";
15
+ const RELAY_POSSESSION_SCHEMA =
16
+ "chainlesschain.remote-session.relay-possession-capability/v1";
17
+ const relayPossessionCapabilities = new WeakMap();
18
+ const stagedRelayPairings = new WeakMap();
19
+ const RELAY_AUTHORITY_SCOPES = new Set([
20
+ "observe",
21
+ "prompt",
22
+ "approve",
23
+ "interrupt",
24
+ ]);
15
25
 
16
26
  function b64(value) {
17
27
  return Buffer.from(value).toString("base64url");
@@ -34,6 +44,229 @@ function importPublicKey(value) {
34
44
  return createPublicKey({ key: der, type: "spki", format: "der" });
35
45
  }
36
46
 
47
+ function canonicalPublicKey(value) {
48
+ const key = importPublicKey(value);
49
+ if (key.asymmetricKeyType !== "x25519") {
50
+ throw new Error("Remote Session peer key must be X25519");
51
+ }
52
+ return exportPublicKey(key);
53
+ }
54
+
55
+ function digest(domain, value) {
56
+ return createHash("sha256")
57
+ .update(domain, "utf8")
58
+ .update("\0", "utf8")
59
+ .update(value)
60
+ .digest("hex");
61
+ }
62
+
63
+ export function remoteSessionPairingTokenDigest(pairingToken) {
64
+ if (typeof pairingToken !== "string" || pairingToken.length === 0) {
65
+ throw new Error("Remote Session pairing token is required");
66
+ }
67
+ return digest("chainlesschain.remote-session.pairing-token.v1", pairingToken);
68
+ }
69
+
70
+ function relayEnvelopeTranscriptHash(envelope) {
71
+ const normalized = {
72
+ v: envelope.v,
73
+ sessionId: envelope.sessionId,
74
+ senderId: envelope.senderId,
75
+ sequence: envelope.sequence,
76
+ nonce: envelope.nonce,
77
+ ciphertext: envelope.ciphertext,
78
+ tag: envelope.tag,
79
+ };
80
+ return digest(
81
+ "chainlesschain.remote-session.relay-envelope.v1",
82
+ JSON.stringify(normalized),
83
+ );
84
+ }
85
+
86
+ export function consumeRemoteRelayPossessionCapability(
87
+ capability,
88
+ {
89
+ sessionId,
90
+ mobilePeerId,
91
+ mobilePublicKey,
92
+ pairingTokenDigest,
93
+ coordinatorId,
94
+ serverInstanceId,
95
+ authorityVersion,
96
+ } = {},
97
+ ) {
98
+ const proof = relayPossessionCapabilities.get(capability);
99
+ // Consume before checking the caller's expectation. A confused-deputy or
100
+ // stale binding attempt cannot probe and then reuse the capability.
101
+ relayPossessionCapabilities.delete(capability);
102
+ const expectedPublicKey = canonicalPublicKey(mobilePublicKey);
103
+ if (
104
+ !proof ||
105
+ proof.sessionId !== sessionId ||
106
+ proof.mobilePeerId !== mobilePeerId ||
107
+ proof.mobilePublicKey !== expectedPublicKey ||
108
+ proof.pairingTokenDigest !== pairingTokenDigest ||
109
+ proof.coordinatorId !== coordinatorId ||
110
+ proof.serverInstanceId !== serverInstanceId ||
111
+ proof.authorityVersion !== authorityVersion
112
+ ) {
113
+ throw new Error("Remote relay possession capability is invalid or stale");
114
+ }
115
+ return Object.freeze({
116
+ ...proof,
117
+ authorizedScopes: Object.freeze([...proof.authorizedScopes]),
118
+ });
119
+ }
120
+
121
+ function normalizeRelayAuthorityScopes(scopes) {
122
+ if (!Array.isArray(scopes) || scopes.length === 0) {
123
+ throw new Error("Durable Remote relay scopes are required");
124
+ }
125
+ const normalized = [...new Set(scopes.map(String))].sort();
126
+ if (normalized.some((scope) => !RELAY_AUTHORITY_SCOPES.has(scope))) {
127
+ throw new Error("Durable Remote relay scopes are invalid");
128
+ }
129
+ return Object.freeze(normalized);
130
+ }
131
+
132
+ function normalizeRelayCapabilities(capabilities) {
133
+ if (capabilities == null) return Object.freeze([]);
134
+ if (!Array.isArray(capabilities) || capabilities.length > 32) {
135
+ throw new Error("Durable Remote relay capabilities are invalid");
136
+ }
137
+ const normalized = [...new Set(capabilities)];
138
+ if (
139
+ normalized.some(
140
+ (capability) =>
141
+ typeof capability !== "string" ||
142
+ capability.length === 0 ||
143
+ capability.length > 128 ||
144
+ capability.includes("\0"),
145
+ )
146
+ ) {
147
+ throw new Error("Durable Remote relay capabilities are invalid");
148
+ }
149
+ return Object.freeze(normalized.sort());
150
+ }
151
+
152
+ function sameStringArray(left, right) {
153
+ return (
154
+ left.length === right.length &&
155
+ left.every((value, index) => value === right[index])
156
+ );
157
+ }
158
+
159
+ function relayCredentialDigest(publicKey) {
160
+ return createHash("sha256").update(unb64(publicKey)).digest("hex");
161
+ }
162
+
163
+ function samePendingRelayRequest(left, right) {
164
+ return (
165
+ left.peerId === right.peerId &&
166
+ left.key.equals(right.key) &&
167
+ left.mobilePublicKey === right.mobilePublicKey &&
168
+ left.pairingTokenDigest === right.pairingTokenDigest &&
169
+ left.initialEnvelopeSequence === right.initialEnvelopeSequence &&
170
+ left.initialEnvelopeTranscriptHash ===
171
+ right.initialEnvelopeTranscriptHash &&
172
+ left.expectedPrincipalId === right.expectedPrincipalId &&
173
+ left.expectedCredentialDigest === right.expectedCredentialDigest &&
174
+ left.expectedSessionEpoch === right.expectedSessionEpoch &&
175
+ left.expectedMembershipEpoch === right.expectedMembershipEpoch &&
176
+ left.baselineAuthorityGeneration === right.baselineAuthorityGeneration &&
177
+ left.baselineMembershipEpoch === right.baselineMembershipEpoch &&
178
+ left.baselineMembershipStatus === right.baselineMembershipStatus &&
179
+ sameStringArray(left.expectedScopes, right.expectedScopes) &&
180
+ sameStringArray(left.expectedCapabilities, right.expectedCapabilities) &&
181
+ sameStringArray(
182
+ left.authority.authorizedScopes,
183
+ right.authority.authorizedScopes,
184
+ ) &&
185
+ left.authority.expiresAtMs === right.authority.expiresAtMs &&
186
+ left.authority.coordinatorId === right.authority.coordinatorId &&
187
+ left.authority.serverInstanceId === right.authority.serverInstanceId &&
188
+ left.authority.authorityVersion === right.authority.authorityVersion
189
+ );
190
+ }
191
+
192
+ function createRelayMembershipAcceptance(
193
+ sessionId,
194
+ peerId,
195
+ {
196
+ mobilePublicKey,
197
+ pairingTokenDigest,
198
+ principalId,
199
+ sessionEpoch,
200
+ membershipEpoch,
201
+ scopes,
202
+ capabilities,
203
+ statement = null,
204
+ } = {},
205
+ ) {
206
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
207
+ const normalizedScopes = normalizeRelayAuthorityScopes(scopes);
208
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
209
+ if (
210
+ typeof pairingTokenDigest !== "string" ||
211
+ !/^[0-9a-f]{64}$/.test(pairingTokenDigest) ||
212
+ typeof principalId !== "string" ||
213
+ principalId.length === 0 ||
214
+ typeof sessionEpoch !== "string" ||
215
+ !/^[1-9]\d*$/.test(sessionEpoch) ||
216
+ typeof membershipEpoch !== "string" ||
217
+ !/^[1-9]\d*$/.test(membershipEpoch)
218
+ ) {
219
+ throw new Error("Remote relay membership acceptance binding is invalid");
220
+ }
221
+ return Object.freeze({
222
+ sessionId,
223
+ mobilePeerId: peerId,
224
+ mobilePublicKey: normalizedPublicKey,
225
+ pairingTokenDigest,
226
+ principalId,
227
+ sessionEpoch,
228
+ membershipEpoch,
229
+ scopes: normalizedScopes,
230
+ capabilities: normalizedCapabilities,
231
+ statement,
232
+ });
233
+ }
234
+
235
+ function decryptEnvelopeWithKey(
236
+ sessionId,
237
+ key,
238
+ envelope,
239
+ previousSequence = 0,
240
+ ) {
241
+ if (envelope?.v !== 1 || envelope.sessionId !== sessionId) {
242
+ throw new Error("Invalid encrypted Remote Session envelope");
243
+ }
244
+ if (
245
+ !Number.isSafeInteger(envelope.sequence) ||
246
+ envelope.sequence <= previousSequence
247
+ ) {
248
+ throw new Error("Remote Session replay or out-of-order envelope rejected");
249
+ }
250
+ try {
251
+ const decipher = createDecipheriv(
252
+ "aes-256-gcm",
253
+ key,
254
+ unb64(envelope.nonce),
255
+ );
256
+ decipher.setAAD(aad(sessionId, envelope.senderId, envelope.sequence));
257
+ decipher.setAuthTag(unb64(envelope.tag));
258
+ const plaintext = Buffer.concat([
259
+ decipher.update(unb64(envelope.ciphertext)),
260
+ decipher.final(),
261
+ ]);
262
+ return JSON.parse(plaintext.toString("utf8"));
263
+ } catch (error) {
264
+ throw new Error(
265
+ `Remote Session envelope authentication failed: ${error.message}`,
266
+ );
267
+ }
268
+ }
269
+
37
270
  function aad(sessionId, senderId, sequence) {
38
271
  return Buffer.from(
39
272
  `${PROTOCOL}\n${sessionId}\n${senderId}\n${sequence}`,
@@ -121,7 +354,14 @@ export function parseRemotePairingUri(uri, now = Date.now()) {
121
354
  }
122
355
 
123
356
  export class RemoteSessionCryptoContext {
124
- constructor({ sessionId, localPeerId, privateKey, publicKey } = {}) {
357
+ constructor({
358
+ sessionId,
359
+ localPeerId,
360
+ privateKey,
361
+ publicKey,
362
+ now = () => Date.now(),
363
+ faultHooks = null,
364
+ } = {}) {
125
365
  if (!sessionId || !localPeerId)
126
366
  throw new Error("sessionId and localPeerId are required");
127
367
  let keyPair;
@@ -134,28 +374,113 @@ export class RemoteSessionCryptoContext {
134
374
  format: "der",
135
375
  });
136
376
  this.publicKey = publicKey || keyPair.publicKey;
377
+ this._now = now;
378
+ this._faultHooks = faultHooks || {};
137
379
  this.keys = new Map();
380
+ this.pairingMaterial = new Map();
381
+ this.relayPeerBindings = new Map();
382
+ this.acceptedRelayMemberships = new Map();
383
+ this.pendingCommittedRelayAcceptances = new Map();
384
+ this.pendingRelayCapabilities = new Map();
385
+ this.outstandingRelayCapabilities = new Map();
138
386
  this.sendSequence = 0;
139
387
  this.receivedSequences = new Map();
140
388
  }
141
389
 
142
390
  pair(peerId, peerPublicKey, pairingToken) {
391
+ return this._pair(peerId, peerPublicKey, pairingToken, null);
392
+ }
393
+
394
+ pairForDurableMembership(
395
+ peerId,
396
+ peerPublicKey,
397
+ pairingToken,
398
+ {
399
+ authorizedScopes,
400
+ expiresAtMs,
401
+ coordinatorId,
402
+ serverInstanceId,
403
+ authorityVersion,
404
+ } = {},
405
+ ) {
406
+ const normalizedExpiry = Number(expiresAtMs);
407
+ if (
408
+ !Number.isSafeInteger(normalizedExpiry) ||
409
+ normalizedExpiry <= this._now()
410
+ ) {
411
+ throw new Error("Durable Remote relay pairing authority is expired");
412
+ }
413
+ if (
414
+ typeof coordinatorId !== "string" ||
415
+ coordinatorId.length === 0 ||
416
+ typeof serverInstanceId !== "string" ||
417
+ serverInstanceId.length === 0 ||
418
+ typeof authorityVersion !== "string" ||
419
+ authorityVersion.length === 0
420
+ ) {
421
+ throw new Error("Durable Remote relay authority domain is required");
422
+ }
423
+ return this._pair(peerId, peerPublicKey, pairingToken, {
424
+ authorizedScopes: normalizeRelayAuthorityScopes(authorizedScopes),
425
+ expiresAtMs: normalizedExpiry,
426
+ coordinatorId,
427
+ serverInstanceId,
428
+ authorityVersion,
429
+ });
430
+ }
431
+
432
+ _pair(peerId, peerPublicKey, pairingToken, durableAuthority) {
143
433
  if (!peerId || !peerPublicKey || !pairingToken)
144
434
  throw new Error("Incomplete peer key material");
435
+ // Preserve the legacy re-pair/rotated-token behavior. Re-pairing replaces
436
+ // the transport key, but first invalidates any not-yet-consumed authority
437
+ // capability minted under the old key/token tuple.
438
+ const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
439
+ if (outstandingCapability) {
440
+ relayPossessionCapabilities.delete(outstandingCapability);
441
+ }
442
+ this.pendingRelayCapabilities.delete(peerId);
443
+ this.outstandingRelayCapabilities.delete(peerId);
444
+ const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
445
+ this.acceptedRelayMemberships.delete(peerId);
446
+ this.discardPendingRelayMembershipCommit(peerId);
145
447
  this.keys.set(
146
448
  peerId,
147
449
  deriveKey(
148
450
  this.privateKey,
149
- importPublicKey(peerPublicKey),
451
+ importPublicKey(normalizedPublicKey),
150
452
  this.sessionId,
151
453
  pairingToken,
152
454
  ),
153
455
  );
456
+ this.pairingMaterial.set(peerId, {
457
+ mobilePublicKey: normalizedPublicKey,
458
+ pairingTokenDigest: remoteSessionPairingTokenDigest(pairingToken),
459
+ authorizedScopes: durableAuthority?.authorizedScopes || null,
460
+ expiresAtMs: durableAuthority?.expiresAtMs || null,
461
+ coordinatorId: durableAuthority?.coordinatorId || null,
462
+ serverInstanceId: durableAuthority?.serverInstanceId || null,
463
+ authorityVersion: durableAuthority?.authorityVersion || null,
464
+ });
465
+ this.relayPeerBindings.set(peerId, {
466
+ mobilePublicKey: normalizedPublicKey,
467
+ pairingTokenDigest: remoteSessionPairingTokenDigest(pairingToken),
468
+ });
154
469
  }
155
470
 
156
471
  encrypt(peerId, message) {
472
+ const revokesThisPeer =
473
+ message?.type === "session.revoked" &&
474
+ message.remoteSessionId === this.sessionId;
157
475
  const key = this.keys.get(peerId);
158
- if (!key) throw new Error(`No encryption key for peer: ${peerId}`);
476
+ if (!key) {
477
+ // A durable join can be authoritative while its candidate key is still
478
+ // pending publication. A courtesy revoke cannot be encrypted in that
479
+ // window, but it must still retire the process-local recovery marker so
480
+ // a later, legitimately re-authorized generation is not blocked.
481
+ if (revokesThisPeer) this.clearRelayPeer(peerId);
482
+ throw new Error(`No encryption key for peer: ${peerId}`);
483
+ }
159
484
  const sequence = ++this.sendSequence;
160
485
  const nonce = randomBytes(12);
161
486
  const cipher = createCipheriv("aes-256-gcm", key, nonce);
@@ -164,7 +489,7 @@ export class RemoteSessionCryptoContext {
164
489
  cipher.update(JSON.stringify(message), "utf8"),
165
490
  cipher.final(),
166
491
  ]);
167
- return {
492
+ const envelope = {
168
493
  v: 1,
169
494
  sessionId: this.sessionId,
170
495
  senderId: this.localPeerId,
@@ -173,6 +498,12 @@ export class RemoteSessionCryptoContext {
173
498
  ciphertext: b64(ciphertext),
174
499
  tag: b64(cipher.getAuthTag()),
175
500
  };
501
+ if (revokesThisPeer) {
502
+ // The ciphertext is complete; remove every local generation for this
503
+ // peer before the courtesy revoke frame is physically sent.
504
+ this.clearRelayPeer(peerId);
505
+ }
506
+ return envelope;
176
507
  }
177
508
 
178
509
  decrypt(envelope) {
@@ -191,27 +522,604 @@ export class RemoteSessionCryptoContext {
191
522
  "Remote Session replay or out-of-order envelope rejected",
192
523
  );
193
524
  }
194
- try {
195
- const decipher = createDecipheriv(
196
- "aes-256-gcm",
197
- key,
198
- unb64(envelope.nonce),
525
+ const message = decryptEnvelopeWithKey(
526
+ this.sessionId,
527
+ key,
528
+ envelope,
529
+ previous,
530
+ );
531
+ this.receivedSequences.set(envelope.senderId, envelope.sequence);
532
+ const pairing = this.pairingMaterial.get(envelope.senderId);
533
+ if (
534
+ pairing &&
535
+ pairing.authorizedScopes !== null &&
536
+ pairing.coordinatorId !== null &&
537
+ pairing.serverInstanceId !== null &&
538
+ pairing.authorityVersion !== null &&
539
+ pairing.expiresAtMs > this._now() &&
540
+ message?.type === "pair.join" &&
541
+ typeof message.token === "string" &&
542
+ remoteSessionPairingTokenDigest(message.token) ===
543
+ pairing.pairingTokenDigest
544
+ ) {
545
+ const proof = Object.freeze({
546
+ schema: RELAY_POSSESSION_SCHEMA,
547
+ sessionId: this.sessionId,
548
+ mobilePeerId: envelope.senderId,
549
+ mobilePublicKey: pairing.mobilePublicKey,
550
+ pairingTokenDigest: pairing.pairingTokenDigest,
551
+ coordinatorId: pairing.coordinatorId,
552
+ serverInstanceId: pairing.serverInstanceId,
553
+ authorityVersion: pairing.authorityVersion,
554
+ authorizedScopes: pairing.authorizedScopes,
555
+ pairingExpiresAtMs: pairing.expiresAtMs,
556
+ envelopeSequence: envelope.sequence,
557
+ envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
558
+ });
559
+ relayPossessionCapabilities.set(proof, { ...proof });
560
+ this.pendingRelayCapabilities.set(envelope.senderId, proof);
561
+ this.outstandingRelayCapabilities.set(envelope.senderId, proof);
562
+ this.pairingMaterial.delete(envelope.senderId);
563
+ }
564
+ return message;
565
+ }
566
+
567
+ /**
568
+ * Authenticate a prospective relay pairing without changing the live peer
569
+ * key, replay high-water mark, or an accepted-membership receipt. The caller
570
+ * commits the opaque stage only after the membership transition succeeds.
571
+ */
572
+ stageRelayPairing(
573
+ peerId,
574
+ peerPublicKey,
575
+ pairingToken,
576
+ envelope,
577
+ durableAuthority = null,
578
+ ) {
579
+ if (!peerId || !peerPublicKey || !pairingToken) {
580
+ throw new Error("Incomplete peer key material");
581
+ }
582
+ if (envelope?.senderId !== peerId) {
583
+ throw new Error("Remote Session relay peer binding is invalid");
584
+ }
585
+ const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
586
+ const pairingTokenDigest = remoteSessionPairingTokenDigest(pairingToken);
587
+ let authority = null;
588
+ if (durableAuthority !== null) {
589
+ const normalizedExpiry = Number(durableAuthority?.expiresAtMs);
590
+ if (
591
+ !Number.isSafeInteger(normalizedExpiry) ||
592
+ normalizedExpiry <= this._now()
593
+ ) {
594
+ throw new Error("Durable Remote relay pairing authority is expired");
595
+ }
596
+ if (
597
+ typeof durableAuthority?.coordinatorId !== "string" ||
598
+ durableAuthority.coordinatorId.length === 0 ||
599
+ typeof durableAuthority?.serverInstanceId !== "string" ||
600
+ durableAuthority.serverInstanceId.length === 0 ||
601
+ typeof durableAuthority?.authorityVersion !== "string" ||
602
+ durableAuthority.authorityVersion.length === 0
603
+ ) {
604
+ throw new Error("Durable Remote relay authority domain is required");
605
+ }
606
+ authority = Object.freeze({
607
+ authorizedScopes: normalizeRelayAuthorityScopes(
608
+ durableAuthority.authorizedScopes,
609
+ ),
610
+ expiresAtMs: normalizedExpiry,
611
+ coordinatorId: durableAuthority.coordinatorId,
612
+ serverInstanceId: durableAuthority.serverInstanceId,
613
+ authorityVersion: durableAuthority.authorityVersion,
614
+ });
615
+ }
616
+ const key = deriveKey(
617
+ this.privateKey,
618
+ importPublicKey(normalizedPublicKey),
619
+ this.sessionId,
620
+ pairingToken,
621
+ );
622
+ // A new key/token tuple starts a new encrypted sequence space. Nothing is
623
+ // published to the live maps until commitStagedRelayPairing().
624
+ const message = decryptEnvelopeWithKey(this.sessionId, key, envelope, 0);
625
+ if (
626
+ message?.type !== "pair.join" ||
627
+ message.remoteSessionId !== this.sessionId ||
628
+ message.token !== pairingToken
629
+ ) {
630
+ throw new Error("Invalid encrypted Remote Session pairing request");
631
+ }
632
+ let possessionCapability = null;
633
+ if (authority) {
634
+ const proof = Object.freeze({
635
+ schema: RELAY_POSSESSION_SCHEMA,
636
+ sessionId: this.sessionId,
637
+ mobilePeerId: peerId,
638
+ mobilePublicKey: normalizedPublicKey,
639
+ pairingTokenDigest,
640
+ coordinatorId: authority.coordinatorId,
641
+ serverInstanceId: authority.serverInstanceId,
642
+ authorityVersion: authority.authorityVersion,
643
+ authorizedScopes: authority.authorizedScopes,
644
+ pairingExpiresAtMs: authority.expiresAtMs,
645
+ envelopeSequence: envelope.sequence,
646
+ envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
647
+ });
648
+ relayPossessionCapabilities.set(proof, { ...proof });
649
+ possessionCapability = proof;
650
+ }
651
+ const stage = Object.freeze({});
652
+ stagedRelayPairings.set(stage, {
653
+ owner: this,
654
+ peerId,
655
+ key,
656
+ mobilePublicKey: normalizedPublicKey,
657
+ pairingTokenDigest,
658
+ envelopeSequence: envelope.sequence,
659
+ envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
660
+ authority,
661
+ possessionCapability,
662
+ });
663
+ return Object.freeze({
664
+ message,
665
+ stage,
666
+ possessionCapability,
667
+ });
668
+ }
669
+
670
+ commitStagedRelayPairing(stage) {
671
+ const candidate = stagedRelayPairings.get(stage);
672
+ stagedRelayPairings.delete(stage);
673
+ if (!candidate || candidate.owner !== this) {
674
+ throw new Error("Remote relay pairing stage is invalid or stale");
675
+ }
676
+ const outstandingCapability = this.outstandingRelayCapabilities.get(
677
+ candidate.peerId,
678
+ );
679
+ if (outstandingCapability) {
680
+ relayPossessionCapabilities.delete(outstandingCapability);
681
+ }
682
+ this.pendingRelayCapabilities.delete(candidate.peerId);
683
+ this.outstandingRelayCapabilities.delete(candidate.peerId);
684
+ this.pairingMaterial.delete(candidate.peerId);
685
+ this.keys.set(candidate.peerId, candidate.key);
686
+ this.relayPeerBindings.set(candidate.peerId, {
687
+ mobilePublicKey: candidate.mobilePublicKey,
688
+ pairingTokenDigest: candidate.pairingTokenDigest,
689
+ });
690
+ this.receivedSequences.set(candidate.peerId, candidate.envelopeSequence);
691
+ this.acceptedRelayMemberships.delete(candidate.peerId);
692
+ this.discardPendingRelayMembershipCommit(candidate.peerId);
693
+ }
694
+
695
+ /**
696
+ * Arm recovery before invoking the durable coordinator/registry. This moves
697
+ * the verified candidate out of the opaque stage and binds its exact request,
698
+ * authority instance, and pre-join authority generation without publishing
699
+ * a live peer key, replay high-water mark, or acceptance receipt.
700
+ */
701
+ armPendingRelayMembershipCommit(
702
+ stage,
703
+ { authoritySnapshot, scopes, capabilities } = {},
704
+ ) {
705
+ const candidate = stagedRelayPairings.get(stage);
706
+ const snapshot = authoritySnapshot;
707
+ if (
708
+ !candidate ||
709
+ candidate.owner !== this ||
710
+ !candidate.authority ||
711
+ snapshot?.sessionId !== this.sessionId ||
712
+ snapshot.status !== "active" ||
713
+ !/^[1-9]\d*$/.test(String(snapshot.sessionEpoch)) ||
714
+ !/^\d+$/.test(String(snapshot.authorityGeneration)) ||
715
+ !Array.isArray(snapshot.members)
716
+ ) {
717
+ throw new Error("Remote relay pairing stage is invalid or stale");
718
+ }
719
+ const expectedScopes = normalizeRelayAuthorityScopes(scopes);
720
+ const expectedCapabilities = normalizeRelayCapabilities(capabilities);
721
+ if (
722
+ expectedScopes.some(
723
+ (scope) => !candidate.authority.authorizedScopes.includes(scope),
724
+ )
725
+ ) {
726
+ throw new Error("Remote relay membership request exceeds its authority");
727
+ }
728
+ const expectedCredentialDigest = relayCredentialDigest(
729
+ candidate.mobilePublicKey,
730
+ );
731
+ const expectedPrincipalId = `relay-x25519:${expectedCredentialDigest}`;
732
+ const baselineMember = snapshot.members.find(
733
+ (member) => member.principalId === expectedPrincipalId,
734
+ );
735
+ if (baselineMember?.status === "active") {
736
+ throw new Error("Remote relay principal is already active");
737
+ }
738
+ if (
739
+ baselineMember &&
740
+ baselineMember.credentialKeySha256 !== expectedCredentialDigest
741
+ ) {
742
+ throw new Error("Remote relay authority credential binding changed");
743
+ }
744
+ const pending = Object.freeze({
745
+ phase: "prepared",
746
+ peerId: candidate.peerId,
747
+ key: candidate.key,
748
+ mobilePublicKey: candidate.mobilePublicKey,
749
+ pairingTokenDigest: candidate.pairingTokenDigest,
750
+ initialEnvelopeSequence: candidate.envelopeSequence,
751
+ initialEnvelopeTranscriptHash: candidate.envelopeTranscriptHash,
752
+ localHighWater: candidate.envelopeSequence,
753
+ authority: candidate.authority,
754
+ possessionCapability: candidate.possessionCapability,
755
+ expectedPrincipalId,
756
+ expectedCredentialDigest,
757
+ expectedSessionEpoch: String(snapshot.sessionEpoch),
758
+ expectedMembershipEpoch: baselineMember
759
+ ? String(BigInt(baselineMember.membershipEpoch) + 1n)
760
+ : "1",
761
+ expectedScopes,
762
+ expectedCapabilities,
763
+ baselineAuthorityGeneration: String(snapshot.authorityGeneration),
764
+ baselineMembershipEpoch: baselineMember?.membershipEpoch || null,
765
+ baselineMembershipStatus: baselineMember?.status || null,
766
+ receipt: null,
767
+ });
768
+ const existing = this.pendingCommittedRelayAcceptances.get(
769
+ candidate.peerId,
770
+ );
771
+ if (existing && !samePendingRelayRequest(existing, pending)) {
772
+ throw new Error(
773
+ "Remote relay peer already has a different pending membership commit",
774
+ );
775
+ }
776
+ if (!existing) {
777
+ this.pendingCommittedRelayAcceptances.set(candidate.peerId, pending);
778
+ } else if (candidate.possessionCapability) {
779
+ relayPossessionCapabilities.delete(candidate.possessionCapability);
780
+ }
781
+ stagedRelayPairings.delete(stage);
782
+ return existing || pending;
783
+ }
784
+
785
+ completePendingCommittedRelayAcceptance(peerId, acceptance) {
786
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
787
+ if (!pending) {
788
+ throw new Error("Remote relay committed acceptance is unavailable");
789
+ }
790
+ // This boundary is deliberately after RemoteSessionRegistry.joinRelayMember
791
+ // has returned (including its local transport attachment). A coordinator
792
+ // commit that fails before registry attachment remains fail-closed because
793
+ // this three-file slice has no trusted public reattach operation.
794
+ this._faultHooks.afterRelayRegistryCommit?.({
795
+ sessionId: this.sessionId,
796
+ peerId,
797
+ expectedPrincipalId: pending.expectedPrincipalId,
798
+ expectedSessionEpoch: pending.expectedSessionEpoch,
799
+ expectedMembershipEpoch: pending.expectedMembershipEpoch,
800
+ });
801
+ const receipt = createRelayMembershipAcceptance(
802
+ this.sessionId,
803
+ peerId,
804
+ acceptance,
805
+ );
806
+ if (
807
+ receipt.mobilePublicKey !== pending.mobilePublicKey ||
808
+ receipt.pairingTokenDigest !== pending.pairingTokenDigest ||
809
+ receipt.principalId !== pending.expectedPrincipalId ||
810
+ receipt.sessionEpoch !== pending.expectedSessionEpoch ||
811
+ receipt.membershipEpoch !== pending.expectedMembershipEpoch ||
812
+ !sameStringArray(receipt.scopes, pending.expectedScopes) ||
813
+ !sameStringArray(receipt.capabilities, pending.expectedCapabilities)
814
+ ) {
815
+ throw new Error("Remote relay membership acceptance binding is invalid");
816
+ }
817
+ this.pendingCommittedRelayAcceptances.set(
818
+ peerId,
819
+ Object.freeze({ ...pending, phase: "committed", receipt }),
820
+ );
821
+ return receipt;
822
+ }
823
+
824
+ resolvePendingRelayMembershipCommit(
825
+ peerId,
826
+ authorityRead,
827
+ authorityDescriptor,
828
+ ) {
829
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
830
+ const session = authorityRead?.session;
831
+ const statement = authorityRead?.statement;
832
+ if (
833
+ !pending ||
834
+ !session ||
835
+ session.sessionId !== this.sessionId ||
836
+ authorityDescriptor?.coordinatorId !== pending.authority.coordinatorId ||
837
+ authorityDescriptor?.serverInstanceId !==
838
+ pending.authority.serverInstanceId ||
839
+ authorityDescriptor?.authorityVersion !==
840
+ pending.authority.authorityVersion ||
841
+ statement?.coordinatorId !== pending.authority.coordinatorId ||
842
+ statement?.authorityVersion !== pending.authority.authorityVersion ||
843
+ statement?.generation !== String(session.authorityGeneration) ||
844
+ statement?.payload?.sessionId !== session.sessionId ||
845
+ statement?.payload?.sessionEpoch !== session.sessionEpoch
846
+ ) {
847
+ return Object.freeze({ status: "fenced", receipt: null });
848
+ }
849
+ const member = session.members?.find(
850
+ (candidate) => candidate.principalId === pending.expectedPrincipalId,
851
+ );
852
+ const generationAdvanced =
853
+ /^\d+$/.test(String(session.authorityGeneration)) &&
854
+ BigInt(session.authorityGeneration) >
855
+ BigInt(pending.baselineAuthorityGeneration);
856
+ if (
857
+ generationAdvanced &&
858
+ session.status === "active" &&
859
+ String(session.sessionEpoch) === pending.expectedSessionEpoch &&
860
+ member?.status === "active" &&
861
+ member.credentialKeySha256 === pending.expectedCredentialDigest &&
862
+ String(member.membershipEpoch) === pending.expectedMembershipEpoch &&
863
+ sameStringArray(
864
+ normalizeRelayAuthorityScopes(member.scopes),
865
+ pending.expectedScopes,
866
+ )
867
+ ) {
868
+ const receipt = createRelayMembershipAcceptance(this.sessionId, peerId, {
869
+ mobilePublicKey: pending.mobilePublicKey,
870
+ pairingTokenDigest: pending.pairingTokenDigest,
871
+ principalId: pending.expectedPrincipalId,
872
+ sessionEpoch: pending.expectedSessionEpoch,
873
+ membershipEpoch: pending.expectedMembershipEpoch,
874
+ scopes: pending.expectedScopes,
875
+ capabilities: pending.expectedCapabilities,
876
+ statement: authorityRead.statement || null,
877
+ });
878
+ this.pendingCommittedRelayAcceptances.set(
879
+ peerId,
880
+ Object.freeze({ ...pending, phase: "committed", receipt }),
881
+ );
882
+ return Object.freeze({ status: "committed", receipt });
883
+ }
884
+ const baselineMemberUnchanged = pending.baselineMembershipEpoch
885
+ ? member?.status === pending.baselineMembershipStatus &&
886
+ String(member.membershipEpoch) === pending.baselineMembershipEpoch &&
887
+ member.credentialKeySha256 === pending.expectedCredentialDigest
888
+ : !member;
889
+ if (
890
+ session.status === "active" &&
891
+ String(session.sessionEpoch) === pending.expectedSessionEpoch &&
892
+ baselineMemberUnchanged
893
+ ) {
894
+ return Object.freeze({ status: "not-committed", receipt: null });
895
+ }
896
+ return Object.freeze({ status: "fenced", receipt: null });
897
+ }
898
+
899
+ discardPendingRelayMembershipCommit(peerId) {
900
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
901
+ if (!pending) return false;
902
+ if (pending.possessionCapability) {
903
+ relayPossessionCapabilities.delete(pending.possessionCapability);
904
+ }
905
+ this.pendingCommittedRelayAcceptances.delete(peerId);
906
+ return true;
907
+ }
908
+
909
+ /**
910
+ * Publish a previously armed durable outcome. Pending is deleted last, so a
911
+ * synchronous failure at either injected boundary can safely re-enter this
912
+ * method without lowering an already-observed replay high-water mark.
913
+ */
914
+ finalizeCommittedRelayAcceptance(peerId) {
915
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
916
+ if (!pending) {
917
+ const accepted = this.acceptedRelayMemberships.get(peerId);
918
+ if (accepted) return accepted;
919
+ throw new Error("Remote relay committed acceptance is unavailable");
920
+ }
921
+ if (pending.phase !== "committed" || !pending.receipt) {
922
+ throw new Error("Remote relay committed acceptance is not authoritative");
923
+ }
924
+ const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
925
+ if (outstandingCapability) {
926
+ relayPossessionCapabilities.delete(outstandingCapability);
927
+ }
928
+ this.pendingRelayCapabilities.delete(peerId);
929
+ this.outstandingRelayCapabilities.delete(peerId);
930
+ this.pairingMaterial.delete(peerId);
931
+ this.keys.set(peerId, pending.key);
932
+ this.relayPeerBindings.set(peerId, {
933
+ mobilePublicKey: pending.mobilePublicKey,
934
+ pairingTokenDigest: pending.pairingTokenDigest,
935
+ });
936
+ // The candidate key starts a distinct sequence generation. Never mix an
937
+ // old live key's high-water into it; pending-local authenticated progress
938
+ // becomes live only with the exact committed authority tuple.
939
+ this.receivedSequences.set(peerId, pending.localHighWater);
940
+ this.acceptedRelayMemberships.delete(peerId);
941
+ this._faultHooks.afterCommittedRelayKeyPublication?.({
942
+ sessionId: this.sessionId,
943
+ peerId,
944
+ principalId: pending.receipt.principalId,
945
+ sessionEpoch: pending.receipt.sessionEpoch,
946
+ membershipEpoch: pending.receipt.membershipEpoch,
947
+ });
948
+ this.acceptedRelayMemberships.set(peerId, pending.receipt);
949
+ this.pendingCommittedRelayAcceptances.delete(peerId);
950
+ this._faultHooks.afterCommittedRelayReceiptPublication?.({
951
+ sessionId: this.sessionId,
952
+ peerId,
953
+ principalId: pending.receipt.principalId,
954
+ sessionEpoch: pending.receipt.sessionEpoch,
955
+ membershipEpoch: pending.receipt.membershipEpoch,
956
+ });
957
+ return pending.receipt;
958
+ }
959
+
960
+ decryptPendingCommittedRelayAcceptance(peerId, envelope) {
961
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
962
+ if (!pending || envelope?.senderId !== peerId) {
963
+ throw new Error("Remote relay committed acceptance is unavailable");
964
+ }
965
+ const previous = pending.localHighWater;
966
+ const message = decryptEnvelopeWithKey(
967
+ this.sessionId,
968
+ pending.key,
969
+ envelope,
970
+ previous,
971
+ );
972
+ // Keep retry progress isolated from the live key generation until fresh
973
+ // authority readback and exact tuple validation permit final publication.
974
+ this.pendingCommittedRelayAcceptances.set(
975
+ peerId,
976
+ Object.freeze({ ...pending, localHighWater: envelope.sequence }),
977
+ );
978
+ return message;
979
+ }
980
+
981
+ readPendingCommittedRelayAcceptance(
982
+ peerId,
983
+ { mobilePublicKey, pairingTokenDigest, capabilities } = {},
984
+ ) {
985
+ const receipt =
986
+ this.pendingCommittedRelayAcceptances.get(peerId)?.receipt || null;
987
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
988
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
989
+ if (
990
+ !receipt ||
991
+ receipt.mobilePublicKey !== normalizedPublicKey ||
992
+ receipt.pairingTokenDigest !== pairingTokenDigest ||
993
+ !sameStringArray(receipt.capabilities, normalizedCapabilities)
994
+ ) {
995
+ throw new Error(
996
+ "Remote relay committed acceptance is unavailable or mismatched",
997
+ );
998
+ }
999
+ return receipt;
1000
+ }
1001
+
1002
+ hasPendingCommittedRelayAcceptance(peerId) {
1003
+ return this.pendingCommittedRelayAcceptances.has(peerId);
1004
+ }
1005
+
1006
+ readPendingRelayMembershipRequest(
1007
+ peerId,
1008
+ { mobilePublicKey, pairingTokenDigest, capabilities } = {},
1009
+ ) {
1010
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId) || null;
1011
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
1012
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
1013
+ if (
1014
+ !pending ||
1015
+ pending.mobilePublicKey !== normalizedPublicKey ||
1016
+ pending.pairingTokenDigest !== pairingTokenDigest ||
1017
+ !sameStringArray(pending.expectedCapabilities, normalizedCapabilities)
1018
+ ) {
1019
+ throw new Error(
1020
+ "Remote relay pending membership request is unavailable or mismatched",
199
1021
  );
200
- decipher.setAAD(
201
- aad(this.sessionId, envelope.senderId, envelope.sequence),
1022
+ }
1023
+ return Object.freeze({
1024
+ sessionId: this.sessionId,
1025
+ mobilePeerId: peerId,
1026
+ pairingTokenDigest: pending.pairingTokenDigest,
1027
+ expectedPrincipalId: pending.expectedPrincipalId,
1028
+ expectedSessionEpoch: pending.expectedSessionEpoch,
1029
+ expectedMembershipEpoch: pending.expectedMembershipEpoch,
1030
+ scopes: pending.expectedScopes,
1031
+ capabilities: pending.expectedCapabilities,
1032
+ phase: pending.phase,
1033
+ });
1034
+ }
1035
+
1036
+ clearRelayPeer(peerId) {
1037
+ const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
1038
+ if (outstandingCapability) {
1039
+ relayPossessionCapabilities.delete(outstandingCapability);
1040
+ }
1041
+ this.discardPendingRelayMembershipCommit(peerId);
1042
+ this.pendingRelayCapabilities.delete(peerId);
1043
+ this.outstandingRelayCapabilities.delete(peerId);
1044
+ this.pairingMaterial.delete(peerId);
1045
+ this.keys.delete(peerId);
1046
+ this.relayPeerBindings.delete(peerId);
1047
+ this.receivedSequences.delete(peerId);
1048
+ this.acceptedRelayMemberships.delete(peerId);
1049
+ }
1050
+
1051
+ takeRelayPossessionCapability(peerId) {
1052
+ const capability = this.pendingRelayCapabilities.get(peerId);
1053
+ this.pendingRelayCapabilities.delete(peerId);
1054
+ if (!capability) {
1055
+ throw new Error(
1056
+ "No authenticated Remote relay pairing transcript is available",
202
1057
  );
203
- decipher.setAuthTag(unb64(envelope.tag));
204
- const plaintext = Buffer.concat([
205
- decipher.update(unb64(envelope.ciphertext)),
206
- decipher.final(),
207
- ]);
208
- const message = JSON.parse(plaintext.toString("utf8"));
209
- this.receivedSequences.set(envelope.senderId, envelope.sequence);
210
- return message;
211
- } catch (error) {
1058
+ }
1059
+ return capability;
1060
+ }
1061
+
1062
+ /**
1063
+ * Keep the exact, already-committed relay membership tuple in this crypto
1064
+ * process. It is deliberately not persisted: it only reconciles an ACK lost
1065
+ * while the same host process still owns the authenticated DH/AEAD context.
1066
+ */
1067
+ rememberRelayMembershipAcceptance(
1068
+ peerId,
1069
+ {
1070
+ mobilePublicKey,
1071
+ pairingTokenDigest,
1072
+ principalId,
1073
+ sessionEpoch,
1074
+ membershipEpoch,
1075
+ scopes,
1076
+ capabilities,
1077
+ statement = null,
1078
+ } = {},
1079
+ ) {
1080
+ const peer = this.relayPeerBindings.get(peerId);
1081
+ const receipt = createRelayMembershipAcceptance(this.sessionId, peerId, {
1082
+ mobilePublicKey,
1083
+ pairingTokenDigest,
1084
+ principalId,
1085
+ sessionEpoch,
1086
+ membershipEpoch,
1087
+ scopes,
1088
+ capabilities,
1089
+ statement,
1090
+ });
1091
+ if (
1092
+ !peer ||
1093
+ peer.mobilePublicKey !== receipt.mobilePublicKey ||
1094
+ peer.pairingTokenDigest !== receipt.pairingTokenDigest
1095
+ ) {
1096
+ throw new Error("Remote relay membership acceptance binding is invalid");
1097
+ }
1098
+ this.acceptedRelayMemberships.set(peerId, receipt);
1099
+ return receipt;
1100
+ }
1101
+
1102
+ readRelayMembershipAcceptance(
1103
+ peerId,
1104
+ { mobilePublicKey, pairingTokenDigest, capabilities } = {},
1105
+ ) {
1106
+ const receipt = this.acceptedRelayMemberships.get(peerId);
1107
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
1108
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
1109
+ if (
1110
+ !receipt ||
1111
+ receipt.mobilePublicKey !== normalizedPublicKey ||
1112
+ receipt.pairingTokenDigest !== pairingTokenDigest ||
1113
+ !sameStringArray(receipt.capabilities, normalizedCapabilities)
1114
+ ) {
212
1115
  throw new Error(
213
- `Remote Session envelope authentication failed: ${error.message}`,
1116
+ "Remote relay membership acceptance is unavailable or mismatched",
214
1117
  );
215
1118
  }
1119
+ return receipt;
1120
+ }
1121
+
1122
+ hasRelayMembershipAcceptance(peerId) {
1123
+ return this.acceptedRelayMemberships.has(peerId);
216
1124
  }
217
1125
  }