chainlesschain 0.162.122 → 0.162.124

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 (228) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/AIOps-BsDJlD_l.js +1 -0
  4. package/src/assets/web-panel/assets/{ActionButton-Bqvaz1Yj.js → ActionButton-COjZZKWG.js} +1 -1
  5. package/src/assets/web-panel/assets/Analytics-4ZDZE7lc.js +3 -0
  6. package/src/assets/web-panel/assets/{AppLayout-cs0TRZya.js → AppLayout-CYH5k2Rp.js} +5 -5
  7. package/src/assets/web-panel/assets/Audit-BKlFbLdl.js +1 -0
  8. package/src/assets/web-panel/assets/Backup-CiX61Qc2.js +1 -0
  9. package/src/assets/web-panel/assets/{BaseInput-4qhQiJfI.js → BaseInput-CDxIQokd.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-DUFX3Mv4.js → Chat-CaKgdRab.js} +6 -6
  11. package/src/assets/web-panel/assets/ChatBubbleRenderer-DgvpwU5o.js +1 -0
  12. package/src/assets/web-panel/assets/{Checkbox-C4eyD_nB.js → Checkbox-CjiWfu4s.js} +1 -1
  13. package/src/assets/web-panel/assets/Codegen-tKWGzajw.js +1 -0
  14. package/src/assets/web-panel/assets/{Col-3eHStfvJ.js → Col-ZKWREyNs.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-DFbKjEgh.js → Community-CmLuPBUU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-D5str1h-.js → Compact-DqknM98n.js} +1 -1
  17. package/src/assets/web-panel/assets/Compliance-PZw0aBLI.js +1 -0
  18. package/src/assets/web-panel/assets/{Cowork-Cdliboj6.js → Cowork-Dp29kHsC.js} +2 -2
  19. package/src/assets/web-panel/assets/{Cron-Bfmwl6iZ.js → Cron-zmEJAetz.js} +2 -2
  20. package/src/assets/web-panel/assets/Crosschain-Sb-uM7Ty.js +1 -0
  21. package/src/assets/web-panel/assets/{DID-xqi72kjo.js → DID-D_UFNzJ5.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dashboard-4l68QdSQ.js → Dashboard-Btag698v.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-C2wsZOdn.js → Dropdown-CH7l3KCs.js} +1 -1
  24. package/src/assets/web-panel/assets/EmailListRenderer-Bud2M3XX.js +1 -0
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CHK5rapA.js → FamilyGuardDashboard-Bd2_8uME.js} +1 -1
  26. package/src/assets/web-panel/assets/Federation-1jhstF5P.js +1 -0
  27. package/src/assets/web-panel/assets/{FormItemContext-lLOEHvAb.js → FormItemContext-CpSH464Y.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-CaDh2XiY.js → GenericCardRenderer-CANo8O-y.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-B9SmGPMU.js → Git-hvuZVoD3.js} +2 -2
  30. package/src/assets/web-panel/assets/Governance-BQrWksKt.js +1 -0
  31. package/src/assets/web-panel/assets/Inference-jEBTp12v.js +1 -0
  32. package/src/assets/web-panel/assets/KnowledgeGraph-tDiV2taf.js +1 -0
  33. package/src/assets/web-panel/assets/Logs-DDkTnedu.js +2 -0
  34. package/src/assets/web-panel/assets/Marketplace-9Yi32avt.js +1 -0
  35. package/src/assets/web-panel/assets/{McpTools-Dxlypa0R.js → McpTools-Qx78XyOT.js} +5 -5
  36. package/src/assets/web-panel/assets/{Memory-BxBHR7eW.js → Memory-CXYDO1oT.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileBridge-MfUfkHA0.js +1 -0
  38. package/src/assets/web-panel/assets/{MobileProjects-59gyfK6T.js → MobileProjects-DEDbWNIk.js} +1 -1
  39. package/src/assets/web-panel/assets/{Mtc-2YBBawFi.js → Mtc-Dwa_vlR8.js} +2 -2
  40. package/src/assets/web-panel/assets/{MtcAudit-DczAeHfM.js → MtcAudit-DAVkxhJv.js} +2 -2
  41. package/src/assets/web-panel/assets/{Multisig-CPxyt7a-.js → Multisig-C3upmgPN.js} +3 -3
  42. package/src/assets/web-panel/assets/NLProgramming-DO3CZ0_z.js +1 -0
  43. package/src/assets/web-panel/assets/{Notes-tHRMLOQ3.js → Notes-B1Oy3FbC.js} +4 -4
  44. package/src/assets/web-panel/assets/{NotificationSettings-DxIYUWPW.js → NotificationSettings-Bs4-Fc6b.js} +1 -1
  45. package/src/assets/web-panel/assets/OrderTableRenderer-CEy1pIeq.js +1 -0
  46. package/src/assets/web-panel/assets/Organization-D4cJ1UNo.js +4 -0
  47. package/src/assets/web-panel/assets/{Overflow-DrdVw_fC.js → Overflow-fdzvlF7x.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-CMBYxejX.js → P2P-Y13PwXBA.js} +2 -2
  49. package/src/assets/web-panel/assets/PdhVaultBrowser-Ck1zCLe6.js +7 -0
  50. package/src/assets/web-panel/assets/Permissions-CvEXP6LC.js +4 -0
  51. package/src/assets/web-panel/assets/{PersonalDataHub-nuqNTK2w.js → PersonalDataHub-JeP7IWMW.js} +3 -3
  52. package/src/assets/web-panel/assets/Pipeline-BZ7wRzG1.js +1 -0
  53. package/src/assets/web-panel/assets/Privacy-BJ6qj9Hv.js +1 -0
  54. package/src/assets/web-panel/assets/{ProjectInit-hCWXNnGJ.js → ProjectInit-DeWd9UcS.js} +2 -2
  55. package/src/assets/web-panel/assets/ProjectSettings-DUpVuU9F.js +2 -0
  56. package/src/assets/web-panel/assets/{Projects-C6l6z4Mo.js → Projects-y1WbI337.js} +1 -1
  57. package/src/assets/web-panel/assets/{Providers-kEifEWLb.js → Providers-DECW00ek.js} +1 -1
  58. package/src/assets/web-panel/assets/{QuickAsk-B6oLUYKR.js → QuickAsk-Dl-pK9Io.js} +1 -1
  59. package/src/assets/web-panel/assets/Recommend-Bju04wTd.js +1 -0
  60. package/src/assets/web-panel/assets/Reputation-BUPa3nEk.js +1 -0
  61. package/src/assets/web-panel/assets/{Row-W6LSmdzh.js → Row-WYqqaq_5.js} +1 -1
  62. package/src/assets/web-panel/assets/{RssFeed-DJi97lWh.js → RssFeed-iyQT5q9l.js} +3 -3
  63. package/src/assets/web-panel/assets/Search-CrfwJF0R.js +1 -0
  64. package/src/assets/web-panel/assets/{Security-BtawKhmm.js → Security-c4Jxf5Ih.js} +4 -4
  65. package/src/assets/web-panel/assets/{Services-BdCe11of.js → Services-CRuisolj.js} +2 -2
  66. package/src/assets/web-panel/assets/{Skeleton-C_wcDxpN.js → Skeleton-CCnzJkb-.js} +1 -1
  67. package/src/assets/web-panel/assets/{Skills-n2GgEfQ1.js → Skills-CZURCwZg.js} +1 -1
  68. package/src/assets/web-panel/assets/Sla-CrMzaEi2.js +1 -0
  69. package/src/assets/web-panel/assets/SpeechSettings-jTDOM5eY.js +1 -0
  70. package/src/assets/web-panel/assets/{SyncSettings-BBbldyeR.js → SyncSettings-CJWVtd87.js} +2 -2
  71. package/src/assets/web-panel/assets/{Tasks-DMkBOCW-.js → Tasks-61lrQ_lS.js} +1 -1
  72. package/src/assets/web-panel/assets/Templates-Cg-iF2g1.js +1 -0
  73. package/src/assets/web-panel/assets/Tenant-DrDv1FFc.js +1 -0
  74. package/src/assets/web-panel/assets/Terminal-CP15hcNS.js +3 -0
  75. package/src/assets/web-panel/assets/{TimelineRenderer-Drf_B0al.js → TimelineRenderer-Sl5uXrkN.js} +1 -1
  76. package/src/assets/web-panel/assets/Tokens-yRIZTVsR.js +1 -0
  77. package/src/assets/web-panel/assets/{Trigger-CNUB_qRQ.js → Trigger-BLCQEOF2.js} +1 -1
  78. package/src/assets/web-panel/assets/Trust-D5xq8z6s.js +1 -0
  79. package/src/assets/web-panel/assets/{UkeySign-7o1O51qV.js → UkeySign-Dwp0zXQ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{VideoEditing-Ccb1cTxo.js → VideoEditing-CIHA55Sx.js} +1 -1
  81. package/src/assets/web-panel/assets/Wallet-Hjajvnto.js +4 -0
  82. package/src/assets/web-panel/assets/WebAuthn-DqSURUvI.js +5 -0
  83. package/src/assets/web-panel/assets/{WorkflowEditor-BcFvWc2d.js → WorkflowEditor-B5bHRwUG.js} +1 -1
  84. package/src/assets/web-panel/assets/{chat-CPvCSHY4.js → chat-DsheLKkG.js} +1 -1
  85. package/src/assets/web-panel/assets/{colors-Ca7MoLtb.js → colors-CST2UkgL.js} +1 -1
  86. package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +3 -0
  87. package/src/assets/web-panel/assets/{compact-item-B9AUAVCQ.js → compact-item-yqEoy1L7.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-CnMJHqHw.js → createContext-Jwp78HFY.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-C8to6gQk.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-Bevd8pXd.js → hasIn-CVS5mFEP.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-gR1xGRvy.js → index-AnW25bEq.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-B3zlqc60.js → index-BCHAUdel.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BzmhHe0m.js → index-BLTUVd0V.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BM1tI3g5.js → index-BeblNJDH.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CAKYScOz.js → index-BuI27WSx.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-PidVLx0u.js → index-BzetOasL.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dnk8TOm3.js → index-C0FZScMe.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CvhLkwFR.js → index-C130LLPq.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-0DtNS6oi.js → index-C8rq85gu.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BUlsrbu2.js → index-CDAPnZUs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-HdGuA1pS.js → index-CIE2n8k_.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-ecThTNhE.js → index-CRBbVS43.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DW3rTBy_.js → index-CSBPc-sI.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C5i4gKvN.js → index-CTd3lDxp.js} +1 -1
  105. package/src/assets/web-panel/assets/index-CUWj5L2s.js +1 -0
  106. package/src/assets/web-panel/assets/{index-B619_m_T.js → index-ChRL6rgA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-tUD_CWx-.js → index-CjfN2CpJ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BcRDjaEi.js → index-Cjig5i3a.js} +3 -3
  109. package/src/assets/web-panel/assets/{index-Ct3wP67S.js → index-Crk4KWLW.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-p1rI3d66.js → index-CrlRa054.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DPk2HXTA.js → index-CtyEOGuj.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-aB7GLimB.js → index-Cu6Ql64n.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BFkrnvs_.js → index-Cx_t5rWw.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-ienIjr7_.js → index-Cy0dNzkp.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DAjYLhik.js → index-CzsKK0tQ.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CGEc6vlv.js → index-DDAigsel.js} +1 -1
  117. package/src/assets/web-panel/assets/index-DZfnYE6e.js +1 -0
  118. package/src/assets/web-panel/assets/{index-C3biz3RB.js → index-D_1b7uh6.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D9VR1tES.js → index-D_e9gwfn.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BGBbFzIJ.js → index-DbxAlpPC.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CVxhWXMd.js → index-De600Jjm.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-2KMpdEzs.js → index-Dgyn8-wN.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CQKVKyjf.js → index-DiK4vSZa.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CtIv89mI.js → index-DpYn5v2k.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DTxmgfz2.js → index-DxaU_lza.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cd62bwRd.js → index-Dxljh9Fu.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-kUeSjyHt.js → index-R-VGFpi6.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BwDJvHfR.js → index-SjxCCf5h.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BV1U7tR6.js → index-diWkx2Wq.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-dsKHYCPH.js → initDefaultProps-ClAjrsQ-.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-DdPCK2zh.js → motion-BalXv_60.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-Bi0bmOjg.js → move-DMelzIW-.js} +1 -1
  133. package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +1 -0
  134. package/src/assets/web-panel/assets/{omit-DCrBiySU.js → omit-oxecfU3b.js} +1 -1
  135. package/src/assets/web-panel/assets/{pickAttrs-CcJuqfya.js → pickAttrs-DJ5F5M6x.js} +1 -1
  136. package/src/assets/web-panel/assets/{placementArrow-DFGuzth-.js → placementArrow-DdbKsant.js} +1 -1
  137. package/src/assets/web-panel/assets/{responsiveObserve-B_WDlgvI.js → responsiveObserve-BUuM5cmS.js} +1 -1
  138. package/src/assets/web-panel/assets/{slide-BSFabVgO.js → slide-DSV0ccvR.js} +1 -1
  139. package/src/assets/web-panel/assets/{statusUtils-CebvsGH8.js → statusUtils-B-6oLAXf.js} +1 -1
  140. package/src/assets/web-panel/assets/{styleChecker-CMuE7NWy.js → styleChecker-DdCAHtsZ.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFlexGapSupport-RWGVgZNu.js → useFlexGapSupport-ZORkcoZd.js} +1 -1
  142. package/src/assets/web-panel/assets/{useFs-srxzzfG_.js → useFs-BFp46LoP.js} +1 -1
  143. package/src/assets/web-panel/assets/{usePersonalDataHub-B5OwxZuK.js → usePersonalDataHub-H7rxgbdW.js} +1 -1
  144. package/src/assets/web-panel/assets/{vnode-CXOEebAH.js → vnode-eIq4Tk0J.js} +1 -1
  145. package/src/assets/web-panel/assets/{zoom-DKNwqG2v.js → zoom-CTNrv8-H.js} +1 -1
  146. package/src/assets/web-panel/index.html +1 -1
  147. package/src/commands/ask.js +24 -1
  148. package/src/commands/init.js +5 -4
  149. package/src/commands/instinct.js +18 -5
  150. package/src/commands/mcp.js +30 -3
  151. package/src/commands/memory.js +18 -5
  152. package/src/gateways/http/envelope-http-server.js +17 -2
  153. package/src/gateways/terminal/PtyManager.js +15 -2
  154. package/src/gateways/ws/ws-server.js +20 -0
  155. package/src/gateways/ws/ws-session-gateway.js +16 -0
  156. package/src/harness/mcp-client.js +124 -6
  157. package/src/harness/worktree-isolator.js +20 -9
  158. package/src/lib/agent-core.js +3 -0
  159. package/src/lib/autonomous-agent.js +7 -6
  160. package/src/lib/chat-intent-service.js +4 -2
  161. package/src/lib/checkpoint-store.js +23 -0
  162. package/src/lib/credential-guard.js +291 -0
  163. package/src/lib/ensure-utf8.js +6 -1
  164. package/src/lib/git-integration.js +57 -1
  165. package/src/lib/hook-manager.js +15 -6
  166. package/src/lib/hook-runner.cjs +11 -1
  167. package/src/lib/interactive-planner.js +4 -3
  168. package/src/lib/json-schema-output.js +47 -0
  169. package/src/lib/learning/reflection-engine.js +4 -3
  170. package/src/lib/learning/skill-improver.js +4 -3
  171. package/src/lib/learning/skill-synthesizer.js +4 -3
  172. package/src/lib/mcp-client.js +1 -0
  173. package/src/lib/mcp-oauth.js +243 -24
  174. package/src/lib/mcp-serve.js +56 -6
  175. package/src/lib/orchestrator.js +4 -2
  176. package/src/lib/process-manager.js +31 -3
  177. package/src/lib/repl-completer.js +29 -0
  178. package/src/lib/repl-rewind.js +58 -0
  179. package/src/lib/repl-vim.js +13 -1
  180. package/src/lib/settings-hooks.cjs +12 -3
  181. package/src/lib/slot-filler.js +4 -3
  182. package/src/lib/sub-agent-context.js +6 -0
  183. package/src/lib/web-fetch.js +54 -3
  184. package/src/lib/workflow-engine.js +35 -11
  185. package/src/repl/agent-repl.js +52 -2
  186. package/src/runtime/agent-core.js +324 -26
  187. package/src/runtime/headless-stream.js +124 -17
  188. package/src/assets/web-panel/assets/AIOps-BmS46GrQ.js +0 -1
  189. package/src/assets/web-panel/assets/Analytics-BhCnMbKz.js +0 -3
  190. package/src/assets/web-panel/assets/Audit-AwLNQzvX.js +0 -1
  191. package/src/assets/web-panel/assets/Backup-BOglo5g6.js +0 -1
  192. package/src/assets/web-panel/assets/ChatBubbleRenderer-LWgyWUF_.js +0 -1
  193. package/src/assets/web-panel/assets/Codegen-B9SRqAqh.js +0 -1
  194. package/src/assets/web-panel/assets/Compliance-DDYPQDiG.js +0 -1
  195. package/src/assets/web-panel/assets/Crosschain-Bfo6RvSg.js +0 -1
  196. package/src/assets/web-panel/assets/EmailListRenderer-CWh_fKmI.js +0 -1
  197. package/src/assets/web-panel/assets/Federation-JpiE2iKR.js +0 -1
  198. package/src/assets/web-panel/assets/Governance-B3ODoSO2.js +0 -1
  199. package/src/assets/web-panel/assets/Inference-C_jP_nlz.js +0 -1
  200. package/src/assets/web-panel/assets/KnowledgeGraph-zYwypnwu.js +0 -1
  201. package/src/assets/web-panel/assets/Logs-D6owai5o.js +0 -2
  202. package/src/assets/web-panel/assets/Marketplace-umRB80w4.js +0 -1
  203. package/src/assets/web-panel/assets/MobileBridge-Car9kLtE.js +0 -3
  204. package/src/assets/web-panel/assets/NLProgramming-DvKJ8imu.js +0 -1
  205. package/src/assets/web-panel/assets/OrderTableRenderer-BMpVfQkg.js +0 -1
  206. package/src/assets/web-panel/assets/Organization-BBsie-kN.js +0 -4
  207. package/src/assets/web-panel/assets/PdhVaultBrowser-BBHzSaeq.js +0 -7
  208. package/src/assets/web-panel/assets/Permissions-W4kD06Cj.js +0 -4
  209. package/src/assets/web-panel/assets/Pipeline-DzCX3yHy.js +0 -1
  210. package/src/assets/web-panel/assets/Privacy-9ENAJQOC.js +0 -1
  211. package/src/assets/web-panel/assets/ProjectSettings--BzvEaxE.js +0 -2
  212. package/src/assets/web-panel/assets/Recommend-BIEMGN0q.js +0 -1
  213. package/src/assets/web-panel/assets/Reputation-D-9AEaRr.js +0 -1
  214. package/src/assets/web-panel/assets/Search-DOEB2utf.js +0 -1
  215. package/src/assets/web-panel/assets/Sla-D5WfrvLP.js +0 -1
  216. package/src/assets/web-panel/assets/SpeechSettings-D6qCWpan.js +0 -1
  217. package/src/assets/web-panel/assets/Templates-DbLImR1I.js +0 -1
  218. package/src/assets/web-panel/assets/Tenant-B0qM4w6Z.js +0 -1
  219. package/src/assets/web-panel/assets/Terminal-BjKUkiR1.js +0 -3
  220. package/src/assets/web-panel/assets/Tokens-DgCx2Bfa.js +0 -1
  221. package/src/assets/web-panel/assets/Trust-JQUTjXXI.js +0 -1
  222. package/src/assets/web-panel/assets/Wallet-EMf9MlLK.js +0 -4
  223. package/src/assets/web-panel/assets/WebAuthn-DOYoaCmi.js +0 -5
  224. package/src/assets/web-panel/assets/community-parser-CO25nZFt.js +0 -3
  225. package/src/assets/web-panel/assets/devWarning-vFgm_Ssk.js +0 -1
  226. package/src/assets/web-panel/assets/index-BGUjPh7h.js +0 -1
  227. package/src/assets/web-panel/assets/index-Cx-wP5XE.js +0 -1
  228. package/src/assets/web-panel/assets/mtc-parser-BbAbUB-9.js +0 -1
@@ -17,7 +17,7 @@
17
17
  import http from "http";
18
18
  import fsDefault from "fs";
19
19
  import pathDefault from "path";
20
- import { randomBytes } from "crypto";
20
+ import { randomBytes, timingSafeEqual } from "crypto";
21
21
 
22
22
  export const MAX_READ_BYTES = 200 * 1024;
23
23
  export const MAX_LIST_ENTRIES = 500;
@@ -29,14 +29,65 @@ export const _deps = { fs: fsDefault, path: pathDefault };
29
29
 
30
30
  /** Resolve `rel` inside `root`; throws on escape (.. traversal, abs paths out). */
31
31
  export function confine(root, rel, deps = _deps) {
32
- const abs = deps.path.resolve(root, rel || ".");
33
- const normRoot = deps.path.resolve(root);
34
- if (abs !== normRoot && !abs.startsWith(normRoot + deps.path.sep)) {
32
+ const { path } = deps;
33
+ const fs = deps.fs;
34
+ const normRoot = path.resolve(root);
35
+ const abs = path.resolve(normRoot, rel || ".");
36
+ // Fast string guard: blocks `..` traversal and absolute-path escapes.
37
+ if (abs !== normRoot && !abs.startsWith(normRoot + path.sep)) {
35
38
  throw new Error(`path escapes serve root: ${rel}`);
36
39
  }
40
+ // Symlink guard: path.resolve does NOT follow symlinks, so a symlink INSIDE
41
+ // root pointing OUTSIDE would pass the string check yet the fs op (read/write/
42
+ // list) would escape the serve boundary — e.g. reading ~/.ssh through a
43
+ // workspace symlink, defeating a `--read-only` exposure. Resolve the real path
44
+ // of the deepest EXISTING ancestor (a not-yet-created file can't be a symlink)
45
+ // and re-check containment against the real root.
46
+ if (fs && typeof fs.realpathSync === "function") {
47
+ let realRoot;
48
+ try {
49
+ realRoot = fs.realpathSync(normRoot);
50
+ } catch {
51
+ return abs; // root itself unresolvable → the string guard is all we have
52
+ }
53
+ let probe = abs;
54
+ for (;;) {
55
+ let real;
56
+ try {
57
+ real = fs.realpathSync(probe);
58
+ } catch {
59
+ const parent = path.dirname(probe);
60
+ if (parent === probe) break; // reached fs root, nothing existed
61
+ probe = parent;
62
+ continue;
63
+ }
64
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
65
+ throw new Error(`path escapes serve root (symlink): ${rel}`);
66
+ }
67
+ break;
68
+ }
69
+ }
37
70
  return abs;
38
71
  }
39
72
 
73
+ /**
74
+ * Constant-time check of an `Authorization: Bearer <token>` header against the
75
+ * expected token. A plain `!==` leaks timing that could let a caller recover the
76
+ * token byte-by-byte; hash-length differs are rejected up front (the token is a
77
+ * fixed-length random value, so its length is not secret), then `timingSafeEqual`
78
+ * compares the bytes. Mirrors the ws-server's token check.
79
+ */
80
+ export function bearerMatches(authHeader, token) {
81
+ const prefix = "Bearer ";
82
+ if (typeof authHeader !== "string" || !authHeader.startsWith(prefix)) {
83
+ return false;
84
+ }
85
+ const a = Buffer.from(authHeader.slice(prefix.length), "utf-8");
86
+ const b = Buffer.from(String(token), "utf-8");
87
+ if (a.length !== b.length) return false;
88
+ return timingSafeEqual(a, b);
89
+ }
90
+
40
91
  function ok(text) {
41
92
  return { content: [{ type: "text", text: String(text) }] };
42
93
  }
@@ -194,8 +245,7 @@ export function startMcpServe(opts = {}) {
194
245
  return send(405, rpcError(null, -32600, "POST only"));
195
246
  }
196
247
  if (token) {
197
- const auth = req.headers.authorization || "";
198
- if (auth !== `Bearer ${token}`) {
248
+ if (!bearerMatches(req.headers.authorization, token)) {
199
249
  return send(401, rpcError(null, -32001, "unauthorized"));
200
250
  }
201
251
  }
@@ -27,6 +27,7 @@ import crypto from "crypto";
27
27
  import { AgentRouter } from "./agent-router.js";
28
28
  import { NotificationManager } from "./notifiers/index.js";
29
29
  import { createChatFn } from "./cowork-adapter.js";
30
+ import { firstBalancedJson } from "./json-schema-output.js";
30
31
 
31
32
  /* ---------- _deps injection (Vitest CJS mock pattern) ---------- */
32
33
  export const _deps = { execSync };
@@ -403,8 +404,9 @@ export class Orchestrator extends EventEmitter {
403
404
 
404
405
  /** Extract the first JSON array from an LLM response string. */
405
406
  function _extractJson(text) {
406
- const match = text.match(/\[[\s\S]*\]/);
407
- return match ? match[0] : "[]";
407
+ // Balanced extraction stops at the first complete array, so trailing prose
408
+ // (or a second array) no longer over-captures into an unparseable string.
409
+ return firstBalancedJson(text, "[") || "[]";
408
410
  }
409
411
 
410
412
  /** Parse error lines from CI output (test failures, lint errors). */
@@ -58,6 +58,17 @@ export function startApp(options = {}) {
58
58
  return child.pid;
59
59
  }
60
60
 
61
+ /**
62
+ * Parse a PID-file's contents into a valid positive integer, or null.
63
+ * A corrupt / empty / partially-written file must not yield NaN — that NaN
64
+ * would otherwise flow into `taskkill /PID NaN` or `process.kill(NaN, …)`,
65
+ * which silently fails to act on the real process.
66
+ */
67
+ export function parsePid(content) {
68
+ const pid = parseInt(String(content).trim(), 10);
69
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
70
+ }
71
+
61
72
  export function stopApp() {
62
73
  const pidFile = getPidFilePath();
63
74
 
@@ -66,7 +77,16 @@ export function stopApp() {
66
77
  return false;
67
78
  }
68
79
 
69
- const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
80
+ const pid = parsePid(readFileSync(pidFile, "utf-8"));
81
+ if (pid === null) {
82
+ logger.warn("PID file is empty or corrupt; removing it.");
83
+ try {
84
+ unlinkSync(pidFile);
85
+ } catch {
86
+ /* best-effort */
87
+ }
88
+ return false;
89
+ }
70
90
 
71
91
  try {
72
92
  if (isWindows()) {
@@ -90,7 +110,15 @@ export function isAppRunning() {
90
110
  return false;
91
111
  }
92
112
 
93
- const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
113
+ const pid = parsePid(readFileSync(pidFile, "utf-8"));
114
+ if (pid === null) {
115
+ try {
116
+ unlinkSync(pidFile);
117
+ } catch {
118
+ /* best-effort */
119
+ }
120
+ return false;
121
+ }
94
122
 
95
123
  try {
96
124
  process.kill(pid, 0);
@@ -104,7 +132,7 @@ export function isAppRunning() {
104
132
  export function getAppPid() {
105
133
  const pidFile = getPidFilePath();
106
134
  if (!existsSync(pidFile)) return null;
107
- return parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
135
+ return parsePid(readFileSync(pidFile, "utf-8"));
108
136
  }
109
137
 
110
138
  function findExecutable(binDir, extension) {
@@ -32,6 +32,20 @@ export function extractAtPrefix(line) {
32
32
  return m ? { prefix: m[2] } : null;
33
33
  }
34
34
 
35
+ /**
36
+ * In `!` bash mode (the line starts with `!`), the trailing whitespace-delimited
37
+ * token completes to a filesystem path (Claude-Code 2.1.193 bash-mode file
38
+ * autocomplete). Returns `{ prefix }` — the token with any leading `!` stripped
39
+ * (so `!cat src/fo` → `src/fo`, `!scr` → `scr`) — or null when not in bash mode.
40
+ */
41
+ export function extractBashPathPrefix(line) {
42
+ if (!/^\s*!/.test(line || "")) return null;
43
+ const m = /(\S*)$/.exec(line || "");
44
+ let tok = m ? m[1] : "";
45
+ if (tok.startsWith("!")) tok = tok.slice(1); // `!src/fo` → `src/fo`
46
+ return { prefix: tok };
47
+ }
48
+
35
49
  /** Normalize to forward slashes (what @refs use on every platform). */
36
50
  function fwd(p) {
37
51
  return String(p).replace(/\\/g, "/");
@@ -183,6 +197,21 @@ export function makeAtCompleter(opts = {}) {
183
197
  return [hits, line];
184
198
  }
185
199
  }
200
+ // `!` bash-mode file-path completion (Claude-Code 2.1.193 parity): the
201
+ // trailing token completes to filesystem paths under cwd — a shell command
202
+ // runs from cwd, so this uses the filesystem candidate set only (not the
203
+ // editor's open-file list). Checked before @refs so a `@` inside a bash
204
+ // command (e.g. `!grep @x file`) still completes the trailing path token.
205
+ const bash = extractBashPathPrefix(line);
206
+ if (bash) {
207
+ const fromFs = fileCandidates(bash.prefix, {
208
+ cwd: getCwd(),
209
+ deps: opts.deps,
210
+ });
211
+ // readline replaces the trailing `prefix` with the chosen path, leaving
212
+ // the `!` and the rest of the command intact.
213
+ return [fromFs.slice(0, MAX_CANDIDATES), bash.prefix];
214
+ }
186
215
  const at = extractAtPrefix(line);
187
216
  if (!at) return [[], line];
188
217
  refreshIde(); // async top-up for the NEXT tab; this one uses the cache
@@ -161,6 +161,64 @@ export function buildResumeRecap(messages, { previewChars = 160 } = {}) {
161
161
  return lines;
162
162
  }
163
163
 
164
+ /**
165
+ * Snapshot the live conversation so a later `/rewind clear` can restore it
166
+ * (Claude-Code 2.1.191: "/rewind support for resuming a conversation from
167
+ * before /clear was run"). Drops the system prompt (index 0, re-added on
168
+ * restore) and copies the checkpoint marks. Returns null when there is nothing
169
+ * worth stashing (empty conversation) so a no-op /clear can't clobber an
170
+ * existing restorable snapshot.
171
+ *
172
+ * @param {Array} messages
173
+ * @param {Array} [marks]
174
+ * @returns {{messages:Array, marks:Array}|null}
175
+ */
176
+ export function snapshotClearedConversation(messages, marks) {
177
+ const body = (messages || []).slice(1);
178
+ if (body.length === 0) return null;
179
+ return {
180
+ messages: body.slice(),
181
+ marks: Array.isArray(marks) ? marks.slice() : [],
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Restore a conversation stashed by /clear, swapping it IN PLACE with the
187
+ * current (post-clear) conversation so the restore is itself undoable. Keeps
188
+ * `messages[0]` (the system prompt). Returns the new stash (the swapped-out
189
+ * current state, or null when it was empty) plus counts, or null when there is
190
+ * nothing to restore.
191
+ *
192
+ * @param {Array} messages live conversation (index 0 = system prompt, kept)
193
+ * @param {Array} marks live checkpoint marks array (mutated in place)
194
+ * @param {{messages:Array, marks:Array}|null} cleared stash from /clear
195
+ * @returns {{restored:number, stashed:number, newCleared:{messages:Array,marks:Array}|null}|null}
196
+ */
197
+ export function restoreClearedConversation(messages, marks, cleared) {
198
+ if (
199
+ !cleared ||
200
+ !Array.isArray(cleared.messages) ||
201
+ cleared.messages.length === 0
202
+ ) {
203
+ return null;
204
+ }
205
+ const curMsgs = (messages || []).slice(1);
206
+ const curMarks = Array.isArray(marks) ? marks.slice() : [];
207
+ // Replace messages[1..] with the stashed conversation (system prompt kept).
208
+ messages.splice(1, messages.length - 1, ...cleared.messages);
209
+ if (Array.isArray(marks)) {
210
+ marks.splice(0, marks.length, ...(cleared.marks || []));
211
+ }
212
+ const newCleared = curMsgs.length
213
+ ? { messages: curMsgs, marks: curMarks }
214
+ : null;
215
+ return {
216
+ restored: cleared.messages.length,
217
+ stashed: curMsgs.length,
218
+ newCleared,
219
+ };
220
+ }
221
+
164
222
  /** Render the picker list (shared by /rewind and double-Esc). */
165
223
  export function renderTurnList(turns) {
166
224
  if (!turns.length) return " (no user turns yet)";
@@ -248,7 +248,7 @@ const bell = (state, patch = {}) => ({
248
248
  * with line/cursor; `submit:true` ⇒ run the line.
249
249
  */
250
250
  export function feedNormalKey(state, ch, key = {}) {
251
- const s = { ...state, message: null };
251
+ const s = { ...state, message: null, notice: null };
252
252
  const line = s.line;
253
253
 
254
254
  // Enter → submit the line (parity with insert-mode Enter).
@@ -420,6 +420,18 @@ export function feedNormalKey(state, ch, key = {}) {
420
420
  case "t":
421
421
  case "T":
422
422
  return { ...s, awaitChar: { kind: ch }, count: String(count) };
423
+ case "/":
424
+ case "?":
425
+ // cc's NORMAL mode has no prompt-history search (readline owns history in
426
+ // INSERT mode). A user pressing `/` here most likely wants a slash
427
+ // command, so hint how to reach one instead of an opaque bell
428
+ // (Claude-Code 2.1.191: "hint how to reach slash commands").
429
+ return {
430
+ ...s,
431
+ count: "",
432
+ notice:
433
+ "NORMAL /: press i, then type /command (slash commands run in insert mode; history = ↑ or Ctrl-R)",
434
+ };
423
435
  default: {
424
436
  // Pure motions (h l 0 ^ $ w b e) and Backspace/arrows mapped to h/l.
425
437
  let motionCh = ch;
@@ -132,9 +132,18 @@ function compileMatcher(pattern) {
132
132
  /* fall through to wildcard */
133
133
  }
134
134
  }
135
- if (pattern.includes("|")) {
136
- const ms = pattern.split("|").map((p) => compileMatcher(p.trim()));
137
- return (v) => ms.some((m) => m(v));
135
+ // OR-separated alternatives: pipe `Edit|Write` AND comma `Bash,PowerShell`
136
+ // (Claude-Code 2.1.191: comma-separated matchers silently never fired). Empty
137
+ // segments (trailing `Bash,` / `Edit|`) are dropped so they can't collapse to
138
+ // a match-everything matcher. Regex `/…/` matchers are handled above, so a
139
+ // comma inside `{1,3}` is never split here.
140
+ if (pattern.includes("|") || pattern.includes(",")) {
141
+ const ms = pattern
142
+ .split(/[|,]/)
143
+ .map((p) => p.trim())
144
+ .filter(Boolean)
145
+ .map((p) => compileMatcher(p));
146
+ return ms.length ? (v) => ms.some((m) => m(v)) : () => false;
138
147
  }
139
148
  const esc = pattern
140
149
  .replace(/[.+^${}()[\]\\]/g, "\\$&")
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { EventEmitter } from "events";
11
+ import { firstBalancedJson } from "./json-schema-output.js";
11
12
 
12
13
  /**
13
14
  * Required slots per intent type — must be filled before execution.
@@ -341,9 +342,9 @@ Keep values concise (single words or short strings).`;
341
342
  ]);
342
343
 
343
344
  const content = response?.message?.content || response?.content || "";
344
- const jsonMatch = content.match(/\{[\s\S]*\}/);
345
- if (jsonMatch) {
346
- const parsed = JSON.parse(jsonMatch[0]);
345
+ const jsonText = firstBalancedJson(content, "{");
346
+ if (jsonText) {
347
+ const parsed = JSON.parse(jsonText);
347
348
  // Filter out null values
348
349
  const result = {};
349
350
  for (const [key, value] of Object.entries(parsed)) {
@@ -74,6 +74,9 @@ export class SubAgentContext {
74
74
  this.inheritedContext = options.inheritedContext || null;
75
75
  this.allowedTools = options.allowedTools || null; // null = all
76
76
  this.depth = options.depth || 1; // nesting level (parent main loop = 0)
77
+ // Shared run-wide TOTAL-sub-agent counter (one object across the whole tree)
78
+ // so this sub-agent's own spawns draw from the same breadth pool.
79
+ this.subAgentBudget = options.subAgentBudget || null;
77
80
  this.cwd = options.cwd || process.cwd();
78
81
  this.status = "active";
79
82
  this.result = null;
@@ -252,6 +255,9 @@ export class SubAgentContext {
252
255
  cwd: this.cwd,
253
256
  // Nesting level: lets a nested spawn_sub_agent see — and cap — its depth.
254
257
  subAgentDepth: this.depth,
258
+ // Shared total-sub-agent counter so a nested spawn_sub_agent draws from
259
+ // (and is bounded by) the run's single breadth pool.
260
+ subAgentBudget: this.subAgentBudget,
255
261
  ...loopOptions,
256
262
  };
257
263
  if (this.iterationBudget) {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import http from "http";
9
9
  import https from "https";
10
+ import { lookup as dnsLookup } from "dns";
10
11
  import { URL } from "url";
11
12
 
12
13
  const DEFAULT_MAX_BYTES = 2_000_000;
@@ -139,7 +140,46 @@ function stripTags(html) {
139
140
  return String(html).replace(/<[^>]+>/g, "");
140
141
  }
141
142
 
142
- async function _doRequest(parsed, { maxBytes, timeout, headers }) {
143
+ /**
144
+ * A `lookup` for http(s).request that resolves the hostname and REJECTS the
145
+ * connection if any resolved IP is private/loopback/link-local. checkAllowed
146
+ * only inspects the URL's literal hostname, so a public-looking domain that
147
+ * DNS-resolves to an internal IP (e.g. 169.254.169.254 cloud metadata) would
148
+ * otherwise sail through — classic DNS-based SSRF. Because Node connects to the
149
+ * exact address this returns, it also closes the DNS-rebinding TOCTOU window.
150
+ * Returns a Node-style lookup fn; `allowPrivateHosts` opts out (same flag as
151
+ * checkAllowed).
152
+ */
153
+ export function makeSafeLookup(allowPrivateHosts, deps = _deps) {
154
+ const lookup = deps.lookup || dnsLookup;
155
+ return (hostname, options, callback) => {
156
+ const cb = typeof options === "function" ? options : callback;
157
+ const opts = typeof options === "function" ? {} : options || {};
158
+ lookup(hostname, { ...opts, all: true }, (err, addresses) => {
159
+ if (err) return cb(err);
160
+ const list = Array.isArray(addresses) ? addresses : [];
161
+ if (list.length === 0) {
162
+ return cb(new Error(`could not resolve host: ${hostname}`));
163
+ }
164
+ if (!allowPrivateHosts) {
165
+ for (const a of list) {
166
+ if (isPrivateHost(a.address)) {
167
+ return cb(
168
+ new Error(`private/loopback resolved IP blocked: ${a.address}`),
169
+ );
170
+ }
171
+ }
172
+ }
173
+ const chosen = list[0];
174
+ cb(null, chosen.address, chosen.family);
175
+ });
176
+ };
177
+ }
178
+
179
+ async function _doRequest(
180
+ parsed,
181
+ { maxBytes, timeout, headers, allowPrivateHosts },
182
+ ) {
143
183
  const lib = parsed.protocol === "https:" ? https : http;
144
184
  return new Promise((resolve, reject) => {
145
185
  const req = lib.request(
@@ -149,6 +189,10 @@ async function _doRequest(parsed, { maxBytes, timeout, headers }) {
149
189
  hostname: parsed.hostname,
150
190
  port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
151
191
  path: parsed.pathname + parsed.search,
192
+ // Resolve-and-check: reject the connection if the host resolves to a
193
+ // private IP (DNS-SSRF + rebinding guard). String-host check is in
194
+ // checkAllowed; this covers the resolved address.
195
+ lookup: makeSafeLookup(allowPrivateHosts),
152
196
  headers: {
153
197
  "User-Agent": "ChainlessChain-Agent/1.0",
154
198
  Accept: "*/*",
@@ -210,7 +254,12 @@ export async function webFetch(url, options = {}) {
210
254
  let redirects = 0;
211
255
  let response;
212
256
  while (true) {
213
- response = await _doRequest(parsed, { maxBytes, timeout, headers });
257
+ response = await _doRequest(parsed, {
258
+ maxBytes,
259
+ timeout,
260
+ headers,
261
+ allowPrivateHosts: config.allowPrivateHosts,
262
+ });
214
263
  if (!response.redirect) break;
215
264
  if (++redirects > maxRedirects) {
216
265
  return { error: "too many redirects" };
@@ -250,7 +299,9 @@ export async function webFetch(url, options = {}) {
250
299
  };
251
300
  }
252
301
 
253
- export const _deps = { http, https };
302
+ // `lookup` is injectable so tests can resolve a domain to a private IP without
303
+ // real network; production uses the system resolver.
304
+ export const _deps = { http, https, lookup: dnsLookup };
254
305
 
255
306
  // ===== V2 Surface: Web Fetch governance overlay (CLI v0.142.0) =====
256
307
  export const WFET_TARGET_MATURITY_V2 = Object.freeze({
@@ -37,6 +37,22 @@ export const TEMPLATE_TYPE = Object.freeze({
37
37
  TEST_SUITE: "test_suite",
38
38
  });
39
39
 
40
+ /**
41
+ * Parse a JSON column from a DB row without throwing. A truncated write, a
42
+ * manual edit, or a schema-drift row would otherwise make `JSON.parse` throw and
43
+ * crash the whole operation — and for list/map callers, ONE corrupt row would
44
+ * hide every other workflow. On bad/missing JSON we return `fallback` so a
45
+ * single bad row degrades to an empty/null value instead of poisoning the call.
46
+ */
47
+ export function _safeParse(json, fallback) {
48
+ if (json == null) return fallback;
49
+ try {
50
+ return JSON.parse(json);
51
+ } catch {
52
+ return fallback;
53
+ }
54
+ }
55
+
40
56
  /**
41
57
  * Ensure workflow tables exist in the database.
42
58
  */
@@ -183,7 +199,7 @@ export function getWorkflow(db, workflowId) {
183
199
  .prepare("SELECT * FROM workflows WHERE id = ?")
184
200
  .get(workflowId);
185
201
  if (!row) return null;
186
- return { ...row, dag: JSON.parse(row.dag) };
202
+ return { ...row, dag: _safeParse(row.dag, null) };
187
203
  }
188
204
 
189
205
  /**
@@ -196,7 +212,7 @@ export function listWorkflows(db) {
196
212
  .all();
197
213
  return rows.map((row) => ({
198
214
  ...row,
199
- dag: JSON.parse(row.dag),
215
+ dag: _safeParse(row.dag, null),
200
216
  }));
201
217
  }
202
218
 
@@ -222,6 +238,11 @@ export function executeWorkflow(db, workflowId, input = {}) {
222
238
  if (!workflow) {
223
239
  throw new Error(`Workflow not found: ${workflowId}`);
224
240
  }
241
+ if (!Array.isArray(workflow.dag) || workflow.dag.length === 0) {
242
+ // _safeParse yields null for a corrupt `dag` column — give a clear error
243
+ // instead of an opaque NPE in topologicalSort.
244
+ throw new Error(`Workflow ${workflowId} has a corrupt or empty DAG`);
245
+ }
225
246
 
226
247
  const execId = `exec-${crypto.randomBytes(8).toString("hex")}`;
227
248
  const stages = workflow.dag;
@@ -321,7 +342,7 @@ export function pauseExecution(db, executionId) {
321
342
  executionId,
322
343
  );
323
344
 
324
- const log = JSON.parse(exec.log || "[]");
345
+ const log = _safeParse(exec.log, []);
325
346
  log.push({
326
347
  action: "paused",
327
348
  timestamp: new Date().toISOString(),
@@ -354,7 +375,7 @@ export function resumeExecution(db, executionId) {
354
375
  executionId,
355
376
  );
356
377
 
357
- const log = JSON.parse(exec.log || "[]");
378
+ const log = _safeParse(exec.log, []);
358
379
  log.push({
359
380
  action: "resumed",
360
381
  timestamp: new Date().toISOString(),
@@ -387,7 +408,7 @@ export function rollbackExecution(db, executionId) {
387
408
  executionId,
388
409
  );
389
410
 
390
- const log = JSON.parse(exec.log || "[]");
411
+ const log = _safeParse(exec.log, []);
391
412
  log.push({
392
413
  action: "rolled_back",
393
414
  timestamp: new Date().toISOString(),
@@ -415,7 +436,7 @@ export function getExecutionLog(db, executionId) {
415
436
  id: exec.id,
416
437
  workflowId: exec.workflow_id,
417
438
  status: exec.status,
418
- log: JSON.parse(exec.log || "[]"),
439
+ log: _safeParse(exec.log, []),
419
440
  startedAt: exec.started_at,
420
441
  completedAt: exec.completed_at,
421
442
  };
@@ -688,7 +709,7 @@ export function createCheckpoint(db, executionId) {
688
709
  currentStage: exec.current_stage,
689
710
  capturedAt: new Date().toISOString(),
690
711
  };
691
- const nodeStates = JSON.parse(exec.log || "[]");
712
+ const nodeStates = _safeParse(exec.log, []);
692
713
 
693
714
  db.prepare(
694
715
  `INSERT INTO workflow_checkpoints (id, execution_id, workflow_id, state_snapshot, node_states, created_at)
@@ -722,8 +743,8 @@ export function listCheckpoints(db, executionId) {
722
743
  id: row.id,
723
744
  executionId: row.execution_id,
724
745
  workflowId: row.workflow_id,
725
- snapshot: JSON.parse(row.state_snapshot || "{}"),
726
- nodeStates: JSON.parse(row.node_states || "[]"),
746
+ snapshot: _safeParse(row.state_snapshot, {}),
747
+ nodeStates: _safeParse(row.node_states, []),
727
748
  createdAt: row.created_at,
728
749
  }));
729
750
  }
@@ -750,8 +771,8 @@ export function rollbackToCheckpoint(db, executionId, checkpointId) {
750
771
  throw new Error(`Execution not found: ${executionId}`);
751
772
  }
752
773
 
753
- const snapshot = JSON.parse(cp.state_snapshot || "{}");
754
- const nodeStates = JSON.parse(cp.node_states || "[]");
774
+ const snapshot = _safeParse(cp.state_snapshot, {});
775
+ const nodeStates = _safeParse(cp.node_states, []);
755
776
 
756
777
  const restoredLog = [
757
778
  ...nodeStates,
@@ -792,6 +813,9 @@ export function setBreakpoint(db, workflowId, nodeId, condition = null) {
792
813
  if (!workflow) {
793
814
  throw new Error(`Workflow not found: ${workflowId}`);
794
815
  }
816
+ if (!Array.isArray(workflow.dag)) {
817
+ throw new Error(`Workflow ${workflowId} has a corrupt DAG`);
818
+ }
795
819
  const nodeExists = workflow.dag.some((s) => s.id === nodeId);
796
820
  if (!nodeExists) {
797
821
  throw new Error(`Node ${nodeId} not in workflow ${workflowId}`);