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::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}