Skip to main content

everruns_core/
feature_flags.rs

1// Feature flags system
2//
3// Decision: Feature flags are system-level, computed from env vars + deployment grade.
4// Decision: Flags marked "experimental" auto-enable in dev (DeploymentGrade::Dev).
5// Decision: Explicit env var (FEATURE_<NAME>=true/false) always takes priority.
6// Decision: Struct-based for type safety; `is_enabled(&str)` for dynamic lookup.
7// Decision: Two structs — FeatureFlags (API-visible) and InternalFeatureFlags (backend-only).
8// Decision: Future extensibility: per-org/per-user flags, external providers (LaunchDarkly).
9// Decision: No database storage needed yet — env vars + deployment grade suffice.
10
11use std::collections::BTreeMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::deployment::DeploymentGrade;
16
17/// Feature flags exposed via `GET /v1/feature-flags` and consumed by the frontend.
18///
19/// Currently backed by environment variables and deployment grade.
20/// Future: per-org flags, per-user flags, external providers.
21///
22/// Decision: this is the type-safe representation used throughout the backend.
23/// The API does not serialize it directly — it exposes the untyped
24/// [`FeatureFlagMap`] instead, so adding/removing a flag never changes
25/// `docs/api/openapi.json`.
26#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub struct FeatureFlags {
28    /// Platform Chat: the per-user singleton assistant chat surface — sidebar
29    /// entry, the `/chat` page, and the `POST /v1/sessions/chat` (+ voice)
30    /// endpoints. When off, the whole Platform Chat feature is disabled
31    /// (UI hidden and APIs return 404); other chat surfaces (per-session,
32    /// agent, channel) are unaffected. Experimental.
33    pub global_chat: bool,
34    /// In-app notifications (bell, toasts, notification SSE). Experimental.
35    pub notifications: bool,
36    /// Evals (user-facing behavioral evals for agents). Experimental.
37    pub evals: bool,
38    /// App / channel scoped budgets and periodic budget resets (`5h`, `1d`, ...).
39    /// Experimental.
40    pub app_budgets: bool,
41    /// Immutable agent versions, snapshots, forks, and app version binding.
42    /// Experimental.
43    pub agent_versions: bool,
44    /// Realtime voice endpoints and microphone controls. Experimental.
45    pub voice: bool,
46    /// Outbound agent delegation capabilities (`a2a_agent_delegation`, `agent_handoff`).
47    /// Experimental: auto-enabled in dev, off in prod by default.
48    /// When off, these capabilities are not registered and cannot be assigned to agents.
49    pub agent_delegation: bool,
50    /// Observers (online scoring of production sessions). Experimental.
51    pub observers: bool,
52    /// Public Chat (isolated, public-facing chat web app + `public_chat`
53    /// channel). Experimental. Gates the public endpoints, channel creation,
54    /// the builder UI, and the public web route. See `specs/public-chat.md`.
55    pub public_chat: bool,
56    /// First-party MCP server endpoint (`POST /mcp`) and OAuth-minted MCP tokens.
57    /// Experimental remote-control surface; requires both deployment enablement
58    /// and per-org opt-in.
59    pub mcp_endpoint: bool,
60}
61
62/// Untyped API representation of feature flags: a generic `{ "<flag>": bool }` map.
63///
64/// Decision: the public API is intentionally untyped. The set of flags churns
65/// frequently; encoding each flag as a named schema property would force a
66/// `docs/api/openapi.json` change on every add/remove. A generic string→bool map
67/// keeps the API spec stable. The frontend layers its own typed view on top.
68#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
70#[serde(transparent)]
71pub struct FeatureFlagMap(pub BTreeMap<String, bool>);
72
73impl From<&FeatureFlags> for FeatureFlagMap {
74    fn from(flags: &FeatureFlags) -> Self {
75        flags.to_map()
76    }
77}
78
79impl From<FeatureFlags> for FeatureFlagMap {
80    fn from(flags: FeatureFlags) -> Self {
81        flags.to_map()
82    }
83}
84
85/// Metadata for an API-visible feature flag (org opt-in UI + catalog).
86#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
87pub struct FeatureFlagDefinition {
88    /// Stable flag key (matches `FeatureFlags` field / `is_enabled` name).
89    pub name: &'static str,
90    /// Human-readable title for settings UI.
91    pub label: &'static str,
92    /// Short description of what the flag gates.
93    pub description: &'static str,
94    /// When true, shown with experimental badges in the UI.
95    pub experimental: bool,
96}
97
98/// All API-visible flags that organizations may opt into when the deployment allows them.
99pub const API_FEATURE_FLAG_DEFINITIONS: &[FeatureFlagDefinition] = &[
100    FeatureFlagDefinition {
101        name: "global_chat",
102        label: "Platform Chat",
103        description: "Adds a personal Platform Chat assistant you can open from the sidebar or the \
104             /chat page anywhere in the app. It's a quick scratchpad to talk to your agents \
105             without first setting up a dedicated app or channel. Turning it off disables the \
106             whole feature — the chat page, the sidebar entry, and its session and voice APIs all \
107             become unavailable.",
108        experimental: true,
109    },
110    FeatureFlagDefinition {
111        name: "notifications",
112        label: "Notifications",
113        description: "Turns on the in-app notification bell, toasts, and live updates. You get \
114             alerted in real time when something you care about happens, instead of refreshing \
115             or checking back manually.",
116        experimental: true,
117    },
118    FeatureFlagDefinition {
119        name: "evals",
120        label: "Evals",
121        description: "Lets you define and run behavioral evals against your agents. Use it to \
122             confirm an agent responds the way you expect and to catch regressions as you change \
123             prompts or models.",
124        experimental: true,
125    },
126    FeatureFlagDefinition {
127        name: "app_budgets",
128        label: "App budgets",
129        description: "Adds spending limits scoped to individual apps and channels, with automatic \
130             resets on a schedule. It helps you cap and control costs so a single app or channel \
131             can't run away with your usage.",
132        experimental: true,
133    },
134    FeatureFlagDefinition {
135        name: "agent_versions",
136        label: "Agent versions",
137        description: "Captures immutable snapshots of your agents so you can fork, roll back, and \
138             pin apps to a specific version. This gives you a safety net to experiment freely \
139             and return to a known-good agent at any time.",
140        experimental: true,
141    },
142    FeatureFlagDefinition {
143        name: "voice",
144        label: "Voice",
145        description: "Enables realtime voice in chat with microphone controls. You can talk to \
146             your agents and hear responses instead of typing, for a hands-free conversation.",
147        experimental: true,
148    },
149    FeatureFlagDefinition {
150        name: "observers",
151        label: "Observers",
152        description: "Runs automatic online scoring on your production sessions. It continuously \
153             evaluates live conversations so you can monitor quality on real traffic without \
154             manually reviewing each one.",
155        experimental: true,
156    },
157    FeatureFlagDefinition {
158        name: "public_chat",
159        label: "Public Chat",
160        description: "Isolated, public-facing chat web app and the public_chat channel.",
161        experimental: true,
162    },
163    FeatureFlagDefinition {
164        name: "mcp_endpoint",
165        label: "MCP endpoint",
166        description: "Exposes the first-party MCP server at POST /mcp for authenticated MCP \
167             clients. This remote-control surface can list and invoke MCP tools, including \
168             side-effecting operations within the caller's permissions.",
169        experimental: true,
170    },
171];
172
173impl FeatureFlags {
174    /// Effective flags for an organization: deployment/system gates AND explicit org opt-in.
175    ///
176    /// Org overrides default to disabled; a flag is on only when the deployment allows it
177    /// and the org has opted in (`enabled: true` in storage).
178    pub fn for_org(system: &Self, org_enabled: &std::collections::HashMap<String, bool>) -> Self {
179        let opt_in = |name: &str, system_on: bool| -> bool {
180            system_on && org_enabled.get(name).copied().unwrap_or(false)
181        };
182        Self {
183            global_chat: opt_in("global_chat", system.global_chat),
184            notifications: opt_in("notifications", system.notifications),
185            evals: opt_in("evals", system.evals),
186            app_budgets: opt_in("app_budgets", system.app_budgets),
187            agent_versions: opt_in("agent_versions", system.agent_versions),
188            voice: opt_in("voice", system.voice),
189            agent_delegation: opt_in("agent_delegation", system.agent_delegation),
190            observers: opt_in("observers", system.observers),
191            public_chat: opt_in("public_chat", system.public_chat),
192            mcp_endpoint: opt_in("mcp_endpoint", system.mcp_endpoint),
193        }
194    }
195
196    /// Compute feature flags from environment variables and deployment grade.
197    pub fn from_env(grade: &DeploymentGrade) -> Self {
198        Self {
199            global_chat: experimental_flag("FEATURE_GLOBAL_CHAT", grade),
200            notifications: experimental_flag("FEATURE_NOTIFICATIONS", grade),
201            evals: experimental_flag("FEATURE_EVALS", grade),
202            app_budgets: experimental_flag("FEATURE_APP_BUDGETS", grade),
203            agent_versions: experimental_flag("FEATURE_AGENT_VERSIONS", grade),
204            voice: experimental_flag("FEATURE_VOICE", grade),
205            agent_delegation: experimental_flag("FEATURE_AGENT_DELEGATION", grade),
206            observers: experimental_flag("FEATURE_OBSERVERS", grade),
207            public_chat: experimental_flag("FEATURE_PUBLIC_CHAT", grade),
208            mcp_endpoint: experimental_flag("FEATURE_MCP_ENDPOINT", grade),
209        }
210    }
211
212    /// Resolve the current feature flags from env + the env-derived deployment grade.
213    /// Convenience for callers that don't have a `FeatureFlags` instance handy.
214    pub fn current() -> Self {
215        Self::from_env(&DeploymentGrade::from_env())
216    }
217
218    /// Generic `name -> enabled` map for the untyped API representation.
219    ///
220    /// Keys and values match the JSON wire format of the typed `FeatureFlags`
221    /// response body. The JSON content is equivalent; only the key order differs
222    /// (`BTreeMap` sorts keys, whereas the struct serializes in field order), which
223    /// is irrelevant to JSON consumers.
224    pub fn to_map(&self) -> FeatureFlagMap {
225        FeatureFlagMap(BTreeMap::from([
226            ("global_chat".to_string(), self.global_chat),
227            ("notifications".to_string(), self.notifications),
228            ("evals".to_string(), self.evals),
229            ("app_budgets".to_string(), self.app_budgets),
230            ("agent_versions".to_string(), self.agent_versions),
231            ("voice".to_string(), self.voice),
232            ("agent_delegation".to_string(), self.agent_delegation),
233            ("observers".to_string(), self.observers),
234            ("public_chat".to_string(), self.public_chat),
235            ("mcp_endpoint".to_string(), self.mcp_endpoint),
236        ]))
237    }
238
239    /// Look up a flag by name (for dynamic/string-based access).
240    pub fn is_enabled(&self, flag: &str) -> bool {
241        match flag {
242            "global_chat" => self.global_chat,
243            "notifications" => self.notifications,
244            "evals" => self.evals,
245            "app_budgets" => self.app_budgets,
246            "agent_versions" => self.agent_versions,
247            "voice" => self.voice,
248            "agent_delegation" => self.agent_delegation,
249            "observers" => self.observers,
250            "public_chat" => self.public_chat,
251            "mcp_endpoint" => self.mcp_endpoint,
252            _ => false,
253        }
254    }
255
256    /// All flags enabled (for testing).
257    #[cfg(test)]
258    pub fn all_enabled() -> Self {
259        Self {
260            global_chat: true,
261            notifications: true,
262            evals: true,
263            app_budgets: true,
264            agent_versions: true,
265            voice: true,
266            agent_delegation: true,
267            observers: true,
268            public_chat: true,
269            mcp_endpoint: true,
270        }
271    }
272}
273
274/// Backend-only feature flags. Not exposed via API or frontend.
275///
276/// Used for internal gating (capability registration, infrastructure behavior).
277#[derive(Debug, Default, Clone, PartialEq, Eq)]
278pub struct InternalFeatureFlags {
279    /// Docker container capability. Disabled by default on all envs.
280    /// Enable via `FEATURE_DOCKER_CAPABILITY=true`.
281    pub docker_capability: bool,
282    /// Self-hosted container sandbox capability and coding harness.
283    /// Disabled by default on all envs.
284    /// Enable via `FEATURE_CONTAINER_SANDBOX=true`, or via the legacy
285    /// fallback `FEATURE_DOCKER_CAPABILITY=true` when
286    /// `FEATURE_CONTAINER_SANDBOX` is unset.
287    pub container_sandbox: bool,
288    /// Managed session-owned sandbox capability and lifecycle orchestration.
289    /// Experimental and disabled by default.
290    pub session_sandbox: bool,
291    /// Machine-payment capabilities (e.g. the Parallel paid search/extract/task
292    /// capability). Gates registration of any capability that spends real money
293    /// through `PaymentAuthority`. Disabled by default on all envs, including dev,
294    /// because spend is irreversible. Enable via `FEATURE_MACHINE_PAYMENTS=true`.
295    pub machine_payments: bool,
296    /// Experimental sandboxed Lua execution capability (`specs/lua-execution.md`).
297    /// Disabled by default; requires the `lua` cargo feature to be compiled in to
298    /// actually run scripts. Enable via `FEATURE_LUA=true`.
299    pub lua: bool,
300}
301
302impl InternalFeatureFlags {
303    /// Compute internal feature flags from environment variables.
304    pub fn from_env() -> Self {
305        let docker_capability = standard_flag("FEATURE_DOCKER_CAPABILITY", false);
306
307        Self {
308            docker_capability,
309            container_sandbox: standard_flag("FEATURE_CONTAINER_SANDBOX", docker_capability),
310            session_sandbox: standard_flag("FEATURE_SESSION_SANDBOX", false),
311            machine_payments: standard_flag("FEATURE_MACHINE_PAYMENTS", false),
312            lua: standard_flag("FEATURE_LUA", false),
313        }
314    }
315
316    /// Look up a flag by name (for dynamic/string-based access).
317    pub fn is_enabled(&self, flag: &str) -> bool {
318        match flag {
319            "docker_capability" => self.docker_capability,
320            "container_sandbox" => self.container_sandbox,
321            "session_sandbox" => self.session_sandbox,
322            "machine_payments" => self.machine_payments,
323            "lua" => self.lua,
324            _ => false,
325        }
326    }
327}
328
329/// Resolve an experimental flag.
330///
331/// Priority: explicit env var > experimental default (enabled in dev) > false.
332fn experimental_flag(env_var: &str, grade: &DeploymentGrade) -> bool {
333    if let Ok(val) = std::env::var(env_var) {
334        return val == "true" || val == "1";
335    }
336    grade.experimental_features_enabled()
337}
338
339/// Resolve a standard (non-experimental) flag.
340///
341/// Priority: explicit env var > default.
342fn standard_flag(env_var: &str, default: bool) -> bool {
343    std::env::var(env_var)
344        .map(|v| v == "true" || v == "1")
345        .unwrap_or(default)
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    // Env-var-mutating tests must not run in parallel.
353    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
354
355    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
356        ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
357    }
358
359    /// Restore an env var to a previously captured value, or remove it if it was unset.
360    fn restore_env(key: &str, prev: Option<String>) {
361        match prev {
362            Some(value) => unsafe { std::env::set_var(key, value) },
363            None => unsafe { std::env::remove_var(key) },
364        }
365    }
366
367    #[test]
368    fn test_default_flags() {
369        let flags = FeatureFlags::default();
370        assert!(!flags.global_chat);
371        assert!(!flags.notifications);
372    }
373
374    // SAFETY: env var tests must run single-threaded (--test-threads=1).
375    // set_var/remove_var are unsafe in edition 2024 due to thread-safety.
376
377    #[test]
378    fn test_experimental_enabled_in_dev() {
379        let _lock = lock_env();
380        unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
381        unsafe { std::env::remove_var("FEATURE_EVALS") };
382        let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
383        assert!(flags.global_chat);
384        assert!(flags.evals);
385    }
386
387    #[test]
388    fn test_experimental_disabled_in_prod() {
389        let _lock = lock_env();
390        unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
391        unsafe { std::env::remove_var("FEATURE_EVALS") };
392        let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
393        assert!(!flags.global_chat);
394        assert!(!flags.evals);
395    }
396
397    #[test]
398    fn test_env_override_enables_in_prod() {
399        let _lock = lock_env();
400        unsafe { std::env::set_var("FEATURE_GLOBAL_CHAT", "true") };
401        let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
402        assert!(flags.global_chat);
403        unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
404    }
405
406    #[test]
407    fn test_env_override_disables_in_dev() {
408        let _lock = lock_env();
409        unsafe { std::env::set_var("FEATURE_GLOBAL_CHAT", "false") };
410        let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
411        assert!(!flags.global_chat);
412        unsafe { std::env::remove_var("FEATURE_GLOBAL_CHAT") };
413    }
414
415    #[test]
416    fn test_is_enabled_dynamic() {
417        let flags = FeatureFlags {
418            global_chat: true,
419            notifications: true,
420            evals: true,
421            app_budgets: true,
422            agent_versions: true,
423            voice: true,
424            agent_delegation: true,
425            observers: true,
426            public_chat: true,
427            mcp_endpoint: true,
428        };
429        assert!(flags.is_enabled("global_chat"));
430        assert!(flags.is_enabled("notifications"));
431        assert!(flags.is_enabled("evals"));
432        assert!(flags.is_enabled("app_budgets"));
433        assert!(flags.is_enabled("agent_versions"));
434        assert!(flags.is_enabled("voice"));
435        assert!(flags.is_enabled("agent_delegation"));
436        assert!(flags.is_enabled("observers"));
437        assert!(flags.is_enabled("public_chat"));
438        assert!(flags.is_enabled("mcp_endpoint"));
439        assert!(!flags.is_enabled("nonexistent"));
440    }
441
442    #[test]
443    fn test_serialization() {
444        let flags = FeatureFlags {
445            global_chat: true,
446            notifications: true,
447            evals: true,
448            app_budgets: true,
449            agent_versions: true,
450            voice: true,
451            agent_delegation: true,
452            observers: true,
453            public_chat: true,
454            mcp_endpoint: true,
455        };
456        let json = serde_json::to_string(&flags).unwrap();
457        assert!(json.contains("\"global_chat\":true"));
458        assert!(json.contains("\"notifications\":true"));
459        assert!(json.contains("\"app_budgets\":true"));
460        assert!(json.contains("\"agent_versions\":true"));
461        assert!(json.contains("\"voice\":true"));
462        assert!(json.contains("\"agent_delegation\":true"));
463        assert!(json.contains("\"observers\":true"));
464
465        let parsed: FeatureFlags = serde_json::from_str(&json).unwrap();
466        assert_eq!(flags, parsed);
467    }
468
469    #[test]
470    fn test_to_map_matches_serialized_flags() {
471        // `to_map` must produce the same keys/values as the struct's JSON form
472        // (compared as `serde_json::Value`, so key order is ignored). If a field is
473        // added to `FeatureFlags` but not to `to_map`, this fails — keeping the
474        // untyped API representation in sync with the struct.
475        let flags = FeatureFlags::all_enabled();
476        let typed: serde_json::Value = serde_json::to_value(&flags).unwrap();
477        let map: serde_json::Value = serde_json::to_value(flags.to_map()).unwrap();
478        assert_eq!(typed, map);
479
480        let default_typed: serde_json::Value =
481            serde_json::to_value(FeatureFlags::default()).unwrap();
482        let default_map: serde_json::Value =
483            serde_json::to_value(FeatureFlags::default().to_map()).unwrap();
484        assert_eq!(default_typed, default_map);
485    }
486
487    #[test]
488    fn test_agent_delegation_enabled_in_dev() {
489        let _lock = lock_env();
490        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
491        let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
492        assert!(flags.agent_delegation);
493    }
494
495    #[test]
496    fn test_agent_delegation_disabled_in_prod() {
497        let _lock = lock_env();
498        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
499        let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
500        assert!(!flags.agent_delegation);
501    }
502
503    #[test]
504    fn test_agent_delegation_env_override_in_prod() {
505        let _lock = lock_env();
506        unsafe { std::env::set_var("FEATURE_AGENT_DELEGATION", "true") };
507        let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
508        assert!(flags.agent_delegation);
509        unsafe { std::env::remove_var("FEATURE_AGENT_DELEGATION") };
510    }
511
512    #[test]
513    fn test_standard_flag() {
514        let _lock = lock_env();
515        unsafe { std::env::remove_var("FEATURE_TEST_STD") };
516        assert!(!standard_flag("FEATURE_TEST_STD", false));
517        assert!(standard_flag("FEATURE_TEST_STD", true));
518
519        unsafe { std::env::set_var("FEATURE_TEST_STD", "1") };
520        assert!(standard_flag("FEATURE_TEST_STD", false));
521        unsafe { std::env::remove_var("FEATURE_TEST_STD") };
522    }
523
524    #[test]
525    fn test_notifications_enabled_in_dev() {
526        let _lock = lock_env();
527        unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
528        let flags = FeatureFlags::from_env(&DeploymentGrade::Dev);
529        assert!(flags.notifications);
530    }
531
532    #[test]
533    fn test_notifications_disabled_in_prod() {
534        let _lock = lock_env();
535        unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
536        let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
537        assert!(!flags.notifications);
538    }
539
540    #[test]
541    fn test_for_org_requires_system_and_opt_in() {
542        let system = FeatureFlags {
543            global_chat: true,
544            evals: true,
545            ..FeatureFlags::default()
546        };
547        let mut org = std::collections::HashMap::new();
548        org.insert("global_chat".to_string(), true);
549        let effective = FeatureFlags::for_org(&system, &org);
550        assert!(effective.global_chat);
551        assert!(!effective.evals);
552
553        let effective_none = FeatureFlags::for_org(&system, &std::collections::HashMap::new());
554        assert!(!effective_none.global_chat);
555    }
556
557    #[test]
558    fn test_for_org_cannot_enable_when_system_off() {
559        let system = FeatureFlags::default();
560        let mut org = std::collections::HashMap::new();
561        org.insert("global_chat".to_string(), true);
562        let effective = FeatureFlags::for_org(&system, &org);
563        assert!(!effective.global_chat);
564    }
565
566    #[test]
567    fn test_notifications_respects_env_override() {
568        let _lock = lock_env();
569        unsafe { std::env::set_var("FEATURE_NOTIFICATIONS", "true") };
570        let flags = FeatureFlags::from_env(&DeploymentGrade::Prod);
571        assert!(flags.notifications);
572        unsafe { std::env::remove_var("FEATURE_NOTIFICATIONS") };
573    }
574
575    // =========================================================================
576    // InternalFeatureFlags tests
577    // =========================================================================
578
579    #[test]
580    fn test_internal_default_flags() {
581        let flags = InternalFeatureFlags::default();
582        assert!(!flags.docker_capability);
583        assert!(!flags.container_sandbox);
584        assert!(!flags.session_sandbox);
585        assert!(!flags.machine_payments);
586    }
587
588    #[test]
589    fn test_docker_capability_flag_disabled_by_default_in_dev() {
590        let _lock = lock_env();
591        unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
592        let flags = InternalFeatureFlags::from_env();
593        assert!(
594            !flags.docker_capability,
595            "docker_capability should be disabled by default even in dev"
596        );
597    }
598
599    #[test]
600    fn test_docker_capability_flag_enabled_by_env_override() {
601        let _lock = lock_env();
602        unsafe { std::env::set_var("FEATURE_DOCKER_CAPABILITY", "true") };
603        let flags = InternalFeatureFlags::from_env();
604        assert!(flags.docker_capability);
605        unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
606    }
607
608    #[test]
609    fn test_container_sandbox_flag_enabled_by_env_override() {
610        let _lock = lock_env();
611        unsafe { std::env::set_var("FEATURE_CONTAINER_SANDBOX", "true") };
612        unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
613        let flags = InternalFeatureFlags::from_env();
614        assert!(flags.container_sandbox);
615        unsafe { std::env::remove_var("FEATURE_CONTAINER_SANDBOX") };
616    }
617
618    #[test]
619    fn test_container_sandbox_flag_falls_back_to_legacy_docker_flag() {
620        let _lock = lock_env();
621        unsafe { std::env::remove_var("FEATURE_CONTAINER_SANDBOX") };
622        unsafe { std::env::set_var("FEATURE_DOCKER_CAPABILITY", "true") };
623        let flags = InternalFeatureFlags::from_env();
624        assert!(flags.container_sandbox);
625        unsafe { std::env::remove_var("FEATURE_DOCKER_CAPABILITY") };
626    }
627
628    #[test]
629    fn test_internal_is_enabled_dynamic() {
630        let flags = InternalFeatureFlags {
631            docker_capability: true,
632            container_sandbox: true,
633            session_sandbox: true,
634            machine_payments: true,
635            lua: true,
636        };
637        assert!(flags.is_enabled("docker_capability"));
638        assert!(flags.is_enabled("container_sandbox"));
639        assert!(flags.is_enabled("session_sandbox"));
640        assert!(flags.is_enabled("machine_payments"));
641        assert!(flags.is_enabled("lua"));
642        assert!(!flags.is_enabled("nonexistent"));
643    }
644
645    #[test]
646    fn test_machine_payments_disabled_by_default() {
647        let _lock = lock_env();
648        let prev = std::env::var("FEATURE_MACHINE_PAYMENTS").ok();
649        unsafe { std::env::remove_var("FEATURE_MACHINE_PAYMENTS") };
650        let flags = InternalFeatureFlags::from_env();
651        assert!(
652            !flags.machine_payments,
653            "machine_payments should be disabled by default on all envs"
654        );
655        restore_env("FEATURE_MACHINE_PAYMENTS", prev);
656    }
657
658    #[test]
659    fn test_machine_payments_enabled_by_env_override() {
660        let _lock = lock_env();
661        let prev = std::env::var("FEATURE_MACHINE_PAYMENTS").ok();
662        unsafe { std::env::set_var("FEATURE_MACHINE_PAYMENTS", "true") };
663        let flags = InternalFeatureFlags::from_env();
664        assert!(flags.machine_payments);
665        restore_env("FEATURE_MACHINE_PAYMENTS", prev);
666    }
667
668    #[test]
669    fn test_session_sandbox_flag_enabled_by_env_override() {
670        let _lock = lock_env();
671        unsafe { std::env::set_var("FEATURE_SESSION_SANDBOX", "true") };
672        let flags = InternalFeatureFlags::from_env();
673        assert!(flags.session_sandbox);
674        unsafe { std::env::remove_var("FEATURE_SESSION_SANDBOX") };
675    }
676}