chainlesschain 0.162.95 → 0.162.96

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 (217) hide show
  1. package/README.md +1 -1
  2. package/bin/chainlesschain.js +8 -18
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-Bo9Ux24z.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-CExgXSQT.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-BNnZhRTZ.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-D9xfqFCT.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-Cpgu8uCw.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CHdEJnNE.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-Cbw-PxBq.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-ChHTZK_6.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-Dl6z6ZH7.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Cj8d9bru.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-C2YobW4j.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-CxfV5FGT.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-CiwOASMj.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CTg_2iKW.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-HMbsTmtw.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-DyNDqGR2.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-9-x5M7qy.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-4msgDaYJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID--2Beb5Po.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-T6nq0Lzs.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-D_Q6T10v.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-TnN21bJx.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-kky_uIij.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-C1GkNY4B.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-5-_Fc5YV.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-CmHiB7sr.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-Cuci_BPI.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-BU0cO2LD.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-BDFNgEcx.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-DITXUzHz.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Ea-07CRd.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-D6OBbRVG.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-eiKpIJMi.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-M-X-T8UF.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-xcZb9Ekv.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-DCfoTPRC.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-BWfGDzD8.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-imBuld-U.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C2kcsq7I.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-DObvXSXP.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-C-6OGJMe.js} +4 -4
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-ub7Ozxci.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-CR2W89Vk.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-DNA_Wty7.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-rT4Wb4rQ.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CK-hARKv.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-C2pbtNbT.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-B8EIEqGJ.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-BxNVNU3s.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-CT9pbkyY.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-DhCNd2CS.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-Qngnx-vQ.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Dfn-nQc1.js} +2 -2
  57. package/src/assets/web-panel/assets/Projects-BKdiSaPR.js +1 -0
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-4o9B_n-X.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-bs1qvF30.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-CuEnKA2K.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-DbfvmRb2.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-DvMJGAI3.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-oJ0BZvmA.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DnIVrY9O.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-CIWhI_Tc.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-DLrnpjwm.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CYmqgtVb.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-BUzCkoeu.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DYRE4bnS.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-_6MurdOg.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-UIdnF5Li.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-COx2CjT8.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-BcHAmkZo.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-CjYQS8yZ.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-BmfOPcWt.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-D5HuarNQ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-C_fbProA.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-JPulpRx8.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-CnO-062O.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-HpNovvau.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-yMncxH7y.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-Cug-t7Np.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn--U1-NR-3.js} +2 -2
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-R-XvSJ3S.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CCI1i6Rp.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-C-mqoaLt.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-De_N2iRR.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-DbTM6sjZ.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-OcLPHSl5.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-D0Adylyc.js} +1 -1
  91. package/src/assets/web-panel/assets/index-7E_mHkDU.js +1 -0
  92. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-AY937BxS.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B6hgy5VC.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-B7XnpLHk.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-B7gw-tDm.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-BBgDgkwW.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BMmeiw88.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BU6XtnSx.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-Bi7VUG0R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-BtE3cw9m.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-C9ej-MF0.js} +3 -3
  102. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-CIEiQl1j.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CKgaW9E5.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-CY0KKR5f.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CbV3-WlB.js +1 -0
  106. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-Co1UkcFX.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CzP5d5LW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-D0NJ5lRi.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-D0gBsqTX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-D5F6shtn.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-DVtamtlm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-DWT18hDG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DaXb4B9P.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-DjQA0Vez.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-Ds8NJVEL.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-DuP-8HWu.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-DwLTiJ31.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-T3VhH8cO.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-TDY1o0w3.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-TRy2Gc3V.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-W1OMxCmI.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-ftFRZbuc.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-iGPGaORA.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-jhy5KQvg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-qUSZJcgw.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-w3f6lqkD.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-xdMc71Kh.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-xhX5eg2Z.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-zkTiW6d6.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-CIHqouWk.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-D1iUKftk.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-YOpWjmjL.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-C_1357UY.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-CDb4dq6o.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-D-OGYyhd.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve-hS-j69A0.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-tnjuvphU.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-BERfMMcU.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-DKpl2K9E.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-C_sX4D3N.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-DBcxC1vK.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-CeoDf2XI.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-DC9Pmj_u.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-CZA58J7M.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +19 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/ask.js +13 -0
  149. package/src/commands/config.js +17 -1
  150. package/src/commands/cost.js +6 -1
  151. package/src/commands/dao.js +16 -3
  152. package/src/commands/dlp.js +7 -1
  153. package/src/commands/hub.js +13 -0
  154. package/src/commands/mtc.js +37 -1
  155. package/src/gateways/discord/discord-formatter.js +18 -1
  156. package/src/gateways/telegram/telegram-formatter.js +6 -3
  157. package/src/gateways/ws/ws-server.js +19 -1
  158. package/src/harness/plugin-manager.js +20 -0
  159. package/src/harness/worktree-isolator.js +17 -1
  160. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  161. package/src/lib/a2a-protocol.js +23 -9
  162. package/src/lib/agent-core.js +2 -5
  163. package/src/lib/audit-mtc.js +40 -2
  164. package/src/lib/chat-core.js +141 -18
  165. package/src/lib/cli-anything-bridge.js +5 -0
  166. package/src/lib/cli-numeric.js +38 -0
  167. package/src/lib/config-manager.js +37 -6
  168. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  169. package/src/lib/cowork-cron.js +45 -34
  170. package/src/lib/cowork-learning.js +31 -1
  171. package/src/lib/cowork-share.js +39 -3
  172. package/src/lib/cowork-template-marketplace.js +54 -2
  173. package/src/lib/cowork-workflow.js +33 -6
  174. package/src/lib/cross-chain-mtc.js +41 -12
  175. package/src/lib/crypto-manager.js +12 -2
  176. package/src/lib/dbevo.js +19 -8
  177. package/src/lib/downloader.js +24 -9
  178. package/src/lib/evomap-governance.js +4 -4
  179. package/src/lib/evomap-manager.js +54 -7
  180. package/src/lib/fatal-handler.js +48 -0
  181. package/src/lib/file-checkpoint.js +26 -0
  182. package/src/lib/git-integration.js +28 -0
  183. package/src/lib/goal-store.js +36 -10
  184. package/src/lib/hierarchical-memory.js +6 -0
  185. package/src/lib/llm-pricing.js +52 -2
  186. package/src/lib/mcp-oauth.js +37 -7
  187. package/src/lib/memory-manager.js +23 -2
  188. package/src/lib/model-deprecation.js +249 -0
  189. package/src/lib/pdh-feedback-ledger.js +131 -0
  190. package/src/lib/permanent-memory.js +30 -2
  191. package/src/lib/permission-rules.cjs +11 -3
  192. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  193. package/src/lib/personal-data-hub-wiring.js +36 -13
  194. package/src/lib/project-instructions.js +10 -4
  195. package/src/lib/runnable-provider.js +62 -31
  196. package/src/lib/session-usage.js +34 -1
  197. package/src/lib/skill-loader.js +4 -1
  198. package/src/lib/skill-packs/generator.js +25 -2
  199. package/src/lib/stream-retry.js +56 -0
  200. package/src/lib/sync-credentials.js +53 -17
  201. package/src/lib/token-tracker.js +12 -2
  202. package/src/lib/with-file-lock.js +87 -0
  203. package/src/repl/agent-repl.js +39 -7
  204. package/src/repl/chat-repl.js +10 -0
  205. package/src/repl/session-cost.js +31 -3
  206. package/src/runtime/agent-core.js +474 -53
  207. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  208. package/src/runtime/coding-agent-policy.cjs +10 -0
  209. package/src/runtime/file-ref-expander.js +29 -1
  210. package/src/runtime/headless-runner.js +12 -1
  211. package/src/runtime/headless-stream.js +131 -5
  212. package/src/skills/video-editing/tools/commit.js +15 -1
  213. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  214. package/src/assets/web-panel/assets/Projects-CXydUXp8.js +0 -1
  215. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  216. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  217. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -22,6 +22,7 @@ import {
22
22
  writeFileSync,
23
23
  readdirSync,
24
24
  unlinkSync,
25
+ renameSync,
25
26
  } from "node:fs";
26
27
  import { join } from "node:path";
27
28
 
@@ -32,10 +33,38 @@ export const _deps = {
32
33
  writeFileSync,
33
34
  readdirSync,
34
35
  unlinkSync,
36
+ renameSync,
35
37
  // Injected at runtime by CLI to avoid eager evomap-client load
36
38
  evomapClient: null,
37
39
  };
38
40
 
41
+ /**
42
+ * Atomically write a JSON file via _deps. A user template (<id>.json) is
43
+ * persistent state; a crash mid-write truncates it and listUserTemplates /
44
+ * loadUserTemplate's JSON.parse then drops or throws on it. Temp sibling +
45
+ * rename (atomic within a filesystem). Graceful-degrade to a direct write when
46
+ * the injected fs lacks renameSync — the real fs always has it, so production
47
+ * is always atomic.
48
+ */
49
+ function _atomicWriteJson(file, data) {
50
+ if (typeof _deps.renameSync !== "function") {
51
+ _deps.writeFileSync(file, data, "utf-8");
52
+ return;
53
+ }
54
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
55
+ try {
56
+ _deps.writeFileSync(tmp, data, "utf-8");
57
+ _deps.renameSync(tmp, file);
58
+ } catch (err) {
59
+ try {
60
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
61
+ } catch {
62
+ /* best-effort temp cleanup */
63
+ }
64
+ throw err;
65
+ }
66
+ }
67
+
39
68
  const EVOMAP_CATEGORY = "cowork-template";
40
69
 
41
70
  // ─── Paths ───────────────────────────────────────────────────────────────────
@@ -48,6 +77,26 @@ function _userTemplateFile(cwd, id) {
48
77
  return join(_userTemplatesDir(cwd), `${id}.json`);
49
78
  }
50
79
 
80
+ /**
81
+ * A template id is interpolated into a filesystem path (user-templates/<id>.json)
82
+ * and arrives from installed templates whose payload comes from an external hub /
83
+ * MCP download. A malicious id like "../../.ssh/authorized_keys" or
84
+ * "C:\\Windows\\..." would escape the user-templates dir and overwrite/delete
85
+ * arbitrary files. Restrict it to a single safe path segment.
86
+ */
87
+ function _isSafeTemplateId(id) {
88
+ return (
89
+ typeof id === "string" &&
90
+ id.length > 0 &&
91
+ id !== "." &&
92
+ id !== ".." &&
93
+ !id.includes("/") &&
94
+ !id.includes("\\") &&
95
+ !id.includes(":") &&
96
+ !id.includes("\0")
97
+ );
98
+ }
99
+
51
100
  // ─── Serialization ───────────────────────────────────────────────────────────
52
101
 
53
102
  const SHARED_FIELDS = [
@@ -148,18 +197,21 @@ export function listUserTemplates(cwd) {
148
197
  /** Save a template to the local user-templates directory. */
149
198
  export function saveUserTemplate(cwd, template) {
150
199
  if (!template?.id) throw new Error("template.id is required");
200
+ if (!_isSafeTemplateId(template.id)) {
201
+ throw new Error(`Unsafe template id (path traversal): ${template.id}`);
202
+ }
151
203
  const dir = _userTemplatesDir(cwd);
152
204
  _deps.mkdirSync(dir, { recursive: true });
153
- _deps.writeFileSync(
205
+ _atomicWriteJson(
154
206
  _userTemplateFile(cwd, template.id),
155
207
  JSON.stringify(template, null, 2),
156
- "utf-8",
157
208
  );
158
209
  return template;
159
210
  }
160
211
 
161
212
  /** Remove an installed user template. Returns true if removed. */
162
213
  export function removeUserTemplate(cwd, id) {
214
+ if (!_isSafeTemplateId(id)) return false;
163
215
  const file = _userTemplateFile(cwd, id);
164
216
  if (!_deps.existsSync(file)) return false;
165
217
  _deps.unlinkSync(file);
@@ -29,6 +29,7 @@ import {
29
29
  readdirSync,
30
30
  unlinkSync,
31
31
  appendFileSync,
32
+ renameSync,
32
33
  } from "node:fs";
33
34
  import { join } from "node:path";
34
35
  import { evaluate as evalExpr, resolveReference } from "./workflow-expr.js";
@@ -47,6 +48,7 @@ export const _deps = {
47
48
  readdirSync,
48
49
  unlinkSync,
49
50
  appendFileSync,
51
+ renameSync,
50
52
  now: () => Date.now(),
51
53
  runTask: null, // injected by CLI
52
54
  // Timer seams (injectable for deterministic retry/timeout tests).
@@ -55,6 +57,32 @@ export const _deps = {
55
57
  clearTimeout: (t) => clearTimeout(t),
56
58
  };
57
59
 
60
+ /**
61
+ * Atomically write a JSON file via _deps. A saved workflow definition is
62
+ * persistent user state; a crash mid-write truncates <id>.json and getWorkflow
63
+ * then fails to parse it. Temp sibling + rename (atomic within a filesystem).
64
+ * Graceful-degrade to a direct write when the injected fs lacks renameSync — the
65
+ * real fs always has it, so production is always atomic.
66
+ */
67
+ function _atomicWriteJson(file, data) {
68
+ if (typeof _deps.renameSync !== "function") {
69
+ _deps.writeFileSync(file, data, "utf-8");
70
+ return;
71
+ }
72
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
73
+ try {
74
+ _deps.writeFileSync(tmp, data, "utf-8");
75
+ _deps.renameSync(tmp, file);
76
+ } catch (err) {
77
+ try {
78
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
79
+ } catch {
80
+ /* best-effort temp cleanup */
81
+ }
82
+ throw err;
83
+ }
84
+ }
85
+
58
86
  // ─── Paths ───────────────────────────────────────────────────────────────────
59
87
 
60
88
  function workflowsDir(cwd) {
@@ -302,7 +330,10 @@ export function resolveForEachItems(forEach, resultsById) {
302
330
  export function substituteItem(template, item) {
303
331
  if (typeof template !== "string") return template;
304
332
  const repl = typeof item === "string" ? item : JSON.stringify(item);
305
- return template.replace(/\$\{item\}/g, repl);
333
+ // Insert via a function so $-sequences in the item ($&, $`, $', $$ — common in
334
+ // JSON values / shell snippets) are placed literally rather than interpreted
335
+ // as String.replace patterns, which would corrupt the substituted value.
336
+ return template.replace(/\$\{item\}/g, () => repl);
306
337
  }
307
338
 
308
339
  /** Evaluate a step's `when` expression. Missing expression → always true. */
@@ -376,11 +407,7 @@ export function saveWorkflow(cwd, wf) {
376
407
  if (!valid) throw new Error(`Invalid workflow: ${errors.join("; ")}`);
377
408
  const dir = workflowsDir(cwd);
378
409
  _deps.mkdirSync(dir, { recursive: true });
379
- _deps.writeFileSync(
380
- workflowFile(cwd, wf.id),
381
- JSON.stringify(wf, null, 2),
382
- "utf-8",
383
- );
410
+ _atomicWriteJson(workflowFile(cwd, wf.id), JSON.stringify(wf, null, 2));
384
411
  return wf;
385
412
  }
386
413
 
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import mtcLib from "@chainlesschain/core-mtc";
22
+ import { withFileLock } from "./with-file-lock.js";
22
23
 
23
24
  const {
24
25
  assembleBatch,
@@ -95,6 +96,28 @@ function ensureDirs(dir) {
95
96
  }
96
97
  }
97
98
 
99
+ /**
100
+ * Atomically write a UTF-8 file. The cross-chain MTC store holds config, trust
101
+ * anchors (federation security material), staged bridge ops, the batch-seq
102
+ * counter, and closed batches — a crash mid-write corrupts ledger/security
103
+ * state. Temp sibling + rename (atomic within a filesystem); temp names end in
104
+ * `.tmp` (never `.json`), so a hard-crash leftover is ignored by listStagedOps.
105
+ */
106
+ function atomicWriteFileSync(filePath, data) {
107
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
108
+ try {
109
+ fs.writeFileSync(tmp, data, "utf-8");
110
+ fs.renameSync(tmp, filePath);
111
+ } catch (err) {
112
+ try {
113
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
114
+ } catch {
115
+ /* best-effort temp cleanup */
116
+ }
117
+ throw err;
118
+ }
119
+ }
120
+
98
121
  // ─────────────────────────────────────────────────────────────────────
99
122
  // Namespace
100
123
  // ─────────────────────────────────────────────────────────────────────
@@ -173,7 +196,7 @@ export function saveCrossChainMtcConfig(dir, patch) {
173
196
  "config.mode must be independent, federated, or light-client",
174
197
  );
175
198
  }
176
- fs.writeFileSync(configPath(dir), JSON.stringify(merged, null, 2), "utf-8");
199
+ atomicWriteFileSync(configPath(dir), JSON.stringify(merged, null, 2));
177
200
  return merged;
178
201
  }
179
202
 
@@ -201,11 +224,7 @@ export function loadTrustAnchors(dir) {
201
224
  }
202
225
 
203
226
  function saveTrustAnchorStore(dir, store) {
204
- fs.writeFileSync(
205
- trustAnchorsPath(dir),
206
- JSON.stringify(store, null, 2),
207
- "utf-8",
208
- );
227
+ atomicWriteFileSync(trustAnchorsPath(dir), JSON.stringify(store, null, 2));
209
228
  }
210
229
 
211
230
  /**
@@ -456,7 +475,7 @@ export function stageBridgeOp(dir, op, opts = {}) {
456
475
  if (fs.existsSync(file)) {
457
476
  return { staged: false, path: file, reason: "ALREADY_STAGED" };
458
477
  }
459
- fs.writeFileSync(file, JSON.stringify(op, null, 2), "utf-8");
478
+ atomicWriteFileSync(file, JSON.stringify(op, null, 2));
460
479
  return { staged: true, path: file };
461
480
  }
462
481
 
@@ -498,7 +517,7 @@ function nextBatchSeq(dir, pair) {
498
517
  }
499
518
  const next = (map[pair] || 0) + 1;
500
519
  map[pair] = next;
501
- fs.writeFileSync(p, JSON.stringify(map, null, 2), "utf-8");
520
+ atomicWriteFileSync(p, JSON.stringify(map, null, 2));
502
521
  return next;
503
522
  }
504
523
 
@@ -514,6 +533,18 @@ function nextBatchSeq(dir, pair) {
514
533
  * skipped: { reason: string } | null }}
515
534
  */
516
535
  export function closeBatch(dir, opts = {}) {
536
+ // Serialize the whole staging-scan → batch-write → staging-cleanup section
537
+ // across processes. Without it, two concurrent closes both read the same
538
+ // staged ops, allocate the same per-pair sequence (namespace collision), and
539
+ // batch the same ops twice before clearing staging. Best-effort lock (never
540
+ // hangs); generous timeout since the section includes batch assembly+signing.
541
+ ensureDirs(dir);
542
+ return withFileLock(batchesDir(dir), () => _closeBatchImpl(dir, opts), {
543
+ timeoutMs: 15000,
544
+ });
545
+ }
546
+
547
+ function _closeBatchImpl(dir, opts = {}) {
517
548
  ensureDirs(dir);
518
549
  const cfg = loadCrossChainMtcConfig(dir);
519
550
  const grouped = listStagedOps(dir);
@@ -546,16 +577,14 @@ export function closeBatch(dir, opts = {}) {
546
577
  `${pair}-${String(seq).padStart(6, "0")}`,
547
578
  );
548
579
  fs.mkdirSync(outDir, { recursive: true });
549
- fs.writeFileSync(
580
+ atomicWriteFileSync(
550
581
  path.join(outDir, "landmark.json"),
551
582
  JSON.stringify(result.landmark, null, 2),
552
- "utf-8",
553
583
  );
554
584
  result.envelopes.forEach((env, i) => {
555
- fs.writeFileSync(
585
+ atomicWriteFileSync(
556
586
  path.join(outDir, `envelope-${String(i).padStart(4, "0")}.json`),
557
587
  JSON.stringify(env, null, 2),
558
- "utf-8",
559
588
  );
560
589
  });
561
590
 
@@ -136,14 +136,24 @@ export function decryptFile(inputPath, password, outputPath) {
136
136
  * Check if a file is encrypted with our format.
137
137
  */
138
138
  export function isEncryptedFile(filePath) {
139
+ let fd;
139
140
  try {
140
- const fd = fs.openSync(filePath, "r");
141
+ fd = fs.openSync(filePath, "r");
141
142
  const buf = Buffer.alloc(MAGIC_HEADER.length);
142
143
  fs.readSync(fd, buf, 0, MAGIC_HEADER.length, 0);
143
- fs.closeSync(fd);
144
144
  return buf.equals(MAGIC_HEADER);
145
145
  } catch (_err) {
146
146
  return false;
147
+ } finally {
148
+ // Close in finally so a readSync throw (permission change mid-op, OS error)
149
+ // can't leak the descriptor — repeated failures would otherwise exhaust FDs.
150
+ if (fd !== undefined) {
151
+ try {
152
+ fs.closeSync(fd);
153
+ } catch {
154
+ /* already closed / invalid fd — nothing to release */
155
+ }
156
+ }
147
157
  }
148
158
  }
149
159
 
package/src/lib/dbevo.js CHANGED
@@ -41,6 +41,17 @@ export const SUGGESTION_TYPE = Object.freeze({
41
41
 
42
42
  let _migrations = []; // registered migration definitions
43
43
  let _migrationHistory = []; // executed migration records from DB
44
+
45
+ /**
46
+ * Compare two version strings with NUMERIC collation so "10" sorts AFTER "2".
47
+ * A migration framework must order numeric versions correctly; plain
48
+ * localeCompare()/Array.sort() is lexicographic ("10" < "2"), which would run
49
+ * migration v10 before v2 and corrupt the schema. Numeric collation also leaves
50
+ * zero-padded / date / semver-ish versions ordered as expected.
51
+ */
52
+ function cmpVersion(a, b) {
53
+ return String(a).localeCompare(String(b), undefined, { numeric: true });
54
+ }
44
55
  let _queryLogs = [];
45
56
  let _suggestions = new Map();
46
57
  let _slowQueryThresholdMs = 100;
@@ -147,7 +158,7 @@ export function registerMigration(
147
158
  });
148
159
 
149
160
  // Keep sorted by version
150
- _migrations.sort((a, b) => a.version.localeCompare(b.version));
161
+ _migrations.sort((a, b) => cmpVersion(a.version, b.version));
151
162
 
152
163
  return { registered: true, version, checksum };
153
164
  }
@@ -178,13 +189,13 @@ export function getCurrentVersion(db) {
178
189
  );
179
190
 
180
191
  if (active.length === 0) return null;
181
- return active.sort((a, b) => b.localeCompare(a))[0];
192
+ return active.sort((a, b) => cmpVersion(b, a))[0];
182
193
  }
183
194
 
184
195
  export function getPendingMigrations(db) {
185
196
  const current = getCurrentVersion(db);
186
197
  return _migrations.filter(
187
- (m) => !current || m.version.localeCompare(current) > 0,
198
+ (m) => !current || cmpVersion(m.version, current) > 0,
188
199
  );
189
200
  }
190
201
 
@@ -193,7 +204,7 @@ export function migrateUp(db, targetVersion) {
193
204
  if (pending.length === 0) return { migrated: false, reason: "no_pending" };
194
205
 
195
206
  const toRun = targetVersion
196
- ? pending.filter((m) => m.version.localeCompare(targetVersion) <= 0)
207
+ ? pending.filter((m) => cmpVersion(m.version, targetVersion) <= 0)
197
208
  : pending;
198
209
 
199
210
  if (toRun.length === 0) return { migrated: false, reason: "no_pending" };
@@ -248,13 +259,13 @@ export function migrateDown(db, targetVersion) {
248
259
  .filter((m) => {
249
260
  if (targetVersion) {
250
261
  return (
251
- m.version.localeCompare(current) <= 0 &&
252
- m.version.localeCompare(targetVersion) > 0
262
+ cmpVersion(m.version, current) <= 0 &&
263
+ cmpVersion(m.version, targetVersion) > 0
253
264
  );
254
265
  }
255
266
  return m.version === current;
256
267
  })
257
- .sort((a, b) => b.version.localeCompare(a.version));
268
+ .sort((a, b) => cmpVersion(b.version, a.version));
258
269
 
259
270
  if (toRollBack.length === 0)
260
271
  return { rolledBack: false, reason: "nothing_to_rollback" };
@@ -331,7 +342,7 @@ export function validateMigrations() {
331
342
  if (_migrations.length === 0) return { valid: true, issues: [] };
332
343
 
333
344
  const issues = [];
334
- const versions = _migrations.map((m) => m.version).sort();
345
+ const versions = _migrations.map((m) => m.version).sort(cmpVersion);
335
346
 
336
347
  // Check for gaps in version sequence
337
348
  for (let i = 1; i < versions.length; i++) {
@@ -7,7 +7,7 @@ import {
7
7
  } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { pipeline } from "node:stream/promises";
10
- import { execSync } from "node:child_process";
10
+ import { execFileSync } from "node:child_process";
11
11
  import ora from "ora";
12
12
  import { GITHUB_RELEASES_URL } from "../constants.js";
13
13
  import { getBinDir, ensureDir } from "./paths.js";
@@ -208,16 +208,31 @@ async function fetchRelease(url) {
208
208
  return response.json();
209
209
  }
210
210
 
211
- function extractZip(zipPath, destDir) {
212
- const p = getPlatform();
213
- if (p === "win32") {
214
- // Use PowerShell on Windows
215
- execSync(
216
- `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`,
217
- { stdio: "ignore" },
211
+ export function extractZip(
212
+ zipPath,
213
+ destDir,
214
+ { execFileImpl = execFileSync, platform = getPlatform() } = {},
215
+ ) {
216
+ // Never interpolate the paths into a shell string — `zipPath`/`destDir` derive
217
+ // from the (remote) release asset name, so a `"`/`$()`/`'` in a path would be a
218
+ // command-injection vector. execFileSync runs the tool directly (no shell), and
219
+ // on Windows the paths go through the environment so the -Command script stays
220
+ // a constant the shell can't be tricked by.
221
+ if (platform === "win32") {
222
+ execFileImpl(
223
+ "powershell",
224
+ [
225
+ "-NoProfile",
226
+ "-Command",
227
+ "Expand-Archive -Path $env:CC_ZIP_SRC -DestinationPath $env:CC_ZIP_DEST -Force",
228
+ ],
229
+ {
230
+ stdio: "ignore",
231
+ env: { ...process.env, CC_ZIP_SRC: zipPath, CC_ZIP_DEST: destDir },
232
+ },
218
233
  );
219
234
  } else {
220
- execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: "ignore" });
235
+ execFileImpl("unzip", ["-o", zipPath, "-d", destDir], { stdio: "ignore" });
221
236
  }
222
237
  }
223
238
 
@@ -305,8 +305,8 @@ export function createGovernanceProposalV2(db, options = {}) {
305
305
  throw new Error(`Unknown proposal type: ${proposalType}`);
306
306
  }
307
307
 
308
- const quorumValue = typeof quorum === "number" ? quorum : 3;
309
- const thresholdValue = typeof threshold === "number" ? threshold : 0.5;
308
+ const quorumValue = Number.isFinite(quorum) ? quorum : 3;
309
+ const thresholdValue = Number.isFinite(threshold) ? threshold : 0.5;
310
310
  if (quorumValue < 1) throw new Error("Quorum must be >= 1");
311
311
  if (thresholdValue <= 0 || thresholdValue > 1) {
312
312
  throw new Error("Threshold must be in (0, 1]");
@@ -370,7 +370,7 @@ export function castVoteV2(db, options = {}) {
370
370
  if (!Object.values(VOTE_DIRECTION).includes(direction)) {
371
371
  throw new Error(`Unknown vote direction: ${direction}`);
372
372
  }
373
- const weightValue = typeof weight === "number" ? weight : 1;
373
+ const weightValue = Number.isFinite(weight) ? weight : 1;
374
374
  if (weightValue <= 0) throw new Error("Vote weight must be positive");
375
375
 
376
376
  if (direction === VOTE_DIRECTION.FOR) {
@@ -456,7 +456,7 @@ export function cancelProposal(db, proposalId) {
456
456
  }
457
457
 
458
458
  export function expireProposalsV2(db, now) {
459
- const cutoff = typeof now === "number" ? now : Date.now();
459
+ const cutoff = Number.isFinite(now) ? now : Date.now();
460
460
  const expired = [];
461
461
  for (const p of _proposals.values()) {
462
462
  if (p.status === PROPOSAL_STATUS_V2.ACTIVE) {
@@ -14,6 +14,53 @@ export const _deps = {
14
14
  path,
15
15
  };
16
16
 
17
+ /**
18
+ * Atomically write a UTF-8 file via _deps.fs. A gene's gene.json / content.md
19
+ * are persistent store state; a crash mid-write truncates them and getGene's
20
+ * JSON.parse then throws. Temp sibling + rename (atomic within a filesystem).
21
+ * Graceful-degrade to a direct write when the injected fs has no renameSync
22
+ * (partial test mocks); the real fs always has it, so production is atomic.
23
+ */
24
+ function _atomicWriteFile(filePath, data) {
25
+ const fsx = _deps.fs;
26
+ if (typeof fsx.renameSync !== "function") {
27
+ fsx.writeFileSync(filePath, data, "utf-8");
28
+ return;
29
+ }
30
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
31
+ try {
32
+ fsx.writeFileSync(tmp, data, "utf-8");
33
+ fsx.renameSync(tmp, filePath);
34
+ } catch (err) {
35
+ try {
36
+ if (fsx.existsSync(tmp) && fsx.unlinkSync) fsx.unlinkSync(tmp);
37
+ } catch {
38
+ /* best-effort temp cleanup */
39
+ }
40
+ throw err;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * A gene id is interpolated into a filesystem path (genesDir/<id>/...). Gene ids
46
+ * arrive from downloaded/installed genes (an external hub / MCP response), so a
47
+ * malicious `id` like "../../.ssh/authorized_keys" or "C:\\Windows\\..." would
48
+ * escape genesDir and read/overwrite/delete arbitrary files. Restrict it to a
49
+ * single safe path segment (no separators, no traversal, no drive/NUL).
50
+ */
51
+ function _isSafeGeneId(id) {
52
+ return (
53
+ typeof id === "string" &&
54
+ id.length > 0 &&
55
+ id !== "." &&
56
+ id !== ".." &&
57
+ !id.includes("/") &&
58
+ !id.includes("\\") &&
59
+ !id.includes(":") &&
60
+ !id.includes("\0")
61
+ );
62
+ }
63
+
17
64
  export class EvoMapManager {
18
65
  /**
19
66
  * @param {object} options
@@ -71,6 +118,9 @@ export class EvoMapManager {
71
118
  saveGene(gene, content) {
72
119
  this.initialize();
73
120
  if (!gene || !gene.id) throw new Error("Gene ID required");
121
+ if (!_isSafeGeneId(gene.id)) {
122
+ throw new Error(`Unsafe gene id (path traversal): ${gene.id}`);
123
+ }
74
124
 
75
125
  const geneDir = _deps.path.join(this.genesDir, gene.id);
76
126
  if (!_deps.fs.existsSync(geneDir)) {
@@ -78,19 +128,14 @@ export class EvoMapManager {
78
128
  }
79
129
 
80
130
  // Save metadata
81
- _deps.fs.writeFileSync(
131
+ _atomicWriteFile(
82
132
  _deps.path.join(geneDir, "gene.json"),
83
133
  JSON.stringify(gene, null, 2),
84
- "utf-8",
85
134
  );
86
135
 
87
136
  // Save content
88
137
  if (content) {
89
- _deps.fs.writeFileSync(
90
- _deps.path.join(geneDir, "content.md"),
91
- content,
92
- "utf-8",
93
- );
138
+ _atomicWriteFile(_deps.path.join(geneDir, "content.md"), content);
94
139
  }
95
140
 
96
141
  // Register in DB
@@ -170,6 +215,7 @@ export class EvoMapManager {
170
215
  */
171
216
  getGene(geneId) {
172
217
  this.initialize();
218
+ if (!_isSafeGeneId(geneId)) return null;
173
219
  const geneDir = _deps.path.join(this.genesDir, geneId);
174
220
 
175
221
  if (!_deps.fs.existsSync(geneDir)) return null;
@@ -192,6 +238,7 @@ export class EvoMapManager {
192
238
  */
193
239
  removeGene(geneId) {
194
240
  this.initialize();
241
+ if (!_isSafeGeneId(geneId)) return { removed: false };
195
242
  const geneDir = _deps.path.join(this.genesDir, geneId);
196
243
 
197
244
  if (_deps.fs.existsSync(geneDir)) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Centralized friendly-error boundary for the CLI entrypoint.
3
+ *
4
+ * An uncaught error thrown from a command action (malformed --json, a failed DB
5
+ * op) — or, crucially, one that escapes into an async event handler / detached
6
+ * task / third-party lib AFTER the top-level parse — otherwise surfaces as a raw
7
+ * stack trace via Node's default unhandledRejection/uncaughtException, which on
8
+ * modern Node terminates the process. Route all of those through one place that
9
+ * prints a clean `error: <message>` (full stack under --verbose or
10
+ * CC_DEBUG/DEBUG) and exits non-zero.
11
+ *
12
+ * Pure-ish: process bindings are injectable so this is unit-testable.
13
+ */
14
+
15
+ export function reportFatal(
16
+ err,
17
+ {
18
+ stderr = process.stderr,
19
+ exit = process.exit,
20
+ argv = process.argv,
21
+ env = process.env,
22
+ } = {},
23
+ ) {
24
+ const verbose =
25
+ argv.includes("--verbose") || Boolean(env.CC_DEBUG) || Boolean(env.DEBUG);
26
+ if (verbose && err && err.stack) {
27
+ stderr.write(`${err.stack}\n`);
28
+ } else {
29
+ const msg = err && err.message ? err.message : String(err);
30
+ stderr.write(`error: ${msg}\n`);
31
+ }
32
+ exit(1);
33
+ }
34
+
35
+ /**
36
+ * Install process-level safety nets that funnel unhandled rejections and
37
+ * uncaught exceptions through `report`. Node terminates on these by default, so
38
+ * this only swaps the ugly default stack for the friendly boundary — it is not
39
+ * a behavior change (still exits non-zero).
40
+ */
41
+ export function installGlobalErrorHandlers(proc = process, report = reportFatal) {
42
+ proc.on("unhandledRejection", (reason) =>
43
+ report(reason instanceof Error ? reason : new Error(String(reason))),
44
+ );
45
+ proc.on("uncaughtException", (err) =>
46
+ report(err instanceof Error ? err : new Error(String(err))),
47
+ );
48
+ }
@@ -80,6 +80,27 @@ function newId() {
80
80
  return `cp-${Date.now()}-${rand}`;
81
81
  }
82
82
 
83
+ /**
84
+ * A checkpoint id is interpolated into filesystem paths (root/<id>.json and the
85
+ * blob dir root/<id>). The id comes from CLI args (`cc checkpoint show|delete
86
+ * <id>`), so an id like "../../etc/passwd" would read outside the checkpoint
87
+ * store, and "../../important" passed to delete would rmSync outside it. The git
88
+ * engine guards this via sanitizeSession(); mirror it for the copy fallback by
89
+ * restricting the id to a single safe path segment.
90
+ */
91
+ function isSafeCheckpointId(id) {
92
+ return (
93
+ typeof id === "string" &&
94
+ id.length > 0 &&
95
+ id !== "." &&
96
+ id !== ".." &&
97
+ !id.includes("/") &&
98
+ !id.includes("\\") &&
99
+ !id.includes(":") &&
100
+ !id.includes("\0")
101
+ );
102
+ }
103
+
83
104
  /**
84
105
  * Recursively collect regular files under an absolute path, honoring SKIP_DIRS
85
106
  * and a running maxFiles budget. Symlinks are not followed.
@@ -146,6 +167,9 @@ export function createCheckpoint(paths, opts = {}) {
146
167
  const uniqueAbs = [...new Set(absFiles)];
147
168
 
148
169
  const id = opts.id || newId();
170
+ if (!isSafeCheckpointId(id)) {
171
+ throw new Error(`Unsafe checkpoint id (path traversal): ${id}`);
172
+ }
149
173
  const blobDir = path.join(root, id);
150
174
  ensureDir(blobDir);
151
175
 
@@ -181,6 +205,7 @@ export function createCheckpoint(paths, opts = {}) {
181
205
 
182
206
  /** Load a checkpoint manifest by id, or null. */
183
207
  export function getCheckpoint(id, opts = {}) {
208
+ if (!isSafeCheckpointId(id)) return null;
184
209
  const root = opts.root || defaultRoot();
185
210
  const file = path.join(root, `${id}.json`);
186
211
  if (!fs.existsSync(file)) return null;
@@ -310,6 +335,7 @@ export function restoreCheckpoint(id, opts = {}) {
310
335
 
311
336
  /** Delete a checkpoint (manifest + blobs). Returns true if it existed. */
312
337
  export function deleteCheckpoint(id, opts = {}) {
338
+ if (!isSafeCheckpointId(id)) return false;
313
339
  const root = opts.root || defaultRoot();
314
340
  const file = path.join(root, `${id}.json`);
315
341
  const blobDir = path.join(root, id);