use crate::{ ConfigError, ConfigSource, Diagnostic, DiagnosticCode, ProcessScope, SecretString, deployment_field_registry, field_registry, fingerprint::sha256_hex, schema::semantic_path_for, validation::{valid_database_host, valid_database_identifier, valid_percent_encoding}, }; use std::{ collections::BTreeMap, fmt, net::{IpAddr, SocketAddr}, path::PathBuf, }; use url::Url; mod external_references; mod list_parsers; pub use external_references::ExternalReferenceSettings; const MAX_ENV_VALUE_BYTES: usize = 8_192; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ProcessKind { AdminApi, McpServer, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct DeprecationRecord { pub field: &'static str, pub source_class: &'static str, pub replacement: &'static str, pub removal_window: &'static str, } impl ProcessKind { pub const fn as_str(self) -> &'static str { match self { Self::AdminApi => "admin-api", Self::McpServer => "mcp-server", } } const fn scope(self) -> ProcessScope { match self { Self::AdminApi => ProcessScope::AdminApi, Self::McpServer => ProcessScope::McpServer, } } } #[derive(Clone, Debug)] pub struct PoolSettings { pub max_connections: u32, pub min_connections: u32, pub acquire_timeout_ms: u64, pub idle_timeout_ms: u64, pub max_lifetime_ms: u64, } #[derive(Clone)] pub struct DatabaseSettings { pub url: Option, pub host: String, pub port: u16, pub database: String, pub username: String, pub password: SecretString, pub pool: PoolSettings, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CacheBackend { Memory, Valkey, Redis, } #[derive(Clone)] pub struct CacheSettings { pub backend: CacheBackend, pub url: Option, } #[derive(Clone)] pub struct OutboundSettings { pub allowed_hosts: Vec, pub denied_hosts: Vec, pub max_request_bytes: usize, pub max_response_bytes: usize, } #[derive(Clone)] pub struct RuntimeSettings { pub master_key: SecretString, pub base_url: Option, pub max_concurrent_unary: usize, pub max_concurrent_sessions: usize, pub cache: CacheSettings, pub outbound: OutboundSettings, } #[derive(Clone, Debug)] pub struct RateLimitSettings { pub requests_per_second: u32, pub burst: u32, } #[derive(Clone)] pub struct MetricsSettings { pub enabled: bool, pub bind_addr: SocketAddr, pub bearer_token: Option, } #[derive(Clone, Default)] pub struct OtlpSettings { pub endpoint: Option, pub traces_endpoint: Option, pub protocol: Option, pub traces_protocol: Option, pub timeout: Option, pub traces_timeout: Option, pub headers: Option, pub traces_headers: Option, pub max_queue_size: usize, pub max_export_batch_size: usize, pub schedule_delay: String, pub export_timeout: String, } #[derive(Clone)] pub struct ObservabilitySettings { pub environment: String, pub log_filter: String, pub sentry_dsn: Option, pub metrics: MetricsSettings, pub otlp: OtlpSettings, } #[derive(Clone)] pub struct AdminProcessConfig { pub database: DatabaseSettings, pub runtime: RuntimeSettings, pub external_references: ExternalReferenceSettings, pub observability: ObservabilitySettings, pub bind_addr: SocketAddr, pub storage_root: PathBuf, pub rate_limit: RateLimitSettings, pub invocation_log_retention_days: i64, pub session_secret: SecretString, pub password_pepper: SecretString, pub session_ttl_hours: i64, pub trusted_proxy_ips: Vec, pub bootstrap_email: String, pub bootstrap_password: Option, pub bootstrap_display_name: String, pub demo_seed: bool, } #[derive(Clone)] pub struct McpProcessConfig { pub database: DatabaseSettings, pub runtime: RuntimeSettings, pub observability: ObservabilitySettings, pub bind_addr: SocketAddr, pub refresh_ms: u64, pub rate_limit: RateLimitSettings, } #[derive(Clone)] enum Projection { Admin(Box), Mcp(Box), } #[derive(Clone)] pub struct EffectiveConfig { kind: ProcessKind, fingerprint: String, projection: Projection, deprecations: Vec, } impl EffectiveConfig { pub fn admin(&self) -> Option<&AdminProcessConfig> { match &self.projection { Projection::Admin(config) => Some(config), Projection::Mcp(_) => None, } } pub fn mcp(&self) -> Option<&McpProcessConfig> { match &self.projection { Projection::Mcp(config) => Some(config), Projection::Admin(_) => None, } } pub fn fingerprint(&self) -> &str { &self.fingerprint } pub fn deprecations(&self) -> &[DeprecationRecord] { &self.deprecations } } impl fmt::Debug for EffectiveConfig { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("EffectiveConfig") .field("process", &self.kind.as_str()) .field("fingerprint", &self.fingerprint) .finish() } } struct Parser<'a> { kind: ProcessKind, values: &'a BTreeMap, diagnostics: Vec, deprecations: Vec, } impl<'a> Parser<'a> { fn new(kind: ProcessKind, values: &'a BTreeMap) -> Self { Self { kind, values, diagnostics: Vec::new(), deprecations: Vec::new(), } } fn check_source(&mut self) { let known = field_registry() .iter() .map(|field| field.env_name) .collect::>(); let deployment_only = deployment_field_registry() .iter() .copied() .collect::>(); for (name, value) in self.values { let owned = name.starts_with("CRANK_") || name.starts_with("POSTGRES_") || name.starts_with("OTEL_"); if !owned && !known.contains(name.as_str()) { continue; } if deployment_only.contains(name.as_str()) { continue; } let spec = field_registry().iter().find(|field| field.env_name == name); if owned && spec.is_none() { self.push(DiagnosticCode::UnknownField, "environment.unknown"); continue; } if let Some(spec) = spec { let applicable = match spec.process { ProcessScope::Shared => true, ProcessScope::AdminApi => self.kind == ProcessKind::AdminApi, ProcessScope::McpServer => self.kind == ProcessKind::McpServer, }; if !applicable { self.push(DiagnosticCode::UnknownField, spec.semantic_path); continue; } if value.len() > MAX_ENV_VALUE_BYTES || value.chars().any(char::is_control) { self.push(DiagnosticCode::OutOfRange, spec.semantic_path); } } } if self .values .get("CRANK_CACHE_DEFAULT_TTL_MS") .is_some_and(|value| !value.is_empty()) { self.push( DiagnosticCode::DeprecatedNoEffect, "CRANK_CACHE_DEFAULT_TTL_MS", ); } } fn push(&mut self, code: DiagnosticCode, field: impl Into) { let field = field.into(); let semantic = field_registry() .iter() .find(|spec| spec.env_name == field) .map_or(field.as_str(), |spec| spec.semantic_path); self.diagnostics.push(Diagnostic::new(code, semantic)); } fn optional(&mut self, name: &'static str) -> Option { self.values.get(name).and_then(|value| { if value.is_empty() { None } else if value.len() > MAX_ENV_VALUE_BYTES || value.chars().any(char::is_control) { self.push(DiagnosticCode::OutOfRange, name); None } else if value.trim().is_empty() { self.push(DiagnosticCode::InvalidType, name); None } else { Some(value.clone()) } }) } fn string(&mut self, name: &'static str, default: Option<&str>) -> String { match self .optional(name) .or_else(|| default.map(ToOwned::to_owned)) { Some(value) => value, None => { self.push(DiagnosticCode::MissingRequired, name); String::new() } } } fn secret(&mut self, name: &'static str, default: Option<&str>) -> SecretString { let value = self.string(name, default); if let Some(spec) = field_registry().iter().find(|field| field.env_name == name) && let Some(minimum) = spec.minimum && !value.is_empty() && value.len() < minimum as usize { self.push(DiagnosticCode::OutOfRange, spec.semantic_path); } SecretString::new(value) } fn optional_secret(&mut self, name: &'static str) -> Option { self.optional(name).map(SecretString::new) } fn number(&mut self, name: &'static str) -> u64 { let spec = field_registry() .iter() .find(|field| field.env_name == name) .expect("registered numeric field"); let default = spec .default .and_then(|value| value.parse().ok()) .unwrap_or_default(); let min = spec.minimum.expect("numeric minimum"); let max = spec.maximum.expect("numeric maximum"); let Some(raw) = self.values.get(name) else { return default; }; if raw.is_empty() || raw.trim() != raw { self.push(DiagnosticCode::InvalidType, name); return default; } match raw.parse::() { Ok(value) if (min..=max).contains(&value) => value, Ok(_) => { self.push(DiagnosticCode::OutOfRange, name); default } Err(_) => { self.push(DiagnosticCode::InvalidType, name); default } } } fn boolean(&mut self, name: &'static str) -> bool { let default = field_registry() .iter() .find(|field| field.env_name == name) .and_then(|field| field.default) .and_then(|value| value.parse().ok()) .unwrap_or(false); let Some(raw) = self.values.get(name) else { return default; }; match raw.to_ascii_lowercase().as_str() { "true" | "1" => true, "false" | "0" => false, "yes" | "on" => { self.deprecations.push(DeprecationRecord { field: semantic_path_for(name), source_class: "canonical_env_compatibility_spelling", replacement: "true", removal_window: "after-0.3", }); true } "no" | "off" => { self.deprecations.push(DeprecationRecord { field: semantic_path_for(name), source_class: "canonical_env_compatibility_spelling", replacement: "false", removal_window: "after-0.3", }); false } _ => { self.push(DiagnosticCode::InvalidType, name); default } } } fn socket(&mut self, name: &'static str) -> SocketAddr { let default = field_registry() .iter() .find(|field| field.env_name == name) .and_then(|field| field.default) .expect("registered socket default"); let raw = self.string(name, Some(default)); match raw.parse::() { Ok(value) if value.port() != 0 => value, Ok(_) => { self.push(DiagnosticCode::OutOfRange, name); default.parse().expect("static socket default") } Err(_) => { self.push(DiagnosticCode::InvalidType, name); default.parse().expect("static socket default") } } } fn absolute_path(&mut self, name: &'static str, default: &'static str) -> PathBuf { let raw = self.string(name, Some(default)); let path = PathBuf::from(&raw); if raw.len() > 4096 || !path.is_absolute() { self.push(DiagnosticCode::InvalidType, name); return PathBuf::from(default); } path } fn url(&mut self, name: &'static str) -> Option { let raw = self.optional(name)?; match Url::parse(&raw) { Ok(url) if matches!(url.scheme(), "http" | "https") && url.host_str().is_some() && url.username().is_empty() && url.password().is_none() && url.query().is_none() && url.fragment().is_none() => { Some(raw) } _ => { self.push(DiagnosticCode::InvalidType, name); None } } } fn duration(&mut self, name: &'static str, default: Option) -> Option { let raw = match self.values.get(name) { Some(raw) if !raw.is_empty() => raw, _ => return default.map(|value| value.to_string()), }; if raw.trim() != raw { self.push(DiagnosticCode::InvalidType, name); return default.map(|value| value.to_string()); } match raw.parse::() { Ok(value) if (1..=300_000).contains(&value) => Some(value.to_string()), Ok(_) => { self.push(DiagnosticCode::OutOfRange, name); default.map(|value| value.to_string()) } Err(_) => { self.push(DiagnosticCode::InvalidType, name); default.map(|value| value.to_string()) } } } fn headers(&mut self, name: &'static str) -> Option { let raw = self.optional(name)?; let valid = raw .split(',') .filter(|item| !item.trim().is_empty()) .all(|item| { item.split_once('=').is_some_and(|(header, value)| { let header = header.trim(); !header.is_empty() && header.bytes().all(|byte| { byte.is_ascii_alphanumeric() || matches!( byte, b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~' ) }) && !value.trim().is_empty() && valid_percent_encoding(value) }) }); if !valid { self.push(DiagnosticCode::InvalidType, name); None } else { Some(SecretString::new(raw)) } } } fn parse_database(parser: &mut Parser<'_>) -> DatabaseSettings { let database_url = parser.optional_secret("CRANK_DATABASE_URL"); if database_url.is_some() { parser.deprecations.push(DeprecationRecord { field: semantic_path_for("CRANK_DATABASE_URL"), source_class: "legacy_alias", replacement: "POSTGRES_HOST/PORT/DB/USER/PASSWORD", removal_window: "after-0.3", }); } let decomposed_defaults = [ ("POSTGRES_HOST", "postgres"), ("POSTGRES_PORT", "5432"), ("POSTGRES_DB", "crank"), ("POSTGRES_USER", "crank"), ("POSTGRES_PASSWORD", "crank"), ]; if database_url.is_some() && decomposed_defaults.iter().any(|(name, default)| { parser .values .get(*name) .is_some_and(|value| !value.is_empty() && value != default) }) { parser.push(DiagnosticCode::Conflict, "database.source"); } if let Some(url) = database_url.as_ref() { let valid = Url::parse(url.expose_secret()).ok().is_some_and(|url| { matches!(url.scheme(), "postgres" | "postgresql") && url.host_str().is_some() && !url.path().trim_matches('/').is_empty() && url.query_pairs().all(|(name, value)| { name != "sslmode" || matches!( value.as_ref(), "disable" | "allow" | "prefer" | "require" | "verify-ca" | "verify-full" ) }) }); if !valid { parser.push(DiagnosticCode::InvalidType, "CRANK_DATABASE_URL"); } } let database = DatabaseSettings { url: database_url, host: parser.string("POSTGRES_HOST", Some("postgres")), port: parser.number("POSTGRES_PORT") as u16, database: parser.string("POSTGRES_DB", Some("crank")), username: parser.string("POSTGRES_USER", Some("crank")), password: parser.secret("POSTGRES_PASSWORD", Some("crank")), pool: PoolSettings { max_connections: parser.number("POSTGRES_MAX_CONNECTIONS") as u32, min_connections: parser.number("POSTGRES_MIN_CONNECTIONS") as u32, acquire_timeout_ms: parser.number("POSTGRES_ACQUIRE_TIMEOUT_MS"), idle_timeout_ms: parser.number("POSTGRES_IDLE_TIMEOUT_MS"), max_lifetime_ms: parser.number("POSTGRES_MAX_LIFETIME_MS"), }, }; if !valid_database_host(&database.host) { parser.push(DiagnosticCode::InvalidType, "POSTGRES_HOST"); } for (name, value) in [ ("POSTGRES_DB", database.database.as_str()), ("POSTGRES_USER", database.username.as_str()), ] { if !valid_database_identifier(value) { parser.push(DiagnosticCode::InvalidType, name); } } if database.pool.min_connections > database.pool.max_connections { parser.push( DiagnosticCode::UnsafeCombination, "database.pool.min_connections", ); } database } pub(crate) fn parse_database_source( source: ConfigSource, ) -> Result<(DatabaseSettings, Vec), ConfigError> { let values = source.values(); let mut parser = Parser::new(ProcessKind::AdminApi, values); parser.check_source(); let database = parse_database(&mut parser); if !parser.diagnostics.is_empty() { return Err(ConfigError::from_diagnostics(parser.diagnostics)); } Ok((database, parser.deprecations)) } pub fn parse_process( kind: ProcessKind, source: ConfigSource, ) -> Result { let values = source.values(); let mut parser = Parser::new(kind, values); parser.check_source(); let database = parse_database(&mut parser); let cache_backend = match parser .values .get("CRANK_CACHE_BACKEND") .map(String::as_str) .unwrap_or("memory") .to_ascii_lowercase() .as_str() { "memory" => CacheBackend::Memory, "valkey" => CacheBackend::Valkey, "redis" => CacheBackend::Redis, _ => { parser.push(DiagnosticCode::InvalidType, "CRANK_CACHE_BACKEND"); CacheBackend::Memory } }; if cache_backend == CacheBackend::Redis { parser.deprecations.push(DeprecationRecord { field: semantic_path_for("CRANK_CACHE_BACKEND"), source_class: "canonical_env_compatibility_value", replacement: "valkey", removal_window: "after-0.3", }); } let cache_url = parser.optional_secret("CRANK_CACHE_URL"); if let Some(url) = cache_url.as_ref() && Url::parse(url.expose_secret()).ok().is_none_or(|url| { !matches!(url.scheme(), "redis" | "rediss") || url.host_str().is_none() || url.query().is_some() || url.fragment().is_some() }) { parser.push(DiagnosticCode::InvalidType, "CRANK_CACHE_URL"); } if matches!(cache_backend, CacheBackend::Valkey | CacheBackend::Redis) && cache_url.is_none() { parser.push(DiagnosticCode::MissingRequired, "CRANK_CACHE_URL"); } if cache_backend == CacheBackend::Memory && cache_url.is_some() { parser.push(DiagnosticCode::Conflict, "cache.url"); } let runtime = RuntimeSettings { master_key: parser.secret("CRANK_MASTER_KEY", None), base_url: parser.url("CRANK_BASE_URL").or_else(|| { field_registry() .iter() .find(|field| field.env_name == "CRANK_BASE_URL") .and_then(|field| field.default_for(kind.scope())) .map(ToOwned::to_owned) }), max_concurrent_unary: parser.number("CRANK_RUNTIME_MAX_CONCURRENT_UNARY") as usize, max_concurrent_sessions: if kind == ProcessKind::McpServer { parser.number("CRANK_RUNTIME_MAX_CONCURRENT_SESSIONS") as usize } else { 16 }, cache: CacheSettings { backend: cache_backend, url: cache_url, }, outbound: OutboundSettings { allowed_hosts: parser.host_list("CRANK_OUTBOUND_ALLOWED_HOSTS"), denied_hosts: parser.host_list("CRANK_OUTBOUND_DENIED_HOSTS"), max_request_bytes: parser.number("CRANK_OUTBOUND_MAX_REQUEST_BYTES") as usize, max_response_bytes: parser.number("CRANK_OUTBOUND_MAX_RESPONSE_BYTES") as usize, }, }; let metrics_bind_name = match kind { ProcessKind::AdminApi => "CRANK_ADMIN_METRICS_BIND", ProcessKind::McpServer => "CRANK_MCP_METRICS_BIND", }; let metrics = MetricsSettings { enabled: parser.boolean("CRANK_METRICS_ENABLED"), bind_addr: parser.socket(metrics_bind_name), bearer_token: parser.optional_secret("CRANK_METRICS_BEARER_TOKEN"), }; if metrics.enabled && !metrics.bind_addr.ip().is_loopback() && metrics.bearer_token.is_none() { parser.push( DiagnosticCode::UnsafeCombination, "observability.metrics.bearer_token", ); } let otlp = OtlpSettings { endpoint: parser.url("OTEL_EXPORTER_OTLP_ENDPOINT"), traces_endpoint: parser.url("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"), protocol: parser.optional("OTEL_EXPORTER_OTLP_PROTOCOL").or_else(|| { field_registry() .iter() .find(|field| field.env_name == "OTEL_EXPORTER_OTLP_PROTOCOL") .and_then(|field| field.default) .map(ToOwned::to_owned) }), traces_protocol: parser.optional("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"), timeout: parser.duration( "OTEL_EXPORTER_OTLP_TIMEOUT", field_registry() .iter() .find(|field| field.env_name == "OTEL_EXPORTER_OTLP_TIMEOUT") .and_then(|field| field.default) .and_then(|value| value.parse().ok()), ), traces_timeout: parser.duration("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", None), headers: parser.headers("OTEL_EXPORTER_OTLP_HEADERS"), traces_headers: parser.headers("OTEL_EXPORTER_OTLP_TRACES_HEADERS"), max_queue_size: parser.number("OTEL_BSP_MAX_QUEUE_SIZE") as usize, max_export_batch_size: parser.number("OTEL_BSP_MAX_EXPORT_BATCH_SIZE") as usize, schedule_delay: parser .duration("OTEL_BSP_SCHEDULE_DELAY", Some(5_000)) .expect("duration default"), export_timeout: parser .duration("OTEL_BSP_EXPORT_TIMEOUT", Some(30_000)) .expect("duration default"), }; if otlp.max_export_batch_size > otlp.max_queue_size { parser.push( DiagnosticCode::UnsafeCombination, "observability.otlp.max_export_batch_size", ); } for (name, protocol) in [ ("OTEL_EXPORTER_OTLP_PROTOCOL", otlp.protocol.as_deref()), ( "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", otlp.traces_protocol.as_deref(), ), ] { if protocol.is_some_and(|value| value != "http/protobuf") { parser.push(DiagnosticCode::InvalidType, name); } } let observability = ObservabilitySettings { environment: parser.string("CRANK_ENVIRONMENT", Some("development")), log_filter: parser.string( "CRANK_LOG_LEVEL", field_registry() .iter() .find(|field| field.env_name == "CRANK_LOG_LEVEL") .and_then(|field| field.default_for(kind.scope())), ), sentry_dsn: parser.optional_secret("CRANK_SENTRY_DSN"), metrics, otlp, }; if observability.environment.len() > 64 || !observability .environment .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+')) { parser.push(DiagnosticCode::InvalidType, "CRANK_ENVIRONMENT"); } if let Some(dsn) = observability.sentry_dsn.as_ref() && Url::parse(dsn.expose_secret()) .ok() .is_none_or(|url| !matches!(url.scheme(), "http" | "https") || url.host_str().is_none()) { parser.push(DiagnosticCode::InvalidType, "CRANK_SENTRY_DSN"); } let projection = match kind { ProcessKind::AdminApi => { let rps = parser.number("CRANK_ADMIN_RATE_LIMIT_RPS") as u32; let burst = parser.number("CRANK_ADMIN_RATE_LIMIT_BURST") as u32; let bind_addr = parser.socket("CRANK_ADMIN_BIND"); if burst < rps { parser.push(DiagnosticCode::UnsafeCombination, "admin.rate_limit.burst"); } if observability.environment == "production" && !bind_addr.ip().is_loopback() && !runtime .base_url .as_deref() .is_some_and(|url| url.starts_with("https://")) { parser.push(DiagnosticCode::UnsafeCombination, "admin.exposure.tls"); } Projection::Admin(Box::new(AdminProcessConfig { database, runtime, external_references: external_references::parse(&mut parser), observability, bind_addr, storage_root: parser.absolute_path("CRANK_STORAGE_ROOT", "/var/lib/crank/storage"), rate_limit: RateLimitSettings { requests_per_second: rps, burst, }, invocation_log_retention_days: parser.number("CRANK_INVOCATION_LOG_RETENTION_DAYS") as i64, session_secret: parser.secret("CRANK_SESSION_SECRET", None), password_pepper: parser.secret("CRANK_PASSWORD_PEPPER", None), session_ttl_hours: parser.number("CRANK_SESSION_TTL_HOURS") as i64, trusted_proxy_ips: parser.ip_list("CRANK_TRUSTED_PROXY_IPS"), bootstrap_email: parser.string("CRANK_BOOTSTRAP_ADMIN_EMAIL", None), bootstrap_password: parser.optional_secret("CRANK_BOOTSTRAP_ADMIN_PASSWORD"), bootstrap_display_name: parser .string("CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME", Some("Crank Owner")), demo_seed: parser.boolean("CRANK_DEMO_SEED"), })) } ProcessKind::McpServer => { let rps = parser.number("CRANK_MCP_RATE_LIMIT_RPS") as u32; let burst = parser.number("CRANK_MCP_RATE_LIMIT_BURST") as u32; if burst < rps { parser.push(DiagnosticCode::UnsafeCombination, "mcp.rate_limit.burst"); } Projection::Mcp(Box::new(McpProcessConfig { database, runtime, observability, bind_addr: parser.socket("CRANK_MCP_BIND"), refresh_ms: parser.number("CRANK_MCP_REFRESH_MS"), rate_limit: RateLimitSettings { requests_per_second: rps, burst, }, })) } }; if !parser.diagnostics.is_empty() { return Err(ConfigError::from_diagnostics(parser.diagnostics)); } let fingerprint_parts = fingerprint_parts(kind, &projection); Ok(EffectiveConfig { kind, fingerprint: sha256_hex(fingerprint_parts), projection, deprecations: parser.deprecations, }) } fn fingerprint_parts(kind: ProcessKind, projection: &Projection) -> Vec { let (database, runtime, observability, process_parts): ( &DatabaseSettings, &RuntimeSettings, &ObservabilitySettings, Vec, ) = match projection { Projection::Admin(config) => ( &config.database, &config.runtime, &config.observability, vec![ format!("bind={}", config.bind_addr), format!( "rate={}:{}", config.rate_limit.requests_per_second, config.rate_limit.burst ), format!("retention={}", config.invocation_log_retention_days), format!("session_ttl={}", config.session_ttl_hours), format!("trusted_proxies={:?}", config.trusted_proxy_ips), format!("demo={}", config.demo_seed), format!( "external_reference_prefixes={}", config.external_references.allowed_url_prefixes.join(",") ), format!( "external_reference_limits={}:{}:{}:{}:{}", config.external_references.max_depth, config.external_references.max_documents, config.external_references.max_fetch_bytes, config.external_references.fetch_timeout_ms, config.external_references.max_expanded_nodes, ), "storage=path-configured".to_owned(), format!("session_secret={}", config.session_secret.is_configured()), format!("pepper={}", config.password_pepper.is_configured()), format!( "bootstrap_password={}", config .bootstrap_password .as_ref() .is_some_and(SecretString::is_configured) ), ], ), Projection::Mcp(config) => ( &config.database, &config.runtime, &config.observability, vec![ format!("bind={}", config.bind_addr), format!("refresh={}", config.refresh_ms), format!( "rate={}:{}", config.rate_limit.requests_per_second, config.rate_limit.burst ), ], ), }; let (db_host, db_port, db_name, db_user, db_sslmode) = database .url .as_ref() .and_then(|secret| { Url::parse(secret.expose_secret()).ok().map(|url| { ( url.host_str().unwrap_or_default().to_ascii_lowercase(), url.port().unwrap_or(5432), url.path().trim_start_matches('/').to_owned(), url.username().to_owned(), url.query_pairs() .find(|(name, _)| name == "sslmode") .map_or_else(|| "default".to_owned(), |(_, value)| value.into_owned()), ) }) }) .unwrap_or_else(|| { ( database.host.to_ascii_lowercase(), database.port, database.database.clone(), database.username.clone(), "default".to_owned(), ) }); let mut allowed = runtime.outbound.allowed_hosts.clone(); allowed.sort(); let mut denied = runtime.outbound.denied_hosts.clone(); denied.sort(); let mut parts = vec![ "schema=crank-config-v1".to_owned(), format!("process={}", kind.as_str()), format!("database={db_host}:{db_port}/{db_name}:{db_user}:{db_sslmode}"), format!("database_password={}", database.password.is_configured()), format!( "pool={}:{}:{}:{}:{}", database.pool.max_connections, database.pool.min_connections, database.pool.acquire_timeout_ms, database.pool.idle_timeout_ms, database.pool.max_lifetime_ms ), format!("master_key={}", runtime.master_key.is_configured()), format!( "base_url={}", runtime.base_url.as_deref().unwrap_or("unconfigured") ), format!("unary={}", runtime.max_concurrent_unary), format!("sessions={}", runtime.max_concurrent_sessions), format!( "cache={:?}:{}", runtime.cache.backend, runtime.cache.url.is_some() ), format!("allowed={}", allowed.join(",")), format!("denied={}", denied.join(",")), format!("max_request={}", runtime.outbound.max_request_bytes), format!("max_response={}", runtime.outbound.max_response_bytes), format!("environment={}", observability.environment), format!("log_filter={}", observability.log_filter), format!("sentry={}", observability.sentry_dsn.is_some()), format!( "metrics={}:{}:{}", observability.metrics.enabled, observability.metrics.bind_addr, observability.metrics.bearer_token.is_some() ), format!( "otlp={}:{}:{}:{}", observability.otlp.endpoint.is_some(), observability.otlp.traces_endpoint.is_some(), observability.otlp.headers.is_some(), observability.otlp.traces_headers.is_some() ), format!( "otlp_limits={}:{}:{}:{}", observability.otlp.max_queue_size, observability.otlp.max_export_batch_size, observability.otlp.schedule_delay, observability.otlp.export_timeout ), ]; parts.extend(process_parts); parts }