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