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 = Self::parse(&content)
153 .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.iter().any(|perm| {
198 perm.tool.eq_ignore_ascii_case(tool_name) && perm.pattern == "*"
199 })
200 }
201
202 pub fn to_system_prompt(&self) -> String {
204 format!(
205 "# Skill: {}\n\n{}\n\n{}",
206 self.name,
207 self.description,
208 self.content
209 )
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_parse_skill() {
219 let content = r#"---
220name: test-skill
221description: A test skill
222allowed-tools: "read(*), grep(*)"
223kind: instruction
224---
225# Instructions
226
227You are a test assistant.
228"#;
229
230 let skill = Skill::parse(content).unwrap();
231 assert_eq!(skill.name, "test-skill");
232 assert_eq!(skill.description, "A test skill");
233 assert_eq!(skill.kind, SkillKind::Instruction);
234 assert!(skill.content.contains("You are a test assistant"));
235 }
236
237 #[test]
238 fn test_parse_tool_permission() {
239 let perm = ToolPermission::parse("Bash(gh issue view:*)").unwrap();
240 assert_eq!(perm.tool, "Bash");
241 assert_eq!(perm.pattern, "gh issue view:*");
242
243 let perm = ToolPermission::parse("read(*)").unwrap();
244 assert_eq!(perm.tool, "read");
245 assert_eq!(perm.pattern, "*");
246 }
247
248 #[test]
249 fn test_parse_allowed_tools() {
250 let skill = Skill {
251 name: "test".to_string(),
252 description: "test".to_string(),
253 allowed_tools: Some("read(*), grep(*), Bash(gh:*)".to_string()),
254 disable_model_invocation: false,
255 kind: SkillKind::Instruction,
256 content: String::new(),
257 tags: Vec::new(),
258 version: None,
259 };
260
261 let permissions = skill.parse_allowed_tools();
262 assert_eq!(permissions.len(), 3);
263 }
264
265 #[test]
266 fn test_is_tool_allowed() {
267 let skill = Skill {
268 name: "test".to_string(),
269 description: "test".to_string(),
270 allowed_tools: Some("read(*), grep(*)".to_string()),
271 disable_model_invocation: false,
272 kind: SkillKind::Instruction,
273 content: String::new(),
274 tags: Vec::new(),
275 version: None,
276 };
277
278 assert!(skill.is_tool_allowed("read"));
279 assert!(skill.is_tool_allowed("grep"));
280 assert!(!skill.is_tool_allowed("write"));
281 }
282}