1pub mod loader;
36pub mod parser;
37
38use serde::{Deserialize, Serialize};
39use std::collections::HashMap;
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct Skill {
49 pub name: String,
51
52 pub description: String,
54
55 pub system_prompt: String,
57
58 pub tools: Vec<String>,
64
65 pub allowed_tools: Option<Vec<String>>,
68
69 pub denied_tools: Option<Vec<String>>,
72
73 #[serde(default)]
75 pub metadata: HashMap<String, serde_json::Value>,
76}
77
78impl Skill {
79 #[must_use]
81 pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
82 Self {
83 name: name.into(),
84 description: String::new(),
85 system_prompt: system_prompt.into(),
86 tools: Vec::new(),
87 allowed_tools: None,
88 denied_tools: None,
89 metadata: HashMap::new(),
90 }
91 }
92
93 #[must_use]
95 pub fn with_description(mut self, description: impl Into<String>) -> Self {
96 self.description = description.into();
97 self
98 }
99
100 #[must_use]
102 pub fn with_tools(mut self, tools: Vec<String>) -> Self {
103 self.tools = tools;
104 self
105 }
106
107 #[must_use]
109 pub fn with_allowed_tools(mut self, tools: Vec<String>) -> Self {
110 self.allowed_tools = Some(tools);
111 self
112 }
113
114 #[must_use]
116 pub fn with_denied_tools(mut self, tools: Vec<String>) -> Self {
117 self.denied_tools = Some(tools);
118 self
119 }
120
121 #[must_use]
131 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
132 if let Some(ref denied) = self.denied_tools
134 && denied.iter().any(|t| t == tool_name)
135 {
136 return false;
137 }
138
139 let has_allowed_tools = self.allowed_tools.is_some();
142 let has_tools_whitelist = !self.tools.is_empty();
143
144 if has_allowed_tools || has_tools_whitelist {
145 let in_allowed = self
146 .allowed_tools
147 .as_ref()
148 .is_some_and(|allowed| allowed.iter().any(|t| t == tool_name));
149 let in_tools = self.tools.iter().any(|t| t == tool_name);
150 return in_allowed || in_tools;
151 }
152
153 true
155 }
156}
157
158pub use loader::{FileSkillLoader, SkillLoader};
159pub use parser::parse_skill_file;
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_skill_builder() {
167 let skill = Skill::new("test", "You are a test assistant.")
168 .with_description("A test skill")
169 .with_tools(vec!["read".into(), "write".into()])
170 .with_denied_tools(vec!["bash".into()]);
171
172 assert_eq!(skill.name, "test");
173 assert_eq!(skill.description, "A test skill");
174 assert_eq!(skill.system_prompt, "You are a test assistant.");
175 assert_eq!(skill.tools, vec!["read", "write"]);
176 assert_eq!(skill.denied_tools, Some(vec!["bash".into()]));
177 }
178
179 #[test]
180 fn test_is_tool_allowed_no_restrictions() {
181 let skill = Skill::new("test", "prompt");
182
183 assert!(skill.is_tool_allowed("read"));
184 assert!(skill.is_tool_allowed("write"));
185 assert!(skill.is_tool_allowed("bash"));
186 }
187
188 #[test]
189 fn test_is_tool_allowed_with_denied() {
190 let skill = Skill::new("test", "prompt").with_denied_tools(vec!["bash".into()]);
191
192 assert!(skill.is_tool_allowed("read"));
193 assert!(skill.is_tool_allowed("write"));
194 assert!(!skill.is_tool_allowed("bash"));
195 }
196
197 #[test]
198 fn test_is_tool_allowed_with_whitelist() {
199 let skill =
200 Skill::new("test", "prompt").with_allowed_tools(vec!["read".into(), "grep".into()]);
201
202 assert!(skill.is_tool_allowed("read"));
203 assert!(skill.is_tool_allowed("grep"));
204 assert!(!skill.is_tool_allowed("write"));
205 assert!(!skill.is_tool_allowed("bash"));
206 }
207
208 #[test]
209 fn test_is_tool_allowed_denied_takes_precedence() {
210 let skill = Skill::new("test", "prompt")
211 .with_allowed_tools(vec!["read".into(), "bash".into()])
212 .with_denied_tools(vec!["bash".into()]);
213
214 assert!(skill.is_tool_allowed("read"));
215 assert!(!skill.is_tool_allowed("bash")); }
217
218 #[test]
219 fn test_is_tool_allowed_tools_acts_as_whitelist() {
220 let skill = Skill::new("test", "prompt").with_tools(vec!["read".into()]);
224
225 assert!(skill.is_tool_allowed("read"));
226 assert!(!skill.is_tool_allowed("write"));
227 assert!(!skill.is_tool_allowed("bash"));
228 }
229
230 #[test]
231 fn test_is_tool_allowed_tools_unions_with_allowed_tools() {
232 let skill = Skill::new("test", "prompt")
233 .with_tools(vec!["read".into()])
234 .with_allowed_tools(vec!["grep".into()]);
235
236 assert!(skill.is_tool_allowed("read")); assert!(skill.is_tool_allowed("grep")); assert!(!skill.is_tool_allowed("bash"));
239 }
240
241 #[test]
242 fn test_is_tool_allowed_denied_overrides_tools_whitelist() {
243 let skill = Skill::new("test", "prompt")
244 .with_tools(vec!["read".into(), "bash".into()])
245 .with_denied_tools(vec!["bash".into()]);
246
247 assert!(skill.is_tool_allowed("read"));
248 assert!(!skill.is_tool_allowed("bash")); }
250}