systemprompt_models/services/
mod.rs1pub mod agent_config;
4pub mod ai;
5pub mod external_agent;
6pub mod hooks;
7mod includable;
8pub mod marketplace;
9pub mod mcp;
10pub mod plugin;
11pub mod runtime;
12pub mod scheduler;
13pub mod settings;
14pub mod skills;
15pub mod system_admin;
16
17pub use includable::IncludableString;
18
19pub use agent_config::{
20 AGENT_CONFIG_FILENAME, AgentCardConfig, AgentConfig, AgentMetadataConfig, AgentProviderInfo,
21 AgentSkillConfig, AgentSummary, CapabilitiesConfig, DEFAULT_AGENT_SYSTEM_PROMPT_FILE,
22 DiskAgentConfig, OAuthConfig,
23};
24pub use ai::{
25 AiConfig, AiProviderConfig, HistoryConfig, McpConfig, ModelCapabilities, ModelDefinition,
26 ModelLimits, ModelPricing, ResilienceSettings, SamplingConfig, ToolModelConfig,
27 ToolModelSettings,
28};
29pub use external_agent::{ExternalAgentConfig, ExternalAgentKind};
30pub use hooks::{
31 DiskHookConfig, HOOK_CONFIG_FILENAME, HookAction, HookCategory, HookEvent, HookEventsConfig,
32 HookMatcher, HookType,
33};
34pub use marketplace::{
35 MarketplaceAccess, MarketplaceConfig, MarketplaceConfigFile, MarketplaceVisibility,
36};
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 external_agents: HashMap<ExternalAgentId, ExternalAgentConfig>,
89}
90
91impl ServicesConfig {
92 pub fn validate(&self) -> Result<(), ConfigValidationError> {
93 self.validate_port_conflicts()?;
94 self.validate_port_ranges()?;
95 self.validate_mcp_port_ranges()?;
96 self.validate_single_default_agent()?;
97
98 for (name, agent) in &self.agents {
99 agent.validate(name)?;
100 }
101
102 for (name, mcp) in &self.mcp_servers {
103 mcp.validate(name)?;
104 }
105
106 for (name, plugin) in &self.plugins {
107 plugin.validate(name)?;
108 self.validate_plugin_bindings(name, plugin)?;
109 }
110
111 for (id, marketplace) in &self.marketplaces {
112 marketplace.validate(id.as_str())?;
113 self.validate_marketplace_bindings(id.as_str(), marketplace)?;
114 }
115
116 self.validate_default_marketplace_selector()?;
117
118 Ok(())
119 }
120
121 fn validate_default_marketplace_selector(&self) -> Result<(), ConfigValidationError> {
122 if self.marketplaces.len() > 1 && self.settings.default_marketplace_id.is_none() {
123 return Err(ConfigValidationError::business_rule(format!(
124 "{} marketplaces are configured but settings.default_marketplace_id is unset; set \
125 it to select the active marketplace",
126 self.marketplaces.len()
127 )));
128 }
129
130 if let Some(id) = &self.settings.default_marketplace_id {
131 if !self.marketplaces.keys().any(|k| k.as_str() == id.as_str()) {
132 return Err(ConfigValidationError::unknown_reference(format!(
133 "settings.default_marketplace_id '{}' does not match any configured \
134 marketplace",
135 id.as_str()
136 )));
137 }
138 }
139
140 Ok(())
141 }
142
143 fn validate_marketplace_bindings(
144 &self,
145 name: &str,
146 marketplace: &MarketplaceConfig,
147 ) -> Result<(), ConfigValidationError> {
148 for plugin_ref in &marketplace.plugins.include {
149 if !self.plugins.contains_key(plugin_ref) {
150 return Err(ConfigValidationError::unknown_reference(format!(
151 "Marketplace '{name}': plugins.include references unknown plugin \
152 '{plugin_ref}'"
153 )));
154 }
155 }
156
157 for skill_ref in &marketplace.skills.include {
158 let exists = self.skills.skills.keys().any(|k| k.as_str() == skill_ref);
159 if !exists {
160 return Err(ConfigValidationError::unknown_reference(format!(
161 "Marketplace '{name}': skills.include references unknown skill '{skill_ref}'"
162 )));
163 }
164 }
165
166 for mcp_ref in &marketplace.mcp_servers.include {
167 if !self.mcp_servers.contains_key(mcp_ref) {
168 return Err(ConfigValidationError::unknown_reference(format!(
169 "Marketplace '{name}': mcp_servers.include references unknown mcp_server \
170 '{mcp_ref}'"
171 )));
172 }
173 }
174
175 for agent_ref in &marketplace.agents.include {
176 if !self.agents.contains_key(agent_ref) {
177 return Err(ConfigValidationError::unknown_reference(format!(
178 "Marketplace '{name}': agents.include references unknown agent '{agent_ref}'"
179 )));
180 }
181 }
182
183 Ok(())
184 }
185
186 fn validate_plugin_bindings(
187 &self,
188 plugin_name: &str,
189 plugin: &PluginConfig,
190 ) -> Result<(), ConfigValidationError> {
191 for mcp_ref in &plugin.mcp_servers.include {
192 if !self.mcp_servers.contains_key(mcp_ref) {
193 return Err(ConfigValidationError::unknown_reference(format!(
194 "Plugin '{plugin_name}': mcp_servers.include references unknown mcp_server \
195 '{mcp_ref}'"
196 )));
197 }
198 }
199
200 for agent_ref in &plugin.agents.include {
201 if !self.agents.contains_key(agent_ref) {
202 return Err(ConfigValidationError::unknown_reference(format!(
203 "Plugin '{plugin_name}': agents.include references unknown agent '{agent_ref}'"
204 )));
205 }
206 }
207
208 self.validate_skills()?;
209
210 Ok(())
211 }
212
213 fn validate_skills(&self) -> Result<(), ConfigValidationError> {
214 for (key, skill) in &self.skills.skills {
215 if !skill.id.as_str().is_empty() && skill.id.as_str() != key.as_str() {
216 return Err(ConfigValidationError::invalid_field(format!(
217 "Skill map key '{}' does not match skill id '{}'",
218 key, skill.id
219 )));
220 }
221
222 for agent_ref in &skill.assigned_agents.include {
223 if !self.agents.contains_key(agent_ref) {
224 tracing::warn!(
225 skill = %key,
226 agent = %agent_ref,
227 "Skill references agent that is not defined in services config"
228 );
229 }
230 }
231
232 for mcp_ref in &skill.mcp_servers.include {
233 if !self.mcp_servers.contains_key(mcp_ref) {
234 tracing::warn!(
235 skill = %key,
236 mcp_server = %mcp_ref,
237 "Skill references MCP server that is not defined in services config"
238 );
239 }
240 }
241 }
242
243 Ok(())
244 }
245
246 fn validate_port_conflicts(&self) -> Result<(), ConfigValidationError> {
247 let mut seen_ports = HashMap::new();
248
249 for (name, agent) in &self.agents {
250 if let Some(existing) = seen_ports.insert(agent.port, ("agent", name.as_str())) {
251 return Err(ConfigValidationError::port_conflict(format!(
252 "Port conflict: {} used by both {} '{}' and agent '{}'",
253 agent.port, existing.0, existing.1, name
254 )));
255 }
256 }
257
258 for (name, mcp) in &self.mcp_servers {
259 if mcp.server_type == McpServerType::External {
260 continue;
261 }
262 if let Some(existing) = seen_ports.insert(mcp.port, ("mcp_server", name.as_str())) {
263 return Err(ConfigValidationError::port_conflict(format!(
264 "Port conflict: {} used by both {} '{}' and mcp_server '{}'",
265 mcp.port, existing.0, existing.1, name
266 )));
267 }
268 }
269
270 Ok(())
271 }
272
273 fn validate_port_ranges(&self) -> Result<(), ConfigValidationError> {
274 let (min, max) = self.settings.agent_port_range;
275
276 for (name, agent) in &self.agents {
277 if agent.port < min || agent.port > max {
278 return Err(ConfigValidationError::invalid_field(format!(
279 "Agent '{}' port {} is outside allowed range {}-{}",
280 name, agent.port, min, max
281 )));
282 }
283 }
284
285 Ok(())
286 }
287
288 fn validate_mcp_port_ranges(&self) -> Result<(), ConfigValidationError> {
289 let (min, max) = self.settings.mcp_port_range;
290
291 for (name, mcp) in &self.mcp_servers {
292 if mcp.server_type == McpServerType::External {
293 continue;
294 }
295 if mcp.port < min || mcp.port > max {
296 return Err(ConfigValidationError::invalid_field(format!(
297 "MCP server '{}' port {} is outside allowed range {}-{}",
298 name, mcp.port, min, max
299 )));
300 }
301 }
302
303 Ok(())
304 }
305
306 fn validate_single_default_agent(&self) -> Result<(), ConfigValidationError> {
307 let default_agents: Vec<&str> = self
308 .agents
309 .iter()
310 .filter_map(|(name, agent)| {
311 if agent.default {
312 Some(name.as_str())
313 } else {
314 None
315 }
316 })
317 .collect();
318
319 match default_agents.len() {
320 0 | 1 => Ok(()),
321 _ => Err(ConfigValidationError::business_rule(format!(
322 "Multiple agents marked as default: {}. Only one agent can have 'default: true'",
323 default_agents.join(", ")
324 ))),
325 }
326 }
327}