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
@@ -18,6 +18,8 @@ import { EventEmitter } from "events";
18
18
  export const _deps = {
19
19
  spawn,
20
20
  fetch: (...args) => globalThis.fetch(...args),
21
+ // Backoff sleep seam (tests override with a no-op so retries don't wait).
22
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
21
23
  };
22
24
 
23
25
  /**
@@ -95,6 +97,37 @@ export function isLikelyConnectionError(err) {
95
97
  );
96
98
  }
97
99
 
100
+ /** Capability-discovery retry tuning (Claude-Code 2.1.191 "short backoff"). */
101
+ const MCP_DISCOVERY_RETRIES = 2; // up to 3 attempts total
102
+ const MCP_DISCOVERY_BACKOFF_MS = 250; // 250ms, 500ms
103
+
104
+ /**
105
+ * Hard cap on the unterminated stdout tail buffered per stdio server. JSON-RPC
106
+ * frames are newline-delimited; a runaway or non-MCP process that streams
107
+ * without ever emitting a newline would otherwise grow `entry._buffer` without
108
+ * bound and exhaust memory. 16M chars is far above any legitimate single MCP
109
+ * message (even a tool result with embedded base64), so crossing it signals a
110
+ * misbehaving server. Override per server with `config.maxBufferChars` (0
111
+ * disables the cap).
112
+ */
113
+ const MCP_MAX_BUFFER_CHARS = 16 * 1024 * 1024;
114
+
115
+ /**
116
+ * Is this a TRANSIENT error worth a short retry during capability discovery?
117
+ * Narrower than isLikelyConnectionError: connection-level failures and 5xx
118
+ * server errors retry, but 4xx (auth 401/403, not-found 404 = permanent for the
119
+ * same request), already-waited timeouts, and a dead stdio process do not —
120
+ * retrying those just wastes attempts.
121
+ */
122
+ export function isTransientMcpError(err) {
123
+ const msg = String((err && err.message) || err || "");
124
+ if (/HTTP 4\d\d\b/i.test(msg)) return false; // any 4xx is permanent here
125
+ if (/Request timeout/i.test(msg)) return false; // already spent the full budget
126
+ return /fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|EPIPE|socket hang up|network error|HTTP 5\d\d\b/i.test(
127
+ msg,
128
+ );
129
+ }
130
+
98
131
  /**
99
132
  * MCP Client — manages connections to MCP servers.
100
133
  */
@@ -271,14 +304,28 @@ export class MCPClient extends EventEmitter {
271
304
  failPending(`MCP server "${name}" process error: ${err.message}`);
272
305
  this.emit("server-error", { name, error: err.message });
273
306
  });
307
+
308
+ // Writing to a stdio server that closed its stdin read end (or died
309
+ // mid-write) makes the stdin pipe emit an asynchronous 'error' (EPIPE).
310
+ // An 'error' event with no listener is an uncaught exception in Node and
311
+ // would CRASH the whole CLI — and the try/catch around stdin.write only
312
+ // catches synchronous throws, not this async event. Handle it: drain
313
+ // in-flight requests and surface it, mirroring the process 'error' path.
314
+ if (proc.stdin && typeof proc.stdin.on === "function") {
315
+ proc.stdin.on("error", (err) => {
316
+ entry.state = ServerState.ERROR;
317
+ failPending(`MCP server "${name}" stdin error: ${err.message}`);
318
+ this.emit("server-error", { name, error: err.message });
319
+ });
320
+ }
274
321
  } else {
275
322
  throw new Error(
276
323
  `transport "${transportKind}" is not supported by this client`,
277
324
  );
278
325
  }
279
326
 
280
- // Initialize MCP protocol
281
- const initResult = await this._sendRequest(name, "initialize", {
327
+ // Initialize MCP protocol (retried on transient network errors).
328
+ const initResult = await this._requestWithRetry(name, "initialize", {
282
329
  protocolVersion: "2024-11-05",
283
330
  capabilities: { tools: {}, resources: {}, prompts: {} },
284
331
  clientInfo: { name: "chainlesschain-cli", version: "0.37.9" },
@@ -302,7 +349,11 @@ export class MCPClient extends EventEmitter {
302
349
  const advertisesTools =
303
350
  entry.capabilities && entry.capabilities.tools !== undefined;
304
351
  try {
305
- const toolsResult = await this._sendRequest(name, "tools/list", {});
352
+ const toolsResult = await this._requestWithRetry(
353
+ name,
354
+ "tools/list",
355
+ {},
356
+ );
306
357
  entry.tools = toolsResult?.tools || [];
307
358
  } catch (err) {
308
359
  if (advertisesTools) {
@@ -358,6 +409,7 @@ export class MCPClient extends EventEmitter {
358
409
  try {
359
410
  entry.process.stdout?.removeAllListeners();
360
411
  entry.process.stderr?.removeAllListeners();
412
+ entry.process.stdin?.removeAllListeners?.();
361
413
  entry.process.removeAllListeners();
362
414
  entry.process.kill();
363
415
  } catch {
@@ -590,6 +642,34 @@ export class MCPClient extends EventEmitter {
590
642
 
591
643
  // ─── Internal JSON-RPC transport ──────────────────────────────
592
644
 
645
+ /**
646
+ * Capability-discovery request with a short backoff retry on TRANSIENT
647
+ * network errors (Claude-Code 2.1.191: "capability discovery now retries
648
+ * transient network errors with short backoff"). Permanent errors (4xx,
649
+ * JSON-RPC errors, timeouts, dead stdio process) throw on the first failure.
650
+ */
651
+ async _requestWithRetry(
652
+ serverName,
653
+ method,
654
+ params,
655
+ attempts = MCP_DISCOVERY_RETRIES,
656
+ ) {
657
+ for (let i = 0; ; i++) {
658
+ try {
659
+ return await this._sendRequest(serverName, method, params);
660
+ } catch (err) {
661
+ if (i >= attempts || !isTransientMcpError(err)) throw err;
662
+ this.emit("server-retry", {
663
+ name: serverName,
664
+ method,
665
+ attempt: i + 1,
666
+ error: err?.message || String(err),
667
+ });
668
+ await _deps.sleep(MCP_DISCOVERY_BACKOFF_MS * (i + 1));
669
+ }
670
+ }
671
+ }
672
+
593
673
  _sendRequest(serverName, method, params) {
594
674
  const entry = this.servers.get(serverName);
595
675
  if (!entry) return Promise.reject(new Error("Server not available"));
@@ -719,9 +799,16 @@ export class MCPClient extends EventEmitter {
719
799
  if (!response.ok) {
720
800
  const text =
721
801
  typeof response.text === "function" ? await response.text() : "";
722
- throw new Error(
723
- `HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`,
724
- );
802
+ const detail = text ? `: ${text.slice(0, 200)}` : "";
803
+ // 404 usually means a wrong/stale server URL — name it and point at the
804
+ // MCP config (Claude-Code 2.1.191: "HTTP 404 errors now show the URL and
805
+ // point to your MCP config") instead of a bare "HTTP 404".
806
+ if (response.status === 404) {
807
+ throw new Error(
808
+ `HTTP 404${detail} — ${entry.httpUrl} returned Not Found; check this server's "url" in your MCP config`,
809
+ );
810
+ }
811
+ throw new Error(`HTTP ${response.status}${detail}`);
725
812
  }
726
813
 
727
814
  const contentType = response.headers?.get
@@ -807,6 +894,37 @@ export class MCPClient extends EventEmitter {
807
894
  // Skip malformed lines
808
895
  }
809
896
  }
897
+
898
+ // Guard against unbounded buffer growth: a runaway / non-MCP server that
899
+ // streams without ever sending a newline would grow the unterminated tail
900
+ // forever and exhaust memory. If the leftover partial line exceeds the cap,
901
+ // treat it as a fatal transport error (drop the buffer, drain in-flight
902
+ // requests, kill the process) rather than letting it grow without limit.
903
+ const cap = Number.isFinite(entry.config?.maxBufferChars)
904
+ ? entry.config.maxBufferChars
905
+ : MCP_MAX_BUFFER_CHARS;
906
+ if (cap > 0 && entry._buffer.length > cap) {
907
+ entry._buffer = "";
908
+ entry.state = ServerState.ERROR;
909
+ const errMsg = `MCP server "${serverName}" exceeded the ${cap}-char line buffer with no newline (runaway or non-MCP output)`;
910
+ for (const [, pending] of entry._pending) {
911
+ if (pending.timeout) clearTimeout(pending.timeout);
912
+ try {
913
+ pending.reject(new Error(errMsg));
914
+ } catch {
915
+ // already settled — ignore
916
+ }
917
+ }
918
+ entry._pending.clear();
919
+ if (entry.process) {
920
+ try {
921
+ entry.process.kill();
922
+ } catch {
923
+ // best-effort — surfacing the error is what matters
924
+ }
925
+ }
926
+ this.emit("server-error", { name: serverName, error: errMsg });
927
+ }
810
928
  }
811
929
 
812
930
  _handleMessage(serverName, msg) {
@@ -13,7 +13,9 @@ import { resolve } from "node:path";
13
13
  import {
14
14
  isGitRepo,
15
15
  gitExec,
16
+ gitExecArgs,
16
17
  assertSafeGitRef,
18
+ assertSafeGitPath,
17
19
  } from "../lib/git-integration.js";
18
20
 
19
21
  const WORKTREE_DIR = ".worktrees";
@@ -167,6 +169,7 @@ export function diffWorktree(repoDir, branchName, options = {}) {
167
169
  if (options.baseBranch) assertSafeGitRef(options.baseBranch, "base branch");
168
170
  const base = options.baseBranch || "HEAD";
169
171
  const filePath = options.filePath || null;
172
+ if (filePath) assertSafeGitPath(filePath, "file path");
170
173
  const fileArg = filePath ? ` -- "${filePath}"` : "";
171
174
  const statOutput = gitExec(
172
175
  `diff ${base}...${branchName} --stat --numstat${fileArg}`,
@@ -223,14 +226,13 @@ export function mergeWorktree(repoDir, branchName, options = {}) {
223
226
  const msg =
224
227
  options.message ||
225
228
  `feat: merge agent work from ${branchName} (squashed)`;
226
- gitExec(`commit -m "${msg.replace(/"/g, '\\"')}"`, repoDir);
229
+ // Free-text message → argv (no shell) so `$()`/backticks/quotes in it
230
+ // can't inject a command (the old `-m "…"` only escaped `"`).
231
+ gitExecArgs(["commit", "-m", msg], repoDir);
227
232
  } else {
228
233
  const msg =
229
234
  options.message || `Merge branch '${branchName}' (agent task)`;
230
- gitExec(
231
- `merge ${branchName} --no-edit -m "${msg.replace(/"/g, '\\"')}"`,
232
- repoDir,
233
- );
235
+ gitExecArgs(["merge", branchName, "--no-edit", "-m", msg], repoDir);
234
236
  }
235
237
 
236
238
  if (deleteBranch) {
@@ -393,6 +395,7 @@ export function applyWorktreeAutomationCandidate(
393
395
  const conflictType = options.conflictType || null;
394
396
  const baseBranch = _resolveBaseBranchForMerge(repoDir, options.baseBranch);
395
397
 
398
+ if (filePath) assertSafeGitPath(filePath, "file path");
396
399
  if (!filePath) {
397
400
  throw new Error("filePath is required");
398
401
  }
@@ -514,8 +517,10 @@ export function worktreeLog(repoDir, branchName, baseBranch = "HEAD") {
514
517
 
515
518
  function _fileStatus(repoDir, base, branch, filePath) {
516
519
  try {
517
- const output = gitExec(
518
- `diff ${base}...${branch} --name-status -- "${filePath}"`,
520
+ // filePath here comes from git's conflict output (a tracked file name);
521
+ // pass it via argv so an exotic / maliciously-named file can't inject.
522
+ const output = gitExecArgs(
523
+ ["diff", `${base}...${branch}`, "--name-status", "--", filePath],
519
524
  repoDir,
520
525
  );
521
526
  const status = output.trim().charAt(0);
@@ -603,7 +608,10 @@ function _buildConflictSummary(repoDir, filePath, branchName) {
603
608
 
604
609
  function _conflictStatusCode(repoDir, filePath) {
605
610
  try {
606
- const output = gitExec(`status --porcelain -- "${filePath}"`, repoDir);
611
+ const output = gitExecArgs(
612
+ ["status", "--porcelain", "--", filePath],
613
+ repoDir,
614
+ );
607
615
  const firstLine = output.split("\n").find((line) => line.trim());
608
616
  return firstLine ? firstLine.slice(0, 2) : "UU";
609
617
  } catch (_e) {
@@ -779,7 +787,10 @@ function _buildDiffPreview(repoDir, filePath, branchName) {
779
787
  };
780
788
 
781
789
  try {
782
- const diff = gitExec(`diff HEAD...${branchName} -- "${filePath}"`, repoDir);
790
+ const diff = gitExecArgs(
791
+ ["diff", `HEAD...${branchName}`, "--", filePath],
792
+ repoDir,
793
+ );
783
794
  preview.snippet =
784
795
  diff.length > 2000
785
796
  ? `${diff.slice(0, 2000)}\n... [diff truncated]`
@@ -3,8 +3,11 @@ export {
3
3
  AGENT_TOOLS,
4
4
  AGENT_TOOL_REGISTRY,
5
5
  MAX_SUB_AGENT_DEPTH,
6
+ MAX_SUB_AGENTS_PER_RUN,
6
7
  MAX_TOOL_RESULT_CHARS,
7
8
  capToolResultString,
9
+ safeStringifyToolResult,
10
+ tokenizeShellWords,
8
11
  reloadSkills,
9
12
  getAgentToolDefinitions,
10
13
  getAgentToolDescriptors,
@@ -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
  // Exported for test injection
13
14
  export const _deps = {
@@ -471,10 +472,10 @@ Reply with a JSON object: { "action": "retry|add_step|skip", "newParams": {...},
471
472
 
472
473
  _parseSteps(text) {
473
474
  try {
474
- // Extract JSON array from response
475
- const match = text.match(/\[[\s\S]*\]/);
476
- if (match) {
477
- return JSON.parse(match[0]);
475
+ // Extract the first balanced JSON array from the response
476
+ const jsonText = firstBalancedJson(text, "[");
477
+ if (jsonText) {
478
+ return JSON.parse(jsonText);
478
479
  }
479
480
  } catch (_err) {
480
481
  // Parse failure
@@ -484,8 +485,8 @@ Reply with a JSON object: { "action": "retry|add_step|skip", "newParams": {...},
484
485
 
485
486
  _parseJSON(text) {
486
487
  try {
487
- const match = text.match(/\{[\s\S]*\}/);
488
- if (match) return JSON.parse(match[0]);
488
+ const jsonText = firstBalancedJson(text, "{");
489
+ if (jsonText) return JSON.parse(jsonText);
489
490
  } catch (_err) {
490
491
  // Parse failure
491
492
  }
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { chatWithStreaming, chatStream } from "./chat-core.js";
21
+ import { firstBalancedJson } from "./json-schema-output.js";
21
22
 
22
23
  const DEFAULT_INTENT_TIMEOUT_MS = 15000;
23
24
 
@@ -76,8 +77,9 @@ function extractJson(content) {
76
77
  if (fenced) return fenced[1].trim();
77
78
  const generic = content.match(/```\s*([\s\S]*?)```/);
78
79
  if (generic) return generic[1].trim();
79
- const bare = content.match(/\{[\s\S]*\}/);
80
- return bare ? bare[0] : null;
80
+ // Balanced extraction stops at the first complete object instead of the old
81
+ // greedy /\{[\s\S]*\}/, which over-captured trailing prose with a stray }.
82
+ return firstBalancedJson(content, "{");
81
83
  }
82
84
 
83
85
  /**
@@ -256,6 +256,29 @@ export function createCheckpoint(cwd = process.cwd(), opts = {}) {
256
256
  /* non-critical */
257
257
  }
258
258
 
259
+ // Bound the session's checkpoint history when asked. Auto-checkpointing
260
+ // (one ref per mutating tool) passes a cap so a long agentic run can't
261
+ // accumulate unbounded refs — which also keeps nextId's per-create
262
+ // for-each-ref scan bounded. Best-effort: pruning never fails the
263
+ // checkpoint, and the dropped commit objects stay reachable via newer
264
+ // checkpoints' parent chain (only addressability by id is lost for the
265
+ // oldest entries). Manual `cc checkpoint create` omits the cap → unbounded.
266
+ if (Number.isFinite(opts.maxPerSession) && opts.maxPerSession > 0) {
267
+ try {
268
+ const rows = listRefs(root, session); // oldest-first (creatordate)
269
+ const excess = rows.length - opts.maxPerSession;
270
+ for (let i = 0; i < excess; i++) {
271
+ try {
272
+ git(["update-ref", "-d", rows[i].ref], { cwd: root });
273
+ } catch {
274
+ /* best-effort — a failed prune never affects the new checkpoint */
275
+ }
276
+ }
277
+ } catch {
278
+ /* pruning is entirely best-effort */
279
+ }
280
+ }
281
+
259
282
  return { id, ref, commit, tree, parent, label, session, createdAt, files };
260
283
  } finally {
261
284
  rmQuiet(tmpIndex);
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Credential READ guard — Claude-Code 2.1.189 parity ("`sandbox.credentials`
3
+ * setting blocks sandboxed commands from reading credential files and secret
4
+ * environment variables").
5
+ *
6
+ * cc has no OS sandbox (by design), but the same intent applies one layer up:
7
+ * the agent must not silently slurp the user's secrets into model context,
8
+ * where they could be echoed back, logged, summarized into a transcript, or
9
+ * exfiltrated by a later tool call. This guard flags two read paths —
10
+ * 1. the `read_file` tool pointed at a credential file (.env, ~/.aws/credentials,
11
+ * private keys, …), and
12
+ * 2. `run_shell` commands that read a credential file (`cat .env`) or print a
13
+ * secret-looking environment variable (`echo $ANTHROPIC_API_KEY`, `printenv`).
14
+ * — so `executeTool` can confirm first (interactive) or fail closed (headless),
15
+ * exactly like the sensitive-file WRITE guard ([[sensitive-file-guard.js]]).
16
+ *
17
+ * An explicit settings `allow` rule pre-authorizes; `CC_CREDENTIAL_GUARD=0`
18
+ * disables it. Deliberately, `--safe-mode` does NOT disable this guard: like
19
+ * permission deny rules, it is a SAFETY surface, not a customization — "debug
20
+ * my customizations bare" must never widen what secrets the agent may read.
21
+ *
22
+ * Precision over recall: matches are conservative so everyday reads
23
+ * (.env.example, public keys, package.json) never trip a confirm — a guard that
24
+ * cries wolf trains users to blind-approve.
25
+ */
26
+
27
+ import { createRequire } from "node:module";
28
+
29
+ const require_ = createRequire(import.meta.url);
30
+ // Reuse the shell policy's compound-command splitter (separators + $()/backtick
31
+ // extraction). It is regex-based on separators and leaves backslashes intact,
32
+ // so Windows paths survive (unlike the escaping tokenizer, which strips `\`).
33
+ const { splitCommandSegments } = require_(
34
+ "../runtime/coding-agent-shell-policy.cjs",
35
+ );
36
+
37
+ // ── credential FILE patterns ────────────────────────────────────────────────
38
+
39
+ // Exact basenames that are, by convention, secret stores.
40
+ const CREDENTIAL_BASENAMES = new Set([
41
+ ".npmrc",
42
+ ".netrc",
43
+ "_netrc",
44
+ ".pgpass",
45
+ ".git-credentials",
46
+ ".pypirc",
47
+ ".dockercfg",
48
+ "credentials", // ~/.aws/credentials, gcloud, …
49
+ ".credentials",
50
+ "id_rsa",
51
+ "id_dsa",
52
+ "id_ecdsa",
53
+ "id_ed25519", // private SSH keys (the `.pub` siblings have a different basename)
54
+ "id_ecdsa_sk",
55
+ "id_ed25519_sk", // FIDO/security-key SSH keys
56
+ ".htpasswd", // web-server basic-auth hashes
57
+ "kubeconfig", // bare KUBECONFIG file (the ~/.kube/config form is matched below)
58
+ ".terraformrc", // Terraform registry/API tokens
59
+ "terraform.rc",
60
+ ]);
61
+
62
+ // Extensions whose BYTES are the secret (private keys / cert bundles / keyrings).
63
+ // Public material (.pub/.crt/.cert/.cer) is intentionally absent.
64
+ const SECRET_EXT_RE = /\.(pem|key|pfx|p12|keystore|jks|ppk|asc|gpg|pgp)$/i;
65
+
66
+ // `.env` and real env files — but NOT the committed template/example forms.
67
+ const ENV_SAFE_SUFFIX_RE = /\.(example|sample|template|dist|defaults|tpl)$/i;
68
+
69
+ // Directory-scoped credential files (the basename alone is too generic).
70
+ const CREDENTIAL_PATH_RE = [
71
+ /[\\/]\.aws[\\/]credentials$/i,
72
+ /[\\/]\.kube[\\/]config$/i,
73
+ /[\\/]\.docker[\\/]config\.json$/i,
74
+ /[\\/]\.config[\\/]gh[\\/]hosts\.yml$/i, // GitHub CLI OAuth token
75
+ /[\\/]\.gnupg[\\/]/i, // GPG keyring directory
76
+ /service[-_]account[^\\/]*\.json$/i, // GCP service-account key JSON
77
+ ];
78
+
79
+ // secret(s).{json,yml,yaml,env,txt}
80
+ const SECRETS_FILE_RE = /(^|[\\/])secrets?\.(json|ya?ml|env|txt)$/i;
81
+
82
+ function isEnvSecret(base) {
83
+ if (base.toLowerCase() === ".env") return true;
84
+ if (/^\.env\./i.test(base)) return !ENV_SAFE_SUFFIX_RE.test(base);
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * Is this path a credential / secret file the agent should not read unprompted?
90
+ * @param {string} targetPath path as the tool received it (rel or abs)
91
+ * @returns {string|null} human reason when sensitive, null otherwise
92
+ */
93
+ export function credentialFileReason(targetPath) {
94
+ const p = String(targetPath || "");
95
+ if (!p) return null;
96
+ const norm = p.replace(/\\/g, "/");
97
+ const base = norm.split("/").pop() || "";
98
+ if (!base) return null;
99
+ if (isEnvSecret(base)) return `environment/secret file (${base})`;
100
+ if (CREDENTIAL_BASENAMES.has(base.toLowerCase()))
101
+ return `credential file (${base})`;
102
+ if (SECRET_EXT_RE.test(base))
103
+ return `private key / certificate material (${base})`;
104
+ if (SECRETS_FILE_RE.test(norm)) return `secrets file (${base})`;
105
+ for (const re of CREDENTIAL_PATH_RE) {
106
+ if (re.test(norm)) return `credential file (${base})`;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ // ── secret ENV-VAR + command detection ──────────────────────────────────────
112
+
113
+ // Content-reader commands: when one of these is aimed at a credential file, the
114
+ // file's bytes land in the agent's tool output.
115
+ const READ_COMMANDS = new Set([
116
+ "cat",
117
+ "tac",
118
+ "type",
119
+ "gc",
120
+ "get-content",
121
+ "less",
122
+ "more",
123
+ "head",
124
+ "tail",
125
+ "bat",
126
+ "nl",
127
+ "xxd",
128
+ "od",
129
+ "strings",
130
+ ]);
131
+
132
+ // Commands whose job is to print a value into output — the exfil surface for a
133
+ // secret env var (a tool that merely CONSUMES `$API_KEY` does not echo it).
134
+ const PRINT_COMMANDS = new Set([
135
+ "echo",
136
+ "echo.",
137
+ "print",
138
+ "printf",
139
+ "printenv",
140
+ "write-host",
141
+ "write-output",
142
+ "write-information",
143
+ ]);
144
+
145
+ // A var name looks secret when one of these words appears as an underscore- or
146
+ // boundary-delimited segment: ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY,
147
+ // GITHUB_TOKEN, DB_PASSWORD, MY_PASSPHRASE, OPENAI_APIKEY. `MONKEY`/`KEYBOARD`/
148
+ // `TOKENIZER` do NOT match (no boundary).
149
+ const SECRET_VAR_RE =
150
+ /(?:^|_)(KEY|KEYS|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|CREDENTIALS|PRIVATE|APIKEY|ACCESSKEY)(?:_|$)/i;
151
+
152
+ // Ways a secret var is referenced inside a shell segment.
153
+ const SECRET_REF_PATTERNS = [
154
+ /\$env:([A-Za-z_][A-Za-z0-9_]*)/gi, // PowerShell $env:NAME
155
+ /(?:^|[\s"'(=;|&])env:([A-Za-z_][A-Za-z0-9_]*)/gi, // PowerShell `env:` drive (Get-Content env:NAME)
156
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, // ${NAME}
157
+ /\$([A-Za-z_][A-Za-z0-9_]*)/g, // $NAME
158
+ /%([A-Za-z_][A-Za-z0-9_]*)%/g, // %NAME% (cmd.exe)
159
+ ];
160
+
161
+ function referencedSecretVars(segment) {
162
+ const out = [];
163
+ for (const re of SECRET_REF_PATTERNS) {
164
+ re.lastIndex = 0;
165
+ let m;
166
+ while ((m = re.exec(segment))) {
167
+ if (SECRET_VAR_RE.test(m[1])) out.push(m[1]);
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ function baseName(tok) {
174
+ const b =
175
+ String(tok || "")
176
+ .replace(/\\/g, "/")
177
+ .split("/")
178
+ .pop() || "";
179
+ return b.replace(/\.exe$/i, "");
180
+ }
181
+
182
+ function firstCommandToken(segment) {
183
+ const m = segment
184
+ .trim()
185
+ .replace(/^["']/, "")
186
+ .match(/^(\S+)/);
187
+ return m ? baseName(m[1]).toLowerCase() : "";
188
+ }
189
+
190
+ /**
191
+ * Does this (possibly compound) shell command read a credential file or print a
192
+ * secret environment variable?
193
+ * @param {string} command
194
+ * @returns {{reason:string, kind:string, target?:string, segment:string}|null}
195
+ */
196
+ export function commandReadsCredentials(command) {
197
+ const cmd = String(command || "");
198
+ if (!cmd.trim()) return null;
199
+ let segments;
200
+ try {
201
+ segments = splitCommandSegments(cmd);
202
+ } catch {
203
+ segments = [cmd];
204
+ }
205
+ if (!segments.length) segments = [cmd];
206
+
207
+ for (const segment of segments) {
208
+ const seg = String(segment || "");
209
+ if (!seg.trim()) continue;
210
+ const first = firstCommandToken(seg);
211
+ // Backslash-preserving split (the policy's escaping tokenizer would mangle
212
+ // Windows paths); quotes flattened so grouping doesn't hide a token —
213
+ // credentialFileReason keys off the basename, which survives the split.
214
+ const rawTokens = seg.replace(/["']/g, " ").split(/\s+/).filter(Boolean);
215
+ const args = rawTokens.slice(1);
216
+ const nonFlagArgs = args.filter((t) => !t.startsWith("-"));
217
+
218
+ // (a) full-environment dump — printenv/env with no var, or PowerShell
219
+ // `Get-ChildItem env:` family. Reveals EVERY secret at once.
220
+ if (
221
+ ((first === "printenv" || first === "env") && nonFlagArgs.length === 0) ||
222
+ (["get-childitem", "gci", "dir", "ls"].includes(first) &&
223
+ args.some((t) => /^env:?$/i.test(t)))
224
+ ) {
225
+ return {
226
+ reason: "dumps all environment variables (may expose secrets)",
227
+ kind: "env-dump",
228
+ segment: seg,
229
+ };
230
+ }
231
+
232
+ // (b) reading a credential file with a content-reader command.
233
+ if (READ_COMMANDS.has(first)) {
234
+ for (const tok of args) {
235
+ const r = credentialFileReason(tok);
236
+ if (r)
237
+ return {
238
+ reason: `reads ${r}`,
239
+ kind: "file",
240
+ target: tok,
241
+ segment: seg,
242
+ };
243
+ }
244
+ }
245
+
246
+ // (c) printing / reading a secret-looking env var (via $VAR / %VAR% / env:).
247
+ if (PRINT_COMMANDS.has(first) || READ_COMMANDS.has(first)) {
248
+ const vars = referencedSecretVars(seg);
249
+ if (vars.length)
250
+ return {
251
+ reason: `reads secret environment variable ${vars[0]}`,
252
+ kind: "env-var",
253
+ target: vars[0],
254
+ segment: seg,
255
+ };
256
+ }
257
+
258
+ // (d) `printenv ANTHROPIC_API_KEY` — bare var name (no $ sigil).
259
+ if (first === "printenv") {
260
+ const hit = nonFlagArgs.find((t) => SECRET_VAR_RE.test(t));
261
+ if (hit)
262
+ return {
263
+ reason: `reads secret environment variable ${hit}`,
264
+ kind: "env-var",
265
+ target: hit,
266
+ segment: seg,
267
+ };
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+
273
+ /**
274
+ * Whether the credential guard is disabled via `CC_CREDENTIAL_GUARD`
275
+ * (0 / false / no / off). Default ON — only an explicit opt-out turns it off.
276
+ * @param {object} [env=process.env]
277
+ * @returns {boolean}
278
+ */
279
+ export function credentialGuardDisabled(env = process.env) {
280
+ const raw = env && env.CC_CREDENTIAL_GUARD;
281
+ return raw != null && /^(0|false|no|off)$/i.test(String(raw).trim());
282
+ }
283
+
284
+ export const _internals = {
285
+ CREDENTIAL_BASENAMES,
286
+ READ_COMMANDS,
287
+ PRINT_COMMANDS,
288
+ SECRET_VAR_RE,
289
+ referencedSecretVars,
290
+ isEnvSecret,
291
+ };
@@ -45,8 +45,14 @@ export function ensureUtf8() {
45
45
  export function getUtf8SpawnOptions(opts = {}) {
46
46
  if (process.platform !== "win32") return { encoding: "utf-8", ...opts };
47
47
 
48
+ // NOTE: spread `...opts` BEFORE the `env` key so the merged UTF-8 env wins.
49
+ // If `...opts` came last, a caller passing `opts.env` would clobber the whole
50
+ // merge (losing process.env + PYTHONIOENCODING + CHCP) — silently defeating
51
+ // the entire point of this helper. opts.env is still merged in last *within*
52
+ // the env object so caller overrides for individual vars are honored.
48
53
  return {
49
54
  encoding: "utf-8",
55
+ ...opts,
50
56
  env: {
51
57
  ...process.env,
52
58
  PYTHONIOENCODING: "utf-8",
@@ -54,6 +60,5 @@ export function getUtf8SpawnOptions(opts = {}) {
54
60
  CHCP: "65001",
55
61
  ...opts.env,
56
62
  },
57
- ...opts,
58
63
  };
59
64
  }