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 => {
117                Box::new(legacy_analysis_adapter(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| AnalysisBundle {
165                target,
166                composite_flows: Vec::new(),
167            })
168            .collect())
169    }
170}
171
172fn legacy_analysis_adapter<A>(adapter: A) -> impl AnalysisAdapter
173where
174    A: Adapter + 'static + Send + Sync,
175{
176    LegacyAnalysisAdapter(adapter)
177}
178
179pub(crate) fn all_analysis_adapters() -> Vec<Box<dyn AnalysisAdapter>> {
180    analysis_adapters()
181        .iter()
182        .map(|spec| spec.analysis())
183        .collect()
184}
185
186pub(crate) fn all_adapters() -> Vec<Box<dyn Adapter>> {
187    analysis_adapters()
188        .iter()
189        .map(|spec| spec.public())
190        .collect()
191}
192
193/// Auto-detect all matching frameworks and load scan targets from each.
194///
195/// Repos may contain both MCP and OpenClaw artifacts — all matching
196/// adapters contribute targets rather than stopping at the first match.
197pub fn auto_detect_and_load(root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
198    let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
199    auto_detect_and_load_with_filter(root, &filter)
200}
201
202pub fn auto_detect_and_load_with_filter(
203    root: &Path,
204    filter: &ScanPathFilter,
205) -> Result<Vec<ScanTarget>> {
206    let adapters = all_adapters();
207    let mut all_targets = Vec::new();
208
209    for adapter in &adapters {
210        if adapter.detect(root) {
211            match adapter.load_with_filter(root, filter) {
212                Ok(targets) => all_targets.extend(targets),
213                Err(e) => {
214                    tracing::warn!(
215                        framework = %adapter.framework(),
216                        error = %e,
217                        "adapter failed to load, skipping"
218                    );
219                }
220            }
221        }
222    }
223
224    if all_targets.is_empty() {
225        return Err(ShieldError::NoAdapter(root.display().to_string()));
226    }
227
228    Ok(all_targets)
229}
230
231pub(crate) fn auto_detect_analysis_with_filter(
232    root: &Path,
233    filter: &ScanPathFilter,
234) -> Result<Vec<AnalysisBundle>> {
235    let adapters = all_analysis_adapters();
236    let mut all_bundles = Vec::new();
237
238    for adapter in &adapters {
239        if adapter.detect(root) {
240            match adapter.load_analysis_with_filter(root, filter) {
241                Ok(bundles) => all_bundles.extend(bundles),
242                Err(e) => {
243                    tracing::warn!(
244                        framework = %adapter.framework(),
245                        error = %e,
246                        "analysis adapter failed to load, skipping"
247                    );
248                }
249            }
250        }
251    }
252
253    if all_bundles.is_empty() {
254        return Err(ShieldError::NoAdapter(root.display().to_string()));
255    }
256
257    Ok(all_bundles)
258}
259
260#[cfg(test)]
261mod tests {
262    use super::all_adapters;
263    use super::all_analysis_adapters;
264
265    #[test]
266    fn adapter_registries_stay_in_sync() {
267        let public = all_adapters();
268        let analysis = all_analysis_adapters();
269        assert_eq!(public.len(), analysis.len());
270        assert!(
271            public
272                .iter()
273                .zip(analysis.iter())
274                .all(|(a, b)| a.framework() == b.framework())
275        );
276    }
277}