agentshield/adapter/
autogen.rs1use std::path::Path;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use crate::config::ScanPathFilter;
7use crate::error::Result;
8use crate::ir::capability::{project_declared_description, project_declared_permissions};
9use crate::ir::taint_builder::build_data_surface;
10use crate::ir::*;
11
12static AUTOGEN_REGISTER_FN_RE: Lazy<Regex> = Lazy::new(|| {
13 Regex::new(r#"(?s)(?:register_function|@user_proxy\.register_for_execution|@assistant\.register_for_llm)\s*\(\s*(?:(?:f=)?(\w+))?(?:[^\)]*?description\s*=\s*["'`]([^"'`]+)["'`])?"#)
14 .expect("static regex pattern is valid")
15});
16
17static AUTOGEN_DEF_DOC_RE: Lazy<Regex> = Lazy::new(|| {
18 Regex::new(r#"(?m)^\s*(?:async\s+)?def\s+(\w+)\s*\([^)]*\)\s*(?:->[^:]+)?:\s*\n\s*["'`]([\s\S]*?)["'`]"#)
19 .expect("static regex pattern is valid")
20});
21
22pub struct AutoGenAdapter;
28
29impl super::Adapter for AutoGenAdapter {
30 fn framework(&self) -> Framework {
31 Framework::AutoGen
32 }
33
34 fn detect(&self, root: &Path) -> bool {
35 let pyproject = root.join("pyproject.toml");
36 if pyproject.exists() {
37 if let Some(content) = super::read_file_capped(&pyproject) {
38 if content.contains("autogen") || content.contains("pyautogen") {
39 return true;
40 }
41 }
42 }
43
44 let requirements = root.join("requirements.txt");
45 if requirements.exists() {
46 if let Some(content) = super::read_file_capped(&requirements) {
47 if content.lines().any(|l| {
48 let trimmed = l.trim();
49 trimmed.starts_with("autogen")
50 || trimmed.starts_with("pyautogen")
51 || trimmed.starts_with("autogen-agentchat")
52 || trimmed.starts_with("autogen-ext")
53 }) {
54 return true;
55 }
56 }
57 }
58
59 if super::mcp::has_recursive_python_import(
60 root,
61 &[
62 "from autogen",
63 "import autogen",
64 "from autogen.agentchat",
65 "from autogen_agentchat",
66 ],
67 ) {
68 return true;
69 }
70
71 false
72 }
73
74 fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
75 let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
76 self.load_with_filter(root, &filter)
77 }
78
79 fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
80 let name = root
81 .file_name()
82 .map(|n| n.to_string_lossy().to_string())
83 .unwrap_or_else(|| "autogen-project".into());
84
85 let mut source_files = Vec::new();
86 super::mcp::collect_source_files_with_filter(root, filter, &mut source_files)?;
87
88 source_files.retain(|sf| sf.language == Language::Python);
89
90 let mut tools = Vec::new();
91 for sf in &source_files {
92 tools.extend(extract_autogen_tools_from_source(&sf.path, &sf.content));
93 }
94
95 for tool in &mut tools {
96 project_declared_permissions(tool);
97 project_declared_description(tool);
98 }
99
100 let execution = super::pipeline::build_execution_surface(&source_files);
101 let data = build_data_surface(&tools, &execution);
102 let dependencies = super::mcp::parse_dependencies(root, filter);
103 let provenance = super::mcp::parse_provenance(root, filter);
104
105 Ok(vec![ScanTarget {
106 name,
107 framework: Framework::AutoGen,
108 root_path: root.to_path_buf(),
109 tools,
110 execution,
111 data,
112 dependencies,
113 provenance,
114 source_files,
115 }])
116 }
117}
118
119pub(crate) fn extract_autogen_tools_from_source(path: &Path, content: &str) -> Vec<ToolSurface> {
120 let mut tools = Vec::new();
121 let mut doc_map = std::collections::HashMap::new();
122
123 for cap in AUTOGEN_DEF_DOC_RE.captures_iter(content) {
124 let func_name = cap[1].to_string();
125 let doc = cap[2].trim().to_string();
126 doc_map.insert(func_name, doc);
127 }
128
129 for cap in AUTOGEN_REGISTER_FN_RE.captures_iter(content) {
130 let func_name = cap.get(1).map(|m| m.as_str().to_string()).unwrap_or_default();
131 if func_name.is_empty() {
132 continue;
133 }
134
135 let explicit_desc = cap.get(2).map(|m| m.as_str().to_string());
136 let description = explicit_desc.or_else(|| doc_map.get(&func_name).cloned());
137
138 let match_start = cap.get(0).map(|m| m.start()).unwrap_or(0);
139 let line = content[..match_start].lines().count() + 1;
140
141 tools.push(ToolSurface {
142 name: func_name,
143 description,
144 input_schema: None,
145 output_schema: None,
146 declared_permissions: Vec::new(),
147 declared_capabilities: std::collections::BTreeSet::new(),
148 capability_declarations: Vec::new(),
149 observed_capabilities: std::collections::BTreeSet::new(),
150 capability_evidence: Vec::new(),
151 capability_observation_complete: false,
152 defined_at: Some(SourceLocation {
153 file: path.to_path_buf(),
154 line,
155 column: 0,
156 end_line: Some(line),
157 end_column: None,
158 }),
159 });
160 }
161
162 tools
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::adapter::Adapter;
169
170 #[test]
171 fn detects_autogen_requirements() {
172 let dir = tempfile::tempdir().unwrap();
173 let req = dir.path().join("requirements.txt");
174 std::fs::write(&req, "pyautogen>=0.2.0\n").unwrap();
175
176 let adapter = AutoGenAdapter;
177 assert!(adapter.detect(dir.path()));
178 }
179
180 #[test]
181 fn detects_autogen_import() {
182 let dir = tempfile::tempdir().unwrap();
183 let src = dir.path().join("agent.py");
184 std::fs::write(&src, "from autogen import AssistantAgent\n").unwrap();
185
186 let adapter = AutoGenAdapter;
187 assert!(adapter.detect(dir.path()));
188 }
189
190 #[test]
191 fn extracts_autogen_register_function() {
192 let content = r#"
193from autogen import register_function
194
195def execute_command(cmd: str) -> str:
196 """Run a shell command on host."""
197 import subprocess
198 return subprocess.check_output(cmd, shell=True).decode()
199
200register_function(
201 execute_command,
202 caller=assistant,
203 executor=user_proxy,
204 name="execute_command",
205 description="Run shell commands securely"
206)
207"#;
208 let tools = extract_autogen_tools_from_source(Path::new("agent.py"), content);
209 assert_eq!(tools.len(), 1);
210 assert_eq!(tools[0].name, "execute_command");
211 assert_eq!(
212 tools[0].description.as_deref(),
213 Some("Run shell commands securely")
214 );
215 }
216}