Skip to main content

agentshield/adapter/
mod.rs

1pub mod autogen;
2pub mod crewai;
3pub mod cursor_rules;
4pub mod gpt_actions;
5pub mod hermes;
6pub mod langchain;
7pub mod llama_index;
8pub mod mcp;
9pub(super) mod mcp_metadata;
10pub mod openclaw;
11mod pipeline;
12pub mod semantic_kernel;
13pub mod vercel_ai;
14
15use std::path::Path;
16
17use crate::analysis::AnalysisBundle;
18use crate::config::ScanPathFilter;
19use crate::error::{Result, ShieldError};
20use crate::ir::{Framework, ScanTarget};
21
22/// Maximum file size (1 MB) that adapters will read into memory.
23pub(crate) const MAX_ADAPTER_FILE_SIZE_BYTES: u64 = 1_048_576;
24
25/// Safely read a text file up to 1 MB. Returns None if the file exceeds 1 MB or cannot be read.
26pub(crate) fn read_file_capped(path: &Path) -> Option<String> {
27    let metadata = std::fs::metadata(path).ok()?;
28    if metadata.len() > MAX_ADAPTER_FILE_SIZE_BYTES {
29        return None;
30    }
31    std::fs::read_to_string(path).ok()
32}
33
34/// Contextual analysis adapters load both public IR and non-serialized sidecar
35/// data needed by future context-dependent detectors.
36pub(crate) trait AnalysisAdapter: Send + Sync {
37    /// The framework this adapter handles.
38    fn framework(&self) -> Framework;
39
40    /// Check if this adapter can handle the given directory.
41    fn detect(&self, root: &Path) -> bool;
42
43    /// Load analysis bundles from the directory.
44    fn load_analysis_with_filter(
45        &self,
46        root: &Path,
47        filter: &ScanPathFilter,
48    ) -> Result<Vec<AnalysisBundle>>;
49}
50
51/// An adapter detects a specific agent framework and loads its artifacts
52/// into the unified IR.
53pub trait Adapter: Send + Sync {
54    /// The framework this adapter handles.
55    fn framework(&self) -> Framework;
56
57    /// Check if this adapter can handle the given directory.
58    fn detect(&self, root: &Path) -> bool;
59
60    /// Load artifacts from the directory into scan targets.
61    /// When `ignore_tests` is true, test files are excluded before parsing.
62    fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>>;
63
64    fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
65        self.load(root, filter.ignore_tests())
66    }
67}
68
69/// All registered adapters.
70#[derive(Copy, Clone)]
71enum AdapterSpec {
72    Mcp,
73    OpenClaw,
74    Hermes,
75    CrewAi,
76    LangChain,
77    GptActions,
78    CursorRules,
79    VercelAi,
80    AutoGen,
81    LlamaIndex,
82    SemanticKernel,
83}
84
85impl AdapterSpec {
86    fn public(self) -> Box<dyn Adapter> {
87        match self {
88            Self::Mcp => Box::new(mcp::McpAdapter),
89            Self::OpenClaw => Box::new(openclaw::OpenClawAdapter),
90            Self::Hermes => Box::new(hermes::HermesAgentAdapter),
91            Self::CrewAi => Box::new(crewai::CrewAiAdapter),
92            Self::LangChain => Box::new(langchain::LangChainAdapter),
93            Self::GptActions => Box::new(gpt_actions::GptActionsAdapter),
94            Self::CursorRules => Box::new(cursor_rules::CursorRulesAdapter),
95            Self::VercelAi => Box::new(vercel_ai::VercelAiAdapter),
96            Self::AutoGen => Box::new(autogen::AutoGenAdapter),
97            Self::LlamaIndex => Box::new(llama_index::LlamaIndexAdapter),
98            Self::SemanticKernel => Box::new(semantic_kernel::SemanticKernelAdapter),
99        }
100    }
101
102    fn analysis(self) -> Box<dyn AnalysisAdapter> {
103        match self {
104            Self::Mcp => Box::new(mcp::McpAnalysisAdapter),
105            Self::OpenClaw => Box::new(legacy_analysis_adapter(openclaw::OpenClawAdapter)),
106            Self::Hermes => Box::new(legacy_analysis_adapter(hermes::HermesAgentAdapter)),
107            Self::CrewAi => Box::new(legacy_analysis_adapter(crewai::CrewAiAdapter)),
108            Self::LangChain => Box::new(legacy_analysis_adapter(langchain::LangChainAdapter)),
109            Self::GptActions => Box::new(legacy_analysis_adapter(gpt_actions::GptActionsAdapter)),
110            Self::CursorRules => {
111                Box::new(legacy_analysis_adapter(cursor_rules::CursorRulesAdapter))
112            }
113            Self::VercelAi => Box::new(legacy_analysis_adapter(vercel_ai::VercelAiAdapter)),
114            Self::AutoGen => Box::new(legacy_analysis_adapter(autogen::AutoGenAdapter)),
115            Self::LlamaIndex => Box::new(legacy_analysis_adapter(llama_index::LlamaIndexAdapter)),
116            Self::SemanticKernel => Box::new(legacy_analysis_adapter(
117                semantic_kernel::SemanticKernelAdapter,
118            )),
119        }
120    }
121}
122
123fn analysis_adapters() -> &'static [AdapterSpec] {
124    use std::sync::OnceLock;
125
126    static ADAPTERS: OnceLock<[AdapterSpec; 11]> = OnceLock::new();
127    ADAPTERS.get_or_init(|| {
128        [
129            AdapterSpec::Mcp,
130            AdapterSpec::OpenClaw,
131            AdapterSpec::Hermes,
132            AdapterSpec::CrewAi,
133            AdapterSpec::LangChain,
134            AdapterSpec::GptActions,
135            AdapterSpec::CursorRules,
136            AdapterSpec::VercelAi,
137            AdapterSpec::AutoGen,
138            AdapterSpec::LlamaIndex,
139            AdapterSpec::SemanticKernel,
140        ]
141    })
142}
143
144#[derive(Debug)]
145struct LegacyAnalysisAdapter<A: Adapter>(A);
146
147impl<A: Adapter> AnalysisAdapter for LegacyAnalysisAdapter<A> {
148    fn framework(&self) -> Framework {
149        self.0.framework()
150    }
151
152    fn detect(&self, root: &Path) -> bool {
153        self.0.detect(root)
154    }
155
156    fn load_analysis_with_filter(
157        &self,
158        root: &Path,
159        filter: &ScanPathFilter,
160    ) -> Result<Vec<AnalysisBundle>> {
161        let targets = self.0.load_with_filter(root, filter)?;
162        Ok(targets
163            .into_iter()
164            .map(|target| {
165                let composite_flows = mcp::loader::compute_composite_flows_for_target(&target);
166                AnalysisBundle {
167                    target,
168                    composite_flows,
169                }
170            })
171            .collect())
172    }
173}
174
175fn legacy_analysis_adapter<A>(adapter: A) -> impl AnalysisAdapter
176where
177    A: Adapter + 'static + Send + Sync,
178{
179    LegacyAnalysisAdapter(adapter)
180}
181
182pub(crate) fn all_analysis_adapters() -> Vec<Box<dyn AnalysisAdapter>> {
183    analysis_adapters()
184        .iter()
185        .map(|spec| spec.analysis())
186        .collect()
187}
188
189pub(crate) fn all_adapters() -> Vec<Box<dyn Adapter>> {
190    analysis_adapters()
191        .iter()
192        .map(|spec| spec.public())
193        .collect()
194}
195
196/// Auto-detect all matching frameworks and load scan targets from each.
197///
198/// Repos may contain both MCP and OpenClaw artifacts — all matching
199/// adapters contribute targets rather than stopping at the first match.
200pub fn auto_detect_and_load(root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
201    let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
202    auto_detect_and_load_with_filter(root, &filter)
203}
204
205pub fn auto_detect_and_load_with_filter(
206    root: &Path,
207    filter: &ScanPathFilter,
208) -> Result<Vec<ScanTarget>> {
209    let adapters = all_adapters();
210    let mut all_targets = Vec::new();
211
212    for adapter in &adapters {
213        if adapter.detect(root) {
214            match adapter.load_with_filter(root, filter) {
215                Ok(targets) => all_targets.extend(targets),
216                Err(e) => {
217                    tracing::warn!(
218                        framework = %adapter.framework(),
219                        error = %e,
220                        "adapter failed to load, skipping"
221                    );
222                }
223            }
224        }
225    }
226
227    if all_targets.is_empty() {
228        return Err(ShieldError::NoAdapter(root.display().to_string()));
229    }
230
231    Ok(all_targets)
232}
233
234pub(crate) fn auto_detect_analysis_with_filter(
235    root: &Path,
236    filter: &ScanPathFilter,
237) -> Result<Vec<AnalysisBundle>> {
238    let adapters = all_analysis_adapters();
239    let mut all_bundles = Vec::new();
240
241    for adapter in &adapters {
242        if adapter.detect(root) {
243            match adapter.load_analysis_with_filter(root, filter) {
244                Ok(bundles) => all_bundles.extend(bundles),
245                Err(e) => {
246                    tracing::warn!(
247                        framework = %adapter.framework(),
248                        error = %e,
249                        "analysis adapter failed to load, skipping"
250                    );
251                }
252            }
253        }
254    }
255
256    if all_bundles.is_empty() {
257        return Err(ShieldError::NoAdapter(root.display().to_string()));
258    }
259
260    Ok(all_bundles)
261}
262
263#[cfg(test)]
264mod tests {
265    use super::all_adapters;
266    use super::all_analysis_adapters;
267
268    #[test]
269    fn adapter_registries_stay_in_sync() {
270        let public = all_adapters();
271        let analysis = all_analysis_adapters();
272        assert_eq!(public.len(), analysis.len());
273        assert!(
274            public
275                .iter()
276                .zip(analysis.iter())
277                .all(|(a, b)| a.framework() == b.framework())
278        );
279    }
280}