chainlesschain 0.162.159 → 0.162.160
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/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-ByIiF5bT.js → AIOps-CkRHjW8e.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-NlgSFZq5.js → ActionButton-BP3MeNi2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D17cOUcb.js → Analytics-CjbjQsOr.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-B4mEZcbl.js → AppLayout-i0adH1M8.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CASsmyQw.js → Artifacts-CCCG2GOU.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DYAia2e2.js → Audit-DrsgvwHC.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C1lvYUSu.js → BackgroundAgents-DkXNkbZM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B6agK6bA.js → Backup-DarTWCex.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DM43R2vy.js → BaseInput-DaDzhcto.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Dtdp3eXp.js → Chat-C4EzGw6V.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BL5c_OQ1.js → ChatBubbleRenderer-lkavIWMS.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-D2xf7uZt.js → Checkbox-gddNgaQi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-D3j45jJL.js → Codegen-BKldFWf9.js} +1 -1
- package/src/assets/web-panel/assets/{Col-voPJ9fUZ.js → Col-ChR-brDs.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DEsyTESF.js → Community-DlHtZX86.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-OHV_QXLD.js → Compact-BAhfcBpC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-eDTJWC-J.js → Compliance-gIrKkm6m.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BBvy-6d1.js → Cowork-BYnzLBO3.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Dw2eoJxu.js → Cron-ByUDIljI.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DQHms_Bh.js → Crosschain-DOdNbHFp.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cyu7Id1c.js → DID-BJHaNWD2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-sIDzkG92.js → Dashboard-Drm_tClz.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DUW3T2ey.js → Dropdown-CA1WLzGN.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CECELXW8.js → EmailListRenderer-Di1SKjNO.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BIZR37ov.js → FamilyGuardDashboard-44Ve7ZFe.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BEZIQX84.js → Federation-CubPwv_N.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BcdUMznL.js → FormItemContext-CsUFe5bU.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DsN_fr5H.js → GenericCardRenderer-D7xRjPjZ.js} +1 -1
- package/src/assets/web-panel/assets/{Git-63LhjBx2.js → Git-D-Qx9dhO.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYtQtIcH.js → Governance-_YMYaU4n.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BwR6UUox.js → Inference-J7Yzh-K_.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D9lVNTUA.js → KnowledgeGraph-D37iI_Mo.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-C4q5h7Ut.js → Logs-oMTSYfGY.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DexLlXfN.js → MarkdownRenderer-BqaPmC0W.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-Plksliuw.js → Marketplace-DKUqYDaF.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DZUFfeuG.js → McpTools-BEv3Gx61.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-NoDYt29V.js → Memory-CtA8XD2y.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CEXGTkdh.js → MobileBridge-ug7c1mMj.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-DTAa0cwy.js → Mtc-WJaMs4i2.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CVV7LnNS.js → MtcAudit-DXnK_-ru.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BqqGrjt_.js → Multisig-C4Ev7726.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C4WP8DRp.js → NLProgramming-BMFb9_zk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Zo4DJUVI.js → Notes-Bl93nvBX.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DBcjRaz2.js → NotificationSettings-BN2W5cl6.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Dxc9oN4o.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BT9WPBOX.js → Organization-CAs48637.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D6b4bZ3q.js → Overflow-Bhq27Zoc.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cw3OsYx-.js → P2P-D61-2542.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C0nyiK2b.js → PdhVaultBrowser-C1j4iUxz.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-zVBZEB_C.js → Permissions-rJ2-fScM.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BOspsFew.js → PersonalDataHub-DFIlTok6.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CQWfZ4XH.js → Pipeline-BjIgEWoZ.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-C2oGJ_WL.js → Privacy-BQoH5wcX.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BZMR4qY1.js → ProjectInit-CYR_-_Ub.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DAujoP63.js → ProjectSettings-nkatCq-9.js} +2 -2
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +1 -0
- package/src/assets/web-panel/assets/{Providers-D_RGHM-v.js → Providers-BnKM9fsF.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Mf6KfL2J.js → QrScannerModal-Cfe82dbz.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-5D8URoI_.js → QuickAsk-r1RbPufV.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Df4kC8Gy.js → Recommend-CjCbM2WE.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DGi3f3vO.js → RemoteSession-BuMGPH6V.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-jmkGFNWb.js → Reputation-Bmj3NY1F.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DP6Pcu3m.js → Row-BIsNcE_1.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D60hSuPO.js → RssFeed-BObmojD2.js} +3 -3
- package/src/assets/web-panel/assets/{Search-0mf1BQUK.js → Search-BRmbkQNS.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CfZtLFYV.js → Security-_rF3DRpY.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D9zpZLWz.js → Services-D0If9ZV4.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-D1_44eMD.js → Skeleton-o7zIksUg.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-cVFa8zkz.js → Skills-oUR7zi9u.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Cinpzc3L.js → Sla-fqKyGi03.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-bqdiJE_P.js → SpeechSettings-B1wZ_gxx.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Dgl_SLgj.js → SyncSettings-B3qJWYXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DawWAEhy.js → Tasks-BH5_dUfr.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Ca0nY651.js → Templates-CCIVpjRP.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CgaGEhLm.js → Tenant-2GZtTGnB.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D-9EnpRx.js → Terminal-BlMkfs_u.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CSOxDHZ6.js → TimelineRenderer-U6hi5OMc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpwLZpL3.js → Tokens-C-_BerGJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DgowwBK5.js → Trigger-DaKzBgN3.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BZutapjE.js → Trust-CLvcsco3.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-QZPt8_Sp.js → UkeySign-BFn_Y1zZ.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DlQx5UDJ.js → VideoEditing-B0V18dLT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ZfdTgtWA.js → Wallet-BbmP7Imu.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CAp0y_Cr.js → WebAuthn-Dewj_0kt.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BVITZEmr.js → WorkflowEditor-DOxz3Lwa.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CC2tPaNg.js → chat-B1OIpZnh.js} +1 -1
- package/src/assets/web-panel/assets/{colors-31brc9CA.js → colors-CSGNVdri.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DLMkC9C-.js → compact-item-8S0vcg0u.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BMtveI6F.js → createContext-vZyc27TN.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DbD6WHW2.js → hasIn-DWw3-UhA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dm0byjm1.js → index-B4pP2ycr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXa-4zd.js → index-BDHyovnK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_eUKGYP.js → index-BKEmQQvR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOM-VFlv.js → index-BQssf0EQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-bdaJwTXJ.js → index-BZnauCqN.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cwbqh0KZ.js → index-BdyokEau.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJvxFyIz.js → index-BeBv896m.js} +1 -1
- package/src/assets/web-panel/assets/{index-DndNnYc0.js → index-BjC-TrOb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWgfipcC.js → index-BkWd6YL7.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7cV0tu0.js → index-BoIPHP4R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcVszn2b.js → index-BuB5P0TR.js} +1 -1
- package/src/assets/web-panel/assets/{index-COHHQ9cU.js → index-BwKrTVV0.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1PfeI88.js → index-C6r27A-o.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cod97Urs.js → index-CB2MOS_8.js} +3 -3
- package/src/assets/web-panel/assets/{index-CEwjBA9q.js → index-CFQWZvak.js} +1 -1
- package/src/assets/web-panel/assets/{index-D67kIagl.js → index-CI3slBKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-jG6h22Qg.js → index-CJgKP2cm.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZl-UXB6.js → index-CLPr2vC_.js} +1 -1
- package/src/assets/web-panel/assets/{index-ntVQqSML.js → index-CbZqw7pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-cGlCDW.js → index-CfEvacz7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFRLmCR3.js → index-CwXBblm-.js} +1 -1
- package/src/assets/web-panel/assets/{index-AaIKG60c.js → index-D1i_8oop.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPpS-lGb.js → index-D8qXVfoV.js} +1 -1
- package/src/assets/web-panel/assets/{index-WQUO2vvK.js → index-DKwl1AkQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUmLMFsy.js → index-DLAvUtOH.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bbc2JeQG.js → index-DLgkAhEE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKAB6-6V.js → index-DPgY5GLy.js} +1 -1
- package/src/assets/web-panel/assets/{index-ALvoNTRF.js → index-Dc40pX3F.js} +1 -1
- package/src/assets/web-panel/assets/{index-ep6JchNG.js → index-DcfLXAW7.js} +1 -1
- package/src/assets/web-panel/assets/{index-C21pdBqs.js → index-DeCS12Mh.js} +1 -1
- package/src/assets/web-panel/assets/{index-3oBsXZmF.js → index-Dg0Izipr.js} +1 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +1 -0
- package/src/assets/web-panel/assets/{index-BnFLzejT.js → index-DtHWHJnj.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7LHNw_N.js → index-Q6oyLnKC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoO4WEJB.js → index-dxocDVD_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiRQUoyE.js → index-t-q3rkVm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFXKbOek.js → index-tr1x96W9.js} +1 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +1 -0
- package/src/assets/web-panel/assets/{index-CJhREOlG.js → index-xx5M5Rbj.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D5UoBRhN.js → initDefaultProps-Di5YFId0.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DltOw0CR.js → motion-BVSN0aeL.js} +1 -1
- package/src/assets/web-panel/assets/{move-_oPkk5zj.js → move-DHNiaP2l.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-EES8ROCS.js → mtc-parser-xJPf3gQD.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B-_39x_M.js → omit-C7qzUdkN.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-l6D0yJHz.js → pickAttrs-pSpjg3Le.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DIeUK31L.js → placementArrow-cs_jL_UC.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-dG4EzUQo.js → responsiveObserve-DkS_Zs4r.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DWFzYGdH.js → slide-B5O1Kpxo.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CECdTA5H.js → statusUtils-BX02GnZp.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-D6kP-mMK.js → styleChecker-EmBYRftd.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BWfna_rd.js → useFlexGapSupport-ZGzN1cU8.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Bb-j6rzo.js → useFs-CfP7M1m7.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D5OIsYM4.js → usePersonalDataHub-CbIPHgNh.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DNpwzFTM.js → vnode-7xh0If_O.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-vNLpFQBW.js → zoom-DmitvTR2.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +65 -6
- package/src/commands/background-session.js +30 -0
- package/src/commands/mcp.js +46 -0
- package/src/commands/session.js +70 -0
- package/src/data/changelog.json +7 -2
- package/src/harness/jsonl-session-store.js +70 -1
- package/src/harness/mcp-client.js +50 -0
- package/src/lib/agent-sandbox.js +68 -0
- package/src/lib/agents.js +12 -4
- package/src/lib/api-key-helper.js +62 -0
- package/src/lib/background-agent-supervisor.js +32 -0
- package/src/lib/dependency-install-policy.js +130 -0
- package/src/lib/exit-codes.cjs +77 -0
- package/src/lib/headless-manifest.js +175 -0
- package/src/lib/hook-runner.cjs +70 -4
- package/src/lib/ide-context-redaction.js +130 -0
- package/src/lib/ide-context.js +89 -13
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/llm-config-defaults.js +24 -8
- package/src/lib/project-mcp-trust.js +87 -0
- package/src/lib/sensitive-file-guard.js +51 -0
- package/src/lib/sub-agent-context.js +7 -3
- package/src/runtime/agent-core.js +102 -6
- package/src/runtime/coding-agent-contract-shared.cjs +19 -2
- package/src/runtime/headless-runner.js +66 -7
- package/src/runtime/headless-stream.js +29 -1
- package/src/runtime/mcp-config.js +32 -1
- package/src/assets/web-panel/assets/MobileProjects-DnbJkbZo.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +0 -1
- package/src/assets/web-panel/assets/Projects-DvH_P2Uq.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +0 -1
- package/src/assets/web-panel/assets/index-BhXiaiev.js +0 -1
- package/src/assets/web-panel/assets/index-WpaYaE3D.js +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `llm.apiKeyHelper` (gap-analysis 2026-07-11 P0 "依赖安装与凭据"): fetch the
|
|
3
|
+
* LLM API key from an external command instead of storing it in plaintext
|
|
4
|
+
* config.json. Point it at your OS credential store, e.g.:
|
|
5
|
+
*
|
|
6
|
+
* Windows : "powershell -NoProfile -Command \"...CredentialManager...\""
|
|
7
|
+
* macOS : "security find-generic-password -s cc-llm -w"
|
|
8
|
+
* Linux : "secret-tool lookup service cc-llm"
|
|
9
|
+
*
|
|
10
|
+
* Resolution precedence stays: --api-key > CC_API_KEY > config llm.apiKey >
|
|
11
|
+
* llm.apiKeyHelper. The helper's stdout (trimmed) is the key; a failing or
|
|
12
|
+
* empty helper resolves null so the run fails with the provider's own
|
|
13
|
+
* missing-key error (never a silent fallback — see the no-silent-substitution
|
|
14
|
+
* rule). Cached per process (5 min) so multi-call runs don't re-spawn it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
20
|
+
const _cache = new Map(); // helper command → { key, at }
|
|
21
|
+
let _warnedFailure = false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} helperCmd shell command printing the key on stdout
|
|
25
|
+
* @param {{exec?:Function, now?:()=>number, writeErr?:(s:string)=>void}} [opts]
|
|
26
|
+
* @returns {string|null}
|
|
27
|
+
*/
|
|
28
|
+
export function resolveApiKeyFromHelper(helperCmd, opts = {}) {
|
|
29
|
+
const cmd = typeof helperCmd === "string" ? helperCmd.trim() : "";
|
|
30
|
+
if (!cmd) return null;
|
|
31
|
+
const now = opts.now || Date.now;
|
|
32
|
+
const hit = _cache.get(cmd);
|
|
33
|
+
if (hit && now() - hit.at < TTL_MS) return hit.key;
|
|
34
|
+
const exec = opts.exec || execSync;
|
|
35
|
+
try {
|
|
36
|
+
const out = exec(cmd, {
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
timeout: 10000,
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
});
|
|
42
|
+
const key = String(out || "").trim();
|
|
43
|
+
if (!key) return null;
|
|
44
|
+
_cache.set(cmd, { key, at: now() });
|
|
45
|
+
return key;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Warn once per process, then stay quiet — the provider's missing-key
|
|
48
|
+
// error is the actionable surface.
|
|
49
|
+
if (!_warnedFailure) {
|
|
50
|
+
_warnedFailure = true;
|
|
51
|
+
const writeErr = opts.writeErr || ((s) => process.stderr.write(s));
|
|
52
|
+
writeErr(`[api-key-helper] llm.apiKeyHelper failed: ${err.message}\n`);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Test seam: drop the per-process cache + warn-once latch. */
|
|
59
|
+
export function clearApiKeyHelperCache() {
|
|
60
|
+
_cache.clear();
|
|
61
|
+
_warnedFailure = false;
|
|
62
|
+
}
|
|
@@ -223,6 +223,38 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
223
223
|
return next;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Remove a background agent's RECORD (state file + log) — `cc daemon rm`
|
|
228
|
+
* (gap-analysis 2026-07-11 P0 "后台 Agent Supervisor" 补 rm 动词). Terminal
|
|
229
|
+
* sessions (completed/failed/stopped/lost) remove directly; a still-running
|
|
230
|
+
* one is refused unless `force`, which stops it first. The underlying JSONL
|
|
231
|
+
* conversation session is NOT touched — that's `cc session delete`.
|
|
232
|
+
*/
|
|
233
|
+
export function removeBackgroundAgent(id, options = {}) {
|
|
234
|
+
const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
235
|
+
now: options.now,
|
|
236
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
237
|
+
});
|
|
238
|
+
if (!state) throw new Error(`Background agent not found: ${id}`);
|
|
239
|
+
if (state.status === "running") {
|
|
240
|
+
if (options.force !== true) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`${id} is still running — stop it first (cc daemon stop ${id}) or pass --force`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
stopBackgroundAgent(id);
|
|
247
|
+
} catch {
|
|
248
|
+
/* best-effort — the record removal below is the point of rm --force */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
rmSync(statePath(id), { force: true });
|
|
252
|
+
if (options.keepLog !== true) {
|
|
253
|
+
rmSync(logPath(id), { force: true });
|
|
254
|
+
}
|
|
255
|
+
return { id, removed: true, status: state.status };
|
|
256
|
+
}
|
|
257
|
+
|
|
226
258
|
/**
|
|
227
259
|
* Pin/unpin a session for the dashboard (`cc daemon view`) — pinned sessions
|
|
228
260
|
* sort first inside their group. Same read-modify-write + verify-retry dance
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-install policy for `run_code` (gap-analysis 2026-07-11,
|
|
3
|
+
* docs/CLAUDE_CODE_CLI_GAP_ANALYSIS.md P0 "依赖安装与凭据").
|
|
4
|
+
*
|
|
5
|
+
* Auto `pip install` on a Python import error used to be unconditional. It is
|
|
6
|
+
* now OPT-IN:
|
|
7
|
+
* - env `CC_RUN_CODE_AUTO_INSTALL=1|0` wins outright (kill-switch friendly)
|
|
8
|
+
* - settings.json `runCode.autoInstall: true` opts in per project/user
|
|
9
|
+
* - default: DISABLED — the tool result explains how to enable it
|
|
10
|
+
*
|
|
11
|
+
* When enabled, an optional `runCode.installAllowlist` (array of package
|
|
12
|
+
* names, PEP 503-normalized) restricts WHICH packages may be installed, and
|
|
13
|
+
* every attempt — including blocked/disabled ones — is appended to an audit
|
|
14
|
+
* log at ~/.chainlesschain/audit/dependency-install.jsonl.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
|
|
22
|
+
const _require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
/** PEP 503 name normalization: lowercase, runs of -_. collapse to "-". */
|
|
25
|
+
export function normalizePackageName(name) {
|
|
26
|
+
return String(name || "")
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[-_.]+/g, "-");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the effective auto-install policy.
|
|
34
|
+
* @param {{cwd?:string, env?:object, settingsFile?:string|null}} opts
|
|
35
|
+
* @returns {{enabled:boolean, allowlist:string[]|null, source:string}}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveAutoInstallPolicy({
|
|
38
|
+
cwd = process.cwd(),
|
|
39
|
+
env = process.env,
|
|
40
|
+
settingsFile = null,
|
|
41
|
+
} = {}) {
|
|
42
|
+
const raw = env.CC_RUN_CODE_AUTO_INSTALL;
|
|
43
|
+
if (raw === "1" || raw === "0") {
|
|
44
|
+
return {
|
|
45
|
+
enabled: raw === "1",
|
|
46
|
+
allowlist: raw === "1" ? readAllowlist({ cwd, settingsFile }) : null,
|
|
47
|
+
source: "env",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
let enabled = false;
|
|
51
|
+
try {
|
|
52
|
+
const { readBooleanSetting } = _require("./settings-loader.cjs");
|
|
53
|
+
enabled =
|
|
54
|
+
readBooleanSetting("runCode.autoInstall", { cwd, settingsFile }) === true;
|
|
55
|
+
} catch {
|
|
56
|
+
enabled = false; // fail-closed: no settings → stay disabled
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
enabled,
|
|
60
|
+
allowlist: enabled ? readAllowlist({ cwd, settingsFile }) : null,
|
|
61
|
+
source: enabled ? "settings" : "default",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readAllowlist({ cwd = process.cwd(), settingsFile }) {
|
|
66
|
+
try {
|
|
67
|
+
const { settingsPaths, readSettingsFile, loadManagedSettings } = _require(
|
|
68
|
+
"./settings-loader.cjs",
|
|
69
|
+
);
|
|
70
|
+
let value;
|
|
71
|
+
for (const file of settingsPaths(cwd, settingsFile)) {
|
|
72
|
+
const node = readSettingsFile(file)?.runCode?.installAllowlist;
|
|
73
|
+
if (Array.isArray(node)) value = node; // closest layer wins
|
|
74
|
+
}
|
|
75
|
+
const managed = loadManagedSettings({});
|
|
76
|
+
const mNode = managed?.settings?.runCode?.installAllowlist;
|
|
77
|
+
if (Array.isArray(mNode)) value = mNode; // managed policy wins outright
|
|
78
|
+
if (!Array.isArray(value)) return null;
|
|
79
|
+
const list = value.map(normalizePackageName).filter(Boolean);
|
|
80
|
+
return list.length ? list : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a package may be installed under the policy's allowlist.
|
|
88
|
+
* No allowlist configured → any (validity is checked separately).
|
|
89
|
+
*/
|
|
90
|
+
export function isPackageAllowed(name, allowlist) {
|
|
91
|
+
if (!allowlist) return true;
|
|
92
|
+
return allowlist.includes(normalizePackageName(name));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Human hint for the disabled-by-default path (returned in the tool result). */
|
|
96
|
+
export function autoInstallDisabledHint(packageName) {
|
|
97
|
+
return (
|
|
98
|
+
`Missing Python package "${packageName}". Auto-install is disabled by ` +
|
|
99
|
+
`default; install it yourself (pip install ${packageName}), or opt in via ` +
|
|
100
|
+
`settings.json {"runCode":{"autoInstall":true}} or CC_RUN_CODE_AUTO_INSTALL=1.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Append an install-attempt audit record (best-effort, never throws).
|
|
106
|
+
* @param {{package:string, interpreter?:string, cwd?:string,
|
|
107
|
+
* outcome:"installed"|"failed"|"blocked"|"disabled", detail?:string}} entry
|
|
108
|
+
* @param {{baseDir?:string, now?:()=>number}} [opts] injection seams
|
|
109
|
+
*/
|
|
110
|
+
export function recordInstallAudit(entry, opts = {}) {
|
|
111
|
+
try {
|
|
112
|
+
const baseDir =
|
|
113
|
+
opts.baseDir ||
|
|
114
|
+
process.env.CC_AUDIT_DIR ||
|
|
115
|
+
path.join(os.homedir(), ".chainlesschain", "audit");
|
|
116
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
117
|
+
const line = JSON.stringify({
|
|
118
|
+
ts: new Date(opts.now ? opts.now() : Date.now()).toISOString(),
|
|
119
|
+
kind: "dependency-install",
|
|
120
|
+
...entry,
|
|
121
|
+
});
|
|
122
|
+
fs.appendFileSync(
|
|
123
|
+
path.join(baseDir, "dependency-install.jsonl"),
|
|
124
|
+
line + "\n",
|
|
125
|
+
"utf-8",
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
/* audit is best-effort — never affect the run */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless exit-code taxonomy (gap-analysis 2026-07-11, P0 "确定性 Headless"):
|
|
3
|
+
* scripted callers can tell apart WHY a run ended without parsing stderr.
|
|
4
|
+
*
|
|
5
|
+
* Backward compatibility: every failure is still non-zero, 0 stays success,
|
|
6
|
+
* 2 stays "settings hook blocked the prompt" (pre-existing), 130/143 stay the
|
|
7
|
+
* signal-interrupt codes. The new codes carve budget/turn exhaustion and
|
|
8
|
+
* config errors out of the generic 1.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const HEADLESS_EXIT_CODES = Object.freeze({
|
|
12
|
+
OK: 0,
|
|
13
|
+
/** generic failure (tool/loop/unclassified error) */
|
|
14
|
+
ERROR: 1,
|
|
15
|
+
/** a settings hook blocked the prompt (pre-existing semantic) */
|
|
16
|
+
HOOK_BLOCKED: 2,
|
|
17
|
+
/** the turn budget (--max-turns / loop iteration cap) ran out first */
|
|
18
|
+
MAX_TURNS: 3,
|
|
19
|
+
/** the --max-budget-usd cost cap stopped the run */
|
|
20
|
+
MAX_BUDGET: 4,
|
|
21
|
+
/** the LLM call itself failed (provider/model/network error) */
|
|
22
|
+
MODEL_ERROR: 5,
|
|
23
|
+
/** bad run configuration (--mcp-config / --permission-prompt-tool / settings) */
|
|
24
|
+
CONFIG_ERROR: 6,
|
|
25
|
+
/** user interrupt (SIGINT) — 128+2, shell convention */
|
|
26
|
+
INTERRUPT_SIGINT: 130,
|
|
27
|
+
/** termination (SIGTERM) — 128+15, shell convention */
|
|
28
|
+
INTERRUPT_SIGTERM: 143,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort classifier for errors thrown out of the agent loop: provider /
|
|
33
|
+
* transport failures → MODEL_ERROR, everything else → generic ERROR. Checks
|
|
34
|
+
* structural markers (HTTP status, network error codes) before falling back —
|
|
35
|
+
* never message-text guessing beyond the explicit LLM markers.
|
|
36
|
+
*/
|
|
37
|
+
function classifyLoopError(err) {
|
|
38
|
+
if (!err) return HEADLESS_EXIT_CODES.ERROR;
|
|
39
|
+
const status = err.status ?? err.statusCode ?? err.response?.status;
|
|
40
|
+
if (Number.isFinite(Number(status))) return HEADLESS_EXIT_CODES.MODEL_ERROR;
|
|
41
|
+
const code = err.code || err.cause?.code;
|
|
42
|
+
if (
|
|
43
|
+
typeof code === "string" &&
|
|
44
|
+
/^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|UND_ERR)/.test(
|
|
45
|
+
code,
|
|
46
|
+
)
|
|
47
|
+
) {
|
|
48
|
+
return HEADLESS_EXIT_CODES.MODEL_ERROR;
|
|
49
|
+
}
|
|
50
|
+
if (typeof err.name === "string" && /LLM|Provider|Api/i.test(err.name)) {
|
|
51
|
+
return HEADLESS_EXIT_CODES.MODEL_ERROR;
|
|
52
|
+
}
|
|
53
|
+
return HEADLESS_EXIT_CODES.ERROR;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Map a finished run's endReason (+ error flag) to an exit code.
|
|
58
|
+
* @param {string} endReason "complete" | "max_turns" | "budget-exhausted" |
|
|
59
|
+
* "cost-budget-exhausted" | "no-response" | ...
|
|
60
|
+
* @param {boolean} isError
|
|
61
|
+
*/
|
|
62
|
+
function exitCodeForEndReason(endReason, isError) {
|
|
63
|
+
if (!isError) return HEADLESS_EXIT_CODES.OK;
|
|
64
|
+
if (endReason === "max_turns" || endReason === "budget-exhausted") {
|
|
65
|
+
return HEADLESS_EXIT_CODES.MAX_TURNS;
|
|
66
|
+
}
|
|
67
|
+
if (endReason === "cost-budget-exhausted") {
|
|
68
|
+
return HEADLESS_EXIT_CODES.MAX_BUDGET;
|
|
69
|
+
}
|
|
70
|
+
return HEADLESS_EXIT_CODES.ERROR;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
HEADLESS_EXIT_CODES,
|
|
75
|
+
classifyLoopError,
|
|
76
|
+
exitCodeForEndReason,
|
|
77
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic-headless manifest helpers (gap-analysis 2026-07-11,
|
|
3
|
+
* docs/CLAUDE_CODE_CLI_GAP_ANALYSIS.md P0 "确定性 Headless").
|
|
4
|
+
*
|
|
5
|
+
* The stream-json init event carries a protocol version, whether this run
|
|
6
|
+
* persists its session, which ambient customization sources are live, and
|
|
7
|
+
* short digests of the resolved permission policy + tool surface — so a CI
|
|
8
|
+
* caller can assert "same run shape" across invocations without diffing the
|
|
9
|
+
* whole event stream.
|
|
10
|
+
*
|
|
11
|
+
* STREAM_PROTOCOL_VERSION mirrors packages/agent-sdk/src/protocol.ts
|
|
12
|
+
* PROTOCOL_VERSION — a BREAKING stream-json change must bump both in the same
|
|
13
|
+
* commit (see agent-sdk docs/PROTOCOL.md). Additive optional fields stay on
|
|
14
|
+
* the current version.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from "node:crypto";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
|
|
20
|
+
export const STREAM_PROTOCOL_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
const _require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
function shortHash(value) {
|
|
25
|
+
return crypto
|
|
26
|
+
.createHash("sha256")
|
|
27
|
+
.update(JSON.stringify(value))
|
|
28
|
+
.digest("hex")
|
|
29
|
+
.slice(0, 16);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sortedList = (v) =>
|
|
33
|
+
[...new Set((Array.isArray(v) ? v : []).filter(Boolean))].sort();
|
|
34
|
+
|
|
35
|
+
// null = "no explicit list" (the default full tool set) — kept distinct from
|
|
36
|
+
// an explicit empty list so the digest can tell the two apart.
|
|
37
|
+
const sortedListOrNull = (v) => (Array.isArray(v) ? sortedList(v) : null);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Digest of the resolved permission policy: permission mode + allow/deny tool
|
|
41
|
+
* lists + settings permission rules. Stable across key order / duplicates so
|
|
42
|
+
* two runs with the same effective policy produce the same digest.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} p
|
|
45
|
+
* @param {string} [p.permissionMode]
|
|
46
|
+
* @param {string[]} [p.allowedTools]
|
|
47
|
+
* @param {string[]} [p.disallowedTools]
|
|
48
|
+
* @param {{allow?:string[],ask?:string[],deny?:string[]}|null} [p.permissionRules]
|
|
49
|
+
* @returns {string} 16-hex-char sha256 prefix
|
|
50
|
+
*/
|
|
51
|
+
export function computePolicyDigest({
|
|
52
|
+
permissionMode,
|
|
53
|
+
allowedTools,
|
|
54
|
+
disallowedTools,
|
|
55
|
+
permissionRules,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const rules = permissionRules
|
|
58
|
+
? {
|
|
59
|
+
allow: sortedList(permissionRules.allow),
|
|
60
|
+
ask: sortedList(permissionRules.ask),
|
|
61
|
+
deny: sortedList(permissionRules.deny),
|
|
62
|
+
}
|
|
63
|
+
: null;
|
|
64
|
+
return shortHash({
|
|
65
|
+
v: 1,
|
|
66
|
+
permissionMode: permissionMode || "default",
|
|
67
|
+
allowedTools: sortedListOrNull(allowedTools),
|
|
68
|
+
disallowedTools: sortedList(disallowedTools),
|
|
69
|
+
rules,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Digest of the tool surface exposed to the model (name set, order-independent).
|
|
75
|
+
* @param {string[]} toolNames
|
|
76
|
+
* @returns {string} 16-hex-char sha256 prefix
|
|
77
|
+
*/
|
|
78
|
+
export function computeToolsHash(toolNames) {
|
|
79
|
+
return shortHash({ v: 1, tools: sortedListOrNull(toolNames) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Which ambient customization sources are LIVE for this run. Reflects the
|
|
84
|
+
* kill-switch env vars (--safe-mode / --bare flip these) plus what the runner
|
|
85
|
+
* actually wired. Under `--bare` this collapses to at most
|
|
86
|
+
* ["settings-permissions"] (+ explicit MCP), which is the reproducibility
|
|
87
|
+
* signal CI wants.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} p
|
|
90
|
+
* @param {object|null} [p.permissionRules] resolved settings permission rules
|
|
91
|
+
* @param {object|null} [p.settingsHooks] resolved settings hooks
|
|
92
|
+
* @param {boolean} [p.mcp] MCP surface enabled/connected
|
|
93
|
+
* @param {string[]} [p.enabledToolNames]
|
|
94
|
+
* @param {object} [p.env=process.env]
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
*/
|
|
97
|
+
export function buildLoadedSources({
|
|
98
|
+
permissionRules = null,
|
|
99
|
+
settingsHooks = null,
|
|
100
|
+
mcp = false,
|
|
101
|
+
enabledToolNames = null,
|
|
102
|
+
env = process.env,
|
|
103
|
+
} = {}) {
|
|
104
|
+
const on = (k) => env[k] !== "0";
|
|
105
|
+
// null tool list = "default full set" → run_skill is available.
|
|
106
|
+
const skillToolAvailable =
|
|
107
|
+
!Array.isArray(enabledToolNames) || enabledToolNames.includes("run_skill");
|
|
108
|
+
const sources = [];
|
|
109
|
+
if (permissionRules) sources.push("settings-permissions");
|
|
110
|
+
if (settingsHooks) sources.push("settings-hooks");
|
|
111
|
+
if (on("CC_PROJECT_MEMORY")) sources.push("project-memory");
|
|
112
|
+
if (on("CC_MEMORY_INJECT")) sources.push("memory-recall");
|
|
113
|
+
if (on("CC_SKILLS") && skillToolAvailable) {
|
|
114
|
+
sources.push("skills");
|
|
115
|
+
}
|
|
116
|
+
if (on("CC_PLUGINS")) sources.push("plugins");
|
|
117
|
+
if (mcp) sources.push("mcp");
|
|
118
|
+
return sources;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Machine-readable capability manifest for `cc agent --capabilities`:
|
|
123
|
+
* everything a programmatic caller needs to know up front to drive this CLI
|
|
124
|
+
* (protocol version, tool names, permission modes, IO formats, exit codes,
|
|
125
|
+
* feature flags) without parsing --help text.
|
|
126
|
+
*/
|
|
127
|
+
export function buildAgentCapabilities() {
|
|
128
|
+
let version = null;
|
|
129
|
+
try {
|
|
130
|
+
version = _require("../../package.json").version || null;
|
|
131
|
+
} catch {
|
|
132
|
+
/* manifest stays usable without a version */
|
|
133
|
+
}
|
|
134
|
+
const contract = _require("../runtime/coding-agent-contract-shared.cjs");
|
|
135
|
+
const { HEADLESS_EXIT_CODES } = _require("./exit-codes.cjs");
|
|
136
|
+
return {
|
|
137
|
+
name: "chainlesschain-cli",
|
|
138
|
+
version,
|
|
139
|
+
protocol_version: STREAM_PROTOCOL_VERSION,
|
|
140
|
+
agent_tools: contract.listCodingAgentToolNames(),
|
|
141
|
+
permission_modes: [
|
|
142
|
+
"default",
|
|
143
|
+
"manual",
|
|
144
|
+
"auto",
|
|
145
|
+
"dontAsk",
|
|
146
|
+
"plan",
|
|
147
|
+
"acceptEdits",
|
|
148
|
+
"bypassPermissions",
|
|
149
|
+
],
|
|
150
|
+
output_formats: ["text", "json", "stream-json"],
|
|
151
|
+
input_formats: ["text", "stream-json"],
|
|
152
|
+
exit_codes: HEADLESS_EXIT_CODES,
|
|
153
|
+
features: {
|
|
154
|
+
bare: true,
|
|
155
|
+
safe_mode: true,
|
|
156
|
+
ephemeral: true,
|
|
157
|
+
background: true,
|
|
158
|
+
worktree: true,
|
|
159
|
+
checkpoint: true,
|
|
160
|
+
json_schema_output: true,
|
|
161
|
+
partial_messages: true,
|
|
162
|
+
interactive_approvals: true,
|
|
163
|
+
permission_prompt_tool: true,
|
|
164
|
+
remote_control: true,
|
|
165
|
+
mcp: {
|
|
166
|
+
config_file: true,
|
|
167
|
+
registry: true,
|
|
168
|
+
tool_search: true,
|
|
169
|
+
oauth: true,
|
|
170
|
+
managed_allowlist: true,
|
|
171
|
+
},
|
|
172
|
+
sandbox_engines: ["docker", "bubblewrap"],
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
package/src/lib/hook-runner.cjs
CHANGED
|
@@ -117,6 +117,69 @@ function tryParseDecision(stdout) {
|
|
|
117
117
|
* nonBlockingError?:boolean, error?:string }}
|
|
118
118
|
*/
|
|
119
119
|
function runCommandHook(command, input = {}, opts = {}) {
|
|
120
|
+
if (!command) {
|
|
121
|
+
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
|
|
122
|
+
}
|
|
123
|
+
// Circuit breaker (gap 2026-07-11 P2 "Hooks 硬化"): N consecutive
|
|
124
|
+
// non-blocking failures (spawn error / timeout / non-zero non-2 exit) of
|
|
125
|
+
// the SAME command trip an open state — further runs are skipped for a
|
|
126
|
+
// cooldown, then one half-open trial runs; success (or a real block/ask
|
|
127
|
+
// decision) closes it. CC_HOOK_BREAKER_THRESHOLD=0 disables.
|
|
128
|
+
const breaker = hookBreakerConfig();
|
|
129
|
+
if (breaker.threshold > 0) {
|
|
130
|
+
const st = _breaker.get(command);
|
|
131
|
+
if (st && st.fails >= breaker.threshold) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
if (now - st.openedAt < breaker.cooldownMs) {
|
|
134
|
+
return {
|
|
135
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
136
|
+
reason: `hook circuit breaker open (${st.fails} consecutive failures) — skipped for cooldown`,
|
|
137
|
+
exitCode: null,
|
|
138
|
+
skipped: true,
|
|
139
|
+
breakerOpen: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// half-open: let this one trial through; the outcome below decides.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const result = _runCommandHookInner(command, input, opts);
|
|
146
|
+
if (breaker.threshold > 0) {
|
|
147
|
+
if (result.nonBlockingError) {
|
|
148
|
+
const st = _breaker.get(command) || { fails: 0, openedAt: 0 };
|
|
149
|
+
st.fails += 1;
|
|
150
|
+
if (st.fails >= breaker.threshold) st.openedAt = Date.now();
|
|
151
|
+
_breaker.set(command, st);
|
|
152
|
+
} else {
|
|
153
|
+
_breaker.delete(command); // success / block / ask closes the breaker
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Payload schema version (gap 2026-07-11 P2): every hook's stdin JSON carries
|
|
160
|
+
// schema_version so hook scripts can branch on future payload changes.
|
|
161
|
+
// Additive-only changes keep the version; a breaking reshape must bump it.
|
|
162
|
+
const HOOK_PAYLOAD_SCHEMA_VERSION = 1;
|
|
163
|
+
|
|
164
|
+
// command → { fails, openedAt } (per process; hooks are per-run anyway)
|
|
165
|
+
const _breaker = new Map();
|
|
166
|
+
|
|
167
|
+
function hookBreakerConfig(env = process.env) {
|
|
168
|
+
const threshold = Number(env.CC_HOOK_BREAKER_THRESHOLD);
|
|
169
|
+
const cooldown = Number(env.CC_HOOK_BREAKER_COOLDOWN_MS);
|
|
170
|
+
return {
|
|
171
|
+
threshold:
|
|
172
|
+
Number.isFinite(threshold) && threshold >= 0 ? Math.floor(threshold) : 3,
|
|
173
|
+
cooldownMs: Number.isFinite(cooldown) && cooldown > 0 ? cooldown : 60000,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Test seam: clear breaker state between cases. */
|
|
178
|
+
function _resetHookBreaker() {
|
|
179
|
+
_breaker.clear();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _runCommandHookInner(command, input = {}, opts = {}) {
|
|
120
183
|
const { cwd, event } = opts;
|
|
121
184
|
// spawnSync is SYNCHRONOUS — it blocks the whole agent until the hook returns
|
|
122
185
|
// or its timeout fires. Node treats a 0/undefined timeout as UNLIMITED, so a
|
|
@@ -128,15 +191,15 @@ function runCommandHook(command, input = {}, opts = {}) {
|
|
|
128
191
|
Number.isFinite(_rawTimeout) && _rawTimeout > 0
|
|
129
192
|
? Math.min(_rawTimeout, 600000)
|
|
130
193
|
: 60000;
|
|
131
|
-
if (!command) {
|
|
132
|
-
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
|
|
133
|
-
}
|
|
134
194
|
const shellKind = normalizeShellKind(opts.shell);
|
|
135
195
|
const usePowershell = shellKind === "powershell" || shellKind === "pwsh";
|
|
136
196
|
let res;
|
|
137
197
|
try {
|
|
138
198
|
const common = {
|
|
139
|
-
input: JSON.stringify(
|
|
199
|
+
input: JSON.stringify({
|
|
200
|
+
...input,
|
|
201
|
+
schema_version: input.schema_version ?? HOOK_PAYLOAD_SCHEMA_VERSION,
|
|
202
|
+
}),
|
|
140
203
|
cwd: cwd || process.cwd(),
|
|
141
204
|
encoding: "utf-8",
|
|
142
205
|
timeout,
|
|
@@ -258,5 +321,8 @@ module.exports = {
|
|
|
258
321
|
runHooks,
|
|
259
322
|
tryParseDecision,
|
|
260
323
|
HOOK_DECISIONS,
|
|
324
|
+
HOOK_PAYLOAD_SCHEMA_VERSION,
|
|
325
|
+
hookBreakerConfig,
|
|
326
|
+
_resetHookBreaker,
|
|
261
327
|
_deps,
|
|
262
328
|
};
|