Skip to main content

systemprompt_runtime/startup_validation/
mod.rs

1//! Startup configuration and extension validation.
2//!
3//! [`StartupValidator`] composes per-domain validators via
4//! [`DomainConfigRegistry`], aggregates results into a
5//! [`StartupValidationReport`], and is consumed by `systemprompt`'s
6//! boot sequence to gate startup on configuration health.
7
8mod 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}