systemprompt-config 0.14.5

Profile-based configuration for systemprompt.io AI governance infrastructure. Bootstraps profiles, secrets, and credentials with zero environment-variable fallback.
//! `DomainConfig` implementation that validates the on-disk skill
//! catalog referenced by `skills_path`.

use std::path::Path;

use systemprompt_models::{DiskSkillConfig, SKILL_CONFIG_FILENAME};
use systemprompt_traits::validation_report::{ValidationError, ValidationReport};
use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};

#[derive(Debug, Default)]
pub struct SkillConfigValidator {
    skills_path: Option<String>,
}

impl SkillConfigValidator {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }
}

impl DomainConfig for SkillConfigValidator {
    fn domain_id(&self) -> &'static str {
        "skills"
    }

    fn priority(&self) -> u32 {
        25
    }

    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
        let skills_path = config
            .get("skills_path")
            .ok_or_else(|| DomainConfigError::NotFound("skills_path not configured".into()))?;

        self.skills_path = Some(skills_path);
        Ok(())
    }

    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
        let mut report = ValidationReport::new("skills");

        let skills_path = self
            .skills_path
            .as_ref()
            .ok_or_else(|| DomainConfigError::ValidationError("Skills path not set".into()))?;

        let skills_dir = Path::new(skills_path);
        if !skills_dir.exists() {
            report.add_error(
                ValidationError::new("skills_path", "Skills directory does not exist")
                    .with_path(skills_dir)
                    .with_suggestion("Create the skills directory or update skills_path in config"),
            );
            return Ok(report);
        }

        let entries = std::fs::read_dir(skills_dir).map_err(|e| {
            DomainConfigError::LoadError(format!("Cannot read skills directory: {e}"))
        })?;

        for entry in entries {
            let entry = entry.map_err(|e| {
                DomainConfigError::LoadError(format!("Cannot read directory entry: {e}"))
            })?;
            validate_skill_entry(&entry, &mut report);
        }

        Ok(report)
    }
}

fn validate_skill_entry(entry: &std::fs::DirEntry, report: &mut ValidationReport) {
    if !entry.path().is_dir() {
        return;
    }

    let dir_name = entry.file_name().to_string_lossy().to_string();
    let config_path = entry.path().join(SKILL_CONFIG_FILENAME);

    if !config_path.exists() {
        report.add_error(
            ValidationError::new(
                format!("skills.{dir_name}"),
                format!("Missing {SKILL_CONFIG_FILENAME}"),
            )
            .with_path(&config_path)
            .with_suggestion("Add a config.yaml with id, name, and description"),
        );
        return;
    }

    let config_text = match std::fs::read_to_string(&config_path) {
        Ok(text) => text,
        Err(e) => {
            report.add_error(
                ValidationError::new(
                    format!("skills.{dir_name}"),
                    format!("Cannot read {SKILL_CONFIG_FILENAME}: {e}"),
                )
                .with_path(&config_path),
            );
            return;
        },
    };

    let config: DiskSkillConfig = match serde_yaml::from_str(&config_text) {
        Ok(cfg) => cfg,
        Err(e) => {
            report.add_error(
                ValidationError::new(
                    format!("skills.{dir_name}"),
                    format!("Invalid {SKILL_CONFIG_FILENAME}: {e}"),
                )
                .with_path(&config_path)
                .with_suggestion("Ensure config.yaml has required fields: id, name, description"),
            );
            return;
        },
    };

    let content_file = config.content_file();
    let content_path = entry.path().join(content_file);
    if !content_path.exists() {
        report.add_error(
            ValidationError::new(
                format!("skills.{dir_name}.file"),
                format!("Content file '{content_file}' not found"),
            )
            .with_path(&content_path)
            .with_suggestion("Create the content file or update the file field in config.yaml"),
        );
    }
}