use serde::{Deserialize, Serialize};
use crate::deployment::DeploymentGrade;
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FeatureFlags {
pub global_chat: bool,
pub notifications: bool,
pub mcp_endpoint: bool,
pub evals: bool,
pub app_budgets: bool,
pub agent_versions: bool,
pub voice: bool,
#[serde(rename = "apps.detailV2")]
pub apps_detail_v2: bool,
}
impl FeatureFlags {
pub fn from_env(grade: &DeploymentGrade) -> Self {
Self {
global_chat: experimental_flag("FEATURE_GLOBAL_CHAT", grade),
notifications: experimental_flag("FEATURE_NOTIFICATIONS", grade),
mcp_endpoint: experimental_flag("FEATURE_MCP_ENDPOINT", grade),
evals: experimental_flag("FEATURE_EVALS", grade),
app_budgets: experimental_flag("FEATURE_APP_BUDGETS", grade),
agent_versions: experimental_flag("FEATURE_AGENT_VERSIONS", grade),
voice: experimental_flag("FEATURE_VOICE", grade),
apps_detail_v2: experimental_flag("FEATURE_APPS_DETAIL_V2", grade),
}
}
pub fn current() -> Self {
Self::from_env(&DeploymentGrade::from_env())
}
pub fn is_enabled(&self, flag: &str) -> bool {
match flag {
"global_chat" => self.global_chat,
"notifications" => self.notifications,
"mcp_endpoint" => self.mcp_endpoint,
"evals" => self.evals,
"app_budgets" => self.app_budgets,
"agent_versions" => self.agent_versions,
"voice" => self.voice,
"apps.detailV2" => self.apps_detail_v2,
_ => false,
}
}
#[cfg(test)]
pub fn all_enabled() -> Self {
Self {
global_chat: true,
notifications: true,
mcp_endpoint: true,
evals: true,
app_budgets: true,
agent_versions: true,
voice: true,
apps_detail_v2: true,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct InternalFeatureFlags {
pub docker_capability: bool,
pub container_sandbox: bool,
pub session_sandbox: bool,
}
impl InternalFeatureFlags {
pub fn from_env() -> Self {
let docker_capability = standard_flag("FEATURE_DOCKER_CAPABILITY", false);
Self {
docker_capability,
container_sandbox: standard_flag("FEATURE_CONTAINER_SANDBOX", docker_capability),
session_sandbox: standard_flag("FEATURE_SESSION_SANDBOX", false),
}
}
pub fn is_enabled(&self, flag: &str) -> bool {
match flag {
"docker_capability" => self.docker_capability,
"container_sandbox" => self.container_sandbox,
"session_sandbox" => self.session_sandbox,
_ => false,
}
}
}
fn experimental_flag(env_var: &str, grade: &DeploymentGrade) -> bool {
if let Ok(val) = std::env::var(env_var) {
return val == "true" || val == "1";
}
grade.experimental_features_enabled()
}
fn standard_flag(env_var: &str, default: bool) -> bool {
std::env::var(env_var)
.map(|v| v == "true" || v == "1")
.unwrap_or(default)
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn test_default_flags() {
let flags = FeatureFlags::default();
assert!(!flags.global_chat);
assert!(!flags.notifications);
}
#[test]
fn test_experimental_enabled_in_dev() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
unsafe { std::env::remove_var("FEATURE_EVALS") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
assert!(flags.global_chat);
assert!(flags.evals);
}
#[test]
fn test_experimental_disabled_in_prod() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
unsafe { std::env::remove_var("FEATURE_EVALS") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
assert!(!flags.global_chat);
assert!(!flags.evals);
}
#[test]
fn test_env_override_enables_in_prod() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_GLOBAL_CHAT", "true") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
assert!(flags.global_chat);
unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
}
#[test]
fn test_env_override_disables_in_dev() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_GLOBAL_CHAT", "false") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
assert!(!flags.global_chat);
unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
}
#[test]
fn test_is_enabled_dynamic() {
let flags = FeatureFlags {
global_chat: true,
notifications: true,
mcp_endpoint: true,
evals: true,
app_budgets: true,
agent_versions: true,
voice: true,
apps_detail_v2: true,
};
assert!(flags.is_enabled("global_chat"));
assert!(flags.is_enabled("notifications"));
assert!(flags.is_enabled("mcp_endpoint"));
assert!(flags.is_enabled("evals"));
assert!(flags.is_enabled("app_budgets"));
assert!(flags.is_enabled("agent_versions"));
assert!(flags.is_enabled("voice"));
assert!(flags.is_enabled("apps.detailV2"));
assert!(!flags.is_enabled("nonexistent"));
}
#[test]
fn test_serialization() {
let flags = FeatureFlags {
global_chat: true,
notifications: true,
mcp_endpoint: true,
evals: true,
app_budgets: true,
agent_versions: true,
voice: true,
apps_detail_v2: true,
};
let json = serde_json::to_string(&flags).unwrap();
assert!(json.contains("\"global_chat\":true"));
assert!(json.contains("\"notifications\":true"));
assert!(json.contains("\"app_budgets\":true"));
assert!(json.contains("\"agent_versions\":true"));
assert!(json.contains("\"voice\":true"));
assert!(json.contains("\"apps.detailV2\":true"));
let parsed: FeatureFlags = serde_json::from_str(&json).unwrap();
assert_eq!(flags, parsed);
}
#[test]
fn test_standard_flag() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_TEST_STD") };
assert!(!standard_flag("FEATURE_TEST_STD", false));
assert!(standard_flag("FEATURE_TEST_STD", true));
unsafe { std::env::set_var("FEATURE_TEST_STD", "1") };
assert!(standard_flag("FEATURE_TEST_STD", false));
unsafe { std::env::remove_var("FEATURE_TEST_STD") };
}
#[test]
fn test_notifications_enabled_in_dev() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
assert!(flags.notifications);
}
#[test]
fn test_notifications_disabled_in_prod() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
assert!(!flags.notifications);
}
#[test]
fn test_notifications_respects_env_override() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_NOTIFICATIONS", "true") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
assert!(flags.notifications);
unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
}
#[test]
fn test_internal_default_flags() {
let flags = InternalFeatureFlags::default();
assert!(!flags.docker_capability);
assert!(!flags.container_sandbox);
assert!(!flags.session_sandbox);
}
#[test]
fn test_docker_capability_flag_disabled_by_default_in_dev() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
let flags = InternalFeatureFlags::from_env();
assert!(
!flags.docker_capability,
"docker_capability should be disabled by default even in dev"
);
}
#[test]
fn test_docker_capability_flag_enabled_by_env_override() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_DOCKER_CAPABILITY", "true") };
let flags = InternalFeatureFlags::from_env();
assert!(flags.docker_capability);
unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
}
#[test]
fn test_container_sandbox_flag_enabled_by_env_override() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_CONTAINER_SANDBOX", "true") };
unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
let flags = InternalFeatureFlags::from_env();
assert!(flags.container_sandbox);
unsafe { std::env::remove_var("FEATURE_CONTAINER_SANDBOX") };
}
#[test]
fn test_container_sandbox_flag_falls_back_to_legacy_docker_flag() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_CONTAINER_SANDBOX") };
unsafe { std::env::set_var("FEATURE_DOCKER_CAPABILITY", "true") };
let flags = InternalFeatureFlags::from_env();
assert!(flags.container_sandbox);
unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
}
#[test]
fn test_internal_is_enabled_dynamic() {
let flags = InternalFeatureFlags {
docker_capability: true,
container_sandbox: true,
session_sandbox: true,
};
assert!(flags.is_enabled("docker_capability"));
assert!(flags.is_enabled("container_sandbox"));
assert!(flags.is_enabled("session_sandbox"));
assert!(!flags.is_enabled("nonexistent"));
}
#[test]
fn test_session_sandbox_flag_enabled_by_env_override() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_SESSION_SANDBOX", "true") };
let flags = InternalFeatureFlags::from_env();
assert!(flags.session_sandbox);
unsafe { std::env::remove_var("FEATURE_SESSION_SANDBOX") };
}
}