systemprompt_cli/commands/admin/agents/
validate.rs1use 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}