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