Skip to main content

systemprompt_models/profile/
mod.rs

1//! Profile configuration models — the deserialized shape of a
2//! `.systemprompt/profiles/<name>/profile.yaml` document.
3//!
4//! Covers server, database, paths, secrets, security, rate limits,
5//! gateway, governance, and runtime sections, plus validation rules and
6//! environment-variable interpolation.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod cloud;
12mod database;
13mod error;
14mod from_env;
15mod gateway;
16mod governance;
17mod info;
18mod paths;
19mod providers;
20mod rate_limits;
21mod runtime;
22mod secrets;
23mod security;
24mod server;
25mod site;
26mod style;
27mod validation;
28
29pub use cloud::{CloudConfig, CloudValidationMode};
30pub use database::{DatabaseConfig, PoolConfig};
31pub use error::{ProfileError, ProfileResult};
32pub use gateway::{
33    GatewayConfig, GatewayConfigSpec, GatewayProfileError, GatewayResult, GatewayRoute,
34    GatewayState, OverrideRuleAction, ResponseFormatKind, RouteMatch, SystemPromptRule,
35    slugify_pattern, synthesize_route_id,
36};
37pub use governance::{
38    AuthzConfig, AuthzHookConfig, AuthzMode, GovernanceConfig, UNRESTRICTED_ACKNOWLEDGEMENT,
39};
40pub use info::ProfileInfo;
41pub use paths::{PathsConfig, expand_home, resolve_path, resolve_with_home};
42pub use providers::{
43    ApiSurface, ProviderEntry, ProviderModel, ProviderRegistry, ProviderRegistryError,
44    ProviderRegistryResult, WireProtocol,
45};
46pub use rate_limits::{
47    RateLimitsConfig, TierMultipliers, default_a2a_multiplier, default_admin_multiplier,
48    default_agent_registry, default_agents, default_anon_multiplier, default_artifacts,
49    default_burst, default_content, default_contexts, default_mcp, default_mcp_multiplier,
50    default_mcp_registry, default_oauth_auth, default_oauth_public, default_service_multiplier,
51    default_stream, default_tasks, default_user_multiplier,
52};
53pub use runtime::{Environment, LogLevel, OutputFormat, RuntimeConfig};
54pub use secrets::{SecretsConfig, SecretsSource, SecretsValidationMode};
55pub use security::{
56    DEFAULT_ID_JAG_TTL_SECS, GATEWAY_REQUIRED_RESOURCE_AUDIENCES, SecurityConfig, TrustedIssuer,
57    default_resource_audiences,
58};
59pub use server::{ContentNegotiationConfig, SecurityHeadersConfig, ServerConfig};
60pub use site::SiteConfig;
61pub use style::ProfileStyle;
62
63use serde::{Deserialize, Serialize};
64use std::path::Path;
65
66use crate::env::{interpolate, read_env_optional};
67
68#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
69#[serde(deny_unknown_fields)]
70pub struct ExtensionsConfig {
71    #[serde(default)]
72    pub disabled: Vec<String>,
73}
74
75impl ExtensionsConfig {
76    pub fn is_disabled(&self, extension_id: &str) -> bool {
77        self.disabled.iter().any(|id| id == extension_id)
78    }
79}
80
81#[derive(
82    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
83)]
84#[serde(rename_all = "lowercase")]
85pub enum ProfileType {
86    #[default]
87    Local,
88    Cloud,
89}
90
91impl ProfileType {
92    pub const fn is_cloud(&self) -> bool {
93        matches!(self, Self::Cloud)
94    }
95
96    pub const fn is_local(&self) -> bool {
97        matches!(self, Self::Local)
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
102#[serde(deny_unknown_fields)]
103pub struct Profile {
104    pub name: String,
105
106    pub display_name: String,
107
108    #[serde(default)]
109    pub target: ProfileType,
110
111    pub site: SiteConfig,
112
113    pub database: DatabaseConfig,
114
115    pub server: ServerConfig,
116
117    pub paths: PathsConfig,
118
119    pub security: SecurityConfig,
120
121    pub rate_limits: RateLimitsConfig,
122
123    pub system_admin: crate::services::SystemAdminConfig,
124
125    #[serde(default)]
126    pub runtime: RuntimeConfig,
127
128    #[serde(default)]
129    pub cloud: Option<CloudConfig>,
130
131    #[serde(default)]
132    pub secrets: Option<SecretsConfig>,
133
134    #[serde(default)]
135    pub extensions: ExtensionsConfig,
136
137    #[serde(default)]
138    pub providers: ProviderRegistry,
139
140    #[serde(default)]
141    pub gateway: Option<GatewayState>,
142
143    #[serde(default)]
144    pub governance: Option<GovernanceConfig>,
145}
146
147impl Profile {
148    #[must_use]
149    pub fn is_local_trial(&self) -> bool {
150        self.cloud.as_ref().is_none_or(CloudConfig::is_local_trial)
151    }
152
153    pub fn from_yaml(content: &str, profile_path: &Path) -> ProfileResult<Self> {
154        let content = interpolate(content, &|name| read_env_optional(name));
155
156        let mut profile: Self =
157            serde_yaml::from_str(&content).map_err(|source| ProfileError::ParseYaml {
158                path: profile_path.to_path_buf(),
159                source,
160            })?;
161
162        let profile_dir =
163            profile_path
164                .parent()
165                .ok_or_else(|| ProfileError::InvalidProfilePath {
166                    path: profile_path.to_path_buf(),
167                })?;
168
169        profile.paths.resolve_relative_to(profile_dir);
170
171        Ok(profile)
172    }
173
174    pub fn to_yaml(&self) -> ProfileResult<String> {
175        serde_yaml::to_string(self).map_err(ProfileError::SerializeYaml)
176    }
177
178    pub fn profile_style(&self) -> ProfileStyle {
179        match self.name.to_lowercase().as_str() {
180            "dev" | "development" | "local" => ProfileStyle::Development,
181            "prod" | "production" => ProfileStyle::Production,
182            "staging" | "stage" => ProfileStyle::Staging,
183            "test" | "testing" => ProfileStyle::Test,
184            _ => ProfileStyle::Custom,
185        }
186    }
187
188    pub fn mask_secret(value: &str, visible_chars: usize) -> String {
189        if value.is_empty() {
190            return "(not set)".to_owned();
191        }
192        if value.len() <= visible_chars {
193            return "***".to_owned();
194        }
195        format!("{}...", &value[..visible_chars])
196    }
197
198    pub fn mask_database_url(url: &str) -> String {
199        if let Some(at_pos) = url.find('@')
200            && let Some(colon_pos) = url[..at_pos].rfind(':')
201        {
202            let prefix = &url[..=colon_pos];
203            let suffix = &url[at_pos..];
204            return format!("{}***{}", prefix, suffix);
205        }
206        url.to_owned()
207    }
208
209    pub fn is_masked_database_url(url: &str) -> bool {
210        url.contains(":***@") || url.contains(":********@")
211    }
212}