systemprompt_runtime/startup_validation/
extension_validator.rs1use std::path::Path;
7use systemprompt_config::ProfileBootstrap;
8use systemprompt_config::paths::AppPaths;
9use systemprompt_extension::ExtensionRegistry;
10use systemprompt_logging::CliService;
11use systemprompt_logging::services::cli::{BrandColors, render_phase_success};
12use systemprompt_models::Config;
13use systemprompt_traits::validation_report::ValidationIssue;
14use systemprompt_traits::{StartupValidationReport, ValidationReport};
15
16use super::config_loaders::load_extension_config;
17
18pub(super) fn validate_extensions(
19 config: &Config,
20 report: &mut StartupValidationReport,
21 verbose: bool,
22) {
23 let extensions = match ExtensionRegistry::discover() {
24 Ok(extensions) => extensions,
25 Err(e) => {
26 let mut ext_report = ValidationReport::new("ext:registry".to_owned());
27 ext_report.add_error(ValidationIssue::new(
28 "extension_discovery",
29 format!("Failed to discover extensions: {}", e),
30 ));
31 report.add_extension(ext_report);
32 return;
33 },
34 };
35 let config_extensions = extensions.config_extensions();
36 let asset_extensions = extensions.asset_extensions();
37
38 let has_extensions = !config_extensions.is_empty() || !asset_extensions.is_empty();
39
40 if !has_extensions {
41 return;
42 }
43
44 if verbose {
45 CliService::output("");
46 CliService::output(&format!(
47 "{} {}",
48 BrandColors::primary("▸"),
49 BrandColors::white_bold("Validating extensions")
50 ));
51 }
52
53 for ext in config_extensions {
54 validate_single_extension(config, ext.as_ref(), report, verbose);
55 }
56
57 let paths_result = ProfileBootstrap::get()
58 .map_err(|e| e.to_string())
59 .and_then(|p| {
60 AppPaths::from_profile(
61 &p.paths,
62 p.path_resolution(),
63 systemprompt_loader::ServicesRootBootstrap::get().map(|r| r.path.as_path()),
64 )
65 .map_err(|e| e.to_string())
66 });
67
68 match paths_result {
69 Ok(paths) => validate_extension_assets(&extensions, &paths, report, verbose),
70 Err(_) if verbose => {
71 CliService::output(&format!(
72 " {} Asset validation skipped (profile not loaded)",
73 BrandColors::dim("○")
74 ));
75 },
76 Err(_) => {},
77 }
78}
79
80fn validate_extension_assets(
81 registry: &ExtensionRegistry,
82 paths: &AppPaths,
83 report: &mut StartupValidationReport,
84 verbose: bool,
85) {
86 for ext in registry.asset_extensions() {
87 let ext_id = ext.id();
88 let mut has_errors = false;
89
90 for asset in ext.required_assets(paths) {
91 if asset.is_required() && !asset.source().exists() {
92 has_errors = true;
93 let mut ext_report = ValidationReport::new(format!("ext:{}", ext_id));
94 ext_report.add_error(
95 ValidationIssue::new(
96 "required_asset",
97 format!("Missing required asset: {}", asset.source().display()),
98 )
99 .with_suggestion("Ensure the asset file exists at the specified path"),
100 );
101 report.add_extension(ext_report);
102
103 CliService::output(&format!(
104 " {} [ext:{}] Missing asset: {}",
105 BrandColors::stopped("✗"),
106 ext_id,
107 asset.source().display()
108 ));
109 }
110 }
111
112 if !has_errors && verbose {
113 render_phase_success(&format!("[ext:{}]", ext_id), Some("assets valid"));
114 }
115 }
116}
117
118#[derive(Debug)]
124pub struct ExtensionConfigOutcome {
125 pub extension_id: String,
126 pub config_key: String,
127 pub error: Option<String>,
128}
129
130enum ExtConfigError {
131 Load(String),
132 Validate(String),
133}
134
135impl ExtConfigError {
136 fn message(&self) -> &str {
137 match self {
138 Self::Load(m) | Self::Validate(m) => m,
139 }
140 }
141}
142
143fn evaluate_extension_config(
144 ext: &dyn systemprompt_extension::Extension,
145 services_path: &Path,
146) -> Result<(), ExtConfigError> {
147 let Some(prefix) = ext.config_prefix() else {
148 return Ok(());
149 };
150
151 let config_path = services_path
152 .join("config")
153 .join(format!("{}.yaml", prefix));
154
155 let config_json = if config_path.exists() {
156 load_extension_config(&config_path).map_err(ExtConfigError::Load)?
157 } else {
158 serde_json::json!({})
159 };
160
161 ext.validate_config(&config_json)
162 .map_err(|e| ExtConfigError::Validate(e.to_string()))
163}
164
165pub 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(ValidationIssue::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}