Skip to main content

systemprompt_models/profile/
from_env.rs

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