Skip to main content

systemprompt_models/services/
mod.rs

1pub 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}