vtcode_core/plugins/
validation.rs1use 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
14pub struct PluginValidator;
16
17impl PluginValidator {
18 pub fn validate_manifest(manifest: &PluginManifest) -> PluginResult<()> {
20 if manifest.name.is_empty() {
22 return Err(PluginError::ManifestValidationError(
23 "Plugin name is required".to_string(),
24 ));
25 }
26
27 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 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 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 pub async fn validate_plugin_structure(plugin_path: &Path) -> PluginResult<()> {
57 crate::plugins::PluginTemplate::validate_plugin_structure(plugin_path).await
59 }
60
61 fn is_valid_plugin_name(name: &str) -> bool {
63 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 fn is_valid_version(version: &str) -> bool {
73 let parts: Vec<&str> = version.split('.').collect();
75 if parts.len() < 3 {
76 return false;
77 }
78
79 parts.iter().all(|part| {
81 let clean_part = part.split('-').next().unwrap_or(part);
83 clean_part.chars().all(|c| c.is_ascii_digit())
84 })
85 }
86
87 pub fn validate_plugin_security(manifest: &PluginManifest) -> PluginResult<()> {
89 if let Some(mcp_servers) = &manifest.mcp_servers {
91 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 }
106 }
107 }
108
109 Ok(())
110 }
111
112 fn is_dangerous_command(command: &str) -> bool {
114 command_might_be_dangerous(&[command.to_string()])
115 }
116}
117
118pub struct PluginDebugger;
120
121impl PluginDebugger {
122 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 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 pub fn validate_and_debug_manifest(manifest: &PluginManifest) -> Result<String> {
201 let mut issues = Vec::new();
202
203 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 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
228pub async fn handle_plugin_validate(path: &Path) -> Result<()> {
230 if !path.exists() {
232 anyhow::bail!("Plugin path does not exist: {}", path.display());
233 }
234
235 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 let validation_output = PluginDebugger::validate_and_debug_manifest(&manifest)?;
246 tracing::info!(output = %validation_output, "plugin validation result");
247
248 Ok(())
249}