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)]
44#[serde(rename_all = "lowercase")]
45pub enum SkillKind {
46 #[default]
47 Instruction,
48 Persona,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct ToolPermission {
58 pub tool: String,
59 pub pattern: String,
60}
61
62impl ToolPermission {
63 pub fn parse(s: &str) -> Option<Self> {
69 let s = s.trim();
70
71 let open = s.find('(')?;
73 let close = s.rfind(')')?;
74
75 if close <= open {
76 return None;
77 }
78
79 let tool = s[..open].trim().to_string();
80 let pattern = s[open + 1..close].trim().to_string();
81
82 Some(ToolPermission { tool, pattern })
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Skill {
91 #[serde(default)]
93 pub name: String,
94
95 #[serde(default)]
97 pub description: String,
98
99 #[serde(default, rename = "allowed-tools")]
101 pub allowed_tools: Option<String>,
102
103 #[serde(default, rename = "disable-model-invocation")]
105 pub disable_model_invocation: bool,
106
107 #[serde(default)]
109 pub kind: SkillKind,
110
111 #[serde(skip)]
113 pub content: String,
114
115 #[serde(default)]
117 pub tags: Vec<String>,
118
119 #[serde(default)]
121 pub version: Option<String>,
122}
123
124impl Skill {
125 pub fn parse(content: &str) -> Option<Self> {
138 let parts: Vec<&str> = content.splitn(3, "---").collect();
140
141 if parts.len() < 3 {
142 return None;
143 }
144
145 let frontmatter = parts[1].trim();
146 let body = parts[2].trim();
147
148 let mut skill: Skill = serde_yaml::from_str(frontmatter).ok()?;
150 skill.content = body.to_string();
151
152 Some(skill)
153 }
154
155 pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
157 let content = std::fs::read_to_string(path.as_ref())?;
158 let mut skill =
159 Self::parse(&content).ok_or_else(|| anyhow::anyhow!("Failed to parse skill file"))?;
160
161 if skill.name.is_empty() {
163 if let Some(stem) = path.as_ref().file_stem() {
164 skill.name = stem.to_string_lossy().to_string();
165 }
166 }
167
168 Ok(skill)
169 }
170
171 pub fn parse_allowed_tools(&self) -> HashSet<ToolPermission> {
176 let mut permissions = HashSet::new();
177
178 let Some(allowed) = &self.allowed_tools else {
179 return permissions;
180 };
181
182 for part in allowed.split(',') {
184 let part = part.trim();
185 if let Some(perm) = ToolPermission::parse(part) {
186 permissions.insert(perm);
187 }
188 }
189
190 permissions
191 }
192
193 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
195 let permissions = self.parse_allowed_tools();
196
197 if permissions.is_empty() {
198 return true;
200 }
201
202 permissions
204 .iter()
205 .any(|perm| perm.tool.eq_ignore_ascii_case(tool_name) && perm.pattern == "*")
206 }
207
208 pub fn to_system_prompt(&self) -> String {
210 format!(
211 "# Skill: {}\n\n{}\n\n{}",
212 self.name, self.description, self.content
213 )
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_parse_skill() {
223 let content = r#"---
224name: test-skill
225description: A test skill
226allowed-tools: "read(*), grep(*)"
227kind: instruction
228---
229# Instructions
230
231You are a test assistant.
232"#;
233
234 let skill = Skill::parse(content).unwrap();
235 assert_eq!(skill.name, "test-skill");
236 assert_eq!(skill.description, "A test skill");
237 assert_eq!(skill.kind, SkillKind::Instruction);
238 assert!(skill.content.contains("You are a test assistant"));
239 }
240
241 #[test]
242 fn test_parse_tool_permission() {
243 let perm = ToolPermission::parse("Bash(gh issue view:*)").unwrap();
244 assert_eq!(perm.tool, "Bash");
245 assert_eq!(perm.pattern, "gh issue view:*");
246
247 let perm = ToolPermission::parse("read(*)").unwrap();
248 assert_eq!(perm.tool, "read");
249 assert_eq!(perm.pattern, "*");
250 }
251
252 #[test]
253 fn test_parse_allowed_tools() {
254 let skill = Skill {
255 name: "test".to_string(),
256 description: "test".to_string(),
257 allowed_tools: Some("read(*), grep(*), Bash(gh:*)".to_string()),
258 disable_model_invocation: false,
259 kind: SkillKind::Instruction,
260 content: String::new(),
261 tags: Vec::new(),
262 version: None,
263 };
264
265 let permissions = skill.parse_allowed_tools();
266 assert_eq!(permissions.len(), 3);
267 }
268
269 #[test]
270 fn test_is_tool_allowed() {
271 let skill = Skill {
272 name: "test".to_string(),
273 description: "test".to_string(),
274 allowed_tools: Some("read(*), grep(*)".to_string()),
275 disable_model_invocation: false,
276 kind: SkillKind::Instruction,
277 content: String::new(),
278 tags: Vec::new(),
279 version: None,
280 };
281
282 assert!(skill.is_tool_allowed("read"));
283 assert!(skill.is_tool_allowed("grep"));
284 assert!(!skill.is_tool_allowed("write"));
285 }
286}