a3s_code_core/skills/
mod.rs1mod builtin;
21mod registry;
22
23pub use builtin::builtin_skills;
24pub use registry::SkillRegistry;
25
26use serde::{Deserialize, Serialize};
27use std::collections::HashSet;
28use std::path::Path;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
37#[serde(rename_all = "lowercase")]
38pub enum SkillKind {
39 #[default]
40 Instruction,
41 Tool,
42 Agent,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub struct ToolPermission {
52 pub tool: String,
53 pub pattern: String,
54}
55
56impl ToolPermission {
57 pub fn parse(s: &str) -> Option<Self> {
63 let s = s.trim();
64
65 let open = s.find('(')?;
67 let close = s.rfind(')')?;
68
69 if close <= open {
70 return None;
71 }
72
73 let tool = s[..open].trim().to_string();
74 let pattern = s[open + 1..close].trim().to_string();
75
76 Some(ToolPermission { tool, pattern })
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Skill {
85 #[serde(default)]
87 pub name: String,
88
89 #[serde(default)]
91 pub description: String,
92
93 #[serde(default, rename = "allowed-tools")]
95 pub allowed_tools: Option<String>,
96
97 #[serde(default, rename = "disable-model-invocation")]
99 pub disable_model_invocation: bool,
100
101 #[serde(default)]
103 pub kind: SkillKind,
104
105 #[serde(skip)]
107 pub content: String,
108
109 #[serde(default)]
111 pub tags: Vec<String>,
112
113 #[serde(default)]
115 pub version: Option<String>,
116}
117
118impl Skill {
119 pub fn parse(content: &str) -> Option<Self> {
132 let parts: Vec<&str> = content.splitn(3, "---").collect();
134
135 if parts.len() < 3 {
136 return None;
137 }
138
139 let frontmatter = parts[1].trim();
140 let body = parts[2].trim();
141
142 let mut skill: Skill = serde_yaml::from_str(frontmatter).ok()?;
144 skill.content = body.to_string();
145
146 Some(skill)
147 }
148
149 pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
151 let content = std::fs::read_to_string(path.as_ref())?;
152 let mut skill =
153 Self::parse(&content).ok_or_else(|| anyhow::anyhow!("Failed to parse skill file"))?;
154
155 if skill.name.is_empty() {
157 if let Some(stem) = path.as_ref().file_stem() {
158 skill.name = stem.to_string_lossy().to_string();
159 }
160 }
161
162 Ok(skill)
163 }
164
165 pub fn parse_allowed_tools(&self) -> HashSet<ToolPermission> {
170 let mut permissions = HashSet::new();
171
172 let Some(allowed) = &self.allowed_tools else {
173 return permissions;
174 };
175
176 for part in allowed.split(',') {
178 let part = part.trim();
179 if let Some(perm) = ToolPermission::parse(part) {
180 permissions.insert(perm);
181 }
182 }
183
184 permissions
185 }
186
187 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
189 let permissions = self.parse_allowed_tools();
190
191 if permissions.is_empty() {
192 return true;
194 }
195
196 permissions
198 .iter()
199 .any(|perm| perm.tool.eq_ignore_ascii_case(tool_name) && perm.pattern == "*")
200 }
201
202 pub fn to_system_prompt(&self) -> String {
204 format!(
205 "# Skill: {}\n\n{}\n\n{}",
206 self.name, self.description, self.content
207 )
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_parse_skill() {
217 let content = r#"---
218name: test-skill
219description: A test skill
220allowed-tools: "read(*), grep(*)"
221kind: instruction
222---
223# Instructions
224
225You are a test assistant.
226"#;
227
228 let skill = Skill::parse(content).unwrap();
229 assert_eq!(skill.name, "test-skill");
230 assert_eq!(skill.description, "A test skill");
231 assert_eq!(skill.kind, SkillKind::Instruction);
232 assert!(skill.content.contains("You are a test assistant"));
233 }
234
235 #[test]
236 fn test_parse_tool_permission() {
237 let perm = ToolPermission::parse("Bash(gh issue view:*)").unwrap();
238 assert_eq!(perm.tool, "Bash");
239 assert_eq!(perm.pattern, "gh issue view:*");
240
241 let perm = ToolPermission::parse("read(*)").unwrap();
242 assert_eq!(perm.tool, "read");
243 assert_eq!(perm.pattern, "*");
244 }
245
246 #[test]
247 fn test_parse_allowed_tools() {
248 let skill = Skill {
249 name: "test".to_string(),
250 description: "test".to_string(),
251 allowed_tools: Some("read(*), grep(*), Bash(gh:*)".to_string()),
252 disable_model_invocation: false,
253 kind: SkillKind::Instruction,
254 content: String::new(),
255 tags: Vec::new(),
256 version: None,
257 };
258
259 let permissions = skill.parse_allowed_tools();
260 assert_eq!(permissions.len(), 3);
261 }
262
263 #[test]
264 fn test_is_tool_allowed() {
265 let skill = Skill {
266 name: "test".to_string(),
267 description: "test".to_string(),
268 allowed_tools: Some("read(*), grep(*)".to_string()),
269 disable_model_invocation: false,
270 kind: SkillKind::Instruction,
271 content: String::new(),
272 tags: Vec::new(),
273 version: None,
274 };
275
276 assert!(skill.is_tool_allowed("read"));
277 assert!(skill.is_tool_allowed("grep"));
278 assert!(!skill.is_tool_allowed("write"));
279 }
280}