1#[cfg(test)]
15use crate::capability_types::CapabilityStatus;
16use crate::capability_types::{MountDirectoryBuilder, MountPoint};
17
18use super::Capability;
19pub use everruns_core::capabilities::{
20 MAX_SKILLS_PER_CAPABILITY, SKILL_CAPABILITY_PREFIX, SKILLS_DISCOVERY_PATH,
21 SkillCapabilityIdExt, SkillContribution, SkillInstructions, SkillMeta, SkillSource,
22 discover_skills_from_entries, is_skill_capability, parse_skill_capability_id,
23 reconstruct_skill_md, skill_capability_id,
24};
25use uuid::Uuid;
26
27#[derive(Debug, Clone)]
35pub struct AttachSkillCapability {
36 capability_id: String,
38 skill_name: String,
40 skill_description: String,
42 skill_md_content: String,
44 files: Vec<(String, String)>,
46 user_invocable: bool,
48 disable_model_invocation: bool,
50}
51
52impl AttachSkillCapability {
53 pub fn from_registry(
58 skill_id: Uuid,
59 name: String,
60 description: String,
61 instructions: String,
62 files: Vec<(String, String)>,
63 ) -> Self {
64 Self::from_registry_with_options(
65 skill_id,
66 name,
67 description,
68 instructions,
69 files,
70 true,
71 false,
72 )
73 }
74
75 pub fn from_registry_with_invocable(
76 skill_id: Uuid,
77 name: String,
78 description: String,
79 instructions: String,
80 files: Vec<(String, String)>,
81 user_invocable: bool,
82 ) -> Self {
83 Self::from_registry_with_options(
84 skill_id,
85 name,
86 description,
87 instructions,
88 files,
89 user_invocable,
90 false,
91 )
92 }
93
94 pub fn from_registry_with_options(
95 skill_id: Uuid,
96 name: String,
97 description: String,
98 instructions: String,
99 files: Vec<(String, String)>,
100 user_invocable: bool,
101 disable_model_invocation: bool,
102 ) -> Self {
103 let skill_md_content = reconstruct_skill_md(
104 &name,
105 &description,
106 &instructions,
107 user_invocable,
108 disable_model_invocation,
109 );
110
111 Self {
112 capability_id: skill_capability_id(skill_id),
113 skill_name: name,
114 skill_description: description,
115 skill_md_content,
116 files,
117 user_invocable,
118 disable_model_invocation,
119 }
120 }
121
122 pub fn skill_name(&self) -> &str {
124 &self.skill_name
125 }
126
127 pub fn user_invocable(&self) -> bool {
129 self.user_invocable
130 }
131
132 pub fn disable_model_invocation(&self) -> bool {
134 self.disable_model_invocation
135 }
136
137 fn build_mounts(&self) -> Vec<MountPoint> {
141 let mut builder = MountDirectoryBuilder::new();
142 builder = builder.file("SKILL.md", &self.skill_md_content);
143
144 for (path, content) in &self.files {
145 builder = builder.file(path, content);
146 }
147
148 vec![MountPoint::readonly(
149 format!("{}/{}", SKILLS_DISCOVERY_PATH, self.skill_name),
150 builder.build(),
151 &self.capability_id,
152 )]
153 }
154}
155
156impl Capability for AttachSkillCapability {
157 fn id(&self) -> &str {
158 &self.capability_id
159 }
160
161 fn name(&self) -> &str {
162 &self.skill_name
163 }
164
165 fn description(&self) -> &str {
166 &self.skill_description
167 }
168
169 fn icon(&self) -> Option<&str> {
170 Some("wand")
171 }
172
173 fn category(&self) -> Option<&str> {
174 Some("Skills")
175 }
176
177 fn mounts(&self) -> Vec<MountPoint> {
178 self.build_mounts()
179 }
180
181 fn dependencies(&self) -> Vec<&'static str> {
182 vec!["session_file_system"]
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn registry_constructors_preserve_metadata_flags_and_complete_mounts() {
192 use crate::capability_types::MountSource;
193 let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
194 for variant in 0..6 {
195 let (user, disabled) = match variant {
196 0 => (true, false),
197 1 => (false, false),
198 2 => (false, false),
199 3 => (false, true),
200 4 => (true, false),
201 _ => (true, true),
202 };
203 let files = vec![
204 ("scripts/run.sh".into(), "echo α".into()),
205 ("reference.md".into(), "# Reference".into()),
206 ];
207 let cap = match variant {
208 0 => AttachSkillCapability::from_registry(
209 id,
210 "ops".into(),
211 "Operations".into(),
212 "Exact instructions.".into(),
213 files,
214 ),
215 1 => AttachSkillCapability::from_registry_with_invocable(
216 id,
217 "ops".into(),
218 "Operations".into(),
219 "Exact instructions.".into(),
220 files,
221 false,
222 ),
223 _ => AttachSkillCapability::from_registry_with_options(
224 id,
225 "ops".into(),
226 "Operations".into(),
227 "Exact instructions.".into(),
228 files,
229 user,
230 disabled,
231 ),
232 };
233 assert_eq!(cap.id(), "skill:550e8400-e29b-41d4-a716-446655440000");
234 assert_eq!(cap.name(), "ops");
235 assert_eq!(cap.skill_name(), "ops");
236 assert_eq!(cap.description(), "Operations");
237 assert_eq!(cap.status(), CapabilityStatus::Available);
238 assert_eq!(cap.icon(), Some("wand"));
239 assert_eq!(cap.category(), Some("Skills"));
240 assert_eq!(cap.user_invocable(), user);
241 assert_eq!(cap.disable_model_invocation(), disabled);
242 assert_eq!(cap.dependencies(), ["session_file_system"]);
243 assert!(cap.system_prompt_addition().is_none());
244 assert!(cap.tools().is_empty());
245 assert!(cap.tool_definitions().is_empty());
246 let mounts = cap.mounts();
247 assert_eq!(mounts.len(), 1);
248 assert_eq!(mounts[0].path, "/.agents/skills/ops");
249 assert_eq!(
250 mounts[0].capability_id,
251 "skill:550e8400-e29b-41d4-a716-446655440000"
252 );
253 assert!(mounts[0].is_readonly());
254 let MountSource::InlineDirectory { entries } = &mounts[0].source else {
255 panic!("expected directory")
256 };
257 assert_eq!(entries.len(), 3);
258 assert_eq!(
259 entries["scripts/run.sh"].source,
260 MountSource::text_file("echo α")
261 );
262 assert_eq!(
263 entries["reference.md"].source,
264 MountSource::text_file("# Reference")
265 );
266 let MountSource::InlineFile { content, encoding } = &entries["SKILL.md"].source else {
267 panic!("expected SKILL.md")
268 };
269 assert_eq!(encoding, "text");
270 let parsed = crate::skill::parse_skill_md(content).unwrap();
271 assert_eq!(parsed.name, "ops");
272 assert_eq!(parsed.description, "Operations");
273 assert_eq!(parsed.instructions, "Exact instructions.");
274 assert_eq!(parsed.user_invocable, user);
275 assert_eq!(parsed.disable_model_invocation, disabled);
276 }
277 }
278}