Skip to main content

systemprompt_models/profile/
from_env.rs

1//! Profile creation from environment variables.
2//!
3//! This module provides functionality to create Profile configurations
4//! from environment variables, typically used in cloud/container deployments.
5
6use super::{
7    default_agent_registry, default_agents, default_artifacts, default_burst, default_content,
8    default_contexts, default_mcp, default_mcp_registry, default_oauth_auth, default_oauth_public,
9    default_stream, default_tasks, ContentNegotiationConfig, DatabaseConfig, ExtensionsConfig,
10    PathsConfig, Profile, ProfileType, RateLimitsConfig, RuntimeConfig, SecurityConfig,
11    SecurityHeadersConfig, ServerConfig, SiteConfig, TierMultipliers,
12};
13use anyhow::{Context, Result};
14
15impl Profile {
16    /// Creates a Profile from environment variables.
17    ///
18    /// This is primarily used for cloud deployments where configuration
19    /// is passed via environment variables rather than files.
20    pub fn from_env(profile_name: &str, display_name: &str) -> Result<Self> {
21        let require_env = |key: &str| -> Result<String> {
22            std::env::var(key)
23                .with_context(|| format!("Missing required environment variable: {}", key))
24        };
25
26        let db_type = get_env("DATABASE_TYPE")
27            .ok_or_else(|| anyhow::anyhow!("DATABASE_TYPE environment variable is required"))?;
28
29        Ok(Self {
30            name: profile_name.to_string(),
31            display_name: display_name.to_string(),
32            target: ProfileType::Cloud,
33            site: site_config_from_env(&require_env)?,
34            database: DatabaseConfig {
35                db_type,
36                external_db_access: false,
37            },
38            server: server_config_from_env(&require_env)?,
39            paths: paths_config_from_env(&require_env)?,
40            security: security_config_from_env()?,
41            rate_limits: rate_limits_from_env(),
42            runtime: runtime_config_from_env()?,
43            cloud: None,
44            secrets: None,
45            extensions: ExtensionsConfig::default(),
46        })
47    }
48}
49
50fn get_env(key: &str) -> Option<String> {
51    std::env::var(key).ok()
52}
53
54fn site_config_from_env(require_env: &dyn Fn(&str) -> Result<String>) -> Result<SiteConfig> {
55    Ok(SiteConfig {
56        name: require_env("SITENAME")?,
57        github_link: get_env("GITHUB_LINK"),
58    })
59}
60
61fn server_config_from_env(require_env: &dyn Fn(&str) -> Result<String>) -> Result<ServerConfig> {
62    Ok(ServerConfig {
63        host: require_env("HOST")?,
64        port: require_env("PORT")?.parse().context("Invalid PORT")?,
65        api_server_url: require_env("API_SERVER_URL")?,
66        api_internal_url: require_env("API_INTERNAL_URL")?,
67        api_external_url: require_env("API_EXTERNAL_URL")?,
68        use_https: get_env("USE_HTTPS").is_some_and(|v| v.to_lowercase() == "true"),
69        cors_allowed_origins: get_env("CORS_ALLOWED_ORIGINS").map_or_else(Vec::new, |s| {
70            s.split(',').map(|s| s.trim().to_string()).collect()
71        }),
72        content_negotiation: ContentNegotiationConfig {
73            enabled: get_env("CONTENT_NEGOTIATION_ENABLED")
74                .is_some_and(|v| v.to_lowercase() == "true"),
75            ..Default::default()
76        },
77        security_headers: SecurityHeadersConfig::default(),
78    })
79}
80
81fn paths_config_from_env(require_env: &dyn Fn(&str) -> Result<String>) -> Result<PathsConfig> {
82    Ok(PathsConfig {
83        system: require_env("SYSTEM_PATH")?,
84        services: require_env("SYSTEMPROMPT_SERVICES_PATH")?,
85        bin: require_env("BIN_PATH")?,
86        storage: get_env("STORAGE_PATH"),
87        geoip_database: get_env("GEOIP_DATABASE_PATH"),
88        web_path: get_env("SYSTEMPROMPT_WEB_PATH"),
89    })
90}
91
92fn security_config_from_env() -> Result<SecurityConfig> {
93    use crate::auth::JwtAudience;
94
95    let issuer = get_env("JWT_ISSUER")
96        .ok_or_else(|| anyhow::anyhow!("JWT_ISSUER environment variable is required"))?;
97
98    let access_token_expiration = get_env("JWT_ACCESS_TOKEN_EXPIRATION")
99        .ok_or_else(|| {
100            anyhow::anyhow!("JWT_ACCESS_TOKEN_EXPIRATION environment variable is required")
101        })?
102        .parse()
103        .map_err(|e| anyhow::anyhow!("Failed to parse JWT_ACCESS_TOKEN_EXPIRATION: {e}"))?;
104
105    let refresh_token_expiration = get_env("JWT_REFRESH_TOKEN_EXPIRATION")
106        .ok_or_else(|| {
107            anyhow::anyhow!("JWT_REFRESH_TOKEN_EXPIRATION environment variable is required")
108        })?
109        .parse()
110        .map_err(|e| anyhow::anyhow!("Failed to parse JWT_REFRESH_TOKEN_EXPIRATION: {e}"))?;
111
112    let audiences = get_env("JWT_AUDIENCES")
113        .ok_or_else(|| anyhow::anyhow!("JWT_AUDIENCES environment variable is required"))?
114        .split(',')
115        .map(|s| s.trim().parse::<JwtAudience>())
116        .collect::<Result<Vec<_>>>()?;
117
118    let allow_registration =
119        get_env("ALLOW_REGISTRATION").is_none_or(|s| s.eq_ignore_ascii_case("true"));
120
121    Ok(SecurityConfig {
122        issuer,
123        access_token_expiration,
124        refresh_token_expiration,
125        audiences,
126        allow_registration,
127    })
128}
129
130fn rate_limits_from_env() -> RateLimitsConfig {
131    let parse_rate = |key: &str, default: fn() -> u64| -> u64 {
132        get_env(key)
133            .and_then(|s| {
134                s.parse()
135                    .map_err(|e| {
136                        tracing::warn!(key = %key, value = %s, error = %e, "Failed to parse rate limit value");
137                        e
138                    })
139                    .ok()
140            })
141            .unwrap_or_else(default)
142    };
143
144    RateLimitsConfig {
145        disabled: get_env("RATE_LIMIT_DISABLED").is_some_and(|v| v.to_lowercase() == "true"),
146        oauth_public_per_second: parse_rate(
147            "RATE_LIMIT_OAUTH_PUBLIC_PER_SECOND",
148            default_oauth_public,
149        ),
150        oauth_auth_per_second: parse_rate("RATE_LIMIT_OAUTH_AUTH_PER_SECOND", default_oauth_auth),
151        contexts_per_second: parse_rate("RATE_LIMIT_CONTEXTS_PER_SECOND", default_contexts),
152        tasks_per_second: parse_rate("RATE_LIMIT_TASKS_PER_SECOND", default_tasks),
153        artifacts_per_second: parse_rate("RATE_LIMIT_ARTIFACTS_PER_SECOND", default_artifacts),
154        agent_registry_per_second: parse_rate(
155            "RATE_LIMIT_AGENT_REGISTRY_PER_SECOND",
156            default_agent_registry,
157        ),
158        agents_per_second: parse_rate("RATE_LIMIT_AGENTS_PER_SECOND", default_agents),
159        mcp_registry_per_second: parse_rate(
160            "RATE_LIMIT_MCP_REGISTRY_PER_SECOND",
161            default_mcp_registry,
162        ),
163        mcp_per_second: parse_rate("RATE_LIMIT_MCP_PER_SECOND", default_mcp),
164        stream_per_second: parse_rate("RATE_LIMIT_STREAM_PER_SECOND", default_stream),
165        content_per_second: parse_rate("RATE_LIMIT_CONTENT_PER_SECOND", default_content),
166        burst_multiplier: parse_rate("RATE_LIMIT_BURST_MULTIPLIER", default_burst),
167        tier_multipliers: TierMultipliers::default(),
168    }
169}
170
171fn runtime_config_from_env() -> Result<RuntimeConfig> {
172    let parse_or_default = |key: &str, default: &str| -> Result<String> {
173        Ok(get_env(key).unwrap_or_else(|| default.to_string()))
174    };
175
176    Ok(RuntimeConfig {
177        environment: parse_or_default("SYSTEMPROMPT_ENV", "development")?
178            .parse()
179            .map_err(|e| anyhow::anyhow!("{}", e))?,
180        log_level: parse_or_default("SYSTEMPROMPT_LOG_LEVEL", "normal")?
181            .parse()
182            .map_err(|e| anyhow::anyhow!("{}", e))?,
183        output_format: parse_or_default("SYSTEMPROMPT_OUTPUT_FORMAT", "text")?
184            .parse()
185            .map_err(|e| anyhow::anyhow!("{}", e))?,
186        no_color: get_env("NO_COLOR").is_some(),
187        non_interactive: get_env("CI").is_some(),
188    })
189}