Skip to main content

agentshield/adapter/
autogen.rs

1use 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
22/// Microsoft AutoGen framework adapter (`autogen` and `pyautogen`).
23///
24/// Detects AutoGen projects by looking for:
25/// - `pyproject.toml` / `requirements.txt` with `autogen` or `pyautogen`
26/// - Python files importing `from autogen` or `import autogen`
27pub 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}