Skip to main content

ai_agent/
bridge_enabled.rs

1//! Bridge mode entitlement checks.
2//!
3//! Translated from openclaudecode/src/bridge/bridgeEnabled.ts
4
5use crate::constants::env::ai;
6use std::collections::HashMap;
7use std::sync::OnceLock;
8
9// =============================================================================
10// BUILD-TIME FEATURE FLAGS
11// =============================================================================
12
13static BRIDGE_MODE: OnceLock<bool> = OnceLock::new();
14static CCR_AUTO_CONNECT: OnceLock<bool> = OnceLock::new();
15static CCR_MIRROR: OnceLock<bool> = OnceLock::new();
16
17fn is_bridge_mode_enabled() -> bool {
18    *BRIDGE_MODE.get_or_init(|| {
19        std::env::var(ai::BRIDGE_MODE)
20            .map(|v| v == "1" || v.to_lowercase() == "true")
21            .unwrap_or(false)
22    })
23}
24
25fn is_ccr_auto_connect_enabled() -> bool {
26    *CCR_AUTO_CONNECT.get_or_init(|| {
27        std::env::var(ai::CCR_AUTO_CONNECT)
28            .map(|v| v == "1" || v.to_lowercase() == "true")
29            .unwrap_or(false)
30    })
31}
32
33fn is_ccr_mirror_feature_enabled() -> bool {
34    *CCR_MIRROR.get_or_init(|| {
35        std::env::var(ai::CCR_MIRROR)
36            .map(|v| v == "1" || v.to_lowercase() == "true")
37            .unwrap_or(false)
38    })
39}
40
41// =============================================================================
42// AUTH HELPER FUNCTIONS
43// =============================================================================
44
45fn is_claude_ai_subscriber() -> bool {
46    crate::session_history::get_bridge_access_token().is_some() && has_profile_scope()
47}
48
49fn has_profile_scope() -> bool {
50    crate::session_history::get_bridge_access_token().is_some()
51}
52
53fn get_oauth_account_info() -> Option<OauthAccountInfo> {
54    crate::session_history::get_bridge_access_token().map(|_| OauthAccountInfo {
55        organization_uuid: std::env::var(ai::ORGANIZATION_UUID).ok(),
56        organization_name: None,
57        email_address: None,
58    })
59}
60
61#[derive(Debug, Clone)]
62pub struct OauthAccountInfo {
63    pub organization_uuid: Option<String>,
64    pub organization_name: Option<String>,
65    pub email_address: Option<String>,
66}
67
68// =============================================================================
69// GROWTHBOOK STUB FUNCTIONS
70// =============================================================================
71
72static GROWTHBOOK_CACHE: OnceLock<HashMap<String, serde_json::Value>> = OnceLock::new();
73
74fn get_growthbook_cache() -> &'static HashMap<String, serde_json::Value> {
75    GROWTHBOOK_CACHE.get_or_init(|| {
76        let mut map = HashMap::new();
77        map.insert("tengu_ccr_bridge".to_string(), serde_json::json!(false));
78        map.insert("tengu_bridge_repl_v2".to_string(), serde_json::json!(false));
79        map.insert(
80            "tengu_bridge_repl_v2_cse_shim_enabled".to_string(),
81            serde_json::json!(true),
82        );
83        map.insert("tengu_cobalt_harbor".to_string(), serde_json::json!(false));
84        map.insert("tengu_ccr_mirror".to_string(), serde_json::json!(false));
85        map.insert(
86            "tengu_bridge_min_version".to_string(),
87            serde_json::json!({ "minVersion": "0.0.0" }),
88        );
89        map
90    })
91}
92
93fn get_feature_value_cached<T: serde::de::DeserializeOwned>(feature: &str, default: T) -> T {
94    let cache = get_growthbook_cache();
95    cache
96        .get(feature)
97        .and_then(|v| serde_json::from_value(v.clone()).ok())
98        .unwrap_or(default)
99}
100
101async fn check_gate_cached_or_blocking(gate: &str) -> bool {
102    get_feature_value_cached(gate, false)
103}
104
105fn get_dynamic_config_cached<T: serde::de::DeserializeOwned>(config_name: &str, default: T) -> T {
106    get_feature_value_cached(config_name, default)
107}
108
109// =============================================================================
110// VERSION CHECK
111// =============================================================================
112
113fn version_lt(a: &str, b: &str) -> bool {
114    let a_parts: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
115    let b_parts: Vec<u32> = b.split('.').filter_map(|s| s.parse().ok()).collect();
116    for (av, bv) in a_parts.iter().zip(b_parts.iter()) {
117        if av < bv {
118            return true;
119        }
120        if av > bv {
121            return false;
122        }
123    }
124    false
125}
126
127fn get_current_version() -> String {
128    env!("CARGO_PKG_VERSION").to_string()
129}
130
131// =============================================================================
132// ENVIRONMENT UTILITIES
133// =============================================================================
134
135fn is_env_truthy(env_var: &str) -> bool {
136    if env_var.is_empty() {
137        return false;
138    }
139    let binding = env_var.to_lowercase();
140    let normalized = binding.trim();
141    matches!(normalized, "1" | "true" | "yes" | "on")
142}
143
144fn is_env_truthy_opt(env_var: Option<String>) -> bool {
145    env_var.map(|v| is_env_truthy(&v)).unwrap_or(false)
146}
147
148// =============================================================================
149// MAIN BRIDGE ENABLED FUNCTIONS
150// =============================================================================
151
152pub fn is_bridge_enabled() -> bool {
153    if is_bridge_mode_enabled() {
154        is_claude_ai_subscriber() && get_feature_value_cached::<bool>("tengu_ccr_bridge", false)
155    } else {
156        false
157    }
158}
159
160pub async fn is_bridge_enabled_blocking() -> bool {
161    if is_bridge_mode_enabled() {
162        is_claude_ai_subscriber() && check_gate_cached_or_blocking("tengu_ccr_bridge").await
163    } else {
164        false
165    }
166}
167
168pub async fn get_bridge_disabled_reason() -> Option<String> {
169    if is_bridge_mode_enabled() {
170        if !is_claude_ai_subscriber() {
171            return Some("Remote Control requires a claude.ai subscription. Run `ai auth login` to sign in with your claude.ai account.".to_string());
172        }
173        if !has_profile_scope() {
174            return Some("Remote Control requires a full-scope login token. Long-lived tokens (from `ai setup-token` or AI_OAUTH_TOKEN) are limited to inference-only for security reasons. Run `ai auth login` to use Remote Control.".to_string());
175        }
176        if !get_oauth_account_info()
177            .and_then(|info| info.organization_uuid)
178            .is_some()
179        {
180            return Some("Unable to determine your organization for Remote Control eligibility. Run `ai auth login` to refresh your account information.".to_string());
181        }
182        if !check_gate_cached_or_blocking("tengu_ccr_bridge").await {
183            return Some("Remote Control is not yet enabled for your account.".to_string());
184        }
185        None
186    } else {
187        Some("Remote Control is not available in this build.".to_string())
188    }
189}
190
191pub fn is_env_less_bridge_enabled() -> bool {
192    if is_bridge_mode_enabled() {
193        get_feature_value_cached::<bool>("tengu_bridge_repl_v2", false)
194    } else {
195        false
196    }
197}
198
199pub fn is_cse_shim_enabled() -> bool {
200    if is_bridge_mode_enabled() {
201        get_feature_value_cached::<bool>("tengu_bridge_repl_v2_cse_shim_enabled", true)
202    } else {
203        true
204    }
205}
206
207pub fn check_bridge_min_version() -> Option<String> {
208    if is_bridge_mode_enabled() {
209        #[derive(serde::Deserialize)]
210        struct MinVersionConfig {
211            min_version: String,
212        }
213        let config = get_dynamic_config_cached::<MinVersionConfig>(
214            "tengu_bridge_min_version",
215            MinVersionConfig {
216                min_version: "0.0.0".to_string(),
217            },
218        );
219        if !config.min_version.is_empty() && version_lt(&get_current_version(), &config.min_version)
220        {
221            return Some(format!(
222                "Your version of AI Code ({}) is too old for Remote Control.\nVersion {} or higher is required. Run `ai update` to update.",
223                get_current_version(),
224                config.min_version
225            ));
226        }
227    }
228    None
229}
230
231pub fn get_ccr_auto_connect_default() -> bool {
232    if is_ccr_auto_connect_enabled() {
233        get_feature_value_cached::<bool>("tengu_cobalt_harbor", false)
234    } else {
235        false
236    }
237}
238
239pub fn is_ccr_mirror_enabled() -> bool {
240    if is_ccr_mirror_feature_enabled() {
241        is_env_truthy_opt(std::env::var(ai::CCR_MIRROR).ok())
242            || get_feature_value_cached::<bool>("tengu_ccr_mirror", false)
243    } else {
244        false
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    #[test]
252    fn test_version_lt() {
253        assert!(version_lt("0.7.0", "0.8.0"));
254        assert!(version_lt("0.7.2", "0.7.3"));
255        assert!(version_lt("0.6.0", "0.7.0"));
256        assert!(!version_lt("0.8.0", "0.7.0"));
257        assert!(!version_lt("0.7.0", "0.7.0"));
258    }
259    #[test]
260    fn test_is_env_truthy() {
261        assert!(is_env_truthy("1"));
262        assert!(is_env_truthy("true"));
263        assert!(is_env_truthy("TRUE"));
264        assert!(is_env_truthy("yes"));
265        assert!(is_env_truthy("on"));
266        assert!(!is_env_truthy("0"));
267        assert!(!is_env_truthy("false"));
268        assert!(!is_env_truthy(""));
269        assert!(!is_env_truthy("random"));
270    }
271}