Skip to main content

systemprompt_models/config/
mod.rs

1//! Global runtime [`Config`] singleton and validation helpers.
2//!
3//! [`Config`] is the resolved, flat configuration installed once at
4//! startup into a process-wide `OnceLock` and read via [`Config::get`].
5//! Submodules cover environment classification, path/postgres-URL
6//! validation, rate-limit shapes, and verbosity levels.
7//! Accessors return [`crate::errors::ConfigError`] when not initialized.
8
9use std::path::PathBuf;
10use std::sync::OnceLock;
11use systemprompt_traits::ConfigProvider;
12
13use crate::auth::JwtAudience;
14use crate::profile::{ContentNegotiationConfig, SecurityHeadersConfig, TrustedIssuer};
15
16mod environment;
17mod paths;
18mod rate_limits;
19mod validation;
20mod verbosity;
21
22pub use environment::Environment;
23pub use paths::PathNotConfiguredError;
24pub use rate_limits::RateLimitConfig;
25pub use validation::{
26    format_path_errors, validate_optional_path, validate_postgres_url, validate_profile_paths,
27    validate_required_optional_path, validate_required_path,
28};
29pub use verbosity::VerbosityLevel;
30
31static CONFIG: OnceLock<Config> = OnceLock::new();
32
33/// Default global cap on concurrent A2A SSE streams.
34pub const DEFAULT_MAX_CONCURRENT_STREAMS: usize = 256;
35
36/// Resolve a stable instance identifier for this replica.
37///
38/// Prefers the `HOSTNAME` environment variable (set by most container
39/// runtimes and shells); falls back to a generated short id when absent.
40#[must_use]
41pub fn default_instance_id() -> String {
42    std::env::var("HOSTNAME")
43        .ok()
44        .filter(|h| !h.trim().is_empty())
45        .unwrap_or_else(|| format!("instance-{}", uuid::Uuid::new_v4().simple()))
46}
47
48#[derive(Debug, Clone)]
49pub struct Config {
50    pub instance_id: String,
51    pub max_concurrent_streams: usize,
52    pub sitename: String,
53    pub database_type: String,
54    pub database_url: String,
55    pub database_write_url: Option<String>,
56    pub github_link: String,
57    pub github_token: Option<String>,
58    pub system_path: String,
59    pub services_path: String,
60    pub bin_path: String,
61    pub skills_path: String,
62    pub settings_path: String,
63    pub content_config_path: String,
64    pub geoip_database_path: Option<String>,
65    pub web_path: String,
66    pub web_config_path: String,
67    pub web_metadata_path: String,
68    pub host: String,
69    pub port: u16,
70    pub api_server_url: String,
71    pub api_internal_url: String,
72    pub api_external_url: String,
73    pub jwt_issuer: String,
74    pub jwt_access_token_expiration: i64,
75    pub jwt_refresh_token_expiration: i64,
76    pub jwt_audiences: Vec<JwtAudience>,
77    pub allowed_resource_audiences: Vec<String>,
78    pub trusted_issuers: Vec<TrustedIssuer>,
79    pub signing_key_path: PathBuf,
80    pub use_https: bool,
81    pub rate_limits: RateLimitConfig,
82    pub cors_allowed_origins: Vec<String>,
83    pub trusted_proxies: Vec<String>,
84    pub is_cloud: bool,
85    pub content_negotiation: ContentNegotiationConfig,
86    pub security_headers: SecurityHeadersConfig,
87    pub allow_registration: bool,
88    pub system_admin_username: String,
89}
90
91impl Config {
92    pub fn is_initialized() -> bool {
93        CONFIG.get().is_some()
94    }
95
96    pub fn get() -> Result<&'static Self, crate::errors::ConfigError> {
97        CONFIG
98            .get()
99            .ok_or(crate::errors::ConfigError::NotInitialized)
100    }
101
102    pub fn install(config: Self) -> Result<(), Box<Self>> {
103        CONFIG.set(config).map_err(Box::new)
104    }
105
106    pub fn logs_path(&self) -> String {
107        format!("{}/logs", self.system_path)
108    }
109}
110
111impl ConfigProvider for Config {
112    fn get(&self, key: &str) -> Option<String> {
113        match key {
114            "database_type" => Some(self.database_type.clone()),
115            "database_url" => Some(self.database_url.clone()),
116            "database_write_url" => self.database_write_url.clone(),
117            "host" => Some(self.host.clone()),
118            "port" => Some(self.port.to_string()),
119            "system_path" => Some(self.system_path.clone()),
120            "services_path" => Some(self.services_path.clone()),
121            "bin_path" => Some(self.bin_path.clone()),
122            "skills_path" => Some(self.skills_path.clone()),
123            "settings_path" => Some(self.settings_path.clone()),
124            "content_config_path" => Some(self.content_config_path.clone()),
125            "web_path" => Some(self.web_path.clone()),
126            "web_config_path" => Some(self.web_config_path.clone()),
127            "web_metadata_path" => Some(self.web_metadata_path.clone()),
128            "sitename" => Some(self.sitename.clone()),
129            "github_link" => Some(self.github_link.clone()),
130            "github_token" => self.github_token.clone(),
131            "api_server_url" => Some(self.api_server_url.clone()),
132            "api_external_url" => Some(self.api_external_url.clone()),
133            "jwt_issuer" => Some(self.jwt_issuer.clone()),
134            "is_cloud" => Some(self.is_cloud.to_string()),
135            "instance_id" => Some(self.instance_id.clone()),
136            "max_concurrent_streams" => Some(self.max_concurrent_streams.to_string()),
137            _ => None,
138        }
139    }
140
141    fn database_url(&self) -> &str {
142        &self.database_url
143    }
144
145    fn database_write_url(&self) -> Option<&str> {
146        self.database_write_url.as_deref()
147    }
148
149    fn system_path(&self) -> &str {
150        &self.system_path
151    }
152
153    fn api_port(&self) -> u16 {
154        self.port
155    }
156
157    fn as_any(&self) -> &dyn std::any::Any {
158        self
159    }
160}