systemprompt-runtime 0.1.22

Application runtime context and module registry for systemprompt.io
Documentation
mod config_loaders;
mod display;
mod extension_validator;
mod files_validator;
mod mcp_validator;

use systemprompt_logging::services::cli::{
    BrandColors, render_phase_success, render_phase_warning,
};
use systemprompt_logging::{CliService, is_startup_mode};
use systemprompt_models::validators::{
    AgentConfigValidator, AiConfigValidator, ContentConfigValidator, McpConfigValidator,
    RateLimitsConfigValidator, SkillConfigValidator, ValidationConfigProvider, WebConfigValidator,
};
use systemprompt_models::{Config, ProfileBootstrap};
use systemprompt_traits::validation_report::ValidationError;
use systemprompt_traits::{DomainConfigRegistry, StartupValidationReport, ValidationReport};

use config_loaders::{create_spinner, load_content_config, load_web_config, load_web_metadata};
use extension_validator::validate_extensions;
use mcp_validator::validate_mcp_manifests;

pub use display::{display_validation_report, display_validation_warnings};
pub use files_validator::FilesConfigValidator;

#[derive(Debug)]
pub struct StartupValidator {
    registry: DomainConfigRegistry,
}

impl StartupValidator {
    pub fn new() -> Self {
        let mut registry = DomainConfigRegistry::new();

        registry.register(Box::new(FilesConfigValidator::new()));
        registry.register(Box::new(RateLimitsConfigValidator::new()));
        registry.register(Box::new(WebConfigValidator::new()));
        registry.register(Box::new(ContentConfigValidator::new()));
        registry.register(Box::new(SkillConfigValidator::new()));
        registry.register(Box::new(AgentConfigValidator::new()));
        registry.register(Box::new(McpConfigValidator::new()));
        registry.register(Box::new(AiConfigValidator::new()));

        Self { registry }
    }

    pub fn validate(&mut self, config: &Config) -> StartupValidationReport {
        let mut report = StartupValidationReport::new();
        let verbose = is_startup_mode();

        if let Ok(path) = ProfileBootstrap::get_path() {
            report = report.with_profile_path(path);
        }

        if verbose {
            CliService::section("Validating configuration");
        }

        let Some(validation_provider) = Self::load_configs(config, &mut report, verbose) else {
            return report;
        };

        if self.load_domain_validators(&validation_provider, &mut report, verbose) {
            return report;
        }

        self.run_domain_validations(&mut report, verbose);

        validate_mcp_manifests(config, validation_provider.services_config(), &mut report);

        if report.has_errors() {
            return report;
        }

        validate_extensions(config, &mut report, verbose);

        if verbose {
            CliService::output("");
        }

        report
    }

    fn load_configs(
        config: &Config,
        report: &mut StartupValidationReport,
        verbose: bool,
    ) -> Option<ValidationConfigProvider> {
        let spinner = if verbose {
            Some(create_spinner("Loading services config"))
        } else {
            None
        };
        let services_config = match systemprompt_loader::ConfigLoader::load() {
            Ok(cfg) => {
                if let Some(s) = spinner {
                    s.finish_and_clear();
                }
                if verbose {
                    CliService::phase_success("Services config", Some("includes merged"));
                }
                cfg
            },
            Err(e) => {
                if let Some(s) = spinner {
                    s.finish_and_clear();
                }
                CliService::error(&format!("Services config: {}", e));
                let mut domain_report = ValidationReport::new("services");
                domain_report.add_error(ValidationError::new(
                    "services_config",
                    format!("Failed to load: {}", e),
                ));
                report.add_domain(domain_report);
                return None;
            },
        };

        let mut provider = ValidationConfigProvider::new(config.clone(), services_config);

        provider = load_content_config(config, provider, verbose);
        provider = load_web_config(config, provider, verbose);
        provider = load_web_metadata(config, provider, verbose);

        Some(provider)
    }

    fn load_domain_validators(
        &mut self,
        provider: &ValidationConfigProvider,
        report: &mut StartupValidationReport,
        verbose: bool,
    ) -> bool {
        if verbose {
            CliService::output("");
            CliService::output(&format!(
                "{} {}",
                BrandColors::primary(""),
                BrandColors::white_bold("Validating domains")
            ));
        }

        for validator in self.registry.validators_mut() {
            let domain_id = validator.domain_id();
            let spinner = if verbose {
                Some(create_spinner(&format!("Loading {}", domain_id)))
            } else {
                None
            };

            match validator.load(provider) {
                Ok(()) => {
                    if let Some(s) = spinner {
                        s.finish_and_clear();
                    }
                },
                Err(e) => {
                    if let Some(s) = spinner {
                        s.finish_and_clear();
                    }
                    CliService::output(&format!(
                        "  {} [{}] {}",
                        BrandColors::stopped(""),
                        domain_id,
                        e
                    ));

                    let mut domain_report = ValidationReport::new(domain_id);
                    domain_report.add_error(ValidationError::new(
                        format!("{}_config", domain_id),
                        format!("Failed to load: {}", e),
                    ));
                    report.add_domain(domain_report);
                },
            }
        }

        report.has_errors()
    }

    fn run_domain_validations(&self, report: &mut StartupValidationReport, verbose: bool) {
        for validator in self.registry.validators_sorted() {
            let domain_id = validator.domain_id();
            let spinner = if verbose {
                Some(create_spinner(&format!("Validating {}", domain_id)))
            } else {
                None
            };

            match validator.validate() {
                Ok(domain_report) => {
                    if let Some(s) = spinner {
                        s.finish_and_clear();
                    }
                    if verbose {
                        Self::print_domain_result(&domain_report, domain_id);
                    }
                    report.add_domain(domain_report);
                },
                Err(e) => {
                    if let Some(s) = spinner {
                        s.finish_and_clear();
                    }
                    CliService::output(&format!(
                        "  {} [{}] {}",
                        BrandColors::stopped(""),
                        domain_id,
                        e
                    ));

                    let mut domain_report = ValidationReport::new(domain_id);
                    domain_report.add_error(ValidationError::new(
                        format!("{}_validation", domain_id),
                        format!("Validation error: {}", e),
                    ));
                    report.add_domain(domain_report);
                },
            }
        }
    }

    fn print_domain_result(domain_report: &ValidationReport, domain_id: &str) {
        if domain_report.has_errors() {
            CliService::output(&format!(
                "  {} [{}] {} error(s)",
                BrandColors::stopped(""),
                domain_id,
                domain_report.errors.len()
            ));
        } else if domain_report.has_warnings() {
            render_phase_warning(
                &format!("[{}]", domain_id),
                Some(&format!("{} warning(s)", domain_report.warnings.len())),
            );
        } else {
            render_phase_success(&format!("[{}]", domain_id), Some("valid"));
        }
    }
}

impl Default for StartupValidator {
    fn default() -> Self {
        Self::new()
    }
}