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