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