systemprompt_runtime/startup_validation/
mod.rs1mod config_loaders;
9mod display;
10mod extension_validator;
11mod files_validator;
12mod mcp_validator;
13
14use systemprompt_config::{ProfileBootstrap, SkillConfigValidator};
15use systemprompt_logging::services::cli::{
16 BrandColors, render_phase_success, render_phase_warning,
17};
18use systemprompt_logging::{CliService, is_startup_mode};
19use systemprompt_models::Config;
20use systemprompt_models::validators::{
21 AgentConfigValidator, AiConfigValidator, ContentConfigValidator, McpConfigValidator,
22 RateLimitsConfigValidator, ValidationConfigProvider, WebConfigValidator,
23};
24use systemprompt_traits::validation_report::ValidationError;
25use systemprompt_traits::{DomainConfigRegistry, StartupValidationReport, ValidationReport};
26
27use config_loaders::{create_spinner, load_content_config, load_web_config, load_web_metadata};
28use extension_validator::validate_extensions;
29use mcp_validator::validate_mcp_manifests;
30
31pub use display::{display_validation_report, display_validation_warnings};
32pub use files_validator::FilesConfigValidator;
33
34#[derive(Debug)]
35pub struct StartupValidator {
36 registry: DomainConfigRegistry,
37}
38
39impl StartupValidator {
40 pub fn new() -> Self {
41 let mut registry = DomainConfigRegistry::new();
42
43 registry.register(Box::new(FilesConfigValidator::new()));
44 registry.register(Box::new(RateLimitsConfigValidator::new()));
45 registry.register(Box::new(WebConfigValidator::new()));
46 registry.register(Box::new(ContentConfigValidator::new()));
47 registry.register(Box::new(SkillConfigValidator::new()));
48 registry.register(Box::new(AgentConfigValidator::new()));
49 registry.register(Box::new(McpConfigValidator::new()));
50 registry.register(Box::new(AiConfigValidator::new()));
51
52 Self { registry }
53 }
54
55 pub fn validate(&mut self, config: &Config) -> StartupValidationReport {
56 let mut report = StartupValidationReport::new();
57 let verbose = is_startup_mode();
58
59 if let Ok(path) = ProfileBootstrap::get_path() {
60 report = report.with_profile_path(path);
61 }
62
63 if verbose {
64 CliService::section("Validating configuration");
65 }
66
67 let Some(validation_provider) = Self::load_configs(config, &mut report, verbose) else {
68 return report;
69 };
70
71 if self.load_domain_validators(&validation_provider, &mut report, verbose) {
72 return report;
73 }
74
75 self.run_domain_validations(&mut report, verbose);
76
77 validate_mcp_manifests(config, validation_provider.services_config(), &mut report);
78
79 if report.has_errors() {
80 return report;
81 }
82
83 validate_extensions(config, &mut report, verbose);
84
85 if verbose {
86 CliService::output("");
87 }
88
89 report
90 }
91
92 fn load_configs(
93 config: &Config,
94 report: &mut StartupValidationReport,
95 verbose: bool,
96 ) -> Option<ValidationConfigProvider> {
97 let spinner = if verbose {
98 Some(create_spinner("Loading services config"))
99 } else {
100 None
101 };
102 let services_config = match systemprompt_loader::ConfigLoader::load() {
103 Ok(cfg) => {
104 if let Some(s) = spinner {
105 s.finish_and_clear();
106 }
107 if verbose {
108 CliService::phase_success("Services config", Some("includes merged"));
109 }
110 cfg
111 },
112 Err(e) => {
113 if let Some(s) = spinner {
114 s.finish_and_clear();
115 }
116 CliService::error(&format!("Services config: {}", e));
117 let mut domain_report = ValidationReport::new("services");
118 domain_report.add_error(ValidationError::new(
119 "services_config",
120 format!("Failed to load: {}", e),
121 ));
122 report.add_domain(domain_report);
123 return None;
124 },
125 };
126
127 let mut provider = ValidationConfigProvider::new(config.clone(), services_config);
128
129 provider = load_content_config(provider, verbose);
130 provider = load_web_config(config, provider, verbose);
131 provider = load_web_metadata(config, provider, verbose);
132
133 Some(provider)
134 }
135
136 fn load_domain_validators(
137 &mut self,
138 provider: &ValidationConfigProvider,
139 report: &mut StartupValidationReport,
140 verbose: bool,
141 ) -> bool {
142 if verbose {
143 CliService::output("");
144 CliService::output(&format!(
145 "{} {}",
146 BrandColors::primary("▸"),
147 BrandColors::white_bold("Validating domains")
148 ));
149 }
150
151 for validator in self.registry.validators_mut() {
152 let domain_id = validator.domain_id();
153 let spinner = if verbose {
154 Some(create_spinner(&format!("Loading {}", domain_id)))
155 } else {
156 None
157 };
158
159 match validator.load(provider) {
160 Ok(()) => {
161 if let Some(s) = spinner {
162 s.finish_and_clear();
163 }
164 },
165 Err(e) => {
166 if let Some(s) = spinner {
167 s.finish_and_clear();
168 }
169 CliService::output(&format!(
170 " {} [{}] {}",
171 BrandColors::stopped("✗"),
172 domain_id,
173 e
174 ));
175
176 let mut domain_report = ValidationReport::new(domain_id);
177 domain_report.add_error(ValidationError::new(
178 format!("{}_config", domain_id),
179 format!("Failed to load: {}", e),
180 ));
181 report.add_domain(domain_report);
182 },
183 }
184 }
185
186 report.has_errors()
187 }
188
189 fn run_domain_validations(&self, report: &mut StartupValidationReport, verbose: bool) {
190 for validator in self.registry.validators_sorted() {
191 let domain_id = validator.domain_id();
192 let spinner = if verbose {
193 Some(create_spinner(&format!("Validating {}", domain_id)))
194 } else {
195 None
196 };
197
198 match validator.validate() {
199 Ok(domain_report) => {
200 if let Some(s) = spinner {
201 s.finish_and_clear();
202 }
203 if verbose {
204 Self::print_domain_result(&domain_report, domain_id);
205 }
206 report.add_domain(domain_report);
207 },
208 Err(e) => {
209 if let Some(s) = spinner {
210 s.finish_and_clear();
211 }
212 CliService::output(&format!(
213 " {} [{}] {}",
214 BrandColors::stopped("✗"),
215 domain_id,
216 e
217 ));
218
219 let mut domain_report = ValidationReport::new(domain_id);
220 domain_report.add_error(ValidationError::new(
221 format!("{}_validation", domain_id),
222 format!("Validation error: {}", e),
223 ));
224 report.add_domain(domain_report);
225 },
226 }
227 }
228 }
229
230 fn print_domain_result(domain_report: &ValidationReport, domain_id: &str) {
231 if domain_report.has_errors() {
232 CliService::output(&format!(
233 " {} [{}] {} error(s)",
234 BrandColors::stopped("✗"),
235 domain_id,
236 domain_report.errors.len()
237 ));
238 } else if domain_report.has_warnings() {
239 render_phase_warning(
240 &format!("[{}]", domain_id),
241 Some(&format!("{} warning(s)", domain_report.warnings.len())),
242 );
243 } else {
244 render_phase_success(&format!("[{}]", domain_id), Some("valid"));
245 }
246 }
247}
248
249impl Default for StartupValidator {
250 fn default() -> Self {
251 Self::new()
252 }
253}