Skip to main content

vtcode_core/plugins/
validation.rs

1//! Plugin validation and debugging tools for VT Code
2//!
3//! Provides utilities for validating plugin manifests, structures, and debugging
4//! plugin functionality.
5
6use std::path::Path;
7
8use anyhow::Result;
9
10use crate::command_safety::command_might_be_dangerous;
11use crate::plugins::{PluginError, PluginManifest, PluginResult};
12use crate::utils::file_utils::read_file_with_context;
13
14/// Plugin validator
15pub struct PluginValidator;
16
17impl PluginValidator {
18    /// Validate a plugin manifest
19    pub fn validate_manifest(manifest: &PluginManifest) -> PluginResult<()> {
20        // Validate required fields
21        if manifest.name.is_empty() {
22            return Err(PluginError::ManifestValidationError(
23                "Plugin name is required".to_string(),
24            ));
25        }
26
27        // Validate name format (kebab-case)
28        if !Self::is_valid_plugin_name(&manifest.name) {
29            return Err(PluginError::ManifestValidationError(
30                "Plugin name must be in kebab-case (lowercase with hyphens)".to_string(),
31            ));
32        }
33
34        // Validate version format if present
35        if let Some(version) = &manifest.version
36            && !Self::is_valid_version(version)
37        {
38            return Err(PluginError::ManifestValidationError(
39                "Plugin version must follow semantic versioning (e.g., 1.0.0)".to_string(),
40            ));
41        }
42
43        // Validate author if present
44        if let Some(author) = &manifest.author
45            && author.name.is_empty()
46        {
47            return Err(PluginError::ManifestValidationError(
48                "Plugin author name is required when author is specified".to_string(),
49            ));
50        }
51
52        Ok(())
53    }
54
55    /// Validate a plugin directory structure
56    pub async fn validate_plugin_structure(plugin_path: &Path) -> PluginResult<()> {
57        // Call the function from PluginTemplate
58        crate::plugins::PluginTemplate::validate_plugin_structure(plugin_path).await
59    }
60
61    /// Check if a plugin name is valid (kebab-case)
62    fn is_valid_plugin_name(name: &str) -> bool {
63        // Check if name contains only lowercase letters, numbers, and hyphens
64        name.chars()
65            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
66            && !name.starts_with('-')
67            && !name.ends_with('-')
68            && !name.is_empty()
69    }
70
71    /// Check if a version string is valid semantic version
72    fn is_valid_version(version: &str) -> bool {
73        // Basic semantic version check (X.Y.Z format)
74        let parts: Vec<&str> = version.split('.').collect();
75        if parts.len() < 3 {
76            return false;
77        }
78
79        // Check that each part is numeric
80        parts.iter().all(|part| {
81            // Handle pre-release versions (e.g., 1.0.0-beta.1)
82            let clean_part = part.split('-').next().unwrap_or(part);
83            clean_part.chars().all(|c| c.is_ascii_digit())
84        })
85    }
86
87    /// Validate plugin security (check for dangerous patterns)
88    pub fn validate_plugin_security(manifest: &PluginManifest) -> PluginResult<()> {
89        // Check for potentially dangerous configurations
90        if let Some(mcp_servers) = &manifest.mcp_servers {
91            // If it's an inline configuration, validate the commands
92            match mcp_servers {
93                crate::plugins::manifest::McpServerConfig::Inline(servers) => {
94                    for (name, server) in servers {
95                        if Self::is_dangerous_command(&server.command) {
96                            return Err(PluginError::LoadingError(format!(
97                                "MCP server '{}' uses potentially dangerous command: {}",
98                                name, server.command
99                            )));
100                        }
101                    }
102                }
103                crate::plugins::manifest::McpServerConfig::Path(_) => {
104                    // For path-based configs, we can't validate until loaded
105                }
106            }
107        }
108
109        Ok(())
110    }
111
112    /// Check if a command is potentially dangerous
113    fn is_dangerous_command(command: &str) -> bool {
114        command_might_be_dangerous(&[command.to_string()])
115    }
116}
117
118/// Plugin debugging utilities
119pub struct PluginDebugger;
120
121impl PluginDebugger {
122    /// Print detailed information about a plugin manifest
123    pub fn debug_manifest(manifest: &PluginManifest) -> String {
124        let mut output = String::new();
125
126        output.push_str(&format!("Plugin: {}\n", manifest.name));
127        output.push_str(&format!(
128            "  Version: {}\n",
129            manifest.version.as_deref().unwrap_or("not specified")
130        ));
131        output.push_str(&format!(
132            "  Description: {}\n",
133            manifest.description.as_deref().unwrap_or("not specified")
134        ));
135
136        if let Some(author) = &manifest.author {
137            output.push_str(&format!("  Author: {}\n", author.name));
138            if let Some(email) = &author.email {
139                output.push_str(&format!("    Email: {}\n", email));
140            }
141            if let Some(url) = &author.url {
142                output.push_str(&format!("    URL: {}\n", url));
143            }
144        }
145
146        if let Some(homepage) = &manifest.homepage {
147            output.push_str(&format!("  Homepage: {}\n", homepage));
148        }
149
150        if let Some(repository) = &manifest.repository {
151            output.push_str(&format!("  Repository: {}\n", repository));
152        }
153
154        if let Some(license) = &manifest.license {
155            output.push_str(&format!("  License: {}\n", license));
156        }
157
158        if let Some(keywords) = &manifest.keywords {
159            output.push_str(&format!("  Keywords: {}\n", keywords.join(", ")));
160        }
161
162        // Component counts
163        let commands_count = manifest.commands.as_ref().map_or(0, |c| c.len());
164        let agents_count = manifest.agents.as_ref().map_or(0, |a| a.len());
165        let skills_count = manifest.skills.as_ref().map_or(0, |s| s.len());
166
167        output.push_str("  Components:\n");
168        output.push_str(&format!("    Commands: {}\n", commands_count));
169        output.push_str(&format!("    Agents: {}\n", agents_count));
170        output.push_str(&format!("    Skills: {}\n", skills_count));
171        output.push_str(&format!(
172            "    Hooks: {}\n",
173            if manifest.hooks.is_some() {
174                "yes"
175            } else {
176                "no"
177            }
178        ));
179        output.push_str(&format!(
180            "    MCP Servers: {}\n",
181            if manifest.mcp_servers.is_some() {
182                "yes"
183            } else {
184                "no"
185            }
186        ));
187        output.push_str(&format!(
188            "    LSP Servers: {}\n",
189            if manifest.lsp_servers.is_some() {
190                "yes"
191            } else {
192                "no"
193            }
194        ));
195
196        output
197    }
198
199    /// Validate and debug a plugin manifest with detailed output
200    pub fn validate_and_debug_manifest(manifest: &PluginManifest) -> Result<String> {
201        let mut issues = Vec::new();
202
203        // Run validation checks
204        if let Err(e) = PluginValidator::validate_manifest(manifest) {
205            issues.push(format!("Validation error: {}", e));
206        }
207
208        if let Err(e) = PluginValidator::validate_plugin_security(manifest) {
209            issues.push(format!("Security warning: {}", e));
210        }
211
212        // Create debug output
213        let mut output = Self::debug_manifest(manifest);
214
215        if !issues.is_empty() {
216            output.push_str("\nIssues found:\n");
217            for issue in issues {
218                output.push_str(&format!("  - {}\n", issue));
219            }
220        } else {
221            output.push_str("\nNo issues found.\n");
222        }
223
224        Ok(output)
225    }
226}
227
228/// Plugin validation CLI command handler
229pub async fn handle_plugin_validate(path: &Path) -> Result<()> {
230    // Check if path exists
231    if !path.exists() {
232        anyhow::bail!("Plugin path does not exist: {}", path.display());
233    }
234
235    // Try to load the manifest
236    let manifest_path = path.join(".vtcode-plugin/plugin.json");
237    if !manifest_path.exists() {
238        anyhow::bail!("Plugin manifest not found at: {}", manifest_path.display());
239    }
240
241    let manifest_content = read_file_with_context(&manifest_path, "plugin manifest").await?;
242    let manifest: PluginManifest = serde_json::from_str(&manifest_content)?;
243
244    // Validate the manifest
245    let validation_output = PluginDebugger::validate_and_debug_manifest(&manifest)?;
246    tracing::info!(output = %validation_output, "plugin validation result");
247
248    Ok(())
249}