chainlesschain 0.163.8 → 0.165.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/package.json +7 -4
  2. package/scripts/macos-mcp-launcher-build.mjs +317 -0
  3. package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
  4. package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
  5. package/src/assets/web-panel/.build-hash +1 -1
  6. package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
  7. package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
  8. package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
  9. package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
  10. package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
  11. package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
  12. package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
  13. package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
  14. package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
  15. package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
  24. package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
  44. package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
  45. package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
  50. package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
  51. package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
  52. package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
  55. package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
  56. package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
  57. package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
  62. package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
  63. package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
  67. package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
  68. package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
  73. package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
  90. package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
  91. package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
  107. package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
  136. package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
  137. package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +3 -3
  155. package/src/commands/cowork.js +418 -41
  156. package/src/commands/permissions.js +240 -16
  157. package/src/commands/plugin.js +1293 -56
  158. package/src/commands/session-location.js +276 -0
  159. package/src/commands/session.js +2 -0
  160. package/src/data/changelog.json +28 -2
  161. package/src/gateways/ws/action-protocol.js +316 -17
  162. package/src/gateways/ws/message-dispatcher.js +30 -0
  163. package/src/gateways/ws/remote-session-protocol.js +565 -35
  164. package/src/gateways/ws/ws-agent-handler.js +12 -0
  165. package/src/gateways/ws/ws-approval-gate.js +5 -0
  166. package/src/gateways/ws/ws-server.js +510 -50
  167. package/src/harness/jsonl-session-store.js +341 -24
  168. package/src/harness/mcp-client.js +8 -1
  169. package/src/harness/remote-session-audit.js +2 -0
  170. package/src/harness/remote-session-crypto.js +929 -21
  171. package/src/harness/remote-session-registry.js +945 -24
  172. package/src/harness/session-list-index.js +233 -14
  173. package/src/lib/agent-authority.js +6 -0
  174. package/src/lib/auto-mode-config.js +98 -3
  175. package/src/lib/background-agent-keeper-client.js +248 -0
  176. package/src/lib/background-agent-keeper-protocol.js +291 -0
  177. package/src/lib/background-agent-supervisor.js +1357 -85
  178. package/src/lib/background-interaction-resolver.js +12 -0
  179. package/src/lib/background-session-transport.js +23 -6
  180. package/src/lib/background-turn-bootstrap-protocol.js +190 -0
  181. package/src/lib/cowork-workflow.js +1188 -76
  182. package/src/lib/delivery-production-adapter.js +38 -2
  183. package/src/lib/dynamic-workflow-facade.js +1736 -0
  184. package/src/lib/execution-location-contract.js +551 -0
  185. package/src/lib/execution-location-runtime.js +130 -0
  186. package/src/lib/ide-context.js +94 -6
  187. package/src/lib/mcp-stdio-package-materialization.js +166 -14
  188. package/src/lib/packer/pkg-config-generator.js +2 -0
  189. package/src/lib/permission-authority.js +77 -0
  190. package/src/lib/permission-side-effect-center.js +495 -0
  191. package/src/lib/plugin-runtime/install.js +514 -13
  192. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
  193. package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
  194. package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
  195. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
  196. package/src/lib/plugin-security.js +60 -3
  197. package/src/lib/process-execution-broker/index.js +1095 -57
  198. package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
  199. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
  200. package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
  201. package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
  202. package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
  203. package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
  204. package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
  205. package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
  206. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  207. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  208. package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
  209. package/src/lib/public-ide-capability-manifest.js +3 -0
  210. package/src/lib/remote-approval-bridge.js +777 -34
  211. package/src/lib/remote-control.js +2 -0
  212. package/src/lib/remote-membership-authority.js +632 -0
  213. package/src/lib/remote-membership-coordinator.js +2973 -0
  214. package/src/lib/remote-membership-host-store.js +1799 -0
  215. package/src/lib/runtime-provenance-ledger.js +171 -46
  216. package/src/lib/scoped-permission-store.js +369 -0
  217. package/src/lib/session-anti-rollback-anchor.js +262 -5
  218. package/src/lib/shell-approval.js +169 -6
  219. package/src/lib/sub-agent-context.js +4 -0
  220. package/src/lib/with-file-lock.js +73 -38
  221. package/src/lib/workflow-definition-contract.js +149 -0
  222. package/src/repl/agent-repl.js +111 -36
  223. package/src/repl/chat-repl.js +2 -0
  224. package/src/repl/clipboard-image.js +735 -16
  225. package/src/repl/ide-status.js +1 -1
  226. package/src/repl/prompt-interactions.js +59 -6
  227. package/src/repl/remote-approval.js +29 -7
  228. package/src/runtime/agent-core.js +1182 -155
  229. package/src/runtime/headless-runner.js +52 -30
  230. package/src/runtime/headless-stream.js +51 -28
  231. package/src/workers/background-agent-keeper.js +689 -0
  232. package/src/workers/background-agent-worker.js +365 -13
  233. package/src/workers/background-turn-bootstrap.js +170 -0
  234. package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
  235. package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
  236. package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
  237. package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
  238. package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
  239. package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
  240. package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
@@ -28,7 +28,20 @@ import {
28
28
  ApprovalAuthorityStore,
29
29
  defaultApprovalAuthorityStatePath,
30
30
  } from "./approval-authority-store.js";
31
- import { OperationApprovalRegistry } from "./operation-fingerprint.js";
31
+ import {
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";
32
45
  import {
33
46
  buildDirectPairingUri,
34
47
  pickLanAddress,
@@ -37,6 +50,34 @@ import {
37
50
  } from "./remote-control.js";
38
51
 
39
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
+ }
40
81
 
41
82
  export class RemoteApprovalBridge {
42
83
  constructor({
@@ -50,6 +91,11 @@ export class RemoteApprovalBridge {
50
91
  now = Date.now,
51
92
  approvalStore = undefined,
52
93
  approvalStateFile = null,
94
+ membershipHostStore = undefined,
95
+ membershipHostStateFile = null,
96
+ membershipHostWitnessFile = null,
97
+ membershipHostAuthorityLockFile = null,
98
+ expectedCoordinatorPublicKeySha256 = null,
53
99
  onSecurityError = null,
54
100
  } = {}) {
55
101
  if (!wsUrl) throw new Error("wsUrl is required");
@@ -71,6 +117,28 @@ export class RemoteApprovalBridge {
71
117
  this._securityErrors = [];
72
118
  this._onSecurityError =
73
119
  typeof onSecurityError === "function" ? onSecurityError : null;
120
+ this._membershipHostStore =
121
+ membershipHostStore === undefined
122
+ ? () =>
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;
74
142
  const durableStore =
75
143
  approvalStore === undefined
76
144
  ? new ApprovalAuthorityStore({
@@ -100,6 +168,24 @@ export class RemoteApprovalBridge {
100
168
  this._approvalStore = durableStore;
101
169
  }
102
170
 
171
+ _requireMembershipHostStore() {
172
+ if (typeof this._membershipHostStore === "function") {
173
+ this._membershipHostStore = this._membershipHostStore();
174
+ }
175
+ if (
176
+ !this._membershipHostStore ||
177
+ typeof this._membershipHostStore.adopt !== "function" ||
178
+ typeof this._membershipHostStore.requireConsumableLease !== "function"
179
+ ) {
180
+ const error = new Error(
181
+ "Durable Remote membership host store is unavailable; remote approval is denied",
182
+ );
183
+ error.code = REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE;
184
+ throw error;
185
+ }
186
+ return this._membershipHostStore;
187
+ }
188
+
103
189
  _recordSecurityError({ action, requestId = null, reason, errorCode = null }) {
104
190
  const entry = {
105
191
  timestamp: this._now(),
@@ -134,14 +220,126 @@ export class RemoteApprovalBridge {
134
220
  this.client = this._createClient(this.wsUrl);
135
221
  await this.client.connect();
136
222
  await this.client.auth(this.token);
137
- this.client.onEvent((message) => this._onServerEvent(message));
138
- const created = await this.client.request("remote-session-create", {
139
- sessionId: this.agentSessionId,
140
- name: this.name,
141
- scopes: this.scopes,
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
+ });
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
+ }
324
+ if (
325
+ resumed.session?.membershipAuthority !==
326
+ REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
327
+ resumed.member?.principalId !== currentBootstrap.hostPrincipalId
328
+ ) {
329
+ throw new Error("Remote Session host resume binding changed");
330
+ }
331
+ hostStore.adopt(resumed.statement, {
332
+ expectedKind: "session.snapshot",
333
+ expectedSessionId: this.remoteSessionId,
142
334
  });
143
- this.remoteSessionId = created.session.sessionId;
144
- this.pairing = created.pairing;
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;
341
+ }
342
+ this.pairing = pairing;
145
343
  return this;
146
344
  }
147
345
 
@@ -161,6 +359,7 @@ export class RemoteApprovalBridge {
161
359
  pairingToken: this.pairing.token,
162
360
  scopes: this.pairing.scopes,
163
361
  expiresAt: this.pairing.expiresAt,
362
+ durableMembership: true,
164
363
  });
165
364
  return {
166
365
  uri,
@@ -170,7 +369,7 @@ export class RemoteApprovalBridge {
170
369
  };
171
370
  }
172
371
 
173
- _onServerEvent(message) {
372
+ async _onServerEvent(message) {
174
373
  if (
175
374
  message?.type !== "remote-session-control" ||
176
375
  message.remoteSessionId !== this.remoteSessionId ||
@@ -181,6 +380,28 @@ export class RemoteApprovalBridge {
181
380
  const requestId = message.event.requestId || message.event.approvalId;
182
381
  const pending = requestId ? this._pending.get(requestId) : null;
183
382
  if (!pending) return;
383
+ if (message.agentSessionId !== this.agentSessionId) {
384
+ this._recordSecurityError({
385
+ action: "approval.resolve",
386
+ requestId,
387
+ reason: "membership-agent-session-mismatch",
388
+ });
389
+ return;
390
+ }
391
+ if (
392
+ message.membershipAuthority !== REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
393
+ typeof message.from !== "string" ||
394
+ !message.from ||
395
+ typeof message.sessionEpoch !== "string" ||
396
+ typeof message.membershipEpoch !== "string"
397
+ ) {
398
+ this._recordSecurityError({
399
+ action: "approval.resolve",
400
+ requestId,
401
+ reason: "membership-binding-required",
402
+ });
403
+ return;
404
+ }
184
405
  // The device must echo the complete durable capability tuple. Missing,
185
406
  // mismatched, stale, duplicate, or expired resolutions never settle an
186
407
  // approval; only the store's successful CAS can return approved=true.
@@ -202,12 +423,106 @@ export class RemoteApprovalBridge {
202
423
  }
203
424
  const decision =
204
425
  rawAnswer === true || rawAnswer === "true" || rawAnswer === "yes";
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
+ );
435
+ try {
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
+ }
500
+ } catch (error) {
501
+ const verdict = {
502
+ reason: `lease-adopt-or-ack-failed:${error?.cause?.message || error?.message || "unknown"}`,
503
+ errorCode:
504
+ error?.code || REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE,
505
+ };
506
+ this._recordSecurityError({
507
+ action: "approval.resolve",
508
+ requestId,
509
+ reason: verdict.reason,
510
+ errorCode: verdict.errorCode,
511
+ });
512
+ await this._settleDeniedForStateFailure(requestId, pending, verdict, {
513
+ lease: leaseForFailure,
514
+ });
515
+ return;
516
+ }
205
517
  const authority = {
206
518
  origin: ORIGIN.REMOTE,
207
519
  authenticated: true,
208
520
  scopes: ["approve"],
209
- principalId: message.from || "paired-device",
521
+ principalId: message.from,
210
522
  sessionId: this.agentSessionId,
523
+ remoteSessionId: this.remoteSessionId,
524
+ sessionEpoch: message.sessionEpoch,
525
+ membershipEpoch: message.membershipEpoch,
211
526
  };
212
527
  const verdict = this._registry.resolve(pending.fingerprint, {
213
528
  requestId,
@@ -227,7 +542,20 @@ export class RemoteApprovalBridge {
227
542
  errorCode: verdict.errorCode,
228
543
  });
229
544
  if (verdict.reason === "state-unavailable") {
230
- 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
+ );
231
559
  }
232
560
  return;
233
561
  }
@@ -237,18 +565,238 @@ export class RemoteApprovalBridge {
237
565
  approved: verdict.ok === true && verdict.approved === true,
238
566
  via: "remote",
239
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
+ : {}),
240
578
  });
241
579
  }
242
580
 
243
- _settleDeniedForStateFailure(requestId, pending, verdict) {
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,
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;
706
+ }
707
+
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;
244
767
  this._pending.delete(requestId);
245
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
+ }
246
782
  pending.resolve({
247
783
  approved: false,
248
784
  via: "state-error",
249
785
  from: null,
250
786
  errorCode: verdict?.errorCode || null,
251
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
+ });
252
800
  }
253
801
 
254
802
  /** Number of devices that can approve (excludes this host). */
@@ -274,6 +822,7 @@ export class RemoteApprovalBridge {
274
822
  tool,
275
823
  action = null,
276
824
  detail = null,
825
+ operationArgs = undefined,
277
826
  workspace = null,
278
827
  session = null,
279
828
  targetEnv = null,
@@ -281,6 +830,13 @@ export class RemoteApprovalBridge {
281
830
  timeoutMs,
282
831
  onRequestId = null,
283
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
+ }
284
840
  const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
285
841
  const askedAt = this._now();
286
842
  const effectiveTimeout = timeoutMs || this.decisionTimeoutMs;
@@ -289,9 +845,11 @@ export class RemoteApprovalBridge {
289
845
  // approval never carries over to a different operation OR a changed context.
290
846
  // The validity window rides the ask lifetime — a resolve after it expires is
291
847
  // rejected `expired`.
848
+ const params =
849
+ operationArgs === undefined ? detail : structuredClone(operationArgs);
292
850
  const desc = {
293
851
  toolName: tool,
294
- params: detail,
852
+ params,
295
853
  workspace,
296
854
  session: session || this.agentSessionId,
297
855
  targetEnv,
@@ -302,17 +860,15 @@ export class RemoteApprovalBridge {
302
860
  ? askedAt + effectiveTimeout
303
861
  : null,
304
862
  };
305
- const binding = approvalBindingDigest({
306
- toolCallId: requestId,
307
- args: {
308
- tool: tool || null,
309
- action,
310
- detail,
311
- workspace,
312
- session: desc.session,
313
- targetEnv,
314
- },
315
- policyDigest: policyVersion,
863
+ const binding = buildApprovalBinding({
864
+ requestId,
865
+ tool,
866
+ action,
867
+ params,
868
+ workspace,
869
+ session: desc.session,
870
+ targetEnv,
871
+ policyVersion,
316
872
  });
317
873
  let card;
318
874
  try {
@@ -376,6 +932,8 @@ export class RemoteApprovalBridge {
376
932
  fingerprint: card.fingerprint,
377
933
  binding,
378
934
  revision: card.revision,
935
+ descriptor: structuredClone(desc),
936
+ action,
379
937
  resolve: (decision) => {
380
938
  this._publish({
381
939
  type: "permission.resolved",
@@ -386,7 +944,7 @@ export class RemoteApprovalBridge {
386
944
  resolve(decision);
387
945
  },
388
946
  });
389
- this._publish({
947
+ const requestEvent = {
390
948
  type: "permission.request",
391
949
  requestId,
392
950
  tool: tool || null,
@@ -402,7 +960,169 @@ export class RemoteApprovalBridge {
402
960
  notBefore: desc.notBefore,
403
961
  notAfter: desc.notAfter,
404
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,
405
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,
406
1126
  });
407
1127
  }
408
1128
 
@@ -432,7 +1152,7 @@ export class RemoteApprovalBridge {
432
1152
  errorCode: verdict.errorCode,
433
1153
  });
434
1154
  if (verdict.reason === "state-unavailable") {
435
- this._settleDeniedForStateFailure(requestId, pending, verdict);
1155
+ void this._settleDeniedForStateFailure(requestId, pending, verdict);
436
1156
  }
437
1157
  return false;
438
1158
  }
@@ -473,12 +1193,19 @@ export class RemoteApprovalBridge {
473
1193
  detail:
474
1194
  typeof ctx?.command === "string"
475
1195
  ? ctx.command
476
- : ctx?.args
477
- ? 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 }
478
1204
  : null,
479
1205
  // §8.2 context that binds the fingerprint (session defaults to this
480
1206
  // bridge's agent session inside requestDecision).
481
1207
  workspace: ctx?.cwd || ctx?.workspace || null,
1208
+ session: ctx?.session || ctx?.sessionId || null,
482
1209
  targetEnv: ctx?.targetEnv || null,
483
1210
  policyVersion: ctx?.policyVersion || null,
484
1211
  };
@@ -489,6 +1216,9 @@ export class RemoteApprovalBridge {
489
1216
  // observer only
490
1217
  }
491
1218
  }
1219
+ if (!SUPPORTED_REMOTE_LEASE_TOOLS.has(ask.tool) && fallback) {
1220
+ return fallback(ctx);
1221
+ }
492
1222
  let requestId = null;
493
1223
  const remote = this.requestDecision({
494
1224
  ...ask,
@@ -498,13 +1228,18 @@ export class RemoteApprovalBridge {
498
1228
  },
499
1229
  });
500
1230
  if (!fallback) {
501
- const decision = await remote;
502
- return decision.approved;
1231
+ return remote;
503
1232
  }
504
1233
  const local = Promise.resolve()
505
1234
  .then(() => fallback(ctx))
506
1235
  .then((approved) => ({ approved: approved === true, via: "local" }));
507
- 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
+ }
508
1243
  // Local answer won → settle the remote ask too (publishes
509
1244
  // permission.resolved so device UIs clear the pending card).
510
1245
  if (decision.via === "local") {
@@ -512,7 +1247,7 @@ export class RemoteApprovalBridge {
512
1247
  const persisted = this.resolveLocally(requestId, decision.approved);
513
1248
  return persisted && decision.approved === true;
514
1249
  }
515
- return decision.approved === true;
1250
+ return decision;
516
1251
  };
517
1252
  }
518
1253
 
@@ -539,11 +1274,17 @@ export class RemoteApprovalBridge {
539
1274
  }
540
1275
  try {
541
1276
  if (this.client && this.remoteSessionId) {
542
- await this.client
1277
+ const closed = await this.client
543
1278
  .request("remote-session-close", {
544
1279
  remoteSessionId: this.remoteSessionId,
545
1280
  })
546
- .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
+ }
547
1288
  }
548
1289
  } finally {
549
1290
  this.client?.close();
@@ -617,6 +1358,8 @@ export async function startHeadlessRemoteApproval({
617
1358
  }
618
1359
  },
619
1360
  }),
1361
+ consumeAuthorization: (authorization, context) =>
1362
+ bridge.consumeAuthorization(authorization, context),
620
1363
  close: async () => {
621
1364
  await bridge.close().catch(() => undefined);
622
1365
  await server.stop().catch(() => undefined);