Skip to main content

loopsmith_core/config/
environment.rs

1//! Which deployment this config describes, and which risky capabilities are
2//! unlocked in it.
3//!
4//! Both live at the config root rather than inside a bundle because both are
5//! read *before* anything else is interpreted: the environment decides how
6//! strictly the rest is enforced, and a feature flag can switch off a whole
7//! bundle regardless of what that bundle says.
8//!
9//! Every flag defaults to the safe answer. A loop that says nothing acquires no
10//! marketplace skills, evolves nothing, takes no external side effect, and asks
11//! a human before anything irreversible. Turning a capability on is always an
12//! explicit act by the author.
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17use super::yes;
18
19/// Which deployment this config describes.
20///
21/// `Prod` is not merely a label: the gate refuses an unreviewed marketplace
22/// skill and an unapproved evolution proposal outright in production, where in
23/// `Dev` the same config only warns. Authors therefore develop against the
24/// looser setting and get told what would have been refused, instead of
25/// discovering it on the run that mattered.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
27#[serde(rename_all = "lowercase")]
28pub enum Environment {
29    #[default]
30    Dev,
31    Staging,
32    Prod,
33}
34
35impl Environment {
36    /// Whether this environment refuses, rather than warns about, a capability
37    /// used without the review its policy demands.
38    pub fn enforces_strictly(self) -> bool {
39        matches!(self, Environment::Staging | Environment::Prod)
40    }
41
42    pub fn as_str(self) -> &'static str {
43        match self {
44            Environment::Dev => "dev",
45            Environment::Staging => "staging",
46            Environment::Prod => "prod",
47        }
48    }
49}
50
51/// Capability switches, each defaulting to the conservative answer.
52///
53/// These are deliberately coarse. A flag answers "is this class of thing
54/// allowed at all"; the bundle that owns the capability answers "under what
55/// conditions". Keeping the two apart means an operator can disable a whole
56/// capability without reading, or trusting, the policy that configures it.
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct Features {
60    /// Allow the loop to propose and trial changes to itself. Off by default:
61    /// self-modification is the capability most worth opting into knowingly.
62    #[serde(default)]
63    pub self_evolution: bool,
64    /// Allow sub-agents to be acquired from the marketplace. Off by default —
65    /// this is the supply-chain surface.
66    #[serde(default)]
67    pub marketplace_skills: bool,
68    /// Allow nodes to take actions that reach outside the loop directory.
69    #[serde(default)]
70    pub external_side_effects: bool,
71    /// Allow independent nodes in a wave to run concurrently. On by default;
72    /// turning it off forces strict sequential execution, which is the first
73    /// thing to try when debugging a run that behaves differently under load.
74    #[serde(default = "yes")]
75    pub parallel_execution: bool,
76    /// Honour `safety.limits.*.human_checkpoint`. On by default. Turning it
77    /// off is refused outright in `prod`.
78    #[serde(default = "yes")]
79    pub human_approval: bool,
80}
81
82impl Default for Features {
83    fn default() -> Self {
84        Self {
85            self_evolution: false,
86            marketplace_skills: false,
87            external_side_effects: false,
88            parallel_execution: true,
89            human_approval: true,
90        }
91    }
92}