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