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