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