agentshield/adapter/
mod.rs1pub 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
18pub(crate) trait AnalysisAdapter: Send + Sync {
21 fn framework(&self) -> Framework;
23
24 fn detect(&self, root: &Path) -> bool;
26
27 fn load_analysis_with_filter(
29 &self,
30 root: &Path,
31 filter: &ScanPathFilter,
32 ) -> Result<Vec<AnalysisBundle>>;
33}
34
35pub trait Adapter: Send + Sync {
38 fn framework(&self) -> Framework;
40
41 fn detect(&self, root: &Path) -> bool;
43
44 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#[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
159pub 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}