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