Skip to main content

systemprompt_models/profile/
mod.rs

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