chainlesschain 0.164.0 → 0.165.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -2
- package/package.json +7 -4
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
- package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
- package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
- package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
- package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
- package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
- package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
- package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
- package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
- package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
- package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
- package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
- package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
- package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
- package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
- package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/cowork.js +559 -62
- package/src/commands/plugin.js +819 -64
- package/src/commands/session-location.js +318 -7
- package/src/constants.js +2 -2
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +293 -19
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +542 -35
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +462 -38
- package/src/harness/jsonl-session-store.js +638 -2
- package/src/harness/prompt-compressor.js +4 -0
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +750 -50
- package/src/harness/remote-session-registry.js +795 -27
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +29 -0
- package/src/lib/background-agent-keeper-protocol.js +158 -10
- package/src/lib/background-agent-supervisor.js +870 -48
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +64 -0
- package/src/lib/cowork-task-runner.js +70 -0
- package/src/lib/cowork-workflow.js +887 -62
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/doctor-checkup.js +34 -10
- package/src/lib/dynamic-workflow-draft.js +535 -0
- package/src/lib/dynamic-workflow-facade.js +1105 -17
- package/src/lib/dynamic-workflow-runtime.js +2974 -0
- package/src/lib/execution-location-contract.js +111 -7
- package/src/lib/execution-location-runtime.js +3 -1
- package/src/lib/execution-location-target.js +910 -0
- package/src/lib/llm-providers.js +1 -0
- package/src/lib/mcp-call-ledger-store.js +35 -0
- package/src/lib/mcp-call-ledger.js +30 -0
- package/src/lib/mcp-stdio-package-materialization.js +1 -1
- package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
- package/src/lib/plugin-runtime/install.js +4956 -657
- package/src/lib/plugin-runtime/manifest.js +3 -1
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
- package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
- package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
- package/src/lib/plugin-runtime/marketplace-network.js +317 -0
- package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
- package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
- package/src/lib/plugin-runtime/policy.js +21 -6
- package/src/lib/plugin-runtime/publisher-trust.js +332 -0
- package/src/lib/plugin-runtime/remote-source.js +323 -27
- package/src/lib/plugin-runtime/scopes.js +317 -41
- package/src/lib/plugin-runtime/transaction-journal.js +806 -0
- package/src/lib/public-ide-capability-manifest.js +1 -1
- package/src/lib/remote-approval-bridge.js +745 -108
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-coordinator.js +612 -46
- package/src/lib/remote-membership-host-store.js +59 -10
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +268 -2
- package/src/lib/task-model-selector.js +1 -1
- package/src/lib/with-file-lock.js +167 -42
- package/src/repl/agent-repl.js +70 -29
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1695 -194
- package/src/runtime/headless-runner.js +22 -5
- package/src/runtime/headless-stream.js +19 -3
- package/src/workers/background-agent-keeper.js +279 -58
- package/src/workers/background-agent-worker.js +7 -0
- package/src/workers/background-turn-bootstrap.js +29 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
- package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
|
@@ -28,12 +28,20 @@ import {
|
|
|
28
28
|
ApprovalAuthorityStore,
|
|
29
29
|
defaultApprovalAuthorityStatePath,
|
|
30
30
|
} from "./approval-authority-store.js";
|
|
31
|
-
import { OperationApprovalRegistry } from "./operation-fingerprint.js";
|
|
32
31
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from "./
|
|
32
|
+
computeOperationFingerprint,
|
|
33
|
+
fingerprintsMatch,
|
|
34
|
+
OperationApprovalRegistry,
|
|
35
|
+
} from "./operation-fingerprint.js";
|
|
36
|
+
import {
|
|
37
|
+
createRemoteMembershipPrincipalCredential,
|
|
38
|
+
REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE,
|
|
39
|
+
REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
40
|
+
} from "./remote-membership-coordinator.js";
|
|
41
|
+
import {
|
|
42
|
+
DurableRemoteMembershipHostStore,
|
|
43
|
+
REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE,
|
|
44
|
+
} from "./remote-membership-host-store.js";
|
|
37
45
|
import {
|
|
38
46
|
buildDirectPairingUri,
|
|
39
47
|
pickLanAddress,
|
|
@@ -42,6 +50,34 @@ import {
|
|
|
42
50
|
} from "./remote-control.js";
|
|
43
51
|
|
|
44
52
|
const DEFAULT_DECISION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
53
|
+
const REMOTE_LEASE_AUTHORIZATION_KIND =
|
|
54
|
+
"chainlesschain.remote-approval-lease-authorization/v1";
|
|
55
|
+
const SUPPORTED_REMOTE_LEASE_TOOLS = new Set(["run_shell"]);
|
|
56
|
+
const remoteLeaseAuthorizations = new WeakMap();
|
|
57
|
+
|
|
58
|
+
function buildApprovalBinding({
|
|
59
|
+
requestId,
|
|
60
|
+
tool,
|
|
61
|
+
action,
|
|
62
|
+
params,
|
|
63
|
+
workspace,
|
|
64
|
+
session,
|
|
65
|
+
targetEnv,
|
|
66
|
+
policyVersion,
|
|
67
|
+
}) {
|
|
68
|
+
return approvalBindingDigest({
|
|
69
|
+
toolCallId: requestId,
|
|
70
|
+
args: {
|
|
71
|
+
tool: tool || null,
|
|
72
|
+
action: action || null,
|
|
73
|
+
params: params ?? null,
|
|
74
|
+
workspace: workspace || null,
|
|
75
|
+
session: session || null,
|
|
76
|
+
targetEnv: targetEnv || null,
|
|
77
|
+
},
|
|
78
|
+
policyDigest: policyVersion || null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
45
81
|
|
|
46
82
|
export class RemoteApprovalBridge {
|
|
47
83
|
constructor({
|
|
@@ -55,8 +91,11 @@ export class RemoteApprovalBridge {
|
|
|
55
91
|
now = Date.now,
|
|
56
92
|
approvalStore = undefined,
|
|
57
93
|
approvalStateFile = null,
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
membershipHostStore = undefined,
|
|
95
|
+
membershipHostStateFile = null,
|
|
96
|
+
membershipHostWitnessFile = null,
|
|
97
|
+
membershipHostAuthorityLockFile = null,
|
|
98
|
+
expectedCoordinatorPublicKeySha256 = null,
|
|
60
99
|
onSecurityError = null,
|
|
61
100
|
} = {}) {
|
|
62
101
|
if (!wsUrl) throw new Error("wsUrl is required");
|
|
@@ -78,13 +117,28 @@ export class RemoteApprovalBridge {
|
|
|
78
117
|
this._securityErrors = [];
|
|
79
118
|
this._onSecurityError =
|
|
80
119
|
typeof onSecurityError === "function" ? onSecurityError : null;
|
|
81
|
-
this.
|
|
82
|
-
|
|
120
|
+
this._membershipHostStore =
|
|
121
|
+
membershipHostStore === undefined
|
|
83
122
|
? () =>
|
|
84
|
-
new
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
new DurableRemoteMembershipHostStore({
|
|
124
|
+
agentSessionId,
|
|
125
|
+
...(membershipHostStateFile
|
|
126
|
+
? { stateFile: membershipHostStateFile }
|
|
127
|
+
: {}),
|
|
128
|
+
...(membershipHostWitnessFile
|
|
129
|
+
? { witnessFile: membershipHostWitnessFile }
|
|
130
|
+
: {}),
|
|
131
|
+
...(membershipHostAuthorityLockFile
|
|
132
|
+
? { authorityLockFile: membershipHostAuthorityLockFile }
|
|
133
|
+
: {}),
|
|
134
|
+
...(expectedCoordinatorPublicKeySha256
|
|
135
|
+
? {
|
|
136
|
+
expectedPublicKeySha256: expectedCoordinatorPublicKeySha256,
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
now: () => this._now(),
|
|
140
|
+
})
|
|
141
|
+
: membershipHostStore;
|
|
88
142
|
const durableStore =
|
|
89
143
|
approvalStore === undefined
|
|
90
144
|
? new ApprovalAuthorityStore({
|
|
@@ -114,21 +168,22 @@ export class RemoteApprovalBridge {
|
|
|
114
168
|
this._approvalStore = durableStore;
|
|
115
169
|
}
|
|
116
170
|
|
|
117
|
-
|
|
118
|
-
if (typeof this.
|
|
119
|
-
this.
|
|
171
|
+
_requireMembershipHostStore() {
|
|
172
|
+
if (typeof this._membershipHostStore === "function") {
|
|
173
|
+
this._membershipHostStore = this._membershipHostStore();
|
|
120
174
|
}
|
|
121
175
|
if (
|
|
122
|
-
!this.
|
|
123
|
-
typeof this.
|
|
176
|
+
!this._membershipHostStore ||
|
|
177
|
+
typeof this._membershipHostStore.adopt !== "function" ||
|
|
178
|
+
typeof this._membershipHostStore.requireConsumableLease !== "function"
|
|
124
179
|
) {
|
|
125
180
|
const error = new Error(
|
|
126
|
-
"Durable Remote membership
|
|
181
|
+
"Durable Remote membership host store is unavailable; remote approval is denied",
|
|
127
182
|
);
|
|
128
|
-
error.code =
|
|
183
|
+
error.code = REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE;
|
|
129
184
|
throw error;
|
|
130
185
|
}
|
|
131
|
-
return this.
|
|
186
|
+
return this._membershipHostStore;
|
|
132
187
|
}
|
|
133
188
|
|
|
134
189
|
_recordSecurityError({ action, requestId = null, reason, errorCode = null }) {
|
|
@@ -165,24 +220,126 @@ export class RemoteApprovalBridge {
|
|
|
165
220
|
this.client = this._createClient(this.wsUrl);
|
|
166
221
|
await this.client.connect();
|
|
167
222
|
await this.client.auth(this.token);
|
|
168
|
-
this.client.onEvent((message) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
223
|
+
this.client.onEvent((message) => {
|
|
224
|
+
Promise.resolve(this._onServerEvent(message)).catch((error) => {
|
|
225
|
+
this._recordSecurityError({
|
|
226
|
+
action: "approval.resolve",
|
|
227
|
+
reason: "host-event-processing-failed",
|
|
228
|
+
errorCode: error?.code || null,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
174
231
|
});
|
|
232
|
+
const hostStore = this._requireMembershipHostStore();
|
|
233
|
+
const bootstrap = hostStore.getBootstrap();
|
|
234
|
+
let pairing = null;
|
|
235
|
+
if (bootstrap) {
|
|
236
|
+
if (bootstrap.agentSessionId !== this.agentSessionId) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Durable Remote host bootstrap belongs to another agent session",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
this.remoteSessionId = bootstrap.sessionId;
|
|
242
|
+
} else {
|
|
243
|
+
const credential = createRemoteMembershipPrincipalCredential();
|
|
244
|
+
const created = await this.client.request("remote-session-create", {
|
|
245
|
+
sessionId: this.agentSessionId,
|
|
246
|
+
name: this.name,
|
|
247
|
+
scopes: this.scopes,
|
|
248
|
+
requireDurableMembershipAuthority: true,
|
|
249
|
+
hostCredentialPublicKeySpki: credential.publicKey,
|
|
250
|
+
});
|
|
251
|
+
if (
|
|
252
|
+
created.session?.membershipAuthority !==
|
|
253
|
+
REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
|
|
254
|
+
typeof created.session?.sessionEpoch !== "string" ||
|
|
255
|
+
!created.bootstrap?.trust ||
|
|
256
|
+
!created.bootstrap?.statement
|
|
257
|
+
) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
"Remote Session server did not establish a signed durable membership",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
hostStore.pinTrust(created.bootstrap.trust);
|
|
263
|
+
hostStore.recordBootstrap({
|
|
264
|
+
coordinatorId: created.bootstrap.trust.coordinatorId,
|
|
265
|
+
sessionId: created.session.sessionId,
|
|
266
|
+
agentSessionId: this.agentSessionId,
|
|
267
|
+
hostPrincipalId: created.bootstrap.hostPrincipalId,
|
|
268
|
+
hostCredentialPublicKeySpki:
|
|
269
|
+
created.bootstrap.hostCredentialPublicKeySpki,
|
|
270
|
+
hostCredentialPrivateKeyPkcs8: credential.privateKeyPkcs8,
|
|
271
|
+
statement: created.bootstrap.statement,
|
|
272
|
+
});
|
|
273
|
+
hostStore.adopt(created.bootstrap.statement, {
|
|
274
|
+
expectedKind: "session.snapshot",
|
|
275
|
+
expectedSessionId: created.session.sessionId,
|
|
276
|
+
});
|
|
277
|
+
this.remoteSessionId = created.session.sessionId;
|
|
278
|
+
pairing = created.pairing;
|
|
279
|
+
}
|
|
280
|
+
const currentBootstrap = hostStore.getBootstrap();
|
|
281
|
+
let resumed;
|
|
282
|
+
try {
|
|
283
|
+
const challenged = await this.client.request(
|
|
284
|
+
"remote-session-resume-challenge",
|
|
285
|
+
{
|
|
286
|
+
remoteSessionId: this.remoteSessionId,
|
|
287
|
+
principalId: currentBootstrap.hostPrincipalId,
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
const signature = hostStore.signAuthenticationChallenge(
|
|
291
|
+
challenged.challenge,
|
|
292
|
+
);
|
|
293
|
+
resumed = await this.client.request("remote-session-resume", {
|
|
294
|
+
remoteSessionId: this.remoteSessionId,
|
|
295
|
+
challengeId: challenged.challenge.challengeId,
|
|
296
|
+
signature,
|
|
297
|
+
});
|
|
298
|
+
} catch (resumeError) {
|
|
299
|
+
if (!bootstrap) throw resumeError;
|
|
300
|
+
const terminal = await this.client.request("remote-session-terminal", {
|
|
301
|
+
remoteSessionId: this.remoteSessionId,
|
|
302
|
+
principalId: currentBootstrap.hostPrincipalId,
|
|
303
|
+
});
|
|
304
|
+
hostStore.adopt(terminal.statement, {
|
|
305
|
+
expectedKind: "session.snapshot",
|
|
306
|
+
expectedSessionId: this.remoteSessionId,
|
|
307
|
+
});
|
|
308
|
+
const challenged = await this.client.request(
|
|
309
|
+
"remote-session-reenable-challenge",
|
|
310
|
+
{
|
|
311
|
+
remoteSessionId: this.remoteSessionId,
|
|
312
|
+
principalId: currentBootstrap.hostPrincipalId,
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
const signature = hostStore.signAuthenticationChallenge(
|
|
316
|
+
challenged.challenge,
|
|
317
|
+
);
|
|
318
|
+
resumed = await this.client.request("remote-session-reenable", {
|
|
319
|
+
remoteSessionId: this.remoteSessionId,
|
|
320
|
+
challengeId: challenged.challenge.challengeId,
|
|
321
|
+
signature,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
175
324
|
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
325
|
+
resumed.session?.membershipAuthority !==
|
|
326
|
+
REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
|
|
327
|
+
resumed.member?.principalId !== currentBootstrap.hostPrincipalId
|
|
179
328
|
) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
|
|
182
|
-
|
|
329
|
+
throw new Error("Remote Session host resume binding changed");
|
|
330
|
+
}
|
|
331
|
+
hostStore.adopt(resumed.statement, {
|
|
332
|
+
expectedKind: "session.snapshot",
|
|
333
|
+
expectedSessionId: this.remoteSessionId,
|
|
334
|
+
});
|
|
335
|
+
if (!pairing) {
|
|
336
|
+
const issued = await this.client.request("remote-session-pairing-token", {
|
|
337
|
+
remoteSessionId: this.remoteSessionId,
|
|
338
|
+
scopes: this.scopes,
|
|
339
|
+
});
|
|
340
|
+
pairing = issued.pairing;
|
|
183
341
|
}
|
|
184
|
-
this.
|
|
185
|
-
this.pairing = created.pairing;
|
|
342
|
+
this.pairing = pairing;
|
|
186
343
|
return this;
|
|
187
344
|
}
|
|
188
345
|
|
|
@@ -202,6 +359,7 @@ export class RemoteApprovalBridge {
|
|
|
202
359
|
pairingToken: this.pairing.token,
|
|
203
360
|
scopes: this.pairing.scopes,
|
|
204
361
|
expiresAt: this.pairing.expiresAt,
|
|
362
|
+
durableMembership: true,
|
|
205
363
|
});
|
|
206
364
|
return {
|
|
207
365
|
uri,
|
|
@@ -211,7 +369,7 @@ export class RemoteApprovalBridge {
|
|
|
211
369
|
};
|
|
212
370
|
}
|
|
213
371
|
|
|
214
|
-
_onServerEvent(message) {
|
|
372
|
+
async _onServerEvent(message) {
|
|
215
373
|
if (
|
|
216
374
|
message?.type !== "remote-session-control" ||
|
|
217
375
|
message.remoteSessionId !== this.remoteSessionId ||
|
|
@@ -231,7 +389,7 @@ export class RemoteApprovalBridge {
|
|
|
231
389
|
return;
|
|
232
390
|
}
|
|
233
391
|
if (
|
|
234
|
-
message.membershipAuthority !==
|
|
392
|
+
message.membershipAuthority !== REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
|
|
235
393
|
typeof message.from !== "string" ||
|
|
236
394
|
!message.from ||
|
|
237
395
|
typeof message.sessionEpoch !== "string" ||
|
|
@@ -265,47 +423,85 @@ export class RemoteApprovalBridge {
|
|
|
265
423
|
}
|
|
266
424
|
const decision =
|
|
267
425
|
rawAnswer === true || rawAnswer === "true" || rawAnswer === "yes";
|
|
268
|
-
let
|
|
426
|
+
let ackedLease = null;
|
|
427
|
+
// The server creates the lease before forwarding the resolve frame. Keep a
|
|
428
|
+
// strictly request-bound cancellation handle even when durable adoption of
|
|
429
|
+
// that statement fails before it can return a trusted payload.
|
|
430
|
+
let leaseForFailure = this._stateFailureLeaseCandidate(
|
|
431
|
+
message,
|
|
432
|
+
requestId,
|
|
433
|
+
pending,
|
|
434
|
+
);
|
|
269
435
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
436
|
+
if (decision) {
|
|
437
|
+
if (!message.approvalLeaseStatement) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
"Approved remote response did not carry a signed execution lease",
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const hostStore = this._requireMembershipHostStore();
|
|
443
|
+
const adopted = hostStore.adopt(message.approvalLeaseStatement, {
|
|
444
|
+
expectedKind: "lease.created",
|
|
445
|
+
expectedSessionId: this.remoteSessionId,
|
|
446
|
+
});
|
|
447
|
+
const createdLease = adopted.payload;
|
|
448
|
+
if (
|
|
449
|
+
createdLease.status !== "active" ||
|
|
450
|
+
createdLease.sessionId !== this.remoteSessionId ||
|
|
451
|
+
createdLease.sessionEpoch !== message.sessionEpoch ||
|
|
452
|
+
createdLease.principalId !== message.from ||
|
|
453
|
+
createdLease.membershipEpoch !== message.membershipEpoch ||
|
|
454
|
+
createdLease.requestId !== requestId ||
|
|
455
|
+
!fingerprintsMatch(createdLease.fingerprint, pending.fingerprint) ||
|
|
456
|
+
createdLease.fingerprint !== message.event.fingerprint ||
|
|
457
|
+
createdLease.binding !== pending.binding ||
|
|
458
|
+
createdLease.binding !== message.event.binding ||
|
|
459
|
+
createdLease.expiresAt > pending.descriptor.notAfter
|
|
460
|
+
) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
"Signed remote execution lease does not match the pending operation",
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
leaseForFailure = createdLease;
|
|
466
|
+
const bootstrap = hostStore.getBootstrap();
|
|
467
|
+
if (
|
|
468
|
+
!bootstrap ||
|
|
469
|
+
createdLease.hostPrincipalId !== bootstrap.hostPrincipalId
|
|
470
|
+
) {
|
|
471
|
+
throw new Error("Signed remote execution lease targets another host");
|
|
472
|
+
}
|
|
473
|
+
const acked = await this.client.request("remote-session-lease-ack", {
|
|
474
|
+
remoteSessionId: this.remoteSessionId,
|
|
475
|
+
leaseId: createdLease.leaseId,
|
|
476
|
+
expectedCreatedGeneration: createdLease.createdGeneration,
|
|
477
|
+
hostReceiptDigest: adopted.receiptHash,
|
|
478
|
+
});
|
|
479
|
+
const ackReceipt = hostStore.adopt(acked.statement, {
|
|
480
|
+
expectedKind: "lease.acked",
|
|
481
|
+
expectedSessionId: this.remoteSessionId,
|
|
482
|
+
});
|
|
483
|
+
ackedLease = ackReceipt.payload;
|
|
484
|
+
leaseForFailure = ackedLease;
|
|
485
|
+
if (
|
|
486
|
+
acked.dispatchAuthorized === true ||
|
|
487
|
+
ackedLease.status !== "acked" ||
|
|
488
|
+
ackedLease.leaseId !== createdLease.leaseId ||
|
|
489
|
+
ackedLease.createdGeneration !== createdLease.createdGeneration ||
|
|
490
|
+
ackedLease.principalId !== createdLease.principalId ||
|
|
491
|
+
ackedLease.membershipEpoch !== createdLease.membershipEpoch ||
|
|
492
|
+
ackedLease.requestId !== createdLease.requestId ||
|
|
493
|
+
ackedLease.fingerprint !== createdLease.fingerprint ||
|
|
494
|
+
ackedLease.binding !== createdLease.binding ||
|
|
495
|
+
ackedLease.expiresAt !== createdLease.expiresAt
|
|
496
|
+
) {
|
|
497
|
+
throw new Error("Remote execution lease ACK binding changed");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
305
500
|
} catch (error) {
|
|
306
501
|
const verdict = {
|
|
307
|
-
reason:
|
|
308
|
-
errorCode:
|
|
502
|
+
reason: `lease-adopt-or-ack-failed:${error?.cause?.message || error?.message || "unknown"}`,
|
|
503
|
+
errorCode:
|
|
504
|
+
error?.code || REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE,
|
|
309
505
|
};
|
|
310
506
|
this._recordSecurityError({
|
|
311
507
|
action: "approval.resolve",
|
|
@@ -313,18 +509,31 @@ export class RemoteApprovalBridge {
|
|
|
313
509
|
reason: verdict.reason,
|
|
314
510
|
errorCode: verdict.errorCode,
|
|
315
511
|
});
|
|
316
|
-
this._settleDeniedForStateFailure(requestId, pending, verdict
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
if (!fenced.ok) {
|
|
320
|
-
this._recordSecurityError({
|
|
321
|
-
action: "approval.resolve",
|
|
322
|
-
requestId,
|
|
323
|
-
reason: fenced.reason,
|
|
512
|
+
await this._settleDeniedForStateFailure(requestId, pending, verdict, {
|
|
513
|
+
lease: leaseForFailure,
|
|
324
514
|
});
|
|
325
515
|
return;
|
|
326
516
|
}
|
|
327
|
-
const
|
|
517
|
+
const authority = {
|
|
518
|
+
origin: ORIGIN.REMOTE,
|
|
519
|
+
authenticated: true,
|
|
520
|
+
scopes: ["approve"],
|
|
521
|
+
principalId: message.from,
|
|
522
|
+
sessionId: this.agentSessionId,
|
|
523
|
+
remoteSessionId: this.remoteSessionId,
|
|
524
|
+
sessionEpoch: message.sessionEpoch,
|
|
525
|
+
membershipEpoch: message.membershipEpoch,
|
|
526
|
+
};
|
|
527
|
+
const verdict = this._registry.resolve(pending.fingerprint, {
|
|
528
|
+
requestId,
|
|
529
|
+
fingerprint: message.event.fingerprint,
|
|
530
|
+
binding: message.event.binding,
|
|
531
|
+
sessionId: this.agentSessionId,
|
|
532
|
+
decision,
|
|
533
|
+
authority,
|
|
534
|
+
expectedRevision: message.event.revision,
|
|
535
|
+
now: this._now(),
|
|
536
|
+
});
|
|
328
537
|
if (!verdict.ok) {
|
|
329
538
|
this._recordSecurityError({
|
|
330
539
|
action: "approval.resolve",
|
|
@@ -333,7 +542,20 @@ export class RemoteApprovalBridge {
|
|
|
333
542
|
errorCode: verdict.errorCode,
|
|
334
543
|
});
|
|
335
544
|
if (verdict.reason === "state-unavailable") {
|
|
336
|
-
this._settleDeniedForStateFailure(requestId, pending, verdict
|
|
545
|
+
await this._settleDeniedForStateFailure(requestId, pending, verdict, {
|
|
546
|
+
lease: ackedLease,
|
|
547
|
+
});
|
|
548
|
+
} else if (ackedLease) {
|
|
549
|
+
await this._cancelLeaseBestEffort(
|
|
550
|
+
ackedLease,
|
|
551
|
+
`approval-store-${verdict.reason || "rejected"}`,
|
|
552
|
+
{
|
|
553
|
+
requestId,
|
|
554
|
+
fingerprint: pending.fingerprint,
|
|
555
|
+
binding: pending.binding,
|
|
556
|
+
revision: pending.revision,
|
|
557
|
+
},
|
|
558
|
+
);
|
|
337
559
|
}
|
|
338
560
|
return;
|
|
339
561
|
}
|
|
@@ -343,18 +565,238 @@ export class RemoteApprovalBridge {
|
|
|
343
565
|
approved: verdict.ok === true && verdict.approved === true,
|
|
344
566
|
via: "remote",
|
|
345
567
|
from: message.from || null,
|
|
568
|
+
authorizationRequired: verdict.approved === true,
|
|
569
|
+
...(verdict.approved === true && ackedLease
|
|
570
|
+
? {
|
|
571
|
+
authorization: this._createLeaseAuthorization({
|
|
572
|
+
lease: ackedLease,
|
|
573
|
+
descriptor: pending.descriptor,
|
|
574
|
+
action: pending.action,
|
|
575
|
+
}),
|
|
576
|
+
}
|
|
577
|
+
: {}),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
_stateFailureLeaseCandidate(message, requestId, pending) {
|
|
582
|
+
const event = message?.event;
|
|
583
|
+
const lease = message?.approvalLeaseStatement?.payload;
|
|
584
|
+
if (
|
|
585
|
+
!pending ||
|
|
586
|
+
event?.requestId !== requestId ||
|
|
587
|
+
event.fingerprint !== pending.fingerprint ||
|
|
588
|
+
event.binding !== pending.binding ||
|
|
589
|
+
event.revision !== pending.revision ||
|
|
590
|
+
!lease ||
|
|
591
|
+
typeof lease.leaseId !== "string" ||
|
|
592
|
+
!lease.leaseId ||
|
|
593
|
+
lease.status !== "active" ||
|
|
594
|
+
lease.sessionId !== this.remoteSessionId ||
|
|
595
|
+
lease.sessionEpoch !== message.sessionEpoch ||
|
|
596
|
+
lease.principalId !== message.from ||
|
|
597
|
+
lease.membershipEpoch !== message.membershipEpoch ||
|
|
598
|
+
lease.requestId !== requestId ||
|
|
599
|
+
lease.fingerprint !== pending.fingerprint ||
|
|
600
|
+
lease.binding !== pending.binding
|
|
601
|
+
) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
return structuredClone(lease);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
_leaseMatchesApproval(lease, approval) {
|
|
608
|
+
return Boolean(
|
|
609
|
+
lease &&
|
|
610
|
+
approval &&
|
|
611
|
+
typeof lease.leaseId === "string" &&
|
|
612
|
+
lease.leaseId &&
|
|
613
|
+
Number.isInteger(approval.revision) &&
|
|
614
|
+
approval.revision > 0 &&
|
|
615
|
+
lease.requestId === approval.requestId &&
|
|
616
|
+
lease.fingerprint === approval.fingerprint &&
|
|
617
|
+
lease.binding === approval.binding,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async _cancelLeaseBestEffort(lease, reason, approval) {
|
|
622
|
+
if (
|
|
623
|
+
!this.client ||
|
|
624
|
+
!this._leaseMatchesApproval(lease, approval) ||
|
|
625
|
+
lease.sessionId !== this.remoteSessionId
|
|
626
|
+
) {
|
|
627
|
+
this._recordSecurityError({
|
|
628
|
+
action: "lease.cancel",
|
|
629
|
+
requestId: approval?.requestId || lease?.requestId || null,
|
|
630
|
+
reason: "lease-cancel-binding-mismatch",
|
|
631
|
+
});
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
const cancelled = await this.client.request(
|
|
636
|
+
"remote-session-lease-cancel",
|
|
637
|
+
{
|
|
638
|
+
remoteSessionId: this.remoteSessionId,
|
|
639
|
+
leaseId: lease.leaseId,
|
|
640
|
+
reason,
|
|
641
|
+
requestId: approval.requestId,
|
|
642
|
+
fingerprint: approval.fingerprint,
|
|
643
|
+
binding: approval.binding,
|
|
644
|
+
approvalRevision: approval.revision,
|
|
645
|
+
},
|
|
646
|
+
);
|
|
647
|
+
const receipt = this._requireMembershipHostStore().adopt(
|
|
648
|
+
cancelled.statement,
|
|
649
|
+
{
|
|
650
|
+
expectedKind: "lease.cancelled",
|
|
651
|
+
expectedSessionId: this.remoteSessionId,
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
if (
|
|
655
|
+
receipt.payload.status !== "cancelled" ||
|
|
656
|
+
receipt.payload.leaseId !== lease.leaseId ||
|
|
657
|
+
!this._leaseMatchesApproval(receipt.payload, approval)
|
|
658
|
+
) {
|
|
659
|
+
throw new Error("Cancelled Remote approval lease binding changed");
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
} catch (error) {
|
|
663
|
+
// The cancel RPC may have committed while its response (or the local
|
|
664
|
+
// cancellation receipt) failed. Reconcile from a signed session snapshot
|
|
665
|
+
// before declaring the terminal outcome unknown. This also lets a host
|
|
666
|
+
// that missed `lease.created` adopt the final cancelled lease directly.
|
|
667
|
+
try {
|
|
668
|
+
const snapshot = await this._refreshMembershipSnapshot();
|
|
669
|
+
const observed = snapshot.payload.leases.find(
|
|
670
|
+
(candidate) => candidate.leaseId === lease.leaseId,
|
|
671
|
+
);
|
|
672
|
+
if (
|
|
673
|
+
observed &&
|
|
674
|
+
!["active", "acked"].includes(observed.status) &&
|
|
675
|
+
this._leaseMatchesApproval(observed, approval)
|
|
676
|
+
) {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
// Report the original cancellation failure below. The approval itself
|
|
681
|
+
// still settles denied; no authorization handle is ever returned.
|
|
682
|
+
}
|
|
683
|
+
this._recordSecurityError({
|
|
684
|
+
action: "lease.cancel",
|
|
685
|
+
requestId: approval.requestId,
|
|
686
|
+
reason: "lease-cancel-unknown",
|
|
687
|
+
errorCode: error?.code || null,
|
|
688
|
+
});
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
_createLeaseAuthorization({ lease, descriptor, action }) {
|
|
694
|
+
const authorization = Object.freeze({
|
|
695
|
+
kind: REMOTE_LEASE_AUTHORIZATION_KIND,
|
|
346
696
|
});
|
|
697
|
+
remoteLeaseAuthorizations.set(
|
|
698
|
+
authorization,
|
|
699
|
+
Object.freeze({
|
|
700
|
+
lease: structuredClone(lease),
|
|
701
|
+
descriptor: structuredClone(descriptor),
|
|
702
|
+
action: action ?? null,
|
|
703
|
+
}),
|
|
704
|
+
);
|
|
705
|
+
return authorization;
|
|
347
706
|
}
|
|
348
707
|
|
|
349
|
-
|
|
708
|
+
_cancelApprovalForStateFailure(requestId, pending) {
|
|
709
|
+
let cancellation;
|
|
710
|
+
try {
|
|
711
|
+
cancellation = this._registry.cancel(pending.fingerprint, {
|
|
712
|
+
requestId,
|
|
713
|
+
fingerprint: pending.fingerprint,
|
|
714
|
+
binding: pending.binding,
|
|
715
|
+
expectedRevision: pending.revision,
|
|
716
|
+
reason: "state-failure",
|
|
717
|
+
now: this._now(),
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
cancellation = {
|
|
721
|
+
ok: false,
|
|
722
|
+
reason: "state-unavailable",
|
|
723
|
+
errorCode: error?.code || "CC_APPROVAL_STATE_UNKNOWN",
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (cancellation.ok) return true;
|
|
727
|
+
|
|
728
|
+
// A failed resolve may have committed before reporting an unknown write
|
|
729
|
+
// outcome. Accept that only as an exact, one-revision terminal record; a
|
|
730
|
+
// mismatched request/fingerprint/binding never counts as cleanup.
|
|
731
|
+
let terminal = false;
|
|
732
|
+
try {
|
|
733
|
+
const record = this._approvalStore?.getRequest?.(requestId, {
|
|
734
|
+
bestEffort: false,
|
|
735
|
+
});
|
|
736
|
+
terminal = Boolean(
|
|
737
|
+
record &&
|
|
738
|
+
record.requestId === requestId &&
|
|
739
|
+
record.fingerprint === pending.fingerprint &&
|
|
740
|
+
record.binding === pending.binding &&
|
|
741
|
+
record.revision === pending.revision + 1 &&
|
|
742
|
+
["cancelled", "expired", "resolved", "rejected"].includes(
|
|
743
|
+
record.status,
|
|
744
|
+
),
|
|
745
|
+
);
|
|
746
|
+
} catch {
|
|
747
|
+
terminal = false;
|
|
748
|
+
}
|
|
749
|
+
if (!terminal) {
|
|
750
|
+
this._recordSecurityError({
|
|
751
|
+
action: "approval.cancel",
|
|
752
|
+
requestId,
|
|
753
|
+
reason: cancellation.reason || "state-unavailable",
|
|
754
|
+
errorCode: cancellation.errorCode || null,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return terminal;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async _settleDeniedForStateFailure(
|
|
761
|
+
requestId,
|
|
762
|
+
pending,
|
|
763
|
+
verdict,
|
|
764
|
+
{ lease = null } = {},
|
|
765
|
+
) {
|
|
766
|
+
if (!pending || this._pending.get(requestId) !== pending) return false;
|
|
350
767
|
this._pending.delete(requestId);
|
|
351
768
|
clearTimeout(pending.timer);
|
|
769
|
+
this._cancelApprovalForStateFailure(requestId, pending);
|
|
770
|
+
if (lease) {
|
|
771
|
+
await this._cancelLeaseBestEffort(
|
|
772
|
+
lease,
|
|
773
|
+
`approval-state-${verdict?.reason || "failure"}`,
|
|
774
|
+
{
|
|
775
|
+
requestId,
|
|
776
|
+
fingerprint: pending.fingerprint,
|
|
777
|
+
binding: pending.binding,
|
|
778
|
+
revision: pending.revision,
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
}
|
|
352
782
|
pending.resolve({
|
|
353
783
|
approved: false,
|
|
354
784
|
via: "state-error",
|
|
355
785
|
from: null,
|
|
356
786
|
errorCode: verdict?.errorCode || null,
|
|
357
787
|
});
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async _refreshMembershipSnapshot() {
|
|
792
|
+
const result = await this.client.request(
|
|
793
|
+
"remote-session-membership-snapshot",
|
|
794
|
+
{ remoteSessionId: this.remoteSessionId },
|
|
795
|
+
);
|
|
796
|
+
return this._requireMembershipHostStore().adopt(result.statement, {
|
|
797
|
+
expectedKind: "session.snapshot",
|
|
798
|
+
expectedSessionId: this.remoteSessionId,
|
|
799
|
+
});
|
|
358
800
|
}
|
|
359
801
|
|
|
360
802
|
/** Number of devices that can approve (excludes this host). */
|
|
@@ -380,6 +822,7 @@ export class RemoteApprovalBridge {
|
|
|
380
822
|
tool,
|
|
381
823
|
action = null,
|
|
382
824
|
detail = null,
|
|
825
|
+
operationArgs = undefined,
|
|
383
826
|
workspace = null,
|
|
384
827
|
session = null,
|
|
385
828
|
targetEnv = null,
|
|
@@ -387,6 +830,13 @@ export class RemoteApprovalBridge {
|
|
|
387
830
|
timeoutMs,
|
|
388
831
|
onRequestId = null,
|
|
389
832
|
} = {}) {
|
|
833
|
+
if (!SUPPORTED_REMOTE_LEASE_TOOLS.has(tool)) {
|
|
834
|
+
return Promise.resolve({
|
|
835
|
+
approved: false,
|
|
836
|
+
via: "lease-unavailable",
|
|
837
|
+
from: null,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
390
840
|
const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
|
|
391
841
|
const askedAt = this._now();
|
|
392
842
|
const effectiveTimeout = timeoutMs || this.decisionTimeoutMs;
|
|
@@ -395,9 +845,11 @@ export class RemoteApprovalBridge {
|
|
|
395
845
|
// approval never carries over to a different operation OR a changed context.
|
|
396
846
|
// The validity window rides the ask lifetime — a resolve after it expires is
|
|
397
847
|
// rejected `expired`.
|
|
848
|
+
const params =
|
|
849
|
+
operationArgs === undefined ? detail : structuredClone(operationArgs);
|
|
398
850
|
const desc = {
|
|
399
851
|
toolName: tool,
|
|
400
|
-
params
|
|
852
|
+
params,
|
|
401
853
|
workspace,
|
|
402
854
|
session: session || this.agentSessionId,
|
|
403
855
|
targetEnv,
|
|
@@ -408,17 +860,15 @@ export class RemoteApprovalBridge {
|
|
|
408
860
|
? askedAt + effectiveTimeout
|
|
409
861
|
: null,
|
|
410
862
|
};
|
|
411
|
-
const binding =
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
},
|
|
421
|
-
policyDigest: policyVersion,
|
|
863
|
+
const binding = buildApprovalBinding({
|
|
864
|
+
requestId,
|
|
865
|
+
tool,
|
|
866
|
+
action,
|
|
867
|
+
params,
|
|
868
|
+
workspace,
|
|
869
|
+
session: desc.session,
|
|
870
|
+
targetEnv,
|
|
871
|
+
policyVersion,
|
|
422
872
|
});
|
|
423
873
|
let card;
|
|
424
874
|
try {
|
|
@@ -482,6 +932,8 @@ export class RemoteApprovalBridge {
|
|
|
482
932
|
fingerprint: card.fingerprint,
|
|
483
933
|
binding,
|
|
484
934
|
revision: card.revision,
|
|
935
|
+
descriptor: structuredClone(desc),
|
|
936
|
+
action,
|
|
485
937
|
resolve: (decision) => {
|
|
486
938
|
this._publish({
|
|
487
939
|
type: "permission.resolved",
|
|
@@ -492,7 +944,7 @@ export class RemoteApprovalBridge {
|
|
|
492
944
|
resolve(decision);
|
|
493
945
|
},
|
|
494
946
|
});
|
|
495
|
-
|
|
947
|
+
const requestEvent = {
|
|
496
948
|
type: "permission.request",
|
|
497
949
|
requestId,
|
|
498
950
|
tool: tool || null,
|
|
@@ -508,7 +960,169 @@ export class RemoteApprovalBridge {
|
|
|
508
960
|
notBefore: desc.notBefore,
|
|
509
961
|
notAfter: desc.notAfter,
|
|
510
962
|
askedAt,
|
|
963
|
+
};
|
|
964
|
+
this._refreshMembershipSnapshot()
|
|
965
|
+
.then(() => {
|
|
966
|
+
if (this._pending.get(requestId)) this._publish(requestEvent);
|
|
967
|
+
})
|
|
968
|
+
.catch(async (error) => {
|
|
969
|
+
const pending = this._pending.get(requestId);
|
|
970
|
+
if (!pending) return;
|
|
971
|
+
this._recordSecurityError({
|
|
972
|
+
action: "approval.request",
|
|
973
|
+
requestId,
|
|
974
|
+
reason: "membership-snapshot-unavailable",
|
|
975
|
+
errorCode: error?.code || null,
|
|
976
|
+
});
|
|
977
|
+
await this._settleDeniedForStateFailure(requestId, pending, {
|
|
978
|
+
errorCode: error?.code || null,
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Consume one opaque approval authorization online immediately before the
|
|
986
|
+
* exact side effect is dispatched. Pure/local tuple mismatches leave the
|
|
987
|
+
* handle retryable; it is burned immediately before network I/O so an
|
|
988
|
+
* unknown coordinator outcome can never be retried into a second spawn.
|
|
989
|
+
*/
|
|
990
|
+
async consumeAuthorization(authorization, context = {}) {
|
|
991
|
+
const granted = remoteLeaseAuthorizations.get(authorization);
|
|
992
|
+
if (!granted || authorization?.kind !== REMOTE_LEASE_AUTHORIZATION_KIND) {
|
|
993
|
+
throw new Error("Remote approval authorization is invalid or replayed");
|
|
994
|
+
}
|
|
995
|
+
const descriptor = {
|
|
996
|
+
toolName: context.tool || null,
|
|
997
|
+
params: structuredClone(context.args ?? null),
|
|
998
|
+
workspace: context.workspace || null,
|
|
999
|
+
session: context.session || this.agentSessionId,
|
|
1000
|
+
targetEnv: context.targetEnv || null,
|
|
1001
|
+
policyVersion: context.policyVersion || null,
|
|
1002
|
+
notBefore: granted.descriptor.notBefore,
|
|
1003
|
+
notAfter: granted.descriptor.notAfter,
|
|
1004
|
+
};
|
|
1005
|
+
const lease = granted.lease;
|
|
1006
|
+
const fingerprint = computeOperationFingerprint(descriptor);
|
|
1007
|
+
const binding = buildApprovalBinding({
|
|
1008
|
+
requestId: lease.requestId,
|
|
1009
|
+
tool: descriptor.toolName,
|
|
1010
|
+
action: context.action ?? null,
|
|
1011
|
+
params: descriptor.params,
|
|
1012
|
+
workspace: descriptor.workspace,
|
|
1013
|
+
session: descriptor.session,
|
|
1014
|
+
targetEnv: descriptor.targetEnv,
|
|
1015
|
+
policyVersion: descriptor.policyVersion,
|
|
1016
|
+
});
|
|
1017
|
+
if (
|
|
1018
|
+
(context.action ?? null) !== (granted.action ?? null) ||
|
|
1019
|
+
!fingerprintsMatch(fingerprint, lease.fingerprint) ||
|
|
1020
|
+
binding !== lease.binding
|
|
1021
|
+
) {
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
"Remote approval authorization does not match the dispatch operation",
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
const hostStore = this._requireMembershipHostStore();
|
|
1027
|
+
const durableLease = hostStore.requireConsumableLease(lease.leaseId);
|
|
1028
|
+
const bootstrap = hostStore.getBootstrap();
|
|
1029
|
+
if (
|
|
1030
|
+
!bootstrap ||
|
|
1031
|
+
durableLease.status !== "acked" ||
|
|
1032
|
+
durableLease.sessionId !== this.remoteSessionId ||
|
|
1033
|
+
durableLease.hostPrincipalId !== bootstrap.hostPrincipalId ||
|
|
1034
|
+
durableLease.leaseId !== lease.leaseId ||
|
|
1035
|
+
durableLease.sessionEpoch !== lease.sessionEpoch ||
|
|
1036
|
+
durableLease.principalId !== lease.principalId ||
|
|
1037
|
+
durableLease.membershipEpoch !== lease.membershipEpoch ||
|
|
1038
|
+
durableLease.ackedGeneration !== lease.ackedGeneration ||
|
|
1039
|
+
durableLease.requestId !== lease.requestId ||
|
|
1040
|
+
durableLease.fingerprint !== fingerprint ||
|
|
1041
|
+
durableLease.binding !== binding ||
|
|
1042
|
+
durableLease.expiresAt !== lease.expiresAt
|
|
1043
|
+
) {
|
|
1044
|
+
throw new Error("Durable Remote approval receipt binding changed");
|
|
1045
|
+
}
|
|
1046
|
+
// Everything above is deterministic local validation. Preserve the
|
|
1047
|
+
// opaque handle on a mismatch so the caller can retry the same approved
|
|
1048
|
+
// operation tuple. From this point onward the coordinator outcome may be
|
|
1049
|
+
// unknown, so burn before issuing the online consume request.
|
|
1050
|
+
remoteLeaseAuthorizations.delete(authorization);
|
|
1051
|
+
let consumed;
|
|
1052
|
+
try {
|
|
1053
|
+
consumed = await this.client.request("remote-session-lease-consume", {
|
|
1054
|
+
remoteSessionId: this.remoteSessionId,
|
|
1055
|
+
leaseId: durableLease.leaseId,
|
|
1056
|
+
expectedAckedGeneration: durableLease.ackedGeneration,
|
|
1057
|
+
expectedMembershipEpoch: durableLease.membershipEpoch,
|
|
1058
|
+
requestId: durableLease.requestId,
|
|
1059
|
+
fingerprint: durableLease.fingerprint,
|
|
1060
|
+
binding: durableLease.binding,
|
|
511
1061
|
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
this._recordSecurityError({
|
|
1064
|
+
action: "lease.consume",
|
|
1065
|
+
requestId: durableLease.requestId,
|
|
1066
|
+
reason: "consume-outcome-unknown",
|
|
1067
|
+
errorCode: error?.code || null,
|
|
1068
|
+
});
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
if (consumed.dispatchAuthorized !== true || !consumed.statement) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
"Remote coordinator did not return linearized dispatch authority",
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
const adopted = hostStore.adopt(consumed.statement, {
|
|
1077
|
+
expectedKind: "lease.consumed",
|
|
1078
|
+
expectedSessionId: this.remoteSessionId,
|
|
1079
|
+
});
|
|
1080
|
+
const consumedLease = adopted.payload;
|
|
1081
|
+
if (
|
|
1082
|
+
consumedLease.status !== "consumed" ||
|
|
1083
|
+
consumedLease.leaseId !== durableLease.leaseId ||
|
|
1084
|
+
consumedLease.ackedGeneration !== durableLease.ackedGeneration ||
|
|
1085
|
+
consumedLease.requestId !== durableLease.requestId ||
|
|
1086
|
+
consumedLease.fingerprint !== durableLease.fingerprint ||
|
|
1087
|
+
consumedLease.binding !== durableLease.binding
|
|
1088
|
+
) {
|
|
1089
|
+
throw new Error("Consumed Remote approval statement binding changed");
|
|
1090
|
+
}
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Revoke one stable device principal through the coordinator and durably
|
|
1096
|
+
* adopt the resulting membership epoch before returning to the host UI.
|
|
1097
|
+
*/
|
|
1098
|
+
async revokeMember(principalId) {
|
|
1099
|
+
if (!this.client || !this.remoteSessionId) {
|
|
1100
|
+
throw new Error("Remote approval bridge is not connected");
|
|
1101
|
+
}
|
|
1102
|
+
const result = await this.client.request("remote-session-revoke", {
|
|
1103
|
+
remoteSessionId: this.remoteSessionId,
|
|
1104
|
+
clientId: principalId,
|
|
1105
|
+
});
|
|
1106
|
+
if (!result.statement) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
"Remote coordinator did not return a signed revocation statement",
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
const adopted = this._requireMembershipHostStore().adopt(result.statement, {
|
|
1112
|
+
expectedKind: "session.snapshot",
|
|
1113
|
+
expectedSessionId: this.remoteSessionId,
|
|
1114
|
+
});
|
|
1115
|
+
if (
|
|
1116
|
+
adopted.payload.members?.find(
|
|
1117
|
+
(member) => member.principalId === principalId,
|
|
1118
|
+
)?.status !== "revoked"
|
|
1119
|
+
) {
|
|
1120
|
+
throw new Error("Signed revocation statement did not revoke the device");
|
|
1121
|
+
}
|
|
1122
|
+
return Object.freeze({
|
|
1123
|
+
principalId,
|
|
1124
|
+
authorityGeneration: adopted.authorityGeneration,
|
|
1125
|
+
session: result.session,
|
|
512
1126
|
});
|
|
513
1127
|
}
|
|
514
1128
|
|
|
@@ -538,7 +1152,7 @@ export class RemoteApprovalBridge {
|
|
|
538
1152
|
errorCode: verdict.errorCode,
|
|
539
1153
|
});
|
|
540
1154
|
if (verdict.reason === "state-unavailable") {
|
|
541
|
-
this._settleDeniedForStateFailure(requestId, pending, verdict);
|
|
1155
|
+
void this._settleDeniedForStateFailure(requestId, pending, verdict);
|
|
542
1156
|
}
|
|
543
1157
|
return false;
|
|
544
1158
|
}
|
|
@@ -579,12 +1193,19 @@ export class RemoteApprovalBridge {
|
|
|
579
1193
|
detail:
|
|
580
1194
|
typeof ctx?.command === "string"
|
|
581
1195
|
? ctx.command
|
|
582
|
-
: ctx?.args
|
|
583
|
-
?
|
|
1196
|
+
: typeof ctx?.args?.command === "string"
|
|
1197
|
+
? ctx.args.command
|
|
1198
|
+
: null,
|
|
1199
|
+
operationArgs:
|
|
1200
|
+
ctx?.args !== undefined
|
|
1201
|
+
? structuredClone(ctx.args)
|
|
1202
|
+
: typeof ctx?.command === "string"
|
|
1203
|
+
? { command: ctx.command }
|
|
584
1204
|
: null,
|
|
585
1205
|
// §8.2 context that binds the fingerprint (session defaults to this
|
|
586
1206
|
// bridge's agent session inside requestDecision).
|
|
587
1207
|
workspace: ctx?.cwd || ctx?.workspace || null,
|
|
1208
|
+
session: ctx?.session || ctx?.sessionId || null,
|
|
588
1209
|
targetEnv: ctx?.targetEnv || null,
|
|
589
1210
|
policyVersion: ctx?.policyVersion || null,
|
|
590
1211
|
};
|
|
@@ -595,6 +1216,9 @@ export class RemoteApprovalBridge {
|
|
|
595
1216
|
// observer only
|
|
596
1217
|
}
|
|
597
1218
|
}
|
|
1219
|
+
if (!SUPPORTED_REMOTE_LEASE_TOOLS.has(ask.tool) && fallback) {
|
|
1220
|
+
return fallback(ctx);
|
|
1221
|
+
}
|
|
598
1222
|
let requestId = null;
|
|
599
1223
|
const remote = this.requestDecision({
|
|
600
1224
|
...ask,
|
|
@@ -604,13 +1228,18 @@ export class RemoteApprovalBridge {
|
|
|
604
1228
|
},
|
|
605
1229
|
});
|
|
606
1230
|
if (!fallback) {
|
|
607
|
-
|
|
608
|
-
return decision.approved;
|
|
1231
|
+
return remote;
|
|
609
1232
|
}
|
|
610
1233
|
const local = Promise.resolve()
|
|
611
1234
|
.then(() => fallback(ctx))
|
|
612
1235
|
.then((approved) => ({ approved: approved === true, via: "local" }));
|
|
613
|
-
|
|
1236
|
+
let decision = await Promise.race([remote, local]);
|
|
1237
|
+
if (
|
|
1238
|
+
decision.via !== "local" &&
|
|
1239
|
+
["timeout", "closed", "lease-unavailable"].includes(decision.via)
|
|
1240
|
+
) {
|
|
1241
|
+
decision = await local;
|
|
1242
|
+
}
|
|
614
1243
|
// Local answer won → settle the remote ask too (publishes
|
|
615
1244
|
// permission.resolved so device UIs clear the pending card).
|
|
616
1245
|
if (decision.via === "local") {
|
|
@@ -618,7 +1247,7 @@ export class RemoteApprovalBridge {
|
|
|
618
1247
|
const persisted = this.resolveLocally(requestId, decision.approved);
|
|
619
1248
|
return persisted && decision.approved === true;
|
|
620
1249
|
}
|
|
621
|
-
return decision
|
|
1250
|
+
return decision;
|
|
622
1251
|
};
|
|
623
1252
|
}
|
|
624
1253
|
|
|
@@ -645,11 +1274,17 @@ export class RemoteApprovalBridge {
|
|
|
645
1274
|
}
|
|
646
1275
|
try {
|
|
647
1276
|
if (this.client && this.remoteSessionId) {
|
|
648
|
-
await this.client
|
|
1277
|
+
const closed = await this.client
|
|
649
1278
|
.request("remote-session-close", {
|
|
650
1279
|
remoteSessionId: this.remoteSessionId,
|
|
651
1280
|
})
|
|
652
|
-
.catch(() =>
|
|
1281
|
+
.catch(() => null);
|
|
1282
|
+
if (closed?.statement) {
|
|
1283
|
+
this._requireMembershipHostStore().adopt(closed.statement, {
|
|
1284
|
+
expectedKind: "session.snapshot",
|
|
1285
|
+
expectedSessionId: this.remoteSessionId,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
653
1288
|
}
|
|
654
1289
|
} finally {
|
|
655
1290
|
this.client?.close();
|
|
@@ -723,6 +1358,8 @@ export async function startHeadlessRemoteApproval({
|
|
|
723
1358
|
}
|
|
724
1359
|
},
|
|
725
1360
|
}),
|
|
1361
|
+
consumeAuthorization: (authorization, context) =>
|
|
1362
|
+
bridge.consumeAuthorization(authorization, context),
|
|
726
1363
|
close: async () => {
|
|
727
1364
|
await bridge.close().catch(() => undefined);
|
|
728
1365
|
await server.stop().catch(() => undefined);
|