1use crate::skills::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 pub analysis: String,
30 pub patterns_found: Vec<String>,
32 pub recommendations: Vec<String>,
34 pub 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 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 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 `unified_exec` instead of Anthropic container skills.".to_string(),
101 ],
102 should_filter: false,
103 };
104 }
105
106 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 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 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 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 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, )
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, )
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 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 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 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 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#[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#[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 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 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 assert_eq!(result.requirement, ContainerSkillsRequirement::NotRequired);
538 assert!(!result.should_filter);
539 assert!(result.patterns_found.is_empty());
541 }
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 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 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}