systemprompt-models 0.5.0

Foundation data models for systemprompt.io AI governance infrastructure. Shared DTOs, config, and domain types consumed by every layer of the MCP governance pipeline.
Documentation
use super::{
    ContentNegotiationConfig, DatabaseConfig, ExtensionsConfig, PathsConfig, Profile, ProfileError,
    ProfileResult, ProfileType, RateLimitsConfig, RuntimeConfig, SecurityConfig,
    SecurityHeadersConfig, ServerConfig, SiteConfig, TierMultipliers, default_agent_registry,
    default_agents, default_artifacts, default_burst, default_content, default_contexts,
    default_mcp, default_mcp_registry, default_oauth_auth, default_oauth_public, default_stream,
    default_tasks,
};

impl Profile {
    pub fn from_env(profile_name: &str, display_name: &str) -> ProfileResult<Self> {
        let db_type = require_env("DATABASE_TYPE")?;

        Ok(Self {
            name: profile_name.to_string(),
            display_name: display_name.to_string(),
            target: ProfileType::Cloud,
            site: site_config_from_env()?,
            database: DatabaseConfig {
                db_type,
                external_db_access: false,
            },
            server: server_config_from_env()?,
            paths: paths_config_from_env()?,
            security: security_config_from_env()?,
            rate_limits: rate_limits_from_env(),
            runtime: runtime_config_from_env()?,
            cloud: None,
            secrets: None,
            extensions: ExtensionsConfig::default(),
            gateway: None,
        })
    }
}

fn get_env(key: &str) -> Option<String> {
    std::env::var(key).ok()
}

fn require_env(name: &'static str) -> ProfileResult<String> {
    std::env::var(name).map_err(|_| ProfileError::MissingEnvVar { name })
}

fn site_config_from_env() -> ProfileResult<SiteConfig> {
    Ok(SiteConfig {
        name: require_env("SITENAME")?,
        github_link: get_env("GITHUB_LINK"),
    })
}

fn server_config_from_env() -> ProfileResult<ServerConfig> {
    let port = require_env("PORT")?
        .parse()
        .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
            name: "PORT",
            message: e.to_string(),
        })?;

    Ok(ServerConfig {
        host: require_env("HOST")?,
        port,
        api_server_url: require_env("API_SERVER_URL")?,
        api_internal_url: require_env("API_INTERNAL_URL")?,
        api_external_url: require_env("API_EXTERNAL_URL")?,
        use_https: get_env("USE_HTTPS").is_some_and(|v| v.to_lowercase() == "true"),
        cors_allowed_origins: get_env("CORS_ALLOWED_ORIGINS").map_or_else(Vec::new, |s| {
            s.split(',').map(|s| s.trim().to_string()).collect()
        }),
        content_negotiation: ContentNegotiationConfig {
            enabled: get_env("CONTENT_NEGOTIATION_ENABLED")
                .is_some_and(|v| v.to_lowercase() == "true"),
            ..Default::default()
        },
        security_headers: SecurityHeadersConfig::default(),
    })
}

fn paths_config_from_env() -> ProfileResult<PathsConfig> {
    Ok(PathsConfig {
        system: require_env("SYSTEM_PATH")?,
        services: require_env("SYSTEMPROMPT_SERVICES_PATH")?,
        bin: require_env("BIN_PATH")?,
        storage: get_env("STORAGE_PATH"),
        geoip_database: get_env("GEOIP_DATABASE_PATH"),
        web_path: get_env("SYSTEMPROMPT_WEB_PATH"),
    })
}

fn security_config_from_env() -> ProfileResult<SecurityConfig> {
    use crate::auth::JwtAudience;

    let issuer = require_env("JWT_ISSUER")?;

    let access_token_expiration = require_env("JWT_ACCESS_TOKEN_EXPIRATION")?
        .parse()
        .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
            name: "JWT_ACCESS_TOKEN_EXPIRATION",
            message: e.to_string(),
        })?;

    let refresh_token_expiration = require_env("JWT_REFRESH_TOKEN_EXPIRATION")?
        .parse()
        .map_err(|e: std::num::ParseIntError| ProfileError::InvalidEnvVar {
            name: "JWT_REFRESH_TOKEN_EXPIRATION",
            message: e.to_string(),
        })?;

    let audiences_raw = require_env("JWT_AUDIENCES")?;
    let audiences = audiences_raw
        .split(',')
        .map(|s| {
            s.trim()
                .parse::<JwtAudience>()
                .map_err(|e| ProfileError::InvalidEnvVar {
                    name: "JWT_AUDIENCES",
                    message: e.to_string(),
                })
        })
        .collect::<ProfileResult<Vec<_>>>()?;

    let allow_registration =
        get_env("ALLOW_REGISTRATION").is_none_or(|s| s.eq_ignore_ascii_case("true"));

    Ok(SecurityConfig {
        issuer,
        access_token_expiration,
        refresh_token_expiration,
        audiences,
        allow_registration,
    })
}

fn rate_limits_from_env() -> RateLimitsConfig {
    let parse_rate = |key: &str, default: fn() -> u64| -> u64 {
        get_env(key)
            .and_then(|s| {
                s.parse()
                    .map_err(|e| {
                        tracing::warn!(key = %key, value = %s, error = %e, "Failed to parse rate limit value");
                        e
                    })
                    .ok()
            })
            .unwrap_or_else(default)
    };

    RateLimitsConfig {
        disabled: get_env("RATE_LIMIT_DISABLED").is_some_and(|v| v.to_lowercase() == "true"),
        oauth_public_per_second: parse_rate(
            "RATE_LIMIT_OAUTH_PUBLIC_PER_SECOND",
            default_oauth_public,
        ),
        oauth_auth_per_second: parse_rate("RATE_LIMIT_OAUTH_AUTH_PER_SECOND", default_oauth_auth),
        contexts_per_second: parse_rate("RATE_LIMIT_CONTEXTS_PER_SECOND", default_contexts),
        tasks_per_second: parse_rate("RATE_LIMIT_TASKS_PER_SECOND", default_tasks),
        artifacts_per_second: parse_rate("RATE_LIMIT_ARTIFACTS_PER_SECOND", default_artifacts),
        agent_registry_per_second: parse_rate(
            "RATE_LIMIT_AGENT_REGISTRY_PER_SECOND",
            default_agent_registry,
        ),
        agents_per_second: parse_rate("RATE_LIMIT_AGENTS_PER_SECOND", default_agents),
        mcp_registry_per_second: parse_rate(
            "RATE_LIMIT_MCP_REGISTRY_PER_SECOND",
            default_mcp_registry,
        ),
        mcp_per_second: parse_rate("RATE_LIMIT_MCP_PER_SECOND", default_mcp),
        stream_per_second: parse_rate("RATE_LIMIT_STREAM_PER_SECOND", default_stream),
        content_per_second: parse_rate("RATE_LIMIT_CONTENT_PER_SECOND", default_content),
        burst_multiplier: parse_rate("RATE_LIMIT_BURST_MULTIPLIER", default_burst),
        tier_multipliers: TierMultipliers::default(),
    }
}

fn runtime_config_from_env() -> ProfileResult<RuntimeConfig> {
    let environment = get_env("SYSTEMPROMPT_ENV")
        .unwrap_or_else(|| "development".to_string())
        .parse()
        .map_err(|e: String| ProfileError::InvalidEnvVar {
            name: "SYSTEMPROMPT_ENV",
            message: e,
        })?;

    let log_level = get_env("SYSTEMPROMPT_LOG_LEVEL")
        .unwrap_or_else(|| "normal".to_string())
        .parse()
        .map_err(|e: String| ProfileError::InvalidEnvVar {
            name: "SYSTEMPROMPT_LOG_LEVEL",
            message: e,
        })?;

    let output_format = get_env("SYSTEMPROMPT_OUTPUT_FORMAT")
        .unwrap_or_else(|| "text".to_string())
        .parse()
        .map_err(|e: String| ProfileError::InvalidEnvVar {
            name: "SYSTEMPROMPT_OUTPUT_FORMAT",
            message: e,
        })?;

    Ok(RuntimeConfig {
        environment,
        log_level,
        output_format,
        no_color: get_env("NO_COLOR").is_some(),
        non_interactive: get_env("CI").is_some(),
    })
}