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