chainlesschain 0.162.188 → 0.162.193
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 +61 -11
- package/bin/chainlesschain.js +7 -52
- package/completions/_cc +209 -0
- package/completions/cc.bash +18 -0
- package/completions/cc.fish +13 -0
- package/completions/cc.ps1 +226 -0
- package/package.json +10 -2
- package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
- package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
- package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
- package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
- package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
- package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
- package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
- package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
- package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
- package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
- package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
- package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
- package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
- package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
- package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
- package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
- package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
- package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/cli-help.js +181 -0
- package/src/command-help-index.json +181 -0
- package/src/command-manifest.json +1107 -175
- package/src/command-surface-policy.js +197 -0
- package/src/commands/agent.js +98 -33
- package/src/commands/artifacts.js +400 -2
- package/src/commands/background-session.js +41 -3
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint-restore-recovery.js +1476 -0
- package/src/commands/checkpoint.js +950 -22
- package/src/commands/compact.js +101 -25
- package/src/commands/config.js +506 -20
- package/src/commands/doctor.js +4 -1
- package/src/commands/hub.js +1 -1
- package/src/commands/mcp.js +688 -24
- package/src/commands/pack.js +16 -2
- package/src/commands/plugin.js +20 -12
- package/src/commands/session-mcp-recovery.js +218 -0
- package/src/commands/session.js +470 -110
- package/src/commands/setup.js +36 -10
- package/src/commands/skill.js +181 -110
- package/src/commands/status.js +21 -5
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/commands/update.js +68 -6
- package/src/constants.js +11 -0
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/action-protocol.js +10 -0
- package/src/gateways/ws/session-protocol.js +555 -27
- package/src/gateways/ws/ws-agent-handler.js +541 -14
- package/src/gateways/ws/ws-session-gateway.js +129 -22
- package/src/gateways/ws/ws-session-state.js +431 -0
- package/src/harness/background-task-manager.js +233 -36
- package/src/harness/jsonl-session-store.js +2551 -167
- package/src/harness/mcp-client.js +802 -61
- package/src/harness/prompt-compressor.js +227 -27
- package/src/harness/session-index.js +89 -38
- package/src/harness/session-list-index.js +254 -0
- package/src/harness/structured-handoff.js +410 -0
- package/src/lazy-dispatch.js +747 -84
- package/src/lib/__tests__/logger.test.js +50 -2
- package/src/lib/advisor-runtime.js +1047 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +120 -26
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +543 -24
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/artifact-store.js +131 -8
- package/src/lib/background-agent-supervisor.js +185 -13
- package/src/lib/background-interaction-journal.js +37 -9
- package/src/lib/background-launch-profile.js +1058 -0
- package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
- package/src/lib/checkpoint-restore-orchestrator.js +987 -0
- package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
- package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
- package/src/lib/checkpoint-restore-recovery.js +884 -0
- package/src/lib/checkpoint-restore-retention.js +126 -0
- package/src/lib/checkpoint-restore-saga.js +5079 -0
- package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
- package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
- package/src/lib/checkpoint-store.js +1688 -97
- package/src/lib/checkpoint-timeline-authority.js +522 -0
- package/src/lib/checkpoint-timeline.js +642 -0
- package/src/lib/config-keys.js +18 -119
- package/src/lib/config-manager.js +446 -27
- package/src/lib/config-redaction.js +57 -0
- package/src/lib/config-schema.js +853 -0
- package/src/lib/cowork-mcp-tools.js +5 -0
- package/src/lib/cowork-task-runner.js +458 -63
- package/src/lib/delivery-coordinator.js +1234 -0
- package/src/lib/delivery-evidence.js +450 -0
- package/src/lib/doctor-checkup.js +199 -14
- package/src/lib/executable-path.js +34 -0
- package/src/lib/fatal-handler.js +16 -0
- package/src/lib/file-checkpoint.js +4909 -186
- package/src/lib/file-lines.js +125 -0
- package/src/lib/headless-config-command.js +16 -5
- package/src/lib/hook-runner.cjs +139 -38
- package/src/lib/hooks-v2-workspace-context.js +10 -0
- package/src/lib/impacted-gate-selector.js +424 -0
- package/src/lib/interaction-adapter.js +62 -0
- package/src/lib/jsonl-session-store.js +1 -35
- package/src/lib/logger.js +66 -14
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/mcp-call-ledger-store.js +1069 -0
- package/src/lib/mcp-call-ledger.js +747 -0
- package/src/lib/mcp-client.js +5 -0
- package/src/lib/mcp-conflict-scheduler.js +468 -0
- package/src/lib/mcp-effect-contract.js +102 -0
- package/src/lib/mcp-headers-helper-trust.js +145 -0
- package/src/lib/mcp-headers-helper.js +616 -0
- package/src/lib/mcp-host-recovery-runtime.js +131 -0
- package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
- package/src/lib/mcp-recovery-adjudication.js +443 -0
- package/src/lib/output-context.js +86 -0
- package/src/lib/packer/file-identity.js +170 -0
- package/src/lib/packer/index.js +3 -0
- package/src/lib/packer/native-update-state.js +277 -0
- package/src/lib/packer/pack-update-applier.js +1983 -136
- package/src/lib/packer/pack-update-checker.js +34 -0
- package/src/lib/packer/pack-update-downloader.js +875 -86
- package/src/lib/packer/pack-update-signature.js +86 -0
- package/src/lib/packer/pkg-config-generator.js +6 -0
- package/src/lib/paths.js +65 -10
- package/src/lib/plan-mode.js +831 -14
- package/src/lib/plan-persistence.js +540 -0
- package/src/lib/plugin-runtime/hooks.js +227 -69
- package/src/lib/plugin-runtime/plugin-options.js +189 -44
- package/src/lib/pr-automation-policy.js +146 -8
- package/src/lib/process-execution-broker/credential-agent.js +7 -0
- package/src/lib/process-execution-broker/index.js +491 -13
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
- package/src/lib/project-instructions.js +165 -29
- package/src/lib/project-mcp-trust.js +67 -0
- package/src/lib/public-ide-capability-manifest.js +272 -0
- package/src/lib/public-ide-capability-validation.js +351 -0
- package/src/lib/secret-store.js +267 -43
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/secure-fs.js +1089 -0
- package/src/lib/security-audit.js +58 -0
- package/src/lib/service-manager.js +34 -13
- package/src/lib/session-budget-runtime.js +1452 -0
- package/src/lib/session-host-snapshot.js +388 -0
- package/src/lib/session-message-provenance.js +429 -0
- package/src/lib/session-projection.js +804 -0
- package/src/lib/session-resource-budget.js +1495 -0
- package/src/lib/settings-hooks.cjs +138 -22
- package/src/lib/settings-loader.cjs +25 -2
- package/src/lib/side-effect-ledger-store.js +45 -23
- package/src/lib/skill-execution-identity.js +217 -0
- package/src/lib/skill-loader.js +299 -18
- package/src/lib/sub-agent-context.js +236 -28
- package/src/lib/sub-agent-registry.js +5 -0
- package/src/lib/subagent-contract.js +94 -0
- package/src/lib/todo-manager.js +185 -18
- package/src/lib/todo-persistence.js +417 -0
- package/src/lib/turn-binding-store.js +41 -23
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +165 -24
- package/src/lib/ws-chat-handler.js +21 -0
- package/src/program-base.js +18 -0
- package/src/repl/advisor-command.js +103 -0
- package/src/repl/agent-repl.js +2175 -146
- package/src/repl/btw-command.js +93 -10
- package/src/repl/clipboard-image.js +119 -0
- package/src/repl/config-summary.js +64 -108
- package/src/repl/prompt-editor.js +233 -0
- package/src/repl/prompt-interactions.js +344 -0
- package/src/repl/prompt-stash.js +354 -0
- package/src/repl/prompt-suggestions.js +312 -0
- package/src/repl/repl-keybindings.js +227 -0
- package/src/repl/session-recap.js +217 -0
- package/src/repl/slash-command-registry.js +22 -0
- package/src/repl/terminal-layout.js +88 -0
- package/src/runtime/agent-core.js +1630 -447
- package/src/runtime/coding-agent-contract-shared.cjs +13 -1
- package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
- package/src/runtime/diagnostics.js +32 -13
- package/src/runtime/headless-runner.js +310 -118
- package/src/runtime/headless-stream.js +356 -95
- package/src/runtime/mcp-config.js +148 -14
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/runtime/status-diagnostics-lite.js +137 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* artifact next to the old one (conventionally `<exePath>.new`).
|
|
2
|
+
* Stream a packed executable to a unique sibling file, verify its SHA-256,
|
|
3
|
+
* and atomically commit it to `outputPath`.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* ReadableStream, which we consume via its async iterator.
|
|
10
|
-
* - Atomicity: we write to `<outputPath>.partial` and rename on success.
|
|
11
|
-
* A SHA mismatch or mid-stream failure deletes the partial so a retry
|
|
12
|
-
* starts from scratch.
|
|
13
|
-
* - No resume: first cut skips Range / If-Range. Large-artifact resume
|
|
14
|
-
* is a Phase 5d concern if it becomes painful.
|
|
5
|
+
* The active output is never unlinked before the rename. Concurrent writers
|
|
6
|
+
* are serialized by an exclusive sibling lock, and a crashed writer's lock
|
|
7
|
+
* is deliberately left fail-closed for an operator to inspect.
|
|
15
8
|
*/
|
|
16
9
|
|
|
17
10
|
import fs from "node:fs";
|
|
18
11
|
import path from "node:path";
|
|
19
12
|
import crypto from "node:crypto";
|
|
13
|
+
import {
|
|
14
|
+
hasPreciseFileIdentity,
|
|
15
|
+
pathMatchesOpenedFileIdentitySync,
|
|
16
|
+
sameOpenedFileIdentity,
|
|
17
|
+
} from "./file-identity.js";
|
|
18
|
+
import {
|
|
19
|
+
assertSafeRegularFile as assertStateSafeRegularFile,
|
|
20
|
+
assertSafePathAncestors,
|
|
21
|
+
} from "./native-update-state.js";
|
|
20
22
|
|
|
21
23
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* @param {object} ctx
|
|
25
|
-
* @param {string}
|
|
26
|
-
* @param {string}
|
|
27
|
-
* @param {string}
|
|
28
|
-
* @param {typeof fetch} [ctx.fetchImpl]
|
|
27
|
+
* @param {string} ctx.url
|
|
28
|
+
* @param {string} ctx.sha256
|
|
29
|
+
* @param {string} ctx.outputPath
|
|
30
|
+
* @param {typeof fetch} [ctx.fetchImpl]
|
|
29
31
|
* @param {(p:{bytes:number,total:number|null})=>void} [ctx.onProgress]
|
|
30
|
-
*
|
|
31
|
-
* is null if Content-Length was
|
|
32
|
-
* absent or unparseable
|
|
32
|
+
* @param {number} [ctx.timeoutMs]
|
|
33
33
|
* @returns {Promise<{outputPath:string,bytes:number,sha256:string}>}
|
|
34
34
|
*/
|
|
35
35
|
export async function downloadAndVerify(ctx) {
|
|
36
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
url,
|
|
38
|
+
sha256,
|
|
39
|
+
outputPath,
|
|
40
|
+
fetchImpl = fetch,
|
|
41
|
+
onProgress,
|
|
42
|
+
timeoutMs = 300_000,
|
|
43
|
+
} = ctx;
|
|
37
44
|
|
|
38
45
|
if (!url || typeof url !== "string") {
|
|
39
46
|
throw new DownloadError("url is required", "NO_URL");
|
|
@@ -47,112 +54,894 @@ export async function downloadAndVerify(ctx) {
|
|
|
47
54
|
if (!outputPath || typeof outputPath !== "string") {
|
|
48
55
|
throw new DownloadError("outputPath is required", "NO_OUTPUT");
|
|
49
56
|
}
|
|
57
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
58
|
+
throw new DownloadError(
|
|
59
|
+
"timeoutMs must be a positive safe integer",
|
|
60
|
+
"BAD_TIMEOUT",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const abortController = new AbortController();
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
50
66
|
|
|
67
|
+
assertSafeDownloadPath(outputPath, "output path", true);
|
|
51
68
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
52
|
-
|
|
69
|
+
assertSafeDownloadPath(outputPath, "output path", true);
|
|
70
|
+
assertSafeFileOrMissing(outputPath, "output path");
|
|
71
|
+
|
|
72
|
+
const lockPath = `${outputPath}.lock`;
|
|
73
|
+
const lock = acquireLock(lockPath);
|
|
74
|
+
const partialPath = `${outputPath}.partial-${process.pid}-${crypto.randomUUID()}`;
|
|
75
|
+
let partialFd = null;
|
|
76
|
+
let partialIdentity = null;
|
|
77
|
+
let committedPath = false;
|
|
78
|
+
let previousOutput = null;
|
|
79
|
+
let retainLockForRecovery = false;
|
|
80
|
+
const assertOwnedDownloadLock = () => {
|
|
81
|
+
try {
|
|
82
|
+
assertDownloadLockOwned(lock);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error?.code === "DOWNLOAD_LOCK_LOST") {
|
|
85
|
+
// The pathname may already belong to another writer. From this point
|
|
86
|
+
// onward, cleanup may close our old descriptor but must never inspect,
|
|
87
|
+
// restore, delete, or otherwise mutate the current lock pathname.
|
|
88
|
+
retainLockForRecovery = true;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
53
93
|
|
|
54
|
-
// Clean up any stale partial from a prior interrupted attempt. Ignoring
|
|
55
|
-
// ENOENT is fine — we just want to start with a blank slate.
|
|
56
94
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
95
|
+
// Re-check the complete ancestor chain after lock acquisition. This
|
|
96
|
+
// closes the practical junction/symlink replacement window between the
|
|
97
|
+
// initial validation and opening the same-directory partial file.
|
|
98
|
+
assertSafeDownloadPath(outputPath, "output path", true);
|
|
99
|
+
assertSafeFileOrMissing(outputPath, "output path");
|
|
100
|
+
assertSafeDownloadPath(partialPath, "partial download path", true);
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await awaitBeforeDeadline(
|
|
104
|
+
fetchImpl(url, {
|
|
105
|
+
headers: { Accept: "*/*" },
|
|
106
|
+
signal: abortController.signal,
|
|
107
|
+
}),
|
|
108
|
+
{ deadline, abortController, label: "artifact response" },
|
|
109
|
+
);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof DownloadError) throw err;
|
|
112
|
+
throw new DownloadError(
|
|
113
|
+
`fetch failed: ${err.message}`,
|
|
114
|
+
err?.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new DownloadError(
|
|
119
|
+
`artifact fetch failed: HTTP ${response.status}`,
|
|
120
|
+
"FETCH_FAILED",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const totalRaw = response.headers?.get?.("content-length");
|
|
125
|
+
const total = totalRaw ? Number(totalRaw) : null;
|
|
126
|
+
const body = response.body;
|
|
127
|
+
if (!body) {
|
|
128
|
+
throw new DownloadError(
|
|
129
|
+
"response has no body stream (fetch impl returned no body)",
|
|
130
|
+
"NO_BODY",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Network I/O can take arbitrarily long. Revalidate after the response is
|
|
135
|
+
// available and immediately before opening the lexical partial path.
|
|
136
|
+
assertSafeDownloadPath(outputPath, "output path", true);
|
|
137
|
+
assertSafeFileOrMissing(outputPath, "output path");
|
|
138
|
+
assertSafeDownloadPath(partialPath, "partial download path", true);
|
|
139
|
+
|
|
140
|
+
const hasher = crypto.createHash("sha256");
|
|
141
|
+
let bytes = 0;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Keep a read/write descriptor open through the rename. The pathname can
|
|
145
|
+
// be exchanged by another same-user process even while the download lock
|
|
146
|
+
// is held; descriptor/path identity checks on both sides of the commit
|
|
147
|
+
// bind the verified bytes to the file that actually becomes outputPath.
|
|
148
|
+
partialFd = fs.openSync(partialPath, "wx+", 0o600);
|
|
149
|
+
partialIdentity = snapshotDescriptorIdentity(partialFd);
|
|
150
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
151
|
+
while (true) {
|
|
152
|
+
const { value: chunk, done } = await awaitBeforeDeadline(
|
|
153
|
+
iterator.next(),
|
|
154
|
+
{ deadline, abortController, label: "artifact body" },
|
|
155
|
+
);
|
|
156
|
+
if (done) break;
|
|
157
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
158
|
+
hasher.update(buf);
|
|
159
|
+
bytes += buf.length;
|
|
160
|
+
writeAllSync(partialFd, buf);
|
|
161
|
+
if (typeof onProgress === "function") {
|
|
162
|
+
try {
|
|
163
|
+
onProgress({ bytes, total: Number.isFinite(total) ? total : null });
|
|
164
|
+
} catch {
|
|
165
|
+
/* progress callback errors must not interrupt the download */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
fs.fsyncSync(partialFd);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (partialFd !== null) {
|
|
172
|
+
try {
|
|
173
|
+
fs.closeSync(partialFd);
|
|
174
|
+
} catch {
|
|
175
|
+
/* best effort */
|
|
176
|
+
}
|
|
177
|
+
partialFd = null;
|
|
178
|
+
}
|
|
179
|
+
if (err instanceof DownloadError) throw err;
|
|
180
|
+
throw new DownloadError(`stream aborted: ${err.message}`, "STREAM_ERROR");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const actualSha = hasher.digest("hex");
|
|
184
|
+
if (actualSha !== sha256.toLowerCase()) {
|
|
185
|
+
throw new DownloadError(
|
|
186
|
+
`SHA-256 mismatch: expected ${sha256}, got ${actualSha}`,
|
|
187
|
+
"SHA_MISMATCH",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
assertPathMatchesDescriptor(
|
|
192
|
+
partialPath,
|
|
193
|
+
partialFd,
|
|
194
|
+
"verified partial download",
|
|
195
|
+
);
|
|
196
|
+
if (sha256Descriptor(partialFd) !== actualSha) {
|
|
197
|
+
throw new DownloadError(
|
|
198
|
+
"partial download bytes changed after streaming verification",
|
|
199
|
+
"PARTIAL_CHANGED",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Re-check immediately before the commit point. renameSync atomically
|
|
204
|
+
// replaces an existing regular file; there is deliberately no unlink gap.
|
|
205
|
+
assertSafeFileOrMissing(outputPath, "output path");
|
|
206
|
+
assertOwnedDownloadLock();
|
|
207
|
+
previousOutput = snapshotExistingOutput(outputPath);
|
|
208
|
+
try {
|
|
209
|
+
assertPathMatchesDescriptor(
|
|
210
|
+
partialPath,
|
|
211
|
+
partialFd,
|
|
212
|
+
"verified partial download",
|
|
213
|
+
);
|
|
214
|
+
assertOutputMatchesSnapshotForCommit(outputPath, previousOutput);
|
|
215
|
+
// Snapshotting and hashing the previous output can take long enough for
|
|
216
|
+
// lock ownership to change. Keep the final ownership check adjacent to
|
|
217
|
+
// the commit point so a lock already known to be foreign is never
|
|
218
|
+
// followed by an output mutation.
|
|
219
|
+
assertOwnedDownloadLock();
|
|
220
|
+
fs.renameSync(partialPath, outputPath);
|
|
221
|
+
committedPath = true;
|
|
222
|
+
assertOwnedDownloadLock();
|
|
223
|
+
assertPathMatchesDescriptor(outputPath, partialFd, "committed download");
|
|
224
|
+
if (sha256Descriptor(partialFd) !== actualSha) {
|
|
225
|
+
throw new DownloadError(
|
|
226
|
+
"committed download bytes changed before final verification",
|
|
227
|
+
"COMMIT_CHANGED",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
fsyncDirectory(path.dirname(outputPath));
|
|
231
|
+
assertOwnedDownloadLock();
|
|
232
|
+
if (previousOutput) {
|
|
233
|
+
discardPreviousOutputSnapshot(previousOutput);
|
|
234
|
+
previousOutput = null;
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
let recoveryError = null;
|
|
238
|
+
let manualRecoveryError = null;
|
|
239
|
+
if (err instanceof DownloadError && err.code === "DOWNLOAD_LOCK_LOST") {
|
|
240
|
+
if (previousOutput) previousOutput.retained = true;
|
|
241
|
+
retainLockForRecovery = true;
|
|
242
|
+
if (committedPath) {
|
|
243
|
+
throw createOutputRecoveryRequiredError({
|
|
244
|
+
cause: err,
|
|
245
|
+
outputPath,
|
|
246
|
+
snapshot: previousOutput,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
if (!committedPath && err?.retainDownloadLock === true) {
|
|
252
|
+
if (previousOutput) previousOutput.retained = true;
|
|
253
|
+
retainLockForRecovery = true;
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
committedPath &&
|
|
258
|
+
err?.code === "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED" &&
|
|
259
|
+
err?.recoveryArtifactRemoved === true
|
|
260
|
+
) {
|
|
261
|
+
// The verified output is committed and the recovery name is gone in
|
|
262
|
+
// the live namespace, but the unlink could not be made durable. There
|
|
263
|
+
// is no safe rollback source anymore, so preserve the output and lock.
|
|
264
|
+
previousOutput = null;
|
|
265
|
+
retainLockForRecovery = true;
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
if (committedPath) {
|
|
269
|
+
if (previousOutput) {
|
|
270
|
+
try {
|
|
271
|
+
assertOwnedDownloadLock();
|
|
272
|
+
const committedOutput = snapshotOpenFile(
|
|
273
|
+
outputPath,
|
|
274
|
+
partialFd,
|
|
275
|
+
"committed download before recovery",
|
|
276
|
+
);
|
|
277
|
+
fs.closeSync(partialFd);
|
|
278
|
+
partialFd = null;
|
|
279
|
+
restorePreviousOutput({
|
|
280
|
+
snapshot: previousOutput,
|
|
281
|
+
outputPath,
|
|
282
|
+
committedOutput,
|
|
283
|
+
assertLockOwned: assertOwnedDownloadLock,
|
|
284
|
+
});
|
|
285
|
+
previousOutput = null;
|
|
286
|
+
committedPath = false;
|
|
287
|
+
} catch (restoreError) {
|
|
288
|
+
retainLockForRecovery = true;
|
|
289
|
+
if (restoreError?.code === "DOWNLOAD_LOCK_LOST") {
|
|
290
|
+
previousOutput.retained = true;
|
|
291
|
+
manualRecoveryError = createOutputRecoveryRequiredError({
|
|
292
|
+
cause: restoreError,
|
|
293
|
+
outputPath,
|
|
294
|
+
snapshot: previousOutput,
|
|
295
|
+
});
|
|
296
|
+
} else if (
|
|
297
|
+
restoreError?.code === "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED" &&
|
|
298
|
+
restoreError?.recoveryArtifactRemoved === true &&
|
|
299
|
+
restoreError?.outputRestored === true
|
|
300
|
+
) {
|
|
301
|
+
previousOutput = null;
|
|
302
|
+
committedPath = false;
|
|
303
|
+
recoveryError = restoreError;
|
|
304
|
+
} else {
|
|
305
|
+
previousOutput.retained = true;
|
|
306
|
+
recoveryError = restoreError;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
let lockStillOwned = false;
|
|
311
|
+
let lockOwnershipError = null;
|
|
312
|
+
try {
|
|
313
|
+
assertOwnedDownloadLock();
|
|
314
|
+
lockStillOwned = true;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
lockOwnershipError = error;
|
|
317
|
+
}
|
|
318
|
+
if (lockOwnershipError?.code === "DOWNLOAD_LOCK_LOST") {
|
|
319
|
+
manualRecoveryError = createOutputRecoveryRequiredError({
|
|
320
|
+
cause: lockOwnershipError,
|
|
321
|
+
outputPath,
|
|
322
|
+
snapshot: null,
|
|
323
|
+
});
|
|
324
|
+
} else if (lockStillOwned) {
|
|
325
|
+
removeRegularFileIfPresent(outputPath);
|
|
326
|
+
} else {
|
|
327
|
+
removePathIfMatchesDescriptor(outputPath, partialFd);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (manualRecoveryError) throw manualRecoveryError;
|
|
332
|
+
if (!committedPath && previousOutput && !previousOutput.retained) {
|
|
333
|
+
try {
|
|
334
|
+
discardPreviousOutputSnapshot(previousOutput);
|
|
335
|
+
previousOutput = null;
|
|
336
|
+
} catch (cleanupError) {
|
|
337
|
+
retainLockForRecovery = true;
|
|
338
|
+
if (cleanupError?.recoveryArtifactRemoved === true) {
|
|
339
|
+
previousOutput = null;
|
|
340
|
+
} else {
|
|
341
|
+
previousOutput.retained = true;
|
|
342
|
+
}
|
|
343
|
+
recoveryError ||= cleanupError;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (recoveryError) {
|
|
347
|
+
if (
|
|
348
|
+
recoveryError?.code === "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED" &&
|
|
349
|
+
recoveryError?.recoveryArtifactRemoved === true
|
|
350
|
+
) {
|
|
351
|
+
throw recoveryError;
|
|
352
|
+
}
|
|
353
|
+
throw new DownloadError(
|
|
354
|
+
`could not restore the previous output after commit failure (${err.code || err.message}): ${recoveryError.message}; recovery artifact retained at ${previousOutput?.recoveryPath || "unknown"}`,
|
|
355
|
+
"OUTPUT_RECOVERY_FAILED",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (err instanceof DownloadError && err.code === "DOWNLOAD_LOCK_LOST") {
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
throw new DownloadError(
|
|
362
|
+
`could not commit verified artifact: ${err.message}`,
|
|
363
|
+
"FINALIZE_FAILED",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
fs.closeSync(partialFd);
|
|
369
|
+
} catch {
|
|
370
|
+
/* committed bytes and transaction state are already verified */
|
|
371
|
+
}
|
|
372
|
+
partialFd = null;
|
|
373
|
+
return { outputPath, bytes, sha256: actualSha };
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (
|
|
376
|
+
error?.code === "DOWNLOAD_LOCK_LOST" ||
|
|
377
|
+
error?.retainDownloadLock === true
|
|
378
|
+
) {
|
|
379
|
+
retainLockForRecovery = true;
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
} finally {
|
|
383
|
+
if (partialFd !== null) {
|
|
384
|
+
try {
|
|
385
|
+
fs.closeSync(partialFd);
|
|
386
|
+
} catch {
|
|
387
|
+
/* best effort */
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!committedPath && partialIdentity) {
|
|
391
|
+
removePathIfMatchesIdentity(partialPath, partialIdentity);
|
|
392
|
+
}
|
|
393
|
+
if (retainLockForRecovery) retainLock(lock);
|
|
394
|
+
else releaseLock(lock);
|
|
60
395
|
}
|
|
396
|
+
}
|
|
61
397
|
|
|
62
|
-
|
|
398
|
+
async function awaitBeforeDeadline(
|
|
399
|
+
promise,
|
|
400
|
+
{ deadline, abortController, label },
|
|
401
|
+
) {
|
|
402
|
+
const remaining = deadline - Date.now();
|
|
403
|
+
if (remaining <= 0) {
|
|
404
|
+
abortController.abort();
|
|
405
|
+
throw new DownloadError(`${label} timed out`, "TIMEOUT");
|
|
406
|
+
}
|
|
407
|
+
let timer;
|
|
63
408
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
409
|
+
return await Promise.race([
|
|
410
|
+
promise,
|
|
411
|
+
new Promise((_, reject) => {
|
|
412
|
+
timer = setTimeout(() => {
|
|
413
|
+
abortController.abort();
|
|
414
|
+
reject(new DownloadError(`${label} timed out`, "TIMEOUT"));
|
|
415
|
+
}, remaining);
|
|
416
|
+
}),
|
|
417
|
+
]);
|
|
418
|
+
} finally {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function assertPathMatchesDescriptor(filePath, fd, label) {
|
|
424
|
+
let descriptorStat;
|
|
425
|
+
try {
|
|
426
|
+
descriptorStat = fs.fstatSync(fd, { bigint: true });
|
|
427
|
+
} catch (error) {
|
|
66
428
|
throw new DownloadError(
|
|
67
|
-
|
|
68
|
-
|
|
429
|
+
`${label} identity could not be verified: ${error.message}`,
|
|
430
|
+
"PARTIAL_REPLACED",
|
|
69
431
|
);
|
|
70
432
|
}
|
|
71
|
-
if (!
|
|
433
|
+
if (!pathMatchesOpenedFileIdentitySync(filePath, descriptorStat)) {
|
|
72
434
|
throw new DownloadError(
|
|
73
|
-
|
|
74
|
-
"
|
|
435
|
+
`${label} pathname no longer identifies the verified file`,
|
|
436
|
+
"PARTIAL_REPLACED",
|
|
75
437
|
);
|
|
76
438
|
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function removePathIfMatchesDescriptor(filePath, fd) {
|
|
442
|
+
try {
|
|
443
|
+
const descriptorStat = fs.fstatSync(fd, { bigint: true });
|
|
444
|
+
if (pathMatchesOpenedFileIdentitySync(filePath, descriptorStat)) {
|
|
445
|
+
fs.unlinkSync(filePath);
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
/* a missing or exchanged pathname must not be removed */
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
77
453
|
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
if (!body) {
|
|
454
|
+
function snapshotDescriptorIdentity(fd) {
|
|
455
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
456
|
+
if (!stat.isFile() || !hasPreciseFileIdentity(stat)) {
|
|
82
457
|
throw new DownloadError(
|
|
83
|
-
"
|
|
84
|
-
"
|
|
458
|
+
"partial download descriptor has no stable file identity",
|
|
459
|
+
"PARTIAL_REPLACED",
|
|
85
460
|
);
|
|
86
461
|
}
|
|
462
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
463
|
+
}
|
|
87
464
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
465
|
+
function removePathIfMatchesIdentity(filePath, identity) {
|
|
466
|
+
try {
|
|
467
|
+
if (pathMatchesOpenedFileIdentitySync(filePath, identity)) {
|
|
468
|
+
fs.unlinkSync(filePath);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
/* a missing or exchanged pathname must not be removed */
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function createOutputRecoveryRequiredError({ cause, outputPath, snapshot }) {
|
|
478
|
+
const recoveryPath = snapshot?.recoveryPath || null;
|
|
479
|
+
const retained = recoveryPath
|
|
480
|
+
? `the verified previous output is retained at ${recoveryPath}`
|
|
481
|
+
: "no previous output snapshot exists";
|
|
482
|
+
const error = new DownloadError(
|
|
483
|
+
`download lock ownership was lost after the output commit (${cause?.message || "unknown"}); ${outputPath} was left untouched and ${retained}; inspect the output and recovery artifact manually`,
|
|
484
|
+
"OUTPUT_RECOVERY_REQUIRED",
|
|
485
|
+
);
|
|
486
|
+
error.recoveryPath = recoveryPath;
|
|
487
|
+
error.retainDownloadLock = true;
|
|
488
|
+
return error;
|
|
489
|
+
}
|
|
91
490
|
|
|
491
|
+
function createRecoveryCleanupSyncError(recoveryPath, cause) {
|
|
492
|
+
const error = new DownloadError(
|
|
493
|
+
`the recovery artifact was removed but its directory entry could not be synchronized (${cause?.message || "unknown"}); inspect ${path.dirname(recoveryPath)} before retrying`,
|
|
494
|
+
"OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED",
|
|
495
|
+
);
|
|
496
|
+
error.recoveryPath = recoveryPath;
|
|
497
|
+
error.recoveryArtifactRemoved = true;
|
|
498
|
+
error.retainDownloadLock = true;
|
|
499
|
+
return error;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function createSnapshotRecoveryRequiredError(recoveryPath, cause) {
|
|
503
|
+
const retained = recoveryPath
|
|
504
|
+
? `a recovery artifact was retained at ${recoveryPath}`
|
|
505
|
+
: "no verified recovery artifact exists";
|
|
506
|
+
const error = new DownloadError(
|
|
507
|
+
`the existing output changed while its recovery snapshot was being created (${cause?.code || cause?.message || "unknown"}); ${retained} and the download lock was retained for manual inspection`,
|
|
508
|
+
"OUTPUT_SNAPSHOT_RECOVERY_REQUIRED",
|
|
509
|
+
);
|
|
510
|
+
error.recoveryPath = recoveryPath;
|
|
511
|
+
error.retainDownloadLock = true;
|
|
512
|
+
return error;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function snapshotExistingOutput(outputPath) {
|
|
516
|
+
if (!lstatOrNull(outputPath)) return null;
|
|
517
|
+
|
|
518
|
+
const recoveryPath = `${outputPath}.recovery-${process.pid}-${crypto.randomUUID()}`;
|
|
519
|
+
assertSafeDownloadPath(recoveryPath, "output recovery path", true);
|
|
520
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
521
|
+
let fd = null;
|
|
522
|
+
let linked = false;
|
|
523
|
+
let snapshot = null;
|
|
92
524
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
525
|
+
fd = fs.openSync(outputPath, fs.constants.O_RDONLY | noFollow);
|
|
526
|
+
const recorded = snapshotOpenFile(outputPath, fd, "existing output");
|
|
527
|
+
snapshot = {
|
|
528
|
+
recoveryPath,
|
|
529
|
+
...recorded,
|
|
530
|
+
retained: false,
|
|
531
|
+
};
|
|
532
|
+
fs.closeSync(fd);
|
|
533
|
+
fd = null;
|
|
534
|
+
|
|
535
|
+
// Windows cannot replace a pathname while the old inode has an open
|
|
536
|
+
// handle. Close the verifier first, then bind a sibling hard link to the
|
|
537
|
+
// recorded inode and re-open both names to close the exchange window.
|
|
538
|
+
fs.linkSync(outputPath, recoveryPath);
|
|
539
|
+
linked = true;
|
|
540
|
+
try {
|
|
541
|
+
assertPathMatchesSnapshot(outputPath, snapshot, "existing output");
|
|
542
|
+
assertPathMatchesSnapshot(
|
|
543
|
+
recoveryPath,
|
|
544
|
+
snapshot,
|
|
545
|
+
"existing output recovery snapshot",
|
|
546
|
+
);
|
|
547
|
+
} catch (validationError) {
|
|
548
|
+
snapshot.retained = true;
|
|
549
|
+
throw createSnapshotRecoveryRequiredError(recoveryPath, validationError);
|
|
550
|
+
}
|
|
551
|
+
fsyncDirectory(path.dirname(outputPath));
|
|
552
|
+
return snapshot;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
let cleanupError = null;
|
|
555
|
+
if (error?.code === "OUTPUT_SNAPSHOT_RECOVERY_REQUIRED") {
|
|
556
|
+
cleanupError = error;
|
|
557
|
+
} else if (linked && snapshot) {
|
|
558
|
+
if (removePathIfMatchesSnapshot(recoveryPath, snapshot)) {
|
|
106
559
|
try {
|
|
107
|
-
|
|
108
|
-
} catch {
|
|
109
|
-
|
|
560
|
+
fsyncDirectory(path.dirname(recoveryPath));
|
|
561
|
+
} catch (syncError) {
|
|
562
|
+
cleanupError = createRecoveryCleanupSyncError(
|
|
563
|
+
recoveryPath,
|
|
564
|
+
syncError,
|
|
565
|
+
);
|
|
110
566
|
}
|
|
567
|
+
} else {
|
|
568
|
+
cleanupError = createSnapshotRecoveryRequiredError(recoveryPath, error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (fd !== null) {
|
|
572
|
+
try {
|
|
573
|
+
fs.closeSync(fd);
|
|
574
|
+
} catch {
|
|
575
|
+
/* best effort */
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (cleanupError) throw cleanupError;
|
|
579
|
+
if (error instanceof DownloadError) throw error;
|
|
580
|
+
throw new DownloadError(
|
|
581
|
+
`could not snapshot the existing output: ${error.message}`,
|
|
582
|
+
"OUTPUT_SNAPSHOT_FAILED",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function assertOutputMatchesSnapshotForCommit(outputPath, snapshot) {
|
|
588
|
+
if (!snapshot) {
|
|
589
|
+
if (lstatOrNull(outputPath)) {
|
|
590
|
+
throw createSnapshotRecoveryRequiredError(
|
|
591
|
+
null,
|
|
592
|
+
new DownloadError(
|
|
593
|
+
"output appeared after the no-output snapshot",
|
|
594
|
+
"OUTPUT_RECOVERY_CHANGED",
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
assertPathMatchesSnapshot(
|
|
603
|
+
outputPath,
|
|
604
|
+
snapshot,
|
|
605
|
+
"existing output before commit",
|
|
606
|
+
);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
snapshot.retained = true;
|
|
609
|
+
throw createSnapshotRecoveryRequiredError(snapshot.recoveryPath, error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function snapshotOpenFile(filePath, fd, label) {
|
|
614
|
+
assertPathMatchesDescriptor(filePath, fd, label);
|
|
615
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
616
|
+
const sha256 = sha256Descriptor(fd);
|
|
617
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
618
|
+
if (!sameSnapshotState(before, after)) {
|
|
619
|
+
throw new DownloadError(
|
|
620
|
+
`${label} changed while it was being hashed`,
|
|
621
|
+
"OUTPUT_RECOVERY_CHANGED",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
assertPathMatchesDescriptor(filePath, fd, label);
|
|
625
|
+
return {
|
|
626
|
+
sha256,
|
|
627
|
+
dev: after.dev,
|
|
628
|
+
ino: after.ino,
|
|
629
|
+
size: after.size,
|
|
630
|
+
mode: after.mode,
|
|
631
|
+
mtimeNs: after.mtimeNs,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function sameSnapshotState(first, second) {
|
|
636
|
+
return (
|
|
637
|
+
first.isFile() &&
|
|
638
|
+
second.isFile() &&
|
|
639
|
+
sameOpenedFileIdentity(first, second, ["size", "mode", "mtimeNs"])
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function assertPreviousOutputSnapshot(snapshot) {
|
|
644
|
+
assertPathMatchesSnapshot(
|
|
645
|
+
snapshot.recoveryPath,
|
|
646
|
+
snapshot,
|
|
647
|
+
"existing output recovery snapshot",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function restorePreviousOutput({
|
|
652
|
+
snapshot,
|
|
653
|
+
outputPath,
|
|
654
|
+
committedOutput,
|
|
655
|
+
assertLockOwned,
|
|
656
|
+
}) {
|
|
657
|
+
const restorePath = `${outputPath}.restore-${process.pid}-${crypto.randomUUID()}`;
|
|
658
|
+
assertSafeDownloadPath(restorePath, "output restore path", true);
|
|
659
|
+
try {
|
|
660
|
+
assertLockOwned();
|
|
661
|
+
assertPathMatchesSnapshot(
|
|
662
|
+
outputPath,
|
|
663
|
+
committedOutput,
|
|
664
|
+
"committed download before recovery",
|
|
665
|
+
);
|
|
666
|
+
assertPreviousOutputSnapshot(snapshot);
|
|
667
|
+
fs.linkSync(snapshot.recoveryPath, restorePath);
|
|
668
|
+
assertPathMatchesSnapshot(
|
|
669
|
+
restorePath,
|
|
670
|
+
snapshot,
|
|
671
|
+
"output restore candidate",
|
|
672
|
+
);
|
|
673
|
+
assertPathMatchesSnapshot(
|
|
674
|
+
outputPath,
|
|
675
|
+
committedOutput,
|
|
676
|
+
"committed download before recovery",
|
|
677
|
+
);
|
|
678
|
+
assertLockOwned();
|
|
679
|
+
fs.renameSync(restorePath, outputPath);
|
|
680
|
+
assertPathMatchesSnapshot(outputPath, snapshot, "restored previous output");
|
|
681
|
+
fsyncDirectory(path.dirname(outputPath));
|
|
682
|
+
try {
|
|
683
|
+
discardPreviousOutputSnapshot(snapshot);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error?.recoveryArtifactRemoved === true) {
|
|
686
|
+
error.outputRestored = true;
|
|
111
687
|
}
|
|
688
|
+
throw error;
|
|
112
689
|
}
|
|
113
|
-
|
|
114
|
-
|
|
690
|
+
} finally {
|
|
691
|
+
removePathIfMatchesSnapshot(restorePath, snapshot);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function discardPreviousOutputSnapshot(snapshot) {
|
|
696
|
+
assertPathMatchesSnapshot(
|
|
697
|
+
snapshot.recoveryPath,
|
|
698
|
+
snapshot,
|
|
699
|
+
"existing output recovery snapshot",
|
|
700
|
+
);
|
|
701
|
+
if (!removePathIfMatchesSnapshot(snapshot.recoveryPath, snapshot)) {
|
|
702
|
+
throw new DownloadError(
|
|
703
|
+
"existing output recovery snapshot could not be removed safely",
|
|
704
|
+
"OUTPUT_RECOVERY_CLEANUP_FAILED",
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
fsyncDirectory(path.dirname(snapshot.recoveryPath));
|
|
709
|
+
} catch (error) {
|
|
710
|
+
throw createRecoveryCleanupSyncError(snapshot.recoveryPath, error);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function assertPathMatchesSnapshot(filePath, snapshot, label) {
|
|
715
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
716
|
+
let fd = null;
|
|
717
|
+
try {
|
|
718
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
|
719
|
+
assertPathMatchesDescriptor(filePath, fd, label);
|
|
720
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
721
|
+
const sha256 = sha256Descriptor(fd);
|
|
722
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
723
|
+
if (
|
|
724
|
+
!sameSnapshotState(before, after) ||
|
|
725
|
+
after.dev !== snapshot.dev ||
|
|
726
|
+
after.ino !== snapshot.ino ||
|
|
727
|
+
after.size !== snapshot.size ||
|
|
728
|
+
after.mode !== snapshot.mode ||
|
|
729
|
+
after.mtimeNs !== snapshot.mtimeNs ||
|
|
730
|
+
sha256 !== snapshot.sha256
|
|
731
|
+
) {
|
|
732
|
+
throw new DownloadError(
|
|
733
|
+
`${label} changed before it could be used safely`,
|
|
734
|
+
"OUTPUT_RECOVERY_CHANGED",
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
} finally {
|
|
738
|
+
if (fd !== null) fs.closeSync(fd);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function removePathIfMatchesSnapshot(filePath, snapshot) {
|
|
743
|
+
try {
|
|
744
|
+
assertPathMatchesSnapshot(filePath, snapshot, "output recovery artifact");
|
|
745
|
+
if (pathMatchesOpenedFileIdentitySync(filePath, snapshot)) {
|
|
746
|
+
fs.unlinkSync(filePath);
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
/* exchanged or missing recovery paths are retained */
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function sha256Descriptor(fd) {
|
|
756
|
+
const hash = crypto.createHash("sha256");
|
|
757
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
758
|
+
let position = 0;
|
|
759
|
+
while (true) {
|
|
760
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position);
|
|
761
|
+
if (bytesRead === 0) break;
|
|
762
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
763
|
+
position += bytesRead;
|
|
764
|
+
}
|
|
765
|
+
return hash.digest("hex");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function writeAllSync(fd, buffer) {
|
|
769
|
+
let offset = 0;
|
|
770
|
+
while (offset < buffer.length) {
|
|
771
|
+
offset += fs.writeSync(fd, buffer, offset, buffer.length - offset);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function lstatOrNull(filePath) {
|
|
776
|
+
try {
|
|
777
|
+
return fs.lstatSync(filePath);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
if (err?.code === "ENOENT") return null;
|
|
780
|
+
throw err;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function assertSafeFileOrMissing(filePath, label) {
|
|
785
|
+
try {
|
|
786
|
+
assertStateSafeRegularFile(filePath, {
|
|
787
|
+
label,
|
|
788
|
+
allowMissingLeaf: true,
|
|
115
789
|
});
|
|
790
|
+
} catch (error) {
|
|
791
|
+
throw new DownloadError(error.message, "UNSAFE_PATH");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function assertSafeDownloadPath(filePath, label, allowMissingLeaf) {
|
|
796
|
+
try {
|
|
797
|
+
assertSafePathAncestors(filePath, { label, allowMissingLeaf });
|
|
798
|
+
} catch (error) {
|
|
799
|
+
throw new DownloadError(error.message, "UNSAFE_PATH");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function acquireLock(lockPath) {
|
|
804
|
+
assertSafeDownloadPath(lockPath, "download lock", true);
|
|
805
|
+
const existing = lstatOrNull(lockPath);
|
|
806
|
+
if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
|
|
807
|
+
throw new DownloadError(
|
|
808
|
+
`download lock is not a regular file: ${lockPath}`,
|
|
809
|
+
"UNSAFE_PATH",
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const token = `${process.pid}:${crypto.randomUUID()}`;
|
|
814
|
+
let fd;
|
|
815
|
+
try {
|
|
816
|
+
fd = fs.openSync(lockPath, "wx+", 0o600);
|
|
116
817
|
} catch (err) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
818
|
+
if (err?.code === "EEXIST") {
|
|
819
|
+
throw new DownloadError(
|
|
820
|
+
`another download is already using ${outputFromLock(lockPath)}`,
|
|
821
|
+
"DOWNLOAD_LOCKED",
|
|
822
|
+
);
|
|
122
823
|
}
|
|
123
|
-
throw new DownloadError(
|
|
824
|
+
throw new DownloadError(
|
|
825
|
+
`could not acquire download lock: ${err.message}`,
|
|
826
|
+
"LOCK_FAILED",
|
|
827
|
+
);
|
|
124
828
|
}
|
|
125
829
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
830
|
+
try {
|
|
831
|
+
fs.writeFileSync(fd, token, { encoding: "utf8" });
|
|
832
|
+
fs.fsyncSync(fd);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
releaseLock({ fd, lockPath, token });
|
|
835
|
+
throw new DownloadError(
|
|
836
|
+
`could not initialize download lock: ${err.message}`,
|
|
837
|
+
"LOCK_FAILED",
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return { fd, lockPath, token };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function assertDownloadLockOwned(lock) {
|
|
844
|
+
try {
|
|
845
|
+
const before = fs.fstatSync(lock.fd, { bigint: true });
|
|
846
|
+
const pathOwnedBefore = pathMatchesOpenedFileIdentitySync(
|
|
847
|
+
lock.lockPath,
|
|
848
|
+
before,
|
|
849
|
+
{ stateFields: ["size", "mtimeNs", "ctimeNs"] },
|
|
850
|
+
);
|
|
851
|
+
const expected = Buffer.from(lock.token, "utf8");
|
|
852
|
+
const actual = Buffer.alloc(expected.length + 1);
|
|
853
|
+
const bytesRead = fs.readSync(lock.fd, actual, 0, actual.length, 0);
|
|
854
|
+
const after = fs.fstatSync(lock.fd, { bigint: true });
|
|
855
|
+
const pathOwnedAfter = pathMatchesOpenedFileIdentitySync(
|
|
856
|
+
lock.lockPath,
|
|
857
|
+
after,
|
|
858
|
+
{ stateFields: ["size", "mtimeNs", "ctimeNs"] },
|
|
859
|
+
);
|
|
860
|
+
const descriptorStable =
|
|
861
|
+
before.isFile() &&
|
|
862
|
+
after.isFile() &&
|
|
863
|
+
before.nlink > 0n &&
|
|
864
|
+
after.nlink > 0n &&
|
|
865
|
+
sameOpenedFileIdentity(before, after, ["size", "mtimeNs", "ctimeNs"]);
|
|
866
|
+
const pathStillOwned = pathOwnedBefore && pathOwnedAfter;
|
|
867
|
+
const tokenMatches =
|
|
868
|
+
bytesRead === expected.length &&
|
|
869
|
+
actual.subarray(0, bytesRead).equals(expected);
|
|
870
|
+
if (!descriptorStable || !pathStillOwned || !tokenMatches) {
|
|
871
|
+
throw new Error("lock pathname, descriptor, or token changed");
|
|
132
872
|
}
|
|
873
|
+
} catch (error) {
|
|
133
874
|
throw new DownloadError(
|
|
134
|
-
`
|
|
135
|
-
"
|
|
875
|
+
`download lock ownership was lost: ${error.message}`,
|
|
876
|
+
"DOWNLOAD_LOCK_LOST",
|
|
136
877
|
);
|
|
137
878
|
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function outputFromLock(lockPath) {
|
|
882
|
+
return lockPath.endsWith(".lock") ? lockPath.slice(0, -5) : lockPath;
|
|
883
|
+
}
|
|
138
884
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
885
|
+
function releaseLock(lock) {
|
|
886
|
+
if (!lock) return;
|
|
887
|
+
closeLockHandle(lock);
|
|
142
888
|
try {
|
|
143
|
-
fs.
|
|
889
|
+
const stat = fs.lstatSync(lock.lockPath);
|
|
890
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return;
|
|
891
|
+
if (fs.readFileSync(lock.lockPath, "utf8") === lock.token) {
|
|
892
|
+
fs.unlinkSync(lock.lockPath);
|
|
893
|
+
}
|
|
144
894
|
} catch {
|
|
145
|
-
/*
|
|
895
|
+
/* best effort; a stale lock fails closed on the next attempt */
|
|
146
896
|
}
|
|
147
|
-
|
|
897
|
+
}
|
|
148
898
|
|
|
149
|
-
|
|
899
|
+
function retainLock(lock) {
|
|
900
|
+
closeLockHandle(lock);
|
|
150
901
|
}
|
|
151
902
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
903
|
+
function closeLockHandle(lock) {
|
|
904
|
+
if (!lock || lock.fd === null) return;
|
|
905
|
+
try {
|
|
906
|
+
fs.closeSync(lock.fd);
|
|
907
|
+
} catch {
|
|
908
|
+
/* best effort */
|
|
909
|
+
}
|
|
910
|
+
lock.fd = null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function removeRegularFileIfPresent(filePath) {
|
|
914
|
+
try {
|
|
915
|
+
const stat = fs.lstatSync(filePath);
|
|
916
|
+
if (stat.isFile() && !stat.isSymbolicLink()) fs.unlinkSync(filePath);
|
|
917
|
+
} catch {
|
|
918
|
+
/* missing or unsafe paths are intentionally left alone */
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function fsyncDirectory(directoryPath) {
|
|
923
|
+
let fd;
|
|
924
|
+
try {
|
|
925
|
+
fd = fs.openSync(directoryPath, "r");
|
|
926
|
+
fs.fsyncSync(fd);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
// Windows and a few filesystems do not expose directory fsync. The file
|
|
929
|
+
// itself was already fsynced, so only ignore known unsupported cases.
|
|
930
|
+
if (!["EACCES", "EBADF", "EINVAL", "EISDIR", "EPERM"].includes(err?.code)) {
|
|
931
|
+
throw err;
|
|
932
|
+
}
|
|
933
|
+
} finally {
|
|
934
|
+
if (fd !== undefined) {
|
|
935
|
+
try {
|
|
936
|
+
fs.closeSync(fd);
|
|
937
|
+
} catch {
|
|
938
|
+
/* best effort */
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** Typed error with a stable, machine-readable code. */
|
|
156
945
|
export class DownloadError extends Error {
|
|
157
946
|
constructor(message, code) {
|
|
158
947
|
super(message);
|