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