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, TierMultipliers, default_agent_registry,
15    default_agents, default_artifacts, default_burst, default_content, default_contexts,
16    default_mcp, default_mcp_registry, default_oauth_auth, default_oauth_public, default_stream,
17    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            },
42            runtime: runtime_config_from_env()?,
43            cloud: None,
44            secrets: None,
45            extensions: ExtensionsConfig::default(),
46            providers: crate::profile::ProviderRegistry::default(),
47            gateway: None,
48            governance: None,
49        })
50    }
51}
52
53fn get_env(key: &str) -> Option<String> {
54    std::env::var(key).ok()
55}
56
57fn require_env(name: &'static str) -> ProfileResult<String> {
58    std::env::var(name).map_err(|_e| ProfileError::MissingEnvVar { name })
59}
60
61fn site_config_from_env() -> ProfileResult<SiteConfig> {
62    Ok(SiteConfig {
63        name: require_env("SITENAME")?,
64        github_link: get_env("GITHUB_LINK"),
65    })
66}
67
68fn server_config_from_env() -> ProfileResult<ServerConfig> {
69    let port = require_env("PORT")?
70        .parse()
71        .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
72            name: "PORT",
73            message: e.to_string(),
74        })?;
75
76    Ok(ServerConfig {
77        host: require_env("HOST")?,
78        port,
79        api_server_url: require_env("API_SERVER_URL")?,
80        api_internal_url: require_env("API_INTERNAL_URL")?,
81        api_external_url: require_env("API_EXTERNAL_URL")?,
82        use_https: get_env("USE_HTTPS").is_some_and(|v| v.to_lowercase() == "true"),
83        cors_allowed_origins: get_env("CORS_ALLOWED_ORIGINS").map_or_else(Vec::new, |s| {
84            s.split(',').map(|s| s.trim().to_owned()).collect()
85        }),
86        content_negotiation: ContentNegotiationConfig {
87            enabled: get_env("CONTENT_NEGOTIATION_ENABLED")
88                .is_some_and(|v| v.to_lowercase() == "true"),
89            ..Default::default()
90        },
91        security_headers: SecurityHeadersConfig::default(),
92        instance_id: None,
93        max_concurrent_streams: crate::config::DEFAULT_MAX_CONCURRENT_STREAMS,
94        trusted_proxies: Vec::new(),
95    })
96}
97
98fn paths_config_from_env() -> ProfileResult<PathsConfig> {
99    Ok(PathsConfig {
100        system: require_env("SYSTEM_PATH")?,
101        services: require_env("SYSTEMPROMPT_SERVICES_PATH")?,
102        bin: require_env("BIN_PATH")?,
103        storage: get_env("STORAGE_PATH"),
104        geoip_database: get_env("GEOIP_DATABASE_PATH"),
105        web_path: get_env("SYSTEMPROMPT_WEB_PATH"),
106    })
107}
108
109fn security_config_from_env() -> ProfileResult<SecurityConfig> {
110    use crate::auth::JwtAudience;
111
112    let issuer = require_env("JWT_ISSUER")?;
113
114    let access_token_expiration = require_env("JWT_ACCESS_TOKEN_EXPIRATION")?
115        .parse()
116        .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
117            name: "JWT_ACCESS_TOKEN_EXPIRATION",
118            message: e.to_string(),
119        })?;
120
121    let refresh_token_expiration = require_env("JWT_REFRESH_TOKEN_EXPIRATION")?
122        .parse()
123        .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
124            name: "JWT_REFRESH_TOKEN_EXPIRATION",
125            message: e.to_string(),
126        })?;
127
128    let audiences_raw = require_env("JWT_AUDIENCES")?;
129    let audiences = audiences_raw
130        .split(',')
131        .map(|s| {
132            s.trim()
133                .parse::<JwtAudience>()
134                .map_err(|e| ProfileError::InvalidEnvVar {
135                    name: "JWT_AUDIENCES",
136                    message: e.to_string(),
137                })
138        })
139        .collect::<ProfileResult<Vec<_>>>()?;
140
141    let allow_registration =
142        get_env("ALLOW_REGISTRATION").is_none_or(|s| s.eq_ignore_ascii_case("true"));
143
144    Ok(SecurityConfig {
145        issuer,
146        access_token_expiration,
147        refresh_token_expiration,
148        audiences,
149        allowed_resource_audiences: super::default_resource_audiences(),
150        allow_registration,
151        signing_key_path: std::path::PathBuf::from("signing_key.pem"),
152        trusted_issuers: Vec::new(),
153        id_jag_ttl_secs: super::security::DEFAULT_ID_JAG_TTL_SECS,
154    })
155}
156
157fn rate_limits_from_env() -> RateLimitsConfig {
158    let parse_rate = |key: &str, default: fn() -> u64| -> u64 {
159        get_env(key)
160            .and_then(|s| {
161                s.parse()
162                    .map_err(|e| {
163                        tracing::warn!(key = %key, value = %s, error = %e, "Failed to parse rate limit value");
164                        e
165                    })
166                    .ok()
167            })
168            .unwrap_or_else(default)
169    };
170
171    RateLimitsConfig {
172        disabled: get_env("RATE_LIMIT_DISABLED").is_some_and(|v| v.to_lowercase() == "true"),
173        oauth_public_per_second: parse_rate(
174            "RATE_LIMIT_OAUTH_PUBLIC_PER_SECOND",
175            default_oauth_public,
176        ),
177        oauth_auth_per_second: parse_rate("RATE_LIMIT_OAUTH_AUTH_PER_SECOND", default_oauth_auth),
178        contexts_per_second: parse_rate("RATE_LIMIT_CONTEXTS_PER_SECOND", default_contexts),
179        tasks_per_second: parse_rate("RATE_LIMIT_TASKS_PER_SECOND", default_tasks),
180        artifacts_per_second: parse_rate("RATE_LIMIT_ARTIFACTS_PER_SECOND", default_artifacts),
181        agent_registry_per_second: parse_rate(
182            "RATE_LIMIT_AGENT_REGISTRY_PER_SECOND",
183            default_agent_registry,
184        ),
185        agents_per_second: parse_rate("RATE_LIMIT_AGENTS_PER_SECOND", default_agents),
186        mcp_registry_per_second: parse_rate(
187            "RATE_LIMIT_MCP_REGISTRY_PER_SECOND",
188            default_mcp_registry,
189        ),
190        mcp_per_second: parse_rate("RATE_LIMIT_MCP_PER_SECOND", default_mcp),
191        stream_per_second: parse_rate("RATE_LIMIT_STREAM_PER_SECOND", default_stream),
192        content_per_second: parse_rate("RATE_LIMIT_CONTENT_PER_SECOND", default_content),
193        burst_multiplier: parse_rate("RATE_LIMIT_BURST_MULTIPLIER", default_burst),
194        tier_multipliers: TierMultipliers::default(),
195    }
196}
197
198fn runtime_config_from_env() -> ProfileResult<RuntimeConfig> {
199    let environment = get_env("SYSTEMPROMPT_ENV")
200        .unwrap_or_else(|| "development".to_owned())
201        .parse()
202        .map_err(|e: String| ProfileError::InvalidEnvVar {
203            name: "SYSTEMPROMPT_ENV",
204            message: e,
205        })?;
206
207    let log_level = get_env("SYSTEMPROMPT_LOG_LEVEL")
208        .unwrap_or_else(|| "normal".to_owned())
209        .parse()
210        .map_err(|e: String| ProfileError::InvalidEnvVar {
211            name: "SYSTEMPROMPT_LOG_LEVEL",
212            message: e,
213        })?;
214
215    let output_format = get_env("SYSTEMPROMPT_OUTPUT_FORMAT")
216        .unwrap_or_else(|| "text".to_owned())
217        .parse()
218        .map_err(|e: String| ProfileError::InvalidEnvVar {
219            name: "SYSTEMPROMPT_OUTPUT_FORMAT",
220            message: e,
221        })?;
222
223    Ok(RuntimeConfig {
224        environment,
225        log_level,
226        output_format,
227        no_color: get_env("NO_COLOR").is_some(),
228        non_interactive: get_env("CI").is_some(),
229    })
230}