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.
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/AIOps-BsDJlD_l.js +1 -0
- package/src/assets/web-panel/assets/{ActionButton-Bqvaz1Yj.js → ActionButton-COjZZKWG.js} +1 -1
- package/src/assets/web-panel/assets/Analytics-4ZDZE7lc.js +3 -0
- package/src/assets/web-panel/assets/{AppLayout-cs0TRZya.js → AppLayout-CYH5k2Rp.js} +5 -5
- package/src/assets/web-panel/assets/Audit-BKlFbLdl.js +1 -0
- package/src/assets/web-panel/assets/Backup-CiX61Qc2.js +1 -0
- package/src/assets/web-panel/assets/{BaseInput-4qhQiJfI.js → BaseInput-CDxIQokd.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DUFX3Mv4.js → Chat-CaKgdRab.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DgvpwU5o.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-C4eyD_nB.js → Checkbox-CjiWfu4s.js} +1 -1
- package/src/assets/web-panel/assets/Codegen-tKWGzajw.js +1 -0
- package/src/assets/web-panel/assets/{Col-3eHStfvJ.js → Col-ZKWREyNs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DFbKjEgh.js → Community-CmLuPBUU.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-D5str1h-.js → Compact-DqknM98n.js} +1 -1
- package/src/assets/web-panel/assets/Compliance-PZw0aBLI.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-Cdliboj6.js → Cowork-Dp29kHsC.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Bfmwl6iZ.js → Cron-zmEJAetz.js} +2 -2
- package/src/assets/web-panel/assets/Crosschain-Sb-uM7Ty.js +1 -0
- package/src/assets/web-panel/assets/{DID-xqi72kjo.js → DID-D_UFNzJ5.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-4l68QdSQ.js → Dashboard-Btag698v.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-C2wsZOdn.js → Dropdown-CH7l3KCs.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-Bud2M3XX.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CHK5rapA.js → FamilyGuardDashboard-Bd2_8uME.js} +1 -1
- package/src/assets/web-panel/assets/Federation-1jhstF5P.js +1 -0
- package/src/assets/web-panel/assets/{FormItemContext-lLOEHvAb.js → FormItemContext-CpSH464Y.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CaDh2XiY.js → GenericCardRenderer-CANo8O-y.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B9SmGPMU.js → Git-hvuZVoD3.js} +2 -2
- package/src/assets/web-panel/assets/Governance-BQrWksKt.js +1 -0
- package/src/assets/web-panel/assets/Inference-jEBTp12v.js +1 -0
- package/src/assets/web-panel/assets/KnowledgeGraph-tDiV2taf.js +1 -0
- package/src/assets/web-panel/assets/Logs-DDkTnedu.js +2 -0
- package/src/assets/web-panel/assets/Marketplace-9Yi32avt.js +1 -0
- package/src/assets/web-panel/assets/{McpTools-Dxlypa0R.js → McpTools-Qx78XyOT.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BxBHR7eW.js → Memory-CXYDO1oT.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-MfUfkHA0.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-59gyfK6T.js → MobileProjects-DEDbWNIk.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-2YBBawFi.js → Mtc-Dwa_vlR8.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DczAeHfM.js → MtcAudit-DAVkxhJv.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CPxyt7a-.js → Multisig-C3upmgPN.js} +3 -3
- package/src/assets/web-panel/assets/NLProgramming-DO3CZ0_z.js +1 -0
- package/src/assets/web-panel/assets/{Notes-tHRMLOQ3.js → Notes-B1Oy3FbC.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DxIYUWPW.js → NotificationSettings-Bs4-Fc6b.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CEy1pIeq.js +1 -0
- package/src/assets/web-panel/assets/Organization-D4cJ1UNo.js +4 -0
- package/src/assets/web-panel/assets/{Overflow-DrdVw_fC.js → Overflow-fdzvlF7x.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CMBYxejX.js → P2P-Y13PwXBA.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-Ck1zCLe6.js +7 -0
- package/src/assets/web-panel/assets/Permissions-CvEXP6LC.js +4 -0
- package/src/assets/web-panel/assets/{PersonalDataHub-nuqNTK2w.js → PersonalDataHub-JeP7IWMW.js} +3 -3
- package/src/assets/web-panel/assets/Pipeline-BZ7wRzG1.js +1 -0
- package/src/assets/web-panel/assets/Privacy-BJ6qj9Hv.js +1 -0
- package/src/assets/web-panel/assets/{ProjectInit-hCWXNnGJ.js → ProjectInit-DeWd9UcS.js} +2 -2
- package/src/assets/web-panel/assets/ProjectSettings-DUpVuU9F.js +2 -0
- package/src/assets/web-panel/assets/{Projects-C6l6z4Mo.js → Projects-y1WbI337.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-kEifEWLb.js → Providers-DECW00ek.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-B6oLUYKR.js → QuickAsk-Dl-pK9Io.js} +1 -1
- package/src/assets/web-panel/assets/Recommend-Bju04wTd.js +1 -0
- package/src/assets/web-panel/assets/Reputation-BUPa3nEk.js +1 -0
- package/src/assets/web-panel/assets/{Row-W6LSmdzh.js → Row-WYqqaq_5.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DJi97lWh.js → RssFeed-iyQT5q9l.js} +3 -3
- package/src/assets/web-panel/assets/Search-CrfwJF0R.js +1 -0
- package/src/assets/web-panel/assets/{Security-BtawKhmm.js → Security-c4Jxf5Ih.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BdCe11of.js → Services-CRuisolj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C_wcDxpN.js → Skeleton-CCnzJkb-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-n2GgEfQ1.js → Skills-CZURCwZg.js} +1 -1
- package/src/assets/web-panel/assets/Sla-CrMzaEi2.js +1 -0
- package/src/assets/web-panel/assets/SpeechSettings-jTDOM5eY.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-BBbldyeR.js → SyncSettings-CJWVtd87.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DMkBOCW-.js → Tasks-61lrQ_lS.js} +1 -1
- package/src/assets/web-panel/assets/Templates-Cg-iF2g1.js +1 -0
- package/src/assets/web-panel/assets/Tenant-DrDv1FFc.js +1 -0
- package/src/assets/web-panel/assets/Terminal-CP15hcNS.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Drf_B0al.js → TimelineRenderer-Sl5uXrkN.js} +1 -1
- package/src/assets/web-panel/assets/Tokens-yRIZTVsR.js +1 -0
- package/src/assets/web-panel/assets/{Trigger-CNUB_qRQ.js → Trigger-BLCQEOF2.js} +1 -1
- package/src/assets/web-panel/assets/Trust-D5xq8z6s.js +1 -0
- package/src/assets/web-panel/assets/{UkeySign-7o1O51qV.js → UkeySign-Dwp0zXQ6.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Ccb1cTxo.js → VideoEditing-CIHA55Sx.js} +1 -1
- package/src/assets/web-panel/assets/Wallet-Hjajvnto.js +4 -0
- package/src/assets/web-panel/assets/WebAuthn-DqSURUvI.js +5 -0
- package/src/assets/web-panel/assets/{WorkflowEditor-BcFvWc2d.js → WorkflowEditor-B5bHRwUG.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CPvCSHY4.js → chat-DsheLKkG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Ca7MoLtb.js → colors-CST2UkgL.js} +1 -1
- package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +3 -0
- package/src/assets/web-panel/assets/{compact-item-B9AUAVCQ.js → compact-item-yqEoy1L7.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CnMJHqHw.js → createContext-Jwp78HFY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-C8to6gQk.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bevd8pXd.js → hasIn-CVS5mFEP.js} +1 -1
- package/src/assets/web-panel/assets/{index-gR1xGRvy.js → index-AnW25bEq.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3zlqc60.js → index-BCHAUdel.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzmhHe0m.js → index-BLTUVd0V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM1tI3g5.js → index-BeblNJDH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAKYScOz.js → index-BuI27WSx.js} +1 -1
- package/src/assets/web-panel/assets/{index-PidVLx0u.js → index-BzetOasL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dnk8TOm3.js → index-C0FZScMe.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvhLkwFR.js → index-C130LLPq.js} +1 -1
- package/src/assets/web-panel/assets/{index-0DtNS6oi.js → index-C8rq85gu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUlsrbu2.js → index-CDAPnZUs.js} +1 -1
- package/src/assets/web-panel/assets/{index-HdGuA1pS.js → index-CIE2n8k_.js} +1 -1
- package/src/assets/web-panel/assets/{index-ecThTNhE.js → index-CRBbVS43.js} +1 -1
- package/src/assets/web-panel/assets/{index-DW3rTBy_.js → index-CSBPc-sI.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5i4gKvN.js → index-CTd3lDxp.js} +1 -1
- package/src/assets/web-panel/assets/index-CUWj5L2s.js +1 -0
- package/src/assets/web-panel/assets/{index-B619_m_T.js → index-ChRL6rgA.js} +1 -1
- package/src/assets/web-panel/assets/{index-tUD_CWx-.js → index-CjfN2CpJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcRDjaEi.js → index-Cjig5i3a.js} +3 -3
- package/src/assets/web-panel/assets/{index-Ct3wP67S.js → index-Crk4KWLW.js} +1 -1
- package/src/assets/web-panel/assets/{index-p1rI3d66.js → index-CrlRa054.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPk2HXTA.js → index-CtyEOGuj.js} +1 -1
- package/src/assets/web-panel/assets/{index-aB7GLimB.js → index-Cu6Ql64n.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFkrnvs_.js → index-Cx_t5rWw.js} +1 -1
- package/src/assets/web-panel/assets/{index-ienIjr7_.js → index-Cy0dNzkp.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAjYLhik.js → index-CzsKK0tQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGEc6vlv.js → index-DDAigsel.js} +1 -1
- package/src/assets/web-panel/assets/index-DZfnYE6e.js +1 -0
- package/src/assets/web-panel/assets/{index-C3biz3RB.js → index-D_1b7uh6.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9VR1tES.js → index-D_e9gwfn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGBbFzIJ.js → index-DbxAlpPC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVxhWXMd.js → index-De600Jjm.js} +1 -1
- package/src/assets/web-panel/assets/{index-2KMpdEzs.js → index-Dgyn8-wN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQKVKyjf.js → index-DiK4vSZa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtIv89mI.js → index-DpYn5v2k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTxmgfz2.js → index-DxaU_lza.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd62bwRd.js → index-Dxljh9Fu.js} +1 -1
- package/src/assets/web-panel/assets/{index-kUeSjyHt.js → index-R-VGFpi6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwDJvHfR.js → index-SjxCCf5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-BV1U7tR6.js → index-diWkx2Wq.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-dsKHYCPH.js → initDefaultProps-ClAjrsQ-.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DdPCK2zh.js → motion-BalXv_60.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bi0bmOjg.js → move-DMelzIW-.js} +1 -1
- package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +1 -0
- package/src/assets/web-panel/assets/{omit-DCrBiySU.js → omit-oxecfU3b.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CcJuqfya.js → pickAttrs-DJ5F5M6x.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DFGuzth-.js → placementArrow-DdbKsant.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B_WDlgvI.js → responsiveObserve-BUuM5cmS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BSFabVgO.js → slide-DSV0ccvR.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CebvsGH8.js → statusUtils-B-6oLAXf.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CMuE7NWy.js → styleChecker-DdCAHtsZ.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-RWGVgZNu.js → useFlexGapSupport-ZORkcoZd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-srxzzfG_.js → useFs-BFp46LoP.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-B5OwxZuK.js → usePersonalDataHub-H7rxgbdW.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CXOEebAH.js → vnode-eIq4Tk0J.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DKNwqG2v.js → zoom-CTNrv8-H.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/ask.js +24 -1
- package/src/commands/init.js +5 -4
- package/src/commands/instinct.js +18 -5
- package/src/commands/mcp.js +30 -3
- package/src/commands/memory.js +18 -5
- package/src/gateways/http/envelope-http-server.js +17 -2
- package/src/gateways/terminal/PtyManager.js +15 -2
- package/src/gateways/ws/ws-server.js +20 -0
- package/src/gateways/ws/ws-session-gateway.js +16 -0
- package/src/harness/mcp-client.js +124 -6
- package/src/harness/worktree-isolator.js +20 -9
- package/src/lib/agent-core.js +3 -0
- package/src/lib/autonomous-agent.js +7 -6
- package/src/lib/chat-intent-service.js +4 -2
- package/src/lib/checkpoint-store.js +23 -0
- package/src/lib/credential-guard.js +291 -0
- package/src/lib/ensure-utf8.js +6 -1
- package/src/lib/git-integration.js +57 -1
- package/src/lib/hook-manager.js +15 -6
- package/src/lib/hook-runner.cjs +11 -1
- package/src/lib/interactive-planner.js +4 -3
- package/src/lib/json-schema-output.js +47 -0
- package/src/lib/learning/reflection-engine.js +4 -3
- package/src/lib/learning/skill-improver.js +4 -3
- package/src/lib/learning/skill-synthesizer.js +4 -3
- package/src/lib/mcp-client.js +1 -0
- package/src/lib/mcp-oauth.js +243 -24
- package/src/lib/mcp-serve.js +56 -6
- package/src/lib/orchestrator.js +4 -2
- package/src/lib/process-manager.js +31 -3
- package/src/lib/repl-completer.js +29 -0
- package/src/lib/repl-rewind.js +58 -0
- package/src/lib/repl-vim.js +13 -1
- package/src/lib/settings-hooks.cjs +12 -3
- package/src/lib/slot-filler.js +4 -3
- package/src/lib/sub-agent-context.js +6 -0
- package/src/lib/web-fetch.js +54 -3
- package/src/lib/workflow-engine.js +35 -11
- package/src/repl/agent-repl.js +52 -2
- package/src/runtime/agent-core.js +324 -26
- package/src/runtime/headless-stream.js +124 -17
- package/src/assets/web-panel/assets/AIOps-BmS46GrQ.js +0 -1
- package/src/assets/web-panel/assets/Analytics-BhCnMbKz.js +0 -3
- package/src/assets/web-panel/assets/Audit-AwLNQzvX.js +0 -1
- package/src/assets/web-panel/assets/Backup-BOglo5g6.js +0 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-LWgyWUF_.js +0 -1
- package/src/assets/web-panel/assets/Codegen-B9SRqAqh.js +0 -1
- package/src/assets/web-panel/assets/Compliance-DDYPQDiG.js +0 -1
- package/src/assets/web-panel/assets/Crosschain-Bfo6RvSg.js +0 -1
- package/src/assets/web-panel/assets/EmailListRenderer-CWh_fKmI.js +0 -1
- package/src/assets/web-panel/assets/Federation-JpiE2iKR.js +0 -1
- package/src/assets/web-panel/assets/Governance-B3ODoSO2.js +0 -1
- package/src/assets/web-panel/assets/Inference-C_jP_nlz.js +0 -1
- package/src/assets/web-panel/assets/KnowledgeGraph-zYwypnwu.js +0 -1
- package/src/assets/web-panel/assets/Logs-D6owai5o.js +0 -2
- package/src/assets/web-panel/assets/Marketplace-umRB80w4.js +0 -1
- package/src/assets/web-panel/assets/MobileBridge-Car9kLtE.js +0 -3
- package/src/assets/web-panel/assets/NLProgramming-DvKJ8imu.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BMpVfQkg.js +0 -1
- package/src/assets/web-panel/assets/Organization-BBsie-kN.js +0 -4
- package/src/assets/web-panel/assets/PdhVaultBrowser-BBHzSaeq.js +0 -7
- package/src/assets/web-panel/assets/Permissions-W4kD06Cj.js +0 -4
- package/src/assets/web-panel/assets/Pipeline-DzCX3yHy.js +0 -1
- package/src/assets/web-panel/assets/Privacy-9ENAJQOC.js +0 -1
- package/src/assets/web-panel/assets/ProjectSettings--BzvEaxE.js +0 -2
- package/src/assets/web-panel/assets/Recommend-BIEMGN0q.js +0 -1
- package/src/assets/web-panel/assets/Reputation-D-9AEaRr.js +0 -1
- package/src/assets/web-panel/assets/Search-DOEB2utf.js +0 -1
- package/src/assets/web-panel/assets/Sla-D5WfrvLP.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-D6qCWpan.js +0 -1
- package/src/assets/web-panel/assets/Templates-DbLImR1I.js +0 -1
- package/src/assets/web-panel/assets/Tenant-B0qM4w6Z.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BjKUkiR1.js +0 -3
- package/src/assets/web-panel/assets/Tokens-DgCx2Bfa.js +0 -1
- package/src/assets/web-panel/assets/Trust-JQUTjXXI.js +0 -1
- package/src/assets/web-panel/assets/Wallet-EMf9MlLK.js +0 -4
- package/src/assets/web-panel/assets/WebAuthn-DOYoaCmi.js +0 -5
- package/src/assets/web-panel/assets/community-parser-CO25nZFt.js +0 -3
- package/src/assets/web-panel/assets/devWarning-vFgm_Ssk.js +0 -1
- package/src/assets/web-panel/assets/index-BGUjPh7h.js +0 -1
- package/src/assets/web-panel/assets/index-Cx-wP5XE.js +0 -1
- 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.
|
|
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.
|
|
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
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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 =
|
|
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 =
|
|
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]`
|
package/src/lib/agent-core.js
CHANGED
|
@@ -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
|
|
476
|
-
if (
|
|
477
|
-
return JSON.parse(
|
|
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
|
|
488
|
-
if (
|
|
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
|
-
|
|
80
|
-
|
|
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
|
+
};
|
package/src/lib/ensure-utf8.js
CHANGED
|
@@ -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
|
}
|