agentshield/adapter/
semantic_kernel.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 SK_KERNEL_FN_RE: Lazy<Regex> = Lazy::new(|| {
13 Regex::new(r#"(?s)@kernel_function\s*(?:\(\s*(?:[^\)]*?name\s*=\s*["'`]([^"'`]+)["'`])?(?:[^\)]*?description\s*=\s*["'`]([^"'`]+)["'`])?[^\)]*\))?\s*\n\s*(?:async\s+)?def\s+(\w+)\s*\([^)]*\)\s*(?:->[^:]+)?:\s*(?:\n\s*["'`]([\s\S]*?)["'`])?"#)
14 .expect("static regex pattern is valid")
15});
16
17pub struct SemanticKernelAdapter;
23
24impl super::Adapter for SemanticKernelAdapter {
25 fn framework(&self) -> Framework {
26 Framework::SemanticKernel
27 }
28
29 fn detect(&self, root: &Path) -> bool {
30 let pyproject = root.join("pyproject.toml");
31 if pyproject.exists() {
32 if let Some(content) = super::read_file_capped(&pyproject) {
33 if content.contains("semantic-kernel") || content.contains("semantic_kernel") {
34 return true;
35 }
36 }
37 }
38
39 let requirements = root.join("requirements.txt");
40 if requirements.exists() {
41 if let Some(content) = super::read_file_capped(&requirements) {
42 if content.lines().any(|l| {
43 let trimmed = l.trim();
44 trimmed.starts_with("semantic-kernel")
45 || trimmed.starts_with("semantic_kernel")
46 }) {
47 return true;
48 }
49 }
50 }
51
52 if super::mcp::has_recursive_python_import(
53 root,
54 &[
55 "from semantic_kernel",
56 "import semantic_kernel",
57 "@kernel_function",
58 ],
59 ) {
60 return true;
61 }
62
63 false
64 }
65
66 fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
67 let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
68 self.load_with_filter(root, &filter)
69 }
70
71 fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
72 let name = root
73 .file_name()
74 .map(|n| n.to_string_lossy().to_string())
75 .unwrap_or_else(|| "semantic-kernel-project".into());
76
77 let mut source_files = Vec::new();
78 super::mcp::collect_source_files_with_filter(root, filter, &mut source_files)?;
79
80 source_files.retain(|sf| sf.language == Language::Python);
81
82 let mut tools = Vec::new();
83 for sf in &source_files {
84 tools.extend(extract_semantic_kernel_tools_from_source(&sf.path, &sf.content));
85 }
86
87 for tool in &mut tools {
88 project_declared_permissions(tool);
89 project_declared_description(tool);
90 }
91
92 let execution = super::pipeline::build_execution_surface(&source_files);
93 let data = build_data_surface(&tools, &execution);
94 let dependencies = super::mcp::parse_dependencies(root, filter);
95 let provenance = super::mcp::parse_provenance(root, filter);
96
97 Ok(vec![ScanTarget {
98 name,
99 framework: Framework::SemanticKernel,
100 root_path: root.to_path_buf(),
101 tools,
102 execution,
103 data,
104 dependencies,
105 provenance,
106 source_files,
107 }])
108 }
109}
110
111pub(crate) fn extract_semantic_kernel_tools_from_source(path: &Path, content: &str) -> Vec<ToolSurface> {
112 let mut tools = Vec::new();
113
114 for cap in SK_KERNEL_FN_RE.captures_iter(content) {
115 let explicit_name = cap.get(1).map(|m| m.as_str().to_string());
116 let explicit_desc = cap.get(2).map(|m| m.as_str().to_string());
117 let def_name = cap[3].to_string();
118 let docstring = cap.get(4).map(|m| m.as_str().trim().to_string());
119
120 let tool_name = explicit_name.unwrap_or(def_name);
121 let description = explicit_desc.or(docstring);
122
123 let match_start = cap.get(0).map(|m| m.start()).unwrap_or(0);
124 let line = content[..match_start].lines().count() + 1;
125
126 tools.push(ToolSurface {
127 name: tool_name,
128 description,
129 input_schema: None,
130 output_schema: None,
131 declared_permissions: Vec::new(),
132 declared_capabilities: std::collections::BTreeSet::new(),
133 capability_declarations: Vec::new(),
134 observed_capabilities: std::collections::BTreeSet::new(),
135 capability_evidence: Vec::new(),
136 capability_observation_complete: false,
137 defined_at: Some(SourceLocation {
138 file: path.to_path_buf(),
139 line,
140 column: 0,
141 end_line: Some(line),
142 end_column: None,
143 }),
144 });
145 }
146
147 tools
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::adapter::Adapter;
154
155 #[test]
156 fn detects_semantic_kernel_requirements() {
157 let dir = tempfile::tempdir().unwrap();
158 let req = dir.path().join("requirements.txt");
159 std::fs::write(&req, "semantic-kernel>=1.0.0\n").unwrap();
160
161 let adapter = SemanticKernelAdapter;
162 assert!(adapter.detect(dir.path()));
163 }
164
165 #[test]
166 fn detects_semantic_kernel_import() {
167 let dir = tempfile::tempdir().unwrap();
168 let src = dir.path().join("plugin.py");
169 std::fs::write(&src, "from semantic_kernel.functions import kernel_function\n").unwrap();
170
171 let adapter = SemanticKernelAdapter;
172 assert!(adapter.detect(dir.path()));
173 }
174
175 #[test]
176 fn extracts_semantic_kernel_tools() {
177 let content = r#"
178from semantic_kernel.functions import kernel_function
179
180class DatabasePlugin:
181 @kernel_function(name="query_db", description="Execute a database query")
182 def execute_query(self, sql: str) -> str:
183 """Run SQL query against data warehouse."""
184 return db.query(sql)
185"#;
186 let tools = extract_semantic_kernel_tools_from_source(Path::new("plugin.py"), content);
187 assert_eq!(tools.len(), 1);
188 assert_eq!(tools[0].name, "query_db");
189 assert_eq!(
190 tools[0].description.as_deref(),
191 Some("Execute a database query")
192 );
193 }
194}