a3s_code_core/skills/
mod.rs1mod builtin;
27pub mod feedback;
28mod manage;
29mod registry;
30pub mod validator;
31
32pub use builtin::builtin_skills;
33pub use feedback::{DefaultSkillScorer, SkillFeedback, SkillOutcome, SkillScore, SkillScorer};
34pub use manage::ManageSkillTool;
35pub use registry::SkillRegistry;
36pub use validator::{
37 DefaultSkillValidator, SkillValidationError, SkillValidator, ValidationErrorKind,
38};
39
40use serde::{Deserialize, Serialize};
41use std::collections::HashSet;
42use std::path::Path;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
51#[serde(rename_all = "lowercase")]
52pub enum SkillKind {
53 #[default]
54 Instruction,
55 Persona,
56 Tool,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct ToolPermission {
66 pub tool: String,
67 pub pattern: String,
68}
69
70impl ToolPermission {
71 pub fn parse(s: &str) -> Option<Self> {
77 let s = s.trim();
78
79 let open = s.find('(')?;
81 let close = s.rfind(')')?;
82
83 if close <= open {
84 return None;
85 }
86
87 let tool = s[..open].trim().to_string();
88 let pattern = s[open + 1..close].trim().to_string();
89
90 Some(ToolPermission { tool, pattern })
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Skill {
99 #[serde(default)]
101 pub name: String,
102
103 #[serde(default)]
105 pub description: String,
106
107 #[serde(default, rename = "allowed-tools")]
109 pub allowed_tools: Option<String>,
110
111 #[serde(default, rename = "disable-model-invocation")]
113 pub disable_model_invocation: bool,
114
115 #[serde(default)]
117 pub kind: SkillKind,
118
119 #[serde(skip)]
121 pub content: String,
122
123 #[serde(default)]
125 pub tags: Vec<String>,
126
127 #[serde(default)]
129 pub version: Option<String>,
130}
131
132impl Skill {
133 pub fn parse(content: &str) -> Option<Self> {
146 let parts: Vec<&str> = content.splitn(3, "---").collect();
148
149 if parts.len() < 3 {
150 return None;
151 }
152
153 let frontmatter = parts[1].trim();
154 let body = parts[2].trim();
155
156 let mut skill: Skill = serde_yaml::from_str(frontmatter).ok()?;
158 skill.content = body.to_string();
159
160 Some(skill)
161 }
162
163 pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
165 let content = std::fs::read_to_string(path.as_ref())?;
166 let mut skill =
167 Self::parse(&content).ok_or_else(|| anyhow::anyhow!("Failed to parse skill file"))?;
168
169 if skill.name.is_empty() {
171 if let Some(stem) = path.as_ref().file_stem() {
172 skill.name = stem.to_string_lossy().to_string();
173 }
174 }
175
176 Ok(skill)
177 }
178
179 pub fn parse_allowed_tools(&self) -> HashSet<ToolPermission> {
184 let mut permissions = HashSet::new();
185
186 let Some(allowed) = &self.allowed_tools else {
187 return permissions;
188 };
189
190 for part in allowed.split(',') {
192 let part = part.trim();
193 if let Some(perm) = ToolPermission::parse(part) {
194 permissions.insert(perm);
195 }
196 }
197
198 permissions
199 }
200
201 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
203 let permissions = self.parse_allowed_tools();
204
205 if permissions.is_empty() {
206 return true;
208 }
209
210 permissions
212 .iter()
213 .any(|perm| perm.tool.eq_ignore_ascii_case(tool_name) && perm.pattern == "*")
214 }
215
216 pub fn to_system_prompt(&self) -> String {
218 format!(
219 "# Skill: {}\n\n{}\n\n{}",
220 self.name, self.description, self.content
221 )
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_parse_skill() {
231 let content = r#"---
232name: test-skill
233description: A test skill
234allowed-tools: "read(*), grep(*)"
235kind: instruction
236---
237# Instructions
238
239You are a test assistant.
240"#;
241
242 let skill = Skill::parse(content).unwrap();
243 assert_eq!(skill.name, "test-skill");
244 assert_eq!(skill.description, "A test skill");
245 assert_eq!(skill.kind, SkillKind::Instruction);
246 assert!(skill.content.contains("You are a test assistant"));
247 }
248
249 #[test]
250 fn test_parse_tool_permission() {
251 let perm = ToolPermission::parse("Bash(gh issue view:*)").unwrap();
252 assert_eq!(perm.tool, "Bash");
253 assert_eq!(perm.pattern, "gh issue view:*");
254
255 let perm = ToolPermission::parse("read(*)").unwrap();
256 assert_eq!(perm.tool, "read");
257 assert_eq!(perm.pattern, "*");
258 }
259
260 #[test]
261 fn test_parse_allowed_tools() {
262 let skill = Skill {
263 name: "test".to_string(),
264 description: "test".to_string(),
265 allowed_tools: Some("read(*), grep(*), Bash(gh:*)".to_string()),
266 disable_model_invocation: false,
267 kind: SkillKind::Instruction,
268 content: String::new(),
269 tags: Vec::new(),
270 version: None,
271 };
272
273 let permissions = skill.parse_allowed_tools();
274 assert_eq!(permissions.len(), 3);
275 }
276
277 #[test]
278 fn test_is_tool_allowed() {
279 let skill = Skill {
280 name: "test".to_string(),
281 description: "test".to_string(),
282 allowed_tools: Some("read(*), grep(*)".to_string()),
283 disable_model_invocation: false,
284 kind: SkillKind::Instruction,
285 content: String::new(),
286 tags: Vec::new(),
287 version: None,
288 };
289
290 assert!(skill.is_tool_allowed("read"));
291 assert!(skill.is_tool_allowed("grep"));
292 assert!(!skill.is_tool_allowed("write"));
293 }
294}