Skip to main content

systemprompt_models/validators/
agents.rs

1use super::ValidationConfigProvider;
2use crate::ServicesConfig;
3use std::collections::HashMap;
4use std::path::Path;
5use systemprompt_traits::validation_report::{ValidationError, ValidationReport};
6use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
7
8#[derive(Debug, Default)]
9pub struct AgentConfigValidator {
10    config: Option<ServicesConfig>,
11    skills_path: Option<String>,
12}
13
14impl AgentConfigValidator {
15    pub fn new() -> Self {
16        Self::default()
17    }
18}
19
20impl DomainConfig for AgentConfigValidator {
21    fn domain_id(&self) -> &'static str {
22        "agents"
23    }
24
25    fn priority(&self) -> u32 {
26        30
27    }
28
29    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
30        let skills_path = config
31            .get("skills_path")
32            .ok_or_else(|| DomainConfigError::NotFound("skills_path not configured".into()))?;
33
34        self.skills_path = Some(skills_path);
35
36        let provider = config
37            .as_any()
38            .downcast_ref::<ValidationConfigProvider>()
39            .ok_or_else(|| {
40                DomainConfigError::LoadError(
41                    "Expected ValidationConfigProvider with merged ServicesConfig".into(),
42                )
43            })?;
44
45        self.config = Some(provider.services_config().clone());
46        Ok(())
47    }
48
49    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
50        let mut report = ValidationReport::new("agents");
51
52        let config = self
53            .config
54            .as_ref()
55            .ok_or_else(|| DomainConfigError::ValidationError("Not loaded".into()))?;
56
57        let skills_path = self
58            .skills_path
59            .as_ref()
60            .ok_or_else(|| DomainConfigError::ValidationError("Skills path not set".into()))?;
61
62        if !Path::new(skills_path).exists() {
63            report.add_error(
64                ValidationError::new("skills_path", "Skills directory does not exist")
65                    .with_path(skills_path)
66                    .with_suggestion("Create the skills directory"),
67            );
68        }
69
70        let mut used_ports: HashMap<u16, String> = HashMap::new();
71        for (name, agent) in &config.agents {
72            if let Some(existing) = used_ports.get(&agent.port) {
73                report.add_error(
74                    ValidationError::new(
75                        format!("agents.{}.port", name),
76                        format!("Port {} already used by agent '{}'", agent.port, existing),
77                    )
78                    .with_suggestion("Assign unique ports to each agent"),
79                );
80            } else {
81                used_ports.insert(agent.port, name.clone());
82            }
83        }
84
85        for (name, agent) in &config.agents {
86            if agent.name.is_empty() {
87                report.add_error(ValidationError::new(
88                    format!("agents.{}.name", name),
89                    "Agent name cannot be empty",
90                ));
91            }
92
93            for skill in &agent.card.skills {
94                let skill_id = &skill.id;
95                let skill_path = Path::new(skills_path).join(skill_id);
96                if !skill_path.exists() {
97                    report.add_error(
98                        ValidationError::new(
99                            format!("agents.{}.skills", name),
100                            format!("Skill '{}' directory not found", skill_id),
101                        )
102                        .with_path(&skill_path)
103                        .with_suggestion("Create the skill directory or remove it from the agent"),
104                    );
105                }
106            }
107
108            for skill_id in &agent.metadata.skills {
109                let skill_path = Path::new(skills_path).join(skill_id);
110                if !skill_path.exists() {
111                    report.add_error(
112                        ValidationError::new(
113                            format!("agents.{}.metadata.skills", name),
114                            format!("Skill '{}' directory not found", skill_id),
115                        )
116                        .with_path(&skill_path)
117                        .with_suggestion("Create the skill directory or remove it from the agent"),
118                    );
119                }
120            }
121
122            for mcp_server in &agent.metadata.mcp_servers {
123                if !config.mcp_servers.contains_key(mcp_server) {
124                    report.add_error(
125                        ValidationError::new(
126                            format!("agents.{}.metadata.mcp_servers", name),
127                            format!("MCP server '{}' is not defined", mcp_server),
128                        )
129                        .with_suggestion(
130                            "Define the MCP server in mcp_servers or remove it from the agent",
131                        ),
132                    );
133                } else if let Some(mcp_config) = config.mcp_servers.get(mcp_server) {
134                    if mcp_config.dev_only && !agent.dev_only {
135                        report.add_error(
136                            ValidationError::new(
137                                format!("agents.{}.metadata.mcp_servers", name),
138                                format!(
139                                    "Production agent '{}' references dev-only MCP server '{}'",
140                                    name, mcp_server
141                                ),
142                            )
143                            .with_suggestion(
144                                "Either mark the agent as dev_only: true, or use a production MCP \
145                                 server",
146                            ),
147                        );
148                    }
149                }
150            }
151        }
152
153        Ok(report)
154    }
155}