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