Skip to main content

systemprompt_models/profile/
mod.rs

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