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 => 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
196pub 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}