agentshield/adapter/
mod.rs1pub 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
22pub(crate) const MAX_ADAPTER_FILE_SIZE_BYTES: u64 = 1_048_576;
24
25pub(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
34pub(crate) trait AnalysisAdapter: Send + Sync {
37 fn framework(&self) -> Framework;
39
40 fn detect(&self, root: &Path) -> bool;
42
43 fn load_analysis_with_filter(
45 &self,
46 root: &Path,
47 filter: &ScanPathFilter,
48 ) -> Result<Vec<AnalysisBundle>>;
49}
50
51pub trait Adapter: Send + Sync {
54 fn framework(&self) -> Framework;
56
57 fn detect(&self, root: &Path) -> bool;
59
60 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#[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
193pub 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}