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