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}