Skip to main content

vtcode_skills/
container_validation.rs

1//! Container Skills Validation
2//!
3//! Detects and validates skills that require Anthropic's container skills feature,
4//! which is not supported in VT Code. Provides early warnings and filtering
5//! to prevent false positives where skills load but cannot execute properly.
6
7use crate::types::Skill;
8use serde::{Deserialize, Serialize};
9
10/// Container skills requirement detection
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub enum ContainerSkillsRequirement {
13    /// Skill requires container skills (not supported in VT Code)
14    Required,
15    /// Skill provides fallback alternatives
16    RequiredWithFallback,
17    /// Skill does not require container skills
18    NotRequired,
19    /// Cannot determine requirement (default to safe)
20    Unknown,
21}
22
23/// Container skills validation result
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ContainerValidationResult {
26    /// Whether container skills are required
27    pub requirement: ContainerSkillsRequirement,
28    /// Detailed analysis
29    analysis: String,
30    /// Specific patterns found
31    patterns_found: Vec<String>,
32    /// Recommendations for users
33    pub recommendations: Vec<String>,
34    /// Whether skill should be filtered out
35    should_filter: bool,
36}
37
38/// Detects container skills requirements in skill instructions
39pub struct ContainerSkillsValidator {
40    /// Patterns that indicate container skills usage
41    container_patterns: Vec<String>,
42    /// Patterns that indicate fallback alternatives
43    fallback_patterns: Vec<String>,
44    /// Patterns that indicate VT Code incompatibility
45    incompatibility_patterns: Vec<String>,
46}
47
48impl Default for ContainerSkillsValidator {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl ContainerSkillsValidator {
55    /// Create a new container skills validator
56    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    /// Analyze a skill for container skills requirements
79    pub fn analyze_skill(&self, skill: &Skill) -> ContainerValidationResult {
80        // Honor explicit manifest flags first; avoids keyword false-positives
81        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        // Check if skill uses VT Code native features (not container skills)
108        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        // Check for container skills patterns
123        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        // Check for explicit incompatibility statements
132        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        // Check for fallback alternatives
141        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        // Determine requirement level and recommendations
150        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, // Don't filter - provide fallback guidance
158            )
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, // Filter out - no fallback available
173            )
174        } else {
175            (
176                ContainerSkillsRequirement::NotRequired,
177                format!("Skill '{}' does not require container skills.", skill.name()),
178                false,
179            )
180        };
181
182        // Generate recommendations with enhanced user guidance
183        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            // Provide specific alternatives based on skill type
192            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    /// Batch analyze multiple skills
246    pub fn analyze_skills(&self, skills: &[Skill]) -> Vec<ContainerValidationResult> {
247        skills.iter().map(|skill| self.analyze_skill(skill)).collect()
248    }
249
250    /// Filter skills that require container skills without fallback
251    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/// Information about incompatible skills
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct IncompatibleSkillInfo {
277    name: String,
278    description: String,
279    reason: String,
280    recommendations: Vec<String>,
281}
282
283/// Comprehensive validation report for all skills
284#[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                // Treat unknown as compatible for safety
351                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        // Test skill with container usage
437        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        // vtcode_native=true means native execution, not container skills
499        assert_eq!(result.requirement, ContainerSkillsRequirement::NotRequired);
500        assert!(!result.should_filter);
501        // No patterns found for vtcode_native skills (early return)
502        assert!(result.patterns_found.is_empty());
503        // No recommendations for vtcode_native skills (early return)
504    }
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        // Test enhanced recommendations
540        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        // Add test data
552        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}