systemprompt_models/profile/
mod.rs1mod cloud;
15mod database;
16mod error;
17mod from_env;
18mod governance;
19mod info;
20mod oci_reference;
21mod paths;
22mod rate_limits;
23mod runtime;
24mod secrets;
25mod security;
26mod server;
27mod services;
28mod site;
29mod storage;
30mod style;
31mod validation;
32mod vault;
33
34pub use cloud::{CloudConfig, CloudValidationMode};
35pub use database::{DatabaseConfig, PoolConfig};
36pub use error::{ProfileError, ProfileResult};
37pub use governance::{
38 AuthzConfig, AuthzHookConfig, AuthzMode, GovernanceConfig, UNRESTRICTED_ACKNOWLEDGEMENT,
39};
40pub use info::ProfileInfo;
41pub use oci_reference::{OciReference, OciReferenceError};
42pub use paths::{PathsConfig, expand_home, resolve_path, resolve_with_home};
43pub use rate_limits::{
44 RateLimitsConfig, default_agent_registry, default_agents, default_artifacts, default_burst,
45 default_content, default_contexts, default_mcp, default_mcp_registry, default_oauth_auth,
46 default_oauth_public, default_stream, default_tasks,
47};
48pub use runtime::{Environment, LogLevel, OutputFormat, RuntimeConfig};
49pub use secrets::{SecretsConfig, SecretsSource, SecretsValidationMode};
50pub use security::{
51 DEFAULT_ID_JAG_TTL_SECS, GATEWAY_REQUIRED_RESOURCE_AUDIENCES, SecurityConfig, TrustedIssuer,
52 default_resource_audiences,
53};
54pub use server::{
55 ContentNegotiationConfig, FrameOptions, ReferrerPolicy, SecurityHeadersConfig, ServerConfig,
56};
57pub use services::{
58 BundleVerification, FetchFailurePolicy, HttpsServicesSource, OciServicesSource,
59 ServicesProfileConfig, ServicesSource,
60};
61pub use site::SiteConfig;
62pub use storage::{StorageBackend, StorageConfig};
63pub use style::ProfileStyle;
64pub use vault::{
65 DEFAULT_VAULT_RETRIES, DEFAULT_VAULT_TIMEOUT_SECS, MAX_VAULT_RETRIES, MAX_VAULT_TIMEOUT_SECS,
66 VaultAuth, VaultKeyRef, VaultSecretsConfig,
67};
68
69use serde::{Deserialize, Serialize};
70use std::path::Path;
71
72use crate::env::{interpolate, read_env_optional};
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
75#[serde(deny_unknown_fields)]
76pub struct ExtensionsConfig {
77 #[serde(default)]
78 pub disabled: Vec<String>,
79}
80
81impl ExtensionsConfig {
82 pub fn is_disabled(&self, extension_id: &str) -> bool {
83 self.disabled.iter().any(|id| id == extension_id)
84 }
85}
86
87#[derive(
88 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
89)]
90#[serde(rename_all = "lowercase")]
91pub enum ProfileType {
92 #[default]
93 Local,
94 Cloud,
95}
96
97impl ProfileType {
98 pub const fn is_cloud(&self) -> bool {
99 matches!(self, Self::Cloud)
100 }
101
102 pub const fn is_local(&self) -> bool {
103 matches!(self, Self::Local)
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
108#[serde(deny_unknown_fields)]
109pub struct Profile {
110 pub name: String,
111
112 pub display_name: String,
113
114 #[serde(default)]
115 pub target: ProfileType,
116
117 pub site: SiteConfig,
118
119 pub database: DatabaseConfig,
120
121 pub server: ServerConfig,
122
123 pub paths: PathsConfig,
124
125 pub security: SecurityConfig,
126
127 pub rate_limits: RateLimitsConfig,
128
129 pub system_admin: crate::services::SystemAdminConfig,
130
131 #[serde(default)]
132 pub runtime: RuntimeConfig,
133
134 #[serde(default)]
135 pub cloud: Option<CloudConfig>,
136
137 #[serde(default)]
138 pub secrets: Option<SecretsConfig>,
139
140 #[serde(default)]
141 pub extensions: ExtensionsConfig,
142
143 #[serde(default)]
144 pub governance: Option<GovernanceConfig>,
145
146 #[serde(default)]
147 pub services: ServicesProfileConfig,
148
149 #[serde(default)]
150 pub storage: StorageConfig,
151}
152
153const MOVED_SECTIONS: &[(&str, &str)] = &[
154 ("providers", "services/ai/providers.yaml"),
155 ("gateway", "services/ai/gateway.yaml"),
156];
157
158fn reject_moved_sections(content: &str, profile_path: &Path) -> ProfileResult<()> {
159 let Ok(serde_yaml::Value::Mapping(map)) = serde_yaml::from_str::<serde_yaml::Value>(content)
160 else {
161 return Ok(());
162 };
163 for (key, destination) in MOVED_SECTIONS {
164 if map.contains_key(serde_yaml::Value::String((*key).to_owned())) {
165 return Err(ProfileError::MovedToServices {
166 path: profile_path.to_path_buf(),
167 key: (*key).to_owned(),
168 destination: (*destination).to_owned(),
169 });
170 }
171 }
172 Ok(())
173}
174
175impl Profile {
176 #[must_use]
177 pub fn is_local_trial(&self) -> bool {
178 self.cloud.as_ref().is_none_or(CloudConfig::is_local_trial)
179 }
180
181 #[must_use]
182 pub const fn path_resolution(&self) -> crate::paths::PathResolution {
183 if self.target.is_cloud() {
184 crate::paths::PathResolution::Lexical
185 } else {
186 crate::paths::PathResolution::Canonicalize
187 }
188 }
189
190 pub fn from_yaml(content: &str, profile_path: &Path) -> ProfileResult<Self> {
191 let content = interpolate(content, &|name| read_env_optional(name));
192
193 reject_moved_sections(&content, profile_path)?;
194
195 let mut profile: Self =
196 serde_yaml::from_str(&content).map_err(|source| ProfileError::ParseYaml {
197 path: profile_path.to_path_buf(),
198 source,
199 })?;
200
201 let profile_dir =
202 profile_path
203 .parent()
204 .ok_or_else(|| ProfileError::InvalidProfilePath {
205 path: profile_path.to_path_buf(),
206 })?;
207
208 profile.paths.resolve_relative_to(profile_dir);
209
210 if let Some(secrets) = profile.secrets.as_ref() {
211 secrets.validate()?;
212 }
213
214 Ok(profile)
215 }
216
217 pub fn to_yaml(&self) -> ProfileResult<String> {
218 serde_yaml::to_string(self).map_err(ProfileError::SerializeYaml)
219 }
220
221 pub fn profile_style(&self) -> ProfileStyle {
222 match self.name.to_lowercase().as_str() {
223 "dev" | "development" | "local" => ProfileStyle::Development,
224 "prod" | "production" => ProfileStyle::Production,
225 "staging" | "stage" => ProfileStyle::Staging,
226 "test" | "testing" => ProfileStyle::Test,
227 _ => ProfileStyle::Custom,
228 }
229 }
230
231 pub fn mask_secret(value: &str, visible_chars: usize) -> String {
232 if value.is_empty() {
233 return "(not set)".to_owned();
234 }
235 if value.len() <= visible_chars {
236 return "***".to_owned();
237 }
238 format!("{}...", &value[..visible_chars])
239 }
240
241 pub fn mask_database_url(url: &str) -> String {
242 if let Some(at_pos) = url.find('@')
243 && let Some(colon_pos) = url[..at_pos].rfind(':')
244 {
245 let prefix = &url[..=colon_pos];
246 let suffix = &url[at_pos..];
247 return format!("{}***{}", prefix, suffix);
248 }
249 url.to_owned()
250 }
251
252 pub fn is_masked_database_url(url: &str) -> bool {
253 url.contains(":***@") || url.contains(":********@")
254 }
255}