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,211 +1,2058 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* artifact. This is the third step of the OTA chain:
|
|
4
|
-
* Phase 5a (check) → Phase 5b (download+verify) → Phase 5c (apply)
|
|
2
|
+
* Transactional native CLI updater.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Windows: the OS refuses to overwrite a running `.exe`. The only
|
|
16
|
-
* reliable pattern is a sidecar script that (1) waits for the parent
|
|
17
|
-
* PID to exit, (2) moves `<new>` → `<target>`, (3) optionally restarts.
|
|
18
|
-
* We write a tiny `.cmd` to `%TEMP%` and spawn it detached; the parent
|
|
19
|
-
* then exits on its own.
|
|
20
|
-
*
|
|
21
|
-
* All file-system mutations can be gated by `dryRun: true`, which just
|
|
22
|
-
* returns the plan (commands / paths the applier would have executed).
|
|
23
|
-
* Tests use `platform: 'win32' | 'posix'` to exercise both branches on any
|
|
24
|
-
* host without touching the running exe.
|
|
4
|
+
* Security invariants:
|
|
5
|
+
* - the installer and OTA updater share `<canonical>.update.lock`;
|
|
6
|
+
* - apply is bound to the SHA-256 returned by the downloader and re-hashes
|
|
7
|
+
* the sibling staging file immediately before the commit point;
|
|
8
|
+
* - `.previous` is trusted only through a persisted lineage record;
|
|
9
|
+
* - Windows transfers lock ownership (token included) to a sidecar that
|
|
10
|
+
* re-checks hashes, path reparse points and the observed pre-state;
|
|
11
|
+
* - rescue is a separate transaction and never feeds `.previous` back into
|
|
12
|
+
* the forward-update backup rotation.
|
|
25
13
|
*/
|
|
26
14
|
|
|
27
15
|
import fs from "node:fs";
|
|
28
16
|
import os from "node:os";
|
|
29
17
|
import path from "node:path";
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import { spawn as nativeSpawn } from "node:child_process";
|
|
30
20
|
import executionBroker from "../process-execution-broker/index.js";
|
|
21
|
+
import {
|
|
22
|
+
pathMatchesOpenedFileIdentitySync,
|
|
23
|
+
sameOpenedFileIdentity,
|
|
24
|
+
} from "./file-identity.js";
|
|
25
|
+
import {
|
|
26
|
+
NATIVE_UPDATE_LINEAGE_SCHEMA,
|
|
27
|
+
NATIVE_UPDATE_RESULT_SCHEMA,
|
|
28
|
+
SHA256_HEX,
|
|
29
|
+
assertSafeRegularFile as assertStateSafeRegularFile,
|
|
30
|
+
lstatOrNull,
|
|
31
|
+
nativeUpdatePaths,
|
|
32
|
+
nativeResultRequiresRecovery,
|
|
33
|
+
readNativeUpdateResult,
|
|
34
|
+
readNativeLineage,
|
|
35
|
+
resolveNativeLayout,
|
|
36
|
+
} from "./native-update-state.js";
|
|
31
37
|
|
|
32
38
|
export const _deps = {
|
|
33
|
-
|
|
39
|
+
// Replacement/restart children must outlive the CLI process. Platform
|
|
40
|
+
// sandboxes deliberately bind descendants to a parent-owned process tree,
|
|
41
|
+
// so this narrowly scoped host update boundary uses native detached spawn
|
|
42
|
+
// only after path, ownership and digest validation has completed.
|
|
43
|
+
spawn: (...args) => nativeSpawn(...args),
|
|
44
|
+
spawnSync: (...args) => executionBroker.spawnSync(...args),
|
|
45
|
+
tmpdir: () => os.tmpdir(),
|
|
34
46
|
};
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
* @param {object} ctx
|
|
38
|
-
* @param {string} ctx.newExePath the verified artifact from Phase 5b
|
|
39
|
-
* @param {string} ctx.targetExePath the exe to replace (usually process.execPath)
|
|
40
|
-
* @param {boolean} [ctx.restart=false] spawn the new exe after replacement
|
|
41
|
-
* @param {boolean} [ctx.dryRun=false] return the plan, do not mutate disk
|
|
42
|
-
* @param {"win32"|"posix"} [ctx.platform] override host detection for tests
|
|
43
|
-
* @param {number} [ctx.parentPid] PID to wait on (Windows sidecar); defaults to process.pid
|
|
44
|
-
* @param {(cmd:string,args:string[])=>object} [ctx.spawnImpl] injected for tests
|
|
45
|
-
* @returns {Promise<{
|
|
46
|
-
* platform: "win32"|"posix",
|
|
47
|
-
* action: "replace-in-place"|"sidecar-cmd"|"dry-run",
|
|
48
|
-
* targetExePath: string,
|
|
49
|
-
* newExePath: string,
|
|
50
|
-
* sidecarPath: string|null,
|
|
51
|
-
* restartRequested: boolean,
|
|
52
|
-
* }>}
|
|
53
|
-
*/
|
|
54
|
-
export async function scheduleReplace(ctx) {
|
|
48
|
+
export async function scheduleReplace(ctx = {}) {
|
|
55
49
|
const {
|
|
56
50
|
newExePath,
|
|
57
51
|
targetExePath,
|
|
52
|
+
expectedSha256,
|
|
58
53
|
restart = false,
|
|
59
54
|
dryRun = false,
|
|
60
55
|
platform = process.platform === "win32" ? "win32" : "posix",
|
|
61
56
|
parentPid = process.pid,
|
|
62
57
|
spawnImpl = _deps.spawn,
|
|
58
|
+
verify = false,
|
|
59
|
+
verifyImpl = _deps.spawnSync,
|
|
60
|
+
waitForReadyImpl = waitForWindowsSidecarReady,
|
|
63
61
|
} = ctx;
|
|
64
62
|
|
|
63
|
+
validatePlatform(platform);
|
|
65
64
|
if (!newExePath || typeof newExePath !== "string") {
|
|
66
65
|
throw new ApplyError("newExePath is required", "NO_NEW_EXE");
|
|
67
66
|
}
|
|
68
67
|
if (!targetExePath || typeof targetExePath !== "string") {
|
|
69
68
|
throw new ApplyError("targetExePath is required", "NO_TARGET_EXE");
|
|
70
69
|
}
|
|
71
|
-
|
|
70
|
+
const resolvedNewPath = path.resolve(newExePath);
|
|
71
|
+
const layout = resolveNativeLayout(targetExePath, platform);
|
|
72
|
+
const resolvedTargetPath = layout.canonicalPath;
|
|
73
|
+
if (
|
|
74
|
+
platform === "win32" &&
|
|
75
|
+
path.basename(layout.requestedPath).toLowerCase() === "cc.exe" &&
|
|
76
|
+
!lstatOrNull(resolvedTargetPath)
|
|
77
|
+
) {
|
|
72
78
|
throw new ApplyError(
|
|
73
|
-
|
|
74
|
-
"
|
|
79
|
+
"cc.exe is not a managed alias because its canonical chainlesschain.exe is missing",
|
|
80
|
+
"UNMANAGED_ALIAS",
|
|
75
81
|
);
|
|
76
82
|
}
|
|
83
|
+
// Prefer a precise missing/unsafe staging error before checking its
|
|
84
|
+
// placement or the externally supplied digest. The digest is still
|
|
85
|
+
// mandatory before dry-run planning or any transaction can proceed.
|
|
86
|
+
assertApplyPath(resolvedNewPath, "new executable", false, platform);
|
|
87
|
+
if (!sameDirectory(resolvedNewPath, resolvedTargetPath, platform)) {
|
|
88
|
+
throw new ApplyError(
|
|
89
|
+
"newExePath must be staged in the canonical executable directory",
|
|
90
|
+
"STAGING_NOT_SIBLING",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const expectedSha = validateExpectedSha(expectedSha256);
|
|
94
|
+
|
|
95
|
+
const statePaths = nativeUpdatePaths(resolvedTargetPath);
|
|
96
|
+
assertApplyPath(resolvedTargetPath, "canonical executable", true, platform);
|
|
97
|
+
if (layout.aliasPath) {
|
|
98
|
+
assertApplyPath(layout.aliasPath, "managed CLI alias", true, platform);
|
|
99
|
+
}
|
|
100
|
+
for (const [label, filePath] of Object.entries({
|
|
101
|
+
"last-known-good backup": statePaths.backupPath,
|
|
102
|
+
"update lock": statePaths.lockPath,
|
|
103
|
+
"update lineage": statePaths.lineagePath,
|
|
104
|
+
"update result": statePaths.resultPath,
|
|
105
|
+
"last consumed update result": statePaths.lastResultPath,
|
|
106
|
+
})) {
|
|
107
|
+
assertApplyPath(filePath, label, true, platform);
|
|
108
|
+
}
|
|
77
109
|
|
|
78
110
|
if (dryRun) {
|
|
79
|
-
return {
|
|
111
|
+
return buildPlan({
|
|
80
112
|
platform,
|
|
81
113
|
action: "dry-run",
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
layout,
|
|
115
|
+
resolvedNewPath,
|
|
116
|
+
statePaths,
|
|
117
|
+
restart,
|
|
118
|
+
expectedSha,
|
|
119
|
+
transactionId: null,
|
|
84
120
|
sidecarPath: null,
|
|
85
|
-
|
|
86
|
-
};
|
|
121
|
+
});
|
|
87
122
|
}
|
|
88
123
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
if (lstatOrNull(statePaths.lastResultPath)) {
|
|
125
|
+
let lastResult;
|
|
126
|
+
try {
|
|
127
|
+
lastResult = readNativeUpdateResult(statePaths.lastResultPath, {
|
|
128
|
+
label: "last consumed native update result",
|
|
129
|
+
platform,
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw asApplyError(error, error.code || "RESULT_INVALID");
|
|
133
|
+
}
|
|
134
|
+
if (nativeResultRequiresRecovery(lastResult)) {
|
|
135
|
+
throw new ApplyError(
|
|
136
|
+
`the previous native update could not roll back safely; recover before updating again: ${statePaths.lastResultPath}`,
|
|
137
|
+
"RECOVERY_REQUIRED",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (lstatOrNull(statePaths.resultPath)) {
|
|
143
|
+
throw new ApplyError(
|
|
144
|
+
`an unconsumed native update result exists: ${statePaths.resultPath}`,
|
|
145
|
+
"RESULT_PENDING",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const updateLock = acquireUpdateLock(statePaths.lockPath, platform);
|
|
150
|
+
const transactionId = crypto.randomUUID();
|
|
151
|
+
try {
|
|
152
|
+
// A path component can be replaced while lock acquisition is in flight.
|
|
153
|
+
// Repeat all trust-boundary checks while holding the shared installer/OTA
|
|
154
|
+
// lock, before reading or mutating any transaction state.
|
|
155
|
+
assertApplyPath(resolvedNewPath, "new executable", false, platform);
|
|
156
|
+
assertApplyPath(resolvedTargetPath, "canonical executable", true, platform);
|
|
157
|
+
if (layout.aliasPath) {
|
|
158
|
+
assertApplyPath(layout.aliasPath, "managed CLI alias", true, platform);
|
|
159
|
+
}
|
|
160
|
+
for (const [label, filePath] of Object.entries({
|
|
161
|
+
"last-known-good backup": statePaths.backupPath,
|
|
162
|
+
"update lineage": statePaths.lineagePath,
|
|
163
|
+
"update result": statePaths.resultPath,
|
|
164
|
+
"last consumed update result": statePaths.lastResultPath,
|
|
165
|
+
})) {
|
|
166
|
+
assertApplyPath(filePath, label, true, platform);
|
|
167
|
+
}
|
|
168
|
+
// Bind apply to the exact bytes accepted by the downloader. Windows does
|
|
169
|
+
// this again in the sidecar after the parent exits.
|
|
170
|
+
assertExpectedFileHash(resolvedNewPath, expectedSha, "new executable");
|
|
171
|
+
const hadTarget = Boolean(lstatOrNull(resolvedTargetPath));
|
|
172
|
+
const hadAlias = Boolean(layout.aliasPath && lstatOrNull(layout.aliasPath));
|
|
173
|
+
const targetBeforeSha = hadTarget
|
|
174
|
+
? stableSha256(resolvedTargetPath, "canonical executable")
|
|
175
|
+
: null;
|
|
176
|
+
const aliasBeforeSha = hadAlias
|
|
177
|
+
? stableSha256(layout.aliasPath, "managed CLI alias")
|
|
178
|
+
: null;
|
|
179
|
+
const orphaned = hadTarget
|
|
180
|
+
? []
|
|
181
|
+
: quarantineFreshState(statePaths, transactionId, platform, updateLock);
|
|
182
|
+
const hadPriorBackup = Boolean(lstatOrNull(statePaths.backupPath));
|
|
183
|
+
const priorBackupSha = hadPriorBackup
|
|
184
|
+
? stableSha256(statePaths.backupPath, "existing last-known-good backup")
|
|
185
|
+
: null;
|
|
186
|
+
|
|
187
|
+
if (platform === "win32") {
|
|
188
|
+
return scheduleWindowsTransaction({
|
|
189
|
+
operation: "update",
|
|
190
|
+
resolvedNewPath,
|
|
191
|
+
layout,
|
|
192
|
+
statePaths,
|
|
193
|
+
updateLock,
|
|
194
|
+
transactionId,
|
|
195
|
+
expectedSha,
|
|
196
|
+
targetBeforeSha,
|
|
197
|
+
aliasBeforeSha,
|
|
198
|
+
hadTarget,
|
|
199
|
+
hadAlias,
|
|
200
|
+
hadPriorBackup,
|
|
201
|
+
priorBackupSha,
|
|
202
|
+
parentPid,
|
|
203
|
+
restart: Boolean(restart),
|
|
204
|
+
verify: Boolean(verify),
|
|
205
|
+
spawnImpl,
|
|
206
|
+
waitForReadyImpl,
|
|
207
|
+
orphaned,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return applyPosixUpdate({
|
|
212
|
+
resolvedNewPath,
|
|
213
|
+
layout,
|
|
214
|
+
statePaths,
|
|
215
|
+
updateLock,
|
|
216
|
+
transactionId,
|
|
217
|
+
expectedSha,
|
|
218
|
+
targetBeforeSha,
|
|
219
|
+
hadTarget,
|
|
220
|
+
hadPriorBackup,
|
|
94
221
|
restart: Boolean(restart),
|
|
222
|
+
verify: Boolean(verify),
|
|
223
|
+
spawnImpl,
|
|
224
|
+
verifyImpl,
|
|
225
|
+
orphaned,
|
|
95
226
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
windowsHide: true,
|
|
106
|
-
});
|
|
107
|
-
if (child && typeof child.unref === "function") child.unref();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error?.code === "ROLLBACK_FAILED") {
|
|
229
|
+
retainUpdateLockForRecovery(updateLock);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
} finally {
|
|
233
|
+
if (!updateLock.transferred) releaseUpdateLock(updateLock);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
108
236
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
237
|
+
function applyPosixUpdate(ctx) {
|
|
238
|
+
const {
|
|
239
|
+
resolvedNewPath,
|
|
240
|
+
layout,
|
|
241
|
+
statePaths,
|
|
242
|
+
updateLock,
|
|
243
|
+
transactionId,
|
|
244
|
+
expectedSha,
|
|
245
|
+
targetBeforeSha,
|
|
246
|
+
hadTarget,
|
|
247
|
+
hadPriorBackup,
|
|
248
|
+
restart,
|
|
249
|
+
verify,
|
|
250
|
+
spawnImpl,
|
|
251
|
+
verifyImpl,
|
|
252
|
+
orphaned,
|
|
253
|
+
} = ctx;
|
|
254
|
+
const targetPath = layout.canonicalPath;
|
|
255
|
+
let backupStagingPath = null;
|
|
256
|
+
let backupCommitted = false;
|
|
257
|
+
|
|
258
|
+
if (hadTarget) {
|
|
259
|
+
backupStagingPath = uniqueSibling(statePaths.backupPath, "pending");
|
|
260
|
+
try {
|
|
261
|
+
atomicCopyFile(targetPath, backupStagingPath, {
|
|
262
|
+
preserveModeFrom: targetPath,
|
|
263
|
+
updateLock,
|
|
264
|
+
});
|
|
265
|
+
assertExpectedFileHash(
|
|
266
|
+
backupStagingPath,
|
|
267
|
+
targetBeforeSha,
|
|
268
|
+
"pending last-known-good backup",
|
|
269
|
+
);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
removeRegularFileIfPresent(backupStagingPath);
|
|
272
|
+
throw asApplyError(error, "BACKUP_FAILED", "could not create backup");
|
|
273
|
+
}
|
|
117
274
|
}
|
|
118
275
|
|
|
119
|
-
// POSIX: atomic rename works even if targetExePath is the running exe.
|
|
120
|
-
// Preserve executable bit — a downloaded file from fetch defaults to 644.
|
|
121
276
|
try {
|
|
122
|
-
fs.chmodSync(
|
|
277
|
+
fs.chmodSync(resolvedNewPath, 0o755);
|
|
123
278
|
} catch {
|
|
124
|
-
/*
|
|
279
|
+
/* verification remains authoritative on filesystems without POSIX modes */
|
|
125
280
|
}
|
|
281
|
+
|
|
126
282
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
283
|
+
if (hadTarget) {
|
|
284
|
+
assertExpectedFileHash(
|
|
285
|
+
targetPath,
|
|
286
|
+
targetBeforeSha,
|
|
287
|
+
"canonical executable before update commit",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
// These are intentionally the final candidate and lock reads before the
|
|
291
|
+
// canonical rename. If an operator replaces the token pathname while a
|
|
292
|
+
// long-running transaction is staged, the old owner must stop here.
|
|
293
|
+
assertExpectedFileHash(resolvedNewPath, expectedSha, "new executable");
|
|
294
|
+
assertUpdateLockOwned(updateLock);
|
|
295
|
+
fs.renameSync(resolvedNewPath, targetPath);
|
|
296
|
+
fsyncDirectory(path.dirname(targetPath));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
removeRegularFileIfPresent(backupStagingPath);
|
|
299
|
+
if (error instanceof ApplyError) throw error;
|
|
300
|
+
throw new ApplyError(`rename failed: ${error.message}`, "RENAME_FAILED");
|
|
130
301
|
}
|
|
131
302
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
303
|
+
let commitFailure = null;
|
|
304
|
+
try {
|
|
305
|
+
assertExpectedFileHash(
|
|
306
|
+
targetPath,
|
|
307
|
+
expectedSha,
|
|
308
|
+
"canonical executable after update commit",
|
|
309
|
+
);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
commitFailure = new ApplyError(
|
|
312
|
+
`post-commit SHA-256 verification failed: ${error.message}`,
|
|
313
|
+
"POST_COMMIT_HASH_FAILED",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (!commitFailure && verify) {
|
|
317
|
+
commitFailure = verificationFailure(targetPath, verifyImpl);
|
|
318
|
+
}
|
|
319
|
+
if (!commitFailure) {
|
|
320
|
+
try {
|
|
321
|
+
if (hadTarget) {
|
|
322
|
+
assertUpdateLockOwned(updateLock);
|
|
323
|
+
fs.renameSync(backupStagingPath, statePaths.backupPath);
|
|
324
|
+
backupStagingPath = null;
|
|
325
|
+
backupCommitted = true;
|
|
326
|
+
fsyncDirectory(path.dirname(targetPath));
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
commitFailure = new ApplyError(
|
|
330
|
+
`could not persist last-known-good backup: ${error.message}`,
|
|
331
|
+
"BACKUP_COMMIT_FAILED",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!commitFailure) {
|
|
336
|
+
try {
|
|
337
|
+
atomicWriteJson(
|
|
338
|
+
statePaths.lineagePath,
|
|
339
|
+
makeLineage({
|
|
340
|
+
transactionId,
|
|
341
|
+
operation: "update",
|
|
342
|
+
currentSha256: expectedSha,
|
|
343
|
+
previousSha256: hadTarget ? targetBeforeSha : null,
|
|
344
|
+
}),
|
|
345
|
+
updateLock,
|
|
346
|
+
);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
commitFailure = new ApplyError(
|
|
349
|
+
`could not persist update lineage: ${error.message}`,
|
|
350
|
+
"LINEAGE_WRITE_FAILED",
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (commitFailure) {
|
|
356
|
+
rollbackFailedPosixUpdate({
|
|
357
|
+
targetPath,
|
|
358
|
+
statePaths,
|
|
359
|
+
transactionId,
|
|
360
|
+
hadTarget,
|
|
361
|
+
targetBeforeSha,
|
|
362
|
+
rollbackSourcePath: backupStagingPath || statePaths.backupPath,
|
|
363
|
+
backupCommitted,
|
|
364
|
+
hadPriorBackup,
|
|
365
|
+
updateLock,
|
|
140
366
|
});
|
|
141
|
-
|
|
367
|
+
removeRegularFileIfPresent(backupStagingPath);
|
|
368
|
+
throw commitFailure;
|
|
142
369
|
}
|
|
143
370
|
|
|
144
|
-
|
|
145
|
-
|
|
371
|
+
restartVerifiedExecutable(targetPath, restart, spawnImpl);
|
|
372
|
+
return buildPlan({
|
|
373
|
+
platform: "posix",
|
|
146
374
|
action: "replace-in-place",
|
|
147
|
-
|
|
148
|
-
|
|
375
|
+
layout,
|
|
376
|
+
resolvedNewPath,
|
|
377
|
+
statePaths,
|
|
378
|
+
restart,
|
|
379
|
+
expectedSha,
|
|
380
|
+
transactionId,
|
|
149
381
|
sidecarPath: null,
|
|
150
|
-
|
|
151
|
-
};
|
|
382
|
+
orphaned,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function rollbackFailedPosixUpdate(ctx) {
|
|
387
|
+
const {
|
|
388
|
+
targetPath,
|
|
389
|
+
statePaths,
|
|
390
|
+
transactionId,
|
|
391
|
+
hadTarget,
|
|
392
|
+
targetBeforeSha,
|
|
393
|
+
rollbackSourcePath,
|
|
394
|
+
backupCommitted,
|
|
395
|
+
hadPriorBackup,
|
|
396
|
+
updateLock,
|
|
397
|
+
} = ctx;
|
|
398
|
+
const failedPath = uniqueSibling(targetPath, "failed");
|
|
399
|
+
try {
|
|
400
|
+
if (hadTarget) {
|
|
401
|
+
try {
|
|
402
|
+
atomicCopyFile(targetPath, failedPath, {
|
|
403
|
+
preserveModeFrom: targetPath,
|
|
404
|
+
updateLock,
|
|
405
|
+
});
|
|
406
|
+
} catch {
|
|
407
|
+
/* preserving rejected bytes must never block restoration */
|
|
408
|
+
}
|
|
409
|
+
atomicCopyFile(rollbackSourcePath, targetPath, {
|
|
410
|
+
preserveModeFrom: rollbackSourcePath,
|
|
411
|
+
updateLock,
|
|
412
|
+
});
|
|
413
|
+
assertExpectedFileHash(
|
|
414
|
+
targetPath,
|
|
415
|
+
targetBeforeSha,
|
|
416
|
+
"restored executable",
|
|
417
|
+
);
|
|
418
|
+
// When the pending backup was never committed, the old target,
|
|
419
|
+
// `.previous`, and lineage are once again the exact pre-transaction
|
|
420
|
+
// state. Preserve that lineage instead of fabricating a relationship to
|
|
421
|
+
// a backup that was never installed.
|
|
422
|
+
if (backupCommitted) {
|
|
423
|
+
atomicWriteJson(
|
|
424
|
+
statePaths.lineagePath,
|
|
425
|
+
makeLineage({
|
|
426
|
+
transactionId,
|
|
427
|
+
operation: "rolled-back",
|
|
428
|
+
currentSha256: targetBeforeSha,
|
|
429
|
+
previousSha256: targetBeforeSha,
|
|
430
|
+
}),
|
|
431
|
+
updateLock,
|
|
432
|
+
);
|
|
433
|
+
} else if (!hadPriorBackup) {
|
|
434
|
+
assertUpdateLockOwned(updateLock);
|
|
435
|
+
fs.renameSync(rollbackSourcePath, statePaths.backupPath);
|
|
436
|
+
fsyncDirectory(path.dirname(targetPath));
|
|
437
|
+
atomicWriteJson(
|
|
438
|
+
statePaths.lineagePath,
|
|
439
|
+
makeLineage({
|
|
440
|
+
transactionId,
|
|
441
|
+
operation: "rolled-back",
|
|
442
|
+
currentSha256: targetBeforeSha,
|
|
443
|
+
previousSha256: targetBeforeSha,
|
|
444
|
+
}),
|
|
445
|
+
updateLock,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
assertUpdateLockOwned(updateLock);
|
|
450
|
+
fs.renameSync(targetPath, failedPath);
|
|
451
|
+
fsyncDirectory(path.dirname(targetPath));
|
|
452
|
+
assertUpdateLockOwned(updateLock);
|
|
453
|
+
removeRegularFileIfPresent(statePaths.lineagePath);
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
throw new ApplyError(
|
|
457
|
+
`new binary failed and rollback failed: ${error.message}`,
|
|
458
|
+
"ROLLBACK_FAILED",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function scheduleWindowsTransaction(ctx) {
|
|
464
|
+
const {
|
|
465
|
+
operation,
|
|
466
|
+
resolvedNewPath,
|
|
467
|
+
layout,
|
|
468
|
+
statePaths,
|
|
469
|
+
updateLock,
|
|
470
|
+
transactionId,
|
|
471
|
+
expectedSha,
|
|
472
|
+
targetBeforeSha,
|
|
473
|
+
aliasBeforeSha,
|
|
474
|
+
hadTarget,
|
|
475
|
+
hadAlias,
|
|
476
|
+
hadPriorBackup = false,
|
|
477
|
+
priorBackupSha = null,
|
|
478
|
+
parentPid,
|
|
479
|
+
restart,
|
|
480
|
+
verify,
|
|
481
|
+
spawnImpl,
|
|
482
|
+
waitForReadyImpl,
|
|
483
|
+
orphaned = [],
|
|
484
|
+
} = ctx;
|
|
485
|
+
let sidecarPath = null;
|
|
486
|
+
try {
|
|
487
|
+
const windowsSystemRoot = resolveTrustedWindowsSystemRoot();
|
|
488
|
+
sidecarPath = writeWindowsSidecar({
|
|
489
|
+
operation,
|
|
490
|
+
newExePath: resolvedNewPath,
|
|
491
|
+
targetExePath: layout.canonicalPath,
|
|
492
|
+
aliasPath: layout.aliasPath,
|
|
493
|
+
backupPath: statePaths.backupPath,
|
|
494
|
+
lineagePath: statePaths.lineagePath,
|
|
495
|
+
lockPath: statePaths.lockPath,
|
|
496
|
+
resultPath: statePaths.resultPath,
|
|
497
|
+
lockToken: updateLock.token,
|
|
498
|
+
transactionId,
|
|
499
|
+
expectedSha256: expectedSha,
|
|
500
|
+
targetBeforeSha256: targetBeforeSha,
|
|
501
|
+
aliasBeforeSha256: aliasBeforeSha,
|
|
502
|
+
hadTarget,
|
|
503
|
+
hadAlias,
|
|
504
|
+
hadPriorBackup,
|
|
505
|
+
priorBackupSha256: priorBackupSha,
|
|
506
|
+
parentPid,
|
|
507
|
+
restart,
|
|
508
|
+
verify,
|
|
509
|
+
windowsSystemRoot,
|
|
510
|
+
});
|
|
511
|
+
const readyPath = `${sidecarPath}.ready`;
|
|
512
|
+
|
|
513
|
+
// Keep ownership in the token file, but close the Windows handle so the
|
|
514
|
+
// sidecar can hash/read that file during its readiness preflight. A failed
|
|
515
|
+
// spawn is still cleaned up by the parent because `transferred` remains
|
|
516
|
+
// false until the marker is validated.
|
|
517
|
+
closeUpdateLockHandle(updateLock);
|
|
518
|
+
let child;
|
|
519
|
+
try {
|
|
520
|
+
child = spawnImpl(
|
|
521
|
+
path.win32.join(windowsSystemRoot, "System32", "cmd.exe"),
|
|
522
|
+
["/d", "/c", sidecarPath],
|
|
523
|
+
{
|
|
524
|
+
origin: "packer:update-sidecar",
|
|
525
|
+
scope: "pack-update",
|
|
526
|
+
policy: "allow",
|
|
527
|
+
shell: false,
|
|
528
|
+
detached: true,
|
|
529
|
+
stdio: "ignore",
|
|
530
|
+
windowsHide: true,
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
throw new ApplyError(
|
|
535
|
+
`could not start update sidecar: ${error.message}`,
|
|
536
|
+
"SIDECAR_SPAWN_FAILED",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
if (!child) {
|
|
540
|
+
throw new ApplyError(
|
|
541
|
+
"update sidecar spawn returned no child process",
|
|
542
|
+
"SIDECAR_SPAWN_FAILED",
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let ready = false;
|
|
547
|
+
let childError = null;
|
|
548
|
+
if (typeof child.once === "function") {
|
|
549
|
+
// Native spawn failures are normally emitted asynchronously. Attach
|
|
550
|
+
// before the synchronous readiness wait so both the no-ready and the
|
|
551
|
+
// post-transfer paths always consume the ChildProcess error event.
|
|
552
|
+
child.once("error", (error) => {
|
|
553
|
+
childError = error;
|
|
554
|
+
if (ready) {
|
|
555
|
+
removeOwnedLockFile(updateLock);
|
|
556
|
+
removeRegularFileIfPresent(sidecarPath);
|
|
557
|
+
removeRegularFileIfPresent(readyPath);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
ready =
|
|
563
|
+
Boolean(
|
|
564
|
+
waitForReadyImpl({
|
|
565
|
+
readyPath,
|
|
566
|
+
transactionId,
|
|
567
|
+
timeoutMs: 10_000,
|
|
568
|
+
child,
|
|
569
|
+
}),
|
|
570
|
+
) && !childError;
|
|
571
|
+
} catch {
|
|
572
|
+
ready = false;
|
|
573
|
+
}
|
|
574
|
+
if (!ready) {
|
|
575
|
+
try {
|
|
576
|
+
if (typeof child.kill === "function") child.kill();
|
|
577
|
+
} catch {
|
|
578
|
+
/* the owned lock still prevents a late sidecar commit */
|
|
579
|
+
}
|
|
580
|
+
// Do not unlink through a path whose sidecar safety checks never
|
|
581
|
+
// completed. Close our handle and deliberately leave the token file as
|
|
582
|
+
// a fail-closed recovery marker.
|
|
583
|
+
closeUpdateLockHandle(updateLock);
|
|
584
|
+
updateLock.transferred = true;
|
|
585
|
+
throw new ApplyError(
|
|
586
|
+
"Windows update sidecar did not complete its safety handshake",
|
|
587
|
+
"SIDECAR_NOT_READY",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
removeRegularFileIfPresent(readyPath);
|
|
591
|
+
|
|
592
|
+
updateLock.transferred = true;
|
|
593
|
+
if (typeof child.unref === "function") child.unref();
|
|
594
|
+
|
|
595
|
+
return buildPlan({
|
|
596
|
+
platform: "win32",
|
|
597
|
+
action: "sidecar-cmd",
|
|
598
|
+
layout,
|
|
599
|
+
resolvedNewPath,
|
|
600
|
+
statePaths,
|
|
601
|
+
restart,
|
|
602
|
+
expectedSha,
|
|
603
|
+
transactionId,
|
|
604
|
+
sidecarPath,
|
|
605
|
+
orphaned,
|
|
606
|
+
});
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (sidecarPath) removeRegularFileIfPresent(sidecarPath);
|
|
609
|
+
if (sidecarPath) removeRegularFileIfPresent(`${sidecarPath}.ready`);
|
|
610
|
+
if (error instanceof ApplyError) throw error;
|
|
611
|
+
throw new ApplyError(
|
|
612
|
+
`could not schedule Windows replacement: ${error.message}`,
|
|
613
|
+
"SIDECAR_FAILED",
|
|
614
|
+
);
|
|
615
|
+
}
|
|
152
616
|
}
|
|
153
617
|
|
|
154
618
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* so tests can read it back and assert on the shape without actually spawning.
|
|
158
|
-
*
|
|
159
|
-
* @param {object} ctx
|
|
160
|
-
* @param {string} ctx.newExePath
|
|
161
|
-
* @param {string} ctx.targetExePath
|
|
162
|
-
* @param {number} ctx.parentPid
|
|
163
|
-
* @param {boolean} ctx.restart
|
|
164
|
-
* @returns {string} absolute path to the generated .cmd (lives in os.tmpdir())
|
|
619
|
+
* Windows sidecar for both forward update and independent rescue.
|
|
620
|
+
* Every caller must pass the observed pre-state and lock token.
|
|
165
621
|
*/
|
|
166
622
|
export function writeWindowsSidecar(ctx) {
|
|
167
|
-
const {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
623
|
+
const {
|
|
624
|
+
operation = "update",
|
|
625
|
+
newExePath,
|
|
626
|
+
targetExePath,
|
|
627
|
+
aliasPath = null,
|
|
628
|
+
backupPath = `${targetExePath}.previous`,
|
|
629
|
+
lineagePath = `${targetExePath}.update-lineage.json`,
|
|
630
|
+
lockPath = `${targetExePath}.update.lock`,
|
|
631
|
+
resultPath = `${targetExePath}.update-result.json`,
|
|
632
|
+
lockToken,
|
|
633
|
+
transactionId = crypto.randomUUID(),
|
|
634
|
+
expectedSha256,
|
|
635
|
+
targetBeforeSha256 = null,
|
|
636
|
+
aliasBeforeSha256 = null,
|
|
637
|
+
hadTarget,
|
|
638
|
+
hadAlias = false,
|
|
639
|
+
hadPriorBackup = false,
|
|
640
|
+
priorBackupSha256 = null,
|
|
641
|
+
parentPid,
|
|
642
|
+
restart,
|
|
643
|
+
verify = true,
|
|
644
|
+
windowsSystemRoot = resolveTrustedWindowsSystemRoot(),
|
|
645
|
+
} = ctx;
|
|
646
|
+
|
|
647
|
+
if (!["update", "rescue"].includes(operation)) {
|
|
648
|
+
throw new ApplyError("invalid sidecar operation", "BAD_OPERATION");
|
|
649
|
+
}
|
|
650
|
+
if (!Number.isSafeInteger(parentPid) || parentPid <= 0) {
|
|
651
|
+
throw new ApplyError(
|
|
652
|
+
"parentPid must be a positive integer",
|
|
653
|
+
"BAD_PARENT_PID",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (typeof hadTarget !== "boolean" || typeof hadAlias !== "boolean") {
|
|
657
|
+
throw new ApplyError(
|
|
658
|
+
"sidecar requires explicit hadTarget/hadAlias state",
|
|
659
|
+
"MISSING_PRESTATE",
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (typeof hadPriorBackup !== "boolean") {
|
|
663
|
+
throw new ApplyError(
|
|
664
|
+
"sidecar requires explicit prior backup state",
|
|
665
|
+
"MISSING_PRESTATE",
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const expectedSha = validateExpectedSha(expectedSha256);
|
|
669
|
+
if (hadTarget) validateExpectedSha(targetBeforeSha256, "targetBeforeSha256");
|
|
670
|
+
if (hadAlias) validateExpectedSha(aliasBeforeSha256, "aliasBeforeSha256");
|
|
671
|
+
if (hadPriorBackup) {
|
|
672
|
+
validateExpectedSha(priorBackupSha256, "priorBackupSha256");
|
|
673
|
+
}
|
|
674
|
+
if (!lockToken || !/^[0-9]+:[0-9a-f-]{32,36}$/i.test(lockToken)) {
|
|
675
|
+
throw new ApplyError(
|
|
676
|
+
"a valid lock ownership token is required",
|
|
677
|
+
"BAD_LOCK_TOKEN",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (!/^[0-9a-f-]{36}$/i.test(transactionId)) {
|
|
681
|
+
throw new ApplyError(
|
|
682
|
+
"a valid transactionId is required",
|
|
683
|
+
"BAD_TRANSACTION_ID",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const suffix = transactionId.replaceAll("-", "");
|
|
688
|
+
const lockTokenSha = crypto
|
|
689
|
+
.createHash("sha256")
|
|
690
|
+
.update(lockToken, "utf8")
|
|
691
|
+
.digest("hex");
|
|
692
|
+
// GitHub's Windows runners expose TEMP through an 8.3 alias such as
|
|
693
|
+
// C:\Users\RUNNER~1\..., while realpath/ancestor validation returns the
|
|
694
|
+
// corresponding long pathname. Publish and validate the marker through the
|
|
695
|
+
// canonical root so that a harmless DOS alias is not mistaken for a reparse
|
|
696
|
+
// redirect. Keep the original root in the sidecar's reparse preflight below:
|
|
697
|
+
// canonicalization must never erase evidence of a junction in TEMP.
|
|
698
|
+
const rawTempRoot = path.resolve(_deps.tmpdir());
|
|
699
|
+
let sidecarTempRoot;
|
|
700
|
+
try {
|
|
701
|
+
const rawTempStat = fs.lstatSync(rawTempRoot);
|
|
702
|
+
if (rawTempStat.isSymbolicLink() || !rawTempStat.isDirectory()) {
|
|
703
|
+
throw new Error("temporary root must be a real directory");
|
|
704
|
+
}
|
|
705
|
+
sidecarTempRoot = fs.realpathSync.native(rawTempRoot);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
throw new ApplyError(
|
|
708
|
+
`could not bind the Windows sidecar temporary root: ${error.message}`,
|
|
709
|
+
"UNSAFE_WINDOWS_PATH",
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const sidecarPath = path.join(sidecarTempRoot, `cc-pack-apply-${suffix}.cmd`);
|
|
713
|
+
try {
|
|
714
|
+
assertStateSafeRegularFile(sidecarPath, {
|
|
715
|
+
label: "Windows update sidecar",
|
|
716
|
+
allowMissingLeaf: true,
|
|
717
|
+
platform: process.platform,
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
throw new ApplyError(error.message, "UNSAFE_WINDOWS_PATH");
|
|
721
|
+
}
|
|
722
|
+
const readyPath = `${sidecarPath}.ready`;
|
|
723
|
+
const readyTempPath = `${readyPath}.tmp-${suffix}`;
|
|
724
|
+
const parentProbePath = `${sidecarPath}.parent`;
|
|
725
|
+
const backupTempPath = `${backupPath}.tmp-${suffix}`;
|
|
726
|
+
const currentTempPath = `${targetExePath}.rescue-current-${suffix}`;
|
|
727
|
+
const rollbackTempPath = `${targetExePath}.rollback-${suffix}`;
|
|
728
|
+
const failedPath = `${targetExePath}.failed-${suffix}`;
|
|
729
|
+
const aliasCandidatePath = aliasPath
|
|
730
|
+
? `${aliasPath}.candidate-${suffix}`
|
|
731
|
+
: targetExePath;
|
|
732
|
+
const aliasBackupPath = aliasPath
|
|
733
|
+
? `${aliasPath}.previous-${suffix}`
|
|
734
|
+
: targetExePath;
|
|
735
|
+
const aliasRollbackPath = aliasPath
|
|
736
|
+
? `${aliasPath}.rollback-${suffix}`
|
|
737
|
+
: targetExePath;
|
|
738
|
+
const lineageTempPath = `${lineagePath}.tmp-${suffix}`;
|
|
739
|
+
const resultTempPath = `${resultPath}.tmp-${suffix}`;
|
|
740
|
+
|
|
741
|
+
const allPaths = {
|
|
742
|
+
windowsSystemRoot,
|
|
743
|
+
// This is intentionally distinct from sidecarPath's canonical parent.
|
|
744
|
+
// buildWindowsReparseChecks expands every ancestor, so the trusted
|
|
745
|
+
// PowerShell preflight rejects a junction/redirector in the original TEMP
|
|
746
|
+
// spelling before READY_FILE is published and lock ownership transfers.
|
|
747
|
+
rawTempRoot,
|
|
748
|
+
sidecarPath,
|
|
749
|
+
readyPath,
|
|
750
|
+
readyTempPath,
|
|
751
|
+
parentProbePath,
|
|
752
|
+
newExePath,
|
|
753
|
+
targetExePath,
|
|
754
|
+
backupPath,
|
|
755
|
+
lineagePath,
|
|
756
|
+
lockPath,
|
|
757
|
+
resultPath,
|
|
758
|
+
backupTempPath,
|
|
759
|
+
currentTempPath,
|
|
760
|
+
rollbackTempPath,
|
|
761
|
+
failedPath,
|
|
762
|
+
lineageTempPath,
|
|
763
|
+
resultTempPath,
|
|
764
|
+
};
|
|
765
|
+
if (aliasPath) {
|
|
766
|
+
Object.assign(allPaths, {
|
|
767
|
+
aliasPath,
|
|
768
|
+
aliasCandidatePath,
|
|
769
|
+
aliasBackupPath,
|
|
770
|
+
aliasRollbackPath,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
for (const [label, value] of Object.entries(allPaths)) {
|
|
774
|
+
assertWindowsCmdSafePath(value, label);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const successLineage = makeLineage({
|
|
778
|
+
transactionId,
|
|
779
|
+
operation,
|
|
780
|
+
currentSha256: expectedSha,
|
|
781
|
+
previousSha256:
|
|
782
|
+
operation === "update"
|
|
783
|
+
? hadTarget
|
|
784
|
+
? targetBeforeSha256
|
|
785
|
+
: null
|
|
786
|
+
: expectedSha,
|
|
787
|
+
});
|
|
788
|
+
const rolledBackLineage = hadTarget
|
|
789
|
+
? makeLineage({
|
|
790
|
+
transactionId,
|
|
791
|
+
operation: "rolled-back",
|
|
792
|
+
currentSha256: targetBeforeSha256,
|
|
793
|
+
previousSha256: targetBeforeSha256,
|
|
794
|
+
})
|
|
795
|
+
: null;
|
|
796
|
+
const rollbackSourcePath =
|
|
797
|
+
operation === "update" ? backupTempPath : currentTempPath;
|
|
798
|
+
const reparseChecks = buildWindowsReparseChecks(Object.values(allPaths));
|
|
799
|
+
const startupCheck = buildWindowsTimedStartupCheckLines();
|
|
800
|
+
|
|
179
801
|
const cmd = [
|
|
180
802
|
"@echo off",
|
|
181
|
-
"setlocal",
|
|
182
|
-
`set
|
|
183
|
-
|
|
184
|
-
`set
|
|
803
|
+
"setlocal EnableExtensions DisableDelayedExpansion",
|
|
804
|
+
`set "CC_SYSTEM_ROOT=${windowsSystemRoot}"`,
|
|
805
|
+
'"%CC_SYSTEM_ROOT%\\System32\\chcp.com" 65001 >NUL',
|
|
806
|
+
`set "PARENT_PID=${parentPid}"`,
|
|
807
|
+
`set "OPERATION=${operation}"`,
|
|
808
|
+
`set "TRANSACTION_ID=${transactionId}"`,
|
|
809
|
+
`set "NEW_EXE=${newExePath}"`,
|
|
810
|
+
`set "TARGET_EXE=${targetExePath}"`,
|
|
811
|
+
`set "ALIAS_EXE=${aliasPath || ""}"`,
|
|
812
|
+
`set "BACKUP_EXE=${backupPath}"`,
|
|
813
|
+
`set "BACKUP_TEMP=${backupTempPath}"`,
|
|
814
|
+
`set "CURRENT_TEMP=${currentTempPath}"`,
|
|
815
|
+
`set "ROLLBACK_SOURCE=${rollbackSourcePath}"`,
|
|
816
|
+
`set "ROLLBACK_TEMP=${rollbackTempPath}"`,
|
|
817
|
+
`set "FAILED_EXE=${failedPath}"`,
|
|
818
|
+
`set "ALIAS_CANDIDATE=${aliasCandidatePath}"`,
|
|
819
|
+
`set "ALIAS_BACKUP=${aliasBackupPath}"`,
|
|
820
|
+
`set "ALIAS_ROLLBACK=${aliasRollbackPath}"`,
|
|
821
|
+
`set "LINEAGE_FILE=${lineagePath}"`,
|
|
822
|
+
`set "LINEAGE_TEMP=${lineageTempPath}"`,
|
|
823
|
+
`set "LOCK_FILE=${lockPath}"`,
|
|
824
|
+
`set "LOCK_TOKEN=${lockToken}"`,
|
|
825
|
+
`set "LOCK_TOKEN_SHA=${lockTokenSha}"`,
|
|
826
|
+
`set "RESULT_FILE=${resultPath}"`,
|
|
827
|
+
`set "RESULT_TEMP=${resultTempPath}"`,
|
|
828
|
+
`set "READY_FILE=${readyPath}"`,
|
|
829
|
+
`set "READY_TEMP=${readyTempPath}"`,
|
|
830
|
+
`set "PARENT_PROBE=${parentProbePath}"`,
|
|
831
|
+
`set "EXPECTED_SHA=${expectedSha}"`,
|
|
832
|
+
`set "TARGET_BEFORE_SHA=${targetBeforeSha256 || ""}"`,
|
|
833
|
+
`set "ALIAS_BEFORE_SHA=${aliasBeforeSha256 || ""}"`,
|
|
834
|
+
`set "HAD_TARGET=${hadTarget ? "1" : "0"}"`,
|
|
835
|
+
`set "HAD_ALIAS=${hadAlias ? "1" : "0"}"`,
|
|
836
|
+
`set "HAD_BACKUP=${hadPriorBackup ? "1" : "0"}"`,
|
|
837
|
+
`set "BACKUP_BEFORE_SHA=${priorBackupSha256 || ""}"`,
|
|
838
|
+
`set "MANAGE_ALIAS=${aliasPath ? "1" : "0"}"`,
|
|
839
|
+
'set "TARGET_COMMITTED=0"',
|
|
840
|
+
'set "ALIAS_COMMITTED=0"',
|
|
841
|
+
'set "BACKUP_COMMITTED=0"',
|
|
842
|
+
'set "ROLLBACK_OK=1"',
|
|
843
|
+
'set "RESULT_STATUS=not-started"',
|
|
844
|
+
'set "FAIL_STATUS=transaction-failed"',
|
|
845
|
+
'set "EXIT_CODE=1"',
|
|
846
|
+
'set "WRITE_RESULT=1"',
|
|
847
|
+
'set "SKIP_PATH_CLEANUP=0"',
|
|
848
|
+
`set "RESTART_REQUESTED=${restart ? "1" : "0"}"`,
|
|
849
|
+
// The parent transfers lock ownership only after this preflight marker is
|
|
850
|
+
// visible. Everything is checked again after the parent exits.
|
|
851
|
+
"call :checklock",
|
|
852
|
+
"if errorlevel 1 goto locklost",
|
|
853
|
+
'if not exist "%CC_SYSTEM_ROOT%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" goto safetycheckfailed',
|
|
854
|
+
...reparseChecks,
|
|
855
|
+
// Redirection creates/truncates its target before echo writes the token.
|
|
856
|
+
// Publish through a same-directory rename so the parent can never observe
|
|
857
|
+
// a zero-length or partially written readiness marker.
|
|
858
|
+
`> "%READY_TEMP%" echo ${transactionId}`,
|
|
859
|
+
"if errorlevel 1 goto readyfailed",
|
|
860
|
+
'move /Y "%READY_TEMP%" "%READY_FILE%" >NUL 2>&1',
|
|
861
|
+
"if errorlevel 1 goto readyfailed",
|
|
185
862
|
"set /a ATTEMPTS=0",
|
|
186
863
|
":waitloop",
|
|
187
|
-
|
|
864
|
+
// Do not pipe tasklist directly into findstr. A detached cmd.exe with
|
|
865
|
+
// ignored stdio can retain the anonymous pipe writer and deadlock findstr
|
|
866
|
+
// after tasklist exits. The transaction-scoped regular file also lets us
|
|
867
|
+
// distinguish a failed tasklist probe from a genuinely absent parent.
|
|
868
|
+
'"%CC_SYSTEM_ROOT%\\System32\\tasklist.exe" /FI "PID eq %PARENT_PID%" /FO CSV /NH > "%PARENT_PROBE%" 2>NUL',
|
|
869
|
+
"if errorlevel 1 goto parentcheckfailed",
|
|
870
|
+
'"%CC_SYSTEM_ROOT%\\System32\\findstr.exe" /L /C:"%PARENT_PID%" "%PARENT_PROBE%" >NUL 2>&1',
|
|
188
871
|
"if errorlevel 1 goto doreplace",
|
|
189
872
|
"set /a ATTEMPTS=%ATTEMPTS%+1",
|
|
190
|
-
"if %ATTEMPTS% GEQ
|
|
191
|
-
//
|
|
192
|
-
"
|
|
873
|
+
"if %ATTEMPTS% GEQ 120 goto parenttimeout",
|
|
874
|
+
// timeout.exe rejects redirected stdin and would turn the 120-second wait
|
|
875
|
+
// into a tight loop under stdio:"ignore". Loopback ping provides a trusted
|
|
876
|
+
// one-second delay without reading the detached child's stdin.
|
|
877
|
+
'"%CC_SYSTEM_ROOT%\\System32\\ping.exe" -n 2 -w 1000 127.0.0.1 >NUL',
|
|
878
|
+
"if errorlevel 1 goto parentcheckfailed",
|
|
193
879
|
"goto waitloop",
|
|
194
880
|
":doreplace",
|
|
195
|
-
"
|
|
196
|
-
"if errorlevel 1
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
881
|
+
"call :checklock",
|
|
882
|
+
"if errorlevel 1 goto locklost",
|
|
883
|
+
'if not exist "%CC_SYSTEM_ROOT%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" goto safetycheckfailed',
|
|
884
|
+
...reparseChecks,
|
|
885
|
+
"call :checkprestate",
|
|
886
|
+
"if errorlevel 1 goto stateconflict",
|
|
887
|
+
'set "HASH_PATH=%NEW_EXE%"',
|
|
888
|
+
"call :hashpath",
|
|
889
|
+
"if errorlevel 1 goto hashfailed",
|
|
890
|
+
'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto hashmismatch',
|
|
891
|
+
'if "%OPERATION%"=="rescue" goto stagerescue',
|
|
892
|
+
'if "%HAD_TARGET%"=="0" goto stagealias',
|
|
893
|
+
'copy /B /Y "%TARGET_EXE%" "%BACKUP_TEMP%" >NUL',
|
|
894
|
+
"if errorlevel 1 goto backupfailed",
|
|
895
|
+
'set "HASH_PATH=%BACKUP_TEMP%"',
|
|
896
|
+
"call :hashpath",
|
|
897
|
+
"if errorlevel 1 goto backupfailed",
|
|
898
|
+
'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto backupfailed',
|
|
899
|
+
"goto stagealias",
|
|
900
|
+
":stagerescue",
|
|
901
|
+
'if "%HAD_TARGET%"=="0" goto stagealias',
|
|
902
|
+
'copy /B /Y "%TARGET_EXE%" "%CURRENT_TEMP%" >NUL',
|
|
903
|
+
"if errorlevel 1 goto backupfailed",
|
|
904
|
+
'set "HASH_PATH=%CURRENT_TEMP%"',
|
|
905
|
+
"call :hashpath",
|
|
906
|
+
"if errorlevel 1 goto backupfailed",
|
|
907
|
+
'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto backupfailed',
|
|
908
|
+
":stagealias",
|
|
909
|
+
'if "%MANAGE_ALIAS%"=="0" goto precommitcheck',
|
|
910
|
+
'if "%HAD_ALIAS%"=="0" goto makealiascandidate',
|
|
911
|
+
'copy /B /Y "%ALIAS_EXE%" "%ALIAS_BACKUP%" >NUL',
|
|
912
|
+
"if errorlevel 1 goto aliasstagefailed",
|
|
913
|
+
":makealiascandidate",
|
|
914
|
+
'copy /B /Y "%NEW_EXE%" "%ALIAS_CANDIDATE%" >NUL',
|
|
915
|
+
"if errorlevel 1 goto aliasstagefailed",
|
|
916
|
+
'set "HASH_PATH=%ALIAS_CANDIDATE%"',
|
|
917
|
+
"call :hashpath",
|
|
918
|
+
"if errorlevel 1 goto aliasstagefailed",
|
|
919
|
+
'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto aliasstagefailed',
|
|
920
|
+
":precommitcheck",
|
|
921
|
+
// Re-hash after all potentially slow backup work, immediately before move.
|
|
922
|
+
'set "HASH_PATH=%NEW_EXE%"',
|
|
923
|
+
"call :hashpath",
|
|
924
|
+
"if errorlevel 1 goto hashfailed",
|
|
925
|
+
'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto hashmismatch',
|
|
926
|
+
// Ownership and path safety are checked again at the actual commit edge,
|
|
927
|
+
// after all slow copies and hash processes have completed.
|
|
928
|
+
"call :checklock",
|
|
929
|
+
"if errorlevel 1 goto locklost",
|
|
930
|
+
...reparseChecks,
|
|
931
|
+
"call :checkprestate",
|
|
932
|
+
"if errorlevel 1 goto stateconflict",
|
|
933
|
+
'move /Y "%NEW_EXE%" "%TARGET_EXE%" >NUL',
|
|
934
|
+
"if errorlevel 1 goto movefailed",
|
|
935
|
+
'set "TARGET_COMMITTED=1"',
|
|
936
|
+
'if "%MANAGE_ALIAS%"=="0" goto verifytransaction',
|
|
937
|
+
'move /Y "%ALIAS_CANDIDATE%" "%ALIAS_EXE%" >NUL',
|
|
938
|
+
"if errorlevel 1 goto aliascommitfailed",
|
|
939
|
+
'set "ALIAS_COMMITTED=1"',
|
|
940
|
+
":verifytransaction",
|
|
941
|
+
// Hash committed pathnames before executing either binary or persisting
|
|
942
|
+
// backup/lineage. This catches a candidate pathname exchange in the final
|
|
943
|
+
// precommit safety window as well as alias commit substitution.
|
|
944
|
+
'set "HASH_PATH=%TARGET_EXE%"',
|
|
945
|
+
"call :hashpath",
|
|
946
|
+
"if errorlevel 1 goto postcommithashfailed",
|
|
947
|
+
'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto postcommithashfailed',
|
|
948
|
+
'if "%MANAGE_ALIAS%"=="0" goto startupverify',
|
|
949
|
+
'set "HASH_PATH=%ALIAS_EXE%"',
|
|
950
|
+
"call :hashpath",
|
|
951
|
+
"if errorlevel 1 goto postcommithashfailed",
|
|
952
|
+
'if /I not "%OBSERVED_SHA%"=="%EXPECTED_SHA%" goto postcommithashfailed',
|
|
953
|
+
":startupverify",
|
|
954
|
+
...(verify
|
|
955
|
+
? [
|
|
956
|
+
'set "VERIFY_PATH=%TARGET_EXE%"',
|
|
957
|
+
...startupCheck,
|
|
958
|
+
'if "%MANAGE_ALIAS%"=="0" goto writelineage',
|
|
959
|
+
'set "VERIFY_PATH=%ALIAS_EXE%"',
|
|
960
|
+
...startupCheck,
|
|
961
|
+
]
|
|
962
|
+
: ["REM post-replace verification disabled"]),
|
|
963
|
+
":writelineage",
|
|
964
|
+
'if not "%OPERATION%"=="update" goto persistlineage',
|
|
965
|
+
'if "%HAD_TARGET%"=="0" goto persistlineage',
|
|
966
|
+
'move /Y "%BACKUP_TEMP%" "%BACKUP_EXE%" >NUL',
|
|
967
|
+
"if errorlevel 1 goto backupcommitfailed",
|
|
968
|
+
'set "BACKUP_COMMITTED=1"',
|
|
969
|
+
'set "ROLLBACK_SOURCE=%BACKUP_EXE%"',
|
|
970
|
+
":persistlineage",
|
|
971
|
+
`> "%LINEAGE_TEMP%" echo ${JSON.stringify(successLineage)}`,
|
|
972
|
+
'move /Y "%LINEAGE_TEMP%" "%LINEAGE_FILE%" >NUL',
|
|
973
|
+
"if errorlevel 1 goto lineagefailed",
|
|
974
|
+
restart
|
|
975
|
+
? "REM restart deferred until result persistence"
|
|
976
|
+
: "REM restart not requested",
|
|
977
|
+
'set "RESULT_STATUS=success"',
|
|
978
|
+
'set "EXIT_CODE=0"',
|
|
979
|
+
"goto cleanup",
|
|
980
|
+
":verifyfailed",
|
|
981
|
+
'set "FAIL_STATUS=verify-failed"',
|
|
982
|
+
"goto rollbacktransaction",
|
|
983
|
+
":postcommithashfailed",
|
|
984
|
+
'set "FAIL_STATUS=post-commit-hash-failed"',
|
|
985
|
+
"goto rollbacktransaction",
|
|
986
|
+
":aliascommitfailed",
|
|
987
|
+
'set "FAIL_STATUS=alias-commit-failed"',
|
|
988
|
+
"goto rollbacktransaction",
|
|
989
|
+
":lineagefailed",
|
|
990
|
+
'set "FAIL_STATUS=lineage-write-failed"',
|
|
991
|
+
"goto rollbacktransaction",
|
|
992
|
+
":rollbacktransaction",
|
|
993
|
+
'if not "%TARGET_COMMITTED%"=="1" goto rollbackalias',
|
|
994
|
+
'copy /B /Y "%TARGET_EXE%" "%FAILED_EXE%" >NUL 2>&1',
|
|
995
|
+
'if "%HAD_TARGET%"=="0" goto removefreshcanonical',
|
|
996
|
+
'copy /B /Y "%ROLLBACK_SOURCE%" "%ROLLBACK_TEMP%" >NUL',
|
|
997
|
+
"if errorlevel 1 goto targetrollbackfailed",
|
|
998
|
+
'move /Y "%ROLLBACK_TEMP%" "%TARGET_EXE%" >NUL',
|
|
999
|
+
"if errorlevel 1 goto targetrollbackfailed",
|
|
1000
|
+
'set "HASH_PATH=%TARGET_EXE%"',
|
|
1001
|
+
"call :hashpath",
|
|
1002
|
+
"if errorlevel 1 goto targetrollbackfailed",
|
|
1003
|
+
'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" goto targetrollbackfailed',
|
|
1004
|
+
"goto rollbackalias",
|
|
1005
|
+
":removefreshcanonical",
|
|
1006
|
+
'del /F /Q "%TARGET_EXE%" >NUL 2>&1',
|
|
1007
|
+
'if exist "%TARGET_EXE%" goto targetrollbackfailed',
|
|
1008
|
+
"goto rollbackalias",
|
|
1009
|
+
":targetrollbackfailed",
|
|
1010
|
+
'set "ROLLBACK_OK=0"',
|
|
1011
|
+
":rollbackalias",
|
|
1012
|
+
'if not "%ALIAS_COMMITTED%"=="1" goto finishrollback',
|
|
1013
|
+
'if "%HAD_ALIAS%"=="0" goto removefreshalias',
|
|
1014
|
+
'copy /B /Y "%ALIAS_BACKUP%" "%ALIAS_ROLLBACK%" >NUL',
|
|
1015
|
+
"if errorlevel 1 goto aliasrollbackfailed",
|
|
1016
|
+
'move /Y "%ALIAS_ROLLBACK%" "%ALIAS_EXE%" >NUL',
|
|
1017
|
+
"if errorlevel 1 goto aliasrollbackfailed",
|
|
1018
|
+
'set "HASH_PATH=%ALIAS_EXE%"',
|
|
1019
|
+
"call :hashpath",
|
|
1020
|
+
"if errorlevel 1 goto aliasrollbackfailed",
|
|
1021
|
+
'if /I not "%OBSERVED_SHA%"=="%ALIAS_BEFORE_SHA%" goto aliasrollbackfailed',
|
|
1022
|
+
"goto finishrollback",
|
|
1023
|
+
":removefreshalias",
|
|
1024
|
+
'del /F /Q "%ALIAS_EXE%" >NUL 2>&1',
|
|
1025
|
+
'if exist "%ALIAS_EXE%" goto aliasrollbackfailed',
|
|
1026
|
+
"goto finishrollback",
|
|
1027
|
+
":aliasrollbackfailed",
|
|
1028
|
+
'set "ROLLBACK_OK=0"',
|
|
1029
|
+
":finishrollback",
|
|
1030
|
+
'if "%ROLLBACK_OK%"=="0" goto rollbackfailed',
|
|
1031
|
+
...(operation === "update" && rolledBackLineage
|
|
1032
|
+
? [
|
|
1033
|
+
'if "%BACKUP_COMMITTED%"=="1" goto writerolledbacklineage',
|
|
1034
|
+
'if "%HAD_BACKUP%"=="1" goto rollbackstatecomplete',
|
|
1035
|
+
'move /Y "%BACKUP_TEMP%" "%BACKUP_EXE%" >NUL',
|
|
1036
|
+
"if errorlevel 1 goto rollbackstatecomplete",
|
|
1037
|
+
'set "BACKUP_COMMITTED=1"',
|
|
1038
|
+
":writerolledbacklineage",
|
|
1039
|
+
`> "%LINEAGE_TEMP%" echo ${JSON.stringify(rolledBackLineage)}`,
|
|
1040
|
+
'move /Y "%LINEAGE_TEMP%" "%LINEAGE_FILE%" >NUL 2>&1',
|
|
1041
|
+
"if errorlevel 1 goto rollbacklineagefailed",
|
|
1042
|
+
":rollbackstatecomplete",
|
|
1043
|
+
]
|
|
1044
|
+
: []),
|
|
1045
|
+
'set "RESULT_STATUS=%FAIL_STATUS%-rolled-back"',
|
|
1046
|
+
"goto cleanup",
|
|
1047
|
+
":rollbackfailed",
|
|
1048
|
+
'set "RESULT_STATUS=%FAIL_STATUS%-rollback-failed"',
|
|
1049
|
+
"goto cleanup",
|
|
1050
|
+
":rollbacklineagefailed",
|
|
1051
|
+
'set "ROLLBACK_OK=0"',
|
|
1052
|
+
'set "FAIL_STATUS=rollback-lineage-write-failed"',
|
|
1053
|
+
"goto rollbackfailed",
|
|
1054
|
+
":parenttimeout",
|
|
1055
|
+
'set "RESULT_STATUS=parent-timeout"',
|
|
1056
|
+
"goto cleanup",
|
|
1057
|
+
":parentcheckfailed",
|
|
1058
|
+
'set "RESULT_STATUS=parent-check-failed"',
|
|
1059
|
+
"goto cleanup",
|
|
1060
|
+
":safetycheckfailed",
|
|
1061
|
+
'set "RESULT_STATUS=safety-check-unavailable"',
|
|
1062
|
+
'set "WRITE_RESULT=0"',
|
|
1063
|
+
'set "SKIP_PATH_CLEANUP=1"',
|
|
1064
|
+
"goto cleanup",
|
|
1065
|
+
":unsafepath",
|
|
1066
|
+
'set "RESULT_STATUS=unsafe-reparse-path"',
|
|
1067
|
+
'set "WRITE_RESULT=0"',
|
|
1068
|
+
'set "SKIP_PATH_CLEANUP=1"',
|
|
1069
|
+
"goto cleanup",
|
|
1070
|
+
":stateconflict",
|
|
1071
|
+
'set "RESULT_STATUS=prestate-conflict"',
|
|
1072
|
+
"goto cleanup",
|
|
1073
|
+
":hashfailed",
|
|
1074
|
+
'set "RESULT_STATUS=hash-check-failed"',
|
|
1075
|
+
"goto cleanup",
|
|
1076
|
+
":hashmismatch",
|
|
1077
|
+
'set "RESULT_STATUS=sha256-mismatch"',
|
|
1078
|
+
"goto cleanup",
|
|
1079
|
+
":backupfailed",
|
|
1080
|
+
'set "RESULT_STATUS=backup-failed"',
|
|
1081
|
+
"goto cleanup",
|
|
1082
|
+
":backupcommitfailed",
|
|
1083
|
+
'set "FAIL_STATUS=backup-commit-failed"',
|
|
1084
|
+
"goto rollbacktransaction",
|
|
1085
|
+
":aliasstagefailed",
|
|
1086
|
+
'set "RESULT_STATUS=alias-stage-failed"',
|
|
1087
|
+
"goto cleanup",
|
|
1088
|
+
":movefailed",
|
|
1089
|
+
'set "RESULT_STATUS=move-failed"',
|
|
1090
|
+
"goto cleanup",
|
|
1091
|
+
":locklost",
|
|
1092
|
+
'set "RESULT_STATUS=lock-ownership-lost"',
|
|
1093
|
+
'set "WRITE_RESULT=0"',
|
|
1094
|
+
"goto cleanup",
|
|
1095
|
+
":readyfailed",
|
|
1096
|
+
'set "WRITE_RESULT=0"',
|
|
1097
|
+
'set "SKIP_PATH_CLEANUP=1"',
|
|
1098
|
+
"goto cleanup",
|
|
1099
|
+
":cleanup",
|
|
1100
|
+
'if "%SKIP_PATH_CLEANUP%"=="1" goto unsafeexit',
|
|
1101
|
+
'if "%ROLLBACK_OK%"=="1" del /F /Q "%BACKUP_TEMP%" >NUL 2>&1',
|
|
1102
|
+
'if "%ROLLBACK_OK%"=="1" del /F /Q "%CURRENT_TEMP%" >NUL 2>&1',
|
|
1103
|
+
'if "%ROLLBACK_OK%"=="1" del /F /Q "%ROLLBACK_TEMP%" >NUL 2>&1',
|
|
1104
|
+
'if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_CANDIDATE%" >NUL 2>&1',
|
|
1105
|
+
'if "%ROLLBACK_OK%"=="1" if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_BACKUP%" >NUL 2>&1',
|
|
1106
|
+
'if "%ROLLBACK_OK%"=="1" if "%MANAGE_ALIAS%"=="1" del /F /Q "%ALIAS_ROLLBACK%" >NUL 2>&1',
|
|
1107
|
+
'del /F /Q "%LINEAGE_TEMP%" >NUL 2>&1',
|
|
1108
|
+
'del /F /Q "%READY_TEMP%" >NUL 2>&1',
|
|
1109
|
+
'del /F /Q "%READY_FILE%" >NUL 2>&1',
|
|
1110
|
+
'del /F /Q "%PARENT_PROBE%" >NUL 2>&1',
|
|
1111
|
+
'if "%WRITE_RESULT%"=="0" goto releaselock',
|
|
1112
|
+
`> "%RESULT_TEMP%" echo {"schema":"${NATIVE_UPDATE_RESULT_SCHEMA}","transactionId":"${transactionId}","operation":"${operation}","status":"%RESULT_STATUS%","exitCode":%EXIT_CODE%,"hadTarget":${hadTarget ? "true" : "false"},"targetBeforeSha256":${targetBeforeSha256 ? `"${targetBeforeSha256}"` : "null"},"expectedSha256":"${expectedSha}","aliasManaged":${aliasPath ? "true" : "false"},"hadAlias":${hadAlias ? "true" : "false"},"aliasBeforeSha256":${aliasBeforeSha256 ? `"${aliasBeforeSha256}"` : "null"}}`,
|
|
1113
|
+
"if errorlevel 1 goto resultpersistfailed",
|
|
1114
|
+
'move /Y "%RESULT_TEMP%" "%RESULT_FILE%" >NUL 2>&1',
|
|
1115
|
+
"if errorlevel 1 goto resultpersistfailed",
|
|
1116
|
+
"goto releaselock",
|
|
1117
|
+
":resultpersistfailed",
|
|
1118
|
+
// Keep the owned lock and result temp fail-closed: otherwise a detached
|
|
1119
|
+
// transaction could disappear without any durable observable outcome.
|
|
1120
|
+
'set "EXIT_CODE=1"',
|
|
1121
|
+
":unsafeexit",
|
|
1122
|
+
'(goto) 2>NUL & del /F /Q "%~f0" & exit /b %EXIT_CODE%',
|
|
1123
|
+
":releaselock",
|
|
1124
|
+
"call :checklock",
|
|
1125
|
+
'if not errorlevel 1 del /F /Q "%LOCK_FILE%" >NUL 2>&1',
|
|
1126
|
+
'if "%EXIT_CODE%"=="0" if "%RESTART_REQUESTED%"=="1" start "" "%TARGET_EXE%"',
|
|
1127
|
+
'(goto) 2>NUL & del /F /Q "%~f0" & exit /b %EXIT_CODE%',
|
|
1128
|
+
":hashpath",
|
|
1129
|
+
'set "OBSERVED_SHA="',
|
|
1130
|
+
'for /f "usebackq delims=" %%H in (`%CC_SYSTEM_ROOT%\\System32\\certutil.exe -hashfile "%HASH_PATH%" SHA256 ^| %CC_SYSTEM_ROOT%\\System32\\findstr.exe /R /X /I "[0-9A-F][0-9A-F]*"`) do if not defined OBSERVED_SHA set "OBSERVED_SHA=%%H"',
|
|
1131
|
+
"if not defined OBSERVED_SHA exit /b 1",
|
|
1132
|
+
"exit /b 0",
|
|
1133
|
+
":checklock",
|
|
1134
|
+
'set "HASH_PATH=%LOCK_FILE%"',
|
|
1135
|
+
"call :hashpath",
|
|
1136
|
+
"if errorlevel 1 exit /b 1",
|
|
1137
|
+
'if /I not "%OBSERVED_SHA%"=="%LOCK_TOKEN_SHA%" exit /b 1',
|
|
1138
|
+
"exit /b 0",
|
|
1139
|
+
":checkprestate",
|
|
1140
|
+
'if "%HAD_TARGET%"=="1" goto prestate_target_present',
|
|
1141
|
+
'if exist "%TARGET_EXE%" exit /b 1',
|
|
1142
|
+
"goto prestate_alias",
|
|
1143
|
+
":prestate_target_present",
|
|
1144
|
+
'if not exist "%TARGET_EXE%" exit /b 1',
|
|
1145
|
+
'set "HASH_PATH=%TARGET_EXE%"',
|
|
1146
|
+
"call :hashpath",
|
|
1147
|
+
"if errorlevel 1 exit /b 1",
|
|
1148
|
+
'if /I not "%OBSERVED_SHA%"=="%TARGET_BEFORE_SHA%" exit /b 1',
|
|
1149
|
+
":prestate_alias",
|
|
1150
|
+
'if "%MANAGE_ALIAS%"=="0" goto prestate_backup',
|
|
1151
|
+
'if "%HAD_ALIAS%"=="1" goto prestate_alias_present',
|
|
1152
|
+
'if exist "%ALIAS_EXE%" exit /b 1',
|
|
1153
|
+
"goto prestate_backup",
|
|
1154
|
+
":prestate_alias_present",
|
|
1155
|
+
'if not exist "%ALIAS_EXE%" exit /b 1',
|
|
1156
|
+
'set "HASH_PATH=%ALIAS_EXE%"',
|
|
1157
|
+
"call :hashpath",
|
|
1158
|
+
"if errorlevel 1 exit /b 1",
|
|
1159
|
+
'if /I not "%OBSERVED_SHA%"=="%ALIAS_BEFORE_SHA%" exit /b 1',
|
|
1160
|
+
":prestate_backup",
|
|
1161
|
+
'if "%HAD_BACKUP%"=="1" goto prestate_backup_present',
|
|
1162
|
+
'if exist "%BACKUP_EXE%" exit /b 1',
|
|
1163
|
+
"exit /b 0",
|
|
1164
|
+
":prestate_backup_present",
|
|
1165
|
+
'if not exist "%BACKUP_EXE%" exit /b 1',
|
|
1166
|
+
'set "HASH_PATH=%BACKUP_EXE%"',
|
|
1167
|
+
"call :hashpath",
|
|
1168
|
+
"if errorlevel 1 exit /b 1",
|
|
1169
|
+
'if /I not "%OBSERVED_SHA%"=="%BACKUP_BEFORE_SHA%" exit /b 1',
|
|
1170
|
+
"exit /b 0",
|
|
203
1171
|
].join("\r\n");
|
|
204
1172
|
|
|
205
|
-
fs.writeFileSync(sidecarPath, cmd
|
|
1173
|
+
fs.writeFileSync(sidecarPath, `\uFEFF${cmd}`, {
|
|
1174
|
+
encoding: "utf8",
|
|
1175
|
+
flag: "wx",
|
|
1176
|
+
mode: 0o600,
|
|
1177
|
+
});
|
|
206
1178
|
return sidecarPath;
|
|
207
1179
|
}
|
|
208
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* Independent rescue path. It validates `.previous` against lineage, stages a
|
|
1183
|
+
* separate rescue candidate, and never rotates or consumes `.previous`.
|
|
1184
|
+
*/
|
|
1185
|
+
export async function rollbackLastKnownGood(ctx = {}) {
|
|
1186
|
+
const {
|
|
1187
|
+
targetExePath,
|
|
1188
|
+
backupPath: requestedBackupPath,
|
|
1189
|
+
restart = false,
|
|
1190
|
+
verify = true,
|
|
1191
|
+
platform = process.platform === "win32" ? "win32" : "posix",
|
|
1192
|
+
parentPid = process.pid,
|
|
1193
|
+
spawnImpl = _deps.spawn,
|
|
1194
|
+
verifyImpl = _deps.spawnSync,
|
|
1195
|
+
waitForReadyImpl = waitForWindowsSidecarReady,
|
|
1196
|
+
allowMissingTarget = false,
|
|
1197
|
+
} = ctx;
|
|
1198
|
+
validatePlatform(platform);
|
|
1199
|
+
if (!targetExePath || typeof targetExePath !== "string") {
|
|
1200
|
+
throw new ApplyError("targetExePath is required", "NO_TARGET_EXE");
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const layout = resolveNativeLayout(targetExePath, platform);
|
|
1204
|
+
if (
|
|
1205
|
+
platform === "win32" &&
|
|
1206
|
+
path.basename(layout.requestedPath).toLowerCase() === "cc.exe" &&
|
|
1207
|
+
!lstatOrNull(layout.canonicalPath)
|
|
1208
|
+
) {
|
|
1209
|
+
throw new ApplyError(
|
|
1210
|
+
"cc.exe is not a managed alias because its canonical chainlesschain.exe is missing",
|
|
1211
|
+
"UNMANAGED_ALIAS",
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const statePaths = nativeUpdatePaths(layout.canonicalPath);
|
|
1215
|
+
const backupPath = path.resolve(requestedBackupPath || statePaths.backupPath);
|
|
1216
|
+
if (!sameDirectory(backupPath, layout.canonicalPath, platform)) {
|
|
1217
|
+
throw new ApplyError(
|
|
1218
|
+
"backupPath must be in the canonical executable directory",
|
|
1219
|
+
"BACKUP_NOT_SIBLING",
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
assertApplyPath(backupPath, "last-known-good backup", false, platform);
|
|
1223
|
+
assertApplyPath(statePaths.lineagePath, "update lineage", false, platform);
|
|
1224
|
+
assertApplyPath(layout.canonicalPath, "canonical executable", true, platform);
|
|
1225
|
+
if (layout.aliasPath) {
|
|
1226
|
+
assertApplyPath(layout.aliasPath, "managed CLI alias", true, platform);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const updateLock = acquireUpdateLock(statePaths.lockPath, platform);
|
|
1230
|
+
const transactionId = crypto.randomUUID();
|
|
1231
|
+
let rescueStagingPath = null;
|
|
1232
|
+
try {
|
|
1233
|
+
let lineage;
|
|
1234
|
+
try {
|
|
1235
|
+
lineage = readNativeLineage(statePaths.lineagePath);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
throw asApplyError(error, error.code || "LINEAGE_INVALID");
|
|
1238
|
+
}
|
|
1239
|
+
if (!lineage.previousSha256) {
|
|
1240
|
+
throw new ApplyError(
|
|
1241
|
+
"lineage does not identify a previous executable",
|
|
1242
|
+
"NO_PREVIOUS_LINEAGE",
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const hadTarget = Boolean(lstatOrNull(layout.canonicalPath));
|
|
1246
|
+
if (!hadTarget && !allowMissingTarget) {
|
|
1247
|
+
throw new ApplyError(
|
|
1248
|
+
"canonical executable is missing; explicit allowMissingTarget is required for disaster rescue",
|
|
1249
|
+
"TARGET_MISSING_FOR_RESCUE",
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
const targetBeforeSha = hadTarget
|
|
1253
|
+
? stableSha256(layout.canonicalPath, "canonical executable")
|
|
1254
|
+
: null;
|
|
1255
|
+
if (hadTarget && targetBeforeSha !== lineage.currentSha256) {
|
|
1256
|
+
throw new ApplyError(
|
|
1257
|
+
"canonical executable does not match the current lineage generation",
|
|
1258
|
+
"LINEAGE_CURRENT_MISMATCH",
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
assertExpectedFileHash(
|
|
1262
|
+
backupPath,
|
|
1263
|
+
lineage.previousSha256,
|
|
1264
|
+
"last-known-good backup",
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
rescueStagingPath = uniqueSibling(layout.canonicalPath, "rescue");
|
|
1268
|
+
atomicCopyFile(backupPath, rescueStagingPath, {
|
|
1269
|
+
preserveModeFrom: backupPath,
|
|
1270
|
+
updateLock,
|
|
1271
|
+
});
|
|
1272
|
+
assertExpectedFileHash(
|
|
1273
|
+
rescueStagingPath,
|
|
1274
|
+
lineage.previousSha256,
|
|
1275
|
+
"rescue staging executable",
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
const hadAlias = Boolean(layout.aliasPath && lstatOrNull(layout.aliasPath));
|
|
1279
|
+
const aliasBeforeSha = hadAlias
|
|
1280
|
+
? stableSha256(layout.aliasPath, "managed CLI alias")
|
|
1281
|
+
: null;
|
|
1282
|
+
|
|
1283
|
+
if (platform === "win32") {
|
|
1284
|
+
const result = scheduleWindowsTransaction({
|
|
1285
|
+
operation: "rescue",
|
|
1286
|
+
resolvedNewPath: rescueStagingPath,
|
|
1287
|
+
layout,
|
|
1288
|
+
statePaths: { ...statePaths, backupPath },
|
|
1289
|
+
updateLock,
|
|
1290
|
+
transactionId,
|
|
1291
|
+
expectedSha: lineage.previousSha256,
|
|
1292
|
+
targetBeforeSha,
|
|
1293
|
+
aliasBeforeSha,
|
|
1294
|
+
hadTarget,
|
|
1295
|
+
hadAlias,
|
|
1296
|
+
hadPriorBackup: true,
|
|
1297
|
+
priorBackupSha: lineage.previousSha256,
|
|
1298
|
+
parentPid,
|
|
1299
|
+
restart: Boolean(restart),
|
|
1300
|
+
verify: Boolean(verify),
|
|
1301
|
+
spawnImpl,
|
|
1302
|
+
waitForReadyImpl,
|
|
1303
|
+
});
|
|
1304
|
+
rescueStagingPath = null;
|
|
1305
|
+
return result;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const result = applyPosixRescue({
|
|
1309
|
+
rescueStagingPath,
|
|
1310
|
+
layout,
|
|
1311
|
+
statePaths,
|
|
1312
|
+
updateLock,
|
|
1313
|
+
transactionId,
|
|
1314
|
+
rescueSha: lineage.previousSha256,
|
|
1315
|
+
hadTarget,
|
|
1316
|
+
targetBeforeSha,
|
|
1317
|
+
restart: Boolean(restart),
|
|
1318
|
+
verify: Boolean(verify),
|
|
1319
|
+
spawnImpl,
|
|
1320
|
+
verifyImpl,
|
|
1321
|
+
});
|
|
1322
|
+
rescueStagingPath = null;
|
|
1323
|
+
return result;
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (error?.code === "RESCUE_ROLLBACK_FAILED") {
|
|
1326
|
+
retainUpdateLockForRecovery(updateLock);
|
|
1327
|
+
}
|
|
1328
|
+
throw error;
|
|
1329
|
+
} finally {
|
|
1330
|
+
if (rescueStagingPath) removeRegularFileIfPresent(rescueStagingPath);
|
|
1331
|
+
if (!updateLock.transferred) releaseUpdateLock(updateLock);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function applyPosixRescue(ctx) {
|
|
1336
|
+
const {
|
|
1337
|
+
rescueStagingPath,
|
|
1338
|
+
layout,
|
|
1339
|
+
statePaths,
|
|
1340
|
+
updateLock,
|
|
1341
|
+
transactionId,
|
|
1342
|
+
rescueSha,
|
|
1343
|
+
hadTarget,
|
|
1344
|
+
targetBeforeSha,
|
|
1345
|
+
restart,
|
|
1346
|
+
verify,
|
|
1347
|
+
spawnImpl,
|
|
1348
|
+
verifyImpl,
|
|
1349
|
+
} = ctx;
|
|
1350
|
+
const targetPath = layout.canonicalPath;
|
|
1351
|
+
const currentSnapshotPath = uniqueSibling(targetPath, "rescue-current");
|
|
1352
|
+
if (hadTarget) {
|
|
1353
|
+
atomicCopyFile(targetPath, currentSnapshotPath, {
|
|
1354
|
+
preserveModeFrom: targetPath,
|
|
1355
|
+
updateLock,
|
|
1356
|
+
});
|
|
1357
|
+
assertExpectedFileHash(
|
|
1358
|
+
currentSnapshotPath,
|
|
1359
|
+
targetBeforeSha,
|
|
1360
|
+
"pre-rescue executable",
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
assertExpectedFileHash(
|
|
1365
|
+
rescueStagingPath,
|
|
1366
|
+
rescueSha,
|
|
1367
|
+
"rescue staging executable",
|
|
1368
|
+
);
|
|
1369
|
+
if (hadTarget) {
|
|
1370
|
+
assertExpectedFileHash(
|
|
1371
|
+
targetPath,
|
|
1372
|
+
targetBeforeSha,
|
|
1373
|
+
"canonical executable before rescue commit",
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
assertUpdateLockOwned(updateLock);
|
|
1378
|
+
fs.renameSync(rescueStagingPath, targetPath);
|
|
1379
|
+
fsyncDirectory(path.dirname(targetPath));
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
removeRegularFileIfPresent(currentSnapshotPath);
|
|
1382
|
+
throw new ApplyError(
|
|
1383
|
+
`rescue rename failed: ${error.message}`,
|
|
1384
|
+
"RESCUE_RENAME_FAILED",
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
let failure = null;
|
|
1389
|
+
try {
|
|
1390
|
+
assertExpectedFileHash(
|
|
1391
|
+
targetPath,
|
|
1392
|
+
rescueSha,
|
|
1393
|
+
"canonical executable after rescue commit",
|
|
1394
|
+
);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
failure = new ApplyError(
|
|
1397
|
+
`post-rescue SHA-256 verification failed: ${error.message}`,
|
|
1398
|
+
"POST_COMMIT_HASH_FAILED",
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
if (!failure && verify) {
|
|
1402
|
+
failure = verificationFailure(targetPath, verifyImpl);
|
|
1403
|
+
}
|
|
1404
|
+
if (!failure) {
|
|
1405
|
+
try {
|
|
1406
|
+
atomicWriteJson(
|
|
1407
|
+
statePaths.lineagePath,
|
|
1408
|
+
makeLineage({
|
|
1409
|
+
transactionId,
|
|
1410
|
+
operation: "rescue",
|
|
1411
|
+
currentSha256: rescueSha,
|
|
1412
|
+
previousSha256: rescueSha,
|
|
1413
|
+
}),
|
|
1414
|
+
updateLock,
|
|
1415
|
+
);
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
failure = new ApplyError(
|
|
1418
|
+
`could not persist rescue lineage: ${error.message}`,
|
|
1419
|
+
"LINEAGE_WRITE_FAILED",
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (failure) {
|
|
1425
|
+
let restored = false;
|
|
1426
|
+
try {
|
|
1427
|
+
if (hadTarget) {
|
|
1428
|
+
atomicCopyFile(currentSnapshotPath, targetPath, {
|
|
1429
|
+
preserveModeFrom: currentSnapshotPath,
|
|
1430
|
+
updateLock,
|
|
1431
|
+
});
|
|
1432
|
+
assertExpectedFileHash(
|
|
1433
|
+
targetPath,
|
|
1434
|
+
targetBeforeSha,
|
|
1435
|
+
"restored pre-rescue executable",
|
|
1436
|
+
);
|
|
1437
|
+
} else {
|
|
1438
|
+
assertUpdateLockOwned(updateLock);
|
|
1439
|
+
fs.renameSync(targetPath, uniqueSibling(targetPath, "failed-rescue"));
|
|
1440
|
+
}
|
|
1441
|
+
restored = true;
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
throw new ApplyError(
|
|
1444
|
+
`rescue verification failed and pre-rescue restore failed; recovery snapshot preserved at ${currentSnapshotPath}: ${error.message}`,
|
|
1445
|
+
"RESCUE_ROLLBACK_FAILED",
|
|
1446
|
+
);
|
|
1447
|
+
} finally {
|
|
1448
|
+
if (restored) {
|
|
1449
|
+
assertUpdateLockOwned(updateLock);
|
|
1450
|
+
removeRegularFileIfPresent(currentSnapshotPath);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
throw failure;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
assertUpdateLockOwned(updateLock);
|
|
1457
|
+
removeRegularFileIfPresent(currentSnapshotPath);
|
|
1458
|
+
restartVerifiedExecutable(targetPath, restart, spawnImpl);
|
|
1459
|
+
return buildPlan({
|
|
1460
|
+
platform: "posix",
|
|
1461
|
+
action: "rescue-in-place",
|
|
1462
|
+
layout,
|
|
1463
|
+
resolvedNewPath: rescueStagingPath,
|
|
1464
|
+
statePaths,
|
|
1465
|
+
restart,
|
|
1466
|
+
expectedSha: rescueSha,
|
|
1467
|
+
transactionId,
|
|
1468
|
+
sidecarPath: null,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function makeLineage({
|
|
1473
|
+
transactionId,
|
|
1474
|
+
operation,
|
|
1475
|
+
currentSha256,
|
|
1476
|
+
previousSha256,
|
|
1477
|
+
}) {
|
|
1478
|
+
return {
|
|
1479
|
+
schema: NATIVE_UPDATE_LINEAGE_SCHEMA,
|
|
1480
|
+
transactionId,
|
|
1481
|
+
operation,
|
|
1482
|
+
currentSha256,
|
|
1483
|
+
previousSha256,
|
|
1484
|
+
updatedAt: new Date().toISOString(),
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function buildPlan({
|
|
1489
|
+
platform,
|
|
1490
|
+
action,
|
|
1491
|
+
layout,
|
|
1492
|
+
resolvedNewPath,
|
|
1493
|
+
statePaths,
|
|
1494
|
+
restart,
|
|
1495
|
+
expectedSha,
|
|
1496
|
+
transactionId,
|
|
1497
|
+
sidecarPath,
|
|
1498
|
+
orphaned = [],
|
|
1499
|
+
}) {
|
|
1500
|
+
return {
|
|
1501
|
+
platform,
|
|
1502
|
+
action,
|
|
1503
|
+
requestedTargetExePath: layout.requestedPath,
|
|
1504
|
+
targetExePath: layout.canonicalPath,
|
|
1505
|
+
aliasPath: layout.aliasPath,
|
|
1506
|
+
newExePath: resolvedNewPath,
|
|
1507
|
+
sidecarPath,
|
|
1508
|
+
backupPath: statePaths.backupPath,
|
|
1509
|
+
lockPath: statePaths.lockPath,
|
|
1510
|
+
lineagePath: statePaths.lineagePath,
|
|
1511
|
+
resultPath: platform === "win32" ? statePaths.resultPath : null,
|
|
1512
|
+
expectedSha256: expectedSha,
|
|
1513
|
+
transactionId,
|
|
1514
|
+
orphaned,
|
|
1515
|
+
restartRequested: Boolean(restart),
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function validatePlatform(platform) {
|
|
1520
|
+
if (!["win32", "posix"].includes(platform)) {
|
|
1521
|
+
throw new ApplyError(
|
|
1522
|
+
`unsupported apply platform: ${platform}`,
|
|
1523
|
+
"BAD_PLATFORM",
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function resolveTrustedWindowsSystemRoot() {
|
|
1529
|
+
// Tests can force the Windows branch on another host, where the sidecar is
|
|
1530
|
+
// inspected but never executed. A real Windows apply resolves the kernel's
|
|
1531
|
+
// SystemRoot alias instead of trusting the caller-controlled environment.
|
|
1532
|
+
if (process.platform !== "win32") return String.raw`C:\Windows`;
|
|
1533
|
+
|
|
1534
|
+
let systemRoot;
|
|
1535
|
+
try {
|
|
1536
|
+
systemRoot = path.win32.resolve(
|
|
1537
|
+
fs.realpathSync.native(String.raw`\\?\GLOBALROOT\SystemRoot`),
|
|
1538
|
+
);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
throw new ApplyError(
|
|
1541
|
+
`could not resolve the trusted Windows system root: ${error.message}`,
|
|
1542
|
+
"WINDOWS_SYSTEM_ROOT_UNAVAILABLE",
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
if (!/^[A-Za-z]:\\/.test(systemRoot) || systemRoot.includes("\0")) {
|
|
1546
|
+
throw new ApplyError(
|
|
1547
|
+
"the trusted Windows system root resolved to an invalid path",
|
|
1548
|
+
"WINDOWS_SYSTEM_ROOT_UNAVAILABLE",
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
for (const relativePath of [
|
|
1553
|
+
["System32", "cmd.exe"],
|
|
1554
|
+
["System32", "tasklist.exe"],
|
|
1555
|
+
["System32", "findstr.exe"],
|
|
1556
|
+
["System32", "certutil.exe"],
|
|
1557
|
+
["System32", "chcp.com"],
|
|
1558
|
+
["System32", "ping.exe"],
|
|
1559
|
+
["System32", "WindowsPowerShell", "v1.0", "powershell.exe"],
|
|
1560
|
+
]) {
|
|
1561
|
+
const toolPath = path.win32.join(systemRoot, ...relativePath);
|
|
1562
|
+
try {
|
|
1563
|
+
assertStateSafeRegularFile(toolPath, {
|
|
1564
|
+
label: "trusted Windows update helper",
|
|
1565
|
+
allowMissingLeaf: false,
|
|
1566
|
+
platform: "win32",
|
|
1567
|
+
});
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
throw new ApplyError(
|
|
1570
|
+
`trusted Windows update helper is unavailable: ${toolPath}: ${error.message}`,
|
|
1571
|
+
"WINDOWS_SYSTEM_TOOL_UNAVAILABLE",
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return systemRoot;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function validateExpectedSha(value, label = "expectedSha256") {
|
|
1579
|
+
if (!SHA256_HEX.test(value || "")) {
|
|
1580
|
+
throw new ApplyError(
|
|
1581
|
+
`${label} must be a lowercase 64-character SHA-256`,
|
|
1582
|
+
"BAD_EXPECTED_SHA256",
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
return value;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function sameDirectory(firstPath, secondPath, platform = process.platform) {
|
|
1589
|
+
let first = path.dirname(path.resolve(firstPath));
|
|
1590
|
+
let second = path.dirname(path.resolve(secondPath));
|
|
1591
|
+
if (platform === "win32") {
|
|
1592
|
+
first = first.toLowerCase();
|
|
1593
|
+
second = second.toLowerCase();
|
|
1594
|
+
}
|
|
1595
|
+
return first === second;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function assertApplyPath(filePath, label, allowMissingLeaf, platform) {
|
|
1599
|
+
try {
|
|
1600
|
+
assertStateSafeRegularFile(filePath, {
|
|
1601
|
+
label,
|
|
1602
|
+
allowMissingLeaf,
|
|
1603
|
+
platform,
|
|
1604
|
+
});
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
const missingCodes = new Set(["PATH_MISSING", "ENOENT"]);
|
|
1607
|
+
const code = missingCodes.has(error?.code)
|
|
1608
|
+
? label === "last-known-good backup"
|
|
1609
|
+
? "BACKUP_MISSING"
|
|
1610
|
+
: label === "new executable"
|
|
1611
|
+
? "NEW_EXE_MISSING"
|
|
1612
|
+
: "PATH_MISSING"
|
|
1613
|
+
: "UNSAFE_PATH";
|
|
1614
|
+
throw new ApplyError(error.message, code);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function stableSha256(filePath, label) {
|
|
1619
|
+
assertApplyPath(filePath, label, false, process.platform);
|
|
1620
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
1621
|
+
let fd;
|
|
1622
|
+
try {
|
|
1623
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
throw new ApplyError(
|
|
1626
|
+
`could not open ${label} without following links: ${error.message}`,
|
|
1627
|
+
"UNSAFE_PATH",
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
try {
|
|
1631
|
+
// Windows file IDs routinely exceed Number.MAX_SAFE_INTEGER. Keep every
|
|
1632
|
+
// identity field as bigint so a pathname exchange cannot hide behind a
|
|
1633
|
+
// rounded inode/device value.
|
|
1634
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
1635
|
+
if (!before.isFile()) {
|
|
1636
|
+
throw new ApplyError(`${label} is not a regular file`, "UNSAFE_PATH");
|
|
1637
|
+
}
|
|
1638
|
+
const hasher = crypto.createHash("sha256");
|
|
1639
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
1640
|
+
let bytesRead;
|
|
1641
|
+
do {
|
|
1642
|
+
bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
1643
|
+
if (bytesRead > 0) hasher.update(buffer.subarray(0, bytesRead));
|
|
1644
|
+
} while (bytesRead > 0);
|
|
1645
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
1646
|
+
const identityChanged =
|
|
1647
|
+
!sameOpenedFileIdentity(before, after, ["size", "mtimeNs", "ctimeNs"]) ||
|
|
1648
|
+
!pathMatchesOpenedFileIdentitySync(filePath, after, {
|
|
1649
|
+
stateFields: ["size", "mtimeNs", "ctimeNs"],
|
|
1650
|
+
});
|
|
1651
|
+
if (identityChanged) {
|
|
1652
|
+
throw new ApplyError(
|
|
1653
|
+
`${label} changed while it was being verified`,
|
|
1654
|
+
"FILE_CHANGED_DURING_VERIFY",
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
return hasher.digest("hex");
|
|
1658
|
+
} finally {
|
|
1659
|
+
fs.closeSync(fd);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function assertExpectedFileHash(filePath, expectedSha, label) {
|
|
1664
|
+
const actual = stableSha256(filePath, label);
|
|
1665
|
+
if (actual !== expectedSha) {
|
|
1666
|
+
throw new ApplyError(
|
|
1667
|
+
`${label} SHA-256 mismatch: expected ${expectedSha}, got ${actual}`,
|
|
1668
|
+
"APPLY_SHA256_MISMATCH",
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
return actual;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function uniqueSibling(filePath, purpose) {
|
|
1675
|
+
return `${filePath}.${purpose}-${process.pid}-${crypto.randomUUID()}`;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function atomicCopyFile(sourcePath, destinationPath, options = {}) {
|
|
1679
|
+
assertApplyPath(sourcePath, "copy source", false, process.platform);
|
|
1680
|
+
assertApplyPath(destinationPath, "copy destination", true, process.platform);
|
|
1681
|
+
const stagingPath = uniqueSibling(destinationPath, "tmp");
|
|
1682
|
+
try {
|
|
1683
|
+
fs.copyFileSync(sourcePath, stagingPath, fs.constants.COPYFILE_EXCL);
|
|
1684
|
+
if (options.preserveModeFrom) {
|
|
1685
|
+
try {
|
|
1686
|
+
fs.chmodSync(stagingPath, fs.statSync(options.preserveModeFrom).mode);
|
|
1687
|
+
} catch {
|
|
1688
|
+
/* mode preservation is best effort on non-POSIX filesystems */
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
fsyncFile(stagingPath);
|
|
1692
|
+
if (options.updateLock) assertUpdateLockOwned(options.updateLock);
|
|
1693
|
+
fs.renameSync(stagingPath, destinationPath);
|
|
1694
|
+
fsyncDirectory(path.dirname(destinationPath));
|
|
1695
|
+
} finally {
|
|
1696
|
+
removeRegularFileIfPresent(stagingPath);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function atomicWriteJson(destinationPath, value, updateLock = null) {
|
|
1701
|
+
assertApplyPath(
|
|
1702
|
+
destinationPath,
|
|
1703
|
+
"native update state",
|
|
1704
|
+
true,
|
|
1705
|
+
process.platform,
|
|
1706
|
+
);
|
|
1707
|
+
const stagingPath = uniqueSibling(destinationPath, "tmp");
|
|
1708
|
+
try {
|
|
1709
|
+
fs.writeFileSync(stagingPath, `${JSON.stringify(value)}\n`, {
|
|
1710
|
+
encoding: "utf8",
|
|
1711
|
+
flag: "wx",
|
|
1712
|
+
mode: 0o600,
|
|
1713
|
+
});
|
|
1714
|
+
fsyncFile(stagingPath);
|
|
1715
|
+
if (updateLock) assertUpdateLockOwned(updateLock);
|
|
1716
|
+
fs.renameSync(stagingPath, destinationPath);
|
|
1717
|
+
fsyncDirectory(path.dirname(destinationPath));
|
|
1718
|
+
} finally {
|
|
1719
|
+
removeRegularFileIfPresent(stagingPath);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function quarantineFreshState(statePaths, transactionId, platform, updateLock) {
|
|
1724
|
+
const orphaned = [];
|
|
1725
|
+
for (const filePath of [statePaths.backupPath, statePaths.lineagePath]) {
|
|
1726
|
+
const stat = lstatOrNull(filePath);
|
|
1727
|
+
if (!stat) continue;
|
|
1728
|
+
assertApplyPath(filePath, "stale native update state", false, platform);
|
|
1729
|
+
const quarantinePath = `${filePath}.orphaned-${transactionId}`;
|
|
1730
|
+
assertApplyPath(
|
|
1731
|
+
quarantinePath,
|
|
1732
|
+
"quarantined native update state",
|
|
1733
|
+
true,
|
|
1734
|
+
platform,
|
|
1735
|
+
);
|
|
1736
|
+
assertUpdateLockOwned(updateLock);
|
|
1737
|
+
fs.renameSync(filePath, quarantinePath);
|
|
1738
|
+
fsyncDirectory(path.dirname(filePath));
|
|
1739
|
+
orphaned.push(quarantinePath);
|
|
1740
|
+
}
|
|
1741
|
+
return orphaned;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function verificationFailure(targetPath, verifyImpl) {
|
|
1745
|
+
let result;
|
|
1746
|
+
try {
|
|
1747
|
+
result = verifyImpl(targetPath, ["--version"], {
|
|
1748
|
+
encoding: "utf8",
|
|
1749
|
+
stdio: "pipe",
|
|
1750
|
+
timeout: 30_000,
|
|
1751
|
+
killSignal: "SIGKILL",
|
|
1752
|
+
origin: "packer:update-verify",
|
|
1753
|
+
scope: "pack-update",
|
|
1754
|
+
policy: "allow",
|
|
1755
|
+
shell: false,
|
|
1756
|
+
});
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
result = { status: 1, error };
|
|
1759
|
+
}
|
|
1760
|
+
return result?.error || result?.status !== 0
|
|
1761
|
+
? new ApplyError("new binary verification failed", "UPDATE_VERIFY_FAILED")
|
|
1762
|
+
: null;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function buildWindowsTimedStartupCheckLines() {
|
|
1766
|
+
const script = [
|
|
1767
|
+
"$ErrorActionPreference = 'Stop'",
|
|
1768
|
+
"$startInfo = [Diagnostics.ProcessStartInfo]::new()",
|
|
1769
|
+
"$startInfo.FileName = $env:VERIFY_PATH",
|
|
1770
|
+
"$startInfo.Arguments = '--version'",
|
|
1771
|
+
"$startInfo.UseShellExecute = $false",
|
|
1772
|
+
"$startInfo.CreateNoWindow = $true",
|
|
1773
|
+
"$process = [Diagnostics.Process]::new()",
|
|
1774
|
+
"$process.StartInfo = $startInfo",
|
|
1775
|
+
"try {",
|
|
1776
|
+
" if (-not $process.Start()) { exit 1 }",
|
|
1777
|
+
" if (-not $process.WaitForExit(30000)) {",
|
|
1778
|
+
" $killed = $false",
|
|
1779
|
+
" try { $process.Kill(); $killed = $true } catch {}",
|
|
1780
|
+
" if ($killed) { try { [void]$process.WaitForExit(5000) } catch {} }",
|
|
1781
|
+
" exit 124",
|
|
1782
|
+
" }",
|
|
1783
|
+
" exit $process.ExitCode",
|
|
1784
|
+
"} finally { $process.Dispose() }",
|
|
1785
|
+
].join("\r\n");
|
|
1786
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
1787
|
+
return [
|
|
1788
|
+
`"%CC_SYSTEM_ROOT%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -NoProfile -NonInteractive -EncodedCommand ${encoded} >NUL 2>&1`,
|
|
1789
|
+
"if errorlevel 1 goto verifyfailed",
|
|
1790
|
+
];
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function restartVerifiedExecutable(targetPath, restart, spawnImpl) {
|
|
1794
|
+
if (!restart) return;
|
|
1795
|
+
try {
|
|
1796
|
+
// An updater restart starts the newly installed CLI, not the command that
|
|
1797
|
+
// performed the update. Replaying `pack auto-update --current ...` can
|
|
1798
|
+
// otherwise form an update/restart loop, and Windows already restarts
|
|
1799
|
+
// without inherited arguments.
|
|
1800
|
+
const child = spawnImpl(targetPath, [], {
|
|
1801
|
+
origin: "packer:update-restart",
|
|
1802
|
+
scope: "pack-update",
|
|
1803
|
+
policy: "allow",
|
|
1804
|
+
shell: false,
|
|
1805
|
+
detached: true,
|
|
1806
|
+
stdio: "ignore",
|
|
1807
|
+
});
|
|
1808
|
+
if (child && typeof child.once === "function") {
|
|
1809
|
+
// Detached spawn errors arrive asynchronously and cannot be converted
|
|
1810
|
+
// into the already returned transaction result. Consume the event so a
|
|
1811
|
+
// restart failure does not crash an otherwise committed update.
|
|
1812
|
+
child.once("error", () => {});
|
|
1813
|
+
}
|
|
1814
|
+
if (child && typeof child.unref === "function") child.unref();
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
throw new ApplyError(
|
|
1817
|
+
`updated binary was verified but restart failed: ${error.message}`,
|
|
1818
|
+
"RESTART_FAILED",
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function waitForWindowsSidecarReady({ readyPath, transactionId, timeoutMs }) {
|
|
1824
|
+
const deadline = Date.now() + timeoutMs;
|
|
1825
|
+
const waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
1826
|
+
while (Date.now() < deadline) {
|
|
1827
|
+
const stat = lstatOrNull(readyPath);
|
|
1828
|
+
if (stat) {
|
|
1829
|
+
try {
|
|
1830
|
+
assertStateSafeRegularFile(readyPath, {
|
|
1831
|
+
label: "Windows update sidecar ready marker",
|
|
1832
|
+
allowMissingLeaf: false,
|
|
1833
|
+
platform: "win32",
|
|
1834
|
+
});
|
|
1835
|
+
return fs.readFileSync(readyPath, "utf8").trim() === transactionId;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
Atomics.wait(waitCell, 0, 0, 25);
|
|
1841
|
+
}
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function acquireUpdateLock(lockPath, platform) {
|
|
1846
|
+
assertApplyPath(lockPath, "native update lock", true, platform);
|
|
1847
|
+
const token = `${process.pid}:${crypto.randomUUID()}`;
|
|
1848
|
+
let fd;
|
|
1849
|
+
try {
|
|
1850
|
+
fd = fs.openSync(lockPath, "wx+", 0o600);
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
if (error?.code === "EEXIST") {
|
|
1853
|
+
throw new ApplyError(
|
|
1854
|
+
`another native install/update is in progress: ${lockPath}`,
|
|
1855
|
+
"UPDATE_LOCKED",
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
throw new ApplyError(
|
|
1859
|
+
`could not acquire native update lock: ${error.message}`,
|
|
1860
|
+
"LOCK_FAILED",
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
try {
|
|
1864
|
+
fs.writeFileSync(fd, token, { encoding: "utf8" });
|
|
1865
|
+
fs.fsyncSync(fd);
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
releaseUpdateLock({ fd, lockPath, token, transferred: false });
|
|
1868
|
+
throw new ApplyError(
|
|
1869
|
+
`could not initialize native update lock: ${error.message}`,
|
|
1870
|
+
"LOCK_FAILED",
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
return { fd, lockPath, token, transferred: false };
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function assertUpdateLockOwned(lock) {
|
|
1877
|
+
if (!lock || lock.fd === null || lock.fd === undefined) {
|
|
1878
|
+
throw new ApplyError(
|
|
1879
|
+
"native update lock handle is no longer held by this transaction",
|
|
1880
|
+
"UPDATE_LOCK_LOST",
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
try {
|
|
1884
|
+
const before = fs.fstatSync(lock.fd, { bigint: true });
|
|
1885
|
+
const pathOwnedBefore = pathMatchesOpenedFileIdentitySync(
|
|
1886
|
+
lock.lockPath,
|
|
1887
|
+
before,
|
|
1888
|
+
{ stateFields: ["size", "mtimeNs", "ctimeNs"] },
|
|
1889
|
+
);
|
|
1890
|
+
const expected = Buffer.from(lock.token, "utf8");
|
|
1891
|
+
const actual = Buffer.alloc(expected.length + 1);
|
|
1892
|
+
const bytesRead = fs.readSync(lock.fd, actual, 0, actual.length, 0);
|
|
1893
|
+
const after = fs.fstatSync(lock.fd, { bigint: true });
|
|
1894
|
+
const pathOwnedAfter = pathMatchesOpenedFileIdentitySync(
|
|
1895
|
+
lock.lockPath,
|
|
1896
|
+
after,
|
|
1897
|
+
{ stateFields: ["size", "mtimeNs", "ctimeNs"] },
|
|
1898
|
+
);
|
|
1899
|
+
const descriptorStable =
|
|
1900
|
+
before.isFile() &&
|
|
1901
|
+
after.isFile() &&
|
|
1902
|
+
before.nlink > 0n &&
|
|
1903
|
+
after.nlink > 0n &&
|
|
1904
|
+
sameOpenedFileIdentity(before, after, ["size", "mtimeNs", "ctimeNs"]);
|
|
1905
|
+
const pathStillOwned = pathOwnedBefore && pathOwnedAfter;
|
|
1906
|
+
const tokenMatches =
|
|
1907
|
+
bytesRead === expected.length &&
|
|
1908
|
+
actual.subarray(0, bytesRead).equals(expected);
|
|
1909
|
+
if (!descriptorStable || !pathStillOwned || !tokenMatches) {
|
|
1910
|
+
throw new Error("lock pathname, descriptor, or token changed");
|
|
1911
|
+
}
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
throw new ApplyError(
|
|
1914
|
+
`native update lock ownership was lost: ${error.message}`,
|
|
1915
|
+
"UPDATE_LOCK_LOST",
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function closeUpdateLockHandle(lock) {
|
|
1921
|
+
if (!lock || lock.fd === null) return;
|
|
1922
|
+
try {
|
|
1923
|
+
fs.closeSync(lock.fd);
|
|
1924
|
+
} finally {
|
|
1925
|
+
lock.fd = null;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function retainUpdateLockForRecovery(lock) {
|
|
1930
|
+
closeUpdateLockHandle(lock);
|
|
1931
|
+
lock.transferred = true;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function removeOwnedLockFile(lock) {
|
|
1935
|
+
if (!lock) return;
|
|
1936
|
+
try {
|
|
1937
|
+
const stat = fs.lstatSync(lock.lockPath);
|
|
1938
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return;
|
|
1939
|
+
if (fs.readFileSync(lock.lockPath, "utf8") === lock.token) {
|
|
1940
|
+
fs.unlinkSync(lock.lockPath);
|
|
1941
|
+
}
|
|
1942
|
+
} catch {
|
|
1943
|
+
/* stale or replaced locks fail closed */
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function releaseUpdateLock(lock) {
|
|
1948
|
+
closeUpdateLockHandle(lock);
|
|
1949
|
+
removeOwnedLockFile(lock);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
function removeRegularFileIfPresent(filePath) {
|
|
1953
|
+
if (!filePath) return;
|
|
1954
|
+
try {
|
|
1955
|
+
const stat = fs.lstatSync(filePath);
|
|
1956
|
+
if (stat.isFile() && !stat.isSymbolicLink()) fs.unlinkSync(filePath);
|
|
1957
|
+
} catch {
|
|
1958
|
+
/* missing or unsafe paths are intentionally left alone */
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function fsyncFile(filePath) {
|
|
1963
|
+
const fd = fs.openSync(filePath, "r");
|
|
1964
|
+
try {
|
|
1965
|
+
try {
|
|
1966
|
+
fs.fsyncSync(fd);
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
if (!["EBADF", "EINVAL", "ENOTSUP", "EPERM"].includes(error?.code)) {
|
|
1969
|
+
throw error;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
} finally {
|
|
1973
|
+
fs.closeSync(fd);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function fsyncDirectory(directoryPath) {
|
|
1978
|
+
let fd;
|
|
1979
|
+
try {
|
|
1980
|
+
fd = fs.openSync(directoryPath, "r");
|
|
1981
|
+
fs.fsyncSync(fd);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
if (
|
|
1984
|
+
!["EACCES", "EBADF", "EINVAL", "EISDIR", "EPERM"].includes(error?.code)
|
|
1985
|
+
) {
|
|
1986
|
+
throw error;
|
|
1987
|
+
}
|
|
1988
|
+
} finally {
|
|
1989
|
+
if (fd !== undefined) {
|
|
1990
|
+
try {
|
|
1991
|
+
fs.closeSync(fd);
|
|
1992
|
+
} catch {
|
|
1993
|
+
/* best effort */
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function asApplyError(error, code, prefix = "") {
|
|
2000
|
+
if (error instanceof ApplyError) return error;
|
|
2001
|
+
return new ApplyError(`${prefix ? `${prefix}: ` : ""}${error.message}`, code);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function assertWindowsCmdSafePath(value, label) {
|
|
2005
|
+
if (typeof value !== "string" || !value) {
|
|
2006
|
+
throw new ApplyError(`${label} is required`, "UNSAFE_WINDOWS_PATH");
|
|
2007
|
+
}
|
|
2008
|
+
if (/[\r\n%!"^&|<>()]/.test(value)) {
|
|
2009
|
+
throw new ApplyError(
|
|
2010
|
+
`${label} contains characters unsafe for a cmd sidecar`,
|
|
2011
|
+
"UNSAFE_WINDOWS_PATH",
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function buildWindowsReparseChecks(paths) {
|
|
2017
|
+
const seen = new Set();
|
|
2018
|
+
const ordered = [];
|
|
2019
|
+
for (const rawPath of paths) {
|
|
2020
|
+
let current = path.resolve(rawPath);
|
|
2021
|
+
while (current) {
|
|
2022
|
+
const key = current.toLowerCase();
|
|
2023
|
+
if (!seen.has(key)) {
|
|
2024
|
+
seen.add(key);
|
|
2025
|
+
ordered.push(current);
|
|
2026
|
+
}
|
|
2027
|
+
const parent = path.dirname(current);
|
|
2028
|
+
if (!parent || parent === current) break;
|
|
2029
|
+
current = parent;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
// One encoded PowerShell invocation is both substantially faster than
|
|
2033
|
+
// starting fsutil once per component and immune to cmd interpolation of the
|
|
2034
|
+
// inspected paths. Missing leaves are allowed; every other inspection error
|
|
2035
|
+
// fails closed.
|
|
2036
|
+
const literals = ordered
|
|
2037
|
+
.map((candidate) => `'${candidate.replaceAll("'", "''")}'`)
|
|
2038
|
+
.join(",");
|
|
2039
|
+
const script = [
|
|
2040
|
+
"$ErrorActionPreference = 'Stop'",
|
|
2041
|
+
`foreach ($candidate in @(${literals})) {`,
|
|
2042
|
+
" try { $attributes = [IO.File]::GetAttributes($candidate) }",
|
|
2043
|
+
" catch [IO.FileNotFoundException] { continue }",
|
|
2044
|
+
" catch [IO.DirectoryNotFoundException] { continue }",
|
|
2045
|
+
" if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { exit 7 }",
|
|
2046
|
+
"}",
|
|
2047
|
+
"exit 0",
|
|
2048
|
+
].join("\r\n");
|
|
2049
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
2050
|
+
return [
|
|
2051
|
+
`"%CC_SYSTEM_ROOT%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -NoProfile -NonInteractive -EncodedCommand ${encoded} >NUL 2>&1`,
|
|
2052
|
+
"if errorlevel 1 goto unsafepath",
|
|
2053
|
+
];
|
|
2054
|
+
}
|
|
2055
|
+
|
|
209
2056
|
export class ApplyError extends Error {
|
|
210
2057
|
constructor(message, code) {
|
|
211
2058
|
super(message);
|