Skip to main content

systemprompt_models/profile/
mod.rs

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