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