Skip to main content

systemprompt_runtime/startup_validation/
mod.rs

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