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