Skip to main content

systemprompt_models/profile/
mod.rs

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