ai_agent/bridge/
bridge_enabled.rs1use std::env;
10use std::sync::OnceLock;
11
12pub type GateFn = Box<dyn Fn(&str) -> bool + Send + Sync>;
18
19pub type DynamicConfigFn = Box<dyn Fn(&str) -> Option<serde_json::Value> + Send + Sync>;
21
22pub type VersionCheckFn = Box<dyn Fn(&str, &str) -> bool + Send + Sync>;
24
25pub type SubscriberCheckFn = Box<dyn Fn() -> bool + Send + Sync>;
27
28pub type ProfileScopeFn = Box<dyn Fn() -> bool + Send + Sync>;
30
31pub type OauthAccountFn = Box<dyn Fn() -> Option<OAuthAccountInfo> + Send + Sync>;
33
34pub type EnvTruthyFn = Box<dyn Fn(&str) -> bool + Send + Sync>;
36
37static GATE_GETTER: OnceLock<GateFn> = OnceLock::new();
38static DYNAMIC_CONFIG_GETTER: OnceLock<DynamicConfigFn> = OnceLock::new();
39static VERSION_CHECK_GETTER: OnceLock<VersionCheckFn> = OnceLock::new();
40static SUBSCRIBER_CHECK: OnceLock<SubscriberCheckFn> = OnceLock::new();
41static PROFILE_SCOPE_CHECK: OnceLock<ProfileScopeFn> = OnceLock::new();
42static OAUTH_ACCOUNT_GETTER: OnceLock<OauthAccountFn> = OnceLock::new();
43static ENV_TRUTHY_CHECK: OnceLock<EnvTruthyFn> = OnceLock::new();
44
45static BRIDGE_MODE: OnceLock<bool> = OnceLock::new();
47static CCR_AUTO_CONNECT: OnceLock<bool> = OnceLock::new();
48static CCR_MIRROR: OnceLock<bool> = OnceLock::new();
49
50#[derive(Debug, Clone, Default)]
55pub struct OAuthAccountInfo {
56 pub organization_uuid: Option<String>,
57}
58
59pub fn register_gate_check(gate: impl Fn(&str) -> bool + Send + Sync + 'static) {
65 let _ = GATE_GETTER.set(Box::new(gate));
66}
67
68pub fn register_dynamic_config(
70 getter: impl Fn(&str) -> Option<serde_json::Value> + Send + Sync + 'static,
71) {
72 let _ = DYNAMIC_CONFIG_GETTER.set(Box::new(getter));
73}
74
75pub fn register_version_check(checker: impl Fn(&str, &str) -> bool + Send + Sync + 'static) {
77 let _ = VERSION_CHECK_GETTER.set(Box::new(checker));
78}
79
80pub fn register_subscriber_check(check: impl Fn() -> bool + Send + Sync + 'static) {
82 let _ = SUBSCRIBER_CHECK.set(Box::new(check));
83}
84
85pub fn register_profile_scope_check(check: impl Fn() -> bool + Send + Sync + 'static) {
87 let _ = PROFILE_SCOPE_CHECK.set(Box::new(check));
88}
89
90pub fn register_oauth_account_getter(
92 getter: impl Fn() -> Option<OAuthAccountInfo> + Send + Sync + 'static,
93) {
94 let _ = OAUTH_ACCOUNT_GETTER.set(Box::new(getter));
95}
96
97pub fn register_env_truthy_check(check: impl Fn(&str) -> bool + Send + Sync + 'static) {
99 let _ = ENV_TRUTHY_CHECK.set(Box::new(check));
100}
101
102pub fn set_bridge_mode(enabled: bool) {
104 let _ = BRIDGE_MODE.set(enabled);
105}
106
107pub fn set_ccr_auto_connect(enabled: bool) {
108 let _ = CCR_AUTO_CONNECT.set(enabled);
109}
110
111pub fn set_ccr_mirror(enabled: bool) {
112 let _ = CCR_MIRROR.set(enabled);
113}
114
115fn get_gate(gate_name: &str) -> bool {
120 GATE_GETTER
121 .get()
122 .map(|gate| gate(gate_name))
123 .unwrap_or(false)
124}
125
126fn get_dynamic_config(key: &str) -> Option<serde_json::Value> {
127 DYNAMIC_CONFIG_GETTER.get().and_then(|getter| getter(key))
128}
129
130fn check_version(current: &str, min: &str) -> bool {
131 VERSION_CHECK_GETTER
132 .get()
133 .map(|check| check(current, min))
134 .unwrap_or_else(|| {
135 current >= min
138 })
139}
140
141fn is_claude_ai_subscriber() -> bool {
142 SUBSCRIBER_CHECK.get().map(|check| check()).unwrap_or(false)
143}
144
145fn has_profile_scope() -> bool {
146 PROFILE_SCOPE_CHECK
147 .get()
148 .map(|check| check())
149 .unwrap_or(false)
150}
151
152fn get_oauth_account_info() -> Option<OAuthAccountInfo> {
153 OAUTH_ACCOUNT_GETTER.get().and_then(|getter| getter())
154}
155
156fn is_env_truthy(key: &str) -> bool {
157 ENV_TRUTHY_CHECK
158 .get()
159 .map(|check| check(key))
160 .unwrap_or_else(|| {
161 env::var(key)
162 .map(|v| v == "1" || v.to_lowercase() == "true")
163 .unwrap_or(false)
164 })
165}
166
167fn bridge_mode_enabled() -> bool {
168 BRIDGE_MODE.get().copied().unwrap_or(false)
169}
170
171fn ccr_auto_connect_enabled() -> bool {
172 CCR_AUTO_CONNECT.get().copied().unwrap_or(false)
173}
174
175fn ccr_mirror_enabled() -> bool {
176 CCR_MIRROR.get().copied().unwrap_or(false)
177}
178
179pub fn is_bridge_enabled() -> bool {
186 if !bridge_mode_enabled() {
187 return false;
188 }
189
190 get_gate("tengu_ccr_bridge")
193}
194
195pub fn is_bridge_enabled_blocking() -> bool {
198 is_bridge_enabled()
199}
200
201pub fn get_bridge_disabled_reason() -> Option<String> {
204 if !bridge_mode_enabled() {
205 return Some("Remote Control is not available in this build.".to_string());
206 }
207
208 if !is_claude_ai_subscriber() {
209 return Some(
210 "Remote Control requires a claude.ai subscription. Run `claude auth login` to sign in \
211 with your claude.ai account."
212 .to_string(),
213 );
214 }
215
216 if !has_profile_scope() {
217 return Some(
218 "Remote Control requires a full-scope login token. Long-lived tokens (from `claude \
219 setup-token` or AI_CODE_OAUTH_TOKEN) are limited to inference-only for security \
220 reasons. Run `claude auth login` to use Remote Control."
221 .to_string(),
222 );
223 }
224
225 let account = get_oauth_account_info();
226 if account.is_none()
227 || account
228 .as_ref()
229 .and_then(|a| a.organization_uuid.as_ref())
230 .is_none()
231 {
232 return Some(
233 "Unable to determine your organization for Remote Control eligibility. Run \
234 `claude auth login` to refresh your account information."
235 .to_string(),
236 );
237 }
238
239 if !get_gate("tengu_ccr_bridge") {
240 return Some("Remote Control is not yet enabled for your account.".to_string());
241 }
242
243 None
244}
245
246pub fn is_env_less_bridge_enabled() -> bool {
249 if !bridge_mode_enabled() {
250 return false;
251 }
252
253 get_gate("tengu_bridge_repl_v2")
254}
255
256pub fn is_cse_shim_enabled() -> bool {
259 if !bridge_mode_enabled() {
260 return true;
261 }
262
263 get_dynamic_config("tengu_bridge_repl_v2_cse_shim_enabled")
265 .and_then(|v| v.as_bool())
266 .unwrap_or(true)
267}
268
269pub fn check_bridge_min_version(current_version: &str) -> Option<String> {
272 if !bridge_mode_enabled() {
273 return None;
274 }
275
276 let config = get_dynamic_config("tengu_bridge_min_version");
277 let min_version = config
278 .and_then(|c| c.get("minVersion").cloned())
279 .and_then(|v| v.as_str().map(|s| s.to_string()))
280 .unwrap_or_else(|| "0.0.0".to_string());
281
282 if !check_version(current_version, &min_version) {
283 return Some(format!(
284 "Your version of Claude Code ({}) is too old for Remote Control.\nVersion {} or \
285 higher is required. Run `claude update` to update.",
286 current_version, min_version
287 ));
288 }
289
290 None
291}
292
293pub fn get_ccr_auto_connect_default() -> bool {
297 if !ccr_auto_connect_enabled() {
298 return false;
299 }
300
301 get_gate("tengu_cobalt_harbor")
302}
303
304pub fn is_ccr_mirror_enabled() -> bool {
307 if !ccr_mirror_enabled() {
308 return false;
309 }
310
311 is_env_truthy("AI_CODE_CCR_MIRROR") || get_gate("tengu_ccr_mirror")
312}
313
314pub fn register_cse_shim_gate() {
320 use crate::bridge::session_id_compat::set_cse_shim_gate;
321 set_cse_shim_gate(is_cse_shim_enabled);
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_bridge_disabled_without_build_flag() {
330 assert!(!is_bridge_enabled());
332 }
333
334 #[test]
335 fn test_env_less_bridge_default() {
336 assert!(!is_env_less_bridge_enabled());
337 }
338
339 #[test]
340 fn test_cse_shim_default() {
341 assert!(is_cse_shim_enabled());
343 }
344
345 #[test]
346 fn test_check_bridge_min_version() {
347 let result = check_bridge_min_version("1.0.0");
348 assert!(result.is_none());
350 }
351}