Skip to main content

cognee_http_server/
config.rs

1//! HTTP server configuration.
2//!
3//! `HttpServerConfig` holds all tuneable parameters.  `from_env()` reads the
4//! documented environment variables and overlays them on the struct defaults.
5//! Only the standalone binary calls `from_env()`; library embedders construct
6//! `HttpServerConfig` directly.
7
8use std::{path::PathBuf, str::FromStr, time::Duration};
9
10use secrecy::{ExposeSecret, SecretString};
11
12use crate::error::ServerError;
13
14// re-export for use in state.rs
15pub use cognee_core::pipeline_run_registry::RegistryConfig;
16
17// ─── Environment enum ─────────────────────────────────────────────────────────
18
19/// Deployment environment.  Controls log format (pretty vs JSON) and other
20/// dev-vs-prod defaults.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum Environment {
23    Dev,
24    #[default]
25    Prod,
26    Test,
27}
28
29impl FromStr for Environment {
30    type Err = ();
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        match s.to_ascii_lowercase().as_str() {
33            "dev" | "development" => Ok(Environment::Dev),
34            "test" | "testing" => Ok(Environment::Test),
35            _ => Ok(Environment::Prod),
36        }
37    }
38}
39
40// ─── HttpServerConfig ─────────────────────────────────────────────────────────
41
42/// All tuneable server parameters.
43///
44/// Defaults mirror the Python FastAPI server defaults.
45#[derive(Debug, Clone)]
46pub struct HttpServerConfig {
47    /// Bind address. Env: `HTTP_API_HOST`. Default: `"0.0.0.0"`.
48    pub host: String,
49    /// Bind port. Env: `HTTP_API_PORT`. Default: `8000`.
50    pub port: u16,
51    /// Explicit CORS allowed origins. Env: `CORS_ALLOWED_ORIGINS` (comma-sep).
52    /// Falls back to `[ui_app_url]` when empty.
53    pub cors_allowed_origins: Vec<String>,
54    /// Frontend URL used as the CORS fallback. Env: `UI_APP_URL`.
55    /// Default: `"http://localhost:3000"`.
56    pub ui_app_url: String,
57    /// Deployment environment. Env: `ENV`. Default: `Prod`.
58    pub env: Environment,
59    /// Enforce authentication on every request.
60    ///
61    /// OSS default: `false` — the slim auth/extractor.rs falls back
62    /// to `default_user_from_state` (the `uuid5(NAMESPACE_OID, email)`
63    /// default user) when no `AuthResolver` is wired and the request
64    /// carries no credential. Closed cloud builds inject an
65    /// `AuthResolver` via `RouterBuilder::with_auth_resolver(...)` and
66    /// set this to `true` to require credentials.
67    ///
68    /// Override at runtime with `REQUIRE_AUTHENTICATION=true`.
69    pub require_authentication: bool,
70    /// JWT signing secret. Env: `AUTH_JWT_SECRET`.
71    /// Randomly generated at boot when unset (tokens are invalidated on restart).
72    pub jwt_secret: SecretString,
73    /// JWT validity window. Env: `AUTH_JWT_LIFETIME_SECONDS`. Default: 3600 s.
74    pub jwt_lifetime: Duration,
75    /// Maximum request body size in bytes. Env: `HTTP_BODY_LIMIT_BYTES`.
76    /// Default: 100 MiB.
77    pub body_limit: usize,
78
79    // ── Pipeline registry knobs ──────────────────────────────────────────────
80    //
81    // These map to `cognee_core::pipeline_run_registry::RegistryConfig` fields.
82    // Env vars are prefixed `PIPELINE_REGISTRY_` per pipelines.md §6.2.
83    /// Max in-memory runs. Env: `PIPELINE_REGISTRY_MAX_RUNS`. Default: 4096.
84    pub pipeline_registry_max_runs: usize,
85    /// Finished-run retention in seconds. Env: `PIPELINE_REGISTRY_FINISHED_RETENTION_SECS`.
86    /// Default: 3600.
87    pub pipeline_registry_finished_retention_secs: u64,
88    /// Per-run broadcast channel capacity. Env: `PIPELINE_REGISTRY_CHANNEL_CAPACITY`.
89    /// Default: 64.
90    pub pipeline_registry_channel_capacity: usize,
91    /// Whether to write ERRORED rows on abort/shutdown.
92    /// Env: `PIPELINE_REGISTRY_ABORT_WRITES_ERRORED`. Default: true.
93    /// Set to false for strict Python parity (Python leaves rows as STARTED on
94    /// unclean shutdown). See pipelines.md §12.
95    pub pipeline_registry_abort_writes_errored: bool,
96
97    /// Wall-clock timeout for `POST /api/v1/notebooks/{id}/{cell}/run`.
98    /// Env: `NOTEBOOK_RUN_TIMEOUT_SECS`. Default: 30 s.
99    pub notebook_run_timeout: Duration,
100
101    // ── Health checker knobs ─────────────────────────────────────────────────
102    /// Whether the `/health/detailed` probe should test the LLM provider and
103    /// the embedding engine.
104    /// Env: `COGNEE_HEALTH_PROBE_LLM`. Default: `false`.
105    ///
106    /// LLM probes consume tokens; embedding probes can hit a remote provider,
107    /// so both are opt-in. When `false`, the corresponding entries are omitted
108    /// from the report (mirrors Python's opt-in behavior).
109    pub health_probe_llm: bool,
110
111    /// Per-probe timeout in milliseconds. Each component probe is wrapped in
112    /// `tokio::time::timeout(..)` with this value; expiry yields an
113    /// `Unhealthy` (critical) or `Degraded` (non-critical) entry.
114    /// Env: `COGNEE_HEALTH_PROBE_TIMEOUT_MS`. Default: 2000.
115    pub health_probe_timeout_ms: u64,
116
117    /// In-process cache TTL for the aggregated `HealthCheckReport`.
118    /// Back-to-back `/health` requests within this window are served from
119    /// cache to avoid hammering all backends from k8s liveness probes.
120    /// Env: `COGNEE_HEALTH_CACHE_TTL_MS`. Default: 5000. Set to `0` to
121    /// disable caching.
122    pub health_cache_ttl_ms: u64,
123
124    // ── Standalone backend wiring knobs ─────────────────────────────────────
125    /// Root directory for ingested data files (LocalStorage).
126    /// Env: `DATA_ROOT_DIRECTORY`.
127    pub data_root_directory: PathBuf,
128
129    /// Root directory for system state (graph/vector/sqlite files).
130    /// Env: `SYSTEM_ROOT_DIRECTORY`.
131    pub system_root_directory: PathBuf,
132
133    /// Relational DB URL.
134    /// Env: `RELATIONAL_DB_URL` (fallback `DATABASE_URL`).
135    pub relational_db_url: String,
136
137    /// Graph provider name.
138    /// Env: `GRAPH_DATABASE_PROVIDER`. Default: `ladybug`.
139    pub graph_provider: String,
140
141    /// Graph file path (for embedded ladybug graph DB).
142    /// Env: `GRAPH_FILE_PATH`.
143    pub graph_file_path: PathBuf,
144
145    /// Vector provider name.
146    /// Env: `VECTOR_DB_PROVIDER`. Default: `pgvector`.
147    /// Note: the qdrant adapter has been extracted to the closed
148    /// `cognee-vector-qdrant` crate as part of the OSS/closed split. The OSS
149    /// http-server now defaults to pgvector and supports `mock` only when
150    /// built with the `dev-mock` cargo feature.
151    pub vector_provider: String,
152
153    /// Vector DB URL/path.
154    /// Env: `VECTOR_DB_URL`. For pgvector this is a Postgres connection string.
155    pub vector_db_url: String,
156
157    /// Embedding provider name.
158    /// Env: `EMBEDDING_PROVIDER`.
159    pub embedding_provider: String,
160
161    /// Embedding vector dimensions.
162    /// Env: `EMBEDDING_DIMENSIONS`.
163    pub embedding_dimensions: u32,
164
165    /// Embedding model identifier.
166    /// Env: `EMBEDDING_MODEL_NAME` (fallback `EMBEDDING_MODEL`).
167    pub embedding_model_name: String,
168
169    /// Embedding model file path (ONNX).
170    /// Env: `EMBEDDING_MODEL_PATH`.
171    pub embedding_model_path: Option<PathBuf>,
172
173    /// Embedding tokenizer file path (ONNX).
174    /// Env: `EMBEDDING_TOKENIZER_PATH`.
175    pub embedding_tokenizer_path: Option<PathBuf>,
176
177    /// Embedding endpoint for remote providers.
178    /// Env: `EMBEDDING_ENDPOINT`.
179    pub embedding_endpoint: String,
180
181    /// Embedding API key.
182    /// Env: `EMBEDDING_API_KEY` (fallbacks: `LLM_API_KEY`, `OPENAI_TOKEN`).
183    pub embedding_api_key: SecretString,
184
185    /// LLM provider name.
186    /// Env: `LLM_PROVIDER`.
187    pub llm_provider: String,
188
189    /// LLM model name.
190    /// Env: `LLM_MODEL` (fallback `OPENAI_MODEL`).
191    pub llm_model: String,
192
193    /// LLM API key.
194    /// Env: `LLM_API_KEY` (fallback `OPENAI_TOKEN`).
195    pub llm_api_key: SecretString,
196
197    /// LLM endpoint.
198    /// Env: `LLM_ENDPOINT` (fallback `OPENAI_URL`).
199    pub llm_endpoint: String,
200
201    /// LLM retry count for both structured-output and network retries.
202    /// Env: `LLM_MAX_RETRIES`.
203    pub llm_max_retries: u32,
204
205    /// Session store backend selector.
206    /// Env: `COGNEE_SESSION_STORE`.
207    pub session_store_backend: String,
208
209    /// Session root directory (for fs-based stores).
210    /// Env: `COGNEE_SESSION_DIR`.
211    pub session_root_directory: PathBuf,
212
213    /// Whether notebook code execution backend is enabled.
214    /// Env: `COGNEE_NOTEBOOK_RUNNER_ENABLED`.
215    pub notebook_runner_enabled: bool,
216
217    /// Whether Responses API client should be wired.
218    /// Env: `COGNEE_RESPONSES_CLIENT_ENABLED`.
219    pub responses_client_enabled: bool,
220
221    /// Disable standalone default backend wiring in `main`.
222    /// Env: `COGNEE_DISABLE_DEFAULT_BACKENDS`.
223    pub disable_default_backends: bool,
224
225    /// Email address used to derive the synthetic default user when
226    /// `require_authentication=false` and no `AuthResolver` is wired.
227    ///
228    /// The resulting owner id is
229    /// `Uuid::new_v5(&Uuid::NAMESPACE_OID, default_user_email.as_bytes())`
230    /// — the same derivation used by
231    /// [`cognee_lib::api::user::get_or_create_default_user`] and the
232    /// Python reference SDK (`uuid5(NAMESPACE_OID, email)`).
233    ///
234    /// Mirrors `Settings::default_user_email` in `cognee-lib` so the HTTP
235    /// server and the bindings/CLI agree on owner ids for the same
236    /// configured email. Env: `DEFAULT_USER_EMAIL`. Default:
237    /// `"default_user@example.com"`.
238    pub default_user_email: String,
239}
240
241fn default_cache_root() -> PathBuf {
242    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
243        PathBuf::from(xdg).join("cognee")
244    } else if let Ok(home) = std::env::var("HOME") {
245        PathBuf::from(home).join(".cache").join("cognee")
246    } else {
247        PathBuf::from("./.cognee")
248    }
249}
250
251fn parse_env_bool_with_default(v: &str, default: bool) -> bool {
252    if cognee_utils::parse_env_bool(v) {
253        true
254    } else {
255        let trimmed = v.trim().to_ascii_lowercase();
256        if matches!(trimmed.as_str(), "false" | "0" | "no" | "off") {
257            false
258        } else {
259            default
260        }
261    }
262}
263
264fn first_non_empty_env(keys: &[&str]) -> Option<String> {
265    for key in keys {
266        if let Ok(v) = std::env::var(key) {
267            let trimmed = v.trim();
268            if !trimmed.is_empty() {
269                return Some(trimmed.to_string());
270            }
271        }
272    }
273    None
274}
275
276fn default_relational_db_url(system_root_directory: &std::path::Path) -> String {
277    format!(
278        "sqlite://{}",
279        system_root_directory.join("cognee.db").display()
280    )
281}
282
283fn default_graph_file_path(system_root_directory: &std::path::Path) -> PathBuf {
284    system_root_directory.join("graph")
285}
286
287fn default_vector_db_url(system_root_directory: &std::path::Path) -> String {
288    system_root_directory.join("vectors").display().to_string()
289}
290
291fn default_session_root_directory(system_root_directory: &std::path::Path) -> PathBuf {
292    system_root_directory.join("sessions")
293}
294
295impl Default for HttpServerConfig {
296    fn default() -> Self {
297        let cache_root = default_cache_root();
298        let data_root = cache_root.join("data");
299        let system_root = cache_root.join("system");
300        Self {
301            host: "0.0.0.0".into(),
302            port: 8000,
303            cors_allowed_origins: Vec::new(),
304            ui_app_url: "http://localhost:3000".into(),
305            env: Environment::Prod,
306            require_authentication: false,
307            jwt_secret: SecretString::new(uuid::Uuid::new_v4().to_string().into()),
308            jwt_lifetime: Duration::from_secs(3600),
309            body_limit: 100 * 1024 * 1024,
310            pipeline_registry_max_runs: 4096,
311            pipeline_registry_finished_retention_secs: 3600,
312            pipeline_registry_channel_capacity: 64,
313            pipeline_registry_abort_writes_errored: true,
314            notebook_run_timeout: Duration::from_secs(30),
315            health_probe_llm: false,
316            health_probe_timeout_ms: 2000,
317            health_cache_ttl_ms: 5000,
318            data_root_directory: data_root,
319            system_root_directory: system_root.clone(),
320            relational_db_url: default_relational_db_url(&system_root),
321            graph_provider: "ladybug".to_string(),
322            graph_file_path: default_graph_file_path(&system_root),
323            vector_provider: "pgvector".to_string(),
324            vector_db_url: default_vector_db_url(&system_root),
325            embedding_provider: "onnx".to_string(),
326            embedding_dimensions: 384,
327            embedding_model_name: "bge-small-en-v1.5".to_string(),
328            embedding_model_path: None,
329            embedding_tokenizer_path: None,
330            embedding_endpoint: String::new(),
331            embedding_api_key: SecretString::new(String::new().into()),
332            llm_provider: "openai".to_string(),
333            llm_model: "gpt-4o-mini".to_string(),
334            llm_api_key: SecretString::new(String::new().into()),
335            llm_endpoint: String::new(),
336            llm_max_retries: 3,
337            session_store_backend: "seaorm".to_string(),
338            session_root_directory: default_session_root_directory(&system_root),
339            notebook_runner_enabled: false,
340            responses_client_enabled: false,
341            disable_default_backends: false,
342            default_user_email: "default_user@example.com".to_string(),
343        }
344    }
345}
346
347impl HttpServerConfig {
348    /// Build config by overlaying environment variables on top of the defaults.
349    ///
350    /// Called only by the standalone binary entry point; library embedders
351    /// construct `HttpServerConfig` directly.
352    pub fn from_env() -> Result<Self, ServerError> {
353        let mut cfg = Self::default();
354        let default_system_root_directory = cfg.system_root_directory.clone();
355
356        if let Ok(v) = std::env::var("HTTP_API_HOST") {
357            cfg.host = v;
358        }
359        if let Ok(v) = std::env::var("HTTP_API_PORT") {
360            cfg.port = v
361                .parse::<u16>()
362                .map_err(|e| ServerError::Other(anyhow::anyhow!("HTTP_API_PORT: {e}")))?;
363        }
364        if let Ok(v) = std::env::var("CORS_ALLOWED_ORIGINS") {
365            cfg.cors_allowed_origins = v
366                .split(',')
367                .map(|s| s.trim().to_owned())
368                .filter(|s| !s.is_empty())
369                .collect();
370        }
371        if let Ok(v) = std::env::var("UI_APP_URL") {
372            cfg.ui_app_url = v;
373        }
374        if let Ok(v) = std::env::var("ENV") {
375            cfg.env = v.parse().unwrap_or(Environment::Prod);
376        }
377        if let Ok(v) = std::env::var("REQUIRE_AUTHENTICATION") {
378            cfg.require_authentication =
379                !matches!(v.to_ascii_lowercase().as_str(), "false" | "0" | "no");
380        }
381        if let Ok(v) = std::env::var("AUTH_JWT_SECRET") {
382            cfg.jwt_secret = SecretString::new(v.into());
383        }
384        if let Ok(v) = std::env::var("AUTH_JWT_LIFETIME_SECONDS") {
385            let secs = v.parse::<u64>().map_err(|e| {
386                ServerError::Other(anyhow::anyhow!("AUTH_JWT_LIFETIME_SECONDS: {e}"))
387            })?;
388            cfg.jwt_lifetime = Duration::from_secs(secs);
389        }
390        if let Ok(v) = std::env::var("HTTP_BODY_LIMIT_BYTES") {
391            cfg.body_limit = v
392                .parse::<usize>()
393                .map_err(|e| ServerError::Other(anyhow::anyhow!("HTTP_BODY_LIMIT_BYTES: {e}")))?;
394        }
395
396        // Pipeline registry knobs
397        if let Ok(v) = std::env::var("PIPELINE_REGISTRY_MAX_RUNS") {
398            cfg.pipeline_registry_max_runs = v.parse::<usize>().map_err(|e| {
399                ServerError::Other(anyhow::anyhow!("PIPELINE_REGISTRY_MAX_RUNS: {e}"))
400            })?;
401        }
402        if let Ok(v) = std::env::var("PIPELINE_REGISTRY_FINISHED_RETENTION_SECS") {
403            cfg.pipeline_registry_finished_retention_secs = v.parse::<u64>().map_err(|e| {
404                ServerError::Other(anyhow::anyhow!(
405                    "PIPELINE_REGISTRY_FINISHED_RETENTION_SECS: {e}"
406                ))
407            })?;
408        }
409        if let Ok(v) = std::env::var("PIPELINE_REGISTRY_CHANNEL_CAPACITY") {
410            cfg.pipeline_registry_channel_capacity = v.parse::<usize>().map_err(|e| {
411                ServerError::Other(anyhow::anyhow!("PIPELINE_REGISTRY_CHANNEL_CAPACITY: {e}"))
412            })?;
413        }
414        if let Ok(v) = std::env::var("PIPELINE_REGISTRY_ABORT_WRITES_ERRORED") {
415            cfg.pipeline_registry_abort_writes_errored =
416                !matches!(v.to_ascii_lowercase().as_str(), "false" | "0" | "no");
417        }
418
419        if let Ok(v) = std::env::var("NOTEBOOK_RUN_TIMEOUT_SECS") {
420            let secs = v.parse::<u64>().map_err(|e| {
421                ServerError::Other(anyhow::anyhow!("NOTEBOOK_RUN_TIMEOUT_SECS: {e}"))
422            })?;
423            cfg.notebook_run_timeout = Duration::from_secs(secs);
424        }
425
426        // Health checker knobs
427        if let Ok(v) = std::env::var("COGNEE_HEALTH_PROBE_LLM") {
428            cfg.health_probe_llm =
429                matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on");
430        }
431        if let Ok(v) = std::env::var("COGNEE_HEALTH_PROBE_TIMEOUT_MS") {
432            cfg.health_probe_timeout_ms = v.parse::<u64>().map_err(|e| {
433                ServerError::Other(anyhow::anyhow!("COGNEE_HEALTH_PROBE_TIMEOUT_MS: {e}"))
434            })?;
435        }
436        if let Ok(v) = std::env::var("COGNEE_HEALTH_CACHE_TTL_MS") {
437            cfg.health_cache_ttl_ms = v.parse::<u64>().map_err(|e| {
438                ServerError::Other(anyhow::anyhow!("COGNEE_HEALTH_CACHE_TTL_MS: {e}"))
439            })?;
440        }
441
442        // Standalone backend wiring knobs
443        if let Ok(v) = std::env::var("DATA_ROOT_DIRECTORY") {
444            cfg.data_root_directory = PathBuf::from(v);
445        }
446        if let Ok(v) = std::env::var("SYSTEM_ROOT_DIRECTORY") {
447            cfg.system_root_directory = PathBuf::from(v);
448
449            // Keep standalone wiring coherent: if dependent paths still match
450            // their old defaults, rebase them to the new system root.
451            if cfg.relational_db_url == default_relational_db_url(&default_system_root_directory) {
452                cfg.relational_db_url = default_relational_db_url(&cfg.system_root_directory);
453            }
454            if cfg.graph_file_path == default_graph_file_path(&default_system_root_directory) {
455                cfg.graph_file_path = default_graph_file_path(&cfg.system_root_directory);
456            }
457            if cfg.vector_db_url == default_vector_db_url(&default_system_root_directory) {
458                cfg.vector_db_url = default_vector_db_url(&cfg.system_root_directory);
459            }
460            if cfg.session_root_directory
461                == default_session_root_directory(&default_system_root_directory)
462            {
463                cfg.session_root_directory =
464                    default_session_root_directory(&cfg.system_root_directory);
465            }
466        }
467
468        if let Some(v) = first_non_empty_env(&["RELATIONAL_DB_URL", "DATABASE_URL"]) {
469            cfg.relational_db_url = v;
470        }
471
472        if let Ok(v) = std::env::var("GRAPH_DATABASE_PROVIDER") {
473            cfg.graph_provider = v;
474        }
475        if let Ok(v) = std::env::var("GRAPH_FILE_PATH") {
476            cfg.graph_file_path = PathBuf::from(v);
477        }
478
479        if let Ok(v) = std::env::var("VECTOR_DB_PROVIDER") {
480            cfg.vector_provider = v;
481        }
482        if let Ok(v) = std::env::var("VECTOR_DB_URL") {
483            cfg.vector_db_url = v;
484        }
485
486        if let Ok(v) = std::env::var("EMBEDDING_PROVIDER") {
487            cfg.embedding_provider = v;
488        }
489        if let Ok(v) = std::env::var("EMBEDDING_DIMENSIONS") {
490            cfg.embedding_dimensions = v
491                .parse::<u32>()
492                .map_err(|e| ServerError::Other(anyhow::anyhow!("EMBEDDING_DIMENSIONS: {e}")))?;
493        }
494        if let Some(v) = first_non_empty_env(&["EMBEDDING_MODEL_NAME", "EMBEDDING_MODEL"]) {
495            cfg.embedding_model_name = v;
496        }
497        if let Ok(v) = std::env::var("EMBEDDING_MODEL_PATH") {
498            cfg.embedding_model_path = Some(PathBuf::from(v));
499        }
500        if let Ok(v) = std::env::var("EMBEDDING_TOKENIZER_PATH") {
501            cfg.embedding_tokenizer_path = Some(PathBuf::from(v));
502        }
503        if let Ok(v) = std::env::var("EMBEDDING_ENDPOINT") {
504            cfg.embedding_endpoint = v;
505        }
506        if let Some(v) = first_non_empty_env(&["EMBEDDING_API_KEY", "LLM_API_KEY", "OPENAI_TOKEN"])
507        {
508            cfg.embedding_api_key = SecretString::new(v.into());
509        }
510
511        if let Ok(v) = std::env::var("LLM_PROVIDER") {
512            cfg.llm_provider = v;
513        }
514        if let Some(v) = first_non_empty_env(&["LLM_MODEL", "OPENAI_MODEL"]) {
515            cfg.llm_model = v;
516        }
517        if let Some(v) = first_non_empty_env(&["LLM_API_KEY", "OPENAI_TOKEN"]) {
518            cfg.llm_api_key = SecretString::new(v.into());
519        }
520        if let Some(v) = first_non_empty_env(&["LLM_ENDPOINT", "OPENAI_URL"]) {
521            cfg.llm_endpoint = v;
522        }
523        if let Ok(v) = std::env::var("LLM_MAX_RETRIES") {
524            cfg.llm_max_retries = v
525                .parse::<u32>()
526                .map_err(|e| ServerError::Other(anyhow::anyhow!("LLM_MAX_RETRIES: {e}")))?;
527        }
528
529        if let Ok(v) = std::env::var("COGNEE_SESSION_STORE") {
530            cfg.session_store_backend = v;
531        }
532        if let Ok(v) = std::env::var("COGNEE_SESSION_DIR") {
533            cfg.session_root_directory = PathBuf::from(v);
534        }
535
536        if let Ok(v) = std::env::var("COGNEE_NOTEBOOK_RUNNER_ENABLED") {
537            cfg.notebook_runner_enabled = cognee_utils::parse_env_bool(&v);
538        }
539
540        if let Ok(v) = std::env::var("COGNEE_RESPONSES_CLIENT_ENABLED") {
541            cfg.responses_client_enabled = parse_env_bool_with_default(&v, false);
542        } else {
543            cfg.responses_client_enabled = !cfg.llm_api_key.expose_secret().is_empty();
544        }
545
546        if let Ok(v) = std::env::var("COGNEE_DISABLE_DEFAULT_BACKENDS") {
547            cfg.disable_default_backends = cognee_utils::parse_env_bool(&v);
548        }
549
550        if let Ok(v) = std::env::var("DEFAULT_USER_EMAIL") {
551            let trimmed = v.trim();
552            if !trimmed.is_empty() {
553                cfg.default_user_email = trimmed.to_string();
554            }
555        }
556
557        Ok(cfg)
558    }
559}
560
561impl HttpServerConfig {
562    /// Build a `RegistryConfig` from the matching `HttpServerConfig` fields.
563    pub fn to_registry_config(&self) -> RegistryConfig {
564        RegistryConfig {
565            max_in_memory_runs: self.pipeline_registry_max_runs,
566            finished_retention: Duration::from_secs(self.pipeline_registry_finished_retention_secs),
567            channel_capacity: self.pipeline_registry_channel_capacity,
568            yield_throttle: None, // not exposed via env in Phase 3
569            abort_writes_errored_row: self.pipeline_registry_abort_writes_errored,
570        }
571    }
572}
573
574// ─── Unit tests ──────────────────────────────────────────────────────────────
575
576#[cfg(test)]
577#[allow(
578    clippy::unwrap_used,
579    clippy::expect_used,
580    reason = "test code — panics are acceptable failures"
581)]
582mod tests {
583    use super::*;
584    use secrecy::ExposeSecret;
585
586    #[test]
587    fn test_defaults() {
588        let cfg = HttpServerConfig::default();
589        assert_eq!(cfg.host, "0.0.0.0");
590        assert_eq!(cfg.port, 8000);
591        assert_eq!(cfg.ui_app_url, "http://localhost:3000");
592        assert_eq!(cfg.body_limit, 100 * 1024 * 1024);
593        assert_eq!(cfg.jwt_lifetime, Duration::from_secs(3600));
594        // OSS default is `false`: without an `AuthResolver`, requests fall
595        // back to the synthetic default user (see auth/extractor.rs). Closed
596        // cloud builds inject an `AuthResolver` and flip this to `true`.
597        assert!(!cfg.require_authentication);
598        assert!(cfg.cors_allowed_origins.is_empty());
599        assert_eq!(cfg.env, Environment::Prod);
600    }
601
602    #[test]
603    fn test_env_override_port() {
604        // SAFETY: test-only; no concurrent threads modify this env var.
605        unsafe {
606            std::env::set_var("HTTP_API_PORT", "9999");
607        }
608        let cfg = HttpServerConfig::from_env().expect("from_env");
609        // SAFETY: test-only.
610        unsafe {
611            std::env::remove_var("HTTP_API_PORT");
612        }
613        assert_eq!(cfg.port, 9999);
614    }
615
616    #[test]
617    fn test_env_cors_origins() {
618        // SAFETY: test-only; no concurrent threads modify this env var.
619        unsafe {
620            std::env::set_var("CORS_ALLOWED_ORIGINS", "http://a.test, http://b.test");
621        }
622        let cfg = HttpServerConfig::from_env().expect("from_env");
623        // SAFETY: test-only.
624        unsafe {
625            std::env::remove_var("CORS_ALLOWED_ORIGINS");
626        }
627        assert_eq!(
628            cfg.cors_allowed_origins,
629            vec!["http://a.test", "http://b.test"]
630        );
631    }
632
633    #[test]
634    fn test_environment_from_str() {
635        assert_eq!("dev".parse::<Environment>().unwrap(), Environment::Dev);
636        assert_eq!("test".parse::<Environment>().unwrap(), Environment::Test);
637        assert_eq!("prod".parse::<Environment>().unwrap(), Environment::Prod);
638        assert_eq!(
639            "anything".parse::<Environment>().unwrap(),
640            Environment::Prod
641        );
642    }
643
644    #[test]
645    fn test_bool_backend_flags_from_env() {
646        // SAFETY: test-only; no concurrent threads modify these env vars.
647        unsafe {
648            std::env::set_var("COGNEE_NOTEBOOK_RUNNER_ENABLED", "yes");
649            std::env::set_var("COGNEE_RESPONSES_CLIENT_ENABLED", "1");
650            std::env::set_var("COGNEE_DISABLE_DEFAULT_BACKENDS", "true");
651        }
652        let cfg = HttpServerConfig::from_env().expect("from_env");
653        // SAFETY: test-only.
654        unsafe {
655            std::env::remove_var("COGNEE_NOTEBOOK_RUNNER_ENABLED");
656            std::env::remove_var("COGNEE_RESPONSES_CLIENT_ENABLED");
657            std::env::remove_var("COGNEE_DISABLE_DEFAULT_BACKENDS");
658        }
659
660        assert!(cfg.notebook_runner_enabled);
661        assert!(cfg.responses_client_enabled);
662        assert!(cfg.disable_default_backends);
663    }
664
665    #[test]
666    fn test_llm_fallback_env_aliases() {
667        // SAFETY: test-only; no concurrent threads modify these env vars.
668        unsafe {
669            std::env::set_var("OPENAI_TOKEN", "test-key");
670            std::env::set_var("OPENAI_MODEL", "gpt-test");
671            std::env::set_var("OPENAI_URL", "https://example.test/v1");
672            std::env::remove_var("LLM_API_KEY");
673            std::env::remove_var("LLM_MODEL");
674            std::env::remove_var("LLM_ENDPOINT");
675        }
676        let cfg = HttpServerConfig::from_env().expect("from_env");
677        // SAFETY: test-only.
678        unsafe {
679            std::env::remove_var("OPENAI_TOKEN");
680            std::env::remove_var("OPENAI_MODEL");
681            std::env::remove_var("OPENAI_URL");
682        }
683
684        assert_eq!(cfg.llm_api_key.expose_secret(), "test-key");
685        assert_eq!(cfg.llm_model, "gpt-test");
686        assert_eq!(cfg.llm_endpoint, "https://example.test/v1");
687    }
688
689    #[test]
690    fn test_system_root_directory_rebases_dependent_defaults() {
691        let temp = tempfile::tempdir().expect("tempdir");
692        let new_root = temp.path().join("custom-system-root");
693
694        // SAFETY: test-only; no concurrent threads modify these env vars.
695        unsafe {
696            std::env::set_var("SYSTEM_ROOT_DIRECTORY", &new_root);
697            std::env::remove_var("RELATIONAL_DB_URL");
698            std::env::remove_var("DATABASE_URL");
699            std::env::remove_var("GRAPH_FILE_PATH");
700            std::env::remove_var("VECTOR_DB_URL");
701            std::env::remove_var("COGNEE_SESSION_DIR");
702        }
703
704        let cfg = HttpServerConfig::from_env().expect("from_env");
705
706        // SAFETY: test-only.
707        unsafe {
708            std::env::remove_var("SYSTEM_ROOT_DIRECTORY");
709        }
710
711        assert_eq!(cfg.system_root_directory, new_root);
712        assert_eq!(cfg.relational_db_url, default_relational_db_url(&new_root));
713        assert_eq!(cfg.graph_file_path, default_graph_file_path(&new_root));
714        assert_eq!(cfg.vector_db_url, default_vector_db_url(&new_root));
715        assert_eq!(
716            cfg.session_root_directory,
717            default_session_root_directory(&new_root)
718        );
719    }
720}