systemprompt_runtime/startup_validation/
mod.rs1mod 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}