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.
- package/package.json +7 -4
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +418 -41
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +316 -17
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +565 -35
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +510 -50
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +2 -0
- package/src/harness/remote-session-crypto.js +929 -21
- package/src/harness/remote-session-registry.js +945 -24
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +291 -0
- package/src/lib/background-agent-supervisor.js +1357 -85
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +190 -0
- package/src/lib/cowork-workflow.js +1188 -76
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1736 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +777 -34
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2973 -0
- package/src/lib/remote-membership-host-store.js +1799 -0
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/with-file-lock.js +73 -38
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +111 -36
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1182 -155
- package/src/runtime/headless-runner.js +52 -30
- package/src/runtime/headless-stream.js +51 -28
- package/src/workers/background-agent-keeper.js +689 -0
- package/src/workers/background-agent-worker.js +365 -13
- package/src/workers/background-turn-bootstrap.js +170 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- 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 {
|
|
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) =>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(() =>
|
|
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);
|