/** * MCP Tool Composition Inspector — ADR-320 (this ADR; ruvnet/ruflo dream-cycle * proposal, arXiv:2606.27027 "ShareLock"). * * The attack: an adversary registers N seemingly-benign MCP tools whose * individual descriptions look fine in isolation, but whose CONCATENATION * (or pairwise overlap) forms an injection payload targeting a downstream * agent — a Shamir-secret-sharing-style split across tool descriptions. * Per-tool inspection misses this because each fragment sits under any * single-tool detection threshold. * * Relationship to prior art in this repo * --------------------------------------- * A CLI-only v1 of this idea already shipped directly to `main` * (dream-cycle #2783, commits 381b7ebcc/581cd2bf3) as * `v3/@claude-flow/cli/src/security/mcp-composition-inspector.ts` — an * on-demand `ruflo security composition-scan` command using exact * common-substring matching plus an injection-phrase catalog and * typosquat check. That module's own header explicitly scopes itself as * "a bounded engineering fix rather than an ADR-scope subsystem" and lists * "Future v2: SimHash + LSH for scale" as deliberately deferred. THIS * module is that v2: a reusable `@claude-flow/security` library primitive * (not a CLI-only tool) built around a real SimHash fingerprint, so it can * be called from a pre-task/pre-tool-use hook before a multi-tool chain * executes, per this ADR's implementation target. It does not replace or * import the CLI v1 — the two are independent, complementary detectors * (exact-substring vs. hashed-shingle-overlap) that a caller may run * together. * * Detection method (deterministic, no LLM call) * ----------------------------------------------- * 1. Each tool description is normalized and split into overlapping * word-shingles (default: 5-word windows). Shingling — rather than * whole-description comparison — is what lets us find a SMALL embedded * fragment inside an otherwise unrelated, benign description. * 2. Each tool gets a 64-bit SimHash fingerprint: every shingle is hashed * (FNV-1a, 64-bit) and the fingerprint bits are set by majority vote * across all of a tool's shingle hashes. Two tools whose descriptions * share enough content have a SMALL Hamming distance between * fingerprints — this is the O(1)-per-pair coarse filter that avoids an * LLM call and stays cheap even at hundreds of registered tools (fast * 64-bit XOR + popcount per pair). * 3. An inverted shingle-hash index (hash -> set of tool indices) is built * once, in O(total shingles). For each pair flagged by the SimHash * filter (or found in the index) the SAME index is used to compute the * actual overlapping shingle text — this is what makes the finding * reportable (the SimHash score alone can't tell an operator *what* * matched). * 4. A fragment's "population" (how many distinct tools carry it) caps * false positives: shared shingles that appear in more than * `maxFragmentPopulation` (default 3) tools are template language * (e.g. every ruflo `memory_*` tool's boilerplate), not an attack — * Shamir-split attacks concentrate a fragment in a small conspiracy of * 2-3 tools. Same population-cap idea as the CLI v1, reimplemented here * against the hash index instead of raw substrings. * * This is a heuristic detector, not a security boundary: it reports * SUSPECTS. Default action is warn + log; blocking is opt-in via * `CLAUDE_FLOW_MCP_COMPOSITION_BLOCK=1` (see {@link evaluateToolComposition}). * * @module @claude-flow/security/mcp-composition-inspector */ export interface McpToolDescriptor { name: string; description: string; } export interface CompositionInspectorOptions { /** Words per shingle window. Default 5. */ shingleSize?: number; /** SimHash similarity (1 - Hamming/64) at/above which a pair is a candidate. Default 0.6. */ simhashThreshold?: number; /** Fraction of a tool's shingles shared with a peer (low-population only) at/above which a pair is flagged. Default 0.25. */ fragmentOverlapThreshold?: number; /** Max number of distinct tools a shingle may appear in before it's treated as template language, not an attack fragment. Default 3. */ maxFragmentPopulation?: number; /** Minimum character length for a shingle to be indexed (filters trivial/common short phrases). Default 12. */ minShingleChars?: number; /** Max sample shared fragments recorded per finding (display only). Default 5. */ maxSampleFragments?: number; } export interface CompositionFinding { toolA: string; toolB: string; /** 1 - (Hamming distance / 64) between the two tools' SimHash fingerprints. */ simhashSimilarity: number; /** sharedLowPopulationShingles / min(shingleCount(A), shingleCount(B)). */ fragmentOverlapScore: number; /** Sample of the actual shared shingle text driving the finding (truncated). */ sharedFragments: string[]; /** Max population (distinct tools) observed among the shared shingles that triggered this finding. */ fragmentPopulation: number; reason: string; } export interface CompositionInspectionStats { toolsScanned: number; pairsCompared: number; shinglesIndexed: number; /** Pairs whose SimHash similarity alone crossed simhashThreshold (diagnostic — includes pairs later excluded for having no low-population shared shingle). */ candidatePairsFromSimhash: number; } export interface CompositionInspectionResult { findings: CompositionFinding[]; stats: CompositionInspectionStats; } export interface CompositionGuardResult { result: CompositionInspectionResult; action: 'none' | 'warn' | 'block'; blocked: boolean; message?: string; } /** FNV-1a, 64-bit. Deterministic, dependency-free, good avalanche for short strings. */ export declare function fnv1a64(input: string): bigint; /** * Classic SimHash: bit-vote a 64-bit fingerprint across a list of token * hashes. Near-duplicate token sets (even with some fragments differing) * produce fingerprints with a small Hamming distance. */ export declare function simhash64(shingles: readonly string[]): bigint; /** Hamming distance between two 64-bit fingerprints (0..64). */ export declare function hammingDistance64(a: bigint, b: bigint): number; export declare function inspectToolComposition(tools: readonly McpToolDescriptor[], options?: CompositionInspectorOptions): CompositionInspectionResult; /** Reads `CLAUDE_FLOW_MCP_COMPOSITION_BLOCK` fresh on every call. '1' or 'true' (case-insensitive) enables blocking; unset/anything else keeps the default warn+log posture. */ export declare function isCompositionBlockEnabled(): boolean; /** * Runs {@link inspectToolComposition} and applies the ADR-320 default * posture: warn + log (via `console.warn`) unless * `CLAUDE_FLOW_MCP_COMPOSITION_BLOCK=1`, in which case the chain is marked * `blocked: true` — the caller (the pre-task hook / MCP dispatcher) decides * what "blocked" means operationally; this function does not throw or abort * anything itself. */ export declare function evaluateToolComposition(tools: readonly McpToolDescriptor[], options?: CompositionInspectorOptions): CompositionGuardResult; //# sourceMappingURL=mcp-composition-inspector.d.ts.map