use serde::{Deserialize, Serialize};
use crate::deployment::DeploymentGrade;
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
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,
pub agent_delegation: bool,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct FeatureFlagDefinition {
pub name: &'static str,
pub label: &'static str,
pub description: &'static str,
pub experimental: bool,
}
pub const API_FEATURE_FLAG_DEFINITIONS: &[FeatureFlagDefinition] = &[
FeatureFlagDefinition {
name: "global_chat",
label: "Global chat",
description: "Per-user singleton chat session in the sidebar.",
experimental: true,
},
FeatureFlagDefinition {
name: "notifications",
label: "Notifications",
description: "In-app notification bell, toasts, and notification SSE.",
experimental: true,
},
FeatureFlagDefinition {
name: "mcp_endpoint",
label: "MCP server endpoint",
description: "Expose Everruns as an MCP server (POST /mcp).",
experimental: true,
},
FeatureFlagDefinition {
name: "evals",
label: "Evals",
description: "Behavioral evals for agents.",
experimental: true,
},
FeatureFlagDefinition {
name: "app_budgets",
label: "App budgets",
description: "App and channel scoped budgets with periodic resets.",
experimental: true,
},
FeatureFlagDefinition {
name: "agent_versions",
label: "Agent versions",
description: "Immutable agent snapshots, forks, rollback, and app version binding.",
experimental: true,
},
FeatureFlagDefinition {
name: "voice",
label: "Voice",
description: "Realtime voice endpoints and microphone controls in chat.",
experimental: true,
},
FeatureFlagDefinition {
name: "apps.detailV2",
label: "Apps detail v2",
description: "Channels-first app detail page and full-page channel forms.",
experimental: true,
},
];
impl FeatureFlags {
pub fn for_org(system: &Self, org_enabled: &std::collections::HashMap<String, bool>) -> Self {
let opt_in = |name: &str, system_on: bool| -> bool {
system_on && org_enabled.get(name).copied().unwrap_or(false)
};
Self {
global_chat: opt_in("global_chat", system.global_chat),
notifications: opt_in("notifications", system.notifications),
mcp_endpoint: opt_in("mcp_endpoint", system.mcp_endpoint),
evals: opt_in("evals", system.evals),
app_budgets: opt_in("app_budgets", system.app_budgets),
agent_versions: opt_in("agent_versions", system.agent_versions),
voice: opt_in("voice", system.voice),
apps_detail_v2: opt_in("apps.detailV2", system.apps_detail_v2),
agent_delegation: opt_in("agent_delegation", system.agent_delegation),
}
}
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),
agent_delegation: experimental_flag("FEATURE_AGENT_DELEGATION", 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,
"agent_delegation" => self.agent_delegation,
_ => 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,
agent_delegation: true,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct InternalFeatureFlags {
pub docker_capability: bool,
pub container_sandbox: bool,
pub session_sandbox: bool,
pub machine_payments: 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),
machine_payments: standard_flag("FEATURE_MACHINE_PAYMENTS", 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,
"machine_payments" => self.machine_payments,
_ => 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())
}
fn restore_env(key: &str, prev: Option<String>) {
match prev {
Some(value) => unsafe { std::env::set_var(key, value) },
None => unsafe { std::env::remove_var(key) },
}
}
#[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,
agent_delegation: 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("agent_delegation"));
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,
agent_delegation: 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"));
assert!(json.contains("\"agent_delegation\":true"));
let parsed: FeatureFlags = serde_json::from_str(&json).unwrap();
assert_eq!(flags, parsed);
}
#[test]
fn test_agent_delegation_enabled_in_dev() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
assert!(flags.agent_delegation);
}
#[test]
fn test_agent_delegation_disabled_in_prod() {
let _lock = lock_env();
unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
assert!(!flags.agent_delegation);
}
#[test]
fn test_agent_delegation_env_override_in_prod() {
let _lock = lock_env();
unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
assert!(flags.agent_delegation);
unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
}
#[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_for_org_requires_system_and_opt_in() {
let system = FeatureFlags {
global_chat: true,
evals: true,
..FeatureFlags::default()
};
let mut org = std::collections::HashMap::new();
org.insert("global_chat".to_string(), true);
let effective = FeatureFlags::for_org(&system, &org);
assert!(effective.global_chat);
assert!(!effective.evals);
let effective_none = FeatureFlags::for_org(&system, &std::collections::HashMap::new());
assert!(!effective_none.global_chat);
}
#[test]
fn test_for_org_cannot_enable_when_system_off() {
let system = FeatureFlags::default();
let mut org = std::collections::HashMap::new();
org.insert("global_chat".to_string(), true);
let effective = FeatureFlags::for_org(&system, &org);
assert!(!effective.global_chat);
}
#[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);
assert!(!flags.machine_payments);
}
#[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,
machine_payments: true,
};
assert!(flags.is_enabled("docker_capability"));
assert!(flags.is_enabled("container_sandbox"));
assert!(flags.is_enabled("session_sandbox"));
assert!(flags.is_enabled("machine_payments"));
assert!(!flags.is_enabled("nonexistent"));
}
#[test]
fn test_machine_payments_disabled_by_default() {
let _lock = lock_env();
let prev = std::env::var("FEATURE_MACHINE_PAYMENTS").ok();
unsafe { std::env::remove_var("FEATURE_MACHINE_PAYMENTS") };
let flags = InternalFeatureFlags::from_env();
assert!(
!flags.machine_payments,
"machine_payments should be disabled by default on all envs"
);
restore_env("FEATURE_MACHINE_PAYMENTS", prev);
}
#[test]
fn test_machine_payments_enabled_by_env_override() {
let _lock = lock_env();
let prev = std::env::var("FEATURE_MACHINE_PAYMENTS").ok();
unsafe { std::env::set_var("FEATURE_MACHINE_PAYMENTS", "true") };
let flags = InternalFeatureFlags::from_env();
assert!(flags.machine_payments);
restore_env("FEATURE_MACHINE_PAYMENTS", prev);
}
#[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") };
}
}