Skip to main content

agentshield/adapter/
mod.rs

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