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