Skip to main content

lean_ctx/core/config/
enterprise.rs

1//! `[enterprise]` — connect a lean-ctx Runtime to a LeanCTX Enterprise Suite.
2//!
3//! When `gateway_url` is set, the proxy injects `x-leanctx-*` metadata headers
4//! on every upstream request so the Suite can attribute context savings,
5//! populate the economics ledger, and make routing decisions.
6
7use serde::{Deserialize, Serialize};
8
9/// Enterprise Suite connection configuration (`[enterprise]` in config.toml).
10///
11/// Example:
12/// ```toml
13/// [enterprise]
14/// gateway_url = "https://api.leanctx.com"
15/// instance_token = "lctx_inst_..."
16/// instance_id = "runtime-macbook-yves"
17/// ```
18#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
19#[serde(default)]
20pub struct EnterpriseConfig {
21    /// Base URL of the LeanCTX Enterprise Suite Gateway.
22    /// When set, all provider requests are routed through this gateway AND
23    /// `x-leanctx-*` metadata headers are injected.
24    /// Env override: `LEAN_CTX_ENTERPRISE_GATEWAY_URL`.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub gateway_url: Option<String>,
27
28    /// Bearer token for authenticating this Runtime instance with the Suite.
29    /// Env override: `LEAN_CTX_ENTERPRISE_TOKEN`.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub instance_token: Option<String>,
32
33    /// Stable identifier for this Runtime instance (e.g. "macbook-yves",
34    /// "ci-runner-3"). Sent as `x-leanctx-instance`. Auto-generated from
35    /// hostname if unset.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub instance_id: Option<String>,
38
39    /// Whether to inject `x-leanctx-*` headers even when NOT routing through
40    /// the gateway (i.e. when using direct provider upstreams but still wanting
41    /// to report metadata to a sidecar collector). Default: headers are only
42    /// injected when `gateway_url` is set.
43    #[serde(default)]
44    pub always_inject_headers: bool,
45
46    /// Disable the enterprise integration without removing the config block.
47    #[serde(default)]
48    pub disabled: bool,
49}
50
51impl EnterpriseConfig {
52    /// Resolve effective gateway URL (config → env).
53    pub fn effective_gateway_url(&self) -> Option<&str> {
54        if self.disabled {
55            return None;
56        }
57        if let Ok(env_url) = std::env::var("LEAN_CTX_ENTERPRISE_GATEWAY_URL") {
58            if !env_url.is_empty() {
59                // Env override — cannot return a reference to a local.
60                // Caller should use `effective_gateway_url_owned()` for env.
61                return self.gateway_url.as_deref();
62            }
63        }
64        self.gateway_url.as_deref()
65    }
66
67    /// Resolve effective gateway URL with env override (owned).
68    pub fn effective_gateway_url_owned(&self) -> Option<String> {
69        if self.disabled {
70            return None;
71        }
72        std::env::var("LEAN_CTX_ENTERPRISE_GATEWAY_URL")
73            .ok()
74            .filter(|s| !s.is_empty())
75            .or_else(|| self.gateway_url.clone())
76    }
77
78    /// Resolve the instance token (config → env).
79    pub fn effective_token(&self) -> Option<String> {
80        if self.disabled {
81            return None;
82        }
83        std::env::var("LEAN_CTX_ENTERPRISE_TOKEN")
84            .ok()
85            .filter(|s| !s.is_empty())
86            .or_else(|| self.instance_token.clone())
87    }
88
89    /// Resolve instance ID (config → hostname fallback).
90    pub fn effective_instance_id(&self) -> String {
91        if let Some(id) = &self.instance_id {
92            return id.clone();
93        }
94        gethostname::gethostname()
95            .into_string()
96            .unwrap_or_else(|_| "unknown".to_owned())
97    }
98
99    /// Whether header injection is active for the current request.
100    pub fn should_inject_headers(&self) -> bool {
101        if self.disabled {
102            return false;
103        }
104        self.always_inject_headers || self.gateway_url.is_some()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn disabled_blocks_everything() {
114        let cfg = EnterpriseConfig {
115            gateway_url: Some("https://api.leanctx.com".to_owned()),
116            instance_token: Some("tok".to_owned()),
117            disabled: true,
118            ..Default::default()
119        };
120        assert_eq!(cfg.effective_gateway_url(), None);
121        assert_eq!(cfg.effective_token(), None);
122        assert!(!cfg.should_inject_headers());
123    }
124
125    #[test]
126    fn headers_injected_when_gateway_set() {
127        let cfg = EnterpriseConfig {
128            gateway_url: Some("https://api.leanctx.com".to_owned()),
129            ..Default::default()
130        };
131        assert!(cfg.should_inject_headers());
132    }
133
134    #[test]
135    fn always_inject_without_gateway() {
136        let cfg = EnterpriseConfig {
137            always_inject_headers: true,
138            ..Default::default()
139        };
140        assert!(cfg.should_inject_headers());
141    }
142
143    #[test]
144    fn instance_id_fallback() {
145        let cfg = EnterpriseConfig::default();
146        let id = cfg.effective_instance_id();
147        assert!(!id.is_empty());
148    }
149}