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
|
@@ -44,6 +44,83 @@ export const PROJECT_FILE_NAMES = ["cc.md", "CLAUDE.md", "AGENTS.md"];
|
|
|
44
44
|
/** Local (gitignored) companion names, first match wins. */
|
|
45
45
|
export const LOCAL_FILE_NAMES = ["cc.local.md", "CLAUDE.local.md"];
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* `instructionExcludes` — a large-monorepo reduction lever (P1). Subtrees whose
|
|
49
|
+
* instruction / rule / @import files should NEVER load (legacy, vendored,
|
|
50
|
+
* generated). Opt-in: empty by default so behavior is byte-identical unless the
|
|
51
|
+
* caller (config `instructionExcludes`) sets it. Patterns are matched against a
|
|
52
|
+
* path RELATIVE to the project root, with forward slashes:
|
|
53
|
+
* - a bare name (`node_modules`, `dist`) matches that segment ANYWHERE;
|
|
54
|
+
* - a slashed prefix (`packages/legacy`) matches that dir and everything under;
|
|
55
|
+
* - a glob (`**/generated/**`, `vendor/*`) matches via `*`/`**`/`?`.
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeInstructionExcludes(list) {
|
|
58
|
+
if (!Array.isArray(list)) return [];
|
|
59
|
+
return [
|
|
60
|
+
...new Set(
|
|
61
|
+
list
|
|
62
|
+
.map((s) =>
|
|
63
|
+
String(s || "")
|
|
64
|
+
.trim()
|
|
65
|
+
.replace(/\\/g, "/")
|
|
66
|
+
.replace(/\/+$/, ""),
|
|
67
|
+
)
|
|
68
|
+
.filter(Boolean),
|
|
69
|
+
),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function globToRegExp(glob) {
|
|
74
|
+
let re = "";
|
|
75
|
+
for (let i = 0; i < glob.length; i++) {
|
|
76
|
+
const c = glob[i];
|
|
77
|
+
if (c === "*") {
|
|
78
|
+
if (glob[i + 1] === "*") {
|
|
79
|
+
re += ".*";
|
|
80
|
+
i++;
|
|
81
|
+
if (glob[i + 1] === "/") i++; // `**/` also swallows the slash
|
|
82
|
+
} else {
|
|
83
|
+
re += "[^/]*";
|
|
84
|
+
}
|
|
85
|
+
} else if (c === "?") {
|
|
86
|
+
re += "[^/]";
|
|
87
|
+
} else {
|
|
88
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new RegExp("^" + re + "$");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* True when a repo-relative path falls under an excluded subtree. A file is
|
|
96
|
+
* excluded when it, or any of its ancestor directories, matches a pattern.
|
|
97
|
+
*/
|
|
98
|
+
export function pathIsExcluded(relPath, excludes) {
|
|
99
|
+
if (!excludes || excludes.length === 0) return false;
|
|
100
|
+
const rel = String(relPath || "")
|
|
101
|
+
.replace(/\\/g, "/")
|
|
102
|
+
.replace(/^\.\/?/, "")
|
|
103
|
+
.replace(/\/+$/, "");
|
|
104
|
+
if (!rel) return false;
|
|
105
|
+
const segs = rel.split("/");
|
|
106
|
+
for (const pat of excludes) {
|
|
107
|
+
if (/[*?[\]]/.test(pat)) {
|
|
108
|
+
const re = globToRegExp(pat);
|
|
109
|
+
if (re.test(rel)) return true;
|
|
110
|
+
let acc = "";
|
|
111
|
+
for (const s of segs) {
|
|
112
|
+
acc = acc ? `${acc}/${s}` : s;
|
|
113
|
+
if (re.test(acc)) return true;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
if (rel === pat || rel.startsWith(`${pat}/`)) return true;
|
|
117
|
+
// a bare (slash-free) name matches that path segment anywhere
|
|
118
|
+
if (!pat.includes("/") && segs.includes(pat)) return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
47
124
|
// Same boundary rule as file-ref-expander: `@` at start / after whitespace or
|
|
48
125
|
// an opening bracket-quote, so emails and decorative @ never match.
|
|
49
126
|
const IMPORT_TOKEN_RE = /(^|[\s("'`[{])@([^\s"'`)\]}]+)/g;
|
|
@@ -100,13 +177,21 @@ export function findInstructionFiles(opts = {}) {
|
|
|
100
177
|
const { fs, path, os } = resolveDeps(opts);
|
|
101
178
|
const cwd = path.resolve(opts.cwd || process.cwd());
|
|
102
179
|
const home = opts.home || os.homedir() || "";
|
|
180
|
+
const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
|
|
103
181
|
|
|
104
182
|
const seen = new Set();
|
|
105
183
|
const out = [];
|
|
184
|
+
const root = findProjectRoot(cwd, opts) || cwd;
|
|
185
|
+
// A discovered file under an excluded subtree (relative to the project root)
|
|
186
|
+
// never loads. The user-scope file lives outside the repo, so it is exempt.
|
|
187
|
+
const excluded = (abs) =>
|
|
188
|
+
excludes.length > 0 &&
|
|
189
|
+
pathIsExcluded(path.relative(root, path.resolve(abs)), excludes);
|
|
106
190
|
const push = (p, scope) => {
|
|
107
191
|
if (!p) return;
|
|
108
192
|
const abs = path.resolve(p);
|
|
109
193
|
if (seen.has(abs) || !isFile(fs, abs)) return;
|
|
194
|
+
if (scope !== "user" && excluded(abs)) return;
|
|
110
195
|
seen.add(abs);
|
|
111
196
|
out.push({ path: abs, scope });
|
|
112
197
|
};
|
|
@@ -121,7 +206,6 @@ export function findInstructionFiles(opts = {}) {
|
|
|
121
206
|
);
|
|
122
207
|
}
|
|
123
208
|
|
|
124
|
-
const root = findProjectRoot(cwd, opts) || cwd;
|
|
125
209
|
const chain = [];
|
|
126
210
|
let dir = cwd;
|
|
127
211
|
for (;;) {
|
|
@@ -274,6 +358,10 @@ export function loadProjectInstructions(opts = {}) {
|
|
|
274
358
|
: DEFAULT_MAX_TOTAL_BYTES;
|
|
275
359
|
|
|
276
360
|
const roots = findInstructionFiles(opts);
|
|
361
|
+
const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
|
|
362
|
+
const projectRoot =
|
|
363
|
+
findProjectRoot(path.resolve(opts.cwd || process.cwd()), opts) ||
|
|
364
|
+
path.resolve(opts.cwd || process.cwd());
|
|
277
365
|
const visited = new Set(roots.map((r) => r.path));
|
|
278
366
|
const out = [];
|
|
279
367
|
const warnings = [];
|
|
@@ -322,6 +410,15 @@ export function loadProjectInstructions(opts = {}) {
|
|
|
322
410
|
const resolved = path.resolve(baseDir, target);
|
|
323
411
|
if (visited.has(resolved) || !isFile(fs, resolved)) continue; // silent:
|
|
324
412
|
// non-files are decorative @tokens (npm scopes, emails), not imports.
|
|
413
|
+
// An @import that resolves under an excluded subtree does not load — the
|
|
414
|
+
// reduction lever must not be bypassed by an import pointing into it.
|
|
415
|
+
if (
|
|
416
|
+
excludes.length > 0 &&
|
|
417
|
+
pathIsExcluded(path.relative(projectRoot, resolved), excludes)
|
|
418
|
+
) {
|
|
419
|
+
visited.add(resolved);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
325
422
|
// Refuse to import a credential / secret file. `cc agent` auto-loads
|
|
326
423
|
// project memory, so a cloned/untrusted repo's cc.md must NOT be able to
|
|
327
424
|
// pull `@~/.ssh/id_rsa` / `@../.env` into the LLM-bound system prompt
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project `.mcp.json` fingerprint trust (gap-analysis 2026-07-11 P1 "MCP 生命
|
|
3
|
+
* 周期" — 配置指纹变化后重新信任).
|
|
4
|
+
*
|
|
5
|
+
* `--project-mcp` opts a run into a checked-in `.mcp.json`, which can spawn
|
|
6
|
+
* arbitrary commands. That consent is for the file AS THE USER SAW IT — a
|
|
7
|
+
* later commit (or a compromised dependency's postinstall) editing the file
|
|
8
|
+
* must not ride the standing opt-in. So the first successful load records a
|
|
9
|
+
* sha256 fingerprint per absolute file path; a later load whose content no
|
|
10
|
+
* longer matches is REFUSED (fail-closed) until the user re-trusts via
|
|
11
|
+
* `CC_PROJECT_MCP_TRUST=1` (one-shot) or `cc mcp trust-project`.
|
|
12
|
+
*
|
|
13
|
+
* Store: ~/.chainlesschain/trusted-project-mcp.json { [absPath]: {fingerprint, trustedAt} }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { getHomeDir } from "./paths.js";
|
|
20
|
+
|
|
21
|
+
export const _deps = { fs };
|
|
22
|
+
|
|
23
|
+
function storePath(opts = {}) {
|
|
24
|
+
return (
|
|
25
|
+
opts.storePath ||
|
|
26
|
+
process.env.CC_PROJECT_MCP_TRUST_STORE ||
|
|
27
|
+
path.join(getHomeDir(), "trusted-project-mcp.json")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function projectMcpFingerprint(content) {
|
|
32
|
+
return createHash("sha256").update(String(content), "utf-8").digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readStore(opts) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = _deps.fs.readFileSync(storePath(opts), "utf-8");
|
|
38
|
+
const data = JSON.parse(raw);
|
|
39
|
+
return data && typeof data === "object" ? data : {};
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check a project .mcp.json against the trust store.
|
|
47
|
+
* @returns {{status:"first-use"|"trusted"|"changed", fingerprint:string}}
|
|
48
|
+
*/
|
|
49
|
+
export function checkProjectMcpTrust(file, content, opts = {}) {
|
|
50
|
+
const fingerprint = projectMcpFingerprint(content);
|
|
51
|
+
const record = readStore(opts)[path.resolve(file)];
|
|
52
|
+
if (!record || !record.fingerprint) {
|
|
53
|
+
return { status: "first-use", fingerprint };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
status: record.fingerprint === fingerprint ? "trusted" : "changed",
|
|
57
|
+
fingerprint,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Record (or re-record) the trusted fingerprint for a file. */
|
|
62
|
+
export function recordProjectMcpTrust(file, content, opts = {}) {
|
|
63
|
+
const target = storePath(opts);
|
|
64
|
+
const store = readStore(opts);
|
|
65
|
+
store[path.resolve(file)] = {
|
|
66
|
+
fingerprint: projectMcpFingerprint(content),
|
|
67
|
+
trustedAt: new Date(
|
|
68
|
+
typeof opts.now === "number" ? opts.now : Date.now(),
|
|
69
|
+
).toISOString(),
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
_deps.fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
73
|
+
_deps.fs.writeFileSync(target, JSON.stringify(store, null, 2), {
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
mode: 0o600,
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false; // best-effort — a failed record just re-prompts next run
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Truthy CC_PROJECT_MCP_TRUST → the user explicitly re-trusts this run. */
|
|
84
|
+
export function projectMcpRetrustRequested(env = process.env) {
|
|
85
|
+
const raw = String(env.CC_PROJECT_MCP_TRUST || "").toLowerCase();
|
|
86
|
+
return raw === "1" || raw === "true";
|
|
87
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote URI ↔ filesystem path mapping for the IDE bridge.
|
|
3
|
+
*
|
|
4
|
+
* A local IDE (VS Code / JetBrains) can report a file in a representation that
|
|
5
|
+
* does not exist on the host `cc` runs on, so inlining it into the prompt makes
|
|
6
|
+
* the agent's read_file/edit_file target a path the CLI host can't see:
|
|
7
|
+
* - a URI: file:///c:/code/my%20proj/文件.ts
|
|
8
|
+
* vscode-remote://wsl+Ubuntu/home/u/x
|
|
9
|
+
* vscode-remote://ssh-remote+host/home/u/x
|
|
10
|
+
* vscode-remote://dev-container+<hex>/workspaces/x
|
|
11
|
+
* - a WSL UNC path from a Windows host: \\wsl.localhost\Ubuntu\home\u\x
|
|
12
|
+
* (older Windows builds: \\wsl$\Ubuntu\home\u\x)
|
|
13
|
+
* - a Windows drive path while cc runs INSIDE WSL: C:\code\x == /mnt/c/code/x
|
|
14
|
+
*
|
|
15
|
+
* normalizeIdePathForCli() folds these foreign forms into the CLI's native fs
|
|
16
|
+
* path; anything already native (or unrecognized) passes through byte-for-byte.
|
|
17
|
+
* cliPathToIdeUri() is the reverse, for surfaces that must hand a URI back to
|
|
18
|
+
* the editor.
|
|
19
|
+
*
|
|
20
|
+
* Pure: string transforms only (no fs / process). Percent-decoding preserves
|
|
21
|
+
* spaces and non-ASCII (中文) exactly, so round-trips are lossless.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** decodeURIComponent that never throws on a malformed % sequence. */
|
|
25
|
+
function decodeSafe(s) {
|
|
26
|
+
if (typeof s !== "string") return s;
|
|
27
|
+
try {
|
|
28
|
+
return decodeURIComponent(s);
|
|
29
|
+
} catch {
|
|
30
|
+
return s; // leave a malformed sequence as-is rather than lose the value
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Has a `scheme://` prefix (a URI, not a bare path). */
|
|
35
|
+
export function looksLikeUri(value) {
|
|
36
|
+
return typeof value === "string" && /^[a-z][a-z0-9.+-]*:\/\//i.test(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `C:\…` or `C:/…` — a Windows drive-absolute path. */
|
|
40
|
+
export function looksLikeWindowsPath(value) {
|
|
41
|
+
return typeof value === "string" && /^[a-zA-Z]:[\\/]/.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `\\host\…` or `//host/…` — a UNC path. */
|
|
45
|
+
function looksLikeUnc(value) {
|
|
46
|
+
return typeof value === "string" && /^(\\\\|\/\/)[^\\/]/.test(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const WSL_UNC_RE =
|
|
50
|
+
/^(?:\\\\|\/\/)(?:wsl\.localhost|wsl\$)[\\/]([^\\/]+)[\\/]?(.*)$/i;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `\\wsl.localhost\Ubuntu\home\u\x` (or `\\wsl$\…`, forward-slash variants) →
|
|
54
|
+
* `{ distro:"Ubuntu", posix:"/home/u/x" }`, or null when not a WSL UNC path.
|
|
55
|
+
*/
|
|
56
|
+
export function wslUncToPosix(value) {
|
|
57
|
+
if (typeof value !== "string") return null;
|
|
58
|
+
const m = value.match(WSL_UNC_RE);
|
|
59
|
+
if (!m) return null;
|
|
60
|
+
const distro = m[1];
|
|
61
|
+
const rest = (m[2] || "").replace(/\\/g, "/");
|
|
62
|
+
return { distro, posix: "/" + rest.replace(/^\/+/, "") };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `/home/u/x` + distro → `\\wsl.localhost\Ubuntu\home\u\x` (host overridable). */
|
|
66
|
+
export function posixToWslUnc(posix, distro, { host = "wsl.localhost" } = {}) {
|
|
67
|
+
if (typeof posix !== "string" || !distro) return null;
|
|
68
|
+
const win = posix.replace(/^\/+/, "").replace(/\//g, "\\");
|
|
69
|
+
return `\\\\${host}\\${distro}\\${win}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `C:\code\x` → `/mnt/c/code/x`, or null when not a drive path. */
|
|
73
|
+
export function winDriveToWslMount(value) {
|
|
74
|
+
if (!looksLikeWindowsPath(value)) return null;
|
|
75
|
+
const drive = value[0].toLowerCase();
|
|
76
|
+
const rest = value.slice(2).replace(/\\/g, "/").replace(/^\/+/, "");
|
|
77
|
+
return `/mnt/${drive}${rest ? "/" + rest : ""}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const WSL_MOUNT_RE = /^\/mnt\/([a-zA-Z])(?:\/(.*))?$/;
|
|
81
|
+
|
|
82
|
+
/** `/mnt/c/code/x` → `C:\code\x`, or null when not a /mnt/<drive> path. */
|
|
83
|
+
export function wslMountToWinDrive(value) {
|
|
84
|
+
if (typeof value !== "string") return null;
|
|
85
|
+
const m = value.match(WSL_MOUNT_RE);
|
|
86
|
+
if (!m) return null;
|
|
87
|
+
const drive = m[1].toUpperCase();
|
|
88
|
+
const rest = (m[2] || "").replace(/\//g, "\\");
|
|
89
|
+
return `${drive}:\\${rest}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse an IDE URI into its parts. Returns null for a non-URI or an unknown
|
|
94
|
+
* scheme (so the caller leaves it untouched).
|
|
95
|
+
* { scheme, remoteKind, remoteParam, fsPath }
|
|
96
|
+
* `fsPath` is the decoded on-host path: a Windows drive path for a `file://`
|
|
97
|
+
* drive URI, else POSIX. `remoteKind`/`remoteParam` are set for vscode-remote.
|
|
98
|
+
*/
|
|
99
|
+
export function parseRemoteUri(uri) {
|
|
100
|
+
if (!looksLikeUri(uri)) return null;
|
|
101
|
+
const m = uri.match(/^([a-z][a-z0-9.+-]*):\/\/([^/]*)((?:\/[^?#]*)?)/i);
|
|
102
|
+
if (!m) return null;
|
|
103
|
+
const scheme = m[1].toLowerCase();
|
|
104
|
+
const authority = m[2] || "";
|
|
105
|
+
const rawPath = m[3] || "";
|
|
106
|
+
|
|
107
|
+
if (scheme === "file") {
|
|
108
|
+
if (authority) {
|
|
109
|
+
// file://server/share/… → UNC \\server\share\…
|
|
110
|
+
const unc = "\\\\" + authority + rawPath.replace(/\//g, "\\");
|
|
111
|
+
return {
|
|
112
|
+
scheme,
|
|
113
|
+
remoteKind: null,
|
|
114
|
+
remoteParam: null,
|
|
115
|
+
fsPath: decodeSafe(unc),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const decoded = decodeSafe(rawPath);
|
|
119
|
+
// /c:/code/x → c:\code\x (Windows drive URI); else a POSIX path.
|
|
120
|
+
if (/^\/[a-zA-Z]:/.test(decoded)) {
|
|
121
|
+
return {
|
|
122
|
+
scheme,
|
|
123
|
+
remoteKind: null,
|
|
124
|
+
remoteParam: null,
|
|
125
|
+
fsPath: decoded.slice(1).replace(/\//g, "\\"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
scheme,
|
|
130
|
+
remoteKind: null,
|
|
131
|
+
remoteParam: null,
|
|
132
|
+
fsPath: decoded || "/",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (scheme === "vscode-remote" || scheme === "vscode-vfs") {
|
|
137
|
+
// authority = "<kind>+<param>", where `+` may be percent-encoded (%2B).
|
|
138
|
+
const auth = decodeSafe(authority);
|
|
139
|
+
const plus = auth.indexOf("+");
|
|
140
|
+
const remoteKind = plus >= 0 ? auth.slice(0, plus) : auth;
|
|
141
|
+
const remoteParam = plus >= 0 ? auth.slice(plus + 1) : null;
|
|
142
|
+
return {
|
|
143
|
+
scheme,
|
|
144
|
+
remoteKind: remoteKind || null,
|
|
145
|
+
remoteParam: remoteParam || null,
|
|
146
|
+
fsPath: decodeSafe(rawPath) || "/",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null; // unknown scheme — don't guess
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Detect the running CLI's WSL context from env (best-effort). Returns
|
|
155
|
+
* `{ isWsl, distro }`. Injected in tests; ide-context passes process.env.
|
|
156
|
+
*/
|
|
157
|
+
export function detectWsl(env = {}) {
|
|
158
|
+
const distro = env.WSL_DISTRO_NAME || null;
|
|
159
|
+
const isWsl = Boolean(distro || env.WSL_INTEROP);
|
|
160
|
+
return { isWsl, distro };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Fold an IDE-reported file value into the path `cc` can actually open on THIS
|
|
165
|
+
* host. Only recognized foreign forms are rewritten; a native or unrecognized
|
|
166
|
+
* value is returned unchanged (byte-for-byte).
|
|
167
|
+
*
|
|
168
|
+
* @param {string} value URI or path as the IDE reported it
|
|
169
|
+
* @param {object} [opts]
|
|
170
|
+
* @param {string} [opts.platform=process.platform] the CLI host platform
|
|
171
|
+
* @param {{isWsl?:boolean, distro?:string}} [opts.wsl] WSL context (see detectWsl)
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
export function normalizeIdePathForCli(value, opts = {}) {
|
|
175
|
+
if (typeof value !== "string" || value.length === 0) return value;
|
|
176
|
+
const platform = opts.platform || process.platform;
|
|
177
|
+
const wsl = opts.wsl || {};
|
|
178
|
+
const onWindows = platform === "win32";
|
|
179
|
+
|
|
180
|
+
// 1) Resolve a URI down to its on-host fsPath first.
|
|
181
|
+
let base = value;
|
|
182
|
+
let remoteKind = null;
|
|
183
|
+
let remoteParam = null;
|
|
184
|
+
if (looksLikeUri(value)) {
|
|
185
|
+
const parsed = parseRemoteUri(value);
|
|
186
|
+
if (!parsed) return value; // unknown scheme → leave untouched
|
|
187
|
+
base = parsed.fsPath;
|
|
188
|
+
remoteKind = parsed.remoteKind;
|
|
189
|
+
remoteParam = parsed.remoteParam;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (onWindows) {
|
|
193
|
+
// Native form is a Windows path. A POSIX path from a wsl-remote URI is
|
|
194
|
+
// reachable from Windows as a \\wsl.localhost\<distro>\… UNC.
|
|
195
|
+
if (remoteKind === "wsl" && remoteParam && base.startsWith("/")) {
|
|
196
|
+
return posixToWslUnc(base, remoteParam) || base;
|
|
197
|
+
}
|
|
198
|
+
return base;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Native form is POSIX (Linux remote-host or inside WSL).
|
|
202
|
+
const wslUnc = wslUncToPosix(base);
|
|
203
|
+
if (wslUnc) return wslUnc.posix;
|
|
204
|
+
if (looksLikeWindowsPath(base) && wsl.isWsl) {
|
|
205
|
+
return winDriveToWslMount(base) || base;
|
|
206
|
+
}
|
|
207
|
+
return base;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Reverse: a CLI-native fs path → a URI the editor can open. `remoteAuthority`
|
|
212
|
+
* (e.g. "wsl+Ubuntu", "ssh-remote+host") produces a vscode-remote URI; without
|
|
213
|
+
* it, a `file://` URI. Percent-encodes spaces / non-ASCII per component.
|
|
214
|
+
*/
|
|
215
|
+
export function cliPathToIdeUri(fsPath, { remoteAuthority } = {}) {
|
|
216
|
+
if (typeof fsPath !== "string" || fsPath.length === 0) return fsPath;
|
|
217
|
+
const isWinDrive = looksLikeWindowsPath(fsPath);
|
|
218
|
+
const posixish = fsPath.replace(/\\/g, "/");
|
|
219
|
+
const encoded = posixish
|
|
220
|
+
.split("/")
|
|
221
|
+
.map((seg) => (/^[a-zA-Z]:$/.test(seg) ? seg : encodeURIComponent(seg)))
|
|
222
|
+
.join("/");
|
|
223
|
+
if (remoteAuthority) {
|
|
224
|
+
const path = encoded.startsWith("/") ? encoded : "/" + encoded;
|
|
225
|
+
return `vscode-remote://${remoteAuthority}${path}`;
|
|
226
|
+
}
|
|
227
|
+
const path = isWinDrive ? "/" + encoded : encoded;
|
|
228
|
+
return `file://${path.startsWith("/") ? "" : "/"}${path}`;
|
|
229
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scheduling planner for the agent Scheduler / Monitor (P1 §"统一持久
|
|
3
|
+
* Scheduler、Monitor 和 Channel").
|
|
4
|
+
*
|
|
5
|
+
* The persisted store ([[agent-schedule-store.js]]) already guarantees the
|
|
6
|
+
* catch-up invariant — a cron down for N periods fires ONCE, not N times,
|
|
7
|
+
* because `advanceCron` jumps to `nextCronTime(now)` rather than stepping
|
|
8
|
+
* through every miss. This module adds the pieces a unified daemon event
|
|
9
|
+
* runtime still needs, all as PURE, clock-injected functions (no timers, no
|
|
10
|
+
* RNG, deterministic across restarts):
|
|
11
|
+
*
|
|
12
|
+
* - **deterministic jitter** — spread tasks that share a cron minute (e.g.
|
|
13
|
+
* every `0 * * * *`) by a STABLE per-task offset, so a shared tick is not a
|
|
14
|
+
* thundering herd. The offset is a hash of the task id, so it never moves
|
|
15
|
+
* across a restart (an RNG offset would, defeating the point).
|
|
16
|
+
* - **adaptive next-wakeup** — the earliest future fire time, so a daemon
|
|
17
|
+
* sleeps until then instead of polling.
|
|
18
|
+
* - **expiry** — a task past `expiresAt` retires without a final fire.
|
|
19
|
+
*
|
|
20
|
+
* A daemon uses `partitionSchedule` (retire expired, fire due) then
|
|
21
|
+
* `msUntilNextWakeup` (sleep precisely). `jitterMs = 0` (the default) makes the
|
|
22
|
+
* due set byte-identical to the store's existing `due()`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// FNV-1a → a stable 32-bit hash. Deterministic and dependency-free, so the same
|
|
26
|
+
// task always draws the same jitter offset (across processes and restarts).
|
|
27
|
+
function hash32(str) {
|
|
28
|
+
let h = 0x811c9dc5;
|
|
29
|
+
const s = String(str ?? "");
|
|
30
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
31
|
+
h ^= s.charCodeAt(i);
|
|
32
|
+
h = Math.imul(h, 0x01000193);
|
|
33
|
+
}
|
|
34
|
+
return h >>> 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A stable jitter offset in `[0, jitterMs)` derived from the task id. Same id +
|
|
39
|
+
* same jitterMs → same offset, every time. `jitterMs <= 0` → no jitter (0).
|
|
40
|
+
*/
|
|
41
|
+
export function jitterOffsetMs(id, jitterMs) {
|
|
42
|
+
const j = Math.floor(Number(jitterMs) || 0);
|
|
43
|
+
if (j <= 0) return 0;
|
|
44
|
+
return hash32(id) % j;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The entry's base fire time: `dueAt` for a wakeup, `nextAt` otherwise. */
|
|
48
|
+
export function baseFireAt(entry) {
|
|
49
|
+
if (!entry) return null;
|
|
50
|
+
const raw = entry.kind === "wakeup" ? entry.dueAt : entry.nextAt;
|
|
51
|
+
const n = Number(raw);
|
|
52
|
+
return Number.isFinite(n) ? n : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Base fire time with the entry's deterministic jitter applied. */
|
|
56
|
+
export function effectiveFireAt(entry, jitterMs = 0) {
|
|
57
|
+
const base = baseFireAt(entry);
|
|
58
|
+
if (base == null) return null;
|
|
59
|
+
return base + jitterOffsetMs(entry?.id, jitterMs);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Is an entry still pending/active (not in a terminal state)? */
|
|
63
|
+
export function isSchedulable(entry) {
|
|
64
|
+
if (!entry) return false;
|
|
65
|
+
if (entry.kind === "wakeup") return entry.status === "pending";
|
|
66
|
+
return entry.status === "active";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Expiry: a task with an `expiresAt` at or before `now` is retired regardless
|
|
71
|
+
* of its schedule. A missing / non-positive `expiresAt` never expires.
|
|
72
|
+
*/
|
|
73
|
+
export function isEntryExpired(entry, now) {
|
|
74
|
+
const exp = Number(entry?.expiresAt);
|
|
75
|
+
return Number.isFinite(exp) && exp > 0 && exp <= Number(now);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Partition schedulable entries at `now` into what a daemon should do:
|
|
80
|
+
* - `expired` — retire (status → expired). Checked BEFORE due so an expired
|
|
81
|
+
* task never gets a final fire.
|
|
82
|
+
* - `due` — fire now (jittered fire time has arrived).
|
|
83
|
+
* - `waiting` — still pending.
|
|
84
|
+
* Terminal entries are skipped entirely.
|
|
85
|
+
*/
|
|
86
|
+
export function partitionSchedule(entries, { now, jitterMs = 0 } = {}) {
|
|
87
|
+
const t = Number(now);
|
|
88
|
+
const expired = [];
|
|
89
|
+
const due = [];
|
|
90
|
+
const waiting = [];
|
|
91
|
+
for (const entry of entries || []) {
|
|
92
|
+
if (!isSchedulable(entry)) continue;
|
|
93
|
+
if (isEntryExpired(entry, t)) {
|
|
94
|
+
expired.push(entry);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const fireAt = effectiveFireAt(entry, jitterMs);
|
|
98
|
+
if (fireAt != null && fireAt <= t) due.push(entry);
|
|
99
|
+
else waiting.push(entry);
|
|
100
|
+
}
|
|
101
|
+
return { expired, due, waiting };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The earliest future jittered fire time among schedulable, non-expired
|
|
106
|
+
* entries — what a daemon sleeps until (adaptive wakeup). An already-due entry
|
|
107
|
+
* contributes `now` (fire immediately). Returns null when nothing is scheduled.
|
|
108
|
+
*/
|
|
109
|
+
export function nextWakeupAt(entries, { now, jitterMs = 0 } = {}) {
|
|
110
|
+
const t = Number(now);
|
|
111
|
+
let min = null;
|
|
112
|
+
for (const entry of entries || []) {
|
|
113
|
+
if (!isSchedulable(entry) || isEntryExpired(entry, t)) continue;
|
|
114
|
+
const fireAt = effectiveFireAt(entry, jitterMs);
|
|
115
|
+
if (fireAt == null) continue;
|
|
116
|
+
const at = fireAt <= t ? t : fireAt;
|
|
117
|
+
if (min == null || at < min) min = at;
|
|
118
|
+
}
|
|
119
|
+
return min;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Milliseconds until the next wakeup, clamped to `[0, maxMs]` (a cap keeps a
|
|
124
|
+
* far-future task from parking the daemon forever — it re-checks by `maxMs`).
|
|
125
|
+
* null when nothing is scheduled.
|
|
126
|
+
*/
|
|
127
|
+
export function msUntilNextWakeup(
|
|
128
|
+
entries,
|
|
129
|
+
{ now, jitterMs = 0, maxMs = null } = {},
|
|
130
|
+
) {
|
|
131
|
+
const at = nextWakeupAt(entries, { now, jitterMs });
|
|
132
|
+
if (at == null) return null;
|
|
133
|
+
let ms = Math.max(0, at - Number(now));
|
|
134
|
+
if (maxMs != null && Number.isFinite(maxMs)) ms = Math.min(ms, Number(maxMs));
|
|
135
|
+
return ms;
|
|
136
|
+
}
|
|
@@ -66,3 +66,54 @@ export function sensitiveFileReason(targetPath) {
|
|
|
66
66
|
}
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Auto-exec CONFIG write guard — the CLI-side twin of the IDE extension's
|
|
72
|
+
* `classifyAutoExecTarget` (vscode-extension/src/auto-exec-guard.js, kept as
|
|
73
|
+
* an independent pure function: no cross-package import). These files are not
|
|
74
|
+
* shells or hooks, but writing them still plants code that RUNS without an
|
|
75
|
+
* explicit user action: VS Code tasks auto-run on folder open, launch configs
|
|
76
|
+
* fire on debug, `.mcp.json` spawns arbitrary server processes the agent then
|
|
77
|
+
* auto-loads, JetBrains run configurations execute on the next Run, and
|
|
78
|
+
* devcontainer postCreate/postStart commands run on container open. Same
|
|
79
|
+
* conservative name-based posture as `sensitiveFileReason`; an unknown path is
|
|
80
|
+
* simply not risky (null), never a false positive.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} targetPath path as the tool received it (rel or abs)
|
|
83
|
+
* @returns {string|null} human reason when auto-exec-risky, null otherwise
|
|
84
|
+
*/
|
|
85
|
+
export function autoExecConfigReason(targetPath) {
|
|
86
|
+
const p = String(targetPath || "");
|
|
87
|
+
if (!p) return null;
|
|
88
|
+
const norm = p.replace(/\\/g, "/").toLowerCase();
|
|
89
|
+
const base = norm.split("/").pop() || "";
|
|
90
|
+
if (!base) return null;
|
|
91
|
+
if (base === ".mcp.json" || base === "mcp.json") {
|
|
92
|
+
return `auto-exec config: MCP server config (${base}) — can spawn arbitrary server processes the agent auto-loads`;
|
|
93
|
+
}
|
|
94
|
+
if (/(^|\/)\.vscode\/tasks\.json$/.test(norm)) {
|
|
95
|
+
return "auto-exec config: VS Code tasks (.vscode/tasks.json) — tasks can auto-run on folder open";
|
|
96
|
+
}
|
|
97
|
+
if (/(^|\/)\.vscode\/launch\.json$/.test(norm)) {
|
|
98
|
+
return "auto-exec config: VS Code launch/debug config (.vscode/launch.json) — runs commands on the next debug session";
|
|
99
|
+
}
|
|
100
|
+
if (/(^|\/)\.vscode\/settings\.json$/.test(norm)) {
|
|
101
|
+
return "auto-exec config: VS Code workspace settings (.vscode/settings.json) — can rebind shells/tools the user then runs";
|
|
102
|
+
}
|
|
103
|
+
if (/(^|\/)\.idea\/runconfigurations\//.test(norm)) {
|
|
104
|
+
return `auto-exec config: JetBrains run configuration (${base}) — executes on the user's next Run action`;
|
|
105
|
+
}
|
|
106
|
+
if (/(^|\/)\.idea\/workspace\.xml$/.test(norm)) {
|
|
107
|
+
return "auto-exec config: JetBrains workspace.xml — can embed run/startup tasks";
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
base === "devcontainer.json" ||
|
|
111
|
+
/(^|\/)\.devcontainer\/devcontainer\.json$/.test(norm)
|
|
112
|
+
) {
|
|
113
|
+
return "auto-exec config: devcontainer.json — postCreate/postStart commands run on container open";
|
|
114
|
+
}
|
|
115
|
+
if (base.endsWith(".code-workspace")) {
|
|
116
|
+
return `auto-exec config: VS Code workspace file (${base}) — can carry auto-run tasks and settings`;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
@@ -253,12 +253,16 @@ export class SubAgentContext {
|
|
|
253
253
|
const artifacts = [];
|
|
254
254
|
let lastContent = "";
|
|
255
255
|
|
|
256
|
-
// Build filtered tool list
|
|
257
|
-
const tools = this._getFilteredTools();
|
|
258
|
-
|
|
259
256
|
// Merge LLM options — pass shared iteration budget if available
|
|
260
257
|
const options = {
|
|
261
258
|
...this._llmOptions,
|
|
259
|
+
// Tool allowlist (fixed 2026-07-11): the filtered list used to be
|
|
260
|
+
// computed via _getFilteredTools() but never handed to agentLoop, so a
|
|
261
|
+
// sub-agent's `tools`/frontmatter allowlist was silently ignored at the
|
|
262
|
+
// LLM surface. agentLoop consumes NAMES via enabledToolNames.
|
|
263
|
+
...(Array.isArray(this.allowedTools) && this.allowedTools.length > 0
|
|
264
|
+
? { enabledToolNames: this.allowedTools }
|
|
265
|
+
: {}),
|
|
262
266
|
contextEngine: this.contextEngine,
|
|
263
267
|
cwd: this.cwd,
|
|
264
268
|
// Nesting level: lets a nested spawn_sub_agent see — and cap — its depth.
|