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