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