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