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