systemprompt_models/profile/
mod.rs1mod cloud;
15mod database;
16mod error;
17mod evaluation;
18mod from_env;
19mod governance;
20mod info;
21mod observability;
22mod oci_reference;
23mod paths;
24mod rate_limits;
25mod runtime;
26mod secrets;
27mod security;
28mod server;
29mod services;
30mod site;
31mod storage;
32mod style;
33mod validation;
34mod vault;
35
36pub use cloud::{CloudConfig, CloudValidationMode};
37pub use database::{DatabaseConfig, PoolConfig};
38pub use error::{ProfileError, ProfileResult};
39pub use evaluation::EvaluationProfile;
40pub use governance::{
41 AuditConfig, AuthzConfig, AuthzHookConfig, AuthzMode, GovernanceConfig,
42 UNRESTRICTED_ACKNOWLEDGEMENT,
43};
44pub use info::ProfileInfo;
45pub use observability::{ObservabilityConfig, OtlpExportConfig, OtlpProtocol, OtlpSignal};
46pub use oci_reference::{OciReference, OciReferenceError};
47pub use paths::{PathsConfig, expand_home, resolve_path, resolve_with_home};
48pub use rate_limits::{
49 RateLimitsConfig, default_agent_registry, default_agents, default_artifacts, default_burst,
50 default_content, default_contexts, default_gateway, default_mcp, default_mcp_registry,
51 default_oauth_auth, default_oauth_public, default_stream, default_tasks,
52};
53pub use runtime::{Environment, LogLevel, OutputFormat, RuntimeConfig};
54pub use secrets::{SecretsConfig, SecretsSource, SecretsValidationMode};
55pub use security::{
56 DEFAULT_ID_JAG_TTL_SECS, GATEWAY_REQUIRED_RESOURCE_AUDIENCES, SecurityConfig, TrustedIssuer,
57 default_resource_audiences,
58};
59pub use server::{
60 ContentNegotiationConfig, FrameOptions, ReferrerPolicy, SecurityHeadersConfig, ServerConfig,
61};
62pub use services::{
63 BundleVerification, FetchFailurePolicy, HttpsServicesSource, OciServicesSource,
64 ServicesProfileConfig, ServicesSource,
65};
66pub use site::SiteConfig;
67pub use storage::{StorageBackend, StorageConfig};
68pub use style::ProfileStyle;
69pub use vault::{
70 DEFAULT_VAULT_RETRIES, DEFAULT_VAULT_TIMEOUT_SECS, MAX_VAULT_RETRIES, MAX_VAULT_TIMEOUT_SECS,
71 VaultAuth, VaultKeyRef, VaultSecretsConfig,
72};
73
74use serde::{Deserialize, Serialize};
75use std::path::Path;
76
77use crate::env::{interpolate, read_env_optional};
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
80#[serde(deny_unknown_fields)]
81pub struct ExtensionsConfig {
82 #[serde(default)]
83 pub disabled: Vec<String>,
84}
85
86impl ExtensionsConfig {
87 pub fn is_disabled(&self, extension_id: &str) -> bool {
88 self.disabled.iter().any(|id| id == extension_id)
89 }
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 governance: Option<GovernanceConfig>,
150
151 #[serde(default)]
152 pub evaluation: EvaluationProfile,
153
154 #[serde(default)]
155 pub services: ServicesProfileConfig,
156
157 #[serde(default)]
158 pub storage: StorageConfig,
159
160 #[serde(default)]
161 pub observability: ObservabilityConfig,
162}
163
164const MOVED_SECTIONS: &[(&str, &str)] = &[
165 ("providers", "services/ai/providers.yaml"),
166 ("gateway", "services/ai/gateway.yaml"),
167];
168
169fn reject_moved_sections(content: &str, profile_path: &Path) -> ProfileResult<()> {
170 let Ok(serde_yaml::Value::Mapping(map)) = serde_yaml::from_str::<serde_yaml::Value>(content)
171 else {
172 return Ok(());
173 };
174 for (key, destination) in MOVED_SECTIONS {
175 if map.contains_key(serde_yaml::Value::String((*key).to_owned())) {
176 return Err(ProfileError::MovedToServices {
177 path: profile_path.to_path_buf(),
178 key: (*key).to_owned(),
179 destination: (*destination).to_owned(),
180 });
181 }
182 }
183 Ok(())
184}
185
186impl Profile {
187 #[must_use]
188 pub fn is_local_trial(&self) -> bool {
189 self.cloud
190 .as_ref()
191 .map_or_else(|| self.target.is_local(), CloudConfig::is_local_trial)
192 }
193
194 #[must_use]
195 pub const fn path_resolution(&self) -> crate::paths::PathResolution {
196 if self.target.is_cloud() {
197 crate::paths::PathResolution::Lexical
198 } else {
199 crate::paths::PathResolution::Canonicalize
200 }
201 }
202
203 pub fn from_yaml(content: &str, profile_path: &Path) -> ProfileResult<Self> {
204 let content = interpolate(content, &|name| read_env_optional(name));
205
206 reject_moved_sections(&content, profile_path)?;
207
208 let mut profile: Self =
209 serde_yaml::from_str(&content).map_err(|source| ProfileError::ParseYaml {
210 path: profile_path.to_path_buf(),
211 source,
212 })?;
213
214 let profile_dir =
215 profile_path
216 .parent()
217 .ok_or_else(|| ProfileError::InvalidProfilePath {
218 path: profile_path.to_path_buf(),
219 })?;
220
221 profile.paths.resolve_relative_to(profile_dir);
222
223 if let Some(secrets) = profile.secrets.as_ref() {
224 secrets.validate()?;
225 }
226
227 Ok(profile)
228 }
229
230 pub fn to_yaml(&self) -> ProfileResult<String> {
231 serde_yaml::to_string(self).map_err(ProfileError::SerializeYaml)
232 }
233
234 pub fn payload_cap_bytes(&self) -> usize {
235 self.governance
236 .as_ref()
237 .map_or(AuditConfig::DEFAULT_PAYLOAD_CAP_BYTES, |governance| {
238 governance.audit.payload_cap_bytes
239 })
240 }
241
242 pub fn profile_style(&self) -> ProfileStyle {
243 match self.name.to_lowercase().as_str() {
244 "dev" | "development" | "local" => ProfileStyle::Development,
245 "prod" | "production" => ProfileStyle::Production,
246 "staging" | "stage" => ProfileStyle::Staging,
247 "test" | "testing" => ProfileStyle::Test,
248 _ => ProfileStyle::Custom,
249 }
250 }
251
252 pub fn mask_secret(value: &str, visible_chars: usize) -> String {
253 if value.is_empty() {
254 return "(not set)".to_owned();
255 }
256 if value.len() <= visible_chars {
257 return "***".to_owned();
258 }
259 format!("{}...", &value[..visible_chars])
260 }
261
262 pub fn mask_database_url(url: &str) -> String {
263 if let Some(at_pos) = url.find('@')
264 && let Some(colon_pos) = url[..at_pos].rfind(':')
265 {
266 let prefix = &url[..=colon_pos];
267 let suffix = &url[at_pos..];
268 return format!("{}***{}", prefix, suffix);
269 }
270 url.to_owned()
271 }
272
273 pub fn is_masked_database_url(url: &str) -> bool {
274 url.contains(":***@") || url.contains(":********@")
275 }
276}