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
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process ownership and durable journal for one scoped plugin name.
|
|
3
|
+
*
|
|
4
|
+
* A fixed lock directory is published with one same-volume rename. The owner
|
|
5
|
+
* record carries an unguessable token, while every journal replacement is
|
|
6
|
+
* revision/digest chained and fsynced before publication. A crashed owner is
|
|
7
|
+
* never reclaimed by an ordinary mutation: an explicit recovery first proves
|
|
8
|
+
* the recorded local PID dead (or requires an operator force for a remote/
|
|
9
|
+
* unverifiable owner), then claims a second O_EXCL recovery marker.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
|
|
17
|
+
export const PLUGIN_TRANSACTION_JOURNAL_SCHEMA =
|
|
18
|
+
"cc-plugin-lifecycle-transaction/v1";
|
|
19
|
+
export const PLUGIN_TRANSACTION_LOCK_DIRNAME = ".plugin-transaction-lock";
|
|
20
|
+
export const PLUGIN_TRANSACTION_OWNER_FILENAME = "owner.json";
|
|
21
|
+
export const PLUGIN_TRANSACTION_JOURNAL_FILENAME = "journal.json";
|
|
22
|
+
|
|
23
|
+
const TOKEN_PATTERN = /^[a-f0-9]{32}$/;
|
|
24
|
+
const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
25
|
+
const SCOPE_PATTERN = /^(user|project|local)$/;
|
|
26
|
+
|
|
27
|
+
export const _deps = {
|
|
28
|
+
existsSync: fs.existsSync,
|
|
29
|
+
mkdirSync: fs.mkdirSync,
|
|
30
|
+
readFileSync: fs.readFileSync,
|
|
31
|
+
writeFileSync: fs.writeFileSync,
|
|
32
|
+
renameSync: fs.renameSync,
|
|
33
|
+
linkSync: fs.linkSync,
|
|
34
|
+
rmSync: fs.rmSync,
|
|
35
|
+
lstatSync: fs.lstatSync,
|
|
36
|
+
openSync: fs.openSync,
|
|
37
|
+
closeSync: fs.closeSync,
|
|
38
|
+
fsyncSync: fs.fsyncSync,
|
|
39
|
+
randomToken: () => crypto.randomBytes(16).toString("hex"),
|
|
40
|
+
now: () => Date.now(),
|
|
41
|
+
hostname: () => os.hostname(),
|
|
42
|
+
pid: () => process.pid,
|
|
43
|
+
isProcessAlive: (pid) => {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return error?.code === "EPERM";
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function acquirePluginTransactionLock({
|
|
54
|
+
name,
|
|
55
|
+
scope,
|
|
56
|
+
nameDir,
|
|
57
|
+
operation,
|
|
58
|
+
contextDigest,
|
|
59
|
+
}) {
|
|
60
|
+
const identity = normalizeIdentity({
|
|
61
|
+
name,
|
|
62
|
+
scope,
|
|
63
|
+
nameDir,
|
|
64
|
+
operation,
|
|
65
|
+
contextDigest,
|
|
66
|
+
});
|
|
67
|
+
ensureSafeNameDirectory(identity.nameDir);
|
|
68
|
+
const token = _deps.randomToken();
|
|
69
|
+
if (!TOKEN_PATTERN.test(token)) {
|
|
70
|
+
throw transactionError(
|
|
71
|
+
"PLUGIN_TRANSACTION_TOKEN_INVALID",
|
|
72
|
+
"plugin transaction owner token is invalid",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const owner = {
|
|
76
|
+
pid: _deps.pid(),
|
|
77
|
+
startedAt: _deps.now(),
|
|
78
|
+
hostname: cleanString(_deps.hostname(), 255),
|
|
79
|
+
token,
|
|
80
|
+
};
|
|
81
|
+
validateOwner(owner);
|
|
82
|
+
const initial = buildJournal({
|
|
83
|
+
identity,
|
|
84
|
+
owner,
|
|
85
|
+
revision: 0,
|
|
86
|
+
previousJournalDigest: null,
|
|
87
|
+
phase: "acquired",
|
|
88
|
+
transaction: null,
|
|
89
|
+
});
|
|
90
|
+
const lockDir = path.join(identity.nameDir, PLUGIN_TRANSACTION_LOCK_DIRNAME);
|
|
91
|
+
const candidate = `${lockDir}.acquire-${token}`;
|
|
92
|
+
try {
|
|
93
|
+
_deps.mkdirSync(candidate, { mode: 0o700 });
|
|
94
|
+
writeNewDurableJson(
|
|
95
|
+
path.join(candidate, PLUGIN_TRANSACTION_OWNER_FILENAME),
|
|
96
|
+
owner,
|
|
97
|
+
);
|
|
98
|
+
writeNewDurableJson(
|
|
99
|
+
path.join(candidate, PLUGIN_TRANSACTION_JOURNAL_FILENAME),
|
|
100
|
+
initial,
|
|
101
|
+
);
|
|
102
|
+
fsyncDirectory(candidate);
|
|
103
|
+
_deps.renameSync(candidate, lockDir);
|
|
104
|
+
fsyncDirectory(identity.nameDir);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
try {
|
|
107
|
+
_deps.rmSync(candidate, { recursive: true, force: true });
|
|
108
|
+
} catch {
|
|
109
|
+
// A uniquely-tokened acquisition directory cannot own the fixed lock.
|
|
110
|
+
}
|
|
111
|
+
if (_deps.existsSync(lockDir)) {
|
|
112
|
+
const incumbent = safeReadOwner(lockDir);
|
|
113
|
+
const locked = transactionError(
|
|
114
|
+
"PLUGIN_TRANSACTION_LOCKED",
|
|
115
|
+
`plugin lifecycle is already owned${incumbent?.pid ? ` by PID ${incumbent.pid}` : ""}`,
|
|
116
|
+
error,
|
|
117
|
+
);
|
|
118
|
+
if (incumbent?.pid) locked.ownerPid = incumbent.pid;
|
|
119
|
+
throw locked;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
return createHandle({ identity, owner, journal: initial, lockDir });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function assertPluginTransactionLock(
|
|
127
|
+
handle,
|
|
128
|
+
{ name, scope, nameDir, contextDigest } = {},
|
|
129
|
+
) {
|
|
130
|
+
if (!handle || handle.kind !== "plugin-transaction-lock") {
|
|
131
|
+
throw transactionError(
|
|
132
|
+
"PLUGIN_TRANSACTION_LOCK_REQUIRED",
|
|
133
|
+
"plugin lifecycle mutation requires an owned transaction lock",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const expected = normalizeIdentity({
|
|
137
|
+
name: name ?? handle.name,
|
|
138
|
+
scope: scope ?? handle.scope,
|
|
139
|
+
nameDir: nameDir ?? handle.nameDir,
|
|
140
|
+
operation: handle.operation,
|
|
141
|
+
contextDigest: contextDigest ?? handle.contextDigest,
|
|
142
|
+
});
|
|
143
|
+
if (
|
|
144
|
+
handle.name !== expected.name ||
|
|
145
|
+
handle.scope !== expected.scope ||
|
|
146
|
+
handle.contextDigest !== expected.contextDigest ||
|
|
147
|
+
path.resolve(handle.nameDir) !== expected.nameDir
|
|
148
|
+
) {
|
|
149
|
+
throw transactionError(
|
|
150
|
+
"PLUGIN_TRANSACTION_LOCK_SCOPE_MISMATCH",
|
|
151
|
+
"plugin transaction lock does not match the requested name/scope",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
assertCurrentAuthority(handle);
|
|
155
|
+
return handle;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function updatePluginTransactionJournal(
|
|
159
|
+
handle,
|
|
160
|
+
{ phase, transaction = undefined },
|
|
161
|
+
) {
|
|
162
|
+
assertPluginTransactionLock(handle);
|
|
163
|
+
const current = readAndValidateJournal(handle.lockDir, handle.owner);
|
|
164
|
+
if (
|
|
165
|
+
current.revision !== handle.journal.revision ||
|
|
166
|
+
current.journalDigest !== handle.journal.journalDigest
|
|
167
|
+
) {
|
|
168
|
+
throw transactionError(
|
|
169
|
+
"PLUGIN_TRANSACTION_JOURNAL_STALE",
|
|
170
|
+
"plugin transaction journal changed after this owner last observed it",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const next = buildJournal({
|
|
174
|
+
identity: handle,
|
|
175
|
+
owner: handle.owner,
|
|
176
|
+
revision: current.revision + 1,
|
|
177
|
+
previousJournalDigest: current.journalDigest,
|
|
178
|
+
phase,
|
|
179
|
+
transaction:
|
|
180
|
+
transaction === undefined ? current.transaction : cloneJson(transaction),
|
|
181
|
+
});
|
|
182
|
+
replaceDurableJson(
|
|
183
|
+
path.join(handle.lockDir, PLUGIN_TRANSACTION_JOURNAL_FILENAME),
|
|
184
|
+
next,
|
|
185
|
+
handle.owner.token,
|
|
186
|
+
);
|
|
187
|
+
handle.journal = next;
|
|
188
|
+
return cloneJson(next);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function inspectPluginTransactionLock({
|
|
192
|
+
name,
|
|
193
|
+
scope,
|
|
194
|
+
nameDir,
|
|
195
|
+
contextDigest,
|
|
196
|
+
}) {
|
|
197
|
+
const identity = normalizeIdentity({
|
|
198
|
+
name,
|
|
199
|
+
scope,
|
|
200
|
+
nameDir,
|
|
201
|
+
operation: "inspect",
|
|
202
|
+
contextDigest,
|
|
203
|
+
});
|
|
204
|
+
const lockDir = path.join(identity.nameDir, PLUGIN_TRANSACTION_LOCK_DIRNAME);
|
|
205
|
+
if (!_deps.existsSync(lockDir)) return null;
|
|
206
|
+
const owner = readAndValidateOwner(lockDir);
|
|
207
|
+
const journal = readAndValidateJournal(lockDir, owner);
|
|
208
|
+
if (
|
|
209
|
+
journal.name !== identity.name ||
|
|
210
|
+
journal.scope !== identity.scope ||
|
|
211
|
+
journal.contextDigest !== identity.contextDigest
|
|
212
|
+
) {
|
|
213
|
+
throw transactionError(
|
|
214
|
+
"PLUGIN_TRANSACTION_LOCK_SCOPE_MISMATCH",
|
|
215
|
+
"plugin transaction journal does not match the requested name/scope/context",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
...createHandle({
|
|
220
|
+
identity: { ...identity, operation: journal.operation },
|
|
221
|
+
owner,
|
|
222
|
+
journal,
|
|
223
|
+
lockDir,
|
|
224
|
+
}),
|
|
225
|
+
ownerAlive: ownerLiveness(owner),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function claimPluginTransactionRecovery({
|
|
230
|
+
name,
|
|
231
|
+
scope,
|
|
232
|
+
nameDir,
|
|
233
|
+
contextDigest,
|
|
234
|
+
force = false,
|
|
235
|
+
}) {
|
|
236
|
+
const inspected = inspectPluginTransactionLock({
|
|
237
|
+
name,
|
|
238
|
+
scope,
|
|
239
|
+
nameDir,
|
|
240
|
+
contextDigest,
|
|
241
|
+
});
|
|
242
|
+
if (!inspected) {
|
|
243
|
+
throw transactionError(
|
|
244
|
+
"PLUGIN_TRANSACTION_NOT_FOUND",
|
|
245
|
+
"no retained plugin transaction lock exists",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (inspected.ownerAlive === true && !force) {
|
|
249
|
+
throw transactionError(
|
|
250
|
+
"PLUGIN_TRANSACTION_OWNER_LIVE",
|
|
251
|
+
`plugin transaction owner PID ${inspected.owner.pid} is still live`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (inspected.ownerAlive === null && !force) {
|
|
255
|
+
throw transactionError(
|
|
256
|
+
"PLUGIN_TRANSACTION_OWNER_UNVERIFIABLE",
|
|
257
|
+
"plugin transaction owner belongs to another host; explicit force is required",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const claimPath = path.join(inspected.lockDir, ".recovery-claim.json");
|
|
261
|
+
acquireRecoveryClaim(claimPath, inspected, force);
|
|
262
|
+
try {
|
|
263
|
+
const owner = readAndValidateOwner(inspected.lockDir);
|
|
264
|
+
const journal = readAndValidateJournal(inspected.lockDir, owner);
|
|
265
|
+
if (
|
|
266
|
+
!sameOwner(owner, inspected.owner) ||
|
|
267
|
+
journal.journalDigest !== inspected.journal.journalDigest
|
|
268
|
+
) {
|
|
269
|
+
throw transactionError(
|
|
270
|
+
"PLUGIN_TRANSACTION_RECOVERY_STALE",
|
|
271
|
+
"plugin transaction authority changed while recovery was being claimed",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
inspected.recoveryClaim = readRecoveryClaim(claimPath);
|
|
275
|
+
inspected.journal = journal;
|
|
276
|
+
return inspected;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
releaseRecoveryClaim(claimPath, inspected.recoveryClaim);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function releasePluginTransactionLock(handle) {
|
|
284
|
+
assertPluginTransactionLock(handle);
|
|
285
|
+
if (handle.recoveryClaim) {
|
|
286
|
+
const claim = readRecoveryClaim(
|
|
287
|
+
path.join(handle.lockDir, ".recovery-claim.json"),
|
|
288
|
+
);
|
|
289
|
+
if (!sameRecoveryClaim(claim, handle.recoveryClaim)) {
|
|
290
|
+
throw transactionError(
|
|
291
|
+
"PLUGIN_TRANSACTION_RECOVERY_OWNERSHIP_LOST",
|
|
292
|
+
"plugin transaction recovery claim changed before release",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const current = readAndValidateJournal(handle.lockDir, handle.owner);
|
|
297
|
+
if (
|
|
298
|
+
current.revision !== handle.journal.revision ||
|
|
299
|
+
current.journalDigest !== handle.journal.journalDigest
|
|
300
|
+
) {
|
|
301
|
+
throw transactionError(
|
|
302
|
+
"PLUGIN_TRANSACTION_JOURNAL_STALE",
|
|
303
|
+
"plugin transaction journal changed before lock release",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const releasedDir = `${handle.lockDir}.release-${handle.owner.token}`;
|
|
307
|
+
_deps.renameSync(handle.lockDir, releasedDir);
|
|
308
|
+
fsyncDirectory(handle.nameDir);
|
|
309
|
+
let cleanupPending = false;
|
|
310
|
+
try {
|
|
311
|
+
_deps.rmSync(releasedDir, { recursive: true, force: true });
|
|
312
|
+
} catch {
|
|
313
|
+
cleanupPending = true;
|
|
314
|
+
}
|
|
315
|
+
handle.released = true;
|
|
316
|
+
return {
|
|
317
|
+
released: true,
|
|
318
|
+
...(cleanupPending
|
|
319
|
+
? { cleanupPending: true, cleanupPath: releasedDir }
|
|
320
|
+
: {}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function acquireRecoveryClaim(claimPath, inspected, force) {
|
|
325
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
326
|
+
const claim = {
|
|
327
|
+
pid: _deps.pid(),
|
|
328
|
+
startedAt: _deps.now(),
|
|
329
|
+
hostname: cleanString(_deps.hostname(), 255),
|
|
330
|
+
token: _deps.randomToken(),
|
|
331
|
+
observedOwnerToken: inspected.owner.token,
|
|
332
|
+
observedJournalDigest: inspected.journal.journalDigest,
|
|
333
|
+
};
|
|
334
|
+
validateOwner(claim);
|
|
335
|
+
try {
|
|
336
|
+
writeNewDurableJson(claimPath, claim);
|
|
337
|
+
fsyncDirectory(path.dirname(claimPath));
|
|
338
|
+
inspected.recoveryClaim = claim;
|
|
339
|
+
return;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error?.code !== "EEXIST") throw error;
|
|
342
|
+
const incumbent = readRecoveryClaim(claimPath);
|
|
343
|
+
const liveness = ownerLiveness(incumbent);
|
|
344
|
+
if (liveness === true || (liveness === null && !force)) {
|
|
345
|
+
throw transactionError(
|
|
346
|
+
"PLUGIN_TRANSACTION_RECOVERY_LOCKED",
|
|
347
|
+
"another process owns plugin transaction recovery",
|
|
348
|
+
error,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const stalePath = `${claimPath}.reclaim-${claim.token}`;
|
|
352
|
+
try {
|
|
353
|
+
_deps.renameSync(claimPath, stalePath);
|
|
354
|
+
} catch (reclaimError) {
|
|
355
|
+
if (reclaimError?.code === "ENOENT") continue;
|
|
356
|
+
throw reclaimError;
|
|
357
|
+
}
|
|
358
|
+
const moved = readRecoveryClaim(stalePath);
|
|
359
|
+
if (!sameRecoveryClaim(moved, incumbent)) {
|
|
360
|
+
restoreMovedRecoveryClaim(stalePath, claimPath);
|
|
361
|
+
throw transactionError(
|
|
362
|
+
"PLUGIN_TRANSACTION_RECOVERY_STALE",
|
|
363
|
+
"plugin transaction recovery owner changed while a stale claim was being fenced",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const movedLiveness = ownerLiveness(moved);
|
|
367
|
+
if (movedLiveness === true || (movedLiveness === null && !force)) {
|
|
368
|
+
restoreMovedRecoveryClaim(stalePath, claimPath);
|
|
369
|
+
throw transactionError(
|
|
370
|
+
"PLUGIN_TRANSACTION_RECOVERY_LOCKED",
|
|
371
|
+
"another process owns plugin transaction recovery",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
_deps.rmSync(stalePath, { force: true });
|
|
375
|
+
fsyncDirectory(path.dirname(claimPath));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
throw transactionError(
|
|
379
|
+
"PLUGIN_TRANSACTION_RECOVERY_LOCKED",
|
|
380
|
+
"could not claim plugin transaction recovery",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function restoreMovedRecoveryClaim(stalePath, claimPath) {
|
|
385
|
+
try {
|
|
386
|
+
// linkSync is exclusive when the destination exists. Keeping the moved
|
|
387
|
+
// inode until the fixed path is republished prevents a check-then-rename
|
|
388
|
+
// race from overwriting a third recovery owner on POSIX.
|
|
389
|
+
_deps.linkSync(stalePath, claimPath);
|
|
390
|
+
_deps.rmSync(stalePath, { force: true });
|
|
391
|
+
fsyncDirectory(path.dirname(claimPath));
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw transactionError(
|
|
394
|
+
"PLUGIN_TRANSACTION_RECOVERY_OWNERSHIP_LOST",
|
|
395
|
+
"could not restore a recovery claim that changed during stale-owner fencing",
|
|
396
|
+
error,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function releaseRecoveryClaim(claimPath, expected) {
|
|
402
|
+
if (!expected) return false;
|
|
403
|
+
let actual;
|
|
404
|
+
try {
|
|
405
|
+
actual = readRecoveryClaim(claimPath);
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
if (!sameRecoveryClaim(actual, expected)) return false;
|
|
410
|
+
_deps.rmSync(claimPath, { force: true });
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function createHandle({ identity, owner, journal, lockDir }) {
|
|
415
|
+
return {
|
|
416
|
+
kind: "plugin-transaction-lock",
|
|
417
|
+
name: identity.name,
|
|
418
|
+
scope: identity.scope,
|
|
419
|
+
contextDigest: identity.contextDigest,
|
|
420
|
+
nameDir: identity.nameDir,
|
|
421
|
+
operation: identity.operation,
|
|
422
|
+
lockDir,
|
|
423
|
+
owner: cloneJson(owner),
|
|
424
|
+
journal: cloneJson(journal),
|
|
425
|
+
released: false,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function normalizeIdentity({ name, scope, nameDir, operation, contextDigest }) {
|
|
430
|
+
const normalizedName = cleanString(name, 256);
|
|
431
|
+
const normalizedScope = cleanString(scope, 32);
|
|
432
|
+
const normalizedOperation = cleanString(operation, 64);
|
|
433
|
+
if (!normalizedName || !SCOPE_PATTERN.test(normalizedScope)) {
|
|
434
|
+
throw transactionError(
|
|
435
|
+
"PLUGIN_TRANSACTION_IDENTITY_INVALID",
|
|
436
|
+
"plugin transaction name/scope is invalid",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (!normalizedOperation) {
|
|
440
|
+
throw transactionError(
|
|
441
|
+
"PLUGIN_TRANSACTION_OPERATION_INVALID",
|
|
442
|
+
"plugin transaction operation is required",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const rawNameDir = String(nameDir || "").trim();
|
|
446
|
+
if (!rawNameDir) {
|
|
447
|
+
throw transactionError(
|
|
448
|
+
"PLUGIN_TRANSACTION_PATH_INVALID",
|
|
449
|
+
"plugin transaction name directory is required",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const resolvedNameDir = path.resolve(rawNameDir);
|
|
453
|
+
if (
|
|
454
|
+
!path.isAbsolute(resolvedNameDir) ||
|
|
455
|
+
resolvedNameDir === path.parse(resolvedNameDir).root
|
|
456
|
+
) {
|
|
457
|
+
throw transactionError(
|
|
458
|
+
"PLUGIN_TRANSACTION_PATH_INVALID",
|
|
459
|
+
"plugin transaction name directory is unsafe",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
const normalizedContextDigest =
|
|
463
|
+
cleanString(contextDigest, 64) ||
|
|
464
|
+
crypto
|
|
465
|
+
.createHash("sha256")
|
|
466
|
+
.update(`${normalizedScope}\0${resolvedNameDir}`)
|
|
467
|
+
.digest("hex");
|
|
468
|
+
if (!DIGEST_PATTERN.test(normalizedContextDigest)) {
|
|
469
|
+
throw transactionError(
|
|
470
|
+
"PLUGIN_TRANSACTION_CONTEXT_INVALID",
|
|
471
|
+
"plugin transaction context digest is invalid",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
name: normalizedName,
|
|
476
|
+
scope: normalizedScope,
|
|
477
|
+
nameDir: resolvedNameDir,
|
|
478
|
+
operation: normalizedOperation,
|
|
479
|
+
contextDigest: normalizedContextDigest,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function ensureSafeNameDirectory(nameDir) {
|
|
484
|
+
_deps.mkdirSync(nameDir, { recursive: true, mode: 0o700 });
|
|
485
|
+
const stat = _deps.lstatSync(nameDir);
|
|
486
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
487
|
+
throw transactionError(
|
|
488
|
+
"PLUGIN_TRANSACTION_PATH_UNSAFE",
|
|
489
|
+
"plugin transaction name directory must be a real directory",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function assertCurrentAuthority(handle) {
|
|
495
|
+
if (handle.released) {
|
|
496
|
+
throw transactionError(
|
|
497
|
+
"PLUGIN_TRANSACTION_LOCK_RELEASED",
|
|
498
|
+
"plugin transaction lock has already been released",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const owner = readAndValidateOwner(handle.lockDir);
|
|
502
|
+
if (!sameOwner(owner, handle.owner)) {
|
|
503
|
+
throw transactionError(
|
|
504
|
+
"PLUGIN_TRANSACTION_LOCK_OWNERSHIP_LOST",
|
|
505
|
+
"plugin transaction lock owner changed",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const claimPath = path.join(handle.lockDir, ".recovery-claim.json");
|
|
509
|
+
if (handle.recoveryClaim) {
|
|
510
|
+
const claim = readRecoveryClaim(claimPath);
|
|
511
|
+
if (!sameRecoveryClaim(claim, handle.recoveryClaim)) {
|
|
512
|
+
throw transactionError(
|
|
513
|
+
"PLUGIN_TRANSACTION_RECOVERY_OWNERSHIP_LOST",
|
|
514
|
+
"plugin transaction recovery claim changed",
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
} else if (_deps.existsSync(claimPath)) {
|
|
518
|
+
// An explicit recovery claim fences even the original owner. This is what
|
|
519
|
+
// makes --force-owner an override rather than a second concurrent writer.
|
|
520
|
+
readRecoveryClaim(claimPath);
|
|
521
|
+
throw transactionError(
|
|
522
|
+
"PLUGIN_TRANSACTION_RECOVERY_IN_PROGRESS",
|
|
523
|
+
"plugin transaction recovery has fenced the original owner",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function readAndValidateOwner(lockDir) {
|
|
529
|
+
let value;
|
|
530
|
+
try {
|
|
531
|
+
assertSafeLockDirectory(lockDir);
|
|
532
|
+
value = JSON.parse(
|
|
533
|
+
readSafeAuthorityFile(
|
|
534
|
+
path.join(lockDir, PLUGIN_TRANSACTION_OWNER_FILENAME),
|
|
535
|
+
16 * 1024,
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
throw transactionError(
|
|
540
|
+
"PLUGIN_TRANSACTION_OWNER_CORRUPT",
|
|
541
|
+
"plugin transaction owner metadata is unavailable or corrupt",
|
|
542
|
+
error,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
validateOwner(value);
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function safeReadOwner(lockDir) {
|
|
550
|
+
try {
|
|
551
|
+
return readAndValidateOwner(lockDir);
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function validateOwner(value) {
|
|
558
|
+
if (
|
|
559
|
+
!value ||
|
|
560
|
+
typeof value !== "object" ||
|
|
561
|
+
Array.isArray(value) ||
|
|
562
|
+
!Number.isSafeInteger(value.pid) ||
|
|
563
|
+
value.pid <= 0 ||
|
|
564
|
+
!Number.isFinite(value.startedAt) ||
|
|
565
|
+
!TOKEN_PATTERN.test(value.token) ||
|
|
566
|
+
typeof value.hostname !== "string" ||
|
|
567
|
+
!value.hostname
|
|
568
|
+
) {
|
|
569
|
+
throw transactionError(
|
|
570
|
+
"PLUGIN_TRANSACTION_OWNER_CORRUPT",
|
|
571
|
+
"plugin transaction owner metadata has an invalid shape",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function readAndValidateJournal(lockDir, owner) {
|
|
577
|
+
let journal;
|
|
578
|
+
try {
|
|
579
|
+
assertSafeLockDirectory(lockDir);
|
|
580
|
+
journal = JSON.parse(
|
|
581
|
+
readSafeAuthorityFile(
|
|
582
|
+
path.join(lockDir, PLUGIN_TRANSACTION_JOURNAL_FILENAME),
|
|
583
|
+
256 * 1024,
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
throw transactionError(
|
|
588
|
+
"PLUGIN_TRANSACTION_JOURNAL_CORRUPT",
|
|
589
|
+
"plugin transaction journal is unavailable or corrupt",
|
|
590
|
+
error,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const digest = journal?.journalDigest;
|
|
594
|
+
const authority = { ...journal };
|
|
595
|
+
delete authority.journalDigest;
|
|
596
|
+
if (
|
|
597
|
+
journal?.schemaVersion !== PLUGIN_TRANSACTION_JOURNAL_SCHEMA ||
|
|
598
|
+
!DIGEST_PATTERN.test(journal.contextDigest || "") ||
|
|
599
|
+
!Number.isSafeInteger(journal.revision) ||
|
|
600
|
+
journal.revision < 0 ||
|
|
601
|
+
!sameOwner(journal.owner, owner) ||
|
|
602
|
+
!DIGEST_PATTERN.test(digest || "") ||
|
|
603
|
+
sha256Canonical(authority) !== digest ||
|
|
604
|
+
(journal.revision === 0
|
|
605
|
+
? journal.previousJournalDigest !== null
|
|
606
|
+
: !DIGEST_PATTERN.test(journal.previousJournalDigest || ""))
|
|
607
|
+
) {
|
|
608
|
+
throw transactionError(
|
|
609
|
+
"PLUGIN_TRANSACTION_JOURNAL_CORRUPT",
|
|
610
|
+
"plugin transaction journal authority or digest is invalid",
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return journal;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function buildJournal({
|
|
617
|
+
identity,
|
|
618
|
+
owner,
|
|
619
|
+
revision,
|
|
620
|
+
previousJournalDigest,
|
|
621
|
+
phase,
|
|
622
|
+
transaction,
|
|
623
|
+
}) {
|
|
624
|
+
const authority = {
|
|
625
|
+
schemaVersion: PLUGIN_TRANSACTION_JOURNAL_SCHEMA,
|
|
626
|
+
revision,
|
|
627
|
+
previousJournalDigest,
|
|
628
|
+
name: identity.name,
|
|
629
|
+
scope: identity.scope,
|
|
630
|
+
contextDigest: identity.contextDigest,
|
|
631
|
+
operation: identity.operation,
|
|
632
|
+
owner: cloneJson(owner),
|
|
633
|
+
phase: cleanString(phase, 64),
|
|
634
|
+
transaction: transaction == null ? null : cloneJson(transaction),
|
|
635
|
+
updatedAt: new Date(_deps.now()).toISOString(),
|
|
636
|
+
};
|
|
637
|
+
if (!authority.phase) {
|
|
638
|
+
throw transactionError(
|
|
639
|
+
"PLUGIN_TRANSACTION_PHASE_INVALID",
|
|
640
|
+
"plugin transaction phase is required",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return { ...authority, journalDigest: sha256Canonical(authority) };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function readRecoveryClaim(claimPath) {
|
|
647
|
+
let claim;
|
|
648
|
+
try {
|
|
649
|
+
claim = JSON.parse(readSafeAuthorityFile(claimPath, 32 * 1024));
|
|
650
|
+
} catch (error) {
|
|
651
|
+
throw transactionError(
|
|
652
|
+
"PLUGIN_TRANSACTION_RECOVERY_OWNER_CORRUPT",
|
|
653
|
+
"plugin transaction recovery owner metadata is corrupt",
|
|
654
|
+
error,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
validateOwner(claim);
|
|
658
|
+
if (
|
|
659
|
+
!TOKEN_PATTERN.test(claim.observedOwnerToken || "") ||
|
|
660
|
+
!DIGEST_PATTERN.test(claim.observedJournalDigest || "")
|
|
661
|
+
) {
|
|
662
|
+
throw transactionError(
|
|
663
|
+
"PLUGIN_TRANSACTION_RECOVERY_OWNER_CORRUPT",
|
|
664
|
+
"plugin transaction recovery observation is invalid",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return claim;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function assertSafeLockDirectory(lockDir) {
|
|
671
|
+
const stat = _deps.lstatSync(lockDir);
|
|
672
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
673
|
+
throw transactionError(
|
|
674
|
+
"PLUGIN_TRANSACTION_PATH_UNSAFE",
|
|
675
|
+
"plugin transaction lock path is not a real directory",
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function readSafeAuthorityFile(file, maxBytes) {
|
|
681
|
+
const stat = _deps.lstatSync(file);
|
|
682
|
+
if (
|
|
683
|
+
stat.isSymbolicLink() ||
|
|
684
|
+
!stat.isFile() ||
|
|
685
|
+
stat.nlink > 1 ||
|
|
686
|
+
stat.size <= 0 ||
|
|
687
|
+
stat.size > maxBytes
|
|
688
|
+
) {
|
|
689
|
+
throw transactionError(
|
|
690
|
+
"PLUGIN_TRANSACTION_AUTHORITY_UNSAFE",
|
|
691
|
+
`plugin transaction authority file is unsafe: ${path.basename(file)}`,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
return _deps.readFileSync(file, "utf8");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function ownerLiveness(owner) {
|
|
698
|
+
if (owner.hostname !== cleanString(_deps.hostname(), 255)) return null;
|
|
699
|
+
return _deps.isProcessAlive(owner.pid);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function sameOwner(left, right) {
|
|
703
|
+
return Boolean(
|
|
704
|
+
left &&
|
|
705
|
+
right &&
|
|
706
|
+
left.pid === right.pid &&
|
|
707
|
+
left.startedAt === right.startedAt &&
|
|
708
|
+
left.hostname === right.hostname &&
|
|
709
|
+
left.token === right.token,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function sameRecoveryClaim(left, right) {
|
|
714
|
+
return Boolean(
|
|
715
|
+
sameOwner(left, right) &&
|
|
716
|
+
left.observedOwnerToken === right.observedOwnerToken &&
|
|
717
|
+
left.observedJournalDigest === right.observedJournalDigest,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function writeNewDurableJson(file, value) {
|
|
722
|
+
let descriptor = null;
|
|
723
|
+
try {
|
|
724
|
+
descriptor = _deps.openSync(file, "wx", 0o600);
|
|
725
|
+
_deps.writeFileSync(descriptor, `${canonicalJson(value)}\n`, "utf8");
|
|
726
|
+
_deps.fsyncSync(descriptor);
|
|
727
|
+
} finally {
|
|
728
|
+
if (descriptor != null) _deps.closeSync(descriptor);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function replaceDurableJson(file, value, token) {
|
|
733
|
+
const attemptToken = _deps.randomToken();
|
|
734
|
+
if (!TOKEN_PATTERN.test(attemptToken)) {
|
|
735
|
+
throw transactionError(
|
|
736
|
+
"PLUGIN_TRANSACTION_TOKEN_INVALID",
|
|
737
|
+
"plugin transaction journal token is invalid",
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const temporary = path.join(
|
|
741
|
+
path.dirname(file),
|
|
742
|
+
`.${path.basename(file)}.${token}.${value.revision}.${attemptToken}.tmp`,
|
|
743
|
+
);
|
|
744
|
+
try {
|
|
745
|
+
writeNewDurableJson(temporary, value);
|
|
746
|
+
_deps.renameSync(temporary, file);
|
|
747
|
+
fsyncDirectory(path.dirname(file));
|
|
748
|
+
} finally {
|
|
749
|
+
try {
|
|
750
|
+
_deps.rmSync(temporary, { force: true });
|
|
751
|
+
} catch {
|
|
752
|
+
// A failed temp cleanup cannot alter the authoritative journal path.
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function fsyncDirectory(directory) {
|
|
758
|
+
let descriptor = null;
|
|
759
|
+
try {
|
|
760
|
+
descriptor = _deps.openSync(directory, "r");
|
|
761
|
+
_deps.fsyncSync(descriptor);
|
|
762
|
+
return true;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (
|
|
765
|
+
process.platform === "win32" &&
|
|
766
|
+
["EACCES", "EINVAL", "EISDIR", "EPERM"].includes(error?.code)
|
|
767
|
+
) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
} finally {
|
|
772
|
+
if (descriptor != null) _deps.closeSync(descriptor);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function transactionError(code, message, cause = null) {
|
|
777
|
+
const error = cause ? new Error(message, { cause }) : new Error(message);
|
|
778
|
+
error.code = code;
|
|
779
|
+
return error;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function cleanString(value, max) {
|
|
783
|
+
return String(value ?? "")
|
|
784
|
+
.replace(/\p{Cc}/gu, "")
|
|
785
|
+
.trim()
|
|
786
|
+
.slice(0, max);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function cloneJson(value) {
|
|
790
|
+
return JSON.parse(JSON.stringify(value));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function canonicalJson(value) {
|
|
794
|
+
if (value == null || typeof value !== "object") return JSON.stringify(value);
|
|
795
|
+
if (Array.isArray(value)) {
|
|
796
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
797
|
+
}
|
|
798
|
+
return `{${Object.keys(value)
|
|
799
|
+
.sort()
|
|
800
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
801
|
+
.join(",")}}`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function sha256Canonical(value) {
|
|
805
|
+
return crypto.createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
806
|
+
}
|