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
@@ -28,12 +28,20 @@ import {
28
28
  ApprovalAuthorityStore,
29
29
  defaultApprovalAuthorityStatePath,
30
30
  } from "./approval-authority-store.js";
31
- import { OperationApprovalRegistry } from "./operation-fingerprint.js";
32
31
  import {
33
- DurableRemoteMembershipAuthority,
34
- REMOTE_MEMBERSHIP_AUTHORITY_UNAVAILABLE_CODE,
35
- REMOTE_MEMBERSHIP_AUTHORITY_VERSION,
36
- } from "./remote-membership-authority.js";
32
+ computeOperationFingerprint,
33
+ fingerprintsMatch,
34
+ OperationApprovalRegistry,
35
+ } from "./operation-fingerprint.js";
36
+ import {
37
+ createRemoteMembershipPrincipalCredential,
38
+ REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE,
39
+ REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
40
+ } from "./remote-membership-coordinator.js";
41
+ import {
42
+ DurableRemoteMembershipHostStore,
43
+ REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE,
44
+ } from "./remote-membership-host-store.js";
37
45
  import {
38
46
  buildDirectPairingUri,
39
47
  pickLanAddress,
@@ -42,6 +50,34 @@ import {
42
50
  } from "./remote-control.js";
43
51
 
44
52
  const DEFAULT_DECISION_TIMEOUT_MS = 5 * 60 * 1000;
53
+ const REMOTE_LEASE_AUTHORIZATION_KIND =
54
+ "chainlesschain.remote-approval-lease-authorization/v1";
55
+ const SUPPORTED_REMOTE_LEASE_TOOLS = new Set(["run_shell"]);
56
+ const remoteLeaseAuthorizations = new WeakMap();
57
+
58
+ function buildApprovalBinding({
59
+ requestId,
60
+ tool,
61
+ action,
62
+ params,
63
+ workspace,
64
+ session,
65
+ targetEnv,
66
+ policyVersion,
67
+ }) {
68
+ return approvalBindingDigest({
69
+ toolCallId: requestId,
70
+ args: {
71
+ tool: tool || null,
72
+ action: action || null,
73
+ params: params ?? null,
74
+ workspace: workspace || null,
75
+ session: session || null,
76
+ targetEnv: targetEnv || null,
77
+ },
78
+ policyDigest: policyVersion || null,
79
+ });
80
+ }
45
81
 
46
82
  export class RemoteApprovalBridge {
47
83
  constructor({
@@ -55,8 +91,11 @@ export class RemoteApprovalBridge {
55
91
  now = Date.now,
56
92
  approvalStore = undefined,
57
93
  approvalStateFile = null,
58
- membershipAuthority = undefined,
59
- membershipStateFile = null,
94
+ membershipHostStore = undefined,
95
+ membershipHostStateFile = null,
96
+ membershipHostWitnessFile = null,
97
+ membershipHostAuthorityLockFile = null,
98
+ expectedCoordinatorPublicKeySha256 = null,
60
99
  onSecurityError = null,
61
100
  } = {}) {
62
101
  if (!wsUrl) throw new Error("wsUrl is required");
@@ -78,13 +117,28 @@ export class RemoteApprovalBridge {
78
117
  this._securityErrors = [];
79
118
  this._onSecurityError =
80
119
  typeof onSecurityError === "function" ? onSecurityError : null;
81
- this._membershipAuthority =
82
- membershipAuthority === undefined
120
+ this._membershipHostStore =
121
+ membershipHostStore === undefined
83
122
  ? () =>
84
- new DurableRemoteMembershipAuthority(
85
- membershipStateFile ? { filePath: membershipStateFile } : {},
86
- )
87
- : membershipAuthority;
123
+ new DurableRemoteMembershipHostStore({
124
+ agentSessionId,
125
+ ...(membershipHostStateFile
126
+ ? { stateFile: membershipHostStateFile }
127
+ : {}),
128
+ ...(membershipHostWitnessFile
129
+ ? { witnessFile: membershipHostWitnessFile }
130
+ : {}),
131
+ ...(membershipHostAuthorityLockFile
132
+ ? { authorityLockFile: membershipHostAuthorityLockFile }
133
+ : {}),
134
+ ...(expectedCoordinatorPublicKeySha256
135
+ ? {
136
+ expectedPublicKeySha256: expectedCoordinatorPublicKeySha256,
137
+ }
138
+ : {}),
139
+ now: () => this._now(),
140
+ })
141
+ : membershipHostStore;
88
142
  const durableStore =
89
143
  approvalStore === undefined
90
144
  ? new ApprovalAuthorityStore({
@@ -114,21 +168,22 @@ export class RemoteApprovalBridge {
114
168
  this._approvalStore = durableStore;
115
169
  }
116
170
 
117
- _requireMembershipAuthority() {
118
- if (typeof this._membershipAuthority === "function") {
119
- this._membershipAuthority = this._membershipAuthority();
171
+ _requireMembershipHostStore() {
172
+ if (typeof this._membershipHostStore === "function") {
173
+ this._membershipHostStore = this._membershipHostStore();
120
174
  }
121
175
  if (
122
- !this._membershipAuthority ||
123
- typeof this._membershipAuthority.withActiveMembership !== "function"
176
+ !this._membershipHostStore ||
177
+ typeof this._membershipHostStore.adopt !== "function" ||
178
+ typeof this._membershipHostStore.requireConsumableLease !== "function"
124
179
  ) {
125
180
  const error = new Error(
126
- "Durable Remote membership authority is unavailable; remote approval is denied",
181
+ "Durable Remote membership host store is unavailable; remote approval is denied",
127
182
  );
128
- error.code = REMOTE_MEMBERSHIP_AUTHORITY_UNAVAILABLE_CODE;
183
+ error.code = REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE;
129
184
  throw error;
130
185
  }
131
- return this._membershipAuthority;
186
+ return this._membershipHostStore;
132
187
  }
133
188
 
134
189
  _recordSecurityError({ action, requestId = null, reason, errorCode = null }) {
@@ -165,24 +220,126 @@ export class RemoteApprovalBridge {
165
220
  this.client = this._createClient(this.wsUrl);
166
221
  await this.client.connect();
167
222
  await this.client.auth(this.token);
168
- this.client.onEvent((message) => this._onServerEvent(message));
169
- const created = await this.client.request("remote-session-create", {
170
- sessionId: this.agentSessionId,
171
- name: this.name,
172
- scopes: this.scopes,
173
- requireDurableMembershipAuthority: true,
223
+ this.client.onEvent((message) => {
224
+ Promise.resolve(this._onServerEvent(message)).catch((error) => {
225
+ this._recordSecurityError({
226
+ action: "approval.resolve",
227
+ reason: "host-event-processing-failed",
228
+ errorCode: error?.code || null,
229
+ });
230
+ });
174
231
  });
232
+ const hostStore = this._requireMembershipHostStore();
233
+ const bootstrap = hostStore.getBootstrap();
234
+ let pairing = null;
235
+ if (bootstrap) {
236
+ if (bootstrap.agentSessionId !== this.agentSessionId) {
237
+ throw new Error(
238
+ "Durable Remote host bootstrap belongs to another agent session",
239
+ );
240
+ }
241
+ this.remoteSessionId = bootstrap.sessionId;
242
+ } else {
243
+ const credential = createRemoteMembershipPrincipalCredential();
244
+ const created = await this.client.request("remote-session-create", {
245
+ sessionId: this.agentSessionId,
246
+ name: this.name,
247
+ scopes: this.scopes,
248
+ requireDurableMembershipAuthority: true,
249
+ hostCredentialPublicKeySpki: credential.publicKey,
250
+ });
251
+ if (
252
+ created.session?.membershipAuthority !==
253
+ REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
254
+ typeof created.session?.sessionEpoch !== "string" ||
255
+ !created.bootstrap?.trust ||
256
+ !created.bootstrap?.statement
257
+ ) {
258
+ throw new Error(
259
+ "Remote Session server did not establish a signed durable membership",
260
+ );
261
+ }
262
+ hostStore.pinTrust(created.bootstrap.trust);
263
+ hostStore.recordBootstrap({
264
+ coordinatorId: created.bootstrap.trust.coordinatorId,
265
+ sessionId: created.session.sessionId,
266
+ agentSessionId: this.agentSessionId,
267
+ hostPrincipalId: created.bootstrap.hostPrincipalId,
268
+ hostCredentialPublicKeySpki:
269
+ created.bootstrap.hostCredentialPublicKeySpki,
270
+ hostCredentialPrivateKeyPkcs8: credential.privateKeyPkcs8,
271
+ statement: created.bootstrap.statement,
272
+ });
273
+ hostStore.adopt(created.bootstrap.statement, {
274
+ expectedKind: "session.snapshot",
275
+ expectedSessionId: created.session.sessionId,
276
+ });
277
+ this.remoteSessionId = created.session.sessionId;
278
+ pairing = created.pairing;
279
+ }
280
+ const currentBootstrap = hostStore.getBootstrap();
281
+ let resumed;
282
+ try {
283
+ const challenged = await this.client.request(
284
+ "remote-session-resume-challenge",
285
+ {
286
+ remoteSessionId: this.remoteSessionId,
287
+ principalId: currentBootstrap.hostPrincipalId,
288
+ },
289
+ );
290
+ const signature = hostStore.signAuthenticationChallenge(
291
+ challenged.challenge,
292
+ );
293
+ resumed = await this.client.request("remote-session-resume", {
294
+ remoteSessionId: this.remoteSessionId,
295
+ challengeId: challenged.challenge.challengeId,
296
+ signature,
297
+ });
298
+ } catch (resumeError) {
299
+ if (!bootstrap) throw resumeError;
300
+ const terminal = await this.client.request("remote-session-terminal", {
301
+ remoteSessionId: this.remoteSessionId,
302
+ principalId: currentBootstrap.hostPrincipalId,
303
+ });
304
+ hostStore.adopt(terminal.statement, {
305
+ expectedKind: "session.snapshot",
306
+ expectedSessionId: this.remoteSessionId,
307
+ });
308
+ const challenged = await this.client.request(
309
+ "remote-session-reenable-challenge",
310
+ {
311
+ remoteSessionId: this.remoteSessionId,
312
+ principalId: currentBootstrap.hostPrincipalId,
313
+ },
314
+ );
315
+ const signature = hostStore.signAuthenticationChallenge(
316
+ challenged.challenge,
317
+ );
318
+ resumed = await this.client.request("remote-session-reenable", {
319
+ remoteSessionId: this.remoteSessionId,
320
+ challengeId: challenged.challenge.challengeId,
321
+ signature,
322
+ });
323
+ }
175
324
  if (
176
- created.session?.membershipAuthority !==
177
- REMOTE_MEMBERSHIP_AUTHORITY_VERSION ||
178
- typeof created.session?.sessionEpoch !== "string"
325
+ resumed.session?.membershipAuthority !==
326
+ REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
327
+ resumed.member?.principalId !== currentBootstrap.hostPrincipalId
179
328
  ) {
180
- throw new Error(
181
- "Remote Session server did not establish durable membership authority",
182
- );
329
+ throw new Error("Remote Session host resume binding changed");
330
+ }
331
+ hostStore.adopt(resumed.statement, {
332
+ expectedKind: "session.snapshot",
333
+ expectedSessionId: this.remoteSessionId,
334
+ });
335
+ if (!pairing) {
336
+ const issued = await this.client.request("remote-session-pairing-token", {
337
+ remoteSessionId: this.remoteSessionId,
338
+ scopes: this.scopes,
339
+ });
340
+ pairing = issued.pairing;
183
341
  }
184
- this.remoteSessionId = created.session.sessionId;
185
- this.pairing = created.pairing;
342
+ this.pairing = pairing;
186
343
  return this;
187
344
  }
188
345
 
@@ -202,6 +359,7 @@ export class RemoteApprovalBridge {
202
359
  pairingToken: this.pairing.token,
203
360
  scopes: this.pairing.scopes,
204
361
  expiresAt: this.pairing.expiresAt,
362
+ durableMembership: true,
205
363
  });
206
364
  return {
207
365
  uri,
@@ -211,7 +369,7 @@ export class RemoteApprovalBridge {
211
369
  };
212
370
  }
213
371
 
214
- _onServerEvent(message) {
372
+ async _onServerEvent(message) {
215
373
  if (
216
374
  message?.type !== "remote-session-control" ||
217
375
  message.remoteSessionId !== this.remoteSessionId ||
@@ -231,7 +389,7 @@ export class RemoteApprovalBridge {
231
389
  return;
232
390
  }
233
391
  if (
234
- message.membershipAuthority !== REMOTE_MEMBERSHIP_AUTHORITY_VERSION ||
392
+ message.membershipAuthority !== REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
235
393
  typeof message.from !== "string" ||
236
394
  !message.from ||
237
395
  typeof message.sessionEpoch !== "string" ||
@@ -265,47 +423,85 @@ export class RemoteApprovalBridge {
265
423
  }
266
424
  const decision =
267
425
  rawAnswer === true || rawAnswer === "true" || rawAnswer === "yes";
268
- let fenced;
426
+ let ackedLease = null;
427
+ // The server creates the lease before forwarding the resolve frame. Keep a
428
+ // strictly request-bound cancellation handle even when durable adoption of
429
+ // that statement fails before it can return a trusted payload.
430
+ let leaseForFailure = this._stateFailureLeaseCandidate(
431
+ message,
432
+ requestId,
433
+ pending,
434
+ );
269
435
  try {
270
- // The membership lock encloses the approval-store CAS. Therefore a
271
- // revoke either commits first and suppresses this CAS, or waits behind an
272
- // approval that has already linearized. No server-forwarded `from`
273
- // string can authorize a side effect on its own.
274
- fenced = this._requireMembershipAuthority().withActiveMembership(
275
- {
276
- sessionId: this.remoteSessionId,
277
- principalId: message.from,
278
- sessionEpoch: message.sessionEpoch,
279
- membershipEpoch: message.membershipEpoch,
280
- },
281
- "approve",
282
- (membership) => {
283
- const authority = {
284
- origin: ORIGIN.REMOTE,
285
- authenticated: true,
286
- scopes: ["approve"],
287
- principalId: membership.principalId,
288
- sessionId: this.agentSessionId,
289
- remoteSessionId: membership.sessionId,
290
- sessionEpoch: membership.sessionEpoch,
291
- membershipEpoch: membership.membershipEpoch,
292
- };
293
- return this._registry.resolve(pending.fingerprint, {
294
- requestId,
295
- fingerprint: message.event.fingerprint,
296
- binding: message.event.binding,
297
- sessionId: this.agentSessionId,
298
- decision,
299
- authority,
300
- expectedRevision: message.event.revision,
301
- now: this._now(),
302
- });
303
- },
304
- );
436
+ if (decision) {
437
+ if (!message.approvalLeaseStatement) {
438
+ throw new Error(
439
+ "Approved remote response did not carry a signed execution lease",
440
+ );
441
+ }
442
+ const hostStore = this._requireMembershipHostStore();
443
+ const adopted = hostStore.adopt(message.approvalLeaseStatement, {
444
+ expectedKind: "lease.created",
445
+ expectedSessionId: this.remoteSessionId,
446
+ });
447
+ const createdLease = adopted.payload;
448
+ if (
449
+ createdLease.status !== "active" ||
450
+ createdLease.sessionId !== this.remoteSessionId ||
451
+ createdLease.sessionEpoch !== message.sessionEpoch ||
452
+ createdLease.principalId !== message.from ||
453
+ createdLease.membershipEpoch !== message.membershipEpoch ||
454
+ createdLease.requestId !== requestId ||
455
+ !fingerprintsMatch(createdLease.fingerprint, pending.fingerprint) ||
456
+ createdLease.fingerprint !== message.event.fingerprint ||
457
+ createdLease.binding !== pending.binding ||
458
+ createdLease.binding !== message.event.binding ||
459
+ createdLease.expiresAt > pending.descriptor.notAfter
460
+ ) {
461
+ throw new Error(
462
+ "Signed remote execution lease does not match the pending operation",
463
+ );
464
+ }
465
+ leaseForFailure = createdLease;
466
+ const bootstrap = hostStore.getBootstrap();
467
+ if (
468
+ !bootstrap ||
469
+ createdLease.hostPrincipalId !== bootstrap.hostPrincipalId
470
+ ) {
471
+ throw new Error("Signed remote execution lease targets another host");
472
+ }
473
+ const acked = await this.client.request("remote-session-lease-ack", {
474
+ remoteSessionId: this.remoteSessionId,
475
+ leaseId: createdLease.leaseId,
476
+ expectedCreatedGeneration: createdLease.createdGeneration,
477
+ hostReceiptDigest: adopted.receiptHash,
478
+ });
479
+ const ackReceipt = hostStore.adopt(acked.statement, {
480
+ expectedKind: "lease.acked",
481
+ expectedSessionId: this.remoteSessionId,
482
+ });
483
+ ackedLease = ackReceipt.payload;
484
+ leaseForFailure = ackedLease;
485
+ if (
486
+ acked.dispatchAuthorized === true ||
487
+ ackedLease.status !== "acked" ||
488
+ ackedLease.leaseId !== createdLease.leaseId ||
489
+ ackedLease.createdGeneration !== createdLease.createdGeneration ||
490
+ ackedLease.principalId !== createdLease.principalId ||
491
+ ackedLease.membershipEpoch !== createdLease.membershipEpoch ||
492
+ ackedLease.requestId !== createdLease.requestId ||
493
+ ackedLease.fingerprint !== createdLease.fingerprint ||
494
+ ackedLease.binding !== createdLease.binding ||
495
+ ackedLease.expiresAt !== createdLease.expiresAt
496
+ ) {
497
+ throw new Error("Remote execution lease ACK binding changed");
498
+ }
499
+ }
305
500
  } catch (error) {
306
501
  const verdict = {
307
- reason: "membership-state-unavailable",
308
- errorCode: error?.code || REMOTE_MEMBERSHIP_AUTHORITY_UNAVAILABLE_CODE,
502
+ reason: `lease-adopt-or-ack-failed:${error?.cause?.message || error?.message || "unknown"}`,
503
+ errorCode:
504
+ error?.code || REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE,
309
505
  };
310
506
  this._recordSecurityError({
311
507
  action: "approval.resolve",
@@ -313,18 +509,31 @@ export class RemoteApprovalBridge {
313
509
  reason: verdict.reason,
314
510
  errorCode: verdict.errorCode,
315
511
  });
316
- this._settleDeniedForStateFailure(requestId, pending, verdict);
317
- return;
318
- }
319
- if (!fenced.ok) {
320
- this._recordSecurityError({
321
- action: "approval.resolve",
322
- requestId,
323
- reason: fenced.reason,
512
+ await this._settleDeniedForStateFailure(requestId, pending, verdict, {
513
+ lease: leaseForFailure,
324
514
  });
325
515
  return;
326
516
  }
327
- const verdict = fenced.value;
517
+ const authority = {
518
+ origin: ORIGIN.REMOTE,
519
+ authenticated: true,
520
+ scopes: ["approve"],
521
+ principalId: message.from,
522
+ sessionId: this.agentSessionId,
523
+ remoteSessionId: this.remoteSessionId,
524
+ sessionEpoch: message.sessionEpoch,
525
+ membershipEpoch: message.membershipEpoch,
526
+ };
527
+ const verdict = this._registry.resolve(pending.fingerprint, {
528
+ requestId,
529
+ fingerprint: message.event.fingerprint,
530
+ binding: message.event.binding,
531
+ sessionId: this.agentSessionId,
532
+ decision,
533
+ authority,
534
+ expectedRevision: message.event.revision,
535
+ now: this._now(),
536
+ });
328
537
  if (!verdict.ok) {
329
538
  this._recordSecurityError({
330
539
  action: "approval.resolve",
@@ -333,7 +542,20 @@ export class RemoteApprovalBridge {
333
542
  errorCode: verdict.errorCode,
334
543
  });
335
544
  if (verdict.reason === "state-unavailable") {
336
- this._settleDeniedForStateFailure(requestId, pending, verdict);
545
+ await this._settleDeniedForStateFailure(requestId, pending, verdict, {
546
+ lease: ackedLease,
547
+ });
548
+ } else if (ackedLease) {
549
+ await this._cancelLeaseBestEffort(
550
+ ackedLease,
551
+ `approval-store-${verdict.reason || "rejected"}`,
552
+ {
553
+ requestId,
554
+ fingerprint: pending.fingerprint,
555
+ binding: pending.binding,
556
+ revision: pending.revision,
557
+ },
558
+ );
337
559
  }
338
560
  return;
339
561
  }
@@ -343,18 +565,238 @@ export class RemoteApprovalBridge {
343
565
  approved: verdict.ok === true && verdict.approved === true,
344
566
  via: "remote",
345
567
  from: message.from || null,
568
+ authorizationRequired: verdict.approved === true,
569
+ ...(verdict.approved === true && ackedLease
570
+ ? {
571
+ authorization: this._createLeaseAuthorization({
572
+ lease: ackedLease,
573
+ descriptor: pending.descriptor,
574
+ action: pending.action,
575
+ }),
576
+ }
577
+ : {}),
578
+ });
579
+ }
580
+
581
+ _stateFailureLeaseCandidate(message, requestId, pending) {
582
+ const event = message?.event;
583
+ const lease = message?.approvalLeaseStatement?.payload;
584
+ if (
585
+ !pending ||
586
+ event?.requestId !== requestId ||
587
+ event.fingerprint !== pending.fingerprint ||
588
+ event.binding !== pending.binding ||
589
+ event.revision !== pending.revision ||
590
+ !lease ||
591
+ typeof lease.leaseId !== "string" ||
592
+ !lease.leaseId ||
593
+ lease.status !== "active" ||
594
+ lease.sessionId !== this.remoteSessionId ||
595
+ lease.sessionEpoch !== message.sessionEpoch ||
596
+ lease.principalId !== message.from ||
597
+ lease.membershipEpoch !== message.membershipEpoch ||
598
+ lease.requestId !== requestId ||
599
+ lease.fingerprint !== pending.fingerprint ||
600
+ lease.binding !== pending.binding
601
+ ) {
602
+ return null;
603
+ }
604
+ return structuredClone(lease);
605
+ }
606
+
607
+ _leaseMatchesApproval(lease, approval) {
608
+ return Boolean(
609
+ lease &&
610
+ approval &&
611
+ typeof lease.leaseId === "string" &&
612
+ lease.leaseId &&
613
+ Number.isInteger(approval.revision) &&
614
+ approval.revision > 0 &&
615
+ lease.requestId === approval.requestId &&
616
+ lease.fingerprint === approval.fingerprint &&
617
+ lease.binding === approval.binding,
618
+ );
619
+ }
620
+
621
+ async _cancelLeaseBestEffort(lease, reason, approval) {
622
+ if (
623
+ !this.client ||
624
+ !this._leaseMatchesApproval(lease, approval) ||
625
+ lease.sessionId !== this.remoteSessionId
626
+ ) {
627
+ this._recordSecurityError({
628
+ action: "lease.cancel",
629
+ requestId: approval?.requestId || lease?.requestId || null,
630
+ reason: "lease-cancel-binding-mismatch",
631
+ });
632
+ return false;
633
+ }
634
+ try {
635
+ const cancelled = await this.client.request(
636
+ "remote-session-lease-cancel",
637
+ {
638
+ remoteSessionId: this.remoteSessionId,
639
+ leaseId: lease.leaseId,
640
+ reason,
641
+ requestId: approval.requestId,
642
+ fingerprint: approval.fingerprint,
643
+ binding: approval.binding,
644
+ approvalRevision: approval.revision,
645
+ },
646
+ );
647
+ const receipt = this._requireMembershipHostStore().adopt(
648
+ cancelled.statement,
649
+ {
650
+ expectedKind: "lease.cancelled",
651
+ expectedSessionId: this.remoteSessionId,
652
+ },
653
+ );
654
+ if (
655
+ receipt.payload.status !== "cancelled" ||
656
+ receipt.payload.leaseId !== lease.leaseId ||
657
+ !this._leaseMatchesApproval(receipt.payload, approval)
658
+ ) {
659
+ throw new Error("Cancelled Remote approval lease binding changed");
660
+ }
661
+ return true;
662
+ } catch (error) {
663
+ // The cancel RPC may have committed while its response (or the local
664
+ // cancellation receipt) failed. Reconcile from a signed session snapshot
665
+ // before declaring the terminal outcome unknown. This also lets a host
666
+ // that missed `lease.created` adopt the final cancelled lease directly.
667
+ try {
668
+ const snapshot = await this._refreshMembershipSnapshot();
669
+ const observed = snapshot.payload.leases.find(
670
+ (candidate) => candidate.leaseId === lease.leaseId,
671
+ );
672
+ if (
673
+ observed &&
674
+ !["active", "acked"].includes(observed.status) &&
675
+ this._leaseMatchesApproval(observed, approval)
676
+ ) {
677
+ return true;
678
+ }
679
+ } catch {
680
+ // Report the original cancellation failure below. The approval itself
681
+ // still settles denied; no authorization handle is ever returned.
682
+ }
683
+ this._recordSecurityError({
684
+ action: "lease.cancel",
685
+ requestId: approval.requestId,
686
+ reason: "lease-cancel-unknown",
687
+ errorCode: error?.code || null,
688
+ });
689
+ return false;
690
+ }
691
+ }
692
+
693
+ _createLeaseAuthorization({ lease, descriptor, action }) {
694
+ const authorization = Object.freeze({
695
+ kind: REMOTE_LEASE_AUTHORIZATION_KIND,
346
696
  });
697
+ remoteLeaseAuthorizations.set(
698
+ authorization,
699
+ Object.freeze({
700
+ lease: structuredClone(lease),
701
+ descriptor: structuredClone(descriptor),
702
+ action: action ?? null,
703
+ }),
704
+ );
705
+ return authorization;
347
706
  }
348
707
 
349
- _settleDeniedForStateFailure(requestId, pending, verdict) {
708
+ _cancelApprovalForStateFailure(requestId, pending) {
709
+ let cancellation;
710
+ try {
711
+ cancellation = this._registry.cancel(pending.fingerprint, {
712
+ requestId,
713
+ fingerprint: pending.fingerprint,
714
+ binding: pending.binding,
715
+ expectedRevision: pending.revision,
716
+ reason: "state-failure",
717
+ now: this._now(),
718
+ });
719
+ } catch (error) {
720
+ cancellation = {
721
+ ok: false,
722
+ reason: "state-unavailable",
723
+ errorCode: error?.code || "CC_APPROVAL_STATE_UNKNOWN",
724
+ };
725
+ }
726
+ if (cancellation.ok) return true;
727
+
728
+ // A failed resolve may have committed before reporting an unknown write
729
+ // outcome. Accept that only as an exact, one-revision terminal record; a
730
+ // mismatched request/fingerprint/binding never counts as cleanup.
731
+ let terminal = false;
732
+ try {
733
+ const record = this._approvalStore?.getRequest?.(requestId, {
734
+ bestEffort: false,
735
+ });
736
+ terminal = Boolean(
737
+ record &&
738
+ record.requestId === requestId &&
739
+ record.fingerprint === pending.fingerprint &&
740
+ record.binding === pending.binding &&
741
+ record.revision === pending.revision + 1 &&
742
+ ["cancelled", "expired", "resolved", "rejected"].includes(
743
+ record.status,
744
+ ),
745
+ );
746
+ } catch {
747
+ terminal = false;
748
+ }
749
+ if (!terminal) {
750
+ this._recordSecurityError({
751
+ action: "approval.cancel",
752
+ requestId,
753
+ reason: cancellation.reason || "state-unavailable",
754
+ errorCode: cancellation.errorCode || null,
755
+ });
756
+ }
757
+ return terminal;
758
+ }
759
+
760
+ async _settleDeniedForStateFailure(
761
+ requestId,
762
+ pending,
763
+ verdict,
764
+ { lease = null } = {},
765
+ ) {
766
+ if (!pending || this._pending.get(requestId) !== pending) return false;
350
767
  this._pending.delete(requestId);
351
768
  clearTimeout(pending.timer);
769
+ this._cancelApprovalForStateFailure(requestId, pending);
770
+ if (lease) {
771
+ await this._cancelLeaseBestEffort(
772
+ lease,
773
+ `approval-state-${verdict?.reason || "failure"}`,
774
+ {
775
+ requestId,
776
+ fingerprint: pending.fingerprint,
777
+ binding: pending.binding,
778
+ revision: pending.revision,
779
+ },
780
+ );
781
+ }
352
782
  pending.resolve({
353
783
  approved: false,
354
784
  via: "state-error",
355
785
  from: null,
356
786
  errorCode: verdict?.errorCode || null,
357
787
  });
788
+ return true;
789
+ }
790
+
791
+ async _refreshMembershipSnapshot() {
792
+ const result = await this.client.request(
793
+ "remote-session-membership-snapshot",
794
+ { remoteSessionId: this.remoteSessionId },
795
+ );
796
+ return this._requireMembershipHostStore().adopt(result.statement, {
797
+ expectedKind: "session.snapshot",
798
+ expectedSessionId: this.remoteSessionId,
799
+ });
358
800
  }
359
801
 
360
802
  /** Number of devices that can approve (excludes this host). */
@@ -380,6 +822,7 @@ export class RemoteApprovalBridge {
380
822
  tool,
381
823
  action = null,
382
824
  detail = null,
825
+ operationArgs = undefined,
383
826
  workspace = null,
384
827
  session = null,
385
828
  targetEnv = null,
@@ -387,6 +830,13 @@ export class RemoteApprovalBridge {
387
830
  timeoutMs,
388
831
  onRequestId = null,
389
832
  } = {}) {
833
+ if (!SUPPORTED_REMOTE_LEASE_TOOLS.has(tool)) {
834
+ return Promise.resolve({
835
+ approved: false,
836
+ via: "lease-unavailable",
837
+ from: null,
838
+ });
839
+ }
390
840
  const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
391
841
  const askedAt = this._now();
392
842
  const effectiveTimeout = timeoutMs || this.decisionTimeoutMs;
@@ -395,9 +845,11 @@ export class RemoteApprovalBridge {
395
845
  // approval never carries over to a different operation OR a changed context.
396
846
  // The validity window rides the ask lifetime — a resolve after it expires is
397
847
  // rejected `expired`.
848
+ const params =
849
+ operationArgs === undefined ? detail : structuredClone(operationArgs);
398
850
  const desc = {
399
851
  toolName: tool,
400
- params: detail,
852
+ params,
401
853
  workspace,
402
854
  session: session || this.agentSessionId,
403
855
  targetEnv,
@@ -408,17 +860,15 @@ export class RemoteApprovalBridge {
408
860
  ? askedAt + effectiveTimeout
409
861
  : null,
410
862
  };
411
- const binding = approvalBindingDigest({
412
- toolCallId: requestId,
413
- args: {
414
- tool: tool || null,
415
- action,
416
- detail,
417
- workspace,
418
- session: desc.session,
419
- targetEnv,
420
- },
421
- policyDigest: policyVersion,
863
+ const binding = buildApprovalBinding({
864
+ requestId,
865
+ tool,
866
+ action,
867
+ params,
868
+ workspace,
869
+ session: desc.session,
870
+ targetEnv,
871
+ policyVersion,
422
872
  });
423
873
  let card;
424
874
  try {
@@ -482,6 +932,8 @@ export class RemoteApprovalBridge {
482
932
  fingerprint: card.fingerprint,
483
933
  binding,
484
934
  revision: card.revision,
935
+ descriptor: structuredClone(desc),
936
+ action,
485
937
  resolve: (decision) => {
486
938
  this._publish({
487
939
  type: "permission.resolved",
@@ -492,7 +944,7 @@ export class RemoteApprovalBridge {
492
944
  resolve(decision);
493
945
  },
494
946
  });
495
- this._publish({
947
+ const requestEvent = {
496
948
  type: "permission.request",
497
949
  requestId,
498
950
  tool: tool || null,
@@ -508,7 +960,169 @@ export class RemoteApprovalBridge {
508
960
  notBefore: desc.notBefore,
509
961
  notAfter: desc.notAfter,
510
962
  askedAt,
963
+ };
964
+ this._refreshMembershipSnapshot()
965
+ .then(() => {
966
+ if (this._pending.get(requestId)) this._publish(requestEvent);
967
+ })
968
+ .catch(async (error) => {
969
+ const pending = this._pending.get(requestId);
970
+ if (!pending) return;
971
+ this._recordSecurityError({
972
+ action: "approval.request",
973
+ requestId,
974
+ reason: "membership-snapshot-unavailable",
975
+ errorCode: error?.code || null,
976
+ });
977
+ await this._settleDeniedForStateFailure(requestId, pending, {
978
+ errorCode: error?.code || null,
979
+ });
980
+ });
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Consume one opaque approval authorization online immediately before the
986
+ * exact side effect is dispatched. Pure/local tuple mismatches leave the
987
+ * handle retryable; it is burned immediately before network I/O so an
988
+ * unknown coordinator outcome can never be retried into a second spawn.
989
+ */
990
+ async consumeAuthorization(authorization, context = {}) {
991
+ const granted = remoteLeaseAuthorizations.get(authorization);
992
+ if (!granted || authorization?.kind !== REMOTE_LEASE_AUTHORIZATION_KIND) {
993
+ throw new Error("Remote approval authorization is invalid or replayed");
994
+ }
995
+ const descriptor = {
996
+ toolName: context.tool || null,
997
+ params: structuredClone(context.args ?? null),
998
+ workspace: context.workspace || null,
999
+ session: context.session || this.agentSessionId,
1000
+ targetEnv: context.targetEnv || null,
1001
+ policyVersion: context.policyVersion || null,
1002
+ notBefore: granted.descriptor.notBefore,
1003
+ notAfter: granted.descriptor.notAfter,
1004
+ };
1005
+ const lease = granted.lease;
1006
+ const fingerprint = computeOperationFingerprint(descriptor);
1007
+ const binding = buildApprovalBinding({
1008
+ requestId: lease.requestId,
1009
+ tool: descriptor.toolName,
1010
+ action: context.action ?? null,
1011
+ params: descriptor.params,
1012
+ workspace: descriptor.workspace,
1013
+ session: descriptor.session,
1014
+ targetEnv: descriptor.targetEnv,
1015
+ policyVersion: descriptor.policyVersion,
1016
+ });
1017
+ if (
1018
+ (context.action ?? null) !== (granted.action ?? null) ||
1019
+ !fingerprintsMatch(fingerprint, lease.fingerprint) ||
1020
+ binding !== lease.binding
1021
+ ) {
1022
+ throw new Error(
1023
+ "Remote approval authorization does not match the dispatch operation",
1024
+ );
1025
+ }
1026
+ const hostStore = this._requireMembershipHostStore();
1027
+ const durableLease = hostStore.requireConsumableLease(lease.leaseId);
1028
+ const bootstrap = hostStore.getBootstrap();
1029
+ if (
1030
+ !bootstrap ||
1031
+ durableLease.status !== "acked" ||
1032
+ durableLease.sessionId !== this.remoteSessionId ||
1033
+ durableLease.hostPrincipalId !== bootstrap.hostPrincipalId ||
1034
+ durableLease.leaseId !== lease.leaseId ||
1035
+ durableLease.sessionEpoch !== lease.sessionEpoch ||
1036
+ durableLease.principalId !== lease.principalId ||
1037
+ durableLease.membershipEpoch !== lease.membershipEpoch ||
1038
+ durableLease.ackedGeneration !== lease.ackedGeneration ||
1039
+ durableLease.requestId !== lease.requestId ||
1040
+ durableLease.fingerprint !== fingerprint ||
1041
+ durableLease.binding !== binding ||
1042
+ durableLease.expiresAt !== lease.expiresAt
1043
+ ) {
1044
+ throw new Error("Durable Remote approval receipt binding changed");
1045
+ }
1046
+ // Everything above is deterministic local validation. Preserve the
1047
+ // opaque handle on a mismatch so the caller can retry the same approved
1048
+ // operation tuple. From this point onward the coordinator outcome may be
1049
+ // unknown, so burn before issuing the online consume request.
1050
+ remoteLeaseAuthorizations.delete(authorization);
1051
+ let consumed;
1052
+ try {
1053
+ consumed = await this.client.request("remote-session-lease-consume", {
1054
+ remoteSessionId: this.remoteSessionId,
1055
+ leaseId: durableLease.leaseId,
1056
+ expectedAckedGeneration: durableLease.ackedGeneration,
1057
+ expectedMembershipEpoch: durableLease.membershipEpoch,
1058
+ requestId: durableLease.requestId,
1059
+ fingerprint: durableLease.fingerprint,
1060
+ binding: durableLease.binding,
511
1061
  });
1062
+ } catch (error) {
1063
+ this._recordSecurityError({
1064
+ action: "lease.consume",
1065
+ requestId: durableLease.requestId,
1066
+ reason: "consume-outcome-unknown",
1067
+ errorCode: error?.code || null,
1068
+ });
1069
+ throw error;
1070
+ }
1071
+ if (consumed.dispatchAuthorized !== true || !consumed.statement) {
1072
+ throw new Error(
1073
+ "Remote coordinator did not return linearized dispatch authority",
1074
+ );
1075
+ }
1076
+ const adopted = hostStore.adopt(consumed.statement, {
1077
+ expectedKind: "lease.consumed",
1078
+ expectedSessionId: this.remoteSessionId,
1079
+ });
1080
+ const consumedLease = adopted.payload;
1081
+ if (
1082
+ consumedLease.status !== "consumed" ||
1083
+ consumedLease.leaseId !== durableLease.leaseId ||
1084
+ consumedLease.ackedGeneration !== durableLease.ackedGeneration ||
1085
+ consumedLease.requestId !== durableLease.requestId ||
1086
+ consumedLease.fingerprint !== durableLease.fingerprint ||
1087
+ consumedLease.binding !== durableLease.binding
1088
+ ) {
1089
+ throw new Error("Consumed Remote approval statement binding changed");
1090
+ }
1091
+ return true;
1092
+ }
1093
+
1094
+ /**
1095
+ * Revoke one stable device principal through the coordinator and durably
1096
+ * adopt the resulting membership epoch before returning to the host UI.
1097
+ */
1098
+ async revokeMember(principalId) {
1099
+ if (!this.client || !this.remoteSessionId) {
1100
+ throw new Error("Remote approval bridge is not connected");
1101
+ }
1102
+ const result = await this.client.request("remote-session-revoke", {
1103
+ remoteSessionId: this.remoteSessionId,
1104
+ clientId: principalId,
1105
+ });
1106
+ if (!result.statement) {
1107
+ throw new Error(
1108
+ "Remote coordinator did not return a signed revocation statement",
1109
+ );
1110
+ }
1111
+ const adopted = this._requireMembershipHostStore().adopt(result.statement, {
1112
+ expectedKind: "session.snapshot",
1113
+ expectedSessionId: this.remoteSessionId,
1114
+ });
1115
+ if (
1116
+ adopted.payload.members?.find(
1117
+ (member) => member.principalId === principalId,
1118
+ )?.status !== "revoked"
1119
+ ) {
1120
+ throw new Error("Signed revocation statement did not revoke the device");
1121
+ }
1122
+ return Object.freeze({
1123
+ principalId,
1124
+ authorityGeneration: adopted.authorityGeneration,
1125
+ session: result.session,
512
1126
  });
513
1127
  }
514
1128
 
@@ -538,7 +1152,7 @@ export class RemoteApprovalBridge {
538
1152
  errorCode: verdict.errorCode,
539
1153
  });
540
1154
  if (verdict.reason === "state-unavailable") {
541
- this._settleDeniedForStateFailure(requestId, pending, verdict);
1155
+ void this._settleDeniedForStateFailure(requestId, pending, verdict);
542
1156
  }
543
1157
  return false;
544
1158
  }
@@ -579,12 +1193,19 @@ export class RemoteApprovalBridge {
579
1193
  detail:
580
1194
  typeof ctx?.command === "string"
581
1195
  ? ctx.command
582
- : ctx?.args
583
- ? JSON.stringify(ctx.args).slice(0, 2000)
1196
+ : typeof ctx?.args?.command === "string"
1197
+ ? ctx.args.command
1198
+ : null,
1199
+ operationArgs:
1200
+ ctx?.args !== undefined
1201
+ ? structuredClone(ctx.args)
1202
+ : typeof ctx?.command === "string"
1203
+ ? { command: ctx.command }
584
1204
  : null,
585
1205
  // §8.2 context that binds the fingerprint (session defaults to this
586
1206
  // bridge's agent session inside requestDecision).
587
1207
  workspace: ctx?.cwd || ctx?.workspace || null,
1208
+ session: ctx?.session || ctx?.sessionId || null,
588
1209
  targetEnv: ctx?.targetEnv || null,
589
1210
  policyVersion: ctx?.policyVersion || null,
590
1211
  };
@@ -595,6 +1216,9 @@ export class RemoteApprovalBridge {
595
1216
  // observer only
596
1217
  }
597
1218
  }
1219
+ if (!SUPPORTED_REMOTE_LEASE_TOOLS.has(ask.tool) && fallback) {
1220
+ return fallback(ctx);
1221
+ }
598
1222
  let requestId = null;
599
1223
  const remote = this.requestDecision({
600
1224
  ...ask,
@@ -604,13 +1228,18 @@ export class RemoteApprovalBridge {
604
1228
  },
605
1229
  });
606
1230
  if (!fallback) {
607
- const decision = await remote;
608
- return decision.approved;
1231
+ return remote;
609
1232
  }
610
1233
  const local = Promise.resolve()
611
1234
  .then(() => fallback(ctx))
612
1235
  .then((approved) => ({ approved: approved === true, via: "local" }));
613
- const decision = await Promise.race([remote, local]);
1236
+ let decision = await Promise.race([remote, local]);
1237
+ if (
1238
+ decision.via !== "local" &&
1239
+ ["timeout", "closed", "lease-unavailable"].includes(decision.via)
1240
+ ) {
1241
+ decision = await local;
1242
+ }
614
1243
  // Local answer won → settle the remote ask too (publishes
615
1244
  // permission.resolved so device UIs clear the pending card).
616
1245
  if (decision.via === "local") {
@@ -618,7 +1247,7 @@ export class RemoteApprovalBridge {
618
1247
  const persisted = this.resolveLocally(requestId, decision.approved);
619
1248
  return persisted && decision.approved === true;
620
1249
  }
621
- return decision.approved === true;
1250
+ return decision;
622
1251
  };
623
1252
  }
624
1253
 
@@ -645,11 +1274,17 @@ export class RemoteApprovalBridge {
645
1274
  }
646
1275
  try {
647
1276
  if (this.client && this.remoteSessionId) {
648
- await this.client
1277
+ const closed = await this.client
649
1278
  .request("remote-session-close", {
650
1279
  remoteSessionId: this.remoteSessionId,
651
1280
  })
652
- .catch(() => undefined);
1281
+ .catch(() => null);
1282
+ if (closed?.statement) {
1283
+ this._requireMembershipHostStore().adopt(closed.statement, {
1284
+ expectedKind: "session.snapshot",
1285
+ expectedSessionId: this.remoteSessionId,
1286
+ });
1287
+ }
653
1288
  }
654
1289
  } finally {
655
1290
  this.client?.close();
@@ -723,6 +1358,8 @@ export async function startHeadlessRemoteApproval({
723
1358
  }
724
1359
  },
725
1360
  }),
1361
+ consumeAuthorization: (authorization, context) =>
1362
+ bridge.consumeAuthorization(authorization, context),
726
1363
  close: async () => {
727
1364
  await bridge.close().catch(() => undefined);
728
1365
  await server.stop().catch(() => undefined);