systemprompt_runtime/startup_validation/
extension_validator.rs1use std::path::Path;
7use systemprompt_config::ProfileBootstrap;
8use systemprompt_extension::ExtensionRegistry;
9use systemprompt_logging::CliService;
10use systemprompt_logging::services::cli::{BrandColors, render_phase_success};
11use systemprompt_models::{AppPaths, Config};
12use systemprompt_traits::validation_report::ValidationError;
13use systemprompt_traits::{StartupValidationReport, ValidationReport};
14
15use super::config_loaders::load_extension_config;
16
17pub(super) fn validate_extensions(
18 config: &Config,
19 report: &mut StartupValidationReport,
20 verbose: bool,
21) {
22 let extensions = match ExtensionRegistry::discover() {
23 Ok(extensions) => extensions,
24 Err(e) => {
25 let mut ext_report = ValidationReport::new("ext:registry".to_owned());
26 ext_report.add_error(ValidationError::new(
27 "extension_discovery",
28 format!("Failed to discover extensions: {}", e),
29 ));
30 report.add_extension(ext_report);
31 return;
32 },
33 };
34 let config_extensions = extensions.config_extensions();
35 let asset_extensions = extensions.asset_extensions();
36
37 let has_extensions = !config_extensions.is_empty() || !asset_extensions.is_empty();
38
39 if !has_extensions {
40 return;
41 }
42
43 if verbose {
44 CliService::output("");
45 CliService::output(&format!(
46 "{} {}",
47 BrandColors::primary("▸"),
48 BrandColors::white_bold("Validating extensions")
49 ));
50 }
51
52 for ext in config_extensions {
53 validate_single_extension(config, ext.as_ref(), report, verbose);
54 }
55
56 let paths_result = ProfileBootstrap::get()
57 .map_err(|e| e.to_string())
58 .and_then(|p| {
59 AppPaths::from_profile(&p.paths, p.path_resolution()).map_err(|e| e.to_string())
60 });
61
62 match paths_result {
63 Ok(paths) => validate_extension_assets(&extensions, &paths, report, verbose),
64 Err(_) if verbose => {
65 CliService::output(&format!(
66 " {} Asset validation skipped (profile not loaded)",
67 BrandColors::dim("○")
68 ));
69 },
70 Err(_) => {},
71 }
72}
73
74fn validate_extension_assets(
75 registry: &ExtensionRegistry,
76 paths: &AppPaths,
77 report: &mut StartupValidationReport,
78 verbose: bool,
79) {
80 for ext in registry.asset_extensions() {
81 let ext_id = ext.id();
82 let mut has_errors = false;
83
84 for asset in ext.required_assets(paths) {
85 if asset.is_required() && !asset.source().exists() {
86 has_errors = true;
87 let mut ext_report = ValidationReport::new(format!("ext:{}", ext_id));
88 ext_report.add_error(
89 ValidationError::new(
90 "required_asset",
91 format!("Missing required asset: {}", asset.source().display()),
92 )
93 .with_suggestion("Ensure the asset file exists at the specified path"),
94 );
95 report.add_extension(ext_report);
96
97 CliService::output(&format!(
98 " {} [ext:{}] Missing asset: {}",
99 BrandColors::stopped("✗"),
100 ext_id,
101 asset.source().display()
102 ));
103 }
104 }
105
106 if !has_errors && verbose {
107 render_phase_success(&format!("[ext:{}]", ext_id), Some("assets valid"));
108 }
109 }
110}
111
112#[derive(Debug)]
118pub struct ExtensionConfigOutcome {
119 pub extension_id: String,
120 pub config_key: String,
121 pub error: Option<String>,
122}
123
124enum ExtConfigError {
125 Load(String),
126 Validate(String),
127}
128
129impl ExtConfigError {
130 fn message(&self) -> &str {
131 match self {
132 Self::Load(m) | Self::Validate(m) => m,
133 }
134 }
135}
136
137fn evaluate_extension_config(
138 ext: &dyn systemprompt_extension::Extension,
139 services_path: &Path,
140) -> Result<(), ExtConfigError> {
141 let Some(prefix) = ext.config_prefix() else {
142 return Ok(());
143 };
144
145 let config_path = services_path
146 .join("config")
147 .join(format!("{}.yaml", prefix));
148
149 let config_json = if config_path.exists() {
150 load_extension_config(&config_path).map_err(ExtConfigError::Load)?
151 } else {
152 serde_json::json!({})
153 };
154
155 ext.validate_config(&config_json)
156 .map_err(|e| ExtConfigError::Validate(e.to_string()))
157}
158
159pub fn validate_extension_configs(
166 services_path: &Path,
167) -> Result<Vec<ExtensionConfigOutcome>, String> {
168 let extensions = ExtensionRegistry::discover().map_err(|e| e.to_string())?;
169
170 Ok(extensions
171 .config_extensions()
172 .iter()
173 .filter_map(|ext| {
174 let prefix = ext.config_prefix()?;
175 let error = evaluate_extension_config(ext.as_ref(), services_path)
176 .err()
177 .map(|e| e.message().to_owned());
178 Some(ExtensionConfigOutcome {
179 extension_id: ext.id().to_owned(),
180 config_key: format!("{}.config", prefix),
181 error,
182 })
183 })
184 .collect())
185}
186
187fn validate_single_extension(
188 config: &Config,
189 ext: &dyn systemprompt_extension::Extension,
190 report: &mut StartupValidationReport,
191 verbose: bool,
192) {
193 let ext_id = ext.id();
194 let Some(prefix) = ext.config_prefix() else {
195 return;
196 };
197
198 match evaluate_extension_config(ext, Path::new(&config.services_path)) {
199 Ok(()) => {
200 if verbose {
201 render_phase_success(&format!("[ext:{}]", ext_id), Some("valid"));
202 }
203 },
204 Err(e) => {
205 let report_message = match &e {
206 ExtConfigError::Load(m) => format!("Failed to load config: {}", m),
207 ExtConfigError::Validate(m) => m.clone(),
208 };
209 let mut ext_report = ValidationReport::new(format!("ext:{}", ext_id));
210 ext_report.add_error(ValidationError::new(
211 format!("{}.config", prefix),
212 report_message,
213 ));
214 report.add_extension(ext_report);
215 CliService::output(&format!(
216 " {} [ext:{}] {}",
217 BrandColors::stopped("✗"),
218 ext_id,
219 e.message()
220 ));
221 },
222 }
223}