chainlesschain 0.164.0 → 0.165.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +248 -2
  2. package/package.json +7 -4
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
  40. package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
  56. package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
  75. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
  76. package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
  104. package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
  109. package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
  134. package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
  135. package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/cowork.js +559 -62
  154. package/src/commands/plugin.js +819 -64
  155. package/src/commands/session-location.js +318 -7
  156. package/src/constants.js +2 -2
  157. package/src/data/changelog.json +28 -2
  158. package/src/gateways/ws/action-protocol.js +293 -19
  159. package/src/gateways/ws/message-dispatcher.js +30 -0
  160. package/src/gateways/ws/remote-session-protocol.js +542 -35
  161. package/src/gateways/ws/ws-approval-gate.js +5 -0
  162. package/src/gateways/ws/ws-server.js +462 -38
  163. package/src/harness/jsonl-session-store.js +638 -2
  164. package/src/harness/prompt-compressor.js +4 -0
  165. package/src/harness/remote-session-audit.js +1 -0
  166. package/src/harness/remote-session-crypto.js +750 -50
  167. package/src/harness/remote-session-registry.js +795 -27
  168. package/src/lib/auto-mode-config.js +98 -3
  169. package/src/lib/background-agent-keeper-client.js +29 -0
  170. package/src/lib/background-agent-keeper-protocol.js +158 -10
  171. package/src/lib/background-agent-supervisor.js +870 -48
  172. package/src/lib/background-session-transport.js +23 -6
  173. package/src/lib/background-turn-bootstrap-protocol.js +64 -0
  174. package/src/lib/cowork-task-runner.js +70 -0
  175. package/src/lib/cowork-workflow.js +887 -62
  176. package/src/lib/delivery-production-adapter.js +38 -2
  177. package/src/lib/doctor-checkup.js +34 -10
  178. package/src/lib/dynamic-workflow-draft.js +535 -0
  179. package/src/lib/dynamic-workflow-facade.js +1105 -17
  180. package/src/lib/dynamic-workflow-runtime.js +2974 -0
  181. package/src/lib/execution-location-contract.js +111 -7
  182. package/src/lib/execution-location-runtime.js +3 -1
  183. package/src/lib/execution-location-target.js +910 -0
  184. package/src/lib/llm-providers.js +1 -0
  185. package/src/lib/mcp-call-ledger-store.js +35 -0
  186. package/src/lib/mcp-call-ledger.js +30 -0
  187. package/src/lib/mcp-stdio-package-materialization.js +1 -1
  188. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
  189. package/src/lib/plugin-runtime/install.js +4956 -657
  190. package/src/lib/plugin-runtime/manifest.js +3 -1
  191. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
  192. package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
  193. package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
  194. package/src/lib/plugin-runtime/marketplace-network.js +317 -0
  195. package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
  196. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
  197. package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
  198. package/src/lib/plugin-runtime/policy.js +21 -6
  199. package/src/lib/plugin-runtime/publisher-trust.js +332 -0
  200. package/src/lib/plugin-runtime/remote-source.js +323 -27
  201. package/src/lib/plugin-runtime/scopes.js +317 -41
  202. package/src/lib/plugin-runtime/transaction-journal.js +806 -0
  203. package/src/lib/public-ide-capability-manifest.js +1 -1
  204. package/src/lib/remote-approval-bridge.js +745 -108
  205. package/src/lib/remote-control.js +2 -0
  206. package/src/lib/remote-membership-coordinator.js +612 -46
  207. package/src/lib/remote-membership-host-store.js +59 -10
  208. package/src/lib/runtime-provenance-ledger.js +171 -46
  209. package/src/lib/shell-approval.js +169 -6
  210. package/src/lib/sub-agent-context.js +268 -2
  211. package/src/lib/task-model-selector.js +1 -1
  212. package/src/lib/with-file-lock.js +167 -42
  213. package/src/repl/agent-repl.js +70 -29
  214. package/src/repl/remote-approval.js +29 -7
  215. package/src/runtime/agent-core.js +1695 -194
  216. package/src/runtime/headless-runner.js +22 -5
  217. package/src/runtime/headless-stream.js +19 -3
  218. package/src/workers/background-agent-keeper.js +279 -58
  219. package/src/workers/background-agent-worker.js +7 -0
  220. package/src/workers/background-turn-bootstrap.js +29 -2
  221. package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
  222. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
  223. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
  224. package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
  225. package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
  226. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
  227. package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
  228. package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
@@ -8,12 +8,14 @@
8
8
  * with a pinned Ed25519 key; WebSocket delivery is transport only and never an
9
9
  * authority by itself.
10
10
  *
11
- * Integration status: this module is a deployable contract, but the current
12
- * remote-session registry, approval bridge, and agent dispatch paths do not
13
- * call it yet. Cross-host revoke closure therefore remains false until every
14
- * real side-effect entry point performs the online one-shot consume. A
15
- * successful consume linearizes dispatch authority; it cannot undo an external
16
- * effect after that effect has started.
11
+ * Production integration: the Remote Session registry and WebSocket protocol
12
+ * use this coordinator, the client-hosted approval bridge durably adopts and
13
+ * ACKs its signed leases, and Agent shell dispatch consumes the exact one-shot
14
+ * lease online immediately before the side effect. Side-effect paths without
15
+ * that consume boundary reject Remote approval instead of falling back to a
16
+ * cached decision. This provides cross-host revoke fencing under one online,
17
+ * strongly consistent coordinator; it does not claim quorum availability or
18
+ * undo an external effect after a successful consume has linearized dispatch.
17
19
  */
18
20
 
19
21
  import fs from "node:fs";
@@ -44,6 +46,8 @@ export const REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE =
44
46
  "CC_REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE";
45
47
  export const REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE =
46
48
  "CC_REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK";
49
+ export const REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE =
50
+ "CC_REMOTE_MEMBERSHIP_NOT_ACTIVE";
47
51
 
48
52
  const STORE_VERSION = 1;
49
53
  const KEY_VERSION = 1;
@@ -62,6 +66,7 @@ const NON_NEGATIVE_RE = /^(?:0|[1-9]\d*)$/;
62
66
  const VALID_SCOPES = new Set(["observe", "prompt", "approve", "interrupt"]);
63
67
  const VALID_TYPES = new Set([
64
68
  "session.created",
69
+ "session.reenabled",
65
70
  "member.joined",
66
71
  "member.revoked",
67
72
  "session.closed",
@@ -75,6 +80,19 @@ const MAX_LEASE_TTL_MS = 60_000;
75
80
  const DEFAULT_CHALLENGE_TTL_MS = 30_000;
76
81
  const MAX_CHALLENGE_TTL_MS = 60_000;
77
82
  const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
83
+ export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
84
+ const DEFAULT_JOIN_POLICY_VERSION = "remote-session-policy:unrestricted-v1";
85
+ const JOIN_POLICY_EVENT_FIELDS = Object.freeze([
86
+ "policyAllowedScopes",
87
+ "policyAllowRelayPairing",
88
+ "policyMaxDevices",
89
+ "policyVersion",
90
+ ]);
91
+ const MEMBER_JOIN_AUTHORITY_FIELDS = Object.freeze([
92
+ "capabilities",
93
+ "joinVia",
94
+ ...JOIN_POLICY_EVENT_FIELDS,
95
+ ]);
78
96
 
79
97
  const COMMON_EVENT_FIELDS = Object.freeze([
80
98
  "eventHash",
@@ -97,6 +115,22 @@ const EVENT_FIELDS = Object.freeze({
97
115
  "hostCredentialType",
98
116
  "hostPrincipalId",
99
117
  "membershipEpoch",
118
+ ...JOIN_POLICY_EVENT_FIELDS,
119
+ "scopes",
120
+ "sessionEpoch",
121
+ ]),
122
+ "session.reenabled": Object.freeze([
123
+ ...COMMON_EVENT_FIELDS,
124
+ "expectedHostMembershipEpoch",
125
+ "expectedSessionEpoch",
126
+ "expiresAt",
127
+ "hostPrincipalId",
128
+ "membershipEpoch",
129
+ "nextHostCredentialDigest",
130
+ "nextHostCredentialPublicKey",
131
+ "nextHostCredentialType",
132
+ "nextHostPrincipalId",
133
+ ...JOIN_POLICY_EVENT_FIELDS,
100
134
  "scopes",
101
135
  "sessionEpoch",
102
136
  ]),
@@ -109,6 +143,7 @@ const EVENT_FIELDS = Object.freeze({
109
143
  "membershipEpoch",
110
144
  "previousMembershipEpoch",
111
145
  "principalId",
146
+ ...MEMBER_JOIN_AUTHORITY_FIELDS,
112
147
  "scopes",
113
148
  ]),
114
149
  "member.revoked": Object.freeze([
@@ -172,6 +207,19 @@ const EVENT_FIELDS = Object.freeze({
172
207
  ]),
173
208
  });
174
209
 
210
+ const LEGACY_EVENT_FIELDS = Object.freeze({
211
+ "session.created": Object.freeze(
212
+ EVENT_FIELDS["session.created"].filter(
213
+ (field) => !JOIN_POLICY_EVENT_FIELDS.includes(field),
214
+ ),
215
+ ),
216
+ "member.joined": Object.freeze(
217
+ EVENT_FIELDS["member.joined"].filter(
218
+ (field) => !MEMBER_JOIN_AUTHORITY_FIELDS.includes(field),
219
+ ),
220
+ ),
221
+ });
222
+
175
223
  function coordinatorError(code, message, cause = null, details = {}) {
176
224
  const error = new Error(message, cause ? { cause } : undefined);
177
225
  error.name = "RemoteMembershipCoordinatorError";
@@ -183,7 +231,8 @@ function coordinatorError(code, message, cause = null, details = {}) {
183
231
  function unavailable(cause, filePath) {
184
232
  if (
185
233
  cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE ||
186
- cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE
234
+ cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE ||
235
+ cause?.code === REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE
187
236
  ) {
188
237
  return cause;
189
238
  }
@@ -244,6 +293,141 @@ function normalizeScopes(scopes) {
244
293
  return normalized;
245
294
  }
246
295
 
296
+ function normalizeCapabilities(capabilities) {
297
+ if (capabilities == null) return [];
298
+ if (!Array.isArray(capabilities) || capabilities.length > 32) {
299
+ throw new TypeError("membership capabilities must be a bounded array");
300
+ }
301
+ return [
302
+ ...new Set(
303
+ capabilities.map((capability) => {
304
+ if (typeof capability !== "string") {
305
+ throw new TypeError("membership capability must be a string");
306
+ }
307
+ return requiredString(capability, "membership capability", 128);
308
+ }),
309
+ ),
310
+ ].sort();
311
+ }
312
+
313
+ function normalizeJoinPolicy(policy = null) {
314
+ const value = policy || {};
315
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
316
+ throw new TypeError("membership join policy must be an object");
317
+ }
318
+ const allowedScopes =
319
+ value.allowedScopes == null ? null : normalizeScopes(value.allowedScopes);
320
+ const maxDevices = value.maxDevices == null ? null : value.maxDevices;
321
+ if (
322
+ maxDevices !== null &&
323
+ (!Number.isSafeInteger(maxDevices) ||
324
+ maxDevices < 0 ||
325
+ maxDevices > 1_000_000)
326
+ ) {
327
+ throw new TypeError(
328
+ "membership policy maxDevices must be a bounded integer",
329
+ );
330
+ }
331
+ if (
332
+ value.allowRelayPairing != null &&
333
+ typeof value.allowRelayPairing !== "boolean"
334
+ ) {
335
+ throw new TypeError("membership policy allowRelayPairing must be boolean");
336
+ }
337
+ return Object.freeze({
338
+ policyVersion:
339
+ value.policyVersion == null
340
+ ? DEFAULT_JOIN_POLICY_VERSION
341
+ : requiredString(value.policyVersion, "policyVersion", 512),
342
+ allowedScopes:
343
+ allowedScopes === null ? null : Object.freeze([...allowedScopes]),
344
+ maxDevices,
345
+ allowRelayPairing: value.allowRelayPairing !== false,
346
+ });
347
+ }
348
+
349
+ function joinPolicyEventFields(policy) {
350
+ const normalized = normalizeJoinPolicy(policy);
351
+ return {
352
+ policyVersion: normalized.policyVersion,
353
+ policyAllowedScopes:
354
+ normalized.allowedScopes === null ? null : [...normalized.allowedScopes],
355
+ policyMaxDevices: normalized.maxDevices,
356
+ policyAllowRelayPairing: normalized.allowRelayPairing,
357
+ };
358
+ }
359
+
360
+ function joinPolicyFromEvent(event, label) {
361
+ const present = JOIN_POLICY_EVENT_FIELDS.filter((field) =>
362
+ Object.prototype.hasOwnProperty.call(event, field),
363
+ );
364
+ if (present.length === 0) return null;
365
+ if (present.length !== JOIN_POLICY_EVENT_FIELDS.length) {
366
+ throw new TypeError(`${label} has an incomplete membership join policy`);
367
+ }
368
+ return normalizeJoinPolicy({
369
+ policyVersion: event.policyVersion,
370
+ allowedScopes: event.policyAllowedScopes,
371
+ maxDevices: event.policyMaxDevices,
372
+ allowRelayPairing: event.policyAllowRelayPairing,
373
+ });
374
+ }
375
+
376
+ function joinPoliciesMatch(left, right) {
377
+ return (
378
+ canonicalJson(normalizeJoinPolicy(left)) ===
379
+ canonicalJson(normalizeJoinPolicy(right))
380
+ );
381
+ }
382
+
383
+ function enforceJoinAuthority(session, { scopes, via, capabilities, policy }) {
384
+ const normalizedPolicy = normalizeJoinPolicy(policy);
385
+ if (
386
+ session.joinPolicy &&
387
+ !joinPoliciesMatch(session.joinPolicy, normalizedPolicy)
388
+ ) {
389
+ throw new Error("Remote membership policy version or authority changed");
390
+ }
391
+ if (via !== "direct" && via !== "relay") {
392
+ throw new TypeError("membership join transport is unsupported");
393
+ }
394
+ if (via === "relay" && !normalizedPolicy.allowRelayPairing) {
395
+ throw new Error("Relay pairing is disabled by membership policy");
396
+ }
397
+ const normalizedCapabilities = normalizeCapabilities(capabilities);
398
+ let grantedScopes = normalizeScopes(scopes);
399
+ if (normalizedPolicy.allowedScopes !== null) {
400
+ grantedScopes = grantedScopes.filter((scope) =>
401
+ normalizedPolicy.allowedScopes.includes(scope),
402
+ );
403
+ }
404
+ if (!normalizedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY)) {
405
+ grantedScopes = grantedScopes.filter((scope) => scope !== "approve");
406
+ }
407
+ if (grantedScopes.length === 0) {
408
+ throw new Error(
409
+ "Remote membership scopes are denied by policy or capability",
410
+ );
411
+ }
412
+ const deviceCount = [...session.members.values()].filter(
413
+ (member) =>
414
+ member.principalId !== session.hostPrincipalId &&
415
+ member.status === "active",
416
+ ).length;
417
+ if (
418
+ normalizedPolicy.maxDevices !== null &&
419
+ deviceCount >= normalizedPolicy.maxDevices
420
+ ) {
421
+ throw new Error("Remote membership device limit reached");
422
+ }
423
+ return Object.freeze({
424
+ policy: normalizedPolicy,
425
+ scopes: Object.freeze([...grantedScopes]),
426
+ capabilities: Object.freeze([...normalizedCapabilities]),
427
+ via,
428
+ });
429
+ }
430
+
247
431
  function canonicalJson(value) {
248
432
  if (
249
433
  value === null ||
@@ -469,7 +653,13 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
469
653
  if (event.schema !== EVENT_SCHEMA || !VALID_TYPES.has(event.type)) {
470
654
  throw new TypeError(`${label} has an unsupported schema or type`);
471
655
  }
472
- if (!hasExactKeys(event, EVENT_FIELDS[event.type])) {
656
+ if (
657
+ !hasExactKeys(event, EVENT_FIELDS[event.type]) &&
658
+ !(
659
+ LEGACY_EVENT_FIELDS[event.type] &&
660
+ hasExactKeys(event, LEGACY_EVENT_FIELDS[event.type])
661
+ )
662
+ ) {
473
663
  throw new TypeError(
474
664
  `${label} has non-canonical fields for ${event.type}`,
475
665
  );
@@ -554,6 +744,7 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
554
744
  createdAt: event.occurredAtMs,
555
745
  expiresAt,
556
746
  status: "active",
747
+ joinPolicy: joinPolicyFromEvent(event, label),
557
748
  members: new Map([
558
749
  [
559
750
  hostPrincipalId,
@@ -565,6 +756,7 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
565
756
  credentialType: hostCredential.type,
566
757
  credentialPublicKey: hostCredential.publicKey,
567
758
  credentialDigest: hostCredential.digest,
759
+ capabilities: [],
568
760
  },
569
761
  ],
570
762
  ]),
@@ -584,12 +776,83 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
584
776
  }
585
777
  if (
586
778
  event.type !== "session.closed" &&
779
+ event.type !== "session.reenabled" &&
587
780
  event.occurredAtMs >= session.expiresAt
588
781
  ) {
589
782
  throw new TypeError(`${label} occurs after session expiry`);
590
783
  }
591
784
 
592
- if (event.type === "member.joined") {
785
+ if (event.type === "session.reenabled") {
786
+ if (session.status !== "closed") {
787
+ throw new TypeError(`${label} re-enables an active session`);
788
+ }
789
+ const currentHost = session.members.get(session.hostPrincipalId);
790
+ if (
791
+ event.hostPrincipalId !== session.hostPrincipalId ||
792
+ !currentHost ||
793
+ event.expectedHostMembershipEpoch !== currentHost.membershipEpoch
794
+ ) {
795
+ throw new TypeError(
796
+ `${label} was not re-enabled by the current host`,
797
+ );
798
+ }
799
+ const nextSessionEpoch = String(
800
+ canonicalEpoch(event.sessionEpoch, `${label}.sessionEpoch`),
801
+ );
802
+ if (nextSessionEpoch !== String(BigInt(session.sessionEpoch) + 1n)) {
803
+ throw new TypeError(`${label} does not advance the session epoch`);
804
+ }
805
+ const nextHost = normalizePrincipalCredential(
806
+ {
807
+ type: event.nextHostCredentialType,
808
+ publicKey: event.nextHostCredentialPublicKey,
809
+ },
810
+ `${label}.nextHostCredential`,
811
+ );
812
+ if (
813
+ nextHost.digest !== event.nextHostCredentialDigest ||
814
+ nextHost.principalId !== event.nextHostPrincipalId
815
+ ) {
816
+ throw new TypeError(`${label}.nextHostPrincipalId is not key-bound`);
817
+ }
818
+ const priorNextHost = session.members.get(nextHost.principalId) || null;
819
+ const nextMembershipEpoch = String(
820
+ canonicalEpoch(event.membershipEpoch, `${label}.membershipEpoch`),
821
+ );
822
+ const expectedMembershipEpoch = priorNextHost
823
+ ? String(BigInt(priorNextHost.membershipEpoch) + 1n)
824
+ : "1";
825
+ if (nextMembershipEpoch !== expectedMembershipEpoch) {
826
+ throw new TypeError(`${label} does not advance the new host epoch`);
827
+ }
828
+ const expiresAt = safeTimestamp(event.expiresAt, `${label}.expiresAt`);
829
+ if (expiresAt <= event.occurredAtMs) {
830
+ throw new TypeError(`${label}.expiresAt must be in the future`);
831
+ }
832
+ for (const member of session.members.values()) {
833
+ if (member.status === "active") {
834
+ member.status = "revoked";
835
+ member.membershipEpoch = String(
836
+ BigInt(member.membershipEpoch) + 1n,
837
+ );
838
+ }
839
+ }
840
+ session.members.set(nextHost.principalId, {
841
+ principalId: nextHost.principalId,
842
+ membershipEpoch: nextMembershipEpoch,
843
+ status: "active",
844
+ scopes: normalizeScopes(event.scopes),
845
+ credentialType: nextHost.type,
846
+ credentialPublicKey: nextHost.publicKey,
847
+ credentialDigest: nextHost.digest,
848
+ capabilities: [],
849
+ });
850
+ session.hostPrincipalId = nextHost.principalId;
851
+ session.sessionEpoch = nextSessionEpoch;
852
+ session.expiresAt = expiresAt;
853
+ session.status = "active";
854
+ session.joinPolicy = joinPolicyFromEvent(event, label);
855
+ } else if (event.type === "member.joined") {
593
856
  if (session.status !== "active") {
594
857
  throw new TypeError(`${label} joins a closed session`);
595
858
  }
@@ -638,14 +901,56 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
638
901
  if (membershipEpoch !== expected) {
639
902
  throw new TypeError(`${label} does not advance the principal epoch`);
640
903
  }
904
+ const eventPolicy = joinPolicyFromEvent(event, label);
905
+ const joinVia = Object.prototype.hasOwnProperty.call(event, "joinVia")
906
+ ? requiredString(event.joinVia, `${label}.joinVia`, 16)
907
+ : "direct";
908
+ const capabilities = Object.prototype.hasOwnProperty.call(
909
+ event,
910
+ "capabilities",
911
+ )
912
+ ? normalizeCapabilities(event.capabilities)
913
+ : [];
914
+ const declaredScopes = normalizeScopes(event.scopes);
915
+ // Pre-capability stores could persist `approve` without proving that the
916
+ // client understood the exact approval tuple. Preserve non-approval
917
+ // membership on upgrade, but never grandfather that authority through
918
+ // replay. A legacy approve-only member remains counted as active with no
919
+ // usable scopes rather than silently freeing the device-cap slot.
920
+ const replayedScopes = eventPolicy
921
+ ? declaredScopes
922
+ : declaredScopes.filter((scope) => scope !== "approve");
923
+ if (eventPolicy) {
924
+ if (
925
+ session.joinPolicy &&
926
+ !joinPoliciesMatch(session.joinPolicy, eventPolicy)
927
+ ) {
928
+ throw new TypeError(`${label} changes the session join policy`);
929
+ }
930
+ session.joinPolicy = eventPolicy;
931
+ const authority = enforceJoinAuthority(session, {
932
+ scopes: event.scopes,
933
+ via: joinVia,
934
+ capabilities,
935
+ policy: eventPolicy,
936
+ });
937
+ if (
938
+ canonicalJson(authority.scopes) !== canonicalJson(declaredScopes)
939
+ ) {
940
+ throw new TypeError(
941
+ `${label} grants scopes outside join authority`,
942
+ );
943
+ }
944
+ }
641
945
  session.members.set(principalId, {
642
946
  principalId,
643
947
  membershipEpoch,
644
948
  status: "active",
645
- scopes: normalizeScopes(event.scopes),
949
+ scopes: replayedScopes,
646
950
  credentialType: credential.type,
647
951
  credentialPublicKey: credential.publicKey,
648
952
  credentialDigest: credential.digest,
953
+ capabilities,
649
954
  });
650
955
  } else if (event.type === "member.revoked") {
651
956
  if (session.status !== "active") {
@@ -1461,6 +1766,11 @@ export class DurableRemoteMembershipCoordinator {
1461
1766
  member,
1462
1767
  credential,
1463
1768
  scopes = null,
1769
+ capabilities = null,
1770
+ joinPolicy = null,
1771
+ joinVia = null,
1772
+ nextCredential = null,
1773
+ nextSessionExpiresAt = null,
1464
1774
  connectionNonce,
1465
1775
  now,
1466
1776
  ttlMs = null,
@@ -1506,6 +1816,20 @@ export class DurableRemoteMembershipCoordinator {
1506
1816
  connectionNonce: requiredString(connectionNonce, "connectionNonce", 4096),
1507
1817
  serverNonce: requiredString(this._createSecret(), "serverNonce", 4096),
1508
1818
  scopes: scopes === null ? null : Object.freeze([...scopes]),
1819
+ capabilities:
1820
+ capabilities === null
1821
+ ? null
1822
+ : Object.freeze([...normalizeCapabilities(capabilities)]),
1823
+ joinPolicy:
1824
+ joinPolicy === null
1825
+ ? null
1826
+ : Object.freeze({ ...normalizeJoinPolicy(joinPolicy) }),
1827
+ joinVia,
1828
+ nextCredentialType: nextCredential?.type || null,
1829
+ nextCredentialPublicKey: nextCredential?.publicKey || null,
1830
+ nextCredentialKeySha256: nextCredential?.digest || null,
1831
+ nextPrincipalId: nextCredential?.principalId || null,
1832
+ nextSessionExpiresAt,
1509
1833
  issuedAtMs: now,
1510
1834
  expiresAtMs,
1511
1835
  });
@@ -1520,12 +1844,16 @@ export class DurableRemoteMembershipCoordinator {
1520
1844
  credentialPublicKey,
1521
1845
  connectionNonce,
1522
1846
  ttlMs = null,
1847
+ capabilities = null,
1848
+ joinPolicy = null,
1523
1849
  } = {}) {
1524
1850
  const normalizedSessionId = requiredString(sessionId, "sessionId");
1525
1851
  const sessionEpoch = String(
1526
1852
  canonicalEpoch(expectedSessionEpoch, "expectedSessionEpoch"),
1527
1853
  );
1528
1854
  const normalizedScopes = normalizeScopes(scopes);
1855
+ const normalizedCapabilities = normalizeCapabilities(capabilities);
1856
+ const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
1529
1857
  const credential = normalizePrincipalCredential(
1530
1858
  { type: "ed25519", publicKey: credentialPublicKey },
1531
1859
  "credential",
@@ -1544,6 +1872,12 @@ export class DurableRemoteMembershipCoordinator {
1544
1872
  if (prior?.status === "active") {
1545
1873
  throw new Error("Remote membership principal is already active");
1546
1874
  }
1875
+ enforceJoinAuthority(session, {
1876
+ scopes: normalizedScopes,
1877
+ via: "direct",
1878
+ capabilities: normalizedCapabilities,
1879
+ policy: normalizedJoinPolicy,
1880
+ });
1547
1881
  return this._newAuthenticationChallenge({
1548
1882
  purpose: "member.join",
1549
1883
  session,
@@ -1554,6 +1888,9 @@ export class DurableRemoteMembershipCoordinator {
1554
1888
  },
1555
1889
  credential,
1556
1890
  scopes: normalizedScopes,
1891
+ capabilities: normalizedCapabilities,
1892
+ joinPolicy: normalizedJoinPolicy,
1893
+ joinVia: "direct",
1557
1894
  connectionNonce,
1558
1895
  now,
1559
1896
  ttlMs,
@@ -1580,7 +1917,10 @@ export class DurableRemoteMembershipCoordinator {
1580
1917
  member.status !== "active" ||
1581
1918
  member.credentialType !== "ed25519"
1582
1919
  ) {
1583
- throw new Error("Remote membership resume challenge denied");
1920
+ throw coordinatorError(
1921
+ REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE,
1922
+ "Remote membership is not active for resume",
1923
+ );
1584
1924
  }
1585
1925
  return this._newAuthenticationChallenge({
1586
1926
  purpose: "session.resume",
@@ -1600,6 +1940,65 @@ export class DurableRemoteMembershipCoordinator {
1600
1940
  });
1601
1941
  }
1602
1942
 
1943
+ issueSessionReenableChallenge({
1944
+ sessionId,
1945
+ principalId,
1946
+ connectionNonce,
1947
+ newHostCredentialPublicKeySpki = null,
1948
+ scopes,
1949
+ expiresAt,
1950
+ joinPolicy = null,
1951
+ ttlMs = null,
1952
+ } = {}) {
1953
+ const normalizedSessionId = requiredString(sessionId, "sessionId");
1954
+ const normalizedPrincipal = requiredString(principalId, "principalId");
1955
+ const normalizedScopes = normalizeScopes(scopes);
1956
+ const normalizedExpiresAt = safeTimestamp(expiresAt, "expiresAt");
1957
+ const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
1958
+ return this._withRead((snapshot, now) => {
1959
+ const session = snapshot.sessions.get(normalizedSessionId);
1960
+ const member = session?.members.get(normalizedPrincipal);
1961
+ if (
1962
+ !session ||
1963
+ session.status !== "closed" ||
1964
+ session.hostPrincipalId !== normalizedPrincipal ||
1965
+ !member ||
1966
+ member.status !== "active" ||
1967
+ member.credentialType !== "ed25519" ||
1968
+ normalizedExpiresAt <= now
1969
+ ) {
1970
+ throw new Error("Remote membership session re-enable challenge denied");
1971
+ }
1972
+ const nextCredential = normalizePrincipalCredential(
1973
+ {
1974
+ type: "ed25519",
1975
+ publicKey:
1976
+ newHostCredentialPublicKeySpki || member.credentialPublicKey,
1977
+ },
1978
+ "nextHostCredential",
1979
+ );
1980
+ return this._newAuthenticationChallenge({
1981
+ purpose: "session.reenable",
1982
+ session,
1983
+ member,
1984
+ credential: normalizePrincipalCredential(
1985
+ {
1986
+ type: member.credentialType,
1987
+ publicKey: member.credentialPublicKey,
1988
+ },
1989
+ "memberCredential",
1990
+ ),
1991
+ scopes: normalizedScopes,
1992
+ joinPolicy: normalizedJoinPolicy,
1993
+ nextCredential,
1994
+ nextSessionExpiresAt: normalizedExpiresAt,
1995
+ connectionNonce,
1996
+ now,
1997
+ ttlMs,
1998
+ });
1999
+ });
2000
+ }
2001
+
1603
2002
  _consumeAuthenticationChallenge(
1604
2003
  snapshot,
1605
2004
  now,
@@ -1622,10 +2021,11 @@ export class DurableRemoteMembershipCoordinator {
1622
2021
  throw new Error("Remote membership authentication challenge denied");
1623
2022
  }
1624
2023
  const session = snapshot.sessions.get(challenge.sessionId);
2024
+ const reenable = expectedPurpose === "session.reenable";
1625
2025
  if (
1626
2026
  !session ||
1627
- session.status !== "active" ||
1628
- session.expiresAt <= now ||
2027
+ (reenable ? session.status !== "closed" : session.status !== "active") ||
2028
+ (!reenable && session.expiresAt <= now) ||
1629
2029
  session.sessionEpoch !== challenge.sessionEpoch
1630
2030
  ) {
1631
2031
  throw new Error("Remote membership authentication session is stale");
@@ -1643,7 +2043,8 @@ export class DurableRemoteMembershipCoordinator {
1643
2043
  !current ||
1644
2044
  current.status !== "active" ||
1645
2045
  current.membershipEpoch !== challenge.membershipEpoch ||
1646
- current.credentialDigest !== challenge.credentialKeySha256
2046
+ current.credentialDigest !== challenge.credentialKeySha256 ||
2047
+ (reenable && session.hostPrincipalId !== challenge.principalId)
1647
2048
  ) {
1648
2049
  throw new Error("Remote membership resume challenge is stale");
1649
2050
  }
@@ -1685,11 +2086,13 @@ export class DurableRemoteMembershipCoordinator {
1685
2086
  expiresAt,
1686
2087
  hostPrincipalId = null,
1687
2088
  hostCredentialPublicKeySpki,
2089
+ joinPolicy = null,
1688
2090
  } = {}) {
1689
2091
  const normalizedSessionId = requiredString(sessionId, "sessionId");
1690
2092
  const normalizedAgent = requiredString(agentSessionId, "agentSessionId");
1691
2093
  const normalizedScopes = normalizeScopes(scopes);
1692
2094
  const normalizedExpiresAt = safeTimestamp(expiresAt, "expiresAt");
2095
+ const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
1693
2096
  const hostCredential = normalizePrincipalCredential(
1694
2097
  {
1695
2098
  type: "ed25519",
@@ -1722,6 +2125,7 @@ export class DurableRemoteMembershipCoordinator {
1722
2125
  hostCredentialDigest: hostCredential.digest,
1723
2126
  sessionEpoch: "1",
1724
2127
  membershipEpoch: "1",
2128
+ ...joinPolicyEventFields(normalizedJoinPolicy),
1725
2129
  scopes: normalizedScopes,
1726
2130
  expiresAt: normalizedExpiresAt,
1727
2131
  };
@@ -1774,6 +2178,73 @@ export class DurableRemoteMembershipCoordinator {
1774
2178
  }
1775
2179
  }
1776
2180
 
2181
+ reenableSession({ challengeId, connectionNonce, signature } = {}) {
2182
+ const result = this._mutate((snapshot, _generation, now) => {
2183
+ const authenticated = this._consumeAuthenticationChallenge(
2184
+ snapshot,
2185
+ now,
2186
+ { challengeId, connectionNonce, signature },
2187
+ "session.reenable",
2188
+ );
2189
+ const { challenge, session } = authenticated;
2190
+ const nextCredential = normalizePrincipalCredential(
2191
+ {
2192
+ type: challenge.nextCredentialType,
2193
+ publicKey: challenge.nextCredentialPublicKey,
2194
+ },
2195
+ "nextHostCredential",
2196
+ );
2197
+ if (
2198
+ nextCredential.digest !== challenge.nextCredentialKeySha256 ||
2199
+ nextCredential.principalId !== challenge.nextPrincipalId ||
2200
+ challenge.nextSessionExpiresAt <= now
2201
+ ) {
2202
+ throw new Error("Remote membership session re-enable binding changed");
2203
+ }
2204
+ const priorNextHost =
2205
+ session.members.get(nextCredential.principalId) || null;
2206
+ return {
2207
+ type: "session.reenabled",
2208
+ sessionId: session.sessionId,
2209
+ expectedSessionEpoch: session.sessionEpoch,
2210
+ hostPrincipalId: session.hostPrincipalId,
2211
+ expectedHostMembershipEpoch: challenge.membershipEpoch,
2212
+ nextHostPrincipalId: nextCredential.principalId,
2213
+ nextHostCredentialType: nextCredential.type,
2214
+ nextHostCredentialPublicKey: nextCredential.publicKey,
2215
+ nextHostCredentialDigest: nextCredential.digest,
2216
+ sessionEpoch: String(BigInt(session.sessionEpoch) + 1n),
2217
+ membershipEpoch: priorNextHost
2218
+ ? String(BigInt(priorNextHost.membershipEpoch) + 1n)
2219
+ : "1",
2220
+ scopes: [...challenge.scopes],
2221
+ expiresAt: challenge.nextSessionExpiresAt,
2222
+ ...joinPolicyEventFields(challenge.joinPolicy),
2223
+ };
2224
+ });
2225
+ const session = result.snapshot.sessions.get(result.event.sessionId);
2226
+ const host = session.members.get(session.hostPrincipalId);
2227
+ return Object.freeze({
2228
+ authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
2229
+ trust: this.trustDescriptor(),
2230
+ sessionId: session.sessionId,
2231
+ sessionEpoch: session.sessionEpoch,
2232
+ hostPrincipalId: session.hostPrincipalId,
2233
+ membershipEpoch: host.membershipEpoch,
2234
+ hostCredentialPublicKeySpki: host.credentialPublicKey,
2235
+ nextConnectionNonce: requiredString(
2236
+ this._createSecret(),
2237
+ "nextConnectionNonce",
2238
+ 4096,
2239
+ ),
2240
+ statement: this._snapshotStatement(
2241
+ session,
2242
+ result.snapshot.generation,
2243
+ result.event.occurredAtMs,
2244
+ ),
2245
+ });
2246
+ }
2247
+
1777
2248
  joinMember({ challengeId, connectionNonce, signature } = {}) {
1778
2249
  const result = this._mutate((snapshot, _generation, now) => {
1779
2250
  const authenticated = this._consumeAuthenticationChallenge(
@@ -1786,6 +2257,12 @@ export class DurableRemoteMembershipCoordinator {
1786
2257
  if (session.hostPrincipalId === challenge.principalId) {
1787
2258
  throw new Error("Cannot replace the host membership");
1788
2259
  }
2260
+ const authority = enforceJoinAuthority(session, {
2261
+ scopes: challenge.scopes,
2262
+ via: challenge.joinVia,
2263
+ capabilities: challenge.capabilities,
2264
+ policy: challenge.joinPolicy,
2265
+ });
1789
2266
  return {
1790
2267
  type: "member.joined",
1791
2268
  sessionId: session.sessionId,
@@ -1796,7 +2273,10 @@ export class DurableRemoteMembershipCoordinator {
1796
2273
  credentialType: credential.type,
1797
2274
  credentialPublicKey: credential.publicKey,
1798
2275
  credentialDigest: credential.digest,
1799
- scopes: challenge.scopes,
2276
+ ...joinPolicyEventFields(authority.policy),
2277
+ joinVia: authority.via,
2278
+ capabilities: [...authority.capabilities],
2279
+ scopes: [...authority.scopes],
1800
2280
  };
1801
2281
  });
1802
2282
  const session = result.snapshot.sessions.get(result.event.sessionId);
@@ -1808,6 +2288,8 @@ export class DurableRemoteMembershipCoordinator {
1808
2288
  credentialPublicKey: member.credentialPublicKey,
1809
2289
  sessionEpoch: session.sessionEpoch,
1810
2290
  membershipEpoch: member.membershipEpoch,
2291
+ scopes: Object.freeze([...member.scopes]),
2292
+ capabilities: Object.freeze([...(member.capabilities || [])]),
1811
2293
  nextConnectionNonce: requiredString(
1812
2294
  this._createSecret(),
1813
2295
  "nextConnectionNonce",
@@ -1829,12 +2311,16 @@ export class DurableRemoteMembershipCoordinator {
1829
2311
  mobilePublicKey,
1830
2312
  pairingTokenDigest,
1831
2313
  possessionCapability,
2314
+ capabilities = null,
2315
+ joinPolicy = null,
1832
2316
  } = {}) {
1833
2317
  const normalizedSessionId = requiredString(sessionId, "sessionId");
1834
2318
  const sessionEpoch = String(
1835
2319
  canonicalEpoch(expectedSessionEpoch, "expectedSessionEpoch"),
1836
2320
  );
1837
2321
  const normalizedScopes = normalizeScopes(scopes);
2322
+ const normalizedCapabilities = normalizeCapabilities(capabilities);
2323
+ const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
1838
2324
  const normalizedPeer = requiredString(mobilePeerId, "mobilePeerId");
1839
2325
  if (!DIGEST_RE.test(String(pairingTokenDigest))) {
1840
2326
  throw new TypeError("pairingTokenDigest must be a SHA-256 digest");
@@ -1880,6 +2366,12 @@ export class DurableRemoteMembershipCoordinator {
1880
2366
  "Remote relay transcript proof is malformed, expired, or over-scoped",
1881
2367
  );
1882
2368
  }
2369
+ const authority = enforceJoinAuthority(session, {
2370
+ scopes: normalizedScopes,
2371
+ via: "relay",
2372
+ capabilities: normalizedCapabilities,
2373
+ policy: normalizedJoinPolicy,
2374
+ });
1883
2375
  return {
1884
2376
  type: "member.joined",
1885
2377
  sessionId: normalizedSessionId,
@@ -1892,7 +2384,10 @@ export class DurableRemoteMembershipCoordinator {
1892
2384
  credentialType: credential.type,
1893
2385
  credentialPublicKey: credential.publicKey,
1894
2386
  credentialDigest: credential.digest,
1895
- scopes: normalizedScopes,
2387
+ ...joinPolicyEventFields(authority.policy),
2388
+ joinVia: authority.via,
2389
+ capabilities: [...authority.capabilities],
2390
+ scopes: [...authority.scopes],
1896
2391
  };
1897
2392
  });
1898
2393
  const session = result.snapshot.sessions.get(normalizedSessionId);
@@ -1904,6 +2399,8 @@ export class DurableRemoteMembershipCoordinator {
1904
2399
  credentialPublicKey: member.credentialPublicKey,
1905
2400
  sessionEpoch: session.sessionEpoch,
1906
2401
  membershipEpoch: member.membershipEpoch,
2402
+ scopes: Object.freeze([...member.scopes]),
2403
+ capabilities: Object.freeze([...(member.capabilities || [])]),
1907
2404
  statement: this._snapshotStatement(
1908
2405
  session,
1909
2406
  result.snapshot.generation,
@@ -2036,41 +2533,81 @@ export class DurableRemoteMembershipCoordinator {
2036
2533
  "expectedHostMembershipEpoch",
2037
2534
  ),
2038
2535
  );
2039
- const result = this._mutate((snapshot) => {
2040
- const session = snapshot.sessions.get(normalizedSessionId);
2041
- const host = session?.members.get(normalizedHost);
2042
- if (
2043
- !session ||
2044
- session.status !== "active" ||
2045
- session.hostPrincipalId !== normalizedHost ||
2046
- session.sessionEpoch !== sessionEpoch ||
2047
- !host ||
2048
- host.status !== "active" ||
2049
- host.membershipEpoch !== hostMembershipEpoch
2050
- ) {
2051
- throw new Error("Remote membership session close denied");
2052
- }
2053
- const cancelledLeaseIds = [...session.leases.values()]
2054
- .filter(
2055
- (lease) => lease.status === "active" || lease.status === "acked",
2056
- )
2057
- .map((lease) => lease.leaseId)
2058
- .sort();
2059
- return {
2060
- type: "session.closed",
2061
- sessionId: normalizedSessionId,
2062
- expectedSessionEpoch: sessionEpoch,
2063
- hostPrincipalId: normalizedHost,
2064
- expectedHostMembershipEpoch: hostMembershipEpoch,
2065
- sessionEpoch: String(BigInt(sessionEpoch) + 1n),
2066
- cancelledLeaseIds,
2067
- };
2068
- });
2536
+ let result;
2537
+ try {
2538
+ result = this._mutate((snapshot) => {
2539
+ const session = snapshot.sessions.get(normalizedSessionId);
2540
+ const host = session?.members.get(normalizedHost);
2541
+ if (
2542
+ !session ||
2543
+ session.status !== "active" ||
2544
+ session.hostPrincipalId !== normalizedHost ||
2545
+ session.sessionEpoch !== sessionEpoch ||
2546
+ !host ||
2547
+ host.status !== "active" ||
2548
+ host.membershipEpoch !== hostMembershipEpoch
2549
+ ) {
2550
+ throw new Error("Remote membership session close denied");
2551
+ }
2552
+ const cancelledLeaseIds = [...session.leases.values()]
2553
+ .filter(
2554
+ (lease) => lease.status === "active" || lease.status === "acked",
2555
+ )
2556
+ .map((lease) => lease.leaseId)
2557
+ .sort();
2558
+ return {
2559
+ type: "session.closed",
2560
+ sessionId: normalizedSessionId,
2561
+ expectedSessionEpoch: sessionEpoch,
2562
+ hostPrincipalId: normalizedHost,
2563
+ expectedHostMembershipEpoch: hostMembershipEpoch,
2564
+ sessionEpoch: String(BigInt(sessionEpoch) + 1n),
2565
+ cancelledLeaseIds,
2566
+ };
2567
+ });
2568
+ } catch (cause) {
2569
+ if (cause?.commitState === "unknown") throw cause;
2570
+ const terminal = this._withRead((snapshot, now) => {
2571
+ const session = snapshot.sessions.get(normalizedSessionId);
2572
+ const host = session?.members.get(normalizedHost);
2573
+ if (
2574
+ !session ||
2575
+ session.status !== "closed" ||
2576
+ session.hostPrincipalId !== normalizedHost ||
2577
+ session.sessionEpoch !== String(BigInt(sessionEpoch) + 1n) ||
2578
+ !host ||
2579
+ host.membershipEpoch !== hostMembershipEpoch
2580
+ ) {
2581
+ return null;
2582
+ }
2583
+ return Object.freeze({
2584
+ authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
2585
+ sessionEpoch: session.sessionEpoch,
2586
+ cancelledLeaseIds: Object.freeze(
2587
+ [...session.leases.values()]
2588
+ .filter(
2589
+ (lease) =>
2590
+ lease.cancelReason === "session-closed" &&
2591
+ lease.terminalGeneration === String(snapshot.generation),
2592
+ )
2593
+ .map((lease) => lease.leaseId)
2594
+ .sort(),
2595
+ ),
2596
+ terminal: true,
2597
+ alreadyClosed: true,
2598
+ statement: this._snapshotStatement(session, snapshot.generation, now),
2599
+ });
2600
+ });
2601
+ if (terminal) return terminal;
2602
+ throw cause;
2603
+ }
2069
2604
  const session = result.snapshot.sessions.get(normalizedSessionId);
2070
2605
  return Object.freeze({
2071
2606
  authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
2072
2607
  sessionEpoch: session.sessionEpoch,
2073
2608
  cancelledLeaseIds: Object.freeze([...result.event.cancelledLeaseIds]),
2609
+ terminal: true,
2610
+ alreadyClosed: false,
2074
2611
  statement: this._snapshotStatement(
2075
2612
  session,
2076
2613
  result.snapshot.generation,
@@ -2397,6 +2934,35 @@ export class DurableRemoteMembershipCoordinator {
2397
2934
  });
2398
2935
  });
2399
2936
  }
2937
+
2938
+ getSessionSnapshot(sessionId) {
2939
+ const normalizedSessionId = requiredString(sessionId, "sessionId");
2940
+ return this._withRead((snapshot, now) => {
2941
+ const session = snapshot.sessions.get(normalizedSessionId);
2942
+ if (!session) return null;
2943
+ return Object.freeze({
2944
+ session: publicSessionSnapshot(session, snapshot.generation),
2945
+ statement: this._snapshotStatement(session, snapshot.generation, now),
2946
+ });
2947
+ });
2948
+ }
2949
+
2950
+ /**
2951
+ * Enumerate coordinator-owned session snapshots for server-process recovery.
2952
+ * The caller receives immutable public projections only; live transport
2953
+ * attachment remains a registry concern and is deliberately not persisted
2954
+ * here. Reading also exercises the normal clock/witness rollback checks.
2955
+ */
2956
+ listSessionSnapshots({ activeOnly = true } = {}) {
2957
+ return this._withRead((snapshot) =>
2958
+ Object.freeze(
2959
+ [...snapshot.sessions.values()]
2960
+ .filter((session) => !activeOnly || session.status === "active")
2961
+ .map((session) => publicSessionSnapshot(session, snapshot.generation))
2962
+ .sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
2963
+ ),
2964
+ );
2965
+ }
2400
2966
  }
2401
2967
 
2402
2968
  export const _remoteMembershipCoordinatorInternals = Object.freeze({