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