systemprompt_models/validators/
agents.rs1use 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(|| DomainConfigError::LoadError {
40 message: "Expected ValidationConfigProvider with merged ServicesConfig".into(),
41 })?;
42
43 self.config = Some(provider.services_config().clone());
44 Ok(())
45 }
46
47 fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
48 let mut report = ValidationReport::new("agents");
49
50 let config = self
51 .config
52 .as_ref()
53 .ok_or_else(|| DomainConfigError::ValidationError {
54 message: "Not loaded".into(),
55 })?;
56
57 let skills_path =
58 self.skills_path
59 .as_ref()
60 .ok_or_else(|| DomainConfigError::ValidationError {
61 message: "Skills path not set".into(),
62 })?;
63
64 if !Path::new(skills_path).exists() {
65 report.add_error(
66 ValidationError::new("skills_path", "Skills directory does not exist")
67 .with_path(skills_path)
68 .with_suggestion("Create the skills directory"),
69 );
70 }
71
72 Self::validate_port_uniqueness(config, &mut report);
73 for (name, agent) in &config.agents {
74 Self::validate_agent(name, agent, config, skills_path, &mut report);
75 }
76
77 Ok(report)
78 }
79}
80
81impl AgentConfigValidator {
82 fn validate_port_uniqueness(config: &ServicesConfig, report: &mut ValidationReport) {
83 let mut used_ports: HashMap<u16, String> = HashMap::new();
84 for (name, agent) in &config.agents {
85 if let Some(existing) = used_ports.get(&agent.port) {
86 report.add_error(
87 ValidationError::new(
88 format!("agents.{}.port", name),
89 format!("Port {} already used by agent '{}'", agent.port, existing),
90 )
91 .with_suggestion("Assign unique ports to each agent"),
92 );
93 } else {
94 used_ports.insert(agent.port, name.clone());
95 }
96 }
97 }
98
99 fn validate_agent(
100 name: &str,
101 agent: &crate::services::AgentConfig,
102 config: &ServicesConfig,
103 skills_path: &str,
104 report: &mut ValidationReport,
105 ) {
106 if agent.name.is_empty() {
107 report.add_error(ValidationError::new(
108 format!("agents.{}.name", name),
109 "Agent name cannot be empty",
110 ));
111 }
112
113 for skill_id in &agent.metadata.skills.include {
120 let skill_path = Path::new(skills_path).join(skill_id);
121 if !skill_path.exists() {
122 report.add_error(
123 ValidationError::new(
124 format!("agents.{}.metadata.skills", name),
125 format!("Skill '{}' directory not found", skill_id),
126 )
127 .with_path(&skill_path)
128 .with_suggestion("Create the skill directory or remove it from the agent"),
129 );
130 }
131 }
132
133 for mcp_server in &agent.metadata.mcp_servers.include {
134 Self::validate_agent_mcp_ref(name, agent, mcp_server, config, report);
135 }
136 }
137
138 fn validate_agent_mcp_ref(
139 name: &str,
140 agent: &crate::services::AgentConfig,
141 mcp_server: &str,
142 config: &ServicesConfig,
143 report: &mut ValidationReport,
144 ) {
145 let Some(mcp_config) = config.mcp_servers.get(mcp_server) else {
146 report.add_error(
147 ValidationError::new(
148 format!("agents.{}.metadata.mcp_servers", name),
149 format!("MCP server '{}' is not defined", mcp_server),
150 )
151 .with_suggestion(
152 "Define the MCP server in mcp_servers or remove it from the agent",
153 ),
154 );
155 return;
156 };
157
158 if mcp_config.dev_only && !agent.dev_only {
159 report.add_error(
160 ValidationError::new(
161 format!("agents.{}.metadata.mcp_servers", name),
162 format!(
163 "Production agent '{}' references dev-only MCP server '{}'",
164 name, mcp_server
165 ),
166 )
167 .with_suggestion(
168 "Either mark the agent as dev_only: true, or use a production MCP server",
169 ),
170 );
171 }
172 }
173}