chainlesschain 0.162.125 → 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/assets/{AIOps-DcqNhZhA.js → AIOps-BrTOcqIJ.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BigtmPoq.js → ActionButton-na9cocmf.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-1J_X_HF4.js → Analytics-BtDygkpt.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-DlcDG_a_.js → AppLayout-D_t5CS18.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-Do-y8_3w.js → Audit-BYMZccCy.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-S2Df-50Y.js → Backup-D2qDfAdL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-D5kgWJfk.js → BaseInput-kj456Ju9.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-aUcp3kN1.js → Chat-BY0A0ZLE.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-D1VvrOPQ.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-DTg1U7Xs.js → Checkbox-DiBhtXsh.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-YO9ZZ4Ky.js → Codegen-B1ER0L0j.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BHonE9fU.js → Col-CjLmza7D.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DA2AlEFh.js → Community-BNId05U7.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DULxLRNg.js → Compact-BuuMj7K1.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BZqfuuZL.js → Compliance-DUuDycaJ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CJe3vyMS.js → Cowork-BKHiF6uL.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CeV8AtRu.js → Cron-C7p-lHnC.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CAg66zS5.js → Crosschain-BvSOx8a6.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Dtup5JZm.js → DID-D1k9jgHv.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DAR_8PNy.js → Dashboard-B9mCCnqY.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-K5bTaCYN.js → Dropdown-ge9UHSXF.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C890uErx.js → EmailListRenderer-TZO7ppXi.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-B-Tj_8yM.js → FamilyGuardDashboard-DkhLJmzx.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C6WZ98ni.js → Federation-JLuA_8zp.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-D-LTfGWd.js → FormItemContext-C4w-rzNg.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C80DK4W7.js → GenericCardRenderer-Btns4sV9.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Cjvvv3zW.js → Git-BheXMjzC.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C0BND77H.js → Governance-D9rLlatH.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BL9kTtSu.js → Inference-DiklIMcd.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CnVTBmJf.js → KnowledgeGraph-C70EWL03.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CbCbg7K6.js → Logs-Zt_MLI10.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CG5On-fH.js → Marketplace-suQxES99.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-AYYDZZK7.js → McpTools-CclpCDpY.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-tMWbMDQq.js → Memory-B2lHOUwF.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BAOsf4wB.js → MobileBridge-BMXKoDAD.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BT-2komQ.js → MobileProjects-a6mWVdBV.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BFwfC63n.js → Mtc-BFpM4Fkj.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-DYG3CWdH.js → MtcAudit-DDQkxkbS.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-KJwd0yWT.js → Multisig-Crkd_zKQ.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BmL5dNWm.js → NLProgramming-Dpk5An7B.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DmkaVaMc.js → Notes-BrFnOMNz.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-tmVQszDZ.js → NotificationSettings-CM0iApFM.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BtrR7anf.js → OrderTableRenderer-XXjeqkdz.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-pppktQyW.js → Organization-CHFW_38T.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D3lBoQDA.js → Overflow-C-vl3EjV.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BgE2qGlZ.js → P2P-BT7Slavq.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-DRUaULap.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-BR8no2Xe.js → Permissions-DkhOAvMz.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DabbyQEF.js → PersonalDataHub-Dl7dSOvV.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-n4sNpEz8.js → Pipeline-B3nVI4p6.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CGNp4n8M.js → Privacy-Dv1SXx1Q.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CAVA5VsX.js → ProjectInit-DrAhVi5p.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-GCgkfM7A.js → ProjectSettings-h-ggCYN_.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BYK17p52.js → Projects-CM91hYdr.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DpUT5W-d.js → Providers-rfkZel3N.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-ZJxTb7vi.js → QuickAsk-XepZB7h_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CIzFRDk1.js → Recommend-DzQWsh1a.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-lQ_WDNZn.js → Reputation-CDeaal0j.js} +1 -1
- package/src/assets/web-panel/assets/{Row-1MEtrgtF.js → Row-BN4aKhFH.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-SSgcPPl4.js → RssFeed-INP6VYAA.js} +2 -2
- package/src/assets/web-panel/assets/{Search-CcTMOGNY.js → Search-ChPbwatu.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BA1KMaDx.js → Security-C97ZCY8N.js} +3 -3
- package/src/assets/web-panel/assets/{Services-_aj7czZn.js → Services-Cbtl2Ajs.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BWgg_0Zr.js → Skeleton-B13sFxBQ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C2ZIziYM.js → Skills-vI9zSIKX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CYxp4axY.js → Sla-Cr3oBH39.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C7Csp1jV.js → SpeechSettings-BwemqPPV.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-svGeswiw.js → SyncSettings-CLA78P-Z.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-ClISj6oK.js → Tasks-DBjQ_DOM.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CmO-Fp7r.js → Templates-BkSLDkBs.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-a3Ac3lxz.js → Tenant-Dn0nSlib.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DyJIRh81.js → Terminal-C8f4boru.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-acXS5N5h.js → TimelineRenderer-DW_rMtJR.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DJW0KqV4.js → Tokens-BdffrKYV.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-0HeTi4r6.js → Trigger-CX4CUg-Q.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-aFym34eo.js → Trust-Cq9Bl7Od.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CYhszHJv.js → UkeySign-BQy18_VY.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BbJxPF9X.js → VideoEditing-h5hzK7Vw.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DDthEwiu.js → Wallet-BLotT3gw.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-DQ6McYPg.js → WebAuthn-8cCANYLE.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-CnF8zRsi.js → WorkflowEditor-Db6NwtAv.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Dzkx2gTP.js → chat-BPk1LbpL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-35T51Oo5.js → colors-BnNal71p.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-TtzNrlu1.js → compact-item-W2VC0tcy.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Y1LkWrYb.js → createContext-DceVsgnZ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning--rwFDBhx.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BOYw1BgS.js → hasIn-BEq6EDcp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtlJWaSP.js → index-5gIBUFBn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLqC2sRT.js → index-APCHxOkM.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Wt5feN.js → index-B2dMhwZi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqhwG1ox.js → index-B8cPjrEH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxWUEFKy.js → index-BDC5HsxZ.js} +1 -1
- package/src/assets/web-panel/assets/index-BPFuPGKi.js +1 -0
- package/src/assets/web-panel/assets/{index-NM9Oke2k.js → index-BUkDcIwJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWgNn9As.js → index-BWcCyH6-.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVrUs6rf.js → index-BgZikQoh.js} +1 -1
- package/src/assets/web-panel/assets/index-Bwjus13Y.js +1 -0
- package/src/assets/web-panel/assets/{index-D6tG4B25.js → index-BzSzRWsw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjeUZxE5.js → index-C1K9CsGr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmkG479D.js → index-CBZVISK6.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bw1iOGhM.js → index-CEJN-R2L.js} +1 -1
- package/src/assets/web-panel/assets/{index-G4ClSmJc.js → index-CG5dGC9E.js} +1 -1
- package/src/assets/web-panel/assets/{index-D47robkX.js → index-CMGLpstm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKRlWHUp.js → index-CWrGCndd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhMSOd_2.js → index-CbcvfpCV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx0hIfC-.js → index-Ceqv4lI2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdyFg4FI.js → index-CsT6frT8.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7aKAZzS.js → index-D8PQKsDT.js} +1 -1
- package/src/assets/web-panel/assets/{index-8jffdb6b.js → index-DGncdA-j.js} +1 -1
- package/src/assets/web-panel/assets/{index-XIuZV9by.js → index-DHanQHO0.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dhfw-BXs.js → index-DaUBk28E.js} +1 -1
- package/src/assets/web-panel/assets/{index-hyqEKkBT.js → index-Di2J4b4V.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAizH273.js → index-DtuLvWZN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPGNc2tF.js → index-DvS1J8xc.js} +1 -1
- package/src/assets/web-panel/assets/{index-KgXrlfNF.js → index-DxePJdwP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFhlelzN.js → index-Et4XvL49.js} +1 -1
- package/src/assets/web-panel/assets/{index-zF77jnI2.js → index-GlFxgcj4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs82BHAj.js → index-HV119Oui.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dof7lWA_.js → index-InGW87pj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs69SI96.js → index-P1-s8JR7.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4BgCBa3.js → index-SdABCNoi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSpFe6j5.js → index-WTAZbN-c.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7dCBw_M.js → index-_2I-KzOj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGPEaeB1.js → index-m1sVaWVU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZ16GlOs.js → index-s37wwyeN.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2od7Bgx.js → index-tyWABqp9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DD5y5msp.js → initDefaultProps-DzrCDb3j.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BuoutMia.js → motion-BQERVnE3.js} +1 -1
- package/src/assets/web-panel/assets/{move-DkpjqOXg.js → move-I9ACA5XJ.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BmGJD-v-.js → mtc-parser-Dd2Pw-tr.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DftUE0Sz.js → omit-DwzYRzWV.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDRb9FIu.js → pickAttrs-BSGULyIL.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DPgtxOOD.js → placementArrow-tSYYjAts.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BQFRzFeZ.js → responsiveObserve-Dy2lqikT.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DQ4lmUSz.js → slide-RaB4Uq0c.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-C1TR4ZiQ.js → statusUtils-CBpC_wZg.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B-Suj978.js → styleChecker-BEKwPWt5.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Q7bDZ3EI.js → useFlexGapSupport-C4CnYJH1.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DHjRLyQH.js → useFs-n24jutw5.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-lQOvCvCr.js → usePersonalDataHub-DMgS8F6G.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Ie0g4-p3.js → vnode-BSp2KYcj.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CHP0YEoH.js → zoom-7UfQtTPh.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +41 -2
- package/src/commands/compliance.js +3 -1
- package/src/commands/review.js +47 -17
- package/src/commands/session.js +11 -2
- package/src/gateways/ws/message-dispatcher.js +145 -165
- package/src/gateways/ws/ws-session-gateway.js +39 -5
- package/src/harness/jsonl-session-store.js +39 -12
- package/src/harness/worktree-isolator.js +5 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/audit-logger.js +3 -1
- package/src/lib/chat-core.js +5 -2
- package/src/lib/chat-intent-service.js +68 -21
- package/src/lib/config-manager.js +25 -1
- package/src/lib/credential-guard.js +43 -1
- package/src/lib/git-integration.js +5 -0
- package/src/lib/interaction-adapter.js +32 -11
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/llm-config-defaults.js +37 -0
- package/src/lib/mcp-oauth.js +76 -10
- 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-denials.js +137 -0
- 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/settings-hooks.cjs +133 -0
- package/src/lib/settings-loader.cjs +16 -7
- package/src/lib/social-graph.js +3 -1
- package/src/lib/stream-retry.js +27 -0
- package/src/lib/turn-context.js +15 -5
- package/src/repl/agent-repl.js +81 -8
- package/src/runtime/__tests__/message-roles.test.js +36 -3
- package/src/runtime/agent-core.js +175 -50
- 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 +83 -2
- package/src/runtime/headless-stream.js +69 -3
- package/src/runtime/mcp-config.js +42 -1
- package/src/runtime/message-roles.js +14 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-D26Bz5gS.js +0 -7
- package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +0 -1
- package/src/assets/web-panel/assets/index-CEwuCM44.js +0 -1
- package/src/assets/web-panel/assets/index-DzzgU4IU.js +0 -1
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). */
|
|
@@ -371,17 +375,56 @@ export function tokenStorePath() {
|
|
|
371
375
|
return pathDefault.join(_deps.homedir(), ".chainlesschain", "mcp-oauth.json");
|
|
372
376
|
}
|
|
373
377
|
|
|
374
|
-
|
|
375
|
-
|
|
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: {} };
|
|
376
386
|
try {
|
|
377
|
-
if (!_deps.fs.existsSync(file)) return {};
|
|
378
387
|
const data = JSON.parse(_deps.fs.readFileSync(file, "utf-8"));
|
|
379
|
-
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;
|
|
380
414
|
} catch {
|
|
381
|
-
return
|
|
415
|
+
return "";
|
|
382
416
|
}
|
|
383
417
|
}
|
|
384
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
|
+
|
|
385
428
|
export function getStoredToken(serverUrl) {
|
|
386
429
|
return loadTokenStore()[serverKey(serverUrl)] || null;
|
|
387
430
|
}
|
|
@@ -527,7 +570,15 @@ function withStoreLock(file, fn) {
|
|
|
527
570
|
export function saveStoredToken(serverUrl, record) {
|
|
528
571
|
const file = tokenStorePath();
|
|
529
572
|
return withStoreLock(file, () => {
|
|
530
|
-
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;
|
|
531
582
|
store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
|
|
532
583
|
_writeStoreSecure(file, store);
|
|
533
584
|
return store[serverKey(serverUrl)];
|
|
@@ -560,13 +611,28 @@ export function isTokenExpired(record, { skewMs = 60_000 } = {}) {
|
|
|
560
611
|
/**
|
|
561
612
|
* Return a valid bearer token for a server, refreshing if expired. Returns the
|
|
562
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]
|
|
563
623
|
*/
|
|
564
|
-
export async function ensureValidToken(
|
|
624
|
+
export async function ensureValidToken(
|
|
625
|
+
serverUrl,
|
|
626
|
+
{ forceRefresh = false } = {},
|
|
627
|
+
) {
|
|
565
628
|
const record = getStoredToken(serverUrl);
|
|
566
629
|
if (!record) return null;
|
|
567
|
-
if (!isTokenExpired(record)) return record.access_token;
|
|
630
|
+
if (!forceRefresh && !isTokenExpired(record)) return record.access_token;
|
|
568
631
|
if (!record.refresh_token || !record.endpoints?.token_endpoint) {
|
|
569
|
-
|
|
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;
|
|
570
636
|
}
|
|
571
637
|
try {
|
|
572
638
|
const tok = await refreshAccessToken(
|
|
@@ -576,7 +642,7 @@ export async function ensureValidToken(serverUrl) {
|
|
|
576
642
|
const updated = saveStoredToken(serverUrl, { ...record, ...tok });
|
|
577
643
|
return updated.access_token;
|
|
578
644
|
} catch {
|
|
579
|
-
return record.access_token || null;
|
|
645
|
+
return forceRefresh ? null : record.access_token || null;
|
|
580
646
|
}
|
|
581
647
|
}
|
|
582
648
|
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/lib/sandbox-v2.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
|
+
import { safeJsonParse } from "./safe-json.js";
|
|
7
8
|
import { createRequire } from "module";
|
|
8
9
|
|
|
9
10
|
const _require = createRequire(import.meta.url);
|
|
@@ -429,9 +430,9 @@ export function getSandbox(db, sandboxId) {
|
|
|
429
430
|
id: row.id,
|
|
430
431
|
agentId: row.agent_id,
|
|
431
432
|
status: row.status,
|
|
432
|
-
permissions:
|
|
433
|
-
quota:
|
|
434
|
-
resourceUsage:
|
|
433
|
+
permissions: safeJsonParse(row.permissions, {}),
|
|
434
|
+
quota: safeJsonParse(row.quota, {}),
|
|
435
|
+
resourceUsage: safeJsonParse(row.resource_usage, {}),
|
|
435
436
|
createdAt: row.created_at,
|
|
436
437
|
};
|
|
437
438
|
}
|
|
@@ -609,9 +610,9 @@ export function restoreFromDb(db) {
|
|
|
609
610
|
id: row.id,
|
|
610
611
|
agentId: row.agent_id,
|
|
611
612
|
status: row.status || "active",
|
|
612
|
-
permissions:
|
|
613
|
-
quota:
|
|
614
|
-
resourceUsage:
|
|
613
|
+
permissions: safeJsonParse(row.permissions, {}),
|
|
614
|
+
quota: safeJsonParse(row.quota, {}),
|
|
615
|
+
resourceUsage: safeJsonParse(row.resource_usage, {}),
|
|
615
616
|
createdAt: row.created_at || new Date(now).toISOString(),
|
|
616
617
|
policy,
|
|
617
618
|
createdAtMs: row.created_at_ms || now,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search-command — build the shell command for the agent `search_files` tool
|
|
3
|
+
* with the model/user-supplied pattern SAFELY embedded.
|
|
4
|
+
*
|
|
5
|
+
* The pattern flows into execSync (a real shell), so a raw interpolation
|
|
6
|
+
* (`grep -i "${pattern}"`) is a command-injection vector: a pattern like
|
|
7
|
+
* `x" ; curl evil ; "` runs arbitrary commands — and `search_files` is
|
|
8
|
+
* read-only + NOT confirm-gated, so it would bypass the `run_shell`
|
|
9
|
+
* ApprovalGate + credential guards entirely (reachable via prompt-injection).
|
|
10
|
+
* Raw interpolation is also semantically wrong: the shell would expand `$HOME`,
|
|
11
|
+
* backticks, globs, etc. in the pattern instead of searching for them literally.
|
|
12
|
+
*
|
|
13
|
+
* Fix: quote the pattern so every shell metacharacter is inert. The emitted
|
|
14
|
+
* commands (and therefore the output format `_ingestSearchOutput` parses) are
|
|
15
|
+
* otherwise unchanged.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** POSIX single-quote: wrap in '…' and escape any embedded ' as '\''. Single
|
|
19
|
+
* quotes neutralize EVERY shell metacharacter, so nothing inside is expanded
|
|
20
|
+
* or can break out. */
|
|
21
|
+
export function shquotePosix(s) {
|
|
22
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build `{ cmd }` for a search, or `{ error }` when the pattern can't be made
|
|
27
|
+
* safe on the target platform.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} a
|
|
30
|
+
* @param {string} a.pattern the raw search pattern
|
|
31
|
+
* @param {boolean} a.isContent content search (grep/findstr) vs filename
|
|
32
|
+
* @param {string} [a.platform=process.platform]
|
|
33
|
+
* @returns {{cmd:string}|{error:string}}
|
|
34
|
+
*/
|
|
35
|
+
export function buildSearchCommand({
|
|
36
|
+
pattern,
|
|
37
|
+
isContent,
|
|
38
|
+
platform = process.platform,
|
|
39
|
+
}) {
|
|
40
|
+
const pat = String(pattern ?? "");
|
|
41
|
+
|
|
42
|
+
if (platform === "win32") {
|
|
43
|
+
// cmd.exe has no reliable way to escape an embedded double-quote, and the
|
|
44
|
+
// pattern is double-quoted below — so reject a literal `"` (rare in a search
|
|
45
|
+
// pattern; run_shell handles complex cases). Every OTHER metacharacter
|
|
46
|
+
// (& | < > ^ ( )) is literal INSIDE cmd double quotes, so quoting the
|
|
47
|
+
// pattern closes the injection while leaving the redirect (2>NUL) outside.
|
|
48
|
+
if (pat.includes('"')) {
|
|
49
|
+
return {
|
|
50
|
+
error: 'search pattern may not contain a double-quote (") on Windows',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
cmd: isContent
|
|
55
|
+
? `findstr /s /i /n "${pat}" *`
|
|
56
|
+
: `dir /s /b "*${pat}*" 2>NUL`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
cmd: isContent
|
|
62
|
+
? `grep -r -l -i ${shquotePosix(pat)} . --include="*" 2>/dev/null | head -20`
|
|
63
|
+
: `find . -name ${shquotePosix(`*${pat}*`)} -type f 2>/dev/null | head -20`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
const path = require("node:path");
|
|
28
28
|
const os = require("node:os");
|
|
29
29
|
const fsDefault = require("node:fs");
|
|
30
|
+
const crypto = require("node:crypto");
|
|
30
31
|
const { SUGGEST_UMBRELLA } = require("./permission-rules.cjs");
|
|
31
32
|
const { projectRootBase } = require("./project-root.cjs");
|
|
32
33
|
|
|
@@ -179,8 +180,140 @@ function collectHooks(hooksBlock, event, toolName) {
|
|
|
179
180
|
return out;
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Every command-hook in a parsed settings object, tagged with the event +
|
|
185
|
+
* matcher it fires under (so a fingerprint reflects WHEN it runs, not just the
|
|
186
|
+
* command string). Mirrors loadHooks' command-hook filter.
|
|
187
|
+
*/
|
|
188
|
+
function extractCommandHooks(data) {
|
|
189
|
+
const out = [];
|
|
190
|
+
const block =
|
|
191
|
+
data && data.hooks && typeof data.hooks === "object" ? data.hooks : null;
|
|
192
|
+
if (!block) return out;
|
|
193
|
+
for (const [event, groups] of Object.entries(block)) {
|
|
194
|
+
if (!Array.isArray(groups)) continue;
|
|
195
|
+
for (const g of groups) {
|
|
196
|
+
if (!g || typeof g !== "object" || !Array.isArray(g.hooks)) continue;
|
|
197
|
+
for (const h of g.hooks) {
|
|
198
|
+
if (h && h.type === "command" && h.command) {
|
|
199
|
+
out.push({
|
|
200
|
+
event,
|
|
201
|
+
matcher: g.matcher ?? null,
|
|
202
|
+
command: String(h.command),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** `~/.chainlesschain/hook-trust.json` — remembered first-run acknowledgments. */
|
|
212
|
+
function hookTrustStorePath() {
|
|
213
|
+
return path.join(_deps.homedir(), ".chainlesschain", "hook-trust.json");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readHookTrustStore() {
|
|
217
|
+
try {
|
|
218
|
+
const f = hookTrustStorePath();
|
|
219
|
+
if (!_deps.fs.existsSync(f)) return {};
|
|
220
|
+
const parsed = JSON.parse(_deps.fs.readFileSync(f, "utf-8"));
|
|
221
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
222
|
+
} catch {
|
|
223
|
+
return {}; // unreadable store → treat as nothing acknowledged
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function writeHookTrustStore(store) {
|
|
228
|
+
try {
|
|
229
|
+
const f = hookTrustStorePath();
|
|
230
|
+
_deps.fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
231
|
+
_deps.fs.writeFileSync(f, JSON.stringify(store, null, 2), "utf-8");
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false; // best-effort — a failed write just re-shows the notice
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* First-run trust notice for project-sourced settings.json hooks (which run
|
|
240
|
+
* shell). The user's own `~/.claude/settings.json` and an explicit `--settings`
|
|
241
|
+
* file are trusted; only the project's `.claude/settings{,.local}.json` (project
|
|
242
|
+
* root + cwd) carry the cloned-repo auto-execution risk that Claude-Code 2.1.195
|
|
243
|
+
* gated ("untrusted project config must require explicit consent").
|
|
244
|
+
*
|
|
245
|
+
* Returns a one-line notice (and records an acknowledgment hash) the FIRST time
|
|
246
|
+
* a project's shell-running hooks are seen — and again only if those hooks
|
|
247
|
+
* change. Returns null when there are no project hooks, the hooks are unchanged
|
|
248
|
+
* since last acknowledged, or the notice is disabled (`CC_HOOK_TRUST_NOTICE=0`,
|
|
249
|
+
* or hooks themselves are off via `CC_SETTINGS_HOOKS=0`). Best-effort: a failed
|
|
250
|
+
* store write still returns the notice (shown again next run) rather than
|
|
251
|
+
* throwing — it must never abort agent startup.
|
|
252
|
+
*
|
|
253
|
+
* @returns {string|null}
|
|
254
|
+
*/
|
|
255
|
+
function projectHookTrustNotice({ cwd = process.cwd(), settingsFile } = {}) {
|
|
256
|
+
if (process.env.CC_HOOK_TRUST_NOTICE === "0") return null;
|
|
257
|
+
if (process.env.CC_SETTINGS_HOOKS === "0") return null; // hooks won't run anyway
|
|
258
|
+
const home = path.join(_deps.homedir(), ".claude", "settings.json");
|
|
259
|
+
const explicit = settingsFile ? path.resolve(cwd, settingsFile) : null;
|
|
260
|
+
const trusted = new Set([home, explicit].filter(Boolean));
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
const projectFiles = settingsFiles(cwd, settingsFile).filter((f) => {
|
|
263
|
+
if (trusted.has(f) || seen.has(f)) return false;
|
|
264
|
+
seen.add(f);
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const entries = [];
|
|
269
|
+
const contributing = [];
|
|
270
|
+
for (const f of projectFiles) {
|
|
271
|
+
let data;
|
|
272
|
+
try {
|
|
273
|
+
if (!_deps.fs.existsSync(f)) continue;
|
|
274
|
+
data = JSON.parse(_deps.fs.readFileSync(f, "utf-8"));
|
|
275
|
+
} catch {
|
|
276
|
+
continue; // malformed JSON is surfaced by loadHooks' own warn path
|
|
277
|
+
}
|
|
278
|
+
const cmds = extractCommandHooks(data);
|
|
279
|
+
if (cmds.length) {
|
|
280
|
+
entries.push(...cmds);
|
|
281
|
+
contributing.push(f);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (entries.length === 0) return null;
|
|
285
|
+
|
|
286
|
+
const fingerprint = entries
|
|
287
|
+
.map((e) => `${e.event}${e.matcher ?? ""}${e.command}`)
|
|
288
|
+
.sort();
|
|
289
|
+
const hash = crypto
|
|
290
|
+
.createHash("sha256")
|
|
291
|
+
.update(JSON.stringify(fingerprint))
|
|
292
|
+
.digest("hex");
|
|
293
|
+
const key = projectRootBase(cwd, { fs: _deps.fs, path }) || cwd;
|
|
294
|
+
|
|
295
|
+
const store = readHookTrustStore();
|
|
296
|
+
if (store[key] && store[key].hash === hash) return null; // already acknowledged
|
|
297
|
+
store[key] = {
|
|
298
|
+
hash,
|
|
299
|
+
files: contributing,
|
|
300
|
+
count: entries.length,
|
|
301
|
+
at: new Date().toISOString(),
|
|
302
|
+
};
|
|
303
|
+
writeHookTrustStore(store); // best-effort acknowledgment
|
|
304
|
+
|
|
305
|
+
const fileList = contributing.map((f) => ` ${f}`).join("\n");
|
|
306
|
+
return (
|
|
307
|
+
`⚠ This project's .claude/settings.json defines ${entries.length} ` +
|
|
308
|
+
`shell-running hook(s) that auto-run on agent activity:\n${fileList}\n` +
|
|
309
|
+
` Review them before trusting this repo. Shown once per change; ` +
|
|
310
|
+
`disable all hooks with CC_SETTINGS_HOOKS=0.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
182
314
|
module.exports = {
|
|
183
315
|
loadHooks,
|
|
316
|
+
projectHookTrustNotice,
|
|
184
317
|
collectHooks,
|
|
185
318
|
compileMatcher,
|
|
186
319
|
umbrellaFor,
|
|
@@ -240,12 +240,16 @@ function loadSettingsConfig(opts = {}) {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
|
-
* Read a
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* a
|
|
243
|
+
* Read a boolean setting across the layered `.claude/settings.json` files
|
|
244
|
+
* (last/closest layer wins — same precedence as the permission rules). The key
|
|
245
|
+
* may be a dotted path for nested settings (e.g. `autoMode.classifyAllShell`);
|
|
246
|
+
* a plain key walks a one-element path, so existing callers are unaffected.
|
|
247
|
+
* Non-boolean values (and partial paths through a non-object) are ignored.
|
|
248
|
+
* Returns `undefined` when no layer sets it, so a caller can distinguish
|
|
249
|
+
* "unset" from an explicit `false`.
|
|
247
250
|
*
|
|
248
|
-
* @param {string} key
|
|
251
|
+
* @param {string} key settings key or dotted path (e.g. "respondToBashCommands"
|
|
252
|
+
* or "autoMode.classifyAllShell")
|
|
249
253
|
* @param {object} [opts]
|
|
250
254
|
* @param {string} [opts.cwd=process.cwd()]
|
|
251
255
|
* @param {string} [opts.settingsFile]
|
|
@@ -254,11 +258,16 @@ function loadSettingsConfig(opts = {}) {
|
|
|
254
258
|
*/
|
|
255
259
|
function readBooleanSetting(key, opts = {}) {
|
|
256
260
|
const cwd = opts.cwd || process.cwd();
|
|
261
|
+
const parts = String(key).split(".");
|
|
257
262
|
let value;
|
|
258
263
|
for (const file of settingsPaths(cwd, opts.settingsFile)) {
|
|
259
264
|
const data = readSettingsFile(file, { onWarn: opts.onWarn });
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
let node = data;
|
|
266
|
+
for (const p of parts) {
|
|
267
|
+
node = node && typeof node === "object" ? node[p] : undefined;
|
|
268
|
+
}
|
|
269
|
+
if (typeof node === "boolean") {
|
|
270
|
+
value = node; // last (closest) layer wins
|
|
262
271
|
}
|
|
263
272
|
}
|
|
264
273
|
return value;
|