everruns-core 0.8.34

Core agent abstractions for Everruns - agent loop, events, tools, LLM providers
Documentation
// Feature flags system
//
// Decision: Feature flags are system-level, computed from env vars + deployment grade.
// Decision: Flags marked "experimental" auto-enable in dev (DeploymentGrade::Dev).
// Decision: Explicit env var (FEATURE_<NAME>=true/false) always takes priority.
// Decision: Struct-based for type safety; `is_enabled(&str)` for dynamic lookup.
// Decision: Two structs — FeatureFlags (API-visible) and InternalFeatureFlags (backend-only).
// Decision: Future extensibility: per-org/per-user flags, external providers (LaunchDarkly).
// Decision: No database storage needed yet — env vars + deployment grade suffice.

use serde::{Deserialize, Serialize};

use crate::deployment::DeploymentGrade;

/// Feature flags exposed via `GET /v1/feature-flags` and consumed by the frontend.
///
/// Currently backed by environment variables and deployment grade.
/// Future: per-org flags, per-user flags, external providers.
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FeatureFlags {
    /// Global chat (per-user singleton chat session). Experimental.
    pub global_chat: bool,
    /// In-app notifications (bell, toasts, notification SSE). Experimental.
    pub notifications: bool,
    /// MCP endpoint (POST /mcp — Everruns as an MCP server). Experimental.
    pub mcp_endpoint: bool,
    /// Evals (user-facing behavioral evals for agents). Experimental.
    pub evals: bool,
    /// App / channel scoped budgets and periodic budget resets (`5h`, `1d`, ...).
    /// Experimental.
    pub app_budgets: bool,
    /// Immutable agent versions, snapshots, forks, and app version binding.
    /// Experimental.
    pub agent_versions: bool,
    /// Realtime voice endpoints and microphone controls. Experimental.
    pub voice: bool,
    /// Channels-first app detail page and full-page channel forms. Experimental.
    #[serde(rename = "apps.detailV2")]
    pub apps_detail_v2: bool,
}

impl FeatureFlags {
    /// Compute feature flags from environment variables and deployment grade.
    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),
        }
    }

    /// Resolve the current feature flags from env + the env-derived deployment grade.
    /// Convenience for callers that don't have a `FeatureFlags` instance handy.
    pub fn current() -> Self {
        Self::from_env(&DeploymentGrade::from_env())
    }

    /// Look up a flag by name (for dynamic/string-based access).
    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,
        }
    }

    /// All flags enabled (for testing).
    #[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,
        }
    }
}

/// Backend-only feature flags. Not exposed via API or frontend.
///
/// Used for internal gating (capability registration, infrastructure behavior).
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct InternalFeatureFlags {
    /// Docker container capability. Disabled by default on all envs.
    /// Enable via `FEATURE_DOCKER_CAPABILITY=true`.
    pub docker_capability: bool,
    /// Self-hosted container sandbox capability and coding harness.
    /// Disabled by default on all envs.
    /// Enable via `FEATURE_CONTAINER_SANDBOX=true`, or via the legacy
    /// fallback `FEATURE_DOCKER_CAPABILITY=true` when
    /// `FEATURE_CONTAINER_SANDBOX` is unset.
    pub container_sandbox: bool,
    /// Managed session-owned sandbox capability and lifecycle orchestration.
    /// Experimental and disabled by default.
    pub session_sandbox: bool,
}

impl InternalFeatureFlags {
    /// Compute internal feature flags from environment variables.
    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),
        }
    }

    /// Look up a flag by name (for dynamic/string-based access).
    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,
        }
    }
}

/// Resolve an experimental flag.
///
/// Priority: explicit env var > experimental default (enabled in dev) > 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()
}

/// Resolve a standard (non-experimental) flag.
///
/// Priority: explicit env var > default.
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::*;

    // Env-var-mutating tests must not run in parallel.
    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);
    }

    // SAFETY: env var tests must run single-threaded (--test-threads=1).
    // set_var/remove_var are unsafe in edition 2024 due to thread-safety.

    #[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") };
    }

    // =========================================================================
    // InternalFeatureFlags tests
    // =========================================================================

    #[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") };
    }
}