chainlesschain 0.163.2 → 0.163.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/scripts/command-lifecycle-report.mjs +330 -38
- package/src/assets/web-panel/assets/{AIOps-DnGsmsqJ.js → AIOps-DB5X7qqV.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CrO6fcQ7.js → ActionButton-DxHRtKLk.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DsBdOaP2.js → Analytics-DIH5QrxT.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DmQslKUB.js → AppLayout-uu0oxVz7.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BvyrT_QQ.js → Artifacts-BNSMiW9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-prndoLiC.js → Audit-DcAzB4_x.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DOTFyA55.js → BackgroundAgents-ClSsvjIc.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-9zOnPp_8.js → Backup-BMciY0js.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C4PC6sAG.js → BaseInput-Hn3b57Sh.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BBAF-W9v.js → Chat-CoSlC53V.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CWhvvloO.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BI9iculd.js → Checkbox-B4L4Eu4w.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-3DP9BCKb.js → Codegen-C77FgRxF.js} +1 -1
- package/src/assets/web-panel/assets/{Col-df-2TnJX.js → Col-BrLtJLH3.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CHs8gG_A.js → Community-Be7pXXhA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CDfF6q7V.js → Compact-DuGPecjo.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CthrDnO7.js → Compliance-CzS66mjP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-HjIqsWJw.js → Cowork-CEhjCoyK.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BKc196fb.js → Cron-D7ir8Vja.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CJnjdc8W.js → Crosschain-BcXmuLxf.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BHUy-8Q2.js → DID-Cto1dtBy.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-QDB7sKeY.js → Dashboard-B7KjgwOX.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DStcVK11.js → Dropdown-COMqxijg.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BwSUskMJ.js → EmailListRenderer-BZrya-nf.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgF_kurT.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-k8wvv-wN.js → Federation-kZU8c-27.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-C11qoq2r.js → FormItemContext-sOV22v2-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BIIpr9kg.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BlA1exTe.js → Git-K-ZxsLsH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYa1ZYM4.js → Governance-D3OEnawI.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-6_kKVIt6.js → Inference-Bll0gRmP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CB4AwxYy.js → KnowledgeGraph-DBXwjr53.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-xf7nHFAe.js → Logs-CEzPwEfP.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-Bf3DEqP6.js → MarkdownRenderer-CLmCbHx6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-aPg8e4Jt.js → Marketplace-BYyRyOPz.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C-TNlVUf.js → McpTools-DVu8w_rM.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DMTPIpGa.js → Memory-tenuZ_hO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BxyvveRq.js → MobileBridge-Bxgy4ozS.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DyPCrAyQ.js → MobileProjects-Dgx1D6sc.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BdhqNXp6.js → Mtc-CRfJ8r8j.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Cz-HFxzW.js → MtcAudit-BBJq4Aiz.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-CIUaFNsx.js → Multisig-B3ii9lai.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CWe6-J17.js → NLProgramming-FIi-Cubv.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-vYAKeviU.js → Notes-DIq8MPf1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DQL9-2d7.js → NotificationSettings-BFOD9eGd.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DnXj1jBn.js → OrderTableRenderer--41u7vre.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B3spU8AN.js → Organization-B_0AWztW.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-DgEwo7vm.js → Overflow-B6I7TuXv.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-hujMRH8X.js → P2P-Cdf6qens.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C_qhdg9h.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-vp7LMcSf.js → Permissions-6nFBjsQp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Dxr5Hgnq.js → PersonalDataHub-nQYV7xpn.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DEPZqqH4.js → Pipeline-CBVod5jm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DTpDiKPe.js → Privacy-D52jW1Nt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-jDJsRWmF.js → ProjectInit-Bdx3BYhf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-Dhor5RWH.js → ProjectSettings-b7xr4DUa.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DhfifCKx.js +1 -0
- package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-CjIw-wIb.js → QrScannerModal-FQKSmc3e.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DX6jcqoi.js → QuickAsk-CSTrhzcF.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DopXkhar.js → Recommend-CxMABOUh.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BLvqJE9c.js → RemoteSession-Cv2T8yRa.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DhRj2Me7.js → Reputation-CE1k4Vvu.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DQ53vuPq.js → Row-BNgNcAV_.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BslvwiBD.js → RssFeed-5ZiBZ3ph.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DJKBDNKZ.js → Search-mDOIF1UV.js} +1 -1
- package/src/assets/web-panel/assets/{Security-COhpxHER.js → Security-CtuCTC-e.js} +3 -3
- package/src/assets/web-panel/assets/{Services-dbulD6cI.js → Services-DxFO2Zg7.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-ciIVNfhC.js → Skeleton-CKmKkH9y.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-AiRTkTCk.js → Skills-Bmzl9qkk.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DDMcWYBc.js → Sla-9RqaWHOC.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BPxWbpXO.js → SpeechSettings-CGdzFIYw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BoEOXCyg.js → SyncSettings-CvNuCeBT.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-B_sLdcnw.js +1 -0
- package/src/assets/web-panel/assets/{Templates-UowY8Ixe.js → Templates-D9_YijdK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DauSLQXN.js → Tenant-Cuc7BUSP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DBj7Jneq.js → Terminal-6VYRnJgd.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-UMrPwoZC.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-4ea96-bn.js → Tokens-BZ94Lt5W.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger--ISCqt4b.js → Trigger-DEcZgDaX.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-D4GVimRJ.js → Trust-CC3z9THe.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Ci-nXRQc.js → UkeySign-Cvtw8cV3.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Cjk7mKPw.js → VideoEditing-CL4e6Bn3.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BgOLOO3Z.js → Wallet-ycEemP27.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CCX0K29i.js → WebAuthn-DgZ4X_aw.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Ch3zgIsE.js → WorkflowEditor-D0qMhSv7.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DKOU_2Vb.js → chat-DsAg01Zn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-6OVo4u3C.js → colors-B0oqAhrQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BH78Hlsq.js → compact-item-CmX0Rabz.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CFRpSbbC.js → createContext-Be4cn1Ub.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DNQhgcVi.js → hasIn-D5v12kVq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkN3iTTp.js → index-0eWLkQKT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkNEMANR.js → index-301ogenv.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3RtTT4K.js → index-5LGY0JbG.js} +1 -1
- package/src/assets/web-panel/assets/{index-dDkoL4Pe.js → index-7VKDn-t1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHa4WyTZ.js → index-8WvYGnON.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaYqY35Z.js → index-B1YGjOWC.js} +1 -1
- package/src/assets/web-panel/assets/{index-bKm3TUDp.js → index-B3mUHuVc.js} +1 -1
- package/src/assets/web-panel/assets/{index--rq90rL9.js → index-BA4oulaP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaDgdJYj.js → index-BBREnn1V.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_Z-YoxX.js → index-BBvyHguy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkqHgG-b.js → index-BKmsS2jj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrcLHN2F.js → index-BRIqLy1m.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaVE6E8y.js → index-BXGSbelg.js} +3 -3
- package/src/assets/web-panel/assets/{index-BYrnm5Ob.js → index-BaE0Ui07.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDSR7ecX.js → index-BoAIAZ5C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNuCHHQT.js → index-BqltWojQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Eag8e_mA.js → index-CGiIyTZw.js} +1 -1
- package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
- package/src/assets/web-panel/assets/{index-BepBTpa8.js → index-CZ2wUEgr.js} +1 -1
- package/src/assets/web-panel/assets/{index-HNGGVF6o.js → index-CZpfzcuy.js} +1 -1
- package/src/assets/web-panel/assets/{index-AuBhLqOm.js → index-Ckrk3pIV.js} +1 -1
- package/src/assets/web-panel/assets/{index-7TC3S8SN.js → index-CqoCZ5vR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFPJ8yzF.js → index-CyWBir7N.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bb6kDrUx.js → index-CyyGTMKd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKN2noIF.js → index-D0BSkxYQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du0rnksC.js → index-D5YVopyr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxRy5LPR.js → index-DKUBj36_.js} +1 -1
- package/src/assets/web-panel/assets/{index-sm7W8AXy.js → index-DM724NgU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BujsOZfc.js → index-DQRsQen0.js} +1 -1
- package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
- package/src/assets/web-panel/assets/{index-Y9MJv28e.js → index-DikJ0Qqe.js} +1 -1
- package/src/assets/web-panel/assets/{index-caye7AOE.js → index-Dn2dSdXx.js} +1 -1
- package/src/assets/web-panel/assets/{index-cdf7fd7d.js → index-DyUaFWEk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnnXQot_.js → index-Hfe_dEGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvHGtNnF.js → index-_JjRLSD0.js} +1 -1
- package/src/assets/web-panel/assets/{index-nbfHl1oV.js → index-frSojpHj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cu3L4r5l.js → index-jK3ax6Z6.js} +1 -1
- package/src/assets/web-panel/assets/{index-D84id2Y4.js → index-qMuZ-NRR.js} +1 -1
- package/src/assets/web-panel/assets/{index-hPY0fFrd.js → index-q_fQ94SE.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BZ9w6jPL.js → initDefaultProps-D4VdhJ9_.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BnwUiOho.js → motion-CZJA0MQW.js} +1 -1
- package/src/assets/web-panel/assets/{move-ByUKJnNm.js → move-BqEs7Sb1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-V7yK1KEl.js → mtc-parser-BpHEtsu1.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BEXBBt4j.js → omit-BW94tY9W.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-qDm_46Gi.js → pickAttrs-BnBjPWP9.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BIbhEo_0.js → placementArrow-uUkjQ_hu.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-NbPbDgA_.js → responsiveObserve-B-klUqXK.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BnUWglZ0.js → slide-CvY1cNAs.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX8_wZI5.js → statusUtils-Ix5sEDT2.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BV4OycQX.js → styleChecker-C8qcXOne.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Od_H5llY.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BWI-PyiX.js → useFs-DGJr345n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BqMuhG3g.js → usePersonalDataHub-BboN99rI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DNd23z6f.js → vnode-Bgt8Vgte.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Cjaxnm7l.js → zoom-BhJtErgt.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/commands/agenda.js +6 -5
- package/src/commands/agent.js +237 -75
- package/src/commands/agents.js +6 -0
- package/src/commands/artifacts.js +98 -0
- package/src/commands/background-session.js +11 -1
- package/src/commands/mcp.js +218 -31
- package/src/commands/review.js +221 -22
- package/src/commands/session-mcp-recovery.js +5 -5
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/ws-agent-handler.js +492 -14
- package/src/harness/jsonl-session-store.js +39 -1
- package/src/harness/mcp-client.js +414 -71
- package/src/harness/prompt-compressor.js +24 -0
- package/src/harness/provider-backed-compaction.js +205 -0
- package/src/harness/worktree-isolator.js +26 -3
- package/src/lib/agent-worktree.js +41 -2
- package/src/lib/app-preview.js +3 -0
- package/src/lib/background-agent-supervisor.js +790 -186
- package/src/lib/background-command-argv.js +318 -0
- package/src/lib/background-launch-profile.js +1 -0
- package/src/lib/background-phase-reporter.js +22 -2
- package/src/lib/background-worktree-policy.js +109 -0
- package/src/lib/command-lifecycle-report.js +1340 -145
- package/src/lib/cowork-mcp-tools.js +51 -6
- package/src/lib/cowork-task-runner.js +1 -0
- package/src/lib/delivery-coordinator.js +30 -5
- package/src/lib/delivery-production-adapter.js +1777 -0
- package/src/lib/delivery-production-runner.js +240 -0
- package/src/lib/durable-security-store.js +29 -17
- package/src/lib/execution-trace/trace-context.cjs +179 -0
- package/src/lib/execution-trace/trace-context.js +8 -175
- package/src/lib/mcp-call-ledger-store.js +58 -6
- package/src/lib/mcp-headers-helper.js +76 -8
- package/src/lib/mcp-host-recovery-runtime.js +2 -0
- package/src/lib/mcp-ledger-recovery-admission.js +39 -3
- package/src/lib/mcp-recovery-adjudication.js +256 -19
- package/src/lib/mcp-sandbox-policy.js +184 -0
- package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
- package/src/lib/mcp-stdio-executable-identity.js +33 -2
- package/src/lib/mcp-stdio-execution-authority.js +163 -0
- package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
- package/src/lib/mcp-stdio-package-materialization.js +721 -333
- package/src/lib/mcp-stdio-workspace-authority.js +252 -0
- package/src/lib/observability/index.js +2 -0
- package/src/lib/observability/otlp-exporter.js +2 -2
- package/src/lib/packer/pack-update-applier.js +8 -1
- package/src/lib/packer/pkg-config-generator.js +34 -0
- package/src/lib/plugin-runtime/sandbox-policy.js +42 -2
- package/src/lib/process-execution-broker/index.js +213 -109
- package/src/lib/process-execution-broker/platform-sandbox.js +1024 -169
- package/src/lib/process-tree-termination.js +17 -1
- package/src/lib/scheduler-kernel/contract.js +315 -0
- package/src/lib/scheduler-kernel/store.js +1345 -0
- package/src/lib/session-host-lease.js +403 -30
- package/src/lib/session-persistence-failure.js +4 -1
- package/src/lib/skill-mcp.js +65 -4
- package/src/lib/sub-agent-context.js +8 -0
- package/src/lib/web-fetch.js +4 -4
- package/src/repl/agent-repl.js +285 -38
- package/src/runtime/agent-core.js +1044 -90
- package/src/runtime/agent-runtime.js +61 -8
- package/src/runtime/headless-runner.js +260 -166
- package/src/runtime/headless-stream.js +254 -25
- package/src/runtime/mcp-config.js +235 -28
- package/src/workers/background-agent-worker.js +492 -71
- package/src/assets/web-panel/assets/Projects-08xUDZGt.js +0 -1
- package/src/assets/web-panel/assets/Providers-BGs47s1a.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CEYFJwmr.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DEMblAIm.js +0 -1
- package/src/assets/web-panel/assets/index-BTvq-36D.js +0 -1
- package/src/assets/web-panel/assets/index-hUpvk6Jl.js +0 -1
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* sandbox. Every process sharing CHAINLESSCHAIN_HOME observes the same strict
|
|
6
6
|
* state file. A monotonically increasing fencing token prevents an expired or
|
|
7
7
|
* crashed host from publishing through guarded session-store write paths after
|
|
8
|
-
* a successor takes over.
|
|
8
|
+
* a successor takes over. A separate monotonic revocation epoch lets an
|
|
9
|
+
* operator fence a still-live host; every guarded transition re-reads it.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import fs from "node:fs";
|
|
@@ -22,13 +23,19 @@ export const SESSION_HOST_LEASE_HELD_CODE = "CC_SESSION_HOST_LEASE_HELD";
|
|
|
22
23
|
export const SESSION_HOST_LEASE_FENCED_CODE = "CC_SESSION_HOST_LEASE_FENCED";
|
|
23
24
|
export const SESSION_HOST_LEASE_UNAVAILABLE_CODE =
|
|
24
25
|
"CC_SESSION_HOST_LEASE_UNAVAILABLE";
|
|
26
|
+
export const SESSION_HOST_REVOCATION_CONFLICT_CODE =
|
|
27
|
+
"CC_SESSION_HOST_REVOCATION_CONFLICT";
|
|
25
28
|
|
|
26
|
-
const STORE_VERSION =
|
|
29
|
+
const STORE_VERSION = 2;
|
|
30
|
+
const LEGACY_STORE_VERSION = 1;
|
|
27
31
|
const DEFAULT_TTL_MS = 30_000;
|
|
28
32
|
const DEFAULT_HEARTBEAT_MS = 5_000;
|
|
29
33
|
const MAX_SESSION_ID_BYTES = 1024;
|
|
34
|
+
const MAX_REVOCATIONS = 1024;
|
|
30
35
|
const HOST_KIND_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
31
36
|
const LEASE_ID_PATTERN = /^lease-[0-9a-f-]{36}$/;
|
|
37
|
+
const REVOCATION_REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
|
|
38
|
+
const REVOCATION_REASON_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
32
39
|
|
|
33
40
|
function leaseError(code, message, details = {}, cause = null) {
|
|
34
41
|
const error = new Error(message, cause ? { cause } : undefined);
|
|
@@ -58,6 +65,21 @@ function canonicalHostKind(value) {
|
|
|
58
65
|
return kind;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
function canonicalRevocationRequestId(value) {
|
|
69
|
+
if (typeof value !== "string" || !REVOCATION_REQUEST_ID_PATTERN.test(value)) {
|
|
70
|
+
throw new TypeError("session host revocation requestId is invalid");
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function canonicalRevocationReason(value) {
|
|
76
|
+
const reason = String(value || "operator").toLowerCase();
|
|
77
|
+
if (!REVOCATION_REASON_PATTERN.test(reason)) {
|
|
78
|
+
throw new TypeError("session host revocation reasonCode is invalid");
|
|
79
|
+
}
|
|
80
|
+
return reason;
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
function positiveInteger(value, label) {
|
|
62
84
|
const number = Number(value);
|
|
63
85
|
if (!Number.isSafeInteger(number) || number < 1) {
|
|
@@ -66,7 +88,7 @@ function positiveInteger(value, label) {
|
|
|
66
88
|
return number;
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
function normalizeActiveLease(value) {
|
|
91
|
+
function normalizeActiveLease(value, fallbackRevocationEpoch = null) {
|
|
70
92
|
if (value === null) return null;
|
|
71
93
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
72
94
|
throw new TypeError("active session host lease must be an object or null");
|
|
@@ -76,6 +98,8 @@ function normalizeActiveLease(value) {
|
|
|
76
98
|
!LEASE_ID_PATTERN.test(value.leaseId) ||
|
|
77
99
|
!Number.isSafeInteger(value.fencingToken) ||
|
|
78
100
|
value.fencingToken < 1 ||
|
|
101
|
+
!Number.isSafeInteger(value.revocationEpoch ?? fallbackRevocationEpoch) ||
|
|
102
|
+
(value.revocationEpoch ?? fallbackRevocationEpoch) < 0 ||
|
|
79
103
|
!Number.isSafeInteger(value.ownerPid) ||
|
|
80
104
|
value.ownerPid < 1 ||
|
|
81
105
|
typeof value.hostKind !== "string" ||
|
|
@@ -92,6 +116,7 @@ function normalizeActiveLease(value) {
|
|
|
92
116
|
return Object.freeze({
|
|
93
117
|
leaseId: value.leaseId,
|
|
94
118
|
fencingToken: value.fencingToken,
|
|
119
|
+
revocationEpoch: value.revocationEpoch ?? fallbackRevocationEpoch,
|
|
95
120
|
ownerPid: value.ownerPid,
|
|
96
121
|
hostKind: value.hostKind,
|
|
97
122
|
acquiredAtMs: value.acquiredAtMs,
|
|
@@ -100,31 +125,92 @@ function normalizeActiveLease(value) {
|
|
|
100
125
|
});
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
function normalizeRevocation(value, expectedEpoch) {
|
|
129
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
130
|
+
throw new TypeError("session host revocation must be an object");
|
|
131
|
+
}
|
|
132
|
+
const targetIsNull = value.targetLeaseId === null;
|
|
133
|
+
if (
|
|
134
|
+
canonicalRevocationRequestId(value.requestId) !== value.requestId ||
|
|
135
|
+
canonicalRevocationReason(value.reasonCode) !== value.reasonCode ||
|
|
136
|
+
!Number.isSafeInteger(value.revocationEpoch) ||
|
|
137
|
+
value.revocationEpoch !== expectedEpoch ||
|
|
138
|
+
!Number.isSafeInteger(value.revokedAtMs) ||
|
|
139
|
+
value.revokedAtMs < 0 ||
|
|
140
|
+
(targetIsNull
|
|
141
|
+
? value.targetFencingToken !== null || value.targetOwnerPid !== null
|
|
142
|
+
: !LEASE_ID_PATTERN.test(String(value.targetLeaseId || "")) ||
|
|
143
|
+
!Number.isSafeInteger(value.targetFencingToken) ||
|
|
144
|
+
value.targetFencingToken < 1 ||
|
|
145
|
+
!Number.isSafeInteger(value.targetOwnerPid) ||
|
|
146
|
+
value.targetOwnerPid < 1)
|
|
147
|
+
) {
|
|
148
|
+
throw new TypeError("session host revocation is invalid");
|
|
149
|
+
}
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
requestId: value.requestId,
|
|
152
|
+
revocationEpoch: value.revocationEpoch,
|
|
153
|
+
reasonCode: value.reasonCode,
|
|
154
|
+
revokedAtMs: value.revokedAtMs,
|
|
155
|
+
targetLeaseId: value.targetLeaseId,
|
|
156
|
+
targetFencingToken: value.targetFencingToken,
|
|
157
|
+
targetOwnerPid: value.targetOwnerPid,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
103
161
|
function validateStore(value, sessionId, { missing = false } = {}) {
|
|
104
162
|
if (missing && Object.keys(value).length === 0) {
|
|
105
163
|
return Object.freeze({
|
|
106
164
|
version: STORE_VERSION,
|
|
107
165
|
sessionId,
|
|
108
166
|
lastFencingToken: 0,
|
|
167
|
+
revocationEpoch: 0,
|
|
168
|
+
revocations: Object.freeze([]),
|
|
109
169
|
active: null,
|
|
110
170
|
});
|
|
111
171
|
}
|
|
172
|
+
const legacy = value?.version === LEGACY_STORE_VERSION;
|
|
112
173
|
if (
|
|
113
|
-
value?.version !== STORE_VERSION ||
|
|
174
|
+
(!legacy && value?.version !== STORE_VERSION) ||
|
|
114
175
|
value?.sessionId !== sessionId ||
|
|
115
176
|
!Number.isSafeInteger(value?.lastFencingToken) ||
|
|
116
177
|
value.lastFencingToken < 0
|
|
117
178
|
) {
|
|
118
179
|
throw new TypeError("session host lease store is invalid");
|
|
119
180
|
}
|
|
120
|
-
const
|
|
181
|
+
const revocationEpoch = legacy ? 0 : value.revocationEpoch;
|
|
182
|
+
const rawRevocations = legacy ? [] : value.revocations;
|
|
183
|
+
if (
|
|
184
|
+
!Number.isSafeInteger(revocationEpoch) ||
|
|
185
|
+
revocationEpoch < 0 ||
|
|
186
|
+
!Array.isArray(rawRevocations) ||
|
|
187
|
+
rawRevocations.length > MAX_REVOCATIONS ||
|
|
188
|
+
rawRevocations.length !== revocationEpoch
|
|
189
|
+
) {
|
|
190
|
+
throw new TypeError("session host revocation authority is invalid");
|
|
191
|
+
}
|
|
192
|
+
const revocations = rawRevocations.map((record, index) =>
|
|
193
|
+
normalizeRevocation(record, index + 1),
|
|
194
|
+
);
|
|
195
|
+
if (
|
|
196
|
+
new Set(revocations.map((record) => record.requestId)).size !==
|
|
197
|
+
revocations.length
|
|
198
|
+
) {
|
|
199
|
+
throw new TypeError("session host revocation requestIds must be unique");
|
|
200
|
+
}
|
|
201
|
+
const active = normalizeActiveLease(value.active, legacy ? 0 : null);
|
|
121
202
|
if (active && active.fencingToken !== value.lastFencingToken) {
|
|
122
203
|
throw new TypeError("active lease does not match the last fencing token");
|
|
123
204
|
}
|
|
205
|
+
if (active && active.revocationEpoch !== revocationEpoch) {
|
|
206
|
+
throw new TypeError("active lease predates the revocation authority");
|
|
207
|
+
}
|
|
124
208
|
return Object.freeze({
|
|
125
209
|
version: STORE_VERSION,
|
|
126
210
|
sessionId,
|
|
127
211
|
lastFencingToken: value.lastFencingToken,
|
|
212
|
+
revocationEpoch,
|
|
213
|
+
revocations: Object.freeze(revocations),
|
|
128
214
|
active,
|
|
129
215
|
});
|
|
130
216
|
}
|
|
@@ -152,6 +238,10 @@ function publicLease(active) {
|
|
|
152
238
|
return Object.freeze({ ...active });
|
|
153
239
|
}
|
|
154
240
|
|
|
241
|
+
function publicRevocation(record, replayed) {
|
|
242
|
+
return Object.freeze({ ...record, replayed });
|
|
243
|
+
}
|
|
244
|
+
|
|
155
245
|
export function getSessionHostLeaseRoot() {
|
|
156
246
|
return path.join(getStatePath(), "session-host-leases");
|
|
157
247
|
}
|
|
@@ -177,6 +267,7 @@ export class SessionHostLeaseAuthority {
|
|
|
177
267
|
this._clearInterval = clearIntervalFn;
|
|
178
268
|
this._lock = lock;
|
|
179
269
|
this._local = new Map();
|
|
270
|
+
this._fenced = new Map();
|
|
180
271
|
}
|
|
181
272
|
|
|
182
273
|
pathFor(sessionId) {
|
|
@@ -201,16 +292,20 @@ export class SessionHostLeaseAuthority {
|
|
|
201
292
|
_unavailable(sessionId, filePath, cause) {
|
|
202
293
|
if (
|
|
203
294
|
cause?.code === SESSION_HOST_LEASE_HELD_CODE ||
|
|
204
|
-
cause?.code === SESSION_HOST_LEASE_FENCED_CODE
|
|
295
|
+
cause?.code === SESSION_HOST_LEASE_FENCED_CODE ||
|
|
296
|
+
cause?.code === SESSION_HOST_LEASE_UNAVAILABLE_CODE ||
|
|
297
|
+
cause?.code === SESSION_HOST_REVOCATION_CONFLICT_CODE
|
|
205
298
|
) {
|
|
206
299
|
return cause;
|
|
207
300
|
}
|
|
208
|
-
|
|
301
|
+
const error = leaseError(
|
|
209
302
|
SESSION_HOST_LEASE_UNAVAILABLE_CODE,
|
|
210
303
|
`Session host lease authority is unavailable for ${sessionId}`,
|
|
211
304
|
{ sessionId, filePath },
|
|
212
305
|
cause,
|
|
213
306
|
);
|
|
307
|
+
if (cause?.commitState) error.commitState = cause.commitState;
|
|
308
|
+
return error;
|
|
214
309
|
}
|
|
215
310
|
|
|
216
311
|
_lose(record, cause) {
|
|
@@ -223,9 +318,18 @@ export class SessionHostLeaseAuthority {
|
|
|
223
318
|
cause?.code === SESSION_HOST_LEASE_FENCED_CODE
|
|
224
319
|
? cause
|
|
225
320
|
: this._unavailable(record.sessionId, record.filePath, cause);
|
|
321
|
+
this._fenced.set(record.key, reason);
|
|
226
322
|
if (!record.controller.signal.aborted) record.controller.abort(reason);
|
|
227
323
|
}
|
|
228
324
|
|
|
325
|
+
_writeSnapshot(draft, snapshot) {
|
|
326
|
+
draft.version = STORE_VERSION;
|
|
327
|
+
draft.sessionId = snapshot.sessionId;
|
|
328
|
+
draft.lastFencingToken = snapshot.lastFencingToken;
|
|
329
|
+
draft.revocationEpoch = snapshot.revocationEpoch;
|
|
330
|
+
draft.revocations = snapshot.revocations.map((record) => ({ ...record }));
|
|
331
|
+
}
|
|
332
|
+
|
|
229
333
|
acquire(
|
|
230
334
|
sessionId,
|
|
231
335
|
{
|
|
@@ -243,6 +347,8 @@ export class SessionHostLeaseAuthority {
|
|
|
243
347
|
);
|
|
244
348
|
const filePath = this.pathFor(id);
|
|
245
349
|
const key = this._key(id);
|
|
350
|
+
const priorFence = this._fenced.get(key);
|
|
351
|
+
if (priorFence) throw priorFence;
|
|
246
352
|
if (this._local.has(key)) {
|
|
247
353
|
throw leaseError(
|
|
248
354
|
SESSION_HOST_LEASE_HELD_CODE,
|
|
@@ -282,6 +388,7 @@ export class SessionHostLeaseAuthority {
|
|
|
282
388
|
const next = {
|
|
283
389
|
leaseId: this._createLeaseId(),
|
|
284
390
|
fencingToken,
|
|
391
|
+
revocationEpoch: snapshot.revocationEpoch,
|
|
285
392
|
ownerPid: process.pid,
|
|
286
393
|
hostKind: kind,
|
|
287
394
|
acquiredAtMs: now,
|
|
@@ -289,8 +396,7 @@ export class SessionHostLeaseAuthority {
|
|
|
289
396
|
expiresAtMs: now + ttl,
|
|
290
397
|
};
|
|
291
398
|
normalizeActiveLease(next);
|
|
292
|
-
draft
|
|
293
|
-
draft.sessionId = id;
|
|
399
|
+
this._writeSnapshot(draft, snapshot);
|
|
294
400
|
draft.lastFencingToken = fencingToken;
|
|
295
401
|
draft.active = next;
|
|
296
402
|
return next;
|
|
@@ -329,7 +435,10 @@ export class SessionHostLeaseAuthority {
|
|
|
329
435
|
"Session host lease authority",
|
|
330
436
|
(draft) => {
|
|
331
437
|
const snapshot = validateStore(draft, id);
|
|
332
|
-
if (
|
|
438
|
+
if (
|
|
439
|
+
snapshot.revocationEpoch !== record.active.revocationEpoch ||
|
|
440
|
+
!sameLease(snapshot.active, record.active)
|
|
441
|
+
) {
|
|
333
442
|
throw leaseError(
|
|
334
443
|
SESSION_HOST_LEASE_FENCED_CODE,
|
|
335
444
|
`Session host lease was superseded for ${id}`,
|
|
@@ -337,6 +446,8 @@ export class SessionHostLeaseAuthority {
|
|
|
337
446
|
sessionId: id,
|
|
338
447
|
fencingToken: record.active.fencingToken,
|
|
339
448
|
activeFencingToken: snapshot.active?.fencingToken ?? null,
|
|
449
|
+
localRevocationEpoch: record.active.revocationEpoch,
|
|
450
|
+
activeRevocationEpoch: snapshot.revocationEpoch,
|
|
340
451
|
},
|
|
341
452
|
);
|
|
342
453
|
}
|
|
@@ -353,6 +464,7 @@ export class SessionHostLeaseAuthority {
|
|
|
353
464
|
renewedAtMs: now,
|
|
354
465
|
expiresAtMs: now + ttl,
|
|
355
466
|
};
|
|
467
|
+
this._writeSnapshot(draft, snapshot);
|
|
356
468
|
draft.active = next;
|
|
357
469
|
return next;
|
|
358
470
|
},
|
|
@@ -371,19 +483,27 @@ export class SessionHostLeaseAuthority {
|
|
|
371
483
|
if (record.released) return false;
|
|
372
484
|
if (record.timer) this._clearInterval(record.timer);
|
|
373
485
|
record.timer = null;
|
|
374
|
-
let
|
|
486
|
+
let outcome = Object.freeze({ released: false, fenced: true });
|
|
375
487
|
try {
|
|
376
|
-
|
|
488
|
+
outcome = mutateSecurityStore(
|
|
377
489
|
filePath,
|
|
378
490
|
"Session host lease authority",
|
|
379
491
|
(draft) => {
|
|
380
492
|
const snapshot = validateStore(draft, id);
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
493
|
+
if (
|
|
494
|
+
snapshot.revocationEpoch !== record.active.revocationEpoch ||
|
|
495
|
+
!sameLease(snapshot.active, record.active)
|
|
496
|
+
) {
|
|
497
|
+
return Object.freeze({
|
|
498
|
+
released: false,
|
|
499
|
+
fenced: true,
|
|
500
|
+
activeFencingToken: snapshot.active?.fencingToken ?? null,
|
|
501
|
+
activeRevocationEpoch: snapshot.revocationEpoch,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
this._writeSnapshot(draft, snapshot);
|
|
385
505
|
draft.active = null;
|
|
386
|
-
return true;
|
|
506
|
+
return Object.freeze({ released: true, fenced: false });
|
|
387
507
|
},
|
|
388
508
|
{ lock: this._lock, timeoutMs: 30_000, staleMs: 30_000 },
|
|
389
509
|
);
|
|
@@ -392,9 +512,26 @@ export class SessionHostLeaseAuthority {
|
|
|
392
512
|
this._lose(record, error);
|
|
393
513
|
throw error;
|
|
394
514
|
}
|
|
515
|
+
if (!outcome.released) {
|
|
516
|
+
this._lose(
|
|
517
|
+
record,
|
|
518
|
+
leaseError(
|
|
519
|
+
SESSION_HOST_LEASE_FENCED_CODE,
|
|
520
|
+
`Session host lease cannot be released after losing authority for ${id}`,
|
|
521
|
+
{
|
|
522
|
+
sessionId: id,
|
|
523
|
+
fencingToken: record.active.fencingToken,
|
|
524
|
+
activeFencingToken: outcome.activeFencingToken ?? null,
|
|
525
|
+
localRevocationEpoch: record.active.revocationEpoch,
|
|
526
|
+
activeRevocationEpoch: outcome.activeRevocationEpoch ?? null,
|
|
527
|
+
},
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
395
532
|
record.released = true;
|
|
396
533
|
this._local.delete(key);
|
|
397
|
-
return
|
|
534
|
+
return true;
|
|
398
535
|
};
|
|
399
536
|
|
|
400
537
|
record.timer = this._setInterval(() => {
|
|
@@ -406,54 +543,281 @@ export class SessionHostLeaseAuthority {
|
|
|
406
543
|
}, heartbeat);
|
|
407
544
|
record.timer?.unref?.();
|
|
408
545
|
|
|
546
|
+
const admitMcpDispatch = (_metadata, dispatch) => {
|
|
547
|
+
if (typeof dispatch !== "function") {
|
|
548
|
+
throw new TypeError("session host MCP dispatch callback is required");
|
|
549
|
+
}
|
|
550
|
+
return this.withWriteAuthority(id, (authority) => {
|
|
551
|
+
// A lease-owned dispatch must never inherit the legacy no-lease write
|
|
552
|
+
// authority represented by null. Invoke the actual transport send
|
|
553
|
+
// while the authority lock is held, then return its response promise
|
|
554
|
+
// without awaiting it so the lock covers dispatch only.
|
|
555
|
+
if (!authority) {
|
|
556
|
+
throw leaseError(
|
|
557
|
+
SESSION_HOST_LEASE_FENCED_CODE,
|
|
558
|
+
`Session host MCP dispatch is fenced for ${id}`,
|
|
559
|
+
{ sessionId: id, fencingToken: record.active.fencingToken },
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return dispatch();
|
|
563
|
+
});
|
|
564
|
+
};
|
|
565
|
+
|
|
409
566
|
return Object.freeze({
|
|
410
567
|
sessionId: id,
|
|
411
568
|
leaseId: record.active.leaseId,
|
|
412
569
|
fencingToken: record.active.fencingToken,
|
|
570
|
+
revocationEpoch: record.active.revocationEpoch,
|
|
413
571
|
signal: controller.signal,
|
|
414
572
|
renew,
|
|
415
573
|
assert: () => this.withWriteAuthority(id, (authority) => authority),
|
|
574
|
+
admitMcpDispatch,
|
|
416
575
|
release,
|
|
417
576
|
});
|
|
418
577
|
}
|
|
419
578
|
|
|
579
|
+
readAuthority(sessionId) {
|
|
580
|
+
const id = canonicalSessionId(sessionId);
|
|
581
|
+
const filePath = this.pathFor(id);
|
|
582
|
+
if (!fs.existsSync(filePath)) {
|
|
583
|
+
return Object.freeze({
|
|
584
|
+
version: STORE_VERSION,
|
|
585
|
+
sessionId: id,
|
|
586
|
+
lastFencingToken: 0,
|
|
587
|
+
revocationEpoch: 0,
|
|
588
|
+
revocationCount: 0,
|
|
589
|
+
latestRevocation: null,
|
|
590
|
+
active: null,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
return this._lock(
|
|
595
|
+
filePath,
|
|
596
|
+
() => {
|
|
597
|
+
const snapshot = this._read(filePath, id);
|
|
598
|
+
return Object.freeze({
|
|
599
|
+
version: STORE_VERSION,
|
|
600
|
+
sessionId: id,
|
|
601
|
+
lastFencingToken: snapshot.lastFencingToken,
|
|
602
|
+
revocationEpoch: snapshot.revocationEpoch,
|
|
603
|
+
revocationCount: snapshot.revocations.length,
|
|
604
|
+
latestRevocation:
|
|
605
|
+
snapshot.revocations.length > 0
|
|
606
|
+
? publicRevocation(snapshot.revocations.at(-1), false)
|
|
607
|
+
: null,
|
|
608
|
+
active: snapshot.active ? publicLease(snapshot.active) : null,
|
|
609
|
+
});
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
timeoutMs: 30_000,
|
|
613
|
+
staleMs: 30_000,
|
|
614
|
+
retryMs: 1,
|
|
615
|
+
maxRetryMs: 8,
|
|
616
|
+
retryJitterMs: 4,
|
|
617
|
+
failIfUnavailable: true,
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
} catch (cause) {
|
|
621
|
+
throw this._unavailable(id, filePath, cause);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Durably revoke the current host generation. The request id is a permanent
|
|
627
|
+
* idempotency key: an exact replay returns the original record and never
|
|
628
|
+
* revokes a successor. The bounded journal fails closed at capacity rather
|
|
629
|
+
* than evicting replay authority.
|
|
630
|
+
*/
|
|
631
|
+
revoke(sessionId, { requestId, reasonCode = "operator" } = {}) {
|
|
632
|
+
const id = canonicalSessionId(sessionId);
|
|
633
|
+
const canonicalRequestId = canonicalRevocationRequestId(requestId);
|
|
634
|
+
const canonicalReasonCode = canonicalRevocationReason(reasonCode);
|
|
635
|
+
const filePath = this.pathFor(id);
|
|
636
|
+
const key = this._key(id);
|
|
637
|
+
let result;
|
|
638
|
+
try {
|
|
639
|
+
result = mutateSecurityStore(
|
|
640
|
+
filePath,
|
|
641
|
+
"Session host lease authority",
|
|
642
|
+
(draft) => {
|
|
643
|
+
const snapshot = validateStore(draft, id, {
|
|
644
|
+
missing: !fs.existsSync(filePath),
|
|
645
|
+
});
|
|
646
|
+
const replay = snapshot.revocations.find(
|
|
647
|
+
(record) => record.requestId === canonicalRequestId,
|
|
648
|
+
);
|
|
649
|
+
if (replay) {
|
|
650
|
+
if (replay.reasonCode !== canonicalReasonCode) {
|
|
651
|
+
throw leaseError(
|
|
652
|
+
SESSION_HOST_REVOCATION_CONFLICT_CODE,
|
|
653
|
+
`Session host revocation requestId was reused with different authority: ${canonicalRequestId}`,
|
|
654
|
+
{ sessionId: id, requestId: canonicalRequestId },
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return publicRevocation(replay, true);
|
|
658
|
+
}
|
|
659
|
+
if (snapshot.revocations.length >= MAX_REVOCATIONS) {
|
|
660
|
+
throw new RangeError(
|
|
661
|
+
"session host revocation journal is full and requires explicit operator repair",
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const revocationEpoch = snapshot.revocationEpoch + 1;
|
|
665
|
+
if (!Number.isSafeInteger(revocationEpoch)) {
|
|
666
|
+
throw new RangeError("session host revocation epoch is exhausted");
|
|
667
|
+
}
|
|
668
|
+
const active = snapshot.active;
|
|
669
|
+
const record = normalizeRevocation(
|
|
670
|
+
{
|
|
671
|
+
requestId: canonicalRequestId,
|
|
672
|
+
revocationEpoch,
|
|
673
|
+
reasonCode: canonicalReasonCode,
|
|
674
|
+
revokedAtMs: Math.trunc(this._now()),
|
|
675
|
+
targetLeaseId: active?.leaseId ?? null,
|
|
676
|
+
targetFencingToken: active?.fencingToken ?? null,
|
|
677
|
+
targetOwnerPid: active?.ownerPid ?? null,
|
|
678
|
+
},
|
|
679
|
+
revocationEpoch,
|
|
680
|
+
);
|
|
681
|
+
this._writeSnapshot(draft, snapshot);
|
|
682
|
+
draft.revocationEpoch = revocationEpoch;
|
|
683
|
+
draft.revocations = [
|
|
684
|
+
...snapshot.revocations.map((entry) => ({ ...entry })),
|
|
685
|
+
{ ...record },
|
|
686
|
+
];
|
|
687
|
+
draft.active = null;
|
|
688
|
+
return publicRevocation(record, false);
|
|
689
|
+
},
|
|
690
|
+
{ lock: this._lock, timeoutMs: 30_000, staleMs: 30_000 },
|
|
691
|
+
);
|
|
692
|
+
} catch (cause) {
|
|
693
|
+
const error = this._unavailable(id, filePath, cause);
|
|
694
|
+
if (["committed", "unknown"].includes(error.commitState)) {
|
|
695
|
+
try {
|
|
696
|
+
const latest = this.readAuthority(id).latestRevocation;
|
|
697
|
+
if (
|
|
698
|
+
latest?.requestId === canonicalRequestId &&
|
|
699
|
+
latest?.reasonCode === canonicalReasonCode
|
|
700
|
+
) {
|
|
701
|
+
result = publicRevocation(latest, true);
|
|
702
|
+
}
|
|
703
|
+
} catch {
|
|
704
|
+
// The original classified write failure remains the authority when
|
|
705
|
+
// exact readback cannot prove this request's durable record.
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!result) {
|
|
709
|
+
const local = this._local.get(key);
|
|
710
|
+
if (local) this._lose(local, error);
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const local = this._local.get(key);
|
|
716
|
+
if (
|
|
717
|
+
local &&
|
|
718
|
+
local.active.revocationEpoch < result.revocationEpoch &&
|
|
719
|
+
(!result.targetLeaseId || result.targetLeaseId === local.active.leaseId)
|
|
720
|
+
) {
|
|
721
|
+
this._lose(
|
|
722
|
+
local,
|
|
723
|
+
leaseError(
|
|
724
|
+
SESSION_HOST_LEASE_FENCED_CODE,
|
|
725
|
+
`Session host authority was durably revoked for ${id}`,
|
|
726
|
+
{
|
|
727
|
+
sessionId: id,
|
|
728
|
+
requestId: result.requestId,
|
|
729
|
+
fencingToken: local.active.fencingToken,
|
|
730
|
+
localRevocationEpoch: local.active.revocationEpoch,
|
|
731
|
+
activeRevocationEpoch: result.revocationEpoch,
|
|
732
|
+
revoked: true,
|
|
733
|
+
},
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
|
|
420
740
|
withWriteAuthority(sessionId, task) {
|
|
421
741
|
if (typeof task !== "function") {
|
|
422
742
|
throw new TypeError("session host write authority callback is required");
|
|
423
743
|
}
|
|
424
744
|
const id = canonicalSessionId(sessionId);
|
|
425
745
|
const filePath = this.pathFor(id);
|
|
426
|
-
|
|
746
|
+
const key = this._key(id);
|
|
747
|
+
let callbackError = null;
|
|
748
|
+
const invokeTask = (authority) => {
|
|
749
|
+
try {
|
|
750
|
+
return task(authority);
|
|
751
|
+
} catch (cause) {
|
|
752
|
+
callbackError = cause;
|
|
753
|
+
throw cause;
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
const priorFence = this._fenced.get(key);
|
|
757
|
+
if (priorFence) throw priorFence;
|
|
758
|
+
const local = this._local.get(key);
|
|
759
|
+
if (!fs.existsSync(filePath)) {
|
|
760
|
+
if (!local) return invokeTask(null);
|
|
761
|
+
const error = leaseError(
|
|
762
|
+
SESSION_HOST_LEASE_FENCED_CODE,
|
|
763
|
+
`Session host authority disappeared while locally held: ${id}`,
|
|
764
|
+
{
|
|
765
|
+
sessionId: id,
|
|
766
|
+
localFencingToken: local.active.fencingToken,
|
|
767
|
+
localRevocationEpoch: local.active.revocationEpoch,
|
|
768
|
+
},
|
|
769
|
+
);
|
|
770
|
+
this._lose(local, error);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
427
773
|
try {
|
|
428
774
|
return this._lock(
|
|
429
775
|
filePath,
|
|
430
776
|
() => {
|
|
431
777
|
const snapshot = this._read(filePath, id);
|
|
432
|
-
|
|
433
|
-
const record = this._local.get(this._key(id));
|
|
778
|
+
const record = this._local.get(key);
|
|
434
779
|
const now = Math.trunc(this._now());
|
|
435
780
|
if (
|
|
436
|
-
|
|
437
|
-
record.released ||
|
|
438
|
-
|
|
439
|
-
|
|
781
|
+
record &&
|
|
782
|
+
(record.released ||
|
|
783
|
+
record.active.revocationEpoch !== snapshot.revocationEpoch ||
|
|
784
|
+
!sameLease(snapshot.active, record.active) ||
|
|
785
|
+
snapshot.active.expiresAtMs <= now)
|
|
440
786
|
) {
|
|
441
787
|
const error = leaseError(
|
|
442
788
|
SESSION_HOST_LEASE_FENCED_CODE,
|
|
443
|
-
`Session write is fenced by
|
|
789
|
+
`Session write is fenced by a revocation, successor, or expired host lease: ${id}`,
|
|
444
790
|
{
|
|
445
791
|
sessionId: id,
|
|
446
|
-
activeFencingToken: snapshot.active
|
|
447
|
-
localFencingToken: record
|
|
792
|
+
activeFencingToken: snapshot.active?.fencingToken ?? null,
|
|
793
|
+
localFencingToken: record.active.fencingToken,
|
|
794
|
+
localRevocationEpoch: record.active.revocationEpoch,
|
|
795
|
+
activeRevocationEpoch: snapshot.revocationEpoch,
|
|
796
|
+
revoked:
|
|
797
|
+
record.active.revocationEpoch !== snapshot.revocationEpoch,
|
|
448
798
|
},
|
|
449
799
|
);
|
|
450
|
-
|
|
800
|
+
this._lose(record, error);
|
|
451
801
|
throw error;
|
|
452
802
|
}
|
|
453
|
-
|
|
803
|
+
if (!record && snapshot.active) {
|
|
804
|
+
throw leaseError(
|
|
805
|
+
SESSION_HOST_LEASE_FENCED_CODE,
|
|
806
|
+
`Session write is fenced by another host lease: ${id}`,
|
|
807
|
+
{
|
|
808
|
+
sessionId: id,
|
|
809
|
+
activeFencingToken: snapshot.active.fencingToken,
|
|
810
|
+
localFencingToken: null,
|
|
811
|
+
activeRevocationEpoch: snapshot.revocationEpoch,
|
|
812
|
+
},
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (!record) return invokeTask(null);
|
|
816
|
+
return invokeTask(
|
|
454
817
|
Object.freeze({
|
|
455
818
|
leaseId: snapshot.active.leaseId,
|
|
456
819
|
fencingToken: snapshot.active.fencingToken,
|
|
820
|
+
revocationEpoch: snapshot.active.revocationEpoch,
|
|
457
821
|
ownerPid: snapshot.active.ownerPid,
|
|
458
822
|
hostKind: snapshot.active.hostKind,
|
|
459
823
|
}),
|
|
@@ -469,6 +833,7 @@ export class SessionHostLeaseAuthority {
|
|
|
469
833
|
},
|
|
470
834
|
);
|
|
471
835
|
} catch (cause) {
|
|
836
|
+
if (cause === callbackError) throw cause;
|
|
472
837
|
throw this._unavailable(id, filePath, cause);
|
|
473
838
|
}
|
|
474
839
|
}
|
|
@@ -490,3 +855,11 @@ export function assertSessionHostWriteAuthority(sessionId) {
|
|
|
490
855
|
(authority) => authority,
|
|
491
856
|
);
|
|
492
857
|
}
|
|
858
|
+
|
|
859
|
+
export function readSessionHostAuthority(sessionId) {
|
|
860
|
+
return defaultAuthority.readAuthority(sessionId);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function revokeSessionHostAuthority(sessionId, options) {
|
|
864
|
+
return defaultAuthority.revoke(sessionId, options);
|
|
865
|
+
}
|
|
@@ -2,7 +2,10 @@ export const SESSION_PERSISTENCE_FAILURE_SCHEMA =
|
|
|
2
2
|
"chainlesschain.session-persistence-failure.v1";
|
|
3
3
|
export const SESSION_PERSISTENCE_FAILURE_CODE = "CC_SESSION_PERSISTENCE_FAILED";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// EIO is a common fsync/FlushFileBuffers failure. Once the append bytes have
|
|
6
|
+
// been issued its outcome is unknown, so it belongs in the same bounded,
|
|
7
|
+
// content-free persistence error domain as ENOSPC/EROFS.
|
|
8
|
+
const STORAGE_FAILURE_CODES = new Set(["ENOSPC", "EROFS", "EIO"]);
|
|
6
9
|
const COMMIT_STATES = new Set(["not-committed", "unknown", "committed"]);
|
|
7
10
|
const OPERATIONS = new Set([
|
|
8
11
|
"append-event",
|