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