chainlesschain 0.162.159 → 0.162.161
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-DMn528q-.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-NlgSFZq5.js → ActionButton-D9ZcDCJL.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D17cOUcb.js → Analytics-A_K-8PX0.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-B4mEZcbl.js → AppLayout-D4pFW3X6.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CASsmyQw.js → Artifacts-Dz46COAd.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DYAia2e2.js → Audit-cbMjbyg0.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C1lvYUSu.js → BackgroundAgents-Dtf88GAj.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B6agK6bA.js → Backup-C5KRxn52.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DM43R2vy.js → BaseInput-Dd2UK3BN.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Dtdp3eXp.js → Chat-lptKTFdG.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BL5c_OQ1.js → ChatBubbleRenderer-C2KLtwHz.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-D2xf7uZt.js → Checkbox-_yspG6rG.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-D3j45jJL.js → Codegen-BGq_GAha.js} +1 -1
- package/src/assets/web-panel/assets/{Col-voPJ9fUZ.js → Col-CePor71g.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DEsyTESF.js → Community-CLOWhIT7.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-OHV_QXLD.js → Compact-CivJt2vT.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-eDTJWC-J.js → Compliance-Btmo1GGE.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BBvy-6d1.js → Cowork-Ct35OlG4.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Dw2eoJxu.js → Cron-CTc2GmhB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DQHms_Bh.js → Crosschain-ZtiBETk1.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Cyu7Id1c.js → DID-BNqMQQXw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-sIDzkG92.js → Dashboard-BmzZd-9V.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DUW3T2ey.js → Dropdown-CMVgvP2M.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CECELXW8.js → EmailListRenderer-CcqQFAfT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BIZR37ov.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BEZIQX84.js → Federation-CiKOOzlb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BcdUMznL.js → FormItemContext-Wk-OT0gr.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DsN_fr5H.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
- package/src/assets/web-panel/assets/{Git-63LhjBx2.js → Git-0ZvCPSD9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYtQtIcH.js → Governance-CmVuKkPb.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BwR6UUox.js → Inference-CTWE3Jwg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D9lVNTUA.js → KnowledgeGraph-CBEIF00L.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-C4q5h7Ut.js → Logs-DLhxDwVG.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Plksliuw.js → Marketplace-BgsCBrnc.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DZUFfeuG.js → McpTools-ClBUGDS2.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-NoDYt29V.js → Memory-CnG_ZnYw.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CEXGTkdh.js → MobileBridge-BUOAbR1Z.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DnbJkbZo.js → MobileProjects-Dnt_9-WB.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DTAa0cwy.js → Mtc-CLjt4kCD.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CVV7LnNS.js → MtcAudit-f24U9Aes.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BqqGrjt_.js → Multisig-GrmGQbSq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C4WP8DRp.js → NLProgramming-DADaWn-n.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Zo4DJUVI.js → Notes-xtf5RX6C.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DBcjRaz2.js → NotificationSettings-Cd27Rgtx.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-fXRxlDjl.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BT9WPBOX.js → Organization-joCFlNB0.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-D6b4bZ3q.js → Overflow-B1HoURq0.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cw3OsYx-.js → P2P-DiwsKbT7.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C0nyiK2b.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-zVBZEB_C.js → Permissions-C23qi8kl.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BOspsFew.js → PersonalDataHub-BS1syX2f.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CQWfZ4XH.js → Pipeline-DRYwwDyh.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-C2oGJ_WL.js → Privacy-BQKaDHPH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BZMR4qY1.js → ProjectInit-BC6F9xiG.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DAujoP63.js → ProjectSettings-ClFPc1Nd.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +1 -0
- package/src/assets/web-panel/assets/{Providers-D_RGHM-v.js → Providers-CLWuOUNt.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Mf6KfL2J.js → QrScannerModal-DiqULTxb.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-5D8URoI_.js → QuickAsk-9A03P-E_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Df4kC8Gy.js → Recommend-CJZAnbq_.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DGi3f3vO.js → RemoteSession-C9A1R77h.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-jmkGFNWb.js → Reputation-BIZc6Rky.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DP6Pcu3m.js → Row-DfIPNtuG.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D60hSuPO.js → RssFeed-C5znFcQ1.js} +3 -3
- package/src/assets/web-panel/assets/{Search-0mf1BQUK.js → Search-DSs2z3OX.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CfZtLFYV.js → Security-CV6LcrLY.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D9zpZLWz.js → Services-cmhoX0Xj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-D1_44eMD.js → Skeleton-tCsg2po7.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-cVFa8zkz.js → Skills-BK1KUDiy.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Cinpzc3L.js → Sla-yRgyeA82.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-bqdiJE_P.js → SpeechSettings-c1mo1mlp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Dgl_SLgj.js → SyncSettings-koIAfuT3.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DawWAEhy.js → Tasks-DkkreAaB.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Ca0nY651.js → Templates-fy5Ctf9M.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CgaGEhLm.js → Tenant-Bw70Jf6S.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D-9EnpRx.js → Terminal-BKh6Rx-4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CSOxDHZ6.js → TimelineRenderer-Dg54WP0c.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BpwLZpL3.js → Tokens-BvvD5SxJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DgowwBK5.js → Trigger-B7Ug9x4e.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BZutapjE.js → Trust-BjvuMORY.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-QZPt8_Sp.js → UkeySign-CZqhkx8p.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DlQx5UDJ.js → VideoEditing-ClR50lhX.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ZfdTgtWA.js → Wallet-DlfppYjx.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CAp0y_Cr.js → WebAuthn-BirgVr8B.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BVITZEmr.js → WorkflowEditor-B8DP_bax.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CC2tPaNg.js → chat-DA8AUZzL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-31brc9CA.js → colors-C38pLGp1.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DLMkC9C-.js → compact-item-81rtr98o.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BMtveI6F.js → createContext-D34ixq8t.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DbD6WHW2.js → hasIn-D4bzqTrZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoO4WEJB.js → index-9qoVpPt6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJhREOlG.js → index-B0QArPQt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnFLzejT.js → index-B8mkLM4x.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bbc2JeQG.js → index-BIP62a6r.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFXKbOek.js → index-BP-zkXK8.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-cGlCDW.js → index-BS3M5H9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-AaIKG60c.js → index-BXd2_LmH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXa-4zd.js → index-BlxEphl7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZl-UXB6.js → index-BqIhLAbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOM-VFlv.js → index-BtGxuQrO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcVszn2b.js → index-BvD7b2IK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C21pdBqs.js → index-BvL_3J_n.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJvxFyIz.js → index-COe_cOLJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-ALvoNTRF.js → index-CP5alL4t.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
- package/src/assets/web-panel/assets/{index-Dm0byjm1.js → index-CbhE3j5j.js} +1 -1
- package/src/assets/web-panel/assets/{index-WQUO2vvK.js → index-Cd3KuNK5.js} +1 -1
- package/src/assets/web-panel/assets/{index-bdaJwTXJ.js → index-CoVgZw_b.js} +1 -1
- package/src/assets/web-panel/assets/{index-COHHQ9cU.js → index-CuS-UPoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7LHNw_N.js → index-D0Ih7Hwn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cod97Urs.js → index-D3zCCZRS.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cwbqh0KZ.js → index-D6GfvEQG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKAB6-6V.js → index-DBFSo1NF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEwjBA9q.js → index-DFHa1HIg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DndNnYc0.js → index-DFyWKOBD.js} +1 -1
- package/src/assets/web-panel/assets/{index-3oBsXZmF.js → index-DMgnRwrK.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_eUKGYP.js → index-DUQLXLQM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUmLMFsy.js → index-D_2VYPr1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPpS-lGb.js → index-DakfFZ-k.js} +1 -1
- package/src/assets/web-panel/assets/{index-ep6JchNG.js → index-Ddk0e_Pi.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7cV0tu0.js → index-DkHqMnn3.js} +1 -1
- package/src/assets/web-panel/assets/{index-jG6h22Qg.js → index-Dmo3dFHb.js} +1 -1
- package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
- package/src/assets/web-panel/assets/{index-C1PfeI88.js → index-DvfCEJG-.js} +1 -1
- package/src/assets/web-panel/assets/{index-ntVQqSML.js → index-E1OJRXs4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWgfipcC.js → index-F76w1NiL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiRQUoyE.js → index-ZRJRItm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFRLmCR3.js → index-fI5sXMLg.js} +1 -1
- package/src/assets/web-panel/assets/{index-D67kIagl.js → index-tKiC8T-4.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D5UoBRhN.js → initDefaultProps-ByyvflIR.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DltOw0CR.js → motion-B3FQm2By.js} +1 -1
- package/src/assets/web-panel/assets/{move-_oPkk5zj.js → move-BSfns6Ef.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-EES8ROCS.js → mtc-parser-CTG1Ieri.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B-_39x_M.js → omit-DObM_2j6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-l6D0yJHz.js → pickAttrs-DOfelQ1t.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DIeUK31L.js → placementArrow-CHZILds_.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-dG4EzUQo.js → responsiveObserve-REbucKSx.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DWFzYGdH.js → slide-C6EZzD7o.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CECdTA5H.js → statusUtils-BxkWS-g3.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-D6kP-mMK.js → styleChecker-Chklyoq6.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BWfna_rd.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Bb-j6rzo.js → useFs-DV24ik_J.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D5OIsYM4.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DNpwzFTM.js → vnode-By-v5GlF.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-vNLpFQBW.js → zoom-B1w9Am3r.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +72 -6
- package/src/commands/background-session.js +46 -2
- package/src/commands/mcp.js +46 -0
- package/src/commands/session.js +70 -0
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/harness/jsonl-session-store.js +70 -1
- package/src/harness/mcp-client.js +50 -0
- package/src/lib/agent-authority.js +251 -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-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +320 -8
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/dependency-install-policy.js +130 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/exit-codes.cjs +77 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +188 -0
- package/src/lib/hook-runner.cjs +70 -4
- package/src/lib/ide-context-redaction.js +130 -0
- package/src/lib/ide-context.js +128 -13
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/llm-config-defaults.js +24 -8
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/project-mcp-trust.js +87 -0
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/lib/sensitive-file-guard.js +51 -0
- package/src/lib/sub-agent-context.js +7 -3
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +113 -10
- package/src/runtime/coding-agent-contract-shared.cjs +19 -2
- package/src/runtime/headless-runner.js +66 -7
- package/src/runtime/headless-stream.js +224 -12
- package/src/runtime/mcp-config.js +32 -1
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.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,188 @@
|
|
|
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
|
+
import { PROTOCOL_MIN_VERSION } from "./capability-negotiation.js";
|
|
20
|
+
|
|
21
|
+
export const STREAM_PROTOCOL_VERSION = 1;
|
|
22
|
+
|
|
23
|
+
const _require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
function shortHash(value) {
|
|
26
|
+
return crypto
|
|
27
|
+
.createHash("sha256")
|
|
28
|
+
.update(JSON.stringify(value))
|
|
29
|
+
.digest("hex")
|
|
30
|
+
.slice(0, 16);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sortedList = (v) =>
|
|
34
|
+
[...new Set((Array.isArray(v) ? v : []).filter(Boolean))].sort();
|
|
35
|
+
|
|
36
|
+
// null = "no explicit list" (the default full tool set) — kept distinct from
|
|
37
|
+
// an explicit empty list so the digest can tell the two apart.
|
|
38
|
+
const sortedListOrNull = (v) => (Array.isArray(v) ? sortedList(v) : null);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Digest of the resolved permission policy: permission mode + allow/deny tool
|
|
42
|
+
* lists + settings permission rules. Stable across key order / duplicates so
|
|
43
|
+
* two runs with the same effective policy produce the same digest.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} p
|
|
46
|
+
* @param {string} [p.permissionMode]
|
|
47
|
+
* @param {string[]} [p.allowedTools]
|
|
48
|
+
* @param {string[]} [p.disallowedTools]
|
|
49
|
+
* @param {{allow?:string[],ask?:string[],deny?:string[]}|null} [p.permissionRules]
|
|
50
|
+
* @returns {string} 16-hex-char sha256 prefix
|
|
51
|
+
*/
|
|
52
|
+
export function computePolicyDigest({
|
|
53
|
+
permissionMode,
|
|
54
|
+
allowedTools,
|
|
55
|
+
disallowedTools,
|
|
56
|
+
permissionRules,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const rules = permissionRules
|
|
59
|
+
? {
|
|
60
|
+
allow: sortedList(permissionRules.allow),
|
|
61
|
+
ask: sortedList(permissionRules.ask),
|
|
62
|
+
deny: sortedList(permissionRules.deny),
|
|
63
|
+
}
|
|
64
|
+
: null;
|
|
65
|
+
return shortHash({
|
|
66
|
+
v: 1,
|
|
67
|
+
permissionMode: permissionMode || "default",
|
|
68
|
+
allowedTools: sortedListOrNull(allowedTools),
|
|
69
|
+
disallowedTools: sortedList(disallowedTools),
|
|
70
|
+
rules,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Digest of the tool surface exposed to the model (name set, order-independent).
|
|
76
|
+
* @param {string[]} toolNames
|
|
77
|
+
* @returns {string} 16-hex-char sha256 prefix
|
|
78
|
+
*/
|
|
79
|
+
export function computeToolsHash(toolNames) {
|
|
80
|
+
return shortHash({ v: 1, tools: sortedListOrNull(toolNames) });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Which ambient customization sources are LIVE for this run. Reflects the
|
|
85
|
+
* kill-switch env vars (--safe-mode / --bare flip these) plus what the runner
|
|
86
|
+
* actually wired. Under `--bare` this collapses to at most
|
|
87
|
+
* ["settings-permissions"] (+ explicit MCP), which is the reproducibility
|
|
88
|
+
* signal CI wants.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} p
|
|
91
|
+
* @param {object|null} [p.permissionRules] resolved settings permission rules
|
|
92
|
+
* @param {object|null} [p.settingsHooks] resolved settings hooks
|
|
93
|
+
* @param {boolean} [p.mcp] MCP surface enabled/connected
|
|
94
|
+
* @param {string[]} [p.enabledToolNames]
|
|
95
|
+
* @param {object} [p.env=process.env]
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
export function buildLoadedSources({
|
|
99
|
+
permissionRules = null,
|
|
100
|
+
settingsHooks = null,
|
|
101
|
+
mcp = false,
|
|
102
|
+
enabledToolNames = null,
|
|
103
|
+
env = process.env,
|
|
104
|
+
} = {}) {
|
|
105
|
+
const on = (k) => env[k] !== "0";
|
|
106
|
+
// null tool list = "default full set" → run_skill is available.
|
|
107
|
+
const skillToolAvailable =
|
|
108
|
+
!Array.isArray(enabledToolNames) || enabledToolNames.includes("run_skill");
|
|
109
|
+
const sources = [];
|
|
110
|
+
if (permissionRules) sources.push("settings-permissions");
|
|
111
|
+
if (settingsHooks) sources.push("settings-hooks");
|
|
112
|
+
if (on("CC_PROJECT_MEMORY")) sources.push("project-memory");
|
|
113
|
+
if (on("CC_MEMORY_INJECT")) sources.push("memory-recall");
|
|
114
|
+
if (on("CC_SKILLS") && skillToolAvailable) {
|
|
115
|
+
sources.push("skills");
|
|
116
|
+
}
|
|
117
|
+
if (on("CC_PLUGINS")) sources.push("plugins");
|
|
118
|
+
if (mcp) sources.push("mcp");
|
|
119
|
+
return sources;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Machine-readable capability manifest for `cc agent --capabilities`:
|
|
124
|
+
* everything a programmatic caller needs to know up front to drive this CLI
|
|
125
|
+
* (protocol version, tool names, permission modes, IO formats, exit codes,
|
|
126
|
+
* feature flags) without parsing --help text.
|
|
127
|
+
*/
|
|
128
|
+
export function buildAgentCapabilities() {
|
|
129
|
+
let version = null;
|
|
130
|
+
try {
|
|
131
|
+
version = _require("../../package.json").version || null;
|
|
132
|
+
} catch {
|
|
133
|
+
/* manifest stays usable without a version */
|
|
134
|
+
}
|
|
135
|
+
const contract = _require("../runtime/coding-agent-contract-shared.cjs");
|
|
136
|
+
const { HEADLESS_EXIT_CODES } = _require("./exit-codes.cjs");
|
|
137
|
+
return {
|
|
138
|
+
name: "chainlesschain-cli",
|
|
139
|
+
version,
|
|
140
|
+
protocol_version: STREAM_PROTOCOL_VERSION,
|
|
141
|
+
// Oldest wire version this CLI can still negotiate down to (the N-1 in the
|
|
142
|
+
// capability handshake, agent-sdk docs/PROTOCOL.md §1.3). At v1 there is no
|
|
143
|
+
// older line shape, so min === current.
|
|
144
|
+
min_protocol_version: PROTOCOL_MIN_VERSION,
|
|
145
|
+
agent_tools: contract.listCodingAgentToolNames(),
|
|
146
|
+
permission_modes: [
|
|
147
|
+
"default",
|
|
148
|
+
"manual",
|
|
149
|
+
"auto",
|
|
150
|
+
"dontAsk",
|
|
151
|
+
"plan",
|
|
152
|
+
"acceptEdits",
|
|
153
|
+
"bypassPermissions",
|
|
154
|
+
],
|
|
155
|
+
output_formats: ["text", "json", "stream-json"],
|
|
156
|
+
input_formats: ["text", "stream-json"],
|
|
157
|
+
exit_codes: HEADLESS_EXIT_CODES,
|
|
158
|
+
features: {
|
|
159
|
+
bare: true,
|
|
160
|
+
safe_mode: true,
|
|
161
|
+
ephemeral: true,
|
|
162
|
+
background: true,
|
|
163
|
+
worktree: true,
|
|
164
|
+
checkpoint: true,
|
|
165
|
+
json_schema_output: true,
|
|
166
|
+
partial_messages: true,
|
|
167
|
+
interactive_approvals: true,
|
|
168
|
+
permission_prompt_tool: true,
|
|
169
|
+
remote_control: true,
|
|
170
|
+
// Additive protocol-v1 stream fields (agent-sdk docs/PROTOCOL.md §1.2.1):
|
|
171
|
+
// every stream-json stdout line carries a monotonic `seq` and a
|
|
172
|
+
// run-scoped `trace_id`, and tool_use/tool_result lines carry a pairing
|
|
173
|
+
// `id` ("tu-<n>"). `trace_id` is caller-injectable (--trace-id /
|
|
174
|
+
// CC_TRACE_ID) for end-to-end IDE ↔ CLI correlation.
|
|
175
|
+
tool_use_id: true,
|
|
176
|
+
event_seq: true,
|
|
177
|
+
trace_id: true,
|
|
178
|
+
mcp: {
|
|
179
|
+
config_file: true,
|
|
180
|
+
registry: true,
|
|
181
|
+
tool_search: true,
|
|
182
|
+
oauth: true,
|
|
183
|
+
managed_allowlist: true,
|
|
184
|
+
},
|
|
185
|
+
sandbox_engines: ["docker", "bubblewrap"],
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
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
|
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE-context redaction — the implicit-context counterpart of the credential
|
|
3
|
+
* READ guard ([[credential-guard.js]]).
|
|
4
|
+
*
|
|
5
|
+
* The IDE bridge shares editor state (selection text, open-editor tabs,
|
|
6
|
+
* terminal output, diagnostics) with every prompt WITHOUT any tool-permission
|
|
7
|
+
* gate: nothing was "read" through read_file, so denyRead rules and the
|
|
8
|
+
* credential guard never fire. An open `.env` tab or an `export API_KEY=…`
|
|
9
|
+
* still visible in the integrated terminal would ride straight into model
|
|
10
|
+
* context. This module closes that side door:
|
|
11
|
+
*
|
|
12
|
+
* 1. `filterIdeFileEntry` — file-path entries (open editors, selection file,
|
|
13
|
+
* diagnostics files) that name a credential file are dropped, reusing the
|
|
14
|
+
* exact same `credentialFileReason` patterns the read guard enforces. The
|
|
15
|
+
* non-resolved variant is deliberate: these paths come from the IDE, may
|
|
16
|
+
* not exist locally, and a pure string check never touches the fs.
|
|
17
|
+
* 2. `redactSecretsInText` — free text (selection, terminal output,
|
|
18
|
+
* diagnostic messages) is scrubbed with a conservative set of secret
|
|
19
|
+
* shapes: AWS access-key ids, Bearer tokens, PEM private-key blocks,
|
|
20
|
+
* well-known token prefixes, and `SECRET_NAME = value` assignments (only
|
|
21
|
+
* the VALUE is swallowed — key names and ordinary code survive).
|
|
22
|
+
*
|
|
23
|
+
* Precision over recall, same posture as the guards: a redactor that mangles
|
|
24
|
+
* everyday code teaches users to turn it off. `CC_IDE_CONTEXT_REDACTION=0`
|
|
25
|
+
* disables (default ON).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
credentialFileReason,
|
|
30
|
+
_internals as credentialGuardInternals,
|
|
31
|
+
} from "./credential-guard.js";
|
|
32
|
+
|
|
33
|
+
const REDACTED = "[REDACTED]";
|
|
34
|
+
const REDACTED_FILE = "[redacted credential file]";
|
|
35
|
+
|
|
36
|
+
// Boundary-delimited secret words (KEY / TOKEN / SECRET / PASSWORD / …) —
|
|
37
|
+
// shared with the credential guard so both surfaces agree on what "looks
|
|
38
|
+
// secret".
|
|
39
|
+
const SECRET_VAR_RE = credentialGuardInternals.SECRET_VAR_RE;
|
|
40
|
+
|
|
41
|
+
// AWS access-key ids are globally unique, fixed-shape strings.
|
|
42
|
+
const AWS_ACCESS_KEY_RE = /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
|
|
43
|
+
|
|
44
|
+
// `Bearer <token>` in headers / curl lines / pasted requests.
|
|
45
|
+
const BEARER_RE = /\b(Bearer)\s+[A-Za-z0-9\-._~+/=]{8,}/g;
|
|
46
|
+
|
|
47
|
+
// PEM private-key blocks — the bytes ARE the secret. A block whose END marker
|
|
48
|
+
// was truncated off still redacts to the end of the text (fail toward safety).
|
|
49
|
+
const PEM_PRIVATE_KEY_RE =
|
|
50
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g;
|
|
51
|
+
|
|
52
|
+
// High-precision vendor token prefixes (OpenAI/Anthropic sk-, GitHub PATs,
|
|
53
|
+
// GitLab PATs, Slack xox*) — unambiguous even outside an assignment.
|
|
54
|
+
const TOKEN_PREFIX_RE =
|
|
55
|
+
/\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
56
|
+
|
|
57
|
+
// `API_KEY = "value"` / `token: value` / JSON `"secret": "value"` — the key
|
|
58
|
+
// must contain a boundary-delimited secret word, the value must be a bare
|
|
59
|
+
// token-like run (≥8 chars, no spaces). The trailing lookahead rejects values
|
|
60
|
+
// that continue into a call — `token = getToken()` is code, not a secret.
|
|
61
|
+
// Only the value is replaced; the key and separator are kept so the agent
|
|
62
|
+
// still sees WHAT was configured, just not the secret itself.
|
|
63
|
+
const ASSIGNMENT_RE =
|
|
64
|
+
/\b([A-Za-z_][A-Za-z0-9_-]*)(["']?\s*[=:]\s*)(["']?)([A-Za-z0-9+/=_.\-]{8,})\3(?![A-Za-z0-9+/=_.\-(])/g;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Env kill-switch: CC_IDE_CONTEXT_REDACTION=0|false|no|off disables all IDE
|
|
68
|
+
* context redaction. Default ON — only an explicit opt-out turns it off.
|
|
69
|
+
*/
|
|
70
|
+
export function ideRedactionEnabled(env = process.env) {
|
|
71
|
+
const raw = env && env.CC_IDE_CONTEXT_REDACTION;
|
|
72
|
+
return !(raw != null && /^(0|false|no|off)$/i.test(String(raw).trim()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gate one IDE-supplied file-path entry (open-editor tab, selection file,
|
|
77
|
+
* diagnostic file). Returns the path unchanged when it is fine to share, or
|
|
78
|
+
* null when it names a credential/secret file and the whole entry must be
|
|
79
|
+
* dropped (or replaced with the `REDACTED_FILE_PLACEHOLDER`).
|
|
80
|
+
*
|
|
81
|
+
* Pure string check by design (non-resolved `credentialFileReason`): IDE paths
|
|
82
|
+
* may be remote/virtual, and a context filter must never do fs I/O. `cwd` is
|
|
83
|
+
* accepted for signature parity with the resolved guards but unused here.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} filePath
|
|
86
|
+
* @param {object} [opts] { cwd?, env? }
|
|
87
|
+
* @returns {string|null}
|
|
88
|
+
*/
|
|
89
|
+
export function filterIdeFileEntry(filePath, opts = {}) {
|
|
90
|
+
const p = String(filePath || "");
|
|
91
|
+
if (!p) return filePath ?? null;
|
|
92
|
+
if (!ideRedactionEnabled(opts.env || process.env)) return filePath;
|
|
93
|
+
return credentialFileReason(p) ? null : filePath;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Scrub secret-shaped substrings out of IDE-supplied free text (selection
|
|
98
|
+
* text, terminal output, diagnostic messages). Non-strings pass through
|
|
99
|
+
* untouched; with the kill-switch off the text is returned as-is.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} text
|
|
102
|
+
* @param {object} [opts] { env? }
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
export function redactSecretsInText(text, opts = {}) {
|
|
106
|
+
if (typeof text !== "string" || text.length === 0) return text;
|
|
107
|
+
if (!ideRedactionEnabled(opts.env || process.env)) return text;
|
|
108
|
+
let out = text;
|
|
109
|
+
out = out.replace(PEM_PRIVATE_KEY_RE, REDACTED);
|
|
110
|
+
out = out.replace(BEARER_RE, (_m, word) => `${word} ${REDACTED}`);
|
|
111
|
+
out = out.replace(AWS_ACCESS_KEY_RE, REDACTED);
|
|
112
|
+
out = out.replace(TOKEN_PREFIX_RE, REDACTED);
|
|
113
|
+
out = out.replace(ASSIGNMENT_RE, (m, key, sep, quote, _value) => {
|
|
114
|
+
// Hyphens count as segment boundaries too (api-key ≙ api_key).
|
|
115
|
+
if (!SECRET_VAR_RE.test(key.replace(/-/g, "_"))) return m;
|
|
116
|
+
return `${key}${sep}${quote}${REDACTED}${quote}`;
|
|
117
|
+
});
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const REDACTED_MARKER = REDACTED;
|
|
122
|
+
export const REDACTED_FILE_PLACEHOLDER = REDACTED_FILE;
|
|
123
|
+
|
|
124
|
+
export const _internals = {
|
|
125
|
+
AWS_ACCESS_KEY_RE,
|
|
126
|
+
BEARER_RE,
|
|
127
|
+
PEM_PRIVATE_KEY_RE,
|
|
128
|
+
TOKEN_PREFIX_RE,
|
|
129
|
+
ASSIGNMENT_RE,
|
|
130
|
+
};
|
package/src/lib/ide-context.js
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { simpleDiff } from "./note-versioning.js";
|
|
21
|
+
import {
|
|
22
|
+
filterIdeFileEntry,
|
|
23
|
+
redactSecretsInText,
|
|
24
|
+
} from "./ide-context-redaction.js";
|
|
25
|
+
import { normalizeIdePathForCli, detectWsl } from "./remote-path-mapping.js";
|
|
21
26
|
|
|
22
27
|
/** Hard cap on the selected text we inline into the prompt. */
|
|
23
28
|
const SELECTION_TEXT_CAP = 2000;
|
|
@@ -86,6 +91,99 @@ export function parseToolResultJson(result) {
|
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
// ─── Redaction seams (see [[ide-context-redaction.js]]) ─────────────────────
|
|
95
|
+
// IDE context bypasses the tool-permission layer entirely (nothing goes
|
|
96
|
+
// through read_file), so credential files and secret-shaped text are filtered
|
|
97
|
+
// HERE, at the collection boundary, before anything reaches the prompt.
|
|
98
|
+
// CC_IDE_CONTEXT_REDACTION=0 disables (the filters below become pass-through).
|
|
99
|
+
//
|
|
100
|
+
// The same boundary also folds a remote file representation (a vscode-remote://
|
|
101
|
+
// URI, a \\wsl.localhost\… UNC, or a Windows drive path while cc runs in WSL)
|
|
102
|
+
// into the path THIS host can open (see [[remote-path-mapping.js]]) — otherwise
|
|
103
|
+
// the agent, reading the inlined path, would target a file cc can't see. The
|
|
104
|
+
// credential filter runs on the ORIGINAL value (basename is preserved by the
|
|
105
|
+
// map, so the verdict is unchanged). CC_IDE_PATH_MAP=0 disables it.
|
|
106
|
+
|
|
107
|
+
/** Env gate: CC_IDE_PATH_MAP=0|false|off disables remote path normalization. */
|
|
108
|
+
export function idePathMapEnabled(env = process.env) {
|
|
109
|
+
const v = String(env?.CC_IDE_PATH_MAP ?? "").toLowerCase();
|
|
110
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Fold one IDE-reported file value into a path cc can open on this host. */
|
|
114
|
+
function mapIdeFile(file, env) {
|
|
115
|
+
if (typeof file !== "string" || !idePathMapEnabled(env)) return file;
|
|
116
|
+
return normalizeIdePathForCli(file, {
|
|
117
|
+
platform: process.platform,
|
|
118
|
+
wsl: detectWsl(env || {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Drop the selection when it lives in a credential file; scrub its text. */
|
|
123
|
+
function sanitizeIdeSelection(sel, env) {
|
|
124
|
+
if (!sel) return sel;
|
|
125
|
+
if (sel.file && filterIdeFileEntry(sel.file, { env }) === null) return null;
|
|
126
|
+
let out = sel;
|
|
127
|
+
const mappedFile = mapIdeFile(sel.file, env);
|
|
128
|
+
if (mappedFile !== sel.file) out = { ...out, file: mappedFile };
|
|
129
|
+
if (typeof out.text === "string" && out.text.length > 0) {
|
|
130
|
+
const scrubbed = redactSecretsInText(out.text, { env });
|
|
131
|
+
if (scrubbed !== out.text) out = { ...out, text: scrubbed };
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Drop open-editor entries whose file is a credential file; map the rest. */
|
|
137
|
+
function sanitizeIdeEditors(editors, env) {
|
|
138
|
+
if (!Array.isArray(editors)) return editors;
|
|
139
|
+
return editors
|
|
140
|
+
.filter(
|
|
141
|
+
(e) => !(e && e.file && filterIdeFileEntry(e.file, { env }) === null),
|
|
142
|
+
)
|
|
143
|
+
.map((e) => {
|
|
144
|
+
if (!e || typeof e.file !== "string") return e;
|
|
145
|
+
const mapped = mapIdeFile(e.file, env);
|
|
146
|
+
return mapped === e.file ? e : { ...e, file: mapped };
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Scrub secret-shaped text out of terminal commands and their output. */
|
|
151
|
+
function sanitizeIdeTerminals(terms, env) {
|
|
152
|
+
if (!Array.isArray(terms)) return terms;
|
|
153
|
+
return terms.map((t) => {
|
|
154
|
+
if (!t || typeof t !== "object") return t;
|
|
155
|
+
const out = { ...t };
|
|
156
|
+
if (typeof out.command === "string") {
|
|
157
|
+
out.command = redactSecretsInText(out.command, { env });
|
|
158
|
+
}
|
|
159
|
+
if (typeof out.output === "string") {
|
|
160
|
+
out.output = redactSecretsInText(out.output, { env });
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Drop diagnostics for credential files; map their file + scrub message text. */
|
|
167
|
+
function sanitizeIdeDiagnostics(diags, env) {
|
|
168
|
+
if (!Array.isArray(diags)) return diags;
|
|
169
|
+
return diags
|
|
170
|
+
.filter(
|
|
171
|
+
(d) => !(d && d.file && filterIdeFileEntry(d.file, { env }) === null),
|
|
172
|
+
)
|
|
173
|
+
.map((d) => {
|
|
174
|
+
if (!d) return d;
|
|
175
|
+
let out = d;
|
|
176
|
+
if (typeof d.file === "string") {
|
|
177
|
+
const mapped = mapIdeFile(d.file, env);
|
|
178
|
+
if (mapped !== d.file) out = { ...out, file: mapped };
|
|
179
|
+
}
|
|
180
|
+
if (typeof out.message === "string") {
|
|
181
|
+
out = { ...out, message: redactSecretsInText(out.message, { env }) };
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
89
187
|
/** Resolve to the promise's value, or null after `ms` / on rejection. */
|
|
90
188
|
function withTimeout(promise, ms) {
|
|
91
189
|
return new Promise((resolve) => {
|
|
@@ -137,18 +235,22 @@ export async function collectIdeContext(mcp, opts = {}) {
|
|
|
137
235
|
? call("mcp__ide__getTerminalOutput", { limit: TERMINAL_LIMIT })
|
|
138
236
|
: Promise.resolve(null),
|
|
139
237
|
]);
|
|
140
|
-
const
|
|
141
|
-
const
|
|
238
|
+
const env = opts.env || process.env;
|
|
239
|
+
const sel = sanitizeIdeSelection(selection || null, env);
|
|
240
|
+
let openEditors = Array.isArray(editors?.editors) ? editors.editors : null;
|
|
241
|
+
if (openEditors) openEditors = sanitizeIdeEditors(openEditors, env);
|
|
242
|
+
let terminals = Array.isArray(terminalData?.terminals)
|
|
142
243
|
? terminalData.terminals
|
|
143
244
|
: null;
|
|
245
|
+
if (terminals) terminals = sanitizeIdeTerminals(terminals, env);
|
|
144
246
|
if (
|
|
145
|
-
!
|
|
247
|
+
!sel &&
|
|
146
248
|
!(openEditors && openEditors.length > 0) &&
|
|
147
249
|
!(terminals && terminals.length > 0)
|
|
148
250
|
) {
|
|
149
251
|
return null;
|
|
150
252
|
}
|
|
151
|
-
return { selection:
|
|
253
|
+
return { selection: sel, openEditors, terminals };
|
|
152
254
|
}
|
|
153
255
|
|
|
154
256
|
/**
|
|
@@ -304,8 +406,11 @@ export async function collectIdeDiagnostics(mcp, filePath, opts = {}) {
|
|
|
304
406
|
);
|
|
305
407
|
const all = Array.isArray(data?.diagnostics) ? data.diagnostics : null;
|
|
306
408
|
if (!all) return null;
|
|
307
|
-
const relevant =
|
|
308
|
-
|
|
409
|
+
const relevant = sanitizeIdeDiagnostics(
|
|
410
|
+
all.filter(
|
|
411
|
+
(d) => d && DIAG_SEVERITIES.has(String(d.severity).toLowerCase()),
|
|
412
|
+
),
|
|
413
|
+
env,
|
|
309
414
|
);
|
|
310
415
|
return relevant.length > 0 ? relevant : null;
|
|
311
416
|
}
|
|
@@ -653,6 +758,7 @@ export async function expandIdeMentions(text, mcp, opts = {}) {
|
|
|
653
758
|
const mentions = findIdeMentions(src);
|
|
654
759
|
if (mentions.length === 0) return out;
|
|
655
760
|
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
761
|
+
const env = opts.env || process.env;
|
|
656
762
|
const blocks = [];
|
|
657
763
|
for (const kind of mentions) {
|
|
658
764
|
try {
|
|
@@ -663,11 +769,9 @@ export async function expandIdeMentions(text, mcp, opts = {}) {
|
|
|
663
769
|
);
|
|
664
770
|
continue;
|
|
665
771
|
}
|
|
666
|
-
const sel =
|
|
667
|
-
mcp,
|
|
668
|
-
|
|
669
|
-
{},
|
|
670
|
-
timeoutMs,
|
|
772
|
+
const sel = sanitizeIdeSelection(
|
|
773
|
+
await callIdeToolJson(mcp, "mcp__ide__getSelection", {}, timeoutMs),
|
|
774
|
+
env,
|
|
671
775
|
);
|
|
672
776
|
const b = formatSelectionMention(sel);
|
|
673
777
|
if (b) {
|
|
@@ -689,7 +793,14 @@ export async function expandIdeMentions(text, mcp, opts = {}) {
|
|
|
689
793
|
{},
|
|
690
794
|
timeoutMs,
|
|
691
795
|
);
|
|
692
|
-
const
|
|
796
|
+
const safe =
|
|
797
|
+
data && Array.isArray(data.diagnostics)
|
|
798
|
+
? {
|
|
799
|
+
...data,
|
|
800
|
+
diagnostics: sanitizeIdeDiagnostics(data.diagnostics, env),
|
|
801
|
+
}
|
|
802
|
+
: data;
|
|
803
|
+
const b = formatDiagnosticsMention(safe);
|
|
693
804
|
if (b) {
|
|
694
805
|
blocks.push(b);
|
|
695
806
|
out.expanded.push("diagnostics");
|
|
@@ -709,7 +820,11 @@ export async function expandIdeMentions(text, mcp, opts = {}) {
|
|
|
709
820
|
{ limit: TERMINAL_MENTION_LIMIT },
|
|
710
821
|
timeoutMs,
|
|
711
822
|
);
|
|
712
|
-
const
|
|
823
|
+
const safe =
|
|
824
|
+
data && Array.isArray(data.terminals)
|
|
825
|
+
? { ...data, terminals: sanitizeIdeTerminals(data.terminals, env) }
|
|
826
|
+
: data;
|
|
827
|
+
const b = formatTerminalMention(safe);
|
|
713
828
|
if (b) {
|
|
714
829
|
blocks.push(b);
|
|
715
830
|
out.expanded.push("terminal");
|
|
@@ -38,24 +38,40 @@ export async function reconcileConfigLlmProvider(cfgLlm = {}) {
|
|
|
38
38
|
if (!cfgLlm || !cfgLlm.provider || !cfgLlm.baseUrl) {
|
|
39
39
|
return { llm: cfgLlm, changed: false };
|
|
40
40
|
}
|
|
41
|
-
const { inferProviderFromBaseUrl, modelForeignToProvider } =
|
|
42
|
-
"./runnable-provider.js"
|
|
43
|
-
);
|
|
41
|
+
const { inferProviderFromBaseUrl, modelForeignToProvider } =
|
|
42
|
+
await import("./runnable-provider.js");
|
|
44
43
|
const inferred = inferProviderFromBaseUrl(cfgLlm.baseUrl);
|
|
45
44
|
if (!inferred || inferred === cfgLlm.provider) {
|
|
46
45
|
return { llm: cfgLlm, changed: false };
|
|
47
46
|
}
|
|
48
47
|
const corrected = { ...cfgLlm, provider: inferred };
|
|
49
48
|
if (modelForeignToProvider(inferred, cfgLlm.model)) {
|
|
50
|
-
const { selectModelForTask, TaskType } =
|
|
51
|
-
"./task-model-selector.js"
|
|
52
|
-
);
|
|
49
|
+
const { selectModelForTask, TaskType } =
|
|
50
|
+
await import("./task-model-selector.js");
|
|
53
51
|
const def = selectModelForTask(inferred, TaskType.CHAT);
|
|
54
52
|
if (def) corrected.model = def;
|
|
55
53
|
}
|
|
56
54
|
return { llm: corrected, changed: true };
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
import { resolveApiKeyFromHelper } from "./api-key-helper.js";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Key backfill order: config llm.apiKey (plaintext, legacy) first, then
|
|
61
|
+
* llm.apiKeyHelper (external command → OS credential store). The helper only
|
|
62
|
+
* runs when no key is otherwise present, so configured setups never pay a
|
|
63
|
+
* subprocess spawn.
|
|
64
|
+
*/
|
|
65
|
+
function backfillApiKey(options, cfgLlm) {
|
|
66
|
+
if (options.apiKey) return;
|
|
67
|
+
if (cfgLlm.apiKey) {
|
|
68
|
+
options.apiKey = cfgLlm.apiKey;
|
|
69
|
+
} else if (cfgLlm.apiKeyHelper) {
|
|
70
|
+
const key = resolveApiKeyFromHelper(cfgLlm.apiKeyHelper);
|
|
71
|
+
if (key) options.apiKey = key;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
|
|
60
76
|
// The IDE chat panel pins --provider/--model (read from config) but DROPS
|
|
61
77
|
// --base-url/--api-key. With an explicit --provider the block below bails, so
|
|
@@ -71,7 +87,7 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
|
|
|
71
87
|
options.provider === cfgLlm.provider
|
|
72
88
|
) {
|
|
73
89
|
if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = cfgLlm.baseUrl;
|
|
74
|
-
|
|
90
|
+
backfillApiKey(options, cfgLlm);
|
|
75
91
|
if (!options.model && !opts.explicitModel && cfgLlm.model) {
|
|
76
92
|
options.model = cfgLlm.model;
|
|
77
93
|
}
|
|
@@ -85,6 +101,6 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
|
|
|
85
101
|
options.model = cfgLlm.model;
|
|
86
102
|
}
|
|
87
103
|
if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = cfgLlm.baseUrl;
|
|
88
|
-
|
|
104
|
+
backfillApiKey(options, cfgLlm);
|
|
89
105
|
return options;
|
|
90
106
|
}
|