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