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