systemprompt_models/profile/
from_env.rs1use super::{
12 ContentNegotiationConfig, DatabaseConfig, ExtensionsConfig, PathsConfig, Profile, ProfileError,
13 ProfileResult, ProfileType, RateLimitsConfig, RuntimeConfig, SecurityConfig,
14 SecurityHeadersConfig, ServerConfig, SiteConfig, default_agent_registry, default_agents,
15 default_artifacts, default_bridge_auth, default_burst, default_content, default_contexts,
16 default_gateway, default_mcp, default_mcp_registry, default_oauth_auth, default_oauth_public,
17 default_stream, default_tasks,
18};
19use crate::services::SystemAdminConfig;
20
21impl Profile {
22 pub fn from_env(profile_name: &str, display_name: &str) -> ProfileResult<Self> {
23 let db_type = require_env("DATABASE_TYPE")?;
24
25 Ok(Self {
26 name: profile_name.to_owned(),
27 display_name: display_name.to_owned(),
28 target: ProfileType::Cloud,
29 site: site_config_from_env()?,
30 database: DatabaseConfig {
31 db_type,
32 external_db_access: false,
33 pool: None,
34 },
35 server: server_config_from_env()?,
36 paths: paths_config_from_env()?,
37 security: security_config_from_env()?,
38 rate_limits: rate_limits_from_env(),
39 system_admin: SystemAdminConfig {
40 username: require_env("SYSTEM_ADMIN_USERNAME")?,
41 email: system_admin_email_from_env()?,
42 },
43 runtime: runtime_config_from_env()?,
44 cloud: None,
45 secrets: None,
46 extensions: ExtensionsConfig::default(),
47 governance: None,
48 judge: crate::profile::JudgeProfile::default(),
49 retention: crate::profile::RetentionConfig::default(),
50 services: crate::profile::ServicesProfileConfig::default(),
51 storage: crate::profile::StorageConfig::default(),
52 observability: crate::profile::ObservabilityConfig::default(),
53 })
54 }
55}
56
57fn get_env(key: &str) -> Option<String> {
58 std::env::var(key).ok()
59}
60
61fn require_env(name: &'static str) -> ProfileResult<String> {
62 std::env::var(name).map_err(|_e| ProfileError::MissingEnvVar { name })
63}
64
65fn system_admin_email_from_env() -> ProfileResult<Option<systemprompt_identifiers::Email>> {
66 get_env("SYSTEM_ADMIN_EMAIL")
67 .filter(|v| !v.trim().is_empty())
68 .map(|v| {
69 systemprompt_identifiers::Email::try_new(v.trim()).map_err(|e| {
70 ProfileError::InvalidEnvVar {
71 name: "SYSTEM_ADMIN_EMAIL",
72 message: e.to_string(),
73 }
74 })
75 })
76 .transpose()
77}
78
79fn site_config_from_env() -> ProfileResult<SiteConfig> {
80 Ok(SiteConfig {
81 name: require_env("SITENAME")?,
82 github_link: get_env("GITHUB_LINK"),
83 })
84}
85
86fn server_config_from_env() -> ProfileResult<ServerConfig> {
87 let port = require_env("PORT")?
88 .parse()
89 .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
90 name: "PORT",
91 message: e.to_string(),
92 })?;
93
94 Ok(ServerConfig {
95 host: require_env("HOST")?,
96 port,
97 api_server_url: require_env("API_SERVER_URL")?,
98 api_internal_url: require_env("API_INTERNAL_URL")?,
99 api_external_url: require_env("API_EXTERNAL_URL")?,
100 use_https: get_env("USE_HTTPS").is_some_and(|v| v.to_lowercase() == "true"),
101 cors_allowed_origins: get_env("CORS_ALLOWED_ORIGINS").map_or_else(Vec::new, |s| {
102 s.split(',').map(|s| s.trim().to_owned()).collect()
103 }),
104 content_negotiation: ContentNegotiationConfig {
105 enabled: get_env("CONTENT_NEGOTIATION_ENABLED")
106 .is_some_and(|v| v.to_lowercase() == "true"),
107 ..Default::default()
108 },
109 security_headers: SecurityHeadersConfig::default(),
110 instance_id: None,
111 metrics_port: None,
112 max_concurrent_streams: crate::config::DEFAULT_MAX_CONCURRENT_STREAMS,
113 trusted_proxies: Vec::new(),
114 })
115}
116
117fn paths_config_from_env() -> ProfileResult<PathsConfig> {
118 Ok(PathsConfig {
119 system: require_env("SYSTEM_PATH")?,
120 services: require_env("SYSTEMPROMPT_SERVICES_PATH")?,
121 bin: require_env("BIN_PATH")?,
122 storage: get_env("STORAGE_PATH"),
123 geoip_database: get_env("GEOIP_DATABASE_PATH"),
124 web_path: get_env("SYSTEMPROMPT_WEB_PATH"),
125 })
126}
127
128fn security_config_from_env() -> ProfileResult<SecurityConfig> {
129 use crate::auth::JwtAudience;
130
131 let issuer = require_env("JWT_ISSUER")?;
132
133 let access_token_expiration = require_env("JWT_ACCESS_TOKEN_EXPIRATION")?
134 .parse()
135 .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
136 name: "JWT_ACCESS_TOKEN_EXPIRATION",
137 message: e.to_string(),
138 })?;
139
140 let refresh_token_expiration = require_env("JWT_REFRESH_TOKEN_EXPIRATION")?
141 .parse()
142 .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
143 name: "JWT_REFRESH_TOKEN_EXPIRATION",
144 message: e.to_string(),
145 })?;
146
147 let audiences_raw = require_env("JWT_AUDIENCES")?;
148 let audiences = audiences_raw
149 .split(',')
150 .map(|s| {
151 s.trim()
152 .parse::<JwtAudience>()
153 .map_err(|e| ProfileError::InvalidEnvVar {
154 name: "JWT_AUDIENCES",
155 message: e.to_string(),
156 })
157 })
158 .collect::<ProfileResult<Vec<_>>>()?;
159
160 let allow_registration =
161 get_env("ALLOW_REGISTRATION").is_none_or(|s| s.eq_ignore_ascii_case("true"));
162
163 Ok(SecurityConfig {
164 issuer,
165 access_token_expiration,
166 refresh_token_expiration,
167 audiences,
168 allowed_resource_audiences: super::default_resource_audiences(),
169 allow_registration,
170 allow_dynamic_client_registration: true,
171 login_page_url: get_env("LOGIN_PAGE_URL"),
172 signing_key_path: std::path::PathBuf::from("signing_key.pem"),
173 trusted_issuers: Vec::new(),
174 id_jag_ttl_secs: super::security::DEFAULT_ID_JAG_TTL_SECS,
175 })
176}
177
178fn rate_limits_from_env() -> RateLimitsConfig {
179 let parse_rate = |key: &str, default: fn() -> u64| -> u64 {
180 get_env(key)
181 .and_then(|s| {
182 s.parse()
183 .map_err(|e| {
184 tracing::warn!(key = %key, value = %s, error = %e, "Failed to parse rate limit value");
185 e
186 })
187 .ok()
188 })
189 .unwrap_or_else(default)
190 };
191
192 RateLimitsConfig {
193 disabled: get_env("RATE_LIMIT_DISABLED").is_some_and(|v| v.to_lowercase() == "true"),
194 oauth_public_per_second: parse_rate(
195 "RATE_LIMIT_OAUTH_PUBLIC_PER_SECOND",
196 default_oauth_public,
197 ),
198 oauth_auth_per_second: parse_rate("RATE_LIMIT_OAUTH_AUTH_PER_SECOND", default_oauth_auth),
199 contexts_per_second: parse_rate("RATE_LIMIT_CONTEXTS_PER_SECOND", default_contexts),
200 tasks_per_second: parse_rate("RATE_LIMIT_TASKS_PER_SECOND", default_tasks),
201 artifacts_per_second: parse_rate("RATE_LIMIT_ARTIFACTS_PER_SECOND", default_artifacts),
202 agent_registry_per_second: parse_rate(
203 "RATE_LIMIT_AGENT_REGISTRY_PER_SECOND",
204 default_agent_registry,
205 ),
206 agents_per_second: parse_rate("RATE_LIMIT_AGENTS_PER_SECOND", default_agents),
207 mcp_registry_per_second: parse_rate(
208 "RATE_LIMIT_MCP_REGISTRY_PER_SECOND",
209 default_mcp_registry,
210 ),
211 mcp_per_second: parse_rate("RATE_LIMIT_MCP_PER_SECOND", default_mcp),
212 stream_per_second: parse_rate("RATE_LIMIT_STREAM_PER_SECOND", default_stream),
213 content_per_second: parse_rate("RATE_LIMIT_CONTENT_PER_SECOND", default_content),
214 gateway_per_second: parse_rate("RATE_LIMIT_GATEWAY_PER_SECOND", default_gateway),
215 bridge_auth_per_second: parse_rate(
216 "RATE_LIMIT_BRIDGE_AUTH_PER_SECOND",
217 default_bridge_auth,
218 ),
219 burst_multiplier: parse_rate("RATE_LIMIT_BURST_MULTIPLIER", default_burst),
220 }
221}
222
223fn runtime_config_from_env() -> ProfileResult<RuntimeConfig> {
224 let environment = get_env("SYSTEMPROMPT_ENV")
225 .unwrap_or_else(|| "development".to_owned())
226 .parse()
227 .map_err(|e: String| ProfileError::InvalidEnvVar {
228 name: "SYSTEMPROMPT_ENV",
229 message: e,
230 })?;
231
232 let log_level = get_env("SYSTEMPROMPT_LOG_LEVEL")
233 .unwrap_or_else(|| "normal".to_owned())
234 .parse()
235 .map_err(|e: String| ProfileError::InvalidEnvVar {
236 name: "SYSTEMPROMPT_LOG_LEVEL",
237 message: e,
238 })?;
239
240 let output_format = get_env("SYSTEMPROMPT_OUTPUT_FORMAT")
241 .unwrap_or_else(|| "text".to_owned())
242 .parse()
243 .map_err(|e: String| ProfileError::InvalidEnvVar {
244 name: "SYSTEMPROMPT_OUTPUT_FORMAT",
245 message: e,
246 })?;
247
248 Ok(RuntimeConfig {
249 environment,
250 log_level,
251 output_format,
252 no_color: get_env("NO_COLOR").is_some(),
253 non_interactive: get_env("CI").is_some(),
254 })
255}