Skip to main content

systemprompt_models/profile/
mod.rs

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