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 postgres-URL validation and rate-limit shapes.
6//! Accessors return [`crate::errors::ConfigError`] when not initialized.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::path::PathBuf;
12use std::sync::OnceLock;
13use systemprompt_traits::ConfigProvider;
14
15use crate::auth::JwtAudience;
16use crate::profile::{ContentNegotiationConfig, SecurityHeadersConfig, TrustedIssuer};
17
18mod paths;
19mod rate_limits;
20mod validation;
21
22pub use paths::PathNotConfiguredError;
23pub use rate_limits::RateLimitConfig;
24pub use validation::validate_postgres_url;
25
26static CONFIG: OnceLock<Config> = OnceLock::new();
27
28pub const DEFAULT_MAX_CONCURRENT_STREAMS: usize = 256;
29
30#[must_use]
31pub fn stable_instance_id(lookup: impl Fn(&str) -> Option<String>) -> Option<String> {
32    lookup("HOSTNAME")
33        .map(|h| h.trim().to_owned())
34        .filter(|h| !h.is_empty())
35}
36
37#[must_use]
38pub fn random_instance_id() -> String {
39    format!("instance-{}", uuid::Uuid::new_v4().simple())
40}
41
42#[derive(Clone)]
43pub struct Config {
44    pub instance_id: String,
45    pub metrics_port: Option<u16>,
46    pub max_concurrent_streams: usize,
47    pub sitename: String,
48    pub database_type: String,
49    pub database_url: String,
50    pub database_write_url: Option<String>,
51    pub github_link: String,
52    pub github_token: Option<String>,
53    pub system_path: String,
54    pub services_path: String,
55    pub bin_path: String,
56    pub skills_path: String,
57    pub settings_path: String,
58    pub content_config_path: String,
59    pub geoip_database_path: Option<String>,
60    pub web_path: String,
61    pub web_config_path: String,
62    pub web_metadata_path: String,
63    pub host: String,
64    pub port: u16,
65    pub api_server_url: String,
66    pub api_internal_url: String,
67    pub api_external_url: String,
68    pub jwt_issuer: String,
69    pub jwt_access_token_expiration: i64,
70    pub jwt_refresh_token_expiration: i64,
71    pub jwt_audiences: Vec<JwtAudience>,
72    pub allowed_resource_audiences: Vec<String>,
73    pub trusted_issuers: Vec<TrustedIssuer>,
74    pub id_jag_ttl_secs: i64,
75    pub signing_key_path: PathBuf,
76    pub use_https: bool,
77    pub rate_limits: RateLimitConfig,
78    pub cors_allowed_origins: Vec<String>,
79    pub trusted_proxies: Vec<ipnet::IpNet>,
80    pub is_cloud: bool,
81    pub content_negotiation: ContentNegotiationConfig,
82    pub security_headers: SecurityHeadersConfig,
83    pub allow_registration: bool,
84    pub allow_dynamic_client_registration: bool,
85    pub login_page_url: Option<String>,
86    pub system_admin_username: String,
87    pub system_admin_email: Option<systemprompt_identifiers::Email>,
88}
89
90const REDACTED: &str = "<redacted>";
91
92// Why: `database_url` carries the password and `github_token` is a credential;
93// a `?config` in any log line must not print either.
94impl std::fmt::Debug for Config {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.debug_struct("Config")
97            .field("instance_id", &self.instance_id)
98            .field("metrics_port", &self.metrics_port)
99            .field("max_concurrent_streams", &self.max_concurrent_streams)
100            .field("sitename", &self.sitename)
101            .field("database_type", &self.database_type)
102            .field("database_url", &REDACTED)
103            .field("database_write_url", &REDACTED)
104            .field("github_link", &self.github_link)
105            .field("github_token", &REDACTED)
106            .field("system_path", &self.system_path)
107            .field("services_path", &self.services_path)
108            .field("bin_path", &self.bin_path)
109            .field("skills_path", &self.skills_path)
110            .field("settings_path", &self.settings_path)
111            .field("content_config_path", &self.content_config_path)
112            .field("geoip_database_path", &self.geoip_database_path)
113            .field("web_path", &self.web_path)
114            .field("web_config_path", &self.web_config_path)
115            .field("web_metadata_path", &self.web_metadata_path)
116            .field("host", &self.host)
117            .field("port", &self.port)
118            .field("api_server_url", &self.api_server_url)
119            .field("api_internal_url", &self.api_internal_url)
120            .field("api_external_url", &self.api_external_url)
121            .field("jwt_issuer", &self.jwt_issuer)
122            .field(
123                "jwt_access_token_expiration",
124                &self.jwt_access_token_expiration,
125            )
126            .field(
127                "jwt_refresh_token_expiration",
128                &self.jwt_refresh_token_expiration,
129            )
130            .field("jwt_audiences", &self.jwt_audiences)
131            .field(
132                "allowed_resource_audiences",
133                &self.allowed_resource_audiences,
134            )
135            .field("trusted_issuers", &self.trusted_issuers)
136            .field("id_jag_ttl_secs", &self.id_jag_ttl_secs)
137            .field("signing_key_path", &self.signing_key_path)
138            .field("use_https", &self.use_https)
139            .field("rate_limits", &self.rate_limits)
140            .field("cors_allowed_origins", &self.cors_allowed_origins)
141            .field("trusted_proxies", &self.trusted_proxies)
142            .field("is_cloud", &self.is_cloud)
143            .field("content_negotiation", &self.content_negotiation)
144            .field("security_headers", &self.security_headers)
145            .field("allow_registration", &self.allow_registration)
146            .field(
147                "allow_dynamic_client_registration",
148                &self.allow_dynamic_client_registration,
149            )
150            .field("login_page_url", &self.login_page_url)
151            .field("system_admin_username", &self.system_admin_username)
152            .field("system_admin_email", &self.system_admin_email)
153            .finish()
154    }
155}
156
157impl Config {
158    pub fn is_initialized() -> bool {
159        CONFIG.get().is_some()
160    }
161
162    pub fn get() -> Result<&'static Self, crate::errors::ConfigError> {
163        CONFIG
164            .get()
165            .ok_or(crate::errors::ConfigError::NotInitialized)
166    }
167
168    pub fn install(config: Self) -> Result<(), Box<Self>> {
169        CONFIG.set(config).map_err(Box::new)
170    }
171
172    pub fn logs_path(&self) -> String {
173        format!("{}/logs", self.system_path)
174    }
175}
176
177impl ConfigProvider for Config {
178    fn get(&self, key: &str) -> Option<String> {
179        match key {
180            "database_type" => Some(self.database_type.clone()),
181            "database_url" => Some(self.database_url.clone()),
182            "database_write_url" => self.database_write_url.clone(),
183            "host" => Some(self.host.clone()),
184            "port" => Some(self.port.to_string()),
185            "system_path" => Some(self.system_path.clone()),
186            "services_path" => Some(self.services_path.clone()),
187            "bin_path" => Some(self.bin_path.clone()),
188            "skills_path" => Some(self.skills_path.clone()),
189            "settings_path" => Some(self.settings_path.clone()),
190            "content_config_path" => Some(self.content_config_path.clone()),
191            "web_path" => Some(self.web_path.clone()),
192            "web_config_path" => Some(self.web_config_path.clone()),
193            "web_metadata_path" => Some(self.web_metadata_path.clone()),
194            "sitename" => Some(self.sitename.clone()),
195            "github_link" => Some(self.github_link.clone()),
196            "github_token" => self.github_token.clone(),
197            "api_server_url" => Some(self.api_server_url.clone()),
198            "api_external_url" => Some(self.api_external_url.clone()),
199            "jwt_issuer" => Some(self.jwt_issuer.clone()),
200            "is_cloud" => Some(self.is_cloud.to_string()),
201            "instance_id" => Some(self.instance_id.clone()),
202            "max_concurrent_streams" => Some(self.max_concurrent_streams.to_string()),
203            _ => None,
204        }
205    }
206
207    fn database_url(&self) -> &str {
208        &self.database_url
209    }
210
211    fn database_write_url(&self) -> Option<&str> {
212        self.database_write_url.as_deref()
213    }
214
215    fn system_path(&self) -> &str {
216        &self.system_path
217    }
218
219    fn api_port(&self) -> u16 {
220        self.port
221    }
222
223    fn as_any(&self) -> &dyn std::any::Any {
224        self
225    }
226}