Skip to main content

vtcode_core/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::skills::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    pub analysis: String,
30    /// Specific patterns found
31    pub patterns_found: Vec<String>,
32    /// Recommendations for users
33    pub recommendations: Vec<String>,
34    /// Whether skill should be filtered out
35    pub 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 unified_exec".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 `unified_exec` instead of Anthropic container skills.".to_string(),
101                ],
102                should_filter: false,
103            };
104        }
105
106        // Check if skill uses VT Code native features (not container skills)
107        if let Some(true) = skill.manifest.vtcode_native {
108            return ContainerValidationResult {
109                requirement: ContainerSkillsRequirement::NotRequired,
110                analysis: "Skill uses VT Code native features (not container skills)".to_string(),
111                patterns_found: vec![],
112                recommendations: vec![],
113                should_filter: false,
114            };
115        }
116
117        let instructions = &skill.instructions;
118        let mut patterns_found = Vec::new();
119        let mut recommendations = Vec::new();
120
121        // Check for container skills patterns
122        let mut has_container_usage = false;
123        for pattern in &self.container_patterns {
124            if instructions.contains(pattern) {
125                patterns_found.push(pattern.clone());
126                has_container_usage = true;
127            }
128        }
129
130        // Check for explicit incompatibility statements
131        let mut has_incompatibility = false;
132        for pattern in &self.incompatibility_patterns {
133            if instructions.contains(pattern) {
134                patterns_found.push(pattern.clone());
135                has_incompatibility = true;
136            }
137        }
138
139        // Check for fallback alternatives
140        let mut has_fallback = false;
141        for pattern in &self.fallback_patterns {
142            if instructions.contains(pattern) {
143                patterns_found.push(pattern.clone());
144                has_fallback = true;
145            }
146        }
147
148        // Determine requirement level and recommendations
149        let (requirement, analysis, should_filter) = if has_incompatibility {
150            (
151                ContainerSkillsRequirement::RequiredWithFallback,
152                format!(
153                    "Skill '{}' explicitly states it requires Anthropic container skills which VT Code does not support. However, it provides fallback alternatives.",
154                    skill.name()
155                ),
156                false, // Don't filter - provide fallback guidance
157            )
158        } else if has_container_usage && has_fallback {
159            (
160                ContainerSkillsRequirement::RequiredWithFallback,
161                format!(
162                    "Skill '{}' uses container skills but provides VT Code-compatible alternatives.",
163                    skill.name()
164                ),
165                false,
166            )
167        } else if has_container_usage {
168            (
169                ContainerSkillsRequirement::Required,
170                format!(
171                    "Skill '{}' requires Anthropic container skills which are not supported in VT Code.",
172                    skill.name()
173                ),
174                true, // Filter out - no fallback available
175            )
176        } else {
177            (
178                ContainerSkillsRequirement::NotRequired,
179                format!(
180                    "Skill '{}' does not require container skills.",
181                    skill.name()
182                ),
183                false,
184            )
185        };
186
187        // Generate recommendations with enhanced user guidance
188        if requirement == ContainerSkillsRequirement::Required {
189            recommendations.push("This skill requires Anthropic's container skills feature which is not available in VT Code.".to_string());
190            recommendations.push("".to_string());
191            recommendations.push("Consider these VT Code-compatible alternatives:".to_string());
192
193            // Provide specific alternatives based on skill type
194            if skill.name().contains("pdf") || skill.name().contains("report") {
195                recommendations.push(
196                    "  1. Use unified_exec with action='code' and Python libraries: reportlab, fpdf2, or weasyprint"
197                        .to_string(),
198                );
199                recommendations.push("  2. Install: pip install reportlab".to_string());
200                recommendations.push("  3. Use Python code execution to generate PDFs".to_string());
201            } else if skill.name().contains("spreadsheet") || skill.name().contains("excel") {
202                recommendations.push(
203                    "  1. Use unified_exec with action='code' and Python libraries: openpyxl, xlsxwriter, or pandas"
204                        .to_string(),
205                );
206                recommendations.push("  2. Install: pip install openpyxl xlsxwriter".to_string());
207                recommendations
208                    .push("  3. Use Python code execution to create spreadsheets".to_string());
209            } else if skill.name().contains("doc") || skill.name().contains("word") {
210                recommendations.push(
211                    "  1. Use unified_exec with action='code' and Python libraries: python-docx or docxtpl"
212                        .to_string(),
213                );
214                recommendations.push("  2. Install: pip install python-docx".to_string());
215                recommendations
216                    .push("  3. Use Python code execution to generate documents".to_string());
217            } else if skill.name().contains("presentation") || skill.name().contains("ppt") {
218                recommendations.push(
219                    "  1. Use unified_exec with action='code' and Python libraries: python-pptx"
220                        .to_string(),
221                );
222                recommendations.push("  2. Install: pip install python-pptx".to_string());
223                recommendations
224                    .push("  3. Use Python code execution to create presentations".to_string());
225            } else {
226                recommendations.push(
227                    "  1. Use unified_exec with action='code' and appropriate Python libraries"
228                        .to_string(),
229                );
230                recommendations.push(
231                    "  2. Search for VT Code-compatible skills in the documentation".to_string(),
232                );
233            }
234
235            recommendations.push("".to_string());
236            recommendations.push(
237                "Learn more about VT Code's code execution in the documentation.".to_string(),
238            );
239            recommendations.push("Official Anthropic container skills documentation: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview".to_string());
240        } else if requirement == ContainerSkillsRequirement::RequiredWithFallback {
241            recommendations.push(
242                "This skill uses container skills but provides VT Code-compatible alternatives."
243                    .to_string(),
244            );
245            recommendations
246                .push("Use the fallback instructions in the skill documentation.".to_string());
247            recommendations
248                .push("Look for sections marked 'Option 2' or 'VT Code Alternative'.".to_string());
249            recommendations.push(
250                "The skill instructions contain working examples using legacy `execute_code`; map them to `unified_exec` with action='code' in VT Code.".to_string(),
251            );
252        }
253
254        ContainerValidationResult {
255            requirement,
256            analysis,
257            patterns_found,
258            recommendations,
259            should_filter,
260        }
261    }
262
263    /// Batch analyze multiple skills
264    pub fn analyze_skills(&self, skills: &[Skill]) -> Vec<ContainerValidationResult> {
265        skills
266            .iter()
267            .map(|skill| self.analyze_skill(skill))
268            .collect()
269    }
270
271    /// Filter skills that require container skills without fallback
272    pub fn filter_incompatible_skills(
273        &self,
274        skills: Vec<Skill>,
275    ) -> (Vec<Skill>, Vec<IncompatibleSkillInfo>) {
276        let mut compatible_skills = Vec::new();
277        let mut incompatible_skills = Vec::new();
278
279        for skill in skills {
280            let analysis = self.analyze_skill(&skill);
281
282            if analysis.should_filter {
283                incompatible_skills.push(IncompatibleSkillInfo {
284                    name: skill.name().to_string(),
285                    description: skill.description().to_string(),
286                    reason: analysis.analysis,
287                    recommendations: analysis.recommendations,
288                });
289            } else {
290                compatible_skills.push(skill);
291            }
292        }
293
294        (compatible_skills, incompatible_skills)
295    }
296}
297
298/// Information about incompatible skills
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct IncompatibleSkillInfo {
301    pub name: String,
302    pub description: String,
303    pub reason: String,
304    pub recommendations: Vec<String>,
305}
306
307/// Comprehensive validation report for all skills
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ContainerValidationReport {
310    pub total_skills_analyzed: usize,
311    pub compatible_skills: Vec<String>,
312    pub incompatible_skills: Vec<IncompatibleSkillInfo>,
313    pub skills_with_fallbacks: Vec<SkillWithFallback>,
314    pub summary: ValidationSummary,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct SkillWithFallback {
319    pub name: String,
320    pub description: String,
321    pub fallback_description: String,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ValidationSummary {
326    pub total_compatible: usize,
327    pub total_incompatible: usize,
328    pub total_with_fallbacks: usize,
329    pub recommendation: String,
330}
331
332impl ContainerValidationReport {
333    pub fn new() -> Self {
334        Self {
335            total_skills_analyzed: 0,
336            compatible_skills: Vec::new(),
337            incompatible_skills: Vec::new(),
338            skills_with_fallbacks: Vec::new(),
339            summary: ValidationSummary {
340                total_compatible: 0,
341                total_incompatible: 0,
342                total_with_fallbacks: 0,
343                recommendation: String::new(),
344            },
345        }
346    }
347
348    pub fn add_skill_analysis(&mut self, skill_name: String, analysis: ContainerValidationResult) {
349        self.total_skills_analyzed += 1;
350
351        match analysis.requirement {
352            ContainerSkillsRequirement::NotRequired => {
353                self.compatible_skills.push(skill_name);
354                self.summary.total_compatible += 1;
355            }
356            ContainerSkillsRequirement::Required => {
357                self.incompatible_skills.push(IncompatibleSkillInfo {
358                    name: skill_name.clone(),
359                    description: "Container skills required".to_string(),
360                    reason: analysis.analysis,
361                    recommendations: analysis.recommendations,
362                });
363                self.summary.total_incompatible += 1;
364            }
365            ContainerSkillsRequirement::RequiredWithFallback => {
366                self.skills_with_fallbacks.push(SkillWithFallback {
367                    name: skill_name.clone(),
368                    description: "Container skills with fallback".to_string(),
369                    fallback_description: analysis.recommendations.join(" "),
370                });
371                self.summary.total_with_fallbacks += 1;
372            }
373            ContainerSkillsRequirement::Unknown => {
374                // Treat unknown as compatible for safety
375                self.compatible_skills.push(skill_name);
376                self.summary.total_compatible += 1;
377            }
378        }
379    }
380
381    pub fn add_incompatible_skill(&mut self, name: String, description: String, reason: String) {
382        self.incompatible_skills.push(IncompatibleSkillInfo {
383            name,
384            description,
385            reason,
386            recommendations: vec![
387                "This skill requires Anthropic container skills which are not supported in VT Code."
388                    .to_string(),
389                "Consider using alternative approaches with VT Code's code execution tools."
390                    .to_string(),
391            ],
392        });
393        self.summary.total_incompatible += 1;
394        self.total_skills_analyzed += 1;
395    }
396
397    pub fn finalize(&mut self) {
398        self.summary.recommendation = match (
399            self.summary.total_incompatible,
400            self.summary.total_with_fallbacks,
401        ) {
402            (0, 0) => "All skills are fully compatible with VT Code.".to_string(),
403            (0, _) => format!(
404                "{} skills have container skills dependencies but provide VT Code-compatible fallbacks.",
405                self.summary.total_with_fallbacks
406            ),
407            (_, 0) => format!(
408                "{} skills require container skills and cannot be used. Consider the suggested alternatives.",
409                self.summary.total_incompatible
410            ),
411            (_, _) => format!(
412                "{} skills require container skills. {} skills have fallbacks. Use alternatives or fallback instructions.",
413                self.summary.total_incompatible, self.summary.total_with_fallbacks
414            ),
415        };
416    }
417
418    pub fn format_report(&self) -> String {
419        let mut output = String::new();
420        output.push_str(" Container Skills Validation Report\n");
421        output.push_str("=====================================\n\n");
422        output.push_str(&format!(
423            "Total Skills Analyzed: {}\n",
424            self.total_skills_analyzed
425        ));
426        output.push_str(&format!("Compatible: {}\n", self.summary.total_compatible));
427        output.push_str(&format!(
428            "With Fallbacks: {}\n",
429            self.summary.total_with_fallbacks
430        ));
431        output.push_str(&format!(
432            "Incompatible: {}\n\n",
433            self.summary.total_incompatible
434        ));
435        output.push_str(&self.summary.recommendation);
436
437        if !self.incompatible_skills.is_empty() {
438            output.push_str("\n\nIncompatible Skills:");
439            for skill in &self.incompatible_skills {
440                output.push_str(&format!("\n  • {} - {}", skill.name, skill.description));
441                for rec in &skill.recommendations {
442                    output.push_str(&format!("\n    {}", rec));
443                }
444            }
445        }
446
447        if !self.skills_with_fallbacks.is_empty() {
448            output.push_str("\n\nSkills with Fallbacks:");
449            for skill in &self.skills_with_fallbacks {
450                output.push_str(&format!("\n  • {} - {}", skill.name, skill.description));
451            }
452        }
453
454        output
455    }
456}
457
458impl Default for ContainerValidationReport {
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::skills::types::{Skill, SkillManifest};
468    use std::path::PathBuf;
469
470    #[test]
471    fn test_container_skills_detection() {
472        let validator = ContainerSkillsValidator::new();
473
474        // Test skill with container usage
475        let manifest = SkillManifest {
476            name: "pdf-report-generator".to_string(),
477            description: "Generate PDFs".to_string(),
478            version: Some("1.0.0".to_string()),
479            author: Some("Test".to_string()),
480            ..Default::default()
481        };
482
483        let instructions = r#"
484        Generate PDF documents using Anthropic's pdf skill.
485
486        ```python
487        response = client.messages.create(
488            model="claude-haiku-4-5",
489            container={
490                "type": "skills",
491                "skills": [{"type": "anthropic", "skill_id": "pdf", "version": "latest"}]
492            },
493            betas=["skills-2025-10-02"]
494        )
495        ```
496        "#;
497
498        let skill = Skill::new(manifest, PathBuf::from("/tmp"), instructions.to_string()).unwrap();
499        let result = validator.analyze_skill(&skill);
500
501        assert_eq!(result.requirement, ContainerSkillsRequirement::Required);
502        assert!(result.should_filter);
503        assert!(!result.patterns_found.is_empty());
504    }
505
506    #[test]
507    fn test_enhanced_validation_with_fallback() {
508        let validator = ContainerSkillsValidator::new();
509
510        let manifest = SkillManifest {
511            name: "spreadsheet-generator".to_string(),
512            description: "Generate spreadsheets".to_string(),
513            version: Some("1.0.0".to_string()),
514            author: Some("Test".to_string()),
515            vtcode_native: Some(true),
516            ..Default::default()
517        };
518
519        let instructions = r#"
520        **vtcode does not currently support Anthropic container skills.** Instead, use:
521
522        ### Option 1: Python Script with openpyxl
523        Use vtcode's `unified_exec` tool with `action=\"code\"` and Python with openpyxl:
524
525        ```python
526        import openpyxl
527        wb = openpyxl.Workbook()
528        # ... create spreadsheet
529        wb.save("output.xlsx")
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        // vtcode_native=true means native execution, not container skills
537        assert_eq!(result.requirement, ContainerSkillsRequirement::NotRequired);
538        assert!(!result.should_filter);
539        // No patterns found for vtcode_native skills (early return)
540        assert!(result.patterns_found.is_empty());
541        // No recommendations for vtcode_native skills (early return)
542    }
543
544    #[test]
545    fn test_enhanced_validation_without_fallback() {
546        let validator = ContainerSkillsValidator::new();
547
548        let manifest = SkillManifest {
549            name: "pdf-report-generator".to_string(),
550            description: "Generate PDFs".to_string(),
551            version: Some("1.0.0".to_string()),
552            author: Some("Test".to_string()),
553            ..Default::default()
554        };
555
556        let instructions = r#"
557        Generate PDF documents using Anthropic's pdf skill.
558
559        ```python
560        response = client.messages.create(
561            model="claude-haiku-4-5",
562            container={
563                "type": "skills",
564                "skills": [{"type": "anthropic", "skill_id": "pdf", "version": "latest"}]
565            },
566            betas=["skills-2025-10-02"]
567        )
568        ```
569        "#;
570
571        let skill = Skill::new(manifest, PathBuf::from("/tmp"), instructions.to_string()).unwrap();
572        let result = validator.analyze_skill(&skill);
573
574        assert_eq!(result.requirement, ContainerSkillsRequirement::Required);
575        assert!(result.should_filter);
576
577        // Test enhanced recommendations
578        let recommendations = result.recommendations.join(" ");
579        assert!(recommendations.contains("container skills"));
580        assert!(recommendations.contains("not available in VT Code"));
581        assert!(recommendations.contains("reportlab"));
582        assert!(recommendations.contains("unified_exec"));
583    }
584
585    #[test]
586    fn test_validation_report_formatting() {
587        let mut report = ContainerValidationReport::new();
588
589        // Add test data
590        report.add_incompatible_skill(
591            "pdf-report-generator".to_string(),
592            "Generate PDFs".to_string(),
593            "Requires container skills".to_string(),
594        );
595
596        report.add_skill_analysis(
597            "spreadsheet-generator".to_string(),
598            ContainerValidationResult {
599                requirement: ContainerSkillsRequirement::RequiredWithFallback,
600                analysis: "Has fallback".to_string(),
601                patterns_found: vec!["execute_code".to_string()],
602                recommendations: vec!["Use fallback".to_string()],
603                should_filter: false,
604            },
605        );
606
607        report.finalize();
608
609        let formatted = report.format_report();
610        assert!(formatted.contains("Container Skills Validation Report"));
611        assert!(formatted.contains("pdf-report-generator"));
612        assert!(formatted.contains("spreadsheet-generator"));
613        assert!(formatted.contains("Incompatible Skills"));
614        assert!(formatted.contains("Skills with Fallbacks"));
615        assert!(formatted.contains("Total Skills Analyzed"));
616    }
617}