chainlesschain 0.162.160 → 0.162.162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
- package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
- package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
- package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
- package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
- package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
- package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
- package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
- package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
- package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +16 -2
- package/src/commands/review.js +6 -1
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/agents.js +19 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +322 -15
- 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/docs-drift.js +166 -0
- package/src/lib/doctor-checkup.js +76 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +13 -0
- package/src/lib/hook-event-bus.cjs +0 -0
- package/src/lib/ide-context.js +53 -14
- package/src/lib/json-schema-output.js +30 -1
- package/src/lib/json-schema-validate.js +691 -0
- package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
- package/src/lib/lsp/diagnostics-scheduler.js +127 -0
- package/src/lib/operation-fingerprint.js +95 -0
- package/src/lib/plugin-runtime/capabilities.js +371 -0
- package/src/lib/plugin-runtime/manifest.js +30 -0
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/remote-approval-bridge.js +16 -0
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/review-pipeline.js +227 -0
- package/src/lib/runtime-checkup.js +318 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/lib/session-lifecycle.js +305 -0
- package/src/lib/settings-hook-events.cjs +39 -13
- package/src/lib/subagent-contract.js +383 -0
- package/src/lib/telemetry-ids.js +190 -0
- package/src/lib/turn-binding.js +397 -0
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +24 -10
- package/src/runtime/headless-stream.js +195 -11
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
import fs from "fs";
|
|
28
28
|
import path from "path";
|
|
29
29
|
import semver from "semver";
|
|
30
|
+
import {
|
|
31
|
+
normalizeCapabilities,
|
|
32
|
+
normalizeOptionsSchema,
|
|
33
|
+
auditDeclaredCapabilities,
|
|
34
|
+
} from "./capabilities.js";
|
|
30
35
|
|
|
31
36
|
export const _deps = {
|
|
32
37
|
existsSync: fs.existsSync,
|
|
@@ -73,6 +78,14 @@ export function parsePluginManifest(root) {
|
|
|
73
78
|
scope: null,
|
|
74
79
|
metadata: {},
|
|
75
80
|
components: emptyComponents(),
|
|
81
|
+
// Declarative security contract (P1): the plugin's declared capabilities
|
|
82
|
+
// (manifest.permissions) + typed config schema (manifest.optionsSchema),
|
|
83
|
+
// normalized by capabilities.js. `capabilitiesDeclared` distinguishes a
|
|
84
|
+
// plugin that declared a permissions block (subject to the declared-vs-
|
|
85
|
+
// actual audit) from a legacy one that declared nothing (unrestricted).
|
|
86
|
+
capabilities: normalizeCapabilities(null),
|
|
87
|
+
capabilitiesDeclared: false,
|
|
88
|
+
optionsSchema: {},
|
|
76
89
|
errors,
|
|
77
90
|
warnings,
|
|
78
91
|
};
|
|
@@ -165,6 +178,23 @@ export function parsePluginManifest(root) {
|
|
|
165
178
|
c.bin = resolveBin(abs, manifest, safeResolve, warnings);
|
|
166
179
|
c.settings = resolveSettings(abs, manifest, safeResolve);
|
|
167
180
|
|
|
181
|
+
// ── declared capabilities + options schema (P1) ──
|
|
182
|
+
const hasPermissions =
|
|
183
|
+
manifest.permissions &&
|
|
184
|
+
typeof manifest.permissions === "object" &&
|
|
185
|
+
!Array.isArray(manifest.permissions);
|
|
186
|
+
result.capabilities = normalizeCapabilities(manifest.permissions);
|
|
187
|
+
result.capabilitiesDeclared = Boolean(hasPermissions);
|
|
188
|
+
result.optionsSchema = normalizeOptionsSchema(manifest.optionsSchema);
|
|
189
|
+
// Declared-vs-actual audit runs ONLY when a permissions block is present — a
|
|
190
|
+
// legacy plugin that declares nothing is unrestricted by design, so it never
|
|
191
|
+
// gets these warnings (keeps existing manifests warning-free).
|
|
192
|
+
if (hasPermissions) {
|
|
193
|
+
for (const f of auditDeclaredCapabilities(result)) {
|
|
194
|
+
warnings.push(`capability: ${f.reason}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
168
198
|
result.ok = errors.length === 0;
|
|
169
199
|
return result;
|
|
170
200
|
}
|
|
@@ -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
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
|
|
24
24
|
import { randomBytes } from "crypto";
|
|
25
25
|
import { WsRpcClient } from "./ws-rpc-client.js";
|
|
26
|
+
import {
|
|
27
|
+
approvalFingerprintOk,
|
|
28
|
+
operationFingerprint,
|
|
29
|
+
} from "./operation-fingerprint.js";
|
|
26
30
|
import {
|
|
27
31
|
buildDirectPairingUri,
|
|
28
32
|
pickLanAddress,
|
|
@@ -113,6 +117,15 @@ export class RemoteApprovalBridge {
|
|
|
113
117
|
const requestId = message.event.requestId || message.event.approvalId;
|
|
114
118
|
const pending = requestId ? this._pending.get(requestId) : null;
|
|
115
119
|
if (!pending) return;
|
|
120
|
+
// Confused-deputy guard: a resolve that carries an operation fingerprint
|
|
121
|
+
// must match the operation this ask published. A mismatch (stale card /
|
|
122
|
+
// replayed / swapped body) is rejected — the ask stays pending and fails
|
|
123
|
+
// closed on timeout. Absent fingerprint = legacy device, accepted.
|
|
124
|
+
if (
|
|
125
|
+
!approvalFingerprintOk(pending.fingerprint, message.event.fingerprint)
|
|
126
|
+
) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
116
129
|
this._pending.delete(requestId);
|
|
117
130
|
clearTimeout(pending.timer);
|
|
118
131
|
const answer = message.event.answer ?? message.event.approved;
|
|
@@ -150,6 +163,7 @@ export class RemoteApprovalBridge {
|
|
|
150
163
|
onRequestId = null,
|
|
151
164
|
} = {}) {
|
|
152
165
|
const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
|
|
166
|
+
const fingerprint = operationFingerprint({ tool, action, detail });
|
|
153
167
|
if (onRequestId) {
|
|
154
168
|
try {
|
|
155
169
|
onRequestId(requestId);
|
|
@@ -171,6 +185,7 @@ export class RemoteApprovalBridge {
|
|
|
171
185
|
if (typeof timer.unref === "function") timer.unref();
|
|
172
186
|
this._pending.set(requestId, {
|
|
173
187
|
timer,
|
|
188
|
+
fingerprint,
|
|
174
189
|
resolve: (decision) => {
|
|
175
190
|
this._publish({
|
|
176
191
|
type: "permission.resolved",
|
|
@@ -187,6 +202,7 @@ export class RemoteApprovalBridge {
|
|
|
187
202
|
tool: tool || null,
|
|
188
203
|
action,
|
|
189
204
|
detail,
|
|
205
|
+
fingerprint,
|
|
190
206
|
askedAt: this._now(),
|
|
191
207
|
});
|
|
192
208
|
});
|
|
@@ -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,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-agent code-review pipeline core (P2 "多 Agent Code Review").
|
|
3
|
+
*
|
|
4
|
+
* `cc review` today is a single read-only agent pass. The gap upgrades it to:
|
|
5
|
+
* 1. multiple finders (correctness / security / performance / tests),
|
|
6
|
+
* 2. a verifier that independently reproduces each finding,
|
|
7
|
+
* 3. a deduper that merges duplicates and confidence-filters,
|
|
8
|
+
* 4. structured output (path / line / category / severity / failure_scenario /
|
|
9
|
+
* evidence).
|
|
10
|
+
*
|
|
11
|
+
* This module is the PURE aggregation core — normalize, dedup+merge, apply
|
|
12
|
+
* verifier verdicts, confidence-filter, rank, and render the structured report.
|
|
13
|
+
* The fan-out of finder/verifier AGENTS is orchestration the command layer adds;
|
|
14
|
+
* everything here is deterministic and testable without an LLM.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The finder dimensions a full review fans out across. */
|
|
18
|
+
export const REVIEW_DIMENSIONS = Object.freeze([
|
|
19
|
+
"correctness",
|
|
20
|
+
"security",
|
|
21
|
+
"performance",
|
|
22
|
+
"tests",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const SEVERITY_RANK = Object.freeze({
|
|
26
|
+
critical: 5,
|
|
27
|
+
high: 4,
|
|
28
|
+
medium: 3,
|
|
29
|
+
low: 2,
|
|
30
|
+
note: 1,
|
|
31
|
+
});
|
|
32
|
+
const SEVERITY_LABEL = Object.freeze({
|
|
33
|
+
5: "Critical",
|
|
34
|
+
4: "High",
|
|
35
|
+
3: "Medium",
|
|
36
|
+
2: "Low",
|
|
37
|
+
1: "Note",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Canonical severity label (unknown → "Note"). */
|
|
41
|
+
export function normalizeSeverity(sev) {
|
|
42
|
+
const r = SEVERITY_RANK[String(sev || "").toLowerCase()] || 1;
|
|
43
|
+
return SEVERITY_LABEL[r];
|
|
44
|
+
}
|
|
45
|
+
export function severityRank(sev) {
|
|
46
|
+
return SEVERITY_RANK[String(sev || "").toLowerCase()] || 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Clamp a confidence to [0,1]; non-numeric → `dflt`. */
|
|
50
|
+
export function clampConfidence(v, dflt = 0.5) {
|
|
51
|
+
const n = Number(v);
|
|
52
|
+
if (!Number.isFinite(n)) return dflt;
|
|
53
|
+
return Math.max(0, Math.min(1, n));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function confOf(f) {
|
|
57
|
+
const n = Number(f?.confidence);
|
|
58
|
+
return Number.isFinite(n) ? n : 0.5;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normTitle(title) {
|
|
62
|
+
return String(title || "")
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/\s+/g, " ")
|
|
65
|
+
.trim()
|
|
66
|
+
.slice(0, 80);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Dedup identity: same file location + same issue (title). Category-agnostic so
|
|
70
|
+
* two finders reporting one bug under different categories merge. */
|
|
71
|
+
export function findingKey(f) {
|
|
72
|
+
return `${f?.path || ""}:${f?.line ?? ""}:${normTitle(f?.title)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unionField(group, keys) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const f of group) {
|
|
79
|
+
for (const key of keys) {
|
|
80
|
+
const v = f?.[key];
|
|
81
|
+
const list = Array.isArray(v) ? v : v != null ? [v] : [];
|
|
82
|
+
for (const item of list) {
|
|
83
|
+
const s = String(item);
|
|
84
|
+
if (s && !seen.has(s)) {
|
|
85
|
+
seen.add(s);
|
|
86
|
+
out.push(s);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function maxSeverityLabel(group) {
|
|
95
|
+
let best = 0;
|
|
96
|
+
for (const f of group) best = Math.max(best, severityRank(f.severity));
|
|
97
|
+
return SEVERITY_LABEL[best] || "Note";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Merge findings that describe the SAME issue at the SAME location. The
|
|
102
|
+
* representative keeps the highest severity + highest confidence; categories /
|
|
103
|
+
* dimensions / evidence are unioned WHEN PRESENT (so a plain
|
|
104
|
+
* `{path,line,severity,title}` finding round-trips unchanged — the wiring into
|
|
105
|
+
* `parseFindings` relies on this). Shape-preserving for inputs without those
|
|
106
|
+
* fields.
|
|
107
|
+
*/
|
|
108
|
+
export function dedupeFindings(findings) {
|
|
109
|
+
const groups = new Map();
|
|
110
|
+
const order = [];
|
|
111
|
+
for (const f of findings || []) {
|
|
112
|
+
if (!f) continue;
|
|
113
|
+
const key = findingKey(f);
|
|
114
|
+
if (!groups.has(key)) {
|
|
115
|
+
groups.set(key, []);
|
|
116
|
+
order.push(key);
|
|
117
|
+
}
|
|
118
|
+
groups.get(key).push(f);
|
|
119
|
+
}
|
|
120
|
+
return order.map((key) => {
|
|
121
|
+
const group = groups.get(key);
|
|
122
|
+
const winner = [...group].sort(
|
|
123
|
+
(a, b) =>
|
|
124
|
+
severityRank(b.severity) - severityRank(a.severity) ||
|
|
125
|
+
confOf(b) - confOf(a),
|
|
126
|
+
)[0];
|
|
127
|
+
const merged = { ...winner, severity: maxSeverityLabel(group) };
|
|
128
|
+
if (Number.isFinite(Number(winner.confidence))) {
|
|
129
|
+
merged.confidence = Math.max(...group.map(confOf));
|
|
130
|
+
}
|
|
131
|
+
const cats = unionField(group, ["category", "categories"]);
|
|
132
|
+
if (cats.length) merged.categories = cats;
|
|
133
|
+
const dims = unionField(group, ["dimension", "dimensions"]);
|
|
134
|
+
if (dims.length) merged.dimensions = dims;
|
|
135
|
+
if (group.length > 1) merged.mergedCount = group.length;
|
|
136
|
+
return merged;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Apply verifier verdicts. `verdicts` maps a findingKey → { verified, confidence?,
|
|
142
|
+
* note? }. A finding the verifier could NOT reproduce (`verified === false`) is
|
|
143
|
+
* DROPPED; a reproduced one is kept, its confidence updated and a `verified`
|
|
144
|
+
* flag set. Findings with no verdict pass through as unverified.
|
|
145
|
+
*/
|
|
146
|
+
export function applyVerdicts(findings, verdicts = {}) {
|
|
147
|
+
const map =
|
|
148
|
+
verdicts instanceof Map
|
|
149
|
+
? verdicts
|
|
150
|
+
: new Map(Object.entries(verdicts || {}));
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const f of findings || []) {
|
|
153
|
+
if (!f) continue;
|
|
154
|
+
const v = map.get(findingKey(f));
|
|
155
|
+
if (!v) {
|
|
156
|
+
out.push(f);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (v.verified === false) continue; // refuted → drop
|
|
160
|
+
out.push({
|
|
161
|
+
...f,
|
|
162
|
+
verified: true,
|
|
163
|
+
confidence:
|
|
164
|
+
v.confidence != null ? clampConfidence(v.confidence) : confOf(f),
|
|
165
|
+
...(v.note ? { verifierNote: String(v.note) } : {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Keep findings whose confidence is at least `minConfidence`. */
|
|
172
|
+
export function filterByConfidence(findings, { minConfidence = 0 } = {}) {
|
|
173
|
+
return (findings || []).filter((f) => f && confOf(f) >= minConfidence);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Sort by severity (desc) then confidence (desc) then path/line for stability. */
|
|
177
|
+
export function rankFindings(findings) {
|
|
178
|
+
return [...(findings || [])].sort(
|
|
179
|
+
(a, b) =>
|
|
180
|
+
severityRank(b.severity) - severityRank(a.severity) ||
|
|
181
|
+
confOf(b) - confOf(a) ||
|
|
182
|
+
String(a.path || "").localeCompare(String(b.path || "")) ||
|
|
183
|
+
(Number(a.line) || 0) - (Number(b.line) || 0),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toOutputFinding(f) {
|
|
188
|
+
return {
|
|
189
|
+
path: String(f.path || ""),
|
|
190
|
+
line: Number.isFinite(Number(f.line)) ? Math.floor(Number(f.line)) : null,
|
|
191
|
+
category: f.category || (f.categories && f.categories[0]) || "correctness",
|
|
192
|
+
severity: normalizeSeverity(f.severity),
|
|
193
|
+
failure_scenario: String(f.failureScenario || f.failure_scenario || ""),
|
|
194
|
+
evidence: String(f.evidence || ""),
|
|
195
|
+
confidence: clampConfidence(f.confidence),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The full pipeline: dedup → apply verdicts → confidence-filter → rank → render
|
|
201
|
+
* the structured report (the gap's path/line/category/severity/failure_scenario/
|
|
202
|
+
* evidence output) plus severity/category rollups.
|
|
203
|
+
*
|
|
204
|
+
* @param {Array} rawFindings findings from all finders
|
|
205
|
+
* @param {object} [opts] { verdicts, minConfidence }
|
|
206
|
+
*/
|
|
207
|
+
export function buildReviewReport(
|
|
208
|
+
rawFindings,
|
|
209
|
+
{ verdicts = {}, minConfidence = 0 } = {},
|
|
210
|
+
) {
|
|
211
|
+
const deduped = dedupeFindings(rawFindings);
|
|
212
|
+
const verified = applyVerdicts(deduped, verdicts);
|
|
213
|
+
const filtered = filterByConfidence(verified, { minConfidence });
|
|
214
|
+
const ranked = rankFindings(filtered);
|
|
215
|
+
const findings = ranked.map(toOutputFinding);
|
|
216
|
+
|
|
217
|
+
const bySeverity = { Critical: 0, High: 0, Medium: 0, Low: 0, Note: 0 };
|
|
218
|
+
const byCategory = {};
|
|
219
|
+
for (const f of findings) {
|
|
220
|
+
bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
221
|
+
byCategory[f.category] = (byCategory[f.category] || 0) + 1;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
findings,
|
|
225
|
+
summary: { total: findings.length, bySeverity, byCategory },
|
|
226
|
+
};
|
|
227
|
+
}
|