systemprompt_models/services/
mod.rs1pub mod agent_config;
4pub mod ai;
5pub mod content;
6pub mod external_agent;
7pub mod hooks;
8mod includable;
9pub mod mcp;
10pub mod plugin;
11pub mod runtime;
12pub mod scheduler;
13pub mod settings;
14pub mod skills;
15
16pub use includable::IncludableString;
17
18pub use agent_config::{
19 AGENT_CONFIG_FILENAME, AgentCardConfig, AgentConfig, AgentMetadataConfig, AgentProviderInfo,
20 AgentSkillConfig, AgentSummary, CapabilitiesConfig, DEFAULT_AGENT_SYSTEM_PROMPT_FILE,
21 DiskAgentConfig, OAuthConfig,
22};
23pub use ai::{
24 AiConfig, AiProviderConfig, HistoryConfig, McpConfig, ModelCapabilities, ModelDefinition,
25 ModelLimits, ModelPricing, SamplingConfig, ToolModelConfig, ToolModelSettings,
26};
27pub use content::ContentConfig;
28pub use external_agent::{ExternalAgentConfig, ExternalAgentKind};
29pub use hooks::{
30 DiskHookConfig, HOOK_CONFIG_FILENAME, HookAction, HookCategory, HookEvent, HookEventsConfig,
31 HookMatcher, HookType,
32};
33pub use mcp::McpServerSummary;
34pub use plugin::{
35 ComponentFilter, ComponentSource, PluginAuthor, PluginComponentRef, PluginConfig,
36 PluginConfigFile, PluginScript, PluginSummary, PluginVariableDef,
37};
38pub use runtime::{RuntimeStatus, ServiceType};
39pub use scheduler::*;
40pub use settings::*;
41pub use skills::{
42 DEFAULT_SKILL_CONTENT_FILE, DiskSkillConfig, SKILL_CONFIG_FILENAME, SkillConfig, SkillDetail,
43 SkillSummary, SkillsConfig, strip_frontmatter,
44};
45pub use systemprompt_provider_contracts::{BrandingConfig, WebConfig};
46
47use crate::errors::ConfigValidationError;
48use crate::mcp::{Deployment, McpServerType};
49use serde::{Deserialize, Serialize};
50use std::collections::HashMap;
51use systemprompt_identifiers::ExternalAgentId;
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct ServicesConfig {
62 #[serde(default)]
63 pub includes: Vec<String>,
64 #[serde(default)]
65 pub settings: Settings,
66 #[serde(default)]
67 pub agents: HashMap<String, AgentConfig>,
68 #[serde(default)]
69 pub mcp_servers: HashMap<String, Deployment>,
70 #[serde(default)]
71 pub scheduler: Option<SchedulerConfig>,
72 #[serde(default)]
73 pub ai: AiConfig,
74 #[serde(default)]
75 pub web: Option<WebConfig>,
76 #[serde(default)]
77 pub plugins: HashMap<String, PluginConfig>,
78 #[serde(default)]
79 pub skills: SkillsConfig,
80 #[serde(default)]
81 pub content: ContentConfig,
82 #[serde(default)]
83 pub external_agents: HashMap<ExternalAgentId, ExternalAgentConfig>,
84}
85
86impl ServicesConfig {
87 pub fn validate(&self) -> Result<(), ConfigValidationError> {
88 self.validate_port_conflicts()?;
89 self.validate_port_ranges()?;
90 self.validate_mcp_port_ranges()?;
91 self.validate_single_default_agent()?;
92
93 for (name, agent) in &self.agents {
94 agent.validate(name)?;
95 }
96
97 for (name, plugin) in &self.plugins {
98 plugin.validate(name)?;
99 self.validate_plugin_bindings(name, plugin)?;
100 }
101
102 Ok(())
103 }
104
105 fn validate_plugin_bindings(
106 &self,
107 plugin_name: &str,
108 plugin: &PluginConfig,
109 ) -> Result<(), ConfigValidationError> {
110 for mcp_ref in &plugin.mcp_servers {
111 if !self.mcp_servers.contains_key(mcp_ref) {
112 return Err(ConfigValidationError::unknown_reference(format!(
113 "Plugin '{plugin_name}': mcp_servers references unknown mcp_server '{mcp_ref}'"
114 )));
115 }
116 }
117
118 for agent_ref in &plugin.agents.include {
119 if !self.agents.contains_key(agent_ref) {
120 return Err(ConfigValidationError::unknown_reference(format!(
121 "Plugin '{plugin_name}': agents.include references unknown agent '{agent_ref}'"
122 )));
123 }
124 }
125
126 self.validate_skills()?;
127
128 Ok(())
129 }
130
131 fn validate_skills(&self) -> Result<(), ConfigValidationError> {
132 for (key, skill) in &self.skills.skills {
133 if !skill.id.as_str().is_empty() && skill.id.as_str() != key.as_str() {
134 return Err(ConfigValidationError::invalid_field(format!(
135 "Skill map key '{}' does not match skill id '{}'",
136 key, skill.id
137 )));
138 }
139
140 for agent_ref in &skill.assigned_agents {
141 if !self.agents.contains_key(agent_ref) {
142 tracing::warn!(
143 skill = %key,
144 agent = %agent_ref,
145 "Skill references agent that is not defined in services config"
146 );
147 }
148 }
149
150 for mcp_ref in &skill.mcp_servers {
151 if !self.mcp_servers.contains_key(mcp_ref) {
152 tracing::warn!(
153 skill = %key,
154 mcp_server = %mcp_ref,
155 "Skill references MCP server that is not defined in services config"
156 );
157 }
158 }
159 }
160
161 Ok(())
162 }
163
164 fn validate_port_conflicts(&self) -> Result<(), ConfigValidationError> {
165 let mut seen_ports = HashMap::new();
166
167 for (name, agent) in &self.agents {
168 if let Some(existing) = seen_ports.insert(agent.port, ("agent", name.as_str())) {
169 return Err(ConfigValidationError::port_conflict(format!(
170 "Port conflict: {} used by both {} '{}' and agent '{}'",
171 agent.port, existing.0, existing.1, name
172 )));
173 }
174 }
175
176 for (name, mcp) in &self.mcp_servers {
177 if mcp.server_type == McpServerType::External {
178 continue;
179 }
180 if let Some(existing) = seen_ports.insert(mcp.port, ("mcp_server", name.as_str())) {
181 return Err(ConfigValidationError::port_conflict(format!(
182 "Port conflict: {} used by both {} '{}' and mcp_server '{}'",
183 mcp.port, existing.0, existing.1, name
184 )));
185 }
186 }
187
188 Ok(())
189 }
190
191 fn validate_port_ranges(&self) -> Result<(), ConfigValidationError> {
192 let (min, max) = self.settings.agent_port_range;
193
194 for (name, agent) in &self.agents {
195 if agent.port < min || agent.port > max {
196 return Err(ConfigValidationError::invalid_field(format!(
197 "Agent '{}' port {} is outside allowed range {}-{}",
198 name, agent.port, min, max
199 )));
200 }
201 }
202
203 Ok(())
204 }
205
206 fn validate_mcp_port_ranges(&self) -> Result<(), ConfigValidationError> {
207 let (min, max) = self.settings.mcp_port_range;
208
209 for (name, mcp) in &self.mcp_servers {
210 if mcp.server_type == McpServerType::External {
211 continue;
212 }
213 if mcp.port < min || mcp.port > max {
214 return Err(ConfigValidationError::invalid_field(format!(
215 "MCP server '{}' port {} is outside allowed range {}-{}",
216 name, mcp.port, min, max
217 )));
218 }
219 }
220
221 Ok(())
222 }
223
224 fn validate_single_default_agent(&self) -> Result<(), ConfigValidationError> {
225 let default_agents: Vec<&str> = self
226 .agents
227 .iter()
228 .filter_map(|(name, agent)| {
229 if agent.default {
230 Some(name.as_str())
231 } else {
232 None
233 }
234 })
235 .collect();
236
237 match default_agents.len() {
238 0 | 1 => Ok(()),
239 _ => Err(ConfigValidationError::business_rule(format!(
240 "Multiple agents marked as default: {}. Only one agent can have 'default: true'",
241 default_agents.join(", ")
242 ))),
243 }
244 }
245}