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