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