Skip to main content

everruns_core/
execution_features.rs

1// Execution feature decisions (EVE-878).
2//
3// Decision: the org/product feature-flag records and management logic
4// (`FeatureFlags`, `FeatureFlagMap`, `FeatureFlagDefinition`,
5// `API_FEATURE_FLAG_DEFINITIONS`, org opt-in resolution) moved to the
6// `everruns-platform` crate — they are hosted control-plane state resolved by
7// the server before execution. Core retains only the narrowly required
8// execution feature decisions consumed at capability-registration time:
9// - `InternalFeatureFlags`: backend-only infrastructure gates computed from
10//   env vars, never org-configurable and never exposed via API.
11// - `ExecutionFeatureDecisions`: the resolved deployment-level snapshot the
12//   registry builders consult; per-org effective decisions are applied at the
13//   server loading seam (capability filtering before the worker snapshot), so
14//   execution never loads feature-management records.
15// Decision: Explicit env var (FEATURE_<NAME>=true/false) always takes priority.
16// Decision: Flags marked "experimental" auto-enable in dev (DeploymentGrade::Dev).
17
18use crate::deployment::DeploymentGrade;
19
20/// Backend-only feature flags. Not exposed via API or frontend.
21///
22/// Used for internal gating (capability registration, infrastructure behavior).
23#[derive(Debug, Default, Clone, PartialEq, Eq)]
24pub struct InternalFeatureFlags {
25    /// Docker container capability. Disabled by default on all envs.
26    /// Enable via `FEATURE_DOCKER_CAPABILITY=true`.
27    pub docker_capability: bool,
28    /// Self-hosted container sandbox capability and coding harness.
29    /// Disabled by default on all envs.
30    /// Enable via `FEATURE_CONTAINER_SANDBOX=true`, or via the legacy
31    /// fallback `FEATURE_DOCKER_CAPABILITY=true` when
32    /// `FEATURE_CONTAINER_SANDBOX` is unset.
33    pub container_sandbox: bool,
34    /// Managed session-owned sandbox capability and lifecycle orchestration.
35    /// Experimental and disabled by default.
36    pub session_sandbox: bool,
37    /// Experimental sandboxed Lua execution capability (`knowledge/execution/lua-execution.md`).
38    /// Disabled by default; requires the `lua` cargo feature to be compiled in to
39    /// actually run scripts. Enable via `FEATURE_LUA=true`.
40    pub lua: bool,
41}
42
43impl InternalFeatureFlags {
44    /// Compute internal feature flags from environment variables.
45    pub fn from_env() -> Self {
46        let docker_capability = standard_flag("FEATURE_DOCKER_CAPABILITY", false);
47
48        Self {
49            docker_capability,
50            container_sandbox: standard_flag("FEATURE_CONTAINER_SANDBOX", docker_capability),
51            session_sandbox: standard_flag("FEATURE_SESSION_SANDBOX", false),
52            lua: standard_flag("FEATURE_LUA", false),
53        }
54    }
55
56    /// Look up a flag by name (for dynamic/string-based access).
57    pub fn is_enabled(&self, flag: &str) -> bool {
58        match flag {
59            "docker_capability" => self.docker_capability,
60            "container_sandbox" => self.container_sandbox,
61            "session_sandbox" => self.session_sandbox,
62            "lua" => self.lua,
63            _ => false,
64        }
65    }
66}
67
68/// Resolved deployment-level execution feature decisions (EVE-878).
69///
70/// This is the snapshot the capability registry builders consult when
71/// composing built-ins: internal infrastructure gates plus the few
72/// experimental product gates that decide whether a capability is registered
73/// at all. It is computed once from env vars and the deployment grade — it
74/// never reads org feature-management records. Per-org effective decisions
75/// are resolved by the platform/server before execution and applied by
76/// filtering the capability list handed to the worker.
77#[derive(Debug, Clone)]
78pub struct ExecutionFeatureDecisions {
79    /// Outbound agent delegation capabilities (`a2a_agent_delegation`,
80    /// `agent_handoff`). Experimental: auto-enabled in dev, off in prod by
81    /// default. When off, the capabilities are not registered at all.
82    pub agent_delegation: bool,
83    /// Backend-only infrastructure gates.
84    pub internal: InternalFeatureFlags,
85}
86
87impl ExecutionFeatureDecisions {
88    /// Resolve the deployment-level decisions from env vars and the grade.
89    pub fn from_env(grade: DeploymentGrade) -> Self {
90        Self {
91            agent_delegation: experimental_flag("FEATURE_AGENT_DELEGATION", &grade),
92            internal: InternalFeatureFlags::from_env(),
93        }
94    }
95
96    /// Whether a registration-time feature gate is enabled.
97    ///
98    /// Internal infrastructure flags win; any other name resolves via the
99    /// standard `FEATURE_<NAME>` env rule: enabled only by an explicit env
100    /// var, never by the grade's experimental default. Registration gates
101    /// control real side effects (e.g. the `machine_payments` gate on the
102    /// payments capability, where spend is irreversible), so an unknown gate
103    /// must fail closed even in dev — the pre-EVE-878 flag catalog classified
104    /// `machine_payments` as standard/off, and this preserves that. Used by
105    /// `IntegrationPlugin::feature_flag` gating.
106    pub fn is_enabled(&self, flag: &str) -> bool {
107        match flag {
108            "docker_capability" | "container_sandbox" | "session_sandbox" | "lua" => {
109                self.internal.is_enabled(flag)
110            }
111            "agent_delegation" => self.agent_delegation,
112            _ => {
113                let env_var = format!("FEATURE_{}", flag.to_ascii_uppercase());
114                standard_flag(&env_var, false)
115            }
116        }
117    }
118}
119
120/// Resolve an experimental flag.
121///
122/// Priority: explicit env var > experimental default (enabled in dev) > false.
123pub fn experimental_flag(env_var: &str, grade: &DeploymentGrade) -> bool {
124    if let Ok(val) = std::env::var(env_var) {
125        return val == "true" || val == "1";
126    }
127    grade.experimental_features_enabled()
128}
129
130/// Resolve a standard (non-experimental) flag.
131///
132/// Priority: explicit env var > default.
133pub fn standard_flag(env_var: &str, default: bool) -> bool {
134    std::env::var(env_var)
135        .map(|v| v == "true" || v == "1")
136        .unwrap_or(default)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn internal_lookup_reads_each_flag_independently() {
145        for values in [
146            [false, false, false, false],
147            [true, false, false, false],
148            [false, true, false, false],
149            [false, false, true, false],
150            [false, false, false, true],
151        ] {
152            let flags = InternalFeatureFlags {
153                docker_capability: values[0],
154                container_sandbox: values[1],
155                session_sandbox: values[2],
156                lua: values[3],
157            };
158            for (name, expected) in [
159                "docker_capability",
160                "container_sandbox",
161                "session_sandbox",
162                "lua",
163            ]
164            .into_iter()
165            .zip(values)
166            {
167                assert_eq!(flags.is_enabled(name), expected, "{name}, {values:?}");
168            }
169            for unknown in ["nonexistent", "Docker_capability", ""] {
170                assert!(!flags.is_enabled(unknown));
171            }
172        }
173    }
174
175    #[test]
176    fn environment_overrides_and_grade_defaults_are_isolated() {
177        const CHILD: &str = "EVERRUNS_EXECUTION_FEATURE_REVIEW_CASE";
178        const KEYS: &[&str] = &[
179            "FEATURE_DOCKER_CAPABILITY",
180            "FEATURE_CONTAINER_SANDBOX",
181            "FEATURE_SESSION_SANDBOX",
182            "FEATURE_LUA",
183            "FEATURE_AGENT_DELEGATION",
184            "FEATURE_MACHINE_PAYMENTS",
185            "DEPLOYMENT_GRADE",
186            "DEV_MODE",
187            "FEATURE_REVIEW_MISSING",
188        ];
189        struct Case {
190            name: &'static str,
191            env: &'static [(&'static str, &'static str)],
192            internal: [bool; 4],
193            delegation: [bool; 4],
194            grade: DeploymentGrade,
195            payments: bool,
196        }
197        let cases = [
198            Case {
199                name: "unset",
200                env: &[],
201                internal: [false, false, false, false],
202                delegation: [true, false, false, false],
203                grade: DeploymentGrade::Prod,
204                payments: false,
205            },
206            Case {
207                name: "legacy docker and dev mode one",
208                env: &[("FEATURE_DOCKER_CAPABILITY", "true"), ("DEV_MODE", "1")],
209                internal: [true, true, false, false],
210                delegation: [true, false, false, false],
211                grade: DeploymentGrade::Dev,
212                payments: false,
213            },
214            Case {
215                name: "explicit container and preview grade",
216                env: &[
217                    ("FEATURE_CONTAINER_SANDBOX", "1"),
218                    ("DEPLOYMENT_GRADE", "staging"),
219                    ("DEV_MODE", "true"),
220                ],
221                internal: [false, true, false, false],
222                delegation: [true, false, false, false],
223                grade: DeploymentGrade::Preview,
224                payments: false,
225            },
226            Case {
227                name: "explicit container false overrides legacy",
228                env: &[
229                    ("FEATURE_DOCKER_CAPABILITY", "true"),
230                    ("FEATURE_CONTAINER_SANDBOX", "false"),
231                    ("DEPLOYMENT_GRADE", "PoC"),
232                ],
233                internal: [true, false, false, false],
234                delegation: [true, false, false, false],
235                grade: DeploymentGrade::Poc,
236                payments: false,
237            },
238            Case {
239                name: "all enabled with explicit production",
240                env: &[
241                    ("FEATURE_DOCKER_CAPABILITY", "1"),
242                    ("FEATURE_CONTAINER_SANDBOX", "true"),
243                    ("FEATURE_SESSION_SANDBOX", "true"),
244                    ("FEATURE_LUA", "1"),
245                    ("FEATURE_AGENT_DELEGATION", "true"),
246                    ("FEATURE_MACHINE_PAYMENTS", "1"),
247                    ("DEPLOYMENT_GRADE", "production"),
248                    ("DEV_MODE", "true"),
249                ],
250                internal: [true, true, true, true],
251                delegation: [true, true, true, true],
252                grade: DeploymentGrade::Prod,
253                payments: true,
254            },
255            Case {
256                name: "explicit false overrides dev defaults",
257                env: &[
258                    ("FEATURE_DOCKER_CAPABILITY", "false"),
259                    ("FEATURE_CONTAINER_SANDBOX", "0"),
260                    ("FEATURE_SESSION_SANDBOX", "false"),
261                    ("FEATURE_LUA", "0"),
262                    ("FEATURE_AGENT_DELEGATION", "false"),
263                    ("FEATURE_MACHINE_PAYMENTS", "false"),
264                    ("DEV_MODE", "true"),
265                ],
266                internal: [false, false, false, false],
267                delegation: [false, false, false, false],
268                grade: DeploymentGrade::Dev,
269                payments: false,
270            },
271            Case {
272                name: "invalid explicit values fail closed",
273                env: &[
274                    ("FEATURE_DOCKER_CAPABILITY", "true"),
275                    ("FEATURE_CONTAINER_SANDBOX", "TRUE"),
276                    ("FEATURE_SESSION_SANDBOX", "typo"),
277                    ("FEATURE_LUA", "TRUE"),
278                    ("FEATURE_AGENT_DELEGATION", "TRUE"),
279                    ("FEATURE_MACHINE_PAYMENTS", "yes"),
280                    ("DEPLOYMENT_GRADE", "invalid"),
281                    ("DEV_MODE", "true"),
282                ],
283                internal: [true, false, false, false],
284                delegation: [false, false, false, false],
285                grade: DeploymentGrade::Prod,
286                payments: false,
287            },
288            Case {
289                name: "empty explicit grade suppresses legacy dev",
290                env: &[("DEPLOYMENT_GRADE", ""), ("DEV_MODE", "true")],
291                internal: [false, false, false, false],
292                delegation: [true, false, false, false],
293                grade: DeploymentGrade::Prod,
294                payments: false,
295            },
296        ];
297        if let Ok(index) = std::env::var(CHILD) {
298            let index: usize = index.parse().unwrap();
299            let case = &cases[index];
300            let expected_internal = InternalFeatureFlags {
301                docker_capability: case.internal[0],
302                container_sandbox: case.internal[1],
303                session_sandbox: case.internal[2],
304                lua: case.internal[3],
305            };
306            assert_eq!(
307                InternalFeatureFlags::from_env(),
308                expected_internal,
309                "{}",
310                case.name
311            );
312            assert_eq!(DeploymentGrade::from_env(), case.grade, "{}", case.name);
313            for (grade, expected_delegation) in [
314                DeploymentGrade::Dev,
315                DeploymentGrade::Poc,
316                DeploymentGrade::Preview,
317                DeploymentGrade::Prod,
318            ]
319            .into_iter()
320            .zip(case.delegation)
321            {
322                let decisions = ExecutionFeatureDecisions::from_env(grade);
323                assert_eq!(
324                    decisions.internal, expected_internal,
325                    "{}, {grade}",
326                    case.name
327                );
328                assert_eq!(
329                    decisions.agent_delegation, expected_delegation,
330                    "{}, {grade}",
331                    case.name
332                );
333                assert_eq!(
334                    decisions.is_enabled("agent_delegation"),
335                    expected_delegation
336                );
337                for (name, expected) in [
338                    "docker_capability",
339                    "container_sandbox",
340                    "session_sandbox",
341                    "lua",
342                ]
343                .into_iter()
344                .zip(case.internal)
345                {
346                    assert_eq!(decisions.is_enabled(name), expected, "{name}");
347                }
348                assert_eq!(decisions.is_enabled("machine_payments"), case.payments);
349                assert_eq!(decisions.is_enabled("MACHINE_PAYMENTS"), case.payments);
350                assert!(!decisions.is_enabled("review_missing"));
351            }
352            // Known flags come from the resolved snapshot, even if the current
353            // environment says true. Unknown registration gates use the env rule.
354            let captured = ExecutionFeatureDecisions {
355                agent_delegation: false,
356                internal: InternalFeatureFlags::default(),
357            };
358            for name in [
359                "docker_capability",
360                "container_sandbox",
361                "session_sandbox",
362                "lua",
363                "agent_delegation",
364            ] {
365                assert!(!captured.is_enabled(name), "captured {name}");
366            }
367            assert_eq!(captured.is_enabled("machine_payments"), case.payments);
368            assert!(!standard_flag("FEATURE_REVIEW_MISSING", false));
369            assert!(standard_flag("FEATURE_REVIEW_MISSING", true));
370            println!("feature fixture {index} completed");
371            return;
372        }
373        for (index, case) in cases.iter().enumerate() {
374            // A module-local mutex cannot protect process-wide environment reads
375            // by other tests. Set variables before each child starts instead.
376            let mut command = std::process::Command::new(std::env::current_exe().unwrap());
377            command.args([
378                "--exact",
379                concat!(
380                    module_path!(),
381                    "::environment_overrides_and_grade_defaults_are_isolated"
382                )
383                .strip_prefix("everruns_core::")
384                .unwrap(),
385                "--nocapture",
386            ]);
387            for key in KEYS {
388                command.env_remove(key);
389            }
390            let output = command
391                .envs(case.env.iter().copied())
392                .env(CHILD, index.to_string())
393                .output()
394                .unwrap();
395            let stdout = String::from_utf8_lossy(&output.stdout);
396            assert!(
397                output.status.success(),
398                "{} failed:\n{stdout}\n{}",
399                case.name,
400                String::from_utf8_lossy(&output.stderr)
401            );
402            assert!(
403                stdout.contains(&format!("feature fixture {index} completed")),
404                "{} did not run assertions:\n{stdout}",
405                case.name
406            );
407        }
408    }
409}