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::{ProfileBootstrap, SecretsBootstrap};
13use systemprompt_loader::ConfigLoader;
14use systemprompt_models::profile::ProviderRegistry;
15use systemprompt_models::secrets::Secrets;
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 = &ProfileBootstrap::get()
34        .context("Failed to access bootstrapped profile for provider registry")?
35        .providers;
36    let secrets = SecretsBootstrap::get().ok();
37    let sources = ValidationSources {
38        services_config: &services_config,
39        registry,
40        secrets,
41    };
42
43    let mut errors = Vec::new();
44    let mut warnings = Vec::new();
45    let mut agents_checked = 0;
46
47    let agents_to_check: Vec<(&String, &AgentConfig)> = match &args.name {
48        Some(name) => {
49            let agent = services_config
50                .agents
51                .get(name)
52                .ok_or_else(|| anyhow::anyhow!("Agent '{}' not found", name))?;
53            vec![(name, agent)]
54        },
55        None => services_config.agents.iter().collect(),
56    };
57
58    for (name, agent) in agents_to_check {
59        agents_checked += 1;
60        check_basics(name, agent, &mut errors, &mut warnings);
61        check_provider(name, agent, &sources, &mut errors);
62        check_mcp_references(name, agent, &services_config, &mut errors);
63    }
64
65    let output = ValidationOutput {
66        valid: errors.is_empty(),
67        items_checked: agents_checked,
68        errors,
69        warnings,
70    };
71
72    Ok(CommandOutput::card_value("Validation Results", &output))
73}
74
75pub fn check_basics(
76    name: &str,
77    agent: &AgentConfig,
78    errors: &mut Vec<ValidationIssue>,
79    warnings: &mut Vec<ValidationIssue>,
80) {
81    if let Err(e) = agent.validate(name) {
82        errors.push(ValidationIssue {
83            source: name.to_owned(),
84            message: e.to_string(),
85            suggestion: None,
86        });
87    }
88
89    if agent.port == 0 {
90        errors.push(ValidationIssue {
91            source: name.to_owned(),
92            message: "Port cannot be 0".to_owned(),
93            suggestion: None,
94        });
95    }
96
97    if agent.card.display_name.is_empty() {
98        warnings.push(ValidationIssue {
99            source: name.to_owned(),
100            message: "Display name is empty".to_owned(),
101            suggestion: None,
102        });
103    }
104
105    if agent.card.description.is_empty() {
106        warnings.push(ValidationIssue {
107            source: name.to_owned(),
108            message: "Description is empty".to_owned(),
109            suggestion: None,
110        });
111    }
112
113    if agent.enabled && agent.metadata.provider.is_none() {
114        warnings.push(ValidationIssue {
115            source: name.to_owned(),
116            message: "Enabled agent has no AI provider configured".to_owned(),
117            suggestion: None,
118        });
119    }
120}
121
122pub fn check_provider(
123    name: &str,
124    agent: &AgentConfig,
125    sources: &ValidationSources<'_>,
126    errors: &mut Vec<ValidationIssue>,
127) {
128    if !agent.enabled {
129        return;
130    }
131    let Some(provider_name) = &agent.metadata.provider else {
132        return;
133    };
134
135    let Some(provider_config) = sources.services_config.ai.providers.get(provider_name) else {
136        errors.push(ValidationIssue {
137            source: name.to_owned(),
138            message: format!(
139                "Provider '{}' is not configured in ai.providers",
140                provider_name
141            ),
142            suggestion: None,
143        });
144        return;
145    };
146
147    if !provider_config.enabled {
148        errors.push(ValidationIssue {
149            source: name.to_owned(),
150            message: format!(
151                "Provider '{}' is disabled in AI config (set enabled: true)",
152                provider_name
153            ),
154            suggestion: None,
155        });
156    }
157
158    match sources.registry.find_provider(provider_name) {
159        None => {
160            errors.push(ValidationIssue {
161                source: name.to_owned(),
162                message: format!(
163                    "Provider '{}' has no connectivity entry in the profile registry",
164                    provider_name
165                ),
166                suggestion: None,
167            });
168        },
169        Some(entry) => {
170            let secret_name = entry.api_key_secret.as_str();
171            let key_present = sources
172                .secrets
173                .and_then(|s| s.get(secret_name))
174                .is_some_and(|k| !k.is_empty());
175            if !key_present {
176                errors.push(ValidationIssue {
177                    source: name.to_owned(),
178                    message: format!(
179                        "No API key configured for provider '{}' (secret '{}' not found)",
180                        provider_name, secret_name
181                    ),
182                    suggestion: None,
183                });
184            }
185        },
186    }
187}
188
189pub fn check_mcp_references(
190    name: &str,
191    agent: &AgentConfig,
192    services_config: &ServicesConfig,
193    errors: &mut Vec<ValidationIssue>,
194) {
195    for mcp_server in &agent.metadata.mcp_servers.include {
196        if !services_config.mcp_servers.contains_key(mcp_server) {
197            errors.push(ValidationIssue {
198                source: name.to_owned(),
199                message: format!("Referenced MCP server '{}' not found in config", mcp_server),
200                suggestion: None,
201            });
202        }
203    }
204}