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_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}