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.
Files changed (337) hide show
  1. package/README.md +61 -11
  2. package/bin/chainlesschain.js +7 -52
  3. package/completions/_cc +209 -0
  4. package/completions/cc.bash +18 -0
  5. package/completions/cc.fish +13 -0
  6. package/completions/cc.ps1 +226 -0
  7. package/package.json +10 -2
  8. package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
  9. package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
  11. package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
  12. package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
  14. package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
  16. package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
  18. package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
  19. package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
  20. package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
  21. package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
  22. package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
  24. package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
  25. package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
  26. package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
  27. package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
  28. package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
  30. package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
  31. package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
  32. package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
  33. package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
  34. package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
  35. package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
  37. package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
  38. package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
  39. package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
  40. package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
  41. package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
  42. package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
  43. package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
  44. package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
  45. package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
  46. package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
  47. package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
  48. package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
  49. package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
  50. package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
  51. package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
  52. package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
  53. package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
  54. package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
  55. package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
  56. package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
  57. package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
  58. package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
  59. package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
  60. package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
  61. package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
  62. package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
  63. package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
  64. package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
  66. package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
  67. package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
  68. package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
  69. package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
  70. package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
  71. package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
  72. package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
  73. package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
  74. package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
  75. package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
  76. package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
  77. package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
  78. package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
  79. package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
  80. package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
  81. package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
  84. package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
  85. package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
  86. package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
  88. package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
  89. package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
  90. package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
  91. package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
  92. package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
  93. package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
  94. package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
  133. package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
  134. package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
  135. package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
  140. package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
  141. package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
  142. package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
  143. package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
  144. package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
  145. package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
  146. package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
  147. package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
  148. package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
  149. package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
  151. package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
  153. package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
  154. package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
  155. package/src/assets/web-panel/index.html +1 -1
  156. package/src/cli-help.js +181 -0
  157. package/src/command-help-index.json +181 -0
  158. package/src/command-manifest.json +1107 -175
  159. package/src/command-surface-policy.js +197 -0
  160. package/src/commands/agent.js +98 -33
  161. package/src/commands/artifacts.js +400 -2
  162. package/src/commands/background-session.js +41 -3
  163. package/src/commands/checkpoint-managed.js +1059 -0
  164. package/src/commands/checkpoint-restore-recovery.js +1476 -0
  165. package/src/commands/checkpoint.js +950 -22
  166. package/src/commands/compact.js +101 -25
  167. package/src/commands/config.js +506 -20
  168. package/src/commands/doctor.js +4 -1
  169. package/src/commands/hub.js +1 -1
  170. package/src/commands/mcp.js +688 -24
  171. package/src/commands/pack.js +16 -2
  172. package/src/commands/plugin.js +20 -12
  173. package/src/commands/session-mcp-recovery.js +218 -0
  174. package/src/commands/session.js +470 -110
  175. package/src/commands/setup.js +36 -10
  176. package/src/commands/skill.js +181 -110
  177. package/src/commands/status.js +21 -5
  178. package/src/commands/team-distributed.js +3324 -0
  179. package/src/commands/team.js +1130 -74
  180. package/src/commands/update.js +68 -6
  181. package/src/constants.js +11 -0
  182. package/src/data/changelog.json +26 -0
  183. package/src/gateways/ws/action-protocol.js +10 -0
  184. package/src/gateways/ws/session-protocol.js +555 -27
  185. package/src/gateways/ws/ws-agent-handler.js +541 -14
  186. package/src/gateways/ws/ws-session-gateway.js +129 -22
  187. package/src/gateways/ws/ws-session-state.js +431 -0
  188. package/src/harness/background-task-manager.js +233 -36
  189. package/src/harness/jsonl-session-store.js +2551 -167
  190. package/src/harness/mcp-client.js +802 -61
  191. package/src/harness/prompt-compressor.js +227 -27
  192. package/src/harness/session-index.js +89 -38
  193. package/src/harness/session-list-index.js +254 -0
  194. package/src/harness/structured-handoff.js +410 -0
  195. package/src/lazy-dispatch.js +747 -84
  196. package/src/lib/__tests__/logger.test.js +50 -2
  197. package/src/lib/advisor-runtime.js +1047 -0
  198. package/src/lib/agent-core.js +2 -0
  199. package/src/lib/agent-sandbox.js +120 -26
  200. package/src/lib/agent-session-export.js +20 -0
  201. package/src/lib/agent-team/task-lease.js +271 -3
  202. package/src/lib/agent-team/team-adjudication.js +1501 -0
  203. package/src/lib/agent-team/team-budget.js +4 -1
  204. package/src/lib/agent-team/team-control-store.js +582 -0
  205. package/src/lib/agent-team/team-distributed-queue.js +5246 -0
  206. package/src/lib/agent-team/team-process-checkpoint.js +211 -0
  207. package/src/lib/agent-team/team-runner.js +543 -24
  208. package/src/lib/agent-team/team-worktree.js +1403 -95
  209. package/src/lib/artifact-store.js +131 -8
  210. package/src/lib/background-agent-supervisor.js +185 -13
  211. package/src/lib/background-interaction-journal.js +37 -9
  212. package/src/lib/background-launch-profile.js +1058 -0
  213. package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
  214. package/src/lib/checkpoint-restore-orchestrator.js +987 -0
  215. package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
  216. package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
  217. package/src/lib/checkpoint-restore-recovery.js +884 -0
  218. package/src/lib/checkpoint-restore-retention.js +126 -0
  219. package/src/lib/checkpoint-restore-saga.js +5079 -0
  220. package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
  221. package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
  222. package/src/lib/checkpoint-store.js +1688 -97
  223. package/src/lib/checkpoint-timeline-authority.js +522 -0
  224. package/src/lib/checkpoint-timeline.js +642 -0
  225. package/src/lib/config-keys.js +18 -119
  226. package/src/lib/config-manager.js +446 -27
  227. package/src/lib/config-redaction.js +57 -0
  228. package/src/lib/config-schema.js +853 -0
  229. package/src/lib/cowork-mcp-tools.js +5 -0
  230. package/src/lib/cowork-task-runner.js +458 -63
  231. package/src/lib/delivery-coordinator.js +1234 -0
  232. package/src/lib/delivery-evidence.js +450 -0
  233. package/src/lib/doctor-checkup.js +199 -14
  234. package/src/lib/executable-path.js +34 -0
  235. package/src/lib/fatal-handler.js +16 -0
  236. package/src/lib/file-checkpoint.js +4909 -186
  237. package/src/lib/file-lines.js +125 -0
  238. package/src/lib/headless-config-command.js +16 -5
  239. package/src/lib/hook-runner.cjs +139 -38
  240. package/src/lib/hooks-v2-workspace-context.js +10 -0
  241. package/src/lib/impacted-gate-selector.js +424 -0
  242. package/src/lib/interaction-adapter.js +62 -0
  243. package/src/lib/jsonl-session-store.js +1 -35
  244. package/src/lib/logger.js +66 -14
  245. package/src/lib/managed-checkpoint-render.js +60 -0
  246. package/src/lib/managed-tool-checkpoint.js +194 -0
  247. package/src/lib/mcp-call-ledger-store.js +1069 -0
  248. package/src/lib/mcp-call-ledger.js +747 -0
  249. package/src/lib/mcp-client.js +5 -0
  250. package/src/lib/mcp-conflict-scheduler.js +468 -0
  251. package/src/lib/mcp-effect-contract.js +102 -0
  252. package/src/lib/mcp-headers-helper-trust.js +145 -0
  253. package/src/lib/mcp-headers-helper.js +616 -0
  254. package/src/lib/mcp-host-recovery-runtime.js +131 -0
  255. package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
  256. package/src/lib/mcp-recovery-adjudication.js +443 -0
  257. package/src/lib/output-context.js +86 -0
  258. package/src/lib/packer/file-identity.js +170 -0
  259. package/src/lib/packer/index.js +3 -0
  260. package/src/lib/packer/native-update-state.js +277 -0
  261. package/src/lib/packer/pack-update-applier.js +1983 -136
  262. package/src/lib/packer/pack-update-checker.js +34 -0
  263. package/src/lib/packer/pack-update-downloader.js +875 -86
  264. package/src/lib/packer/pack-update-signature.js +86 -0
  265. package/src/lib/packer/pkg-config-generator.js +6 -0
  266. package/src/lib/paths.js +65 -10
  267. package/src/lib/plan-mode.js +831 -14
  268. package/src/lib/plan-persistence.js +540 -0
  269. package/src/lib/plugin-runtime/hooks.js +227 -69
  270. package/src/lib/plugin-runtime/plugin-options.js +189 -44
  271. package/src/lib/pr-automation-policy.js +146 -8
  272. package/src/lib/process-execution-broker/credential-agent.js +7 -0
  273. package/src/lib/process-execution-broker/index.js +491 -13
  274. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
  275. package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
  276. package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
  277. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  278. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  279. package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
  280. package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
  281. package/src/lib/project-instructions.js +165 -29
  282. package/src/lib/project-mcp-trust.js +67 -0
  283. package/src/lib/public-ide-capability-manifest.js +272 -0
  284. package/src/lib/public-ide-capability-validation.js +351 -0
  285. package/src/lib/secret-store.js +267 -43
  286. package/src/lib/secure-file-identity.js +433 -0
  287. package/src/lib/secure-fs.js +1089 -0
  288. package/src/lib/security-audit.js +58 -0
  289. package/src/lib/service-manager.js +34 -13
  290. package/src/lib/session-budget-runtime.js +1452 -0
  291. package/src/lib/session-host-snapshot.js +388 -0
  292. package/src/lib/session-message-provenance.js +429 -0
  293. package/src/lib/session-projection.js +804 -0
  294. package/src/lib/session-resource-budget.js +1495 -0
  295. package/src/lib/settings-hooks.cjs +138 -22
  296. package/src/lib/settings-loader.cjs +25 -2
  297. package/src/lib/side-effect-ledger-store.js +45 -23
  298. package/src/lib/skill-execution-identity.js +217 -0
  299. package/src/lib/skill-loader.js +299 -18
  300. package/src/lib/sub-agent-context.js +236 -28
  301. package/src/lib/sub-agent-registry.js +5 -0
  302. package/src/lib/subagent-contract.js +94 -0
  303. package/src/lib/todo-manager.js +185 -18
  304. package/src/lib/todo-persistence.js +417 -0
  305. package/src/lib/turn-binding-store.js +41 -23
  306. package/src/lib/turn-binding.js +160 -2
  307. package/src/lib/with-file-lock.js +165 -24
  308. package/src/lib/ws-chat-handler.js +21 -0
  309. package/src/program-base.js +18 -0
  310. package/src/repl/advisor-command.js +103 -0
  311. package/src/repl/agent-repl.js +2175 -146
  312. package/src/repl/btw-command.js +93 -10
  313. package/src/repl/clipboard-image.js +119 -0
  314. package/src/repl/config-summary.js +64 -108
  315. package/src/repl/prompt-editor.js +233 -0
  316. package/src/repl/prompt-interactions.js +344 -0
  317. package/src/repl/prompt-stash.js +354 -0
  318. package/src/repl/prompt-suggestions.js +312 -0
  319. package/src/repl/repl-keybindings.js +227 -0
  320. package/src/repl/session-recap.js +217 -0
  321. package/src/repl/slash-command-registry.js +22 -0
  322. package/src/repl/terminal-layout.js +88 -0
  323. package/src/runtime/agent-core.js +1630 -447
  324. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  325. package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
  326. package/src/runtime/diagnostics.js +32 -13
  327. package/src/runtime/headless-runner.js +310 -118
  328. package/src/runtime/headless-stream.js +356 -95
  329. package/src/runtime/mcp-config.js +148 -14
  330. package/src/runtime/policies/agent-policy.js +14 -0
  331. package/src/runtime/status-diagnostics-lite.js +137 -0
  332. package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
  333. package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
  334. package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
  335. package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
  336. package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
  337. package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
@@ -1,211 +1,2058 @@
1
1
  /**
2
- * Phase 5c: replace the currently-running packed exe with a downloaded new
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
- * The two platform flavors behave very differently:
7
- *
8
- * POSIX (Linux / macOS): the kernel keeps the running exe's inode open
9
- * independently of its directory entry. We can `rename(new, target)`
10
- * while the old process is still executing — the new bytes are on disk
11
- * under the target path, the old process continues running the old
12
- * inode until it exits, and a fresh spawn of `target` picks up the new
13
- * code. Clean, atomic, no sidecar.
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
- spawn: (...args) => executionBroker.spawn(...args),
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
- if (!fs.existsSync(newExePath)) {
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
- `new exe does not exist: ${newExePath}`,
74
- "NEW_EXE_MISSING",
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
- targetExePath,
83
- newExePath,
114
+ layout,
115
+ resolvedNewPath,
116
+ statePaths,
117
+ restart,
118
+ expectedSha,
119
+ transactionId: null,
84
120
  sidecarPath: null,
85
- restartRequested: Boolean(restart),
86
- };
121
+ });
87
122
  }
88
123
 
89
- if (platform === "win32") {
90
- const sidecarPath = writeWindowsSidecar({
91
- newExePath,
92
- targetExePath,
93
- parentPid,
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
- // Detach so the sidecar survives our process exit. `windowsHide: true`
97
- // keeps the cmd window from flashing — the replace itself is silent.
98
- const child = spawnImpl("cmd.exe", ["/c", sidecarPath], {
99
- origin: "packer:update-sidecar",
100
- scope: "pack-update",
101
- policy: "allow",
102
- shell: false,
103
- detached: true,
104
- stdio: "ignore",
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
- return {
110
- platform,
111
- action: "sidecar-cmd",
112
- targetExePath,
113
- newExePath,
114
- sidecarPath,
115
- restartRequested: Boolean(restart),
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(newExePath, 0o755);
277
+ fs.chmodSync(resolvedNewPath, 0o755);
123
278
  } catch {
124
- /* best effort — non-fatal if chmod fails (e.g. on FAT32 volumes) */
279
+ /* verification remains authoritative on filesystems without POSIX modes */
125
280
  }
281
+
126
282
  try {
127
- fs.renameSync(newExePath, targetExePath);
128
- } catch (err) {
129
- throw new ApplyError(`rename failed: ${err.message}`, "RENAME_FAILED");
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
- if (restart) {
133
- const child = spawnImpl(targetExePath, process.argv.slice(2), {
134
- origin: "packer:update-restart",
135
- scope: "pack-update",
136
- policy: "allow",
137
- shell: false,
138
- detached: true,
139
- stdio: "ignore",
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
- if (child && typeof child.unref === "function") child.unref();
367
+ removeRegularFileIfPresent(backupStagingPath);
368
+ throw commitFailure;
142
369
  }
143
370
 
144
- return {
145
- platform,
371
+ restartVerifiedExecutable(targetPath, restart, spawnImpl);
372
+ return buildPlan({
373
+ platform: "posix",
146
374
  action: "replace-in-place",
147
- targetExePath,
148
- newExePath,
375
+ layout,
376
+ resolvedNewPath,
377
+ statePaths,
378
+ restart,
379
+ expectedSha,
380
+ transactionId,
149
381
  sidecarPath: null,
150
- restartRequested: Boolean(restart),
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
- * Build the Windows sidecar `.cmd` that waits for the parent, moves the new
156
- * exe over the target, and optionally restarts. Exposed (rather than inlined)
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 { newExePath, targetExePath, parentPid, restart } = ctx;
168
-
169
- // `%TEMP%` is writable, survives across Explorer double-click launches, and
170
- // gets auto-cleaned by Windows eventually. The unique name prevents two
171
- // concurrent applies from clobbering each other.
172
- const sidecarName = `cc-pack-apply-${Date.now()}-${Math.floor(Math.random() * 1e6)}.cmd`;
173
- const sidecarPath = path.join(os.tmpdir(), sidecarName);
174
-
175
- // tasklist /FI "PID eq <pid>" prints "INFO: No tasks..." when the PID is
176
- // gone. We loop every 500ms up to ~10s; that's long enough for the parent
177
- // to close its Electron/UI server gracefully but short enough to avoid
178
- // stalling forever if the detection oddly fails.
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 PARENT_PID=${parentPid}`,
183
- `set NEW_EXE="${newExePath}"`,
184
- `set TARGET_EXE="${targetExePath}"`,
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
- 'tasklist /FI "PID eq %PARENT_PID%" 2>NUL | find /I "%PARENT_PID%" >NUL',
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 20 goto doreplace",
191
- // Timeout with /T /NOBREAK is the only idle-wait available in pure cmd.
192
- "timeout /T 1 /NOBREAK >NUL",
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
- "move /Y %NEW_EXE% %TARGET_EXE% >NUL",
196
- "if errorlevel 1 (",
197
- " echo cc-pack-apply: move failed & exit /b 1",
198
- ")",
199
- restart ? 'start "" %TARGET_EXE%' : "REM restart not requested",
200
- // Self-delete — best effort. Leaving the .cmd in %TEMP% is harmless if
201
- // this fails; Windows will reap it on the next disk-cleanup cycle.
202
- '(goto) 2>NUL & del "%~f0"',
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, { encoding: "utf-8" });
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);