Skip to main content

systemprompt_cli/commands/admin/agents/
validate.rs

1//! `admin agents validate` command checking card, provider, and MCP references.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result};
7use clap::Args;
8
9use super::types::{ValidationIssue, ValidationOutput};
10use crate::CliConfig;
11use crate::shared::CommandOutput;
12use systemprompt_config::SecretsBootstrap;
13use systemprompt_loader::ConfigLoader;
14use systemprompt_models::secrets::Secrets;
15use systemprompt_models::services::ProviderRegistry;
16use systemprompt_models::{AgentConfig, ServicesConfig};
17
18#[derive(Debug, Args)]
19pub struct ValidateArgs {
20    #[arg(help = "Agent name to validate (optional)")]
21    pub name: Option<String>,
22}
23
24#[derive(Debug)]
25pub struct ValidationSources<'a> {
26    pub services_config: &'a ServicesConfig,
27    pub registry: &'a ProviderRegistry,
28    pub secrets: Option<&'a Secrets>,
29}
30
31pub(super) fn execute(args: &ValidateArgs, _config: &CliConfig) -> Result<CommandOutput> {
32    let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
33    let registry = &services_config.providers;
34    let secrets = SecretsBootstrap::get().ok();
35    let sources = ValidationSources {
36        services_config: &services_config,
37        registry,
38        secrets,
39    };
40
41    let mut errors = Vec::new();
42    let mut warnings = Vec::new();
43    let mut agents_checked = 0;
44
45    let agents_to_check: Vec<(&String, &AgentConfig)> = match &args.name {
46        Some(name) => {
47            let agent = services_config
48                .agents
49                .get(name)
50                .ok_or_else(|| anyhow::anyhow!("Agent '{}' not found", name))?;
51            vec![(name, agent)]
52        },
53        None => services_config.agents.iter().collect(),
54    };
55
56    for (name, agent) in agents_to_check {
57        agents_checked += 1;
58        check_basics(name, agent, &mut errors, &mut warnings);
59        check_provider(name, agent, &sources, &mut errors);
60        check_mcp_references(name, agent, &services_config, &mut errors);
61    }
62
63    let output = ValidationOutput {
64        valid: errors.is_empty(),
65        items_checked: agents_checked,
66        errors,
67        warnings,
68    };
69
70    Ok(CommandOutput::card_value("Validation Results", &output))
71}
72
73pub fn check_basics(
74    name: &str,
75    agent: &AgentConfig,
76    errors: &mut Vec<ValidationIssue>,
77    warnings: &mut Vec<ValidationIssue>,
78) {
79    if let Err(e) = agent.validate(name) {
80        errors.push(ValidationIssue {
81            source: name.to_owned(),
82            message: e.to_string(),
83            suggestion: None,
84        });
85    }
86
87    if agent.port == 0 {
88        errors.push(ValidationIssue {
89            source: name.to_owned(),
90            message: "Port cannot be 0".to_owned(),
91            suggestion: None,
92        });
93    }
94
95    if agent.card.display_name.is_empty() {
96        warnings.push(ValidationIssue {
97            source: name.to_owned(),
98            message: "Display name is empty".to_owned(),
99            suggestion: None,
100        });
101    }
102
103    if agent.card.description.is_empty() {
104        warnings.push(ValidationIssue {
105            source: name.to_owned(),
106            message: "Description is empty".to_owned(),
107            suggestion: None,
108        });
109    }
110
111    if agent.enabled && agent.metadata.provider.is_none() {
112        warnings.push(ValidationIssue {
113            source: name.to_owned(),
114            message: "Enabled agent has no AI provider configured".to_owned(),
115            suggestion: None,
116        });
117    }
118}
119
120pub fn check_provider(
121    name: &str,
122    agent: &AgentConfig,
123    sources: &ValidationSources<'_>,
124    errors: &mut Vec<ValidationIssue>,
125) {
126    if !agent.enabled {
127        return;
128    }
129    let Some(provider_name) = &agent.metadata.provider else {
130        return;
131    };
132
133    let Some(provider_config) = sources.services_config.ai.providers.get(provider_name) else {
134        errors.push(ValidationIssue {
135            source: name.to_owned(),
136            message: format!(
137                "Provider '{}' is not configured in ai.providers",
138                provider_name
139            ),
140            suggestion: None,
141        });
142        return;
143    };
144
145    if !provider_config.enabled {
146        errors.push(ValidationIssue {
147            source: name.to_owned(),
148            message: format!(
149                "Provider '{}' is disabled in AI config (set enabled: true)",
150                provider_name
151            ),
152            suggestion: None,
153        });
154    }
155
156    match sources.registry.find_provider(provider_name) {
157        None => {
158            errors.push(ValidationIssue {
159                source: name.to_owned(),
160                message: format!(
161                    "Provider '{}' has no connectivity entry in the services provider registry",
162                    provider_name
163                ),
164                suggestion: None,
165            });
166        },
167        Some(entry) => {
168            let secret_name = entry.api_key_secret.as_str();
169            let key_present = sources
170                .secrets
171                .and_then(|s| s.get(secret_name))
172                .is_some_and(|k| !k.is_empty());
173            if !key_present {
174                errors.push(ValidationIssue {
175                    source: name.to_owned(),
176                    message: format!(
177                        "No API key configured for provider '{}' (secret '{}' not found)",
178                        provider_name, secret_name
179                    ),
180                    suggestion: None,
181                });
182            }
183        },
184    }
185}
186
187pub fn check_mcp_references(
188    name: &str,
189    agent: &AgentConfig,
190    services_config: &ServicesConfig,
191    errors: &mut Vec<ValidationIssue>,
192) {
193    for mcp_server in &agent.metadata.mcp_servers.include {
194        if !services_config.mcp_servers.contains_key(mcp_server) {
195            errors.push(ValidationIssue {
196                source: name.to_owned(),
197                message: format!("Referenced MCP server '{}' not found in config", mcp_server),
198                suggestion: None,
199            });
200        }
201    }
202}