1use crate::types::Skill;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub enum ContainerSkillsRequirement {
13 Required,
15 RequiredWithFallback,
17 NotRequired,
19 Unknown,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ContainerValidationResult {
26 pub requirement: ContainerSkillsRequirement,
28 analysis: String,
30 patterns_found: Vec<String>,
32 pub recommendations: Vec<String>,
34 should_filter: bool,
36}
37
38pub struct ContainerSkillsValidator {
40 container_patterns: Vec<String>,
42 fallback_patterns: Vec<String>,
44 incompatibility_patterns: Vec<String>,
46}
47
48impl Default for ContainerSkillsValidator {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl ContainerSkillsValidator {
55 pub fn new() -> Self {
57 Self {
58 container_patterns: vec![
59 "container={".to_string(),
60 "container.skills".to_string(),
61 "betas=\"skills-".to_string(),
62 "betas=[\"skills-".to_string(),
63 ],
64 fallback_patterns: vec![
65 "vtcode does not currently support".to_string(),
66 "use exec_command".to_string(),
67 "openpyxl".to_string(),
68 "reportlab".to_string(),
69 "python-docx".to_string(),
70 ],
71 incompatibility_patterns: vec![
72 "vtcode does not currently support".to_string(),
73 "requires Anthropic's container skills".to_string(),
74 ],
75 }
76 }
77
78 pub fn analyze_skill(&self, skill: &Skill) -> ContainerValidationResult {
80 if let Some(true) = skill.manifest.requires_container {
82 return ContainerValidationResult {
83 requirement: ContainerSkillsRequirement::Required,
84 analysis: "Manifest sets requires-container=true".to_string(),
85 patterns_found: vec!["requires-container".to_string()],
86 recommendations: vec![
87 "This skill declares Anthropic container skills are required; VT Code cannot execute them directly.".to_string(),
88 "Use a VT Code-native alternative or provide a fallback implementation.".to_string(),
89 ],
90 should_filter: true,
91 };
92 }
93
94 if let Some(true) = skill.manifest.disallow_container {
95 return ContainerValidationResult {
96 requirement: ContainerSkillsRequirement::NotRequired,
97 analysis: "Manifest sets disallow-container=true (VT Code-native only)".to_string(),
98 patterns_found: vec!["disallow-container".to_string()],
99 recommendations: vec![
100 "Use VT Code-native execution paths via `exec_command` instead of Anthropic container skills."
101 .to_string(),
102 ],
103 should_filter: false,
104 };
105 }
106
107 if let Some(true) = skill.manifest.vtcode_native {
109 return ContainerValidationResult {
110 requirement: ContainerSkillsRequirement::NotRequired,
111 analysis: "Skill uses VT Code native features (not container skills)".to_string(),
112 patterns_found: vec![],
113 recommendations: vec![],
114 should_filter: false,
115 };
116 }
117
118 let instructions = &skill.instructions;
119 let mut patterns_found = Vec::new();
120 let mut recommendations = Vec::new();
121
122 let mut has_container_usage = false;
124 for pattern in &self.container_patterns {
125 if instructions.contains(pattern) {
126 patterns_found.push(pattern.clone());
127 has_container_usage = true;
128 }
129 }
130
131 let mut has_incompatibility = false;
133 for pattern in &self.incompatibility_patterns {
134 if instructions.contains(pattern) {
135 patterns_found.push(pattern.clone());
136 has_incompatibility = true;
137 }
138 }
139
140 let mut has_fallback = false;
142 for pattern in &self.fallback_patterns {
143 if instructions.contains(pattern) {
144 patterns_found.push(pattern.clone());
145 has_fallback = true;
146 }
147 }
148
149 let (requirement, analysis, should_filter) = if has_incompatibility {
151 (
152 ContainerSkillsRequirement::RequiredWithFallback,
153 format!(
154 "Skill '{}' explicitly states it requires Anthropic container skills which VT Code does not support. However, it provides fallback alternatives.",
155 skill.name()
156 ),
157 false, )
159 } else if has_container_usage && has_fallback {
160 (
161 ContainerSkillsRequirement::RequiredWithFallback,
162 format!("Skill '{}' uses container skills but provides VT Code-compatible alternatives.", skill.name()),
163 false,
164 )
165 } else if has_container_usage {
166 (
167 ContainerSkillsRequirement::Required,
168 format!(
169 "Skill '{}' requires Anthropic container skills which are not supported in VT Code.",
170 skill.name()
171 ),
172 true, )
174 } else {
175 (
176 ContainerSkillsRequirement::NotRequired,
177 format!("Skill '{}' does not require container skills.", skill.name()),
178 false,
179 )
180 };
181
182 if requirement == ContainerSkillsRequirement::Required {
184 recommendations.push(
185 "This skill requires Anthropic's container skills feature which is not available in VT Code."
186 .to_string(),
187 );
188 recommendations.push("".to_string());
189 recommendations.push("Consider these VT Code-compatible alternatives:".to_string());
190
191 if skill.name().contains("pdf") || skill.name().contains("report") {
193 recommendations.push(
194 " 1. Run Python through exec_command.cmd with libraries: reportlab, fpdf2, or weasyprint"
195 .to_string(),
196 );
197 recommendations.push(" 2. Install: pip install reportlab".to_string());
198 recommendations.push(" 3. Use Python code execution to generate PDFs".to_string());
199 } else if skill.name().contains("spreadsheet") || skill.name().contains("excel") {
200 recommendations.push(
201 " 1. Run Python through exec_command.cmd with libraries: openpyxl, xlsxwriter, or pandas"
202 .to_string(),
203 );
204 recommendations.push(" 2. Install: pip install openpyxl xlsxwriter".to_string());
205 recommendations.push(" 3. Use Python code execution to create spreadsheets".to_string());
206 } else if skill.name().contains("doc") || skill.name().contains("word") {
207 recommendations.push(
208 " 1. Run Python through exec_command.cmd with libraries: python-docx or docxtpl".to_string(),
209 );
210 recommendations.push(" 2. Install: pip install python-docx".to_string());
211 recommendations.push(" 3. Use Python code execution to generate documents".to_string());
212 } else if skill.name().contains("presentation") || skill.name().contains("ppt") {
213 recommendations
214 .push(" 1. Run Python through exec_command.cmd with libraries: python-pptx".to_string());
215 recommendations.push(" 2. Install: pip install python-pptx".to_string());
216 recommendations.push(" 3. Use Python code execution to create presentations".to_string());
217 } else {
218 recommendations
219 .push(" 1. Run Python through exec_command.cmd with appropriate Python libraries".to_string());
220 recommendations.push(" 2. Search for VT Code-compatible skills in the documentation".to_string());
221 }
222
223 recommendations.push("".to_string());
224 recommendations.push("Learn more about VT Code's code execution in the documentation.".to_string());
225 recommendations.push("Official Anthropic container skills documentation: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview".to_string());
226 } else if requirement == ContainerSkillsRequirement::RequiredWithFallback {
227 recommendations
228 .push("This skill uses container skills but provides VT Code-compatible alternatives.".to_string());
229 recommendations.push("Use the fallback instructions in the skill documentation.".to_string());
230 recommendations.push("Look for sections marked 'Option 2' or 'VT Code Alternative'.".to_string());
231 recommendations.push(
232 "The skill instructions contain working examples using legacy `execute_code`; map them to `exec_command` with a Python command in VT Code.".to_string(),
233 );
234 }
235
236 ContainerValidationResult {
237 requirement,
238 analysis,
239 patterns_found,
240 recommendations,
241 should_filter,
242 }
243 }
244
245 pub fn analyze_skills(&self, skills: &[Skill]) -> Vec<ContainerValidationResult> {
247 skills.iter().map(|skill| self.analyze_skill(skill)).collect()
248 }
249
250 pub fn filter_incompatible_skills(&self, skills: Vec<Skill>) -> (Vec<Skill>, Vec<IncompatibleSkillInfo>) {
252 let mut compatible_skills = Vec::new();
253 let mut incompatible_skills = Vec::new();
254
255 for skill in skills {
256 let analysis = self.analyze_skill(&skill);
257
258 if analysis.should_filter {
259 incompatible_skills.push(IncompatibleSkillInfo {
260 name: skill.name().to_string(),
261 description: skill.description().to_string(),
262 reason: analysis.analysis,
263 recommendations: analysis.recommendations,
264 });
265 } else {
266 compatible_skills.push(skill);
267 }
268 }
269
270 (compatible_skills, incompatible_skills)
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct IncompatibleSkillInfo {
277 name: String,
278 description: String,
279 reason: String,
280 recommendations: Vec<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct ContainerValidationReport {
286 total_skills_analyzed: usize,
287 compatible_skills: Vec<String>,
288 pub incompatible_skills: Vec<IncompatibleSkillInfo>,
289 skills_with_fallbacks: Vec<SkillWithFallback>,
290 summary: ValidationSummary,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct SkillWithFallback {
295 name: String,
296 description: String,
297 fallback_description: String,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ValidationSummary {
302 total_compatible: usize,
303 total_incompatible: usize,
304 total_with_fallbacks: usize,
305 recommendation: String,
306}
307
308impl ContainerValidationReport {
309 pub fn new() -> Self {
310 Self {
311 total_skills_analyzed: 0,
312 compatible_skills: Vec::new(),
313 incompatible_skills: Vec::new(),
314 skills_with_fallbacks: Vec::new(),
315 summary: ValidationSummary {
316 total_compatible: 0,
317 total_incompatible: 0,
318 total_with_fallbacks: 0,
319 recommendation: String::new(),
320 },
321 }
322 }
323
324 pub fn add_skill_analysis(&mut self, skill_name: String, analysis: ContainerValidationResult) {
325 self.total_skills_analyzed += 1;
326
327 match analysis.requirement {
328 ContainerSkillsRequirement::NotRequired => {
329 self.compatible_skills.push(skill_name);
330 self.summary.total_compatible += 1;
331 }
332 ContainerSkillsRequirement::Required => {
333 self.incompatible_skills.push(IncompatibleSkillInfo {
334 name: skill_name.clone(),
335 description: "Container skills required".to_string(),
336 reason: analysis.analysis,
337 recommendations: analysis.recommendations,
338 });
339 self.summary.total_incompatible += 1;
340 }
341 ContainerSkillsRequirement::RequiredWithFallback => {
342 self.skills_with_fallbacks.push(SkillWithFallback {
343 name: skill_name.clone(),
344 description: "Container skills with fallback".to_string(),
345 fallback_description: analysis.recommendations.join(" "),
346 });
347 self.summary.total_with_fallbacks += 1;
348 }
349 ContainerSkillsRequirement::Unknown => {
350 self.compatible_skills.push(skill_name);
352 self.summary.total_compatible += 1;
353 }
354 }
355 }
356
357 pub fn add_incompatible_skill(&mut self, name: String, description: String, reason: String) {
358 self.incompatible_skills.push(IncompatibleSkillInfo {
359 name,
360 description,
361 reason,
362 recommendations: vec![
363 "This skill requires Anthropic container skills which are not supported in VT Code.".to_string(),
364 "Consider using alternative approaches with VT Code's code execution tools.".to_string(),
365 ],
366 });
367 self.summary.total_incompatible += 1;
368 self.total_skills_analyzed += 1;
369 }
370
371 pub fn finalize(&mut self) {
372 self.summary.recommendation = match (self.summary.total_incompatible, self.summary.total_with_fallbacks) {
373 (0, 0) => "All skills are fully compatible with VT Code.".to_string(),
374 (0, _) => format!(
375 "{} skills have container skills dependencies but provide VT Code-compatible fallbacks.",
376 self.summary.total_with_fallbacks
377 ),
378 (_, 0) => format!(
379 "{} skills require container skills and cannot be used. Consider the suggested alternatives.",
380 self.summary.total_incompatible
381 ),
382 (_, _) => format!(
383 "{} skills require container skills. {} skills have fallbacks. Use alternatives or fallback instructions.",
384 self.summary.total_incompatible, self.summary.total_with_fallbacks
385 ),
386 };
387 }
388
389 pub fn format_report(&self) -> String {
390 let mut output = String::new();
391 output.push_str(" Container Skills Validation Report\n");
392 output.push_str("=====================================\n\n");
393 output.push_str(&format!("Total Skills Analyzed: {}\n", self.total_skills_analyzed));
394 output.push_str(&format!("Compatible: {}\n", self.summary.total_compatible));
395 output.push_str(&format!("With Fallbacks: {}\n", self.summary.total_with_fallbacks));
396 output.push_str(&format!("Incompatible: {}\n\n", self.summary.total_incompatible));
397 output.push_str(&self.summary.recommendation);
398
399 if !self.incompatible_skills.is_empty() {
400 output.push_str("\n\nIncompatible Skills:");
401 for skill in &self.incompatible_skills {
402 output.push_str(&format!("\n • {} - {}", skill.name, skill.description));
403 for rec in &skill.recommendations {
404 output.push_str(&format!("\n {rec}"));
405 }
406 }
407 }
408
409 if !self.skills_with_fallbacks.is_empty() {
410 output.push_str("\n\nSkills with Fallbacks:");
411 for skill in &self.skills_with_fallbacks {
412 output.push_str(&format!("\n • {} - {}", skill.name, skill.description));
413 }
414 }
415
416 output
417 }
418}
419
420impl Default for ContainerValidationReport {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use crate::types::{Skill, SkillManifest};
430 use std::path::PathBuf;
431
432 #[test]
433 fn test_container_skills_detection() {
434 let validator = ContainerSkillsValidator::new();
435
436 let manifest = SkillManifest {
438 name: "pdf-report-generator".to_string(),
439 description: "Generate PDFs".to_string(),
440 version: Some("1.0.0".to_string()),
441 author: Some("Test".to_string()),
442 ..Default::default()
443 };
444
445 let instructions = r#"
446 Generate PDF documents using Anthropic's pdf skill.
447
448 ```python
449 response = client.messages.create(
450 model="claude-sonnet-5",
451 container={
452 "type": "skills",
453 "skills": [{"type": "anthropic", "skill_id": "pdf", "version": "latest"}]
454 },
455 betas=["skills-2025-10-02"]
456 )
457 ```
458 "#;
459
460 let skill = Skill::new(manifest, PathBuf::from("/tmp"), instructions.to_string()).unwrap();
461 let result = validator.analyze_skill(&skill);
462
463 assert_eq!(result.requirement, ContainerSkillsRequirement::Required);
464 assert!(result.should_filter);
465 assert!(!result.patterns_found.is_empty());
466 }
467
468 #[test]
469 fn test_enhanced_validation_with_fallback() {
470 let validator = ContainerSkillsValidator::new();
471
472 let manifest = SkillManifest {
473 name: "spreadsheet-generator".to_string(),
474 description: "Generate spreadsheets".to_string(),
475 version: Some("1.0.0".to_string()),
476 author: Some("Test".to_string()),
477 vtcode_native: Some(true),
478 ..Default::default()
479 };
480
481 let instructions = r#"
482 **vtcode does not currently support Anthropic container skills.** Instead, use:
483
484 ### Option 1: Python Script with openpyxl
485 Use vtcode's `exec_command` tool with a Python `cmd` and openpyxl:
486
487 ```python
488 import openpyxl
489 wb = openpyxl.Workbook()
490 # ... create spreadsheet
491 wb.save("output.xlsx")
492 ```
493 "#;
494
495 let skill = Skill::new(manifest, PathBuf::from("/tmp"), instructions.to_string()).unwrap();
496 let result = validator.analyze_skill(&skill);
497
498 assert_eq!(result.requirement, ContainerSkillsRequirement::NotRequired);
500 assert!(!result.should_filter);
501 assert!(result.patterns_found.is_empty());
503 }
505
506 #[test]
507 fn test_enhanced_validation_without_fallback() {
508 let validator = ContainerSkillsValidator::new();
509
510 let manifest = SkillManifest {
511 name: "pdf-report-generator".to_string(),
512 description: "Generate PDFs".to_string(),
513 version: Some("1.0.0".to_string()),
514 author: Some("Test".to_string()),
515 ..Default::default()
516 };
517
518 let instructions = r#"
519 Generate PDF documents using Anthropic's pdf skill.
520
521 ```python
522 response = client.messages.create(
523 model="claude-sonnet-5",
524 container={
525 "type": "skills",
526 "skills": [{"type": "anthropic", "skill_id": "pdf", "version": "latest"}]
527 },
528 betas=["skills-2025-10-02"]
529 )
530 ```
531 "#;
532
533 let skill = Skill::new(manifest, PathBuf::from("/tmp"), instructions.to_string()).unwrap();
534 let result = validator.analyze_skill(&skill);
535
536 assert_eq!(result.requirement, ContainerSkillsRequirement::Required);
537 assert!(result.should_filter);
538
539 let recommendations = result.recommendations.join(" ");
541 assert!(recommendations.contains("container skills"));
542 assert!(recommendations.contains("not available in VT Code"));
543 assert!(recommendations.contains("reportlab"));
544 assert!(recommendations.contains("exec_command"));
545 }
546
547 #[test]
548 fn test_validation_report_formatting() {
549 let mut report = ContainerValidationReport::new();
550
551 report.add_incompatible_skill(
553 "pdf-report-generator".to_string(),
554 "Generate PDFs".to_string(),
555 "Requires container skills".to_string(),
556 );
557
558 report.add_skill_analysis(
559 "spreadsheet-generator".to_string(),
560 ContainerValidationResult {
561 requirement: ContainerSkillsRequirement::RequiredWithFallback,
562 analysis: "Has fallback".to_string(),
563 patterns_found: vec!["execute_code".to_string()],
564 recommendations: vec!["Use fallback".to_string()],
565 should_filter: false,
566 },
567 );
568
569 report.finalize();
570
571 let formatted = report.format_report();
572 assert!(formatted.contains("Container Skills Validation Report"));
573 assert!(formatted.contains("pdf-report-generator"));
574 assert!(formatted.contains("spreadsheet-generator"));
575 assert!(formatted.contains("Incompatible Skills"));
576 assert!(formatted.contains("Skills with Fallbacks"));
577 assert!(formatted.contains("Total Skills Analyzed"));
578 }
579}