systemprompt_models/services/
mod.rs1pub 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};
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> {
202 self.providers
203 .validate()
204 .map_err(|e| ConfigValidationError::invalid_field(format!("providers: {e}")))?;
205 match &self.gateway {
206 Some(GatewayState::Resolved(config)) => config.validate(&self.providers),
207 Some(GatewayState::Spec(spec)) => spec.clone().resolve().validate(&self.providers),
208 None => Ok(()),
209 }
210 .map_err(|e| ConfigValidationError::invalid_field(format!("gateway: {e}")))
211 }
212
213 #[must_use]
214 pub fn gateway_config(&self) -> Option<&GatewayConfig> {
215 self.gateway.as_ref().and_then(GatewayState::resolved)
216 }
217
218 #[must_use]
222 pub fn enabled_marketplaces(&self) -> Vec<&MarketplaceConfig> {
223 let mut out: Vec<&MarketplaceConfig> =
224 self.marketplaces.values().filter(|m| m.enabled).collect();
225 out.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str()));
226 out
227 }
228
229 #[must_use]
230 pub fn marketplace_plugin_configs(
231 &self,
232 marketplace: &MarketplaceConfig,
233 ) -> Vec<&PluginConfig> {
234 let mut out: Vec<&PluginConfig> = self
235 .plugins
236 .values()
237 .filter(|p| p.enabled)
238 .filter(|p| {
239 marketplace.plugins.include.is_empty()
240 || marketplace
241 .plugins
242 .include
243 .iter()
244 .any(|inc| inc == p.id.as_str())
245 })
246 .collect();
247 out.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str()));
248 out
249 }
250
251 #[must_use]
252 pub fn plugin_selected_skill_ids(
253 &self,
254 plugin: &PluginConfig,
255 ) -> std::collections::BTreeSet<String> {
256 let mut ids: std::collections::BTreeSet<String> = match plugin.skills.source {
257 ComponentSource::Explicit => plugin.skills.include.iter().cloned().collect(),
258 ComponentSource::Instance => self
259 .skills
260 .skills
261 .keys()
262 .filter(|k| !plugin.skills.exclude.iter().any(|ex| ex == *k))
263 .cloned()
264 .collect(),
265 };
266
267 let selected_agent = |name: &str| match plugin.agents.source {
268 ComponentSource::Explicit => plugin.agents.include.iter().any(|inc| inc == name),
269 ComponentSource::Instance => !plugin.agents.exclude.iter().any(|ex| ex == name),
270 };
271 for (name, agent) in &self.agents {
272 if selected_agent(name) {
273 ids.extend(agent.metadata.skills.include.iter().cloned());
274 }
275 }
276
277 ids
278 }
279
280 #[must_use]
281 pub fn marketplace_skill_members(
282 &self,
283 marketplace: &MarketplaceConfig,
284 ) -> std::collections::BTreeSet<String> {
285 self.marketplace_plugin_configs(marketplace)
286 .into_iter()
287 .flat_map(|plugin| self.plugin_selected_skill_ids(plugin))
288 .collect()
289 }
290}