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