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