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 hooks;
13mod includable;
14pub mod marketplace;
15pub mod mcp;
16pub mod plugin;
17pub mod runtime;
18pub mod scheduler;
19pub mod settings;
20pub mod skills;
21pub mod slack;
22pub mod system_admin;
23pub mod teams;
24mod validation;
25
26pub use includable::IncludableString;
27
28pub use agent_config::{
29    AGENT_CONFIG_FILENAME, AgentCardConfig, AgentConfig, AgentMetadataConfig, AgentProviderInfo,
30    AgentSummary, CapabilitiesConfig, DEFAULT_AGENT_SYSTEM_PROMPT_FILE, DiskAgentConfig,
31    OAuthConfig,
32};
33pub use ai::{
34    AiConfig, AiProviderConfig, HistoryConfig, McpConfig, ModelCapabilities, ModelDefinition,
35    ModelGovernance, ModelLimits, ModelPricing, ResilienceSettings, SamplingConfig,
36};
37pub use artifacts::{ARTIFACT_CONFIG_FILENAME, DEFAULT_ARTIFACT_CONTENT_FILE, DiskArtifactConfig};
38pub use bridge_policy::BridgePolicyConfig;
39pub use external_agent::{ExternalAgentConfig, ExternalAgentKind};
40pub use frontmatter::{Frontmatter, split_frontmatter, strip_frontmatter};
41pub use hooks::{
42    DiskHookConfig, HOOK_CONFIG_FILENAME, HookAction, HookCategory, HookEvent, HookEventsConfig,
43    HookMatcher, HookType,
44};
45pub use marketplace::{
46    MarketplaceAccess, MarketplaceConfig, MarketplaceConfigFile, MarketplaceMemberKind,
47    MarketplaceVisibility,
48};
49pub use mcp::McpServerSummary;
50pub use plugin::{
51    ComponentFilter, ComponentSource, PluginAuthor, PluginComponentRef, PluginConfig,
52    PluginConfigFile, PluginHooksRef, PluginScript, PluginSummary, PluginVariableDef,
53};
54pub use runtime::{RuntimeStatus, ServiceType};
55pub use scheduler::*;
56pub use settings::*;
57pub use skills::{
58    DEFAULT_SKILL_CONTENT_FILE, DiskSkillConfig, SKILL_CONFIG_FILENAME, SkillConfig, SkillDetail,
59    SkillSummary, SkillsConfig,
60};
61pub use slack::{SlackAppConfig, SlackAuthzConfig};
62pub use system_admin::{SystemAdmin, SystemAdminConfig};
63pub use systemprompt_provider_contracts::{BrandingConfig, WebConfig};
64pub use teams::{TeamsAppConfig, TeamsAuthzConfig};
65
66use crate::errors::ConfigValidationError;
67use crate::mcp::{Deployment, McpServerType};
68use serde::{Deserialize, Serialize};
69use std::collections::HashMap;
70use systemprompt_identifiers::{ExternalAgentId, MarketplaceId};
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct ServicesConfig {
75    #[serde(default)]
76    pub includes: Vec<String>,
77    #[serde(default)]
78    pub settings: Settings,
79    #[serde(default)]
80    pub agents: HashMap<String, AgentConfig>,
81    #[serde(default)]
82    pub mcp_servers: HashMap<String, Deployment>,
83    #[serde(default)]
84    pub scheduler: Option<SchedulerConfig>,
85    #[serde(default)]
86    pub ai: AiConfig,
87    #[serde(default)]
88    pub web: Option<WebConfig>,
89    #[serde(default)]
90    pub plugins: HashMap<String, PluginConfig>,
91    #[serde(default)]
92    pub marketplaces: HashMap<MarketplaceId, MarketplaceConfig>,
93    #[serde(default)]
94    pub skills: SkillsConfig,
95    #[serde(default)]
96    pub external_agents: HashMap<ExternalAgentId, ExternalAgentConfig>,
97    #[serde(default)]
98    pub slack_apps: HashMap<String, SlackAppConfig>,
99    #[serde(default)]
100    pub teams_apps: HashMap<String, TeamsAppConfig>,
101    #[serde(default)]
102    pub bridge_policy: Option<BridgePolicyConfig>,
103}
104
105impl ServicesConfig {
106    pub fn apply_port_offset(&mut self, offset: u16) -> Result<(), ConfigValidationError> {
107        if offset == 0 {
108            return Ok(());
109        }
110
111        let shift = |port: u16, what: &str| {
112            port.checked_add(offset).ok_or_else(|| {
113                ConfigValidationError::invalid_field(format!(
114                    "{what} port {port} shifted by services.port_offset {offset} exceeds 65535"
115                ))
116            })
117        };
118
119        for (name, agent) in &mut self.agents {
120            agent.port = shift(agent.port, &format!("Agent '{name}'"))?;
121        }
122
123        for (name, mcp) in &mut self.mcp_servers {
124            if mcp.server_type == McpServerType::External {
125                continue;
126            }
127            mcp.port = shift(mcp.port, &format!("MCP server '{name}'"))?;
128        }
129
130        self.settings.agent_port_range = (
131            shift(self.settings.agent_port_range.0, "agent_port_range lower")?,
132            shift(self.settings.agent_port_range.1, "agent_port_range upper")?,
133        );
134        self.settings.mcp_port_range = (
135            shift(self.settings.mcp_port_range.0, "mcp_port_range lower")?,
136            shift(self.settings.mcp_port_range.1, "mcp_port_range upper")?,
137        );
138
139        Ok(())
140    }
141
142    pub fn validate(&self) -> Result<(), ConfigValidationError> {
143        self.validate_ports()?;
144        self.validate_single_default_agent()?;
145
146        for (name, agent) in &self.agents {
147            agent.validate(name)?;
148        }
149
150        for (name, mcp) in &self.mcp_servers {
151            mcp.validate(name)?;
152        }
153
154        self.validate_skills()?;
155
156        for (name, plugin) in &self.plugins {
157            plugin.validate(name)?;
158            self.validate_plugin_bindings(name, plugin)?;
159        }
160
161        self.validate_single_governance_hook_owner()?;
162
163        for (id, marketplace) in &self.marketplaces {
164            marketplace.validate(id.as_str())?;
165            self.validate_marketplace_bindings(id.as_str(), marketplace)?;
166        }
167
168        self.validate_default_marketplace_selector()?;
169
170        for (name, app) in &self.slack_apps {
171            app.validate(name)?;
172        }
173
174        for (name, app) in &self.teams_apps {
175            app.validate(name)?;
176        }
177
178        Ok(())
179    }
180
181    #[must_use]
182    pub fn marketplace_plugin_configs(
183        &self,
184        marketplace: &MarketplaceConfig,
185    ) -> Vec<&PluginConfig> {
186        self.plugins
187            .values()
188            .filter(|p| p.enabled)
189            .filter(|p| {
190                marketplace.plugins.include.is_empty()
191                    || marketplace
192                        .plugins
193                        .include
194                        .iter()
195                        .any(|inc| inc == p.id.as_str())
196            })
197            .collect()
198    }
199
200    #[must_use]
201    pub fn plugin_selected_skill_ids(
202        &self,
203        plugin: &PluginConfig,
204    ) -> std::collections::BTreeSet<String> {
205        let mut ids: std::collections::BTreeSet<String> = match plugin.skills.source {
206            ComponentSource::Explicit => plugin.skills.include.iter().cloned().collect(),
207            ComponentSource::Instance => self
208                .skills
209                .skills
210                .keys()
211                .filter(|k| !plugin.skills.exclude.iter().any(|ex| ex == *k))
212                .cloned()
213                .collect(),
214        };
215
216        let selected_agent = |name: &str| match plugin.agents.source {
217            ComponentSource::Explicit => plugin.agents.include.iter().any(|inc| inc == name),
218            ComponentSource::Instance => !plugin.agents.exclude.iter().any(|ex| ex == name),
219        };
220        for (name, agent) in &self.agents {
221            if selected_agent(name) {
222                ids.extend(agent.metadata.skills.include.iter().cloned());
223            }
224        }
225
226        ids
227    }
228
229    #[must_use]
230    pub fn marketplace_skill_members(
231        &self,
232        marketplace: &MarketplaceConfig,
233    ) -> std::collections::BTreeSet<String> {
234        self.marketplace_plugin_configs(marketplace)
235            .into_iter()
236            .flat_map(|plugin| self.plugin_selected_skill_ids(plugin))
237            .collect()
238    }
239}