Added cmd to validate bundled extensions json (#7217)

This commit is contained in:
Zane
2026-02-17 09:47:48 -08:00
committed by GitHub
parent ff3ad4349d
commit fea34accc2
4 changed files with 290 additions and 0 deletions
+1
View File
@@ -20,6 +20,7 @@ pub(crate) mod subagent_handler;
pub(crate) mod subagent_task_config;
mod tool_execution;
pub mod types;
pub mod validate_extensions;
pub use agent::{Agent, AgentConfig, AgentEvent, ExtensionLoadResult, GoosePlatform};
pub use container::Container;
@@ -0,0 +1,247 @@
use crate::agents::ExtensionConfig;
use anyhow::Result;
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct BundledExtensionEntry {
id: String,
name: String,
#[serde(rename = "type")]
extension_type: String,
#[allow(dead_code)]
#[serde(default)]
enabled: bool,
}
pub fn validate_bundled_extensions(path: &Path) -> Result<String> {
let content = std::fs::read_to_string(path)?;
let raw_entries: Vec<serde_json::Value> = serde_json::from_str(&content)?;
let total = raw_entries.len();
let mut errors: Vec<String> = Vec::new();
for (index, entry) in raw_entries.iter().enumerate() {
let meta: BundledExtensionEntry = match serde_json::from_value(entry.clone()) {
Ok(m) => m,
Err(e) => {
let id = entry
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let name = entry
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(format!(
"[{index}] {name} (id={id}): missing required metadata fields: {e}"
));
continue;
}
};
// Check for common field name mistakes before full deserialization
if meta.extension_type == "streamable_http"
&& entry.get("url").is_some()
&& entry.get("uri").is_none()
{
errors.push(format!(
"[{index}] {} (id={}): has \"url\" field but streamable_http expects \"uri\" — did you mean \"uri\"?",
meta.name, meta.id
));
continue;
}
if meta.extension_type == "stdio" && entry.get("cmd").is_none() {
errors.push(format!(
"[{index}] {} (id={}): stdio extension is missing required \"cmd\" field",
meta.name, meta.id
));
continue;
}
if let Err(e) = serde_json::from_value::<ExtensionConfig>(entry.clone()) {
errors.push(format!("[{index}] {} (id={}): {e}", meta.name, meta.id));
}
}
if errors.is_empty() {
Ok(format!("✓ All {total} extensions validated successfully."))
} else {
let mut output = format!("✗ Found {} error(s) in {total} extensions:\n", errors.len());
for error in &errors {
output.push_str(&format!("\n {error}"));
}
anyhow::bail!("{output}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn write_json(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
f.write_all(content.as_bytes()).unwrap();
f
}
#[test]
fn test_valid_builtin() {
let f = write_json(
r#"[{
"id": "developer",
"name": "developer",
"display_name": "Developer",
"description": "Dev tools",
"enabled": true,
"type": "builtin",
"timeout": 300,
"bundled": true
}]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_ok());
assert!(result.unwrap().contains("1 extensions validated"));
}
#[test]
fn test_valid_stdio() {
let f = write_json(
r#"[{
"id": "googledrive",
"name": "Google Drive",
"description": "Google Drive integration",
"enabled": false,
"type": "stdio",
"cmd": "uvx",
"args": ["mcp_gdrive@latest"],
"env_keys": [],
"timeout": 300,
"bundled": true
}]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_ok());
}
#[test]
fn test_valid_streamable_http() {
let f = write_json(
r#"[{
"id": "asana",
"name": "Asana",
"display_name": "Asana",
"description": "Manage Asana tasks",
"enabled": false,
"type": "streamable_http",
"uri": "https://mcp.asana.com/mcp",
"env_keys": [],
"timeout": 300,
"bundled": true
}]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_ok());
}
#[test]
fn test_invalid_type_http() {
let f = write_json(
r#"[{
"id": "asana",
"name": "Asana",
"description": "Manage Asana tasks",
"enabled": false,
"type": "http",
"uri": "https://mcp.asana.com/mcp",
"timeout": 300,
"bundled": true
}]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Asana"));
assert!(err.contains("unknown variant `http`"));
}
#[test]
fn test_url_instead_of_uri() {
let f = write_json(
r#"[{
"id": "neighborhood",
"name": "Neighborhood",
"description": "Neighborhood tools",
"enabled": false,
"type": "streamable_http",
"url": "https://example.com/mcp",
"timeout": 300,
"bundled": true
}]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("uri"));
}
#[test]
fn test_missing_cmd_for_stdio() {
let f = write_json(
r#"[{
"id": "test",
"name": "Test",
"description": "Test extension",
"enabled": false,
"type": "stdio",
"args": [],
"timeout": 300,
"bundled": true
}]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cmd"));
}
#[test]
fn test_valid_entries_before_invalid_still_pass() {
let f = write_json(
r#"[
{
"id": "developer",
"name": "developer",
"description": "Dev tools",
"enabled": true,
"type": "builtin",
"timeout": 300,
"bundled": true
},
{
"id": "bad",
"name": "Bad Extension",
"description": "This one is broken",
"enabled": false,
"type": "http",
"uri": "https://example.com",
"timeout": 300,
"bundled": true
}
]"#,
);
let result = validate_bundled_extensions(f.path());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("1 error(s)"));
assert!(err.contains("Bad Extension"));
}
#[test]
fn test_empty_array_is_valid() {
let f = write_json("[]");
let result = validate_bundled_extensions(f.path());
assert!(result.is_ok());
assert!(result.unwrap().contains("0 extensions validated"));
}
}