agentshield/adapter/
openclaw.rs1use std::path::{Path, PathBuf};
2
3use crate::config::ScanPathFilter;
4use crate::error::Result;
5use crate::ir::taint_builder::build_data_surface;
6use crate::ir::*;
7
8pub struct OpenClawAdapter;
13
14impl super::Adapter for OpenClawAdapter {
15 fn framework(&self) -> Framework {
16 Framework::OpenClaw
17 }
18
19 fn detect(&self, root: &Path) -> bool {
20 !find_openclaw_skill_roots(root).is_empty()
21 }
22
23 fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
24 let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
25 self.load_with_filter(root, &filter)
26 }
27
28 fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
29 find_openclaw_skill_roots(root)
30 .into_iter()
31 .map(|skill_root| load_skill_target(root, &skill_root, filter))
32 .collect()
33 }
34}
35
36fn load_skill_target(
37 scan_root: &Path,
38 skill_root: &Path,
39 filter: &ScanPathFilter,
40) -> Result<ScanTarget> {
41 let name = skill_root
42 .file_name()
43 .map(|name| name.to_string_lossy().into_owned())
44 .unwrap_or_else(|| "openclaw-skill".to_owned());
45 let source_files = collect_skill_source_files(scan_root, skill_root, filter)?;
46 let execution = super::pipeline::build_execution_surface(&source_files);
47 let tools = Vec::new();
48 let data = build_data_surface(&tools, &execution);
49
50 Ok(ScanTarget {
51 name,
52 framework: Framework::OpenClaw,
53 root_path: skill_root.to_path_buf(),
54 tools,
55 execution,
56 data,
57 dependencies: Default::default(),
58 provenance: Default::default(),
59 source_files,
60 })
61}
62
63fn collect_skill_source_files(
64 scan_root: &Path,
65 skill_root: &Path,
66 filter: &ScanPathFilter,
67) -> Result<Vec<SourceFile>> {
68 let walker = ignore::WalkBuilder::new(skill_root)
69 .hidden(true)
70 .git_ignore(true)
71 .max_depth(Some(3))
72 .build();
73 let mut source_files = Vec::new();
74
75 for entry in walker.flatten() {
76 let path = entry.path();
77 if !path.is_file()
78 || (filter.ignore_tests() && super::mcp::is_test_file(path))
79 || !filter.allows_path(scan_root, path)
80 {
81 continue;
82 }
83
84 let extension = path
85 .extension()
86 .map(|extension| extension.to_string_lossy())
87 .unwrap_or_default();
88 let language = Language::from_extension(&extension);
89 if !matches!(
90 language,
91 Language::Python | Language::Shell | Language::Markdown
92 ) {
93 continue;
94 }
95
96 let metadata = std::fs::metadata(path)?;
97 if metadata.len() > 1_048_576 {
98 continue;
99 }
100
101 if let Ok(content) = std::fs::read_to_string(path) {
102 use sha2::Digest;
103 let content_hash = format!(
104 "{:x}",
105 sha2::Sha256::new()
106 .chain_update(content.as_bytes())
107 .finalize()
108 );
109 source_files.push(SourceFile {
110 path: path.to_path_buf(),
111 language,
112 size_bytes: metadata.len(),
113 content_hash,
114 content,
115 });
116 }
117 }
118
119 Ok(source_files)
120}
121
122fn find_openclaw_skill_roots(root: &Path) -> Vec<PathBuf> {
123 let mut skills = Vec::new();
124
125 if root.join("SKILL.md").is_file() {
126 skills.push(root.to_path_buf());
127 }
128
129 let agents_dir = root.join(".agents").join("skills");
130 if !agents_dir.is_dir() {
131 return skills;
132 }
133
134 if let Ok(entries) = std::fs::read_dir(agents_dir) {
135 for entry in entries.flatten() {
136 let is_directory = entry
137 .file_type()
138 .map(|file_type| file_type.is_dir())
139 .unwrap_or(false);
140 if is_directory && entry.path().join("SKILL.md").is_file() {
141 skills.push(entry.path());
142 }
143 }
144 }
145
146 skills.sort();
147 skills.dedup();
148 skills
149}
150
151#[cfg(test)]
152mod tests {
153 use std::path::{Path, PathBuf};
154
155 use super::OpenClawAdapter;
156 use crate::adapter::Adapter;
157 use tempfile::tempdir;
158
159 fn write_skill(root: &Path, name: &str, source: &str) -> PathBuf {
160 let skill_root = root.join(".agents").join("skills").join(name);
161 std::fs::create_dir_all(&skill_root).expect("create skill dir");
162 std::fs::write(skill_root.join("SKILL.md"), format!("# {name}\n"))
163 .expect("write skill file");
164 std::fs::write(skill_root.join("tool.py"), source).expect("write source file");
165 skill_root
166 }
167
168 #[test]
169 fn detects_agents_skills_layout() {
170 let temp = tempdir().expect("create tempdir");
171 let skill_root = write_skill(temp.path(), "mbras-harness", "def process():\n pass\n");
172
173 let adapter = OpenClawAdapter;
174 let root = temp.path();
175
176 assert!(adapter.detect(root));
177
178 let targets = adapter
179 .load(root, false)
180 .expect("openclaw load should succeed");
181 assert_eq!(targets.len(), 1);
182 assert_eq!(targets[0].name, "mbras-harness");
183 assert_eq!(targets[0].root_path, skill_root);
184 assert!(!targets[0].source_files.is_empty());
185 }
186
187 #[test]
188 fn loads_each_skill_as_an_isolated_target_in_stable_order() {
189 let temp = tempdir().expect("create tempdir");
190 let alpha = write_skill(
191 temp.path(),
192 "alpha",
193 "import subprocess\nsubprocess.run(user_input, shell=True)\n",
194 );
195 let zulu = write_skill(
196 temp.path(),
197 "zulu",
198 "import requests\nrequests.get(user_url)\n",
199 );
200
201 let targets = OpenClawAdapter
202 .load(temp.path(), false)
203 .expect("load isolated skills");
204
205 assert_eq!(targets.len(), 2);
206 assert_eq!(targets[0].name, "alpha");
207 assert_eq!(targets[0].root_path, alpha);
208 assert!(
209 targets[0]
210 .source_files
211 .iter()
212 .all(|source| source.path.starts_with(&targets[0].root_path))
213 );
214 assert!(!targets[0].execution.commands.is_empty());
215 assert!(targets[0].execution.network_operations.is_empty());
216
217 assert_eq!(targets[1].name, "zulu");
218 assert_eq!(targets[1].root_path, zulu);
219 assert!(
220 targets[1]
221 .source_files
222 .iter()
223 .all(|source| source.path.starts_with(&targets[1].root_path))
224 );
225 assert!(targets[1].execution.commands.is_empty());
226 assert!(!targets[1].execution.network_operations.is_empty());
227 }
228
229 #[test]
230 fn does_not_treat_deeper_nested_skill_as_direct_layout() {
231 let temp = tempdir().expect("create tempdir");
232 let nested = temp
233 .path()
234 .join(".agents")
235 .join("skills")
236 .join("group")
237 .join("nested");
238 std::fs::create_dir_all(&nested).expect("create nested dir");
239 std::fs::write(nested.join("SKILL.md"), "# nested\n").expect("write nested skill");
240
241 assert!(!OpenClawAdapter.detect(temp.path()));
242 }
243
244 #[test]
245 fn root_skill_and_agents_skill_remain_separate_targets() {
246 let temp = tempdir().expect("create tempdir");
247 std::fs::write(temp.path().join("SKILL.md"), "# root\n").expect("write root skill");
248 let nested = write_skill(temp.path(), "nested", "def nested():\n pass\n");
249
250 let targets = OpenClawAdapter
251 .load(temp.path(), false)
252 .expect("load root and nested skills");
253
254 assert_eq!(targets.len(), 2);
255 assert_eq!(targets[0].root_path, temp.path());
256 assert_eq!(targets[1].root_path, nested);
257 }
258}