ai_agent/
bridge_enabled.rs1use crate::constants::env::ai;
6use std::collections::HashMap;
7use std::sync::OnceLock;
8
9static 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
41fn 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
68static 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
109fn 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
131fn 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
148pub 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}