Skip to main content

systemprompt_models/services/
mod.rs

1//! `services` module — see crate-level docs for context.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6pub mod agent_config;
7pub mod ai;
8pub mod artifacts;
9pub mod bridge_policy;
10pub mod external_agent;
11pub mod frontmatter;
12pub mod gateway;
13pub mod hooks;
14mod includable;
15pub mod marketplace;
16pub mod mcp;
17pub mod plugin;
18pub mod providers;
19pub mod runtime;
20pub mod scheduler;
21pub mod settings;
22pub mod skills;
23pub mod slack;
24pub mod system_admin;
25pub mod teams;
26mod validation;
27
28pub use includable::IncludableString;
29
30pub use agent_config::{
31    AGENT_CONFIG_FILENAME, AgentCardConfig, AgentConfig, AgentMetadataConfig, AgentProviderInfo,
32    AgentSummary, CapabilitiesConfig, DEFAULT_AGENT_SYSTEM_PROMPT_FILE, DiskAgentConfig,
33    OAuthConfig,
34};
35pub use ai::{
36    AiConfig, AiProviderConfig, HistoryConfig, McpConfig, ModelCapabilities, ModelDefinition,
37    ModelGovernance, ModelLimits, ModelPricing, ResilienceSettings, SamplingConfig,
38};
39pub use artifacts::{ARTIFACT_CONFIG_FILENAME, DEFAULT_ARTIFACT_CONTENT_FILE, DiskArtifactConfig};
40pub use bridge_policy::BridgePolicyConfig;
41pub use external_agent::{ExternalAgentConfig, ExternalAgentKind};
42pub use frontmatter::{Frontmatter, split_frontmatter, strip_frontmatter};
43pub use gateway::{
44    BridgeReleasesSpec, GatewayConfig, GatewayConfigSpec, GatewayProfileError, GatewayResult,
45    GatewayRoute, GatewayState, OverrideRuleAction, ResponseFormatKind, RouteMatch,
46    RouteRequirements, SystemPromptRule, slugify_pattern, synthesize_route_id,
47};
48pub use hooks::{
49    DiskHookConfig, HOOK_CONFIG_FILENAME, HookAction, HookCategory, HookEvent, HookEventsConfig,
50    HookMatcher, HookType,
51};
52pub use marketplace::{
53    MarketplaceAccess, MarketplaceAccessRule, MarketplaceConfig, MarketplaceConfigFile,
54    MarketplaceMemberKind, MarketplaceRuleAccess, MarketplaceVisibility,
55};
56pub use mcp::McpServerSummary;
57pub use plugin::{
58    ComponentFilter, ComponentSource, PluginAuthor, PluginComponentRef, PluginConfig,
59    PluginConfigFile, PluginHooksRef, PluginScript, PluginSummary, PluginVariableDef,
60};
61pub use providers::{
62    ApiSurface, ProviderEntry, ProviderModel, ProviderRegistry, ProviderRegistryError,
63    ProviderRegistryResult, WireProtocol,
64};
65pub use runtime::{RuntimeStatus, ServiceType};
66pub use scheduler::*;
67pub use settings::*;
68pub use skills::{
69    DEFAULT_SKILL_CONTENT_FILE, DiskSkillConfig, SKILL_CONFIG_FILENAME, SkillConfig, SkillDetail,
70    SkillSummary, SkillsConfig,
71};
72pub use slack::{SlackAppConfig, SlackAuthzConfig};
73pub use system_admin::{SystemAdmin, SystemAdminConfig};
74pub use systemprompt_provider_contracts::{BrandingConfig, WebConfig};
75pub use teams::{TeamsAppConfig, TeamsAuthzConfig, TeamsEndpoints};
76
77use crate::errors::ConfigValidationError;
78use crate::mcp::{Deployment, McpServerType};
79use serde::{Deserialize, Serialize};
80use std::collections::HashMap;
81use systemprompt_identifiers::{ExternalAgentId, MarketplaceId};
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct ServicesConfig {
86    #[serde(default)]
87    pub includes: Vec<String>,
88    #[serde(default)]
89    pub settings: Settings,
90    #[serde(default)]
91    pub agents: HashMap<String, AgentConfig>,
92    #[serde(default)]
93    pub mcp_servers: HashMap<String, Deployment>,
94    #[serde(default)]
95    pub scheduler: Option<SchedulerConfig>,
96    #[serde(default)]
97    pub ai: AiConfig,
98    #[serde(default)]
99    pub web: Option<WebConfig>,
100    #[serde(default)]
101    pub plugins: HashMap<String, PluginConfig>,
102    #[serde(default)]
103    pub marketplaces: HashMap<MarketplaceId, MarketplaceConfig>,
104    #[serde(default)]
105    pub skills: SkillsConfig,
106    #[serde(default)]
107    pub external_agents: HashMap<ExternalAgentId, ExternalAgentConfig>,
108    #[serde(default)]
109    pub slack_apps: HashMap<String, SlackAppConfig>,
110    #[serde(default)]
111    pub teams_apps: HashMap<String, TeamsAppConfig>,
112    #[serde(default)]
113    pub bridge_policy: Option<BridgePolicyConfig>,
114    #[serde(default)]
115    pub providers: ProviderRegistry,
116    #[serde(default)]
117    pub gateway: Option<GatewayState>,
118}
119
120impl ServicesConfig {
121    pub fn apply_port_offset(&mut self, offset: u16) -> Result<(), ConfigValidationError> {
122        if offset == 0 {
123            return Ok(());
124        }
125
126        let shift = |port: u16, what: &str| {
127            port.checked_add(offset).ok_or_else(|| {
128                ConfigValidationError::invalid_field(format!(
129                    "{what} port {port} shifted by services.port_offset {offset} exceeds 65535"
130                ))
131            })
132        };
133
134        for (name, agent) in &mut self.agents {
135            agent.port = shift(agent.port, &format!("Agent '{name}'"))?;
136        }
137
138        for (name, mcp) in &mut self.mcp_servers {
139            if mcp.server_type == McpServerType::External {
140                continue;
141            }
142            mcp.port = shift(mcp.port, &format!("MCP server '{name}'"))?;
143        }
144
145        self.settings.agent_port_range = (
146            shift(self.settings.agent_port_range.0, "agent_port_range lower")?,
147            shift(self.settings.agent_port_range.1, "agent_port_range upper")?,
148        );
149        self.settings.mcp_port_range = (
150            shift(self.settings.mcp_port_range.0, "mcp_port_range lower")?,
151            shift(self.settings.mcp_port_range.1, "mcp_port_range upper")?,
152        );
153
154        Ok(())
155    }
156
157    pub fn validate(&self) -> Result<(), ConfigValidationError> {
158        self.validate_ports()?;
159        self.validate_single_default_agent()?;
160
161        for (name, agent) in &self.agents {
162            agent.validate(name)?;
163        }
164
165        for (name, mcp) in &self.mcp_servers {
166            mcp.validate(name)?;
167        }
168
169        self.validate_skills()?;
170
171        for (name, plugin) in &self.plugins {
172            plugin.validate(name)?;
173            self.validate_plugin_bindings(name, plugin)?;
174        }
175
176        self.validate_single_governance_hook_owner()?;
177
178        for (id, marketplace) in &self.marketplaces {
179            marketplace.validate(id.as_str())?;
180            self.validate_marketplace_bindings(id.as_str(), marketplace)?;
181        }
182
183        self.validate_marketplace_selector()?;
184
185        for (name, app) in &self.slack_apps {
186            app.validate(name)?;
187        }
188
189        for (name, app) in &self.teams_apps {
190            app.validate(name)?;
191        }
192
193        self.validate_providers_and_gateway()
194    }
195
196    fn validate_providers_and_gateway(&self) -> Result<(), ConfigValidationError> {
197        self.providers
198            .validate()
199            .map_err(|e| ConfigValidationError::invalid_field(format!("providers: {e}")))?;
200        match &self.gateway {
201            Some(GatewayState::Resolved(config)) => config.validate(&self.providers),
202            Some(GatewayState::Spec(spec)) => spec.clone().resolve().validate(&self.providers),
203            None => Ok(()),
204        }
205        .map_err(|e| ConfigValidationError::invalid_field(format!("gateway: {e}")))
206    }
207
208    #[must_use]
209    pub fn gateway_config(&self) -> Option<&GatewayConfig> {
210        self.gateway.as_ref().and_then(GatewayState::resolved)
211    }
212
213    #[must_use]
214    pub fn enabled_marketplaces(&self) -> Vec<&MarketplaceConfig> {
215        let mut out: Vec<&MarketplaceConfig> =
216            self.marketplaces.values().filter(|m| m.enabled).collect();
217        out.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str()));
218        out
219    }
220
221    #[must_use]
222    pub fn marketplace_plugin_configs(
223        &self,
224        marketplace: &MarketplaceConfig,
225    ) -> Vec<&PluginConfig> {
226        let mut out: Vec<&PluginConfig> = self
227            .plugins
228            .values()
229            .filter(|p| p.enabled)
230            .filter(|p| {
231                marketplace.plugins.include.is_empty()
232                    || marketplace
233                        .plugins
234                        .include
235                        .iter()
236                        .any(|inc| inc == p.id.as_str())
237            })
238            .collect();
239        out.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str()));
240        out
241    }
242
243    #[must_use]
244    pub fn plugin_selected_skill_ids(
245        &self,
246        plugin: &PluginConfig,
247    ) -> std::collections::BTreeSet<String> {
248        let mut ids: std::collections::BTreeSet<String> = match plugin.skills.source {
249            ComponentSource::Explicit => plugin.skills.include.iter().cloned().collect(),
250            ComponentSource::Instance => self
251                .skills
252                .skills
253                .keys()
254                .filter(|k| !plugin.skills.exclude.iter().any(|ex| ex == *k))
255                .cloned()
256                .collect(),
257        };
258
259        let selected_agent = |name: &str| match plugin.agents.source {
260            ComponentSource::Explicit => plugin.agents.include.iter().any(|inc| inc == name),
261            ComponentSource::Instance => !plugin.agents.exclude.iter().any(|ex| ex == name),
262        };
263        for (name, agent) in &self.agents {
264            if selected_agent(name) {
265                ids.extend(agent.metadata.skills.include.iter().cloned());
266            }
267        }
268
269        ids
270    }
271
272    #[must_use]
273    pub fn marketplace_skill_members(
274        &self,
275        marketplace: &MarketplaceConfig,
276    ) -> std::collections::BTreeSet<String> {
277        self.marketplace_plugin_configs(marketplace)
278            .into_iter()
279            .flat_map(|plugin| self.plugin_selected_skill_ids(plugin))
280            .collect()
281    }
282}