chainlesschain 0.162.124 → 0.162.126
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-BrTOcqIJ.js +1 -0
- package/src/assets/web-panel/assets/{ActionButton-COjZZKWG.js → ActionButton-na9cocmf.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-4ZDZE7lc.js → Analytics-BtDygkpt.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CYH5k2Rp.js → AppLayout-D_t5CS18.js} +5 -5
- package/src/assets/web-panel/assets/Audit-BYMZccCy.js +1 -0
- package/src/assets/web-panel/assets/{Backup-CiX61Qc2.js → Backup-D2qDfAdL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CDxIQokd.js → BaseInput-kj456Ju9.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CaKgdRab.js → Chat-BY0A0ZLE.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-D1VvrOPQ.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CjiWfu4s.js → Checkbox-DiBhtXsh.js} +1 -1
- package/src/assets/web-panel/assets/Codegen-B1ER0L0j.js +1 -0
- package/src/assets/web-panel/assets/{Col-ZKWREyNs.js → Col-CjLmza7D.js} +1 -1
- package/src/assets/web-panel/assets/Community-BNId05U7.js +1 -0
- package/src/assets/web-panel/assets/{Compact-DqknM98n.js → Compact-BuuMj7K1.js} +1 -1
- package/src/assets/web-panel/assets/Compliance-DUuDycaJ.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-Dp29kHsC.js → Cowork-BKHiF6uL.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-zmEJAetz.js → Cron-C7p-lHnC.js} +2 -2
- package/src/assets/web-panel/assets/Crosschain-BvSOx8a6.js +1 -0
- package/src/assets/web-panel/assets/{DID-D_UFNzJ5.js → DID-D1k9jgHv.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-B9mCCnqY.js +3 -0
- package/src/assets/web-panel/assets/{Dropdown-CH7l3KCs.js → Dropdown-ge9UHSXF.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bud2M3XX.js → EmailListRenderer-TZO7ppXi.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bd2_8uME.js → FamilyGuardDashboard-DkhLJmzx.js} +1 -1
- package/src/assets/web-panel/assets/Federation-JLuA_8zp.js +1 -0
- package/src/assets/web-panel/assets/{FormItemContext-CpSH464Y.js → FormItemContext-C4w-rzNg.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CANo8O-y.js → GenericCardRenderer-Btns4sV9.js} +1 -1
- package/src/assets/web-panel/assets/{Git-hvuZVoD3.js → Git-BheXMjzC.js} +2 -2
- package/src/assets/web-panel/assets/Governance-D9rLlatH.js +1 -0
- package/src/assets/web-panel/assets/Inference-DiklIMcd.js +1 -0
- package/src/assets/web-panel/assets/KnowledgeGraph-C70EWL03.js +1 -0
- package/src/assets/web-panel/assets/{Logs-DDkTnedu.js → Logs-Zt_MLI10.js} +2 -2
- package/src/assets/web-panel/assets/Marketplace-suQxES99.js +1 -0
- package/src/assets/web-panel/assets/{McpTools-Qx78XyOT.js → McpTools-CclpCDpY.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXYDO1oT.js → Memory-B2lHOUwF.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-BMXKoDAD.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-DEDbWNIk.js → MobileProjects-a6mWVdBV.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Dwa_vlR8.js → Mtc-BFpM4Fkj.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DAVkxhJv.js → MtcAudit-DDQkxkbS.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-C3upmgPN.js → Multisig-Crkd_zKQ.js} +3 -3
- package/src/assets/web-panel/assets/NLProgramming-Dpk5An7B.js +1 -0
- package/src/assets/web-panel/assets/{Notes-B1Oy3FbC.js → Notes-BrFnOMNz.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Bs4-Fc6b.js → NotificationSettings-CM0iApFM.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CEy1pIeq.js → OrderTableRenderer-XXjeqkdz.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-D4cJ1UNo.js → Organization-CHFW_38T.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-fdzvlF7x.js → Overflow-C-vl3EjV.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Y13PwXBA.js → P2P-BT7Slavq.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Ck1zCLe6.js → PdhVaultBrowser-DRUaULap.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CvEXP6LC.js → Permissions-DkhOAvMz.js} +2 -2
- package/src/assets/web-panel/assets/{PersonalDataHub-JeP7IWMW.js → PersonalDataHub-Dl7dSOvV.js} +3 -3
- package/src/assets/web-panel/assets/Pipeline-B3nVI4p6.js +1 -0
- package/src/assets/web-panel/assets/Privacy-Dv1SXx1Q.js +1 -0
- package/src/assets/web-panel/assets/{ProjectInit-DeWd9UcS.js → ProjectInit-DrAhVi5p.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DUpVuU9F.js → ProjectSettings-h-ggCYN_.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CM91hYdr.js +1 -0
- package/src/assets/web-panel/assets/{Providers-DECW00ek.js → Providers-rfkZel3N.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Dl-pK9Io.js → QuickAsk-XepZB7h_.js} +1 -1
- package/src/assets/web-panel/assets/Recommend-DzQWsh1a.js +1 -0
- package/src/assets/web-panel/assets/Reputation-CDeaal0j.js +1 -0
- package/src/assets/web-panel/assets/Row-BN4aKhFH.js +1 -0
- package/src/assets/web-panel/assets/{RssFeed-iyQT5q9l.js → RssFeed-INP6VYAA.js} +3 -3
- package/src/assets/web-panel/assets/Search-ChPbwatu.js +1 -0
- package/src/assets/web-panel/assets/{Security-c4Jxf5Ih.js → Security-C97ZCY8N.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CRuisolj.js → Services-Cbtl2Ajs.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CCnzJkb-.js → Skeleton-B13sFxBQ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CZURCwZg.js → Skills-vI9zSIKX.js} +1 -1
- package/src/assets/web-panel/assets/Sla-Cr3oBH39.js +1 -0
- package/src/assets/web-panel/assets/SpeechSettings-BwemqPPV.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-CJWVtd87.js → SyncSettings-CLA78P-Z.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-61lrQ_lS.js → Tasks-DBjQ_DOM.js} +1 -1
- package/src/assets/web-panel/assets/Templates-BkSLDkBs.js +1 -0
- package/src/assets/web-panel/assets/Tenant-Dn0nSlib.js +1 -0
- package/src/assets/web-panel/assets/{Terminal-CP15hcNS.js → Terminal-C8f4boru.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Sl5uXrkN.js → TimelineRenderer-DW_rMtJR.js} +1 -1
- package/src/assets/web-panel/assets/Tokens-BdffrKYV.js +1 -0
- package/src/assets/web-panel/assets/{Trigger-BLCQEOF2.js → Trigger-CX4CUg-Q.js} +1 -1
- package/src/assets/web-panel/assets/Trust-Cq9Bl7Od.js +1 -0
- package/src/assets/web-panel/assets/{UkeySign-Dwp0zXQ6.js → UkeySign-BQy18_VY.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CIHA55Sx.js → VideoEditing-h5hzK7Vw.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Hjajvnto.js → Wallet-BLotT3gw.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-DqSURUvI.js → WebAuthn-8cCANYLE.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B5bHRwUG.js → WorkflowEditor-Db6NwtAv.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DsheLKkG.js → chat-BPk1LbpL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CST2UkgL.js → colors-BnNal71p.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-yqEoy1L7.js → compact-item-W2VC0tcy.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Jwp78HFY.js → createContext-DceVsgnZ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning--rwFDBhx.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CVS5mFEP.js → hasIn-BEq6EDcp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLTUVd0V.js → index-5gIBUFBn.js} +2 -2
- package/src/assets/web-panel/assets/index-APCHxOkM.js +3 -0
- package/src/assets/web-panel/assets/index-B2dMhwZi.js +1 -0
- package/src/assets/web-panel/assets/{index-C0FZScMe.js → index-B8cPjrEH.js} +4 -4
- package/src/assets/web-panel/assets/{index-CjfN2CpJ.js → index-BDC5HsxZ.js} +1 -1
- package/src/assets/web-panel/assets/index-BPFuPGKi.js +1 -0
- package/src/assets/web-panel/assets/{index-SjxCCf5h.js → index-BUkDcIwJ.js} +2 -2
- package/src/assets/web-panel/assets/{index-Cjig5i3a.js → index-BWcCyH6-.js} +28 -26
- package/src/assets/web-panel/assets/{index-AnW25bEq.js → index-BgZikQoh.js} +1 -1
- package/src/assets/web-panel/assets/index-Bwjus13Y.js +1 -0
- package/src/assets/web-panel/assets/index-BzSzRWsw.js +55 -0
- package/src/assets/web-panel/assets/index-C1K9CsGr.js +1 -0
- package/src/assets/web-panel/assets/{index-DDAigsel.js → index-CBZVISK6.js} +2 -2
- package/src/assets/web-panel/assets/index-CEJN-R2L.js +1 -0
- package/src/assets/web-panel/assets/index-CG5dGC9E.js +1 -0
- package/src/assets/web-panel/assets/{index-DxaU_lza.js → index-CMGLpstm.js} +1 -1
- package/src/assets/web-panel/assets/index-CWrGCndd.js +1 -0
- package/src/assets/web-panel/assets/index-CbcvfpCV.js +1 -0
- package/src/assets/web-panel/assets/{index-De600Jjm.js → index-Ceqv4lI2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cy0dNzkp.js → index-CsT6frT8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDAPnZUs.js → index-D8PQKsDT.js} +1 -1
- package/src/assets/web-panel/assets/{index-Crk4KWLW.js → index-DGncdA-j.js} +1 -1
- package/src/assets/web-panel/assets/{index-R-VGFpi6.js → index-DHanQHO0.js} +3 -3
- package/src/assets/web-panel/assets/index-DaUBk28E.js +12 -0
- package/src/assets/web-panel/assets/{index-BzetOasL.js → index-Di2J4b4V.js} +1 -1
- package/src/assets/web-panel/assets/index-DtuLvWZN.js +21 -0
- package/src/assets/web-panel/assets/{index-Dgyn8-wN.js → index-DvS1J8xc.js} +2 -2
- package/src/assets/web-panel/assets/{index-CtyEOGuj.js → index-DxePJdwP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzsKK0tQ.js → index-Et4XvL49.js} +2 -2
- package/src/assets/web-panel/assets/{index-BuI27WSx.js → index-GlFxgcj4.js} +2 -2
- package/src/assets/web-panel/assets/index-HV119Oui.js +1 -0
- package/src/assets/web-panel/assets/index-InGW87pj.js +13 -0
- package/src/assets/web-panel/assets/{index-Dxljh9Fu.js → index-P1-s8JR7.js} +2 -2
- package/src/assets/web-panel/assets/{index-CSBPc-sI.js → index-SdABCNoi.js} +2 -2
- package/src/assets/web-panel/assets/{index-BCHAUdel.js → index-WTAZbN-c.js} +1 -1
- package/src/assets/web-panel/assets/index-_2I-KzOj.js +1 -0
- package/src/assets/web-panel/assets/index-m1sVaWVU.js +1 -0
- package/src/assets/web-panel/assets/index-s37wwyeN.js +1 -0
- package/src/assets/web-panel/assets/{index-CRBbVS43.js → index-tyWABqp9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-ClAjrsQ-.js → initDefaultProps-DzrCDb3j.js} +1 -1
- package/src/assets/web-panel/assets/motion-BQERVnE3.js +11 -0
- package/src/assets/web-panel/assets/{move-DMelzIW-.js → move-I9ACA5XJ.js} +1 -1
- package/src/assets/web-panel/assets/mtc-parser-Dd2Pw-tr.js +1 -0
- package/src/assets/web-panel/assets/{omit-oxecfU3b.js → omit-DwzYRzWV.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DJ5F5M6x.js → pickAttrs-BSGULyIL.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DdbKsant.js → placementArrow-tSYYjAts.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BUuM5cmS.js → responsiveObserve-Dy2lqikT.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DSV0ccvR.js → slide-RaB4Uq0c.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-B-6oLAXf.js → statusUtils-CBpC_wZg.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DdCAHtsZ.js → styleChecker-BEKwPWt5.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZORkcoZd.js → useFlexGapSupport-C4CnYJH1.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BFp46LoP.js → useFs-n24jutw5.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-H7rxgbdW.js → usePersonalDataHub-DMgS8F6G.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-eIq4Tk0J.js → vnode-BSp2KYcj.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CTNrv8-H.js → zoom-7UfQtTPh.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +51 -2
- package/src/commands/compliance.js +3 -1
- package/src/commands/ide.js +60 -5
- package/src/commands/incentive.js +22 -22
- package/src/commands/mtc.js +5 -1
- package/src/commands/review.js +47 -17
- package/src/commands/session.js +11 -2
- package/src/gateways/ws/message-dispatcher.js +149 -166
- package/src/gateways/ws/ws-session-gateway.js +95 -9
- package/src/harness/background-task-manager.js +5 -1
- package/src/harness/jsonl-session-store.js +90 -12
- package/src/harness/mcp-client.js +62 -21
- package/src/harness/prompt-compressor.js +24 -5
- package/src/harness/worktree-isolator.js +10 -1
- package/src/lib/agent-core.js +2 -0
- package/src/lib/audit-logger.js +3 -1
- package/src/lib/autonomous-agent.js +29 -0
- package/src/lib/chat-core.js +5 -2
- package/src/lib/chat-intent-service.js +82 -23
- package/src/lib/checkpoint-store.js +31 -4
- package/src/lib/claude-code-bridge.js +48 -3
- package/src/lib/config-manager.js +25 -1
- package/src/lib/cost-budget.js +13 -2
- package/src/lib/credential-guard.js +81 -1
- package/src/lib/git-integration.js +16 -2
- package/src/lib/goal-store.js +11 -0
- package/src/lib/hook-manager.js +4 -0
- package/src/lib/interaction-adapter.js +32 -11
- package/src/lib/jetbrains-bridge.js +147 -0
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/llm-config-defaults.js +37 -0
- package/src/lib/llm-pricing.js +11 -4
- package/src/lib/llm-providers.js +11 -1
- package/src/lib/mcp-oauth.js +88 -10
- package/src/lib/mcp-serve.js +29 -0
- package/src/lib/orchestrator.js +38 -2
- package/src/lib/permission-engine.js +4 -1
- package/src/lib/pipeline-orchestrator.js +20 -2
- package/src/lib/project-instructions.js +13 -0
- package/src/lib/repl-bang-memorize.js +9 -1
- package/src/lib/repl-denials.js +137 -0
- package/src/lib/runnable-provider.js +7 -1
- package/src/lib/safe-json.js +22 -0
- package/src/lib/sandbox-v2.js +7 -6
- package/src/lib/search-command.js +65 -0
- package/src/lib/session-insights.js +13 -6
- package/src/lib/session-usage.js +21 -3
- package/src/lib/settings-hooks.cjs +133 -0
- package/src/lib/settings-loader.cjs +16 -7
- package/src/lib/social-graph.js +3 -1
- package/src/lib/status-line.cjs +4 -1
- package/src/lib/stream-retry.js +27 -0
- package/src/lib/sub-agent-context.js +9 -0
- package/src/lib/turn-context.js +15 -5
- package/src/repl/agent-repl.js +110 -8
- package/src/runtime/__tests__/message-roles.test.js +36 -3
- package/src/runtime/agent-core.js +179 -52
- package/src/runtime/coding-agent-contract-shared.cjs +9 -2
- package/src/runtime/coding-agent-shell-policy.cjs +23 -2
- package/src/runtime/file-ref-expander.js +51 -7
- package/src/runtime/headless-runner.js +96 -2
- package/src/runtime/headless-stream.js +78 -3
- package/src/runtime/mcp-config.js +114 -4
- package/src/runtime/message-roles.js +14 -1
- package/src/runtime/pipe-safety.js +51 -0
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/AIOps-BsDJlD_l.js +0 -1
- package/src/assets/web-panel/assets/Audit-BKlFbLdl.js +0 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DgvpwU5o.js +0 -1
- package/src/assets/web-panel/assets/Codegen-tKWGzajw.js +0 -1
- package/src/assets/web-panel/assets/Community-CmLuPBUU.js +0 -1
- package/src/assets/web-panel/assets/Compliance-PZw0aBLI.js +0 -1
- package/src/assets/web-panel/assets/Crosschain-Sb-uM7Ty.js +0 -1
- package/src/assets/web-panel/assets/Dashboard-Btag698v.js +0 -3
- package/src/assets/web-panel/assets/Federation-1jhstF5P.js +0 -1
- package/src/assets/web-panel/assets/Governance-BQrWksKt.js +0 -1
- package/src/assets/web-panel/assets/Inference-jEBTp12v.js +0 -1
- package/src/assets/web-panel/assets/KnowledgeGraph-tDiV2taf.js +0 -1
- package/src/assets/web-panel/assets/Marketplace-9Yi32avt.js +0 -1
- package/src/assets/web-panel/assets/MobileBridge-MfUfkHA0.js +0 -1
- package/src/assets/web-panel/assets/NLProgramming-DO3CZ0_z.js +0 -1
- package/src/assets/web-panel/assets/Pipeline-BZ7wRzG1.js +0 -1
- package/src/assets/web-panel/assets/Privacy-BJ6qj9Hv.js +0 -1
- package/src/assets/web-panel/assets/Projects-y1WbI337.js +0 -1
- package/src/assets/web-panel/assets/Recommend-Bju04wTd.js +0 -1
- package/src/assets/web-panel/assets/Reputation-BUPa3nEk.js +0 -1
- package/src/assets/web-panel/assets/Row-WYqqaq_5.js +0 -1
- package/src/assets/web-panel/assets/Search-CrfwJF0R.js +0 -1
- package/src/assets/web-panel/assets/Sla-CrMzaEi2.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-jTDOM5eY.js +0 -1
- package/src/assets/web-panel/assets/Templates-Cg-iF2g1.js +0 -1
- package/src/assets/web-panel/assets/Tenant-DrDv1FFc.js +0 -1
- package/src/assets/web-panel/assets/Tokens-yRIZTVsR.js +0 -1
- package/src/assets/web-panel/assets/Trust-D5xq8z6s.js +0 -1
- package/src/assets/web-panel/assets/community-parser-Cuyslaku.js +0 -3
- package/src/assets/web-panel/assets/devWarning-C8to6gQk.js +0 -1
- package/src/assets/web-panel/assets/index-BeblNJDH.js +0 -1
- package/src/assets/web-panel/assets/index-C130LLPq.js +0 -1
- package/src/assets/web-panel/assets/index-C8rq85gu.js +0 -1
- package/src/assets/web-panel/assets/index-CIE2n8k_.js +0 -1
- package/src/assets/web-panel/assets/index-CTd3lDxp.js +0 -13
- package/src/assets/web-panel/assets/index-CUWj5L2s.js +0 -1
- package/src/assets/web-panel/assets/index-ChRL6rgA.js +0 -3
- package/src/assets/web-panel/assets/index-CrlRa054.js +0 -1
- package/src/assets/web-panel/assets/index-Cu6Ql64n.js +0 -1
- package/src/assets/web-panel/assets/index-Cx_t5rWw.js +0 -12
- package/src/assets/web-panel/assets/index-DZfnYE6e.js +0 -1
- package/src/assets/web-panel/assets/index-D_1b7uh6.js +0 -55
- package/src/assets/web-panel/assets/index-D_e9gwfn.js +0 -1
- package/src/assets/web-panel/assets/index-DbxAlpPC.js +0 -21
- package/src/assets/web-panel/assets/index-DiK4vSZa.js +0 -1
- package/src/assets/web-panel/assets/index-DpYn5v2k.js +0 -1
- package/src/assets/web-panel/assets/index-diWkx2Wq.js +0 -1
- package/src/assets/web-panel/assets/motion-BalXv_60.js +0 -11
- package/src/assets/web-panel/assets/mtc-parser-CEQffxds.js +0 -1
|
@@ -17,6 +17,43 @@
|
|
|
17
17
|
* lesson as the --settings "opus"→404 vision trap.
|
|
18
18
|
* - No configured provider → unchanged (runner defaults apply: ollama).
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Reconcile a MISLABELED llm config/options object. The `baseUrl` is the
|
|
22
|
+
* authoritative signal of which provider you actually reach, so when `provider`
|
|
23
|
+
* disagrees with the provider its `baseUrl` belongs to, the config is simply
|
|
24
|
+
* mislabeled — the recurring "provider:anthropic + volces baseUrl + model:haiku"
|
|
25
|
+
* corruption that makes runnable-provider relabel (and emit "provider 配置与
|
|
26
|
+
* baseUrl 不一致") on EVERY run because the on-disk config never gets fixed.
|
|
27
|
+
*
|
|
28
|
+
* Correct `provider` to match the endpoint, and if the model is foreign to the
|
|
29
|
+
* corrected provider, replace it with that provider's default. Pure: the input
|
|
30
|
+
* is never mutated — returns `{ llm, changed }` where `llm` is the original
|
|
31
|
+
* object (changed:false) or a corrected shallow copy (changed:true). Works for
|
|
32
|
+
* both a `config.llm` block and a resolved request-`options` (same shape).
|
|
33
|
+
*
|
|
34
|
+
* `inferProviderFromBaseUrl` returns null for unknown/custom hosts (proxies),
|
|
35
|
+
* so an intentionally-custom endpoint is left untouched.
|
|
36
|
+
*/
|
|
37
|
+
export async function reconcileConfigLlmProvider(cfgLlm = {}) {
|
|
38
|
+
if (!cfgLlm || !cfgLlm.provider || !cfgLlm.baseUrl) {
|
|
39
|
+
return { llm: cfgLlm, changed: false };
|
|
40
|
+
}
|
|
41
|
+
const { inferProviderFromBaseUrl, modelForeignToProvider } =
|
|
42
|
+
await import("./runnable-provider.js");
|
|
43
|
+
const inferred = inferProviderFromBaseUrl(cfgLlm.baseUrl);
|
|
44
|
+
if (!inferred || inferred === cfgLlm.provider) {
|
|
45
|
+
return { llm: cfgLlm, changed: false };
|
|
46
|
+
}
|
|
47
|
+
const corrected = { ...cfgLlm, provider: inferred };
|
|
48
|
+
if (modelForeignToProvider(inferred, cfgLlm.model)) {
|
|
49
|
+
const { selectModelForTask, TaskType } =
|
|
50
|
+
await import("./task-model-selector.js");
|
|
51
|
+
const def = selectModelForTask(inferred, TaskType.CHAT);
|
|
52
|
+
if (def) corrected.model = def;
|
|
53
|
+
}
|
|
54
|
+
return { llm: corrected, changed: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
|
|
21
58
|
// The IDE chat panel pins --provider/--model (read from config) but DROPS
|
|
22
59
|
// --base-url/--api-key. With an explicit --provider the block below bails, so
|
package/src/lib/llm-pricing.js
CHANGED
|
@@ -96,9 +96,10 @@ const round = (n, dp = 6) => {
|
|
|
96
96
|
*
|
|
97
97
|
* Per provider, a user entry whose `match` equals a built-in pattern REPLACES
|
|
98
98
|
* it; brand-new patterns are prepended so they win ties; unknown providers are
|
|
99
|
-
* added. Malformed entries (missing match / non-numeric
|
|
100
|
-
* bad config line can't crash cost reporting
|
|
101
|
-
*
|
|
99
|
+
* added. Malformed entries (missing match / non-numeric / negative / Infinity
|
|
100
|
+
* rate) are skipped so a bad config line can't crash cost reporting or produce a
|
|
101
|
+
* negative cost that undercounts spend. Returns a new table; never mutates
|
|
102
|
+
* PRICE_TABLE or the input.
|
|
102
103
|
*
|
|
103
104
|
* @param {object} [overrides]
|
|
104
105
|
* @param {object} [base=PRICE_TABLE]
|
|
@@ -123,8 +124,14 @@ export function mergePricing(overrides, base = PRICE_TABLE) {
|
|
|
123
124
|
e &&
|
|
124
125
|
typeof e.match === "string" &&
|
|
125
126
|
e.match.trim() &&
|
|
127
|
+
// Rates must be finite AND non-negative — a negative price would
|
|
128
|
+
// produce a negative cost that undercounts spend (and could keep a
|
|
129
|
+
// CostBudget under its cap). `>= 0` also rejects NaN (NaN >= 0 is
|
|
130
|
+
// false); isFinite additionally rejects Infinity.
|
|
126
131
|
Number.isFinite(Number(e.in)) &&
|
|
127
|
-
Number
|
|
132
|
+
Number(e.in) >= 0 &&
|
|
133
|
+
Number.isFinite(Number(e.out)) &&
|
|
134
|
+
Number(e.out) >= 0,
|
|
128
135
|
)
|
|
129
136
|
.map((e) => ({
|
|
130
137
|
match: e.match.toLowerCase(),
|
package/src/lib/llm-providers.js
CHANGED
|
@@ -181,12 +181,22 @@ export class LLMProviderRegistry {
|
|
|
181
181
|
.prepare("SELECT * FROM llm_providers WHERE custom = 1")
|
|
182
182
|
.all();
|
|
183
183
|
for (const row of rows) {
|
|
184
|
+
// Tolerate a corrupt `models` TEXT column: `|| "[]"` only covers null,
|
|
185
|
+
// not malformed JSON, and an unguarded throw here would abort the whole
|
|
186
|
+
// custom-provider load (and the registry constructor) over one bad row.
|
|
187
|
+
let models = [];
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(row.models || "[]");
|
|
190
|
+
if (Array.isArray(parsed)) models = parsed;
|
|
191
|
+
} catch {
|
|
192
|
+
/* corrupt models column — fall back to no models for this provider */
|
|
193
|
+
}
|
|
184
194
|
this.providers.set(row.name, {
|
|
185
195
|
name: row.name,
|
|
186
196
|
displayName: row.display_name,
|
|
187
197
|
baseUrl: row.base_url,
|
|
188
198
|
apiKeyEnv: row.api_key_env,
|
|
189
|
-
models
|
|
199
|
+
models,
|
|
190
200
|
custom: true,
|
|
191
201
|
free: false,
|
|
192
202
|
});
|
package/src/lib/mcp-oauth.js
CHANGED
|
@@ -38,6 +38,10 @@ export const _deps = {
|
|
|
38
38
|
now: () => Date.now(),
|
|
39
39
|
// Backoff sleep seam (tests override with a no-op so the retry doesn't wait).
|
|
40
40
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
41
|
+
// Visible-warning seam (silent under vitest; tests override to assert).
|
|
42
|
+
warn: (msg) => {
|
|
43
|
+
if (!process.env.VITEST) process.stderr.write(msg);
|
|
44
|
+
},
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
/** Discovery/token requests retry ONCE after a transient error (CC 2.1.191). */
|
|
@@ -194,6 +198,12 @@ export async function discoverAuthMetadata(
|
|
|
194
198
|
{ resourceMetadataUrl } = {},
|
|
195
199
|
) {
|
|
196
200
|
const origin = new URL(serverUrl).origin;
|
|
201
|
+
// Discovery itself carries no secrets, but its responses dictate the
|
|
202
|
+
// authorization/token endpoints — so a cleartext-remote discovery is a
|
|
203
|
+
// downgrade vector (a MITM could inject endpoints). Refuse a non-loopback
|
|
204
|
+
// http MCP URL up front (loopback http stays allowed for local dev), matching
|
|
205
|
+
// the endpoint enforcement below.
|
|
206
|
+
assertSecureEndpoint(origin, "MCP server URL");
|
|
197
207
|
// 1. protected-resource metadata (RFC 9728) → authorization_servers[]
|
|
198
208
|
let authServer = origin;
|
|
199
209
|
try {
|
|
@@ -209,6 +219,12 @@ export async function discoverAuthMetadata(
|
|
|
209
219
|
} catch {
|
|
210
220
|
// no protected-resource doc → assume the origin is its own auth server
|
|
211
221
|
}
|
|
222
|
+
// The authorization-server location can come from the REMOTE protected-
|
|
223
|
+
// resource doc, so a malicious/MITM'd doc could point discovery at a
|
|
224
|
+
// cleartext host. Refuse a non-loopback http authServer before fetching its
|
|
225
|
+
// metadata (the discovered endpoints are HTTPS-checked too, but this fails
|
|
226
|
+
// closed earlier and blocks the downgrade).
|
|
227
|
+
assertSecureEndpoint(authServer, "authorization server");
|
|
212
228
|
// 2. authorization-server metadata (RFC 8414)
|
|
213
229
|
const candidates = [
|
|
214
230
|
`${authServer}/.well-known/oauth-authorization-server`,
|
|
@@ -359,17 +375,56 @@ export function tokenStorePath() {
|
|
|
359
375
|
return pathDefault.join(_deps.homedir(), ".chainlesschain", "mcp-oauth.json");
|
|
360
376
|
}
|
|
361
377
|
|
|
362
|
-
|
|
363
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Read the token store, distinguishing "absent / empty" (ok) from "exists but
|
|
380
|
+
* unreadable / corrupt" (ok:false). The distinction matters on the WRITE path:
|
|
381
|
+
* a read-modify-write that treats an unreadable store as `{}` would overwrite
|
|
382
|
+
* and silently destroy every other server's token.
|
|
383
|
+
*/
|
|
384
|
+
function _readStore(file) {
|
|
385
|
+
if (!_deps.fs.existsSync(file)) return { ok: true, store: {} };
|
|
364
386
|
try {
|
|
365
|
-
if (!_deps.fs.existsSync(file)) return {};
|
|
366
387
|
const data = JSON.parse(_deps.fs.readFileSync(file, "utf-8"));
|
|
367
|
-
return data && typeof data === "object" ? data : {};
|
|
388
|
+
return { ok: true, store: data && typeof data === "object" ? data : {} };
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return { ok: false, store: {}, err };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const _warnedCorruptStore = new Set();
|
|
395
|
+
function _warnCorruptStoreOnce(file, err, archived) {
|
|
396
|
+
if (_warnedCorruptStore.has(file)) return;
|
|
397
|
+
_warnedCorruptStore.add(file);
|
|
398
|
+
_deps.warn(
|
|
399
|
+
`⚠️ MCP OAuth token store at ${file} is unreadable (${err?.message || "parse error"}). ` +
|
|
400
|
+
`Saved MCP logins are unavailable${archived ? `; the unreadable file was preserved as ${archived}` : ""} — ` +
|
|
401
|
+
"re-run `cc mcp login <url>` for the servers you use.\n",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Move an unreadable store aside so a fresh write doesn't destroy it. Returns
|
|
406
|
+
* the archive path, or "" when it couldn't be moved (best-effort). */
|
|
407
|
+
function _archiveCorruptStore(file) {
|
|
408
|
+
if (typeof _deps.fs.renameSync !== "function") return "";
|
|
409
|
+
const ts = typeof _deps.now === "function" ? _deps.now() : Date.now();
|
|
410
|
+
const dest = `${file}.corrupt-${ts}`;
|
|
411
|
+
try {
|
|
412
|
+
_deps.fs.renameSync(file, dest);
|
|
413
|
+
return dest;
|
|
368
414
|
} catch {
|
|
369
|
-
return
|
|
415
|
+
return "";
|
|
370
416
|
}
|
|
371
417
|
}
|
|
372
418
|
|
|
419
|
+
export function loadTokenStore() {
|
|
420
|
+
const file = tokenStorePath();
|
|
421
|
+
const r = _readStore(file);
|
|
422
|
+
// Read callers (getStoredToken) treat an unreadable store as "no token", but
|
|
423
|
+
// the user should still know their saved logins silently vanished.
|
|
424
|
+
if (!r.ok) _warnCorruptStoreOnce(file, r.err, "");
|
|
425
|
+
return r.store;
|
|
426
|
+
}
|
|
427
|
+
|
|
373
428
|
export function getStoredToken(serverUrl) {
|
|
374
429
|
return loadTokenStore()[serverKey(serverUrl)] || null;
|
|
375
430
|
}
|
|
@@ -515,7 +570,15 @@ function withStoreLock(file, fn) {
|
|
|
515
570
|
export function saveStoredToken(serverUrl, record) {
|
|
516
571
|
const file = tokenStorePath();
|
|
517
572
|
return withStoreLock(file, () => {
|
|
518
|
-
const
|
|
573
|
+
const r = _readStore(file); // re-read inside the lock (latest state)
|
|
574
|
+
if (!r.ok) {
|
|
575
|
+
// The existing store is unreadable. Writing a fresh single-entry store
|
|
576
|
+
// here would silently destroy every other server's token — move the
|
|
577
|
+
// unreadable file aside first so it can be recovered, and surface it.
|
|
578
|
+
const archived = _archiveCorruptStore(file);
|
|
579
|
+
_warnCorruptStoreOnce(file, r.err, archived);
|
|
580
|
+
}
|
|
581
|
+
const store = r.store;
|
|
519
582
|
store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
|
|
520
583
|
_writeStoreSecure(file, store);
|
|
521
584
|
return store[serverKey(serverUrl)];
|
|
@@ -548,13 +611,28 @@ export function isTokenExpired(record, { skewMs = 60_000 } = {}) {
|
|
|
548
611
|
/**
|
|
549
612
|
* Return a valid bearer token for a server, refreshing if expired. Returns the
|
|
550
613
|
* access_token string, or null if there's no stored token / refresh failed.
|
|
614
|
+
*
|
|
615
|
+
* `forceRefresh` bypasses the local-expiry check and exchanges the refresh
|
|
616
|
+
* token unconditionally. Use it when the server REJECTED a stored token that
|
|
617
|
+
* still looked unexpired locally (mid-session 401/403 from a rotated/revoked
|
|
618
|
+
* grant): the cached access_token is known-bad, so reusing it is pointless —
|
|
619
|
+
* we either mint a fresh one or return null to signal "re-login needed".
|
|
620
|
+
*
|
|
621
|
+
* @param {string} serverUrl
|
|
622
|
+
* @param {{ forceRefresh?: boolean }} [opts]
|
|
551
623
|
*/
|
|
552
|
-
export async function ensureValidToken(
|
|
624
|
+
export async function ensureValidToken(
|
|
625
|
+
serverUrl,
|
|
626
|
+
{ forceRefresh = false } = {},
|
|
627
|
+
) {
|
|
553
628
|
const record = getStoredToken(serverUrl);
|
|
554
629
|
if (!record) return null;
|
|
555
|
-
if (!isTokenExpired(record)) return record.access_token;
|
|
630
|
+
if (!forceRefresh && !isTokenExpired(record)) return record.access_token;
|
|
556
631
|
if (!record.refresh_token || !record.endpoints?.token_endpoint) {
|
|
557
|
-
|
|
632
|
+
// Can't refresh. On a forced refresh the cached token is known-rejected, so
|
|
633
|
+
// hand back null (caller should stop retrying and prompt re-login); on the
|
|
634
|
+
// proactive path keep the existing behaviour of using what we have.
|
|
635
|
+
return forceRefresh ? null : record.access_token || null;
|
|
558
636
|
}
|
|
559
637
|
try {
|
|
560
638
|
const tok = await refreshAccessToken(
|
|
@@ -564,7 +642,7 @@ export async function ensureValidToken(serverUrl) {
|
|
|
564
642
|
const updated = saveStoredToken(serverUrl, { ...record, ...tok });
|
|
565
643
|
return updated.access_token;
|
|
566
644
|
} catch {
|
|
567
|
-
return record.access_token || null;
|
|
645
|
+
return forceRefresh ? null : record.access_token || null;
|
|
568
646
|
}
|
|
569
647
|
}
|
|
570
648
|
|
package/src/lib/mcp-serve.js
CHANGED
|
@@ -108,6 +108,35 @@ export function buildTools({ root, readOnly = false, deps = _deps }) {
|
|
|
108
108
|
},
|
|
109
109
|
handler: ({ path: rel }) => {
|
|
110
110
|
const abs = confine(root, rel, deps);
|
|
111
|
+
// Bounded read: pull at most MAX_READ_BYTES+1 bytes into memory. The
|
|
112
|
+
// old `readFileSync(abs)` loaded the WHOLE file before truncating, so
|
|
113
|
+
// MAX_READ_BYTES only capped the RESPONSE — a multi-GB file under the
|
|
114
|
+
// serve root would OOM the process. The +1 byte detects "longer than
|
|
115
|
+
// the cap" without reading the rest. Fall back to readFileSync only
|
|
116
|
+
// when the fd API isn't available on an injected fs.
|
|
117
|
+
if (typeof fs.openSync === "function") {
|
|
118
|
+
const fd = fs.openSync(abs, "r");
|
|
119
|
+
try {
|
|
120
|
+
const buf = Buffer.alloc(MAX_READ_BYTES + 1);
|
|
121
|
+
const n = fs.readSync(fd, buf, 0, MAX_READ_BYTES + 1, 0);
|
|
122
|
+
const truncated = n > MAX_READ_BYTES;
|
|
123
|
+
const text = buf
|
|
124
|
+
.slice(0, truncated ? MAX_READ_BYTES : n)
|
|
125
|
+
.toString("utf-8");
|
|
126
|
+
if (!truncated) return ok(text);
|
|
127
|
+
let total = null;
|
|
128
|
+
try {
|
|
129
|
+
total = fs.statSync(abs).size;
|
|
130
|
+
} catch {
|
|
131
|
+
/* size is best-effort for the message */
|
|
132
|
+
}
|
|
133
|
+
return ok(
|
|
134
|
+
`${text}\n… [truncated${total != null ? ` ${total} bytes` : ""}]`,
|
|
135
|
+
);
|
|
136
|
+
} finally {
|
|
137
|
+
fs.closeSync(fd);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
111
140
|
const buf = fs.readFileSync(abs);
|
|
112
141
|
const truncated = buf.length > MAX_READ_BYTES;
|
|
113
142
|
const text = (truncated ? buf.slice(0, MAX_READ_BYTES) : buf).toString(
|
package/src/lib/orchestrator.js
CHANGED
|
@@ -82,6 +82,16 @@ export class Orchestrator extends EventEmitter {
|
|
|
82
82
|
this.ciCommand = options.ciCommand || "npm test";
|
|
83
83
|
this.verbose = options.verbose || false;
|
|
84
84
|
|
|
85
|
+
// Cap retained task history. Every addTask stores a record in _tasks and
|
|
86
|
+
// nothing ever removes it, so a long-running orchestrator (cron-watch /
|
|
87
|
+
// daemon) accumulates tasks without bound. Prune the oldest TERMINAL tasks
|
|
88
|
+
// beyond this cap (in-flight tasks are never dropped). Generous default;
|
|
89
|
+
// 0/invalid → 500.
|
|
90
|
+
this.maxTasks =
|
|
91
|
+
Number.isFinite(options.maxTasks) && options.maxTasks > 0
|
|
92
|
+
? options.maxTasks
|
|
93
|
+
: 500;
|
|
94
|
+
|
|
85
95
|
/** @type {Map<string, object>} taskId → task */
|
|
86
96
|
this._tasks = new Map();
|
|
87
97
|
|
|
@@ -151,6 +161,7 @@ export class Orchestrator extends EventEmitter {
|
|
|
151
161
|
};
|
|
152
162
|
|
|
153
163
|
this._tasks.set(task.id, task);
|
|
164
|
+
this._pruneTasks();
|
|
154
165
|
this.emit("task:added", task);
|
|
155
166
|
|
|
156
167
|
if (this.notifier.isConfigured && task.notify) {
|
|
@@ -183,7 +194,7 @@ export class Orchestrator extends EventEmitter {
|
|
|
183
194
|
return {
|
|
184
195
|
tasks,
|
|
185
196
|
pool: this._router.summary(),
|
|
186
|
-
|
|
197
|
+
ciCommand: this.ciCommand,
|
|
187
198
|
cronActive: this._cronTimer !== null,
|
|
188
199
|
};
|
|
189
200
|
}
|
|
@@ -278,7 +289,7 @@ export class Orchestrator extends EventEmitter {
|
|
|
278
289
|
async _dispatch(task) {
|
|
279
290
|
this.emit("agents:dispatched", { task, count: task.subtasks.length });
|
|
280
291
|
this._log(
|
|
281
|
-
`Dispatching ${task.subtasks.length} subtask(s) to ${this.
|
|
292
|
+
`Dispatching ${task.subtasks.length} subtask(s) to ${this._router.summary().length} agent backend(s)`,
|
|
282
293
|
);
|
|
283
294
|
|
|
284
295
|
const results = await this._router.dispatch(task.subtasks, {
|
|
@@ -372,6 +383,11 @@ export class Orchestrator extends EventEmitter {
|
|
|
372
383
|
cwd,
|
|
373
384
|
encoding: "utf-8",
|
|
374
385
|
timeout: 180_000,
|
|
386
|
+
// execSync defaults maxBuffer to 1 MB; a verbose-but-PASSING test run
|
|
387
|
+
// that prints more than that throws ENOBUFS, which the catch below
|
|
388
|
+
// would misreport as a CI failure (triggering needless retries +
|
|
389
|
+
// re-dispatch). Give CI output generous headroom.
|
|
390
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
375
391
|
stdio: "pipe",
|
|
376
392
|
});
|
|
377
393
|
return {
|
|
@@ -387,6 +403,26 @@ export class Orchestrator extends EventEmitter {
|
|
|
387
403
|
}
|
|
388
404
|
}
|
|
389
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Bound retained task history to maxTasks. Evicts the oldest TERMINAL tasks
|
|
408
|
+
* first (Map preserves insertion order = FIFO); an in-flight task is never
|
|
409
|
+
* dropped, so a burst of concurrent runs can briefly exceed the cap rather
|
|
410
|
+
* than losing live work. The dropped records are completed/failed history
|
|
411
|
+
* the caller has already seen via task:complete / task:failed events.
|
|
412
|
+
*/
|
|
413
|
+
_pruneTasks() {
|
|
414
|
+
if (this._tasks.size <= this.maxTasks) return;
|
|
415
|
+
for (const [id, t] of this._tasks) {
|
|
416
|
+
if (this._tasks.size <= this.maxTasks) break;
|
|
417
|
+
if (
|
|
418
|
+
t.status === TASK_STATUS.COMPLETED ||
|
|
419
|
+
t.status === TASK_STATUS.FAILED
|
|
420
|
+
) {
|
|
421
|
+
this._tasks.delete(id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
390
426
|
async _cronTick() {
|
|
391
427
|
this.emit("cron:tick", { at: new Date().toISOString() });
|
|
392
428
|
// Override this method or listen to the event to add input source polling
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
|
+
import { safeJsonParse } from "./safe-json.js";
|
|
7
8
|
|
|
8
9
|
function generateId() {
|
|
9
10
|
return crypto.randomUUID();
|
|
@@ -160,7 +161,9 @@ export function getRoles(db) {
|
|
|
160
161
|
.all();
|
|
161
162
|
return rows.map((r) => ({
|
|
162
163
|
...r,
|
|
163
|
-
|
|
164
|
+
// `|| "[]"` only guarded null — a corrupt permissions cell still threw out
|
|
165
|
+
// of the whole role list (and could break permission checks). Skip it.
|
|
166
|
+
permissions: safeJsonParse(r.permissions, []),
|
|
164
167
|
isBuiltin: r.is_builtin === 1,
|
|
165
168
|
}));
|
|
166
169
|
}
|
|
@@ -553,7 +553,25 @@ export function retryStage(db, pipelineId, stageIndex) {
|
|
|
553
553
|
const pipeline = _getPipelineRow(db, pipelineId);
|
|
554
554
|
if (!pipeline) throw new Error(`Pipeline not found: ${pipelineId}`);
|
|
555
555
|
|
|
556
|
-
|
|
556
|
+
// A retry may only re-open the CURRENT stage or an EARLIER one. A forward
|
|
557
|
+
// jump would move current_stage past the intervening stages — skipping their
|
|
558
|
+
// approval GATES (CODE_REVIEW / DEPLOY) — and let a caller (`cc pipeline retry
|
|
559
|
+
// --stage N`, user-supplied) reach a later stage without the gates that guard
|
|
560
|
+
// it. Going backward is safe: advancing from there re-runs through every gate
|
|
561
|
+
// again. Mirrors the state-machine rigor of completeStage / approveGate.
|
|
562
|
+
const idx = Number(stageIndex);
|
|
563
|
+
if (!Number.isInteger(idx) || idx < 0) {
|
|
564
|
+
throw new Error(`Invalid stage index: ${stageIndex}`);
|
|
565
|
+
}
|
|
566
|
+
if (idx > pipeline.current_stage) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Cannot retry stage ${idx}: it is ahead of the current stage ` +
|
|
569
|
+
`(${pipeline.current_stage}) — retrying forward would skip the ` +
|
|
570
|
+
`intervening stages and their approval gates`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const stage = _getStageRow(db, pipelineId, idx);
|
|
557
575
|
if (!stage) throw new Error(`Stage ${stageIndex} not found`);
|
|
558
576
|
|
|
559
577
|
const now = _now();
|
|
@@ -572,7 +590,7 @@ export function retryStage(db, pipelineId, stageIndex) {
|
|
|
572
590
|
});
|
|
573
591
|
_updatePipelineFields(db, pipelineId, {
|
|
574
592
|
status: PIPELINE_STATUS.RUNNING,
|
|
575
|
-
current_stage:
|
|
593
|
+
current_stage: idx,
|
|
576
594
|
error_message: null,
|
|
577
595
|
completed_at: null,
|
|
578
596
|
});
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import fsDefault from "fs";
|
|
34
34
|
import pathDefault from "path";
|
|
35
35
|
import osDefault from "os";
|
|
36
|
+
import { credentialFileReason } from "./credential-guard.js";
|
|
36
37
|
|
|
37
38
|
export const DEFAULT_MAX_FILE_BYTES = 48 * 1024; // per instruction/import file
|
|
38
39
|
export const DEFAULT_MAX_TOTAL_BYTES = 192 * 1024; // whole block budget
|
|
@@ -321,6 +322,18 @@ export function loadProjectInstructions(opts = {}) {
|
|
|
321
322
|
const resolved = path.resolve(baseDir, target);
|
|
322
323
|
if (visited.has(resolved) || !isFile(fs, resolved)) continue; // silent:
|
|
323
324
|
// non-files are decorative @tokens (npm scopes, emails), not imports.
|
|
325
|
+
// Refuse to import a credential / secret file. `cc agent` auto-loads
|
|
326
|
+
// project memory, so a cloned/untrusted repo's cc.md must NOT be able to
|
|
327
|
+
// pull `@~/.ssh/id_rsa` / `@../.env` into the LLM-bound system prompt
|
|
328
|
+
// (mirrors the read_file/run_shell credential guard).
|
|
329
|
+
const credReason = credentialFileReason(resolved);
|
|
330
|
+
if (credReason) {
|
|
331
|
+
visited.add(resolved); // don't re-warn on a second reference
|
|
332
|
+
warnings.push(
|
|
333
|
+
`refused @import of ${resolved} — ${credReason}; project memory must not pull secrets into the prompt`,
|
|
334
|
+
);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
324
337
|
visited.add(resolved);
|
|
325
338
|
queue.push({ abs: resolved, scope: "import", depth: depth + 1 });
|
|
326
339
|
}
|
|
@@ -25,6 +25,11 @@ import { findProjectRoot } from "./project-instructions.js";
|
|
|
25
25
|
export const BANG_TIMEOUT_MS = 120_000;
|
|
26
26
|
export const BANG_MAX_BUFFER = 10 * 1024 * 1024;
|
|
27
27
|
export const BANG_OUTPUT_CAP = 30_000;
|
|
28
|
+
// A `#` note is a terse one-liner, but readline accepts a long pasted line and
|
|
29
|
+
// the note lands verbatim in cc.md — which is auto-loaded into EVERY future
|
|
30
|
+
// session's system prompt. Without a cap, an accidental fat paste silently
|
|
31
|
+
// bloats every later session's context. Truncate to a sane single-note size.
|
|
32
|
+
export const MEMO_NOTE_MAX = 4_000;
|
|
28
33
|
|
|
29
34
|
export const _deps = {
|
|
30
35
|
spawnSync: spawnSyncDefault,
|
|
@@ -138,7 +143,10 @@ export function appendMemoryNote(rawLine, opts = {}) {
|
|
|
138
143
|
const fs = opts.deps?.fs || _deps.fs;
|
|
139
144
|
const path = opts.deps?.path || _deps.path;
|
|
140
145
|
const cwd = opts.cwd || process.cwd();
|
|
141
|
-
|
|
146
|
+
let note = String(rawLine).replace(/^#/, "").trim();
|
|
147
|
+
if (note.length > MEMO_NOTE_MAX) {
|
|
148
|
+
note = `${note.slice(0, MEMO_NOTE_MAX)} …[truncated]`;
|
|
149
|
+
}
|
|
142
150
|
const stamp = opts.date || new Date().toISOString().slice(0, 10);
|
|
143
151
|
|
|
144
152
|
const root =
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repl-denials — record + render the policy denials seen during an agent run,
|
|
3
|
+
* so the interactive REPL's `/permissions denials` can review what got blocked
|
|
4
|
+
* AND a headless run (`cc agent -p`) can print an end-of-run summary (Claude-
|
|
5
|
+
* Code 2.1.193 parity: "auto-mode denial reasons … /permissions recent
|
|
6
|
+
* denials"). The helpers are generic (not REPL-specific) and shared by both.
|
|
7
|
+
*
|
|
8
|
+
* A *denial* is a tool call the agent was NOT allowed to run — blocked by the
|
|
9
|
+
* shell policy, an ApprovalGate tier, a settings permission rule, or a hook. It
|
|
10
|
+
* is distinct from a tool that ran and *failed* (non-zero exit, missing file):
|
|
11
|
+
* those carry an `error` but none of the denial markers below, so they are not
|
|
12
|
+
* recorded here.
|
|
13
|
+
*
|
|
14
|
+
* Pure + dependency-free so it unit-tests without the REPL runtime.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Keep the most recent N denials per session (bounded ring buffer). */
|
|
18
|
+
export const MAX_RECENT_DENIALS = 20;
|
|
19
|
+
|
|
20
|
+
/** Error-message prefixes agent-core attaches to a blocked tool call. */
|
|
21
|
+
const DENIAL_PREFIX = /^\s*\[(Shell Policy|Permission|ApprovalGate|Hook)\]/;
|
|
22
|
+
|
|
23
|
+
/** Map a `[Prefix]` to a short `via` label when no structured field carries one. */
|
|
24
|
+
function viaFromPrefix(errStr) {
|
|
25
|
+
const m = DENIAL_PREFIX.exec(errStr);
|
|
26
|
+
if (!m) return null;
|
|
27
|
+
return (
|
|
28
|
+
{
|
|
29
|
+
"Shell Policy": "shell-policy",
|
|
30
|
+
Permission: "settings",
|
|
31
|
+
ApprovalGate: "gate",
|
|
32
|
+
Hook: "hook",
|
|
33
|
+
}[m[1]] || "policy"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Classify a tool-result as a policy denial and extract a structured record,
|
|
39
|
+
* or return null when it is not a denial. Detection prefers the structured
|
|
40
|
+
* markers agent-core attaches (`approval.decision` / `policy.decision` /
|
|
41
|
+
* `shellCommandPolicy.allowed`) and falls back to the error-message prefix.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} ev { tool, result, error, argsSummary }
|
|
44
|
+
* @returns {{tool:string,summary:string,reason:string,via:string,rule:(string|null)}|null}
|
|
45
|
+
*/
|
|
46
|
+
export function classifyDenial(ev = {}) {
|
|
47
|
+
const { tool, result, error, argsSummary } = ev;
|
|
48
|
+
const errStr = String(error || (result && result.error) || "").trim();
|
|
49
|
+
const approval = result && result.approval;
|
|
50
|
+
const policy = result && result.policy;
|
|
51
|
+
const shellPol = result && result.shellCommandPolicy;
|
|
52
|
+
const denied =
|
|
53
|
+
(approval && approval.decision === "deny") ||
|
|
54
|
+
(policy && policy.decision === "deny") ||
|
|
55
|
+
(shellPol && shellPol.allowed === false) ||
|
|
56
|
+
DENIAL_PREFIX.test(errStr);
|
|
57
|
+
if (!denied) return null;
|
|
58
|
+
const via =
|
|
59
|
+
(approval && approval.via) ||
|
|
60
|
+
(policy && policy.via) ||
|
|
61
|
+
(shellPol ? "shell-policy" : viaFromPrefix(errStr)) ||
|
|
62
|
+
"policy";
|
|
63
|
+
const rule = (policy && policy.rule) || (shellPol && shellPol.ruleId) || null;
|
|
64
|
+
return {
|
|
65
|
+
tool: tool || "?",
|
|
66
|
+
summary: String(
|
|
67
|
+
argsSummary || (shellPol && shellPol.normalizedCommand) || "",
|
|
68
|
+
).slice(0, 200),
|
|
69
|
+
reason: errStr || "(denied)",
|
|
70
|
+
via,
|
|
71
|
+
rule,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Two denials are "the same" attempt when tool + command + rule + via match. */
|
|
76
|
+
function sameDenial(a, b) {
|
|
77
|
+
return (
|
|
78
|
+
a &&
|
|
79
|
+
b &&
|
|
80
|
+
a.tool === b.tool &&
|
|
81
|
+
a.summary === b.summary &&
|
|
82
|
+
a.via === b.via &&
|
|
83
|
+
a.rule === b.rule
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Append a denial to a bounded most-recent-last ring buffer (mutates + returns
|
|
89
|
+
* it). Drops the oldest entries beyond `max`. No-op on a bad log/record.
|
|
90
|
+
*
|
|
91
|
+
* Consecutive identical denials are COALESCED: a model that hits a policy block
|
|
92
|
+
* usually retries the same command several times, which would otherwise flood
|
|
93
|
+
* the cap and evict other distinct denials. A repeat bumps the last entry's
|
|
94
|
+
* `count` and refreshes its `at` instead of appending.
|
|
95
|
+
*/
|
|
96
|
+
export function recordDenial(log, record, max = MAX_RECENT_DENIALS) {
|
|
97
|
+
if (!Array.isArray(log) || !record) return log;
|
|
98
|
+
const last = log[log.length - 1];
|
|
99
|
+
if (sameDenial(last, record)) {
|
|
100
|
+
last.count = (last.count || 1) + 1;
|
|
101
|
+
if (typeof record.at === "number") last.at = record.at;
|
|
102
|
+
return log;
|
|
103
|
+
}
|
|
104
|
+
log.push(record);
|
|
105
|
+
while (log.length > max) log.shift();
|
|
106
|
+
return log;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render the denial log most-recent-first for `/permissions denials`. Times are
|
|
111
|
+
* relative ("12s ago") when a record carries a numeric `at`; `now` is injectable
|
|
112
|
+
* for deterministic tests.
|
|
113
|
+
*
|
|
114
|
+
* @param {Array} log
|
|
115
|
+
* @param {{now?:number}} [opts]
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
export function formatDenials(log, opts = {}) {
|
|
119
|
+
const now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
120
|
+
if (!Array.isArray(log) || log.length === 0) {
|
|
121
|
+
return " (no tool calls were denied this session)";
|
|
122
|
+
}
|
|
123
|
+
const lines = [` Recent denials (most recent first, ${log.length}):`];
|
|
124
|
+
for (let i = log.length - 1; i >= 0; i--) {
|
|
125
|
+
const d = log[i] || {};
|
|
126
|
+
const ago =
|
|
127
|
+
typeof d.at === "number"
|
|
128
|
+
? ` · ${Math.max(0, Math.round((now - d.at) / 1000))}s ago`
|
|
129
|
+
: "";
|
|
130
|
+
const where = d.rule ? `${d.via}:${d.rule}` : d.via || "policy";
|
|
131
|
+
const what = d.summary ? `${d.tool} ${d.summary}` : d.tool || "?";
|
|
132
|
+
const times = d.count > 1 ? ` ×${d.count}` : "";
|
|
133
|
+
lines.push(` • ${what}${times} [${where}${ago}]`);
|
|
134
|
+
if (d.reason) lines.push(` ${d.reason}`);
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
@@ -31,7 +31,13 @@ export function isAuthError(err) {
|
|
|
31
31
|
: null;
|
|
32
32
|
if (status === 401 || status === 403) return true;
|
|
33
33
|
const msg = String(err.message || err).toLowerCase();
|
|
34
|
-
|
|
34
|
+
// `expired` must be AUTH-scoped (key/token/credential/secret), not bare:
|
|
35
|
+
// a TLS "certificate has expired" or "cache expired" is an infra error, and
|
|
36
|
+
// misclassifying it as auth would trigger an env-key VENDOR hijack on a
|
|
37
|
+
// keyless provider — masking the real problem (the same anti-hijack spirit
|
|
38
|
+
// as the rest of this module). A genuine 401/403 is still caught by status
|
|
39
|
+
// and the explicit auth phrases below.
|
|
40
|
+
return /\b401\b|\b403\b|unauthorized|forbidden|authentication failed|api[\s_-]*key required|invalid api[\s_-]*key|incorrect api[\s_-]*key|(?:api[\s_-]*key|token|credentials?|secret)[\s_-]*(?:has\s+|is\s+|was\s+)?expired|expired[\s_-]+(?:api[\s_-]*key|token|credentials?|secret)/.test(
|
|
35
41
|
msg,
|
|
36
42
|
);
|
|
37
43
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safeJsonParse — JSON.parse that returns `fallback` instead of throwing on
|
|
3
|
+
* malformed (or null/empty) input.
|
|
4
|
+
*
|
|
5
|
+
* Use it for DB columns parsed inside a list `.map()` / loop: an unguarded
|
|
6
|
+
* `JSON.parse(row.col)` lets ONE corrupt cell throw out of the whole function,
|
|
7
|
+
* crashing the entire list load (the unguarded-JSON.parse-in-list-loop bug
|
|
8
|
+
* class — a single bad row should be skipped, not fail every other row).
|
|
9
|
+
*
|
|
10
|
+
* @param {unknown} text the raw string (or null/undefined)
|
|
11
|
+
* @param {*} [fallback=null] returned on null/empty/parse failure
|
|
12
|
+
* @returns {*}
|
|
13
|
+
*/
|
|
14
|
+
export function safeJsonParse(text, fallback = null) {
|
|
15
|
+
if (text == null || text === "") return fallback;
|
|
16
|
+
try {
|
|
17
|
+
const v = JSON.parse(text);
|
|
18
|
+
return v == null ? fallback : v;
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|