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(|| {
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}