Skip to main content

clawft_kernel/
environment.rs

1//! Environment scoping for WeftOS governance.
2//!
3//! Environments (development, staging, production) define governance
4//! scopes with different risk thresholds, capability sets, audit
5//! levels, and learning policies. The same agent identity operates
6//! across environments but with capabilities scoped to each
7//! environment's governance rules.
8//!
9//! # Design
10//!
11//! All types compile unconditionally. Environment-scoped governance
12//! enforcement requires the kernel's capability checker and is wired
13//! in the boot sequence. The self-learning loop (SONA integration)
14//! requires the `ruvector-apps` feature gate.
15
16use std::collections::HashMap;
17
18use dashmap::DashMap;
19use serde::{Deserialize, Serialize};
20use tracing::debug;
21
22/// Unique environment identifier.
23pub type EnvironmentId = String;
24
25/// Environment class determines base governance rules.
26#[non_exhaustive]
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub enum EnvironmentClass {
29    /// Full autonomy. Agents can experiment freely.
30    /// Risk threshold: 0.9 (almost anything allowed).
31    Development,
32
33    /// Deployed builds, automated testing. Agents test but
34    /// do not innovate. Risk threshold: 0.6 (moderate gating).
35    Staging,
36
37    /// Live systems. Strong gating, human approval for
38    /// high-risk actions. Risk threshold: 0.3 (strict gating).
39    Production,
40
41    /// Custom environment with explicit risk threshold.
42    Custom {
43        /// Custom environment name.
44        name: String,
45        /// Risk threshold (0.0 = block everything, 1.0 = allow everything).
46        risk_threshold: f64,
47    },
48}
49
50impl EnvironmentClass {
51    /// Get the default risk threshold for this environment class.
52    pub fn risk_threshold(&self) -> f64 {
53        match self {
54            EnvironmentClass::Development => 0.9,
55            EnvironmentClass::Staging => 0.6,
56            EnvironmentClass::Production => 0.3,
57            EnvironmentClass::Custom { risk_threshold, .. } => *risk_threshold,
58        }
59    }
60}
61
62impl std::fmt::Display for EnvironmentClass {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            EnvironmentClass::Development => write!(f, "development"),
66            EnvironmentClass::Staging => write!(f, "staging"),
67            EnvironmentClass::Production => write!(f, "production"),
68            EnvironmentClass::Custom { name, .. } => write!(f, "custom({name})"),
69        }
70    }
71}
72
73/// Governance scope defining rules for an environment.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct GovernanceScope {
76    /// Risk threshold: actions above this are blocked or escalated.
77    #[serde(default = "default_risk_threshold")]
78    pub risk_threshold: f64,
79
80    /// Whether human approval is required for actions above threshold.
81    #[serde(default)]
82    pub human_approval_required: bool,
83
84    /// Which governance branches are active.
85    #[serde(default)]
86    pub active_branches: GovernanceBranches,
87
88    /// How detailed the audit trail needs to be.
89    #[serde(default)]
90    pub audit_level: AuditLevel,
91
92    /// SONA learning mode for this environment.
93    #[serde(default)]
94    pub learning_mode: LearningMode,
95
96    /// Maximum effect vector magnitude before escalation.
97    #[serde(default = "default_max_effect")]
98    pub max_effect_magnitude: f64,
99}
100
101fn default_risk_threshold() -> f64 {
102    0.6
103}
104
105fn default_max_effect() -> f64 {
106    1.0
107}
108
109impl Default for GovernanceScope {
110    fn default() -> Self {
111        Self {
112            risk_threshold: default_risk_threshold(),
113            human_approval_required: false,
114            active_branches: GovernanceBranches::default(),
115            audit_level: AuditLevel::default(),
116            learning_mode: LearningMode::default(),
117            max_effect_magnitude: default_max_effect(),
118        }
119    }
120}
121
122/// Active governance branches (three-branch separation).
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct GovernanceBranches {
125    /// Legislative: SOP/rule definition.
126    #[serde(default = "default_true")]
127    pub legislative: bool,
128
129    /// Executive: agent actions.
130    #[serde(default = "default_true")]
131    pub executive: bool,
132
133    /// Judicial: CGR validation (off in dev for speed).
134    #[serde(default)]
135    pub judicial: bool,
136}
137
138fn default_true() -> bool {
139    true
140}
141
142impl Default for GovernanceBranches {
143    fn default() -> Self {
144        Self {
145            legislative: true,
146            executive: true,
147            judicial: false,
148        }
149    }
150}
151
152/// Audit trail detail level.
153#[non_exhaustive]
154#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
155pub enum AuditLevel {
156    /// One summary record per agent session.
157    #[default]
158    SessionSummary,
159    /// One record per action taken.
160    PerAction,
161    /// Per-action plus full 5D effect vector.
162    PerActionWithEffects,
163}
164
165/// SONA learning mode.
166#[non_exhaustive]
167#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
168pub enum LearningMode {
169    /// High entropy: try novel approaches, learn from failures.
170    /// Used in development environments.
171    #[default]
172    Explore,
173    /// Medium entropy: test hypotheses from explore phase.
174    /// Used in staging environments.
175    Validate,
176    /// Low entropy: only use proven patterns.
177    /// Used in production environments.
178    Exploit,
179}
180
181/// An environment definition with its governance scope.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Environment {
184    /// Unique environment identifier.
185    pub id: EnvironmentId,
186
187    /// Human-readable name.
188    pub name: String,
189
190    /// Environment class determines base governance rules.
191    pub class: EnvironmentClass,
192
193    /// Governance scope (risk thresholds, approval requirements).
194    #[serde(default)]
195    pub governance: GovernanceScope,
196
197    /// Labels for scheduling and filtering.
198    #[serde(default)]
199    pub labels: HashMap<String, String>,
200}
201
202/// Environment management errors.
203#[non_exhaustive]
204#[derive(Debug, thiserror::Error)]
205pub enum EnvironmentError {
206    /// Environment already exists.
207    #[error("environment already exists: '{id}'")]
208    AlreadyExists {
209        /// Environment ID.
210        id: EnvironmentId,
211    },
212
213    /// Environment not found.
214    #[error("environment not found: '{id}'")]
215    NotFound {
216        /// Environment ID.
217        id: EnvironmentId,
218    },
219
220    /// Invalid risk threshold.
221    #[error("invalid risk threshold {value}: must be between 0.0 and 1.0")]
222    InvalidRiskThreshold {
223        /// The invalid value.
224        value: f64,
225    },
226}
227
228/// Environment manager.
229///
230/// Tracks registered environments and the currently active one.
231/// Governance enforcement is done by the capability checker using
232/// the active environment's governance scope.
233pub struct EnvironmentManager {
234    environments: DashMap<EnvironmentId, Environment>,
235    active: std::sync::RwLock<Option<EnvironmentId>>,
236}
237
238impl EnvironmentManager {
239    /// Create a new environment manager.
240    pub fn new() -> Self {
241        Self {
242            environments: DashMap::new(),
243            active: std::sync::RwLock::new(None),
244        }
245    }
246
247    /// Register an environment.
248    pub fn register(&self, env: Environment) -> Result<(), EnvironmentError> {
249        if env.governance.risk_threshold < 0.0 || env.governance.risk_threshold > 1.0 {
250            return Err(EnvironmentError::InvalidRiskThreshold {
251                value: env.governance.risk_threshold,
252            });
253        }
254
255        if self.environments.contains_key(&env.id) {
256            return Err(EnvironmentError::AlreadyExists { id: env.id });
257        }
258
259        debug!(id = %env.id, name = %env.name, class = %env.class, "registering environment");
260        self.environments.insert(env.id.clone(), env);
261        Ok(())
262    }
263
264    /// Set the active environment.
265    pub fn set_active(&self, id: &str) -> Result<(), EnvironmentError> {
266        if !self.environments.contains_key(id) {
267            return Err(EnvironmentError::NotFound { id: id.to_owned() });
268        }
269        let mut active = self.active.write().unwrap();
270        *active = Some(id.to_owned());
271        Ok(())
272    }
273
274    /// Get the active environment ID.
275    pub fn active_id(&self) -> Option<EnvironmentId> {
276        self.active.read().unwrap().clone()
277    }
278
279    /// Get the active environment.
280    pub fn active(&self) -> Option<Environment> {
281        let id = self.active_id()?;
282        self.environments.get(&id).map(|e| e.value().clone())
283    }
284
285    /// Get an environment by ID.
286    pub fn get(&self, id: &str) -> Option<Environment> {
287        self.environments.get(id).map(|e| e.value().clone())
288    }
289
290    /// List all environments.
291    pub fn list(&self) -> Vec<(EnvironmentId, EnvironmentClass, bool)> {
292        let active_id = self.active_id();
293        self.environments
294            .iter()
295            .map(|e| {
296                let is_active = active_id.as_deref() == Some(e.key().as_str());
297                (e.key().clone(), e.class.clone(), is_active)
298            })
299            .collect()
300    }
301
302    /// Remove an environment.
303    pub fn remove(&self, id: &str) -> Result<Environment, EnvironmentError> {
304        // Cannot remove the active environment
305        if self.active_id().as_deref() == Some(id) {
306            // Deactivate first
307            let mut active = self.active.write().unwrap();
308            *active = None;
309        }
310
311        self.environments
312            .remove(id)
313            .map(|(_, env)| env)
314            .ok_or_else(|| EnvironmentError::NotFound { id: id.to_owned() })
315    }
316
317    /// Count environments.
318    pub fn len(&self) -> usize {
319        self.environments.len()
320    }
321
322    /// Check if empty.
323    pub fn is_empty(&self) -> bool {
324        self.environments.is_empty()
325    }
326
327    /// Create a standard set of environments (dev, staging, prod).
328    pub fn create_standard_set(&self) -> Result<(), EnvironmentError> {
329        self.register(Environment {
330            id: "dev".into(),
331            name: "Development".into(),
332            class: EnvironmentClass::Development,
333            governance: GovernanceScope {
334                risk_threshold: 0.9,
335                human_approval_required: false,
336                active_branches: GovernanceBranches {
337                    legislative: true,
338                    executive: true,
339                    judicial: false,
340                },
341                audit_level: AuditLevel::SessionSummary,
342                learning_mode: LearningMode::Explore,
343                max_effect_magnitude: 2.0,
344            },
345            labels: HashMap::new(),
346        })?;
347
348        self.register(Environment {
349            id: "staging".into(),
350            name: "Staging".into(),
351            class: EnvironmentClass::Staging,
352            governance: GovernanceScope {
353                risk_threshold: 0.6,
354                human_approval_required: false,
355                active_branches: GovernanceBranches {
356                    legislative: true,
357                    executive: true,
358                    judicial: true,
359                },
360                audit_level: AuditLevel::PerAction,
361                learning_mode: LearningMode::Validate,
362                max_effect_magnitude: 1.0,
363            },
364            labels: HashMap::new(),
365        })?;
366
367        self.register(Environment {
368            id: "prod".into(),
369            name: "Production".into(),
370            class: EnvironmentClass::Production,
371            governance: GovernanceScope {
372                risk_threshold: 0.3,
373                human_approval_required: true,
374                active_branches: GovernanceBranches {
375                    legislative: true,
376                    executive: true,
377                    judicial: true,
378                },
379                audit_level: AuditLevel::PerActionWithEffects,
380                learning_mode: LearningMode::Exploit,
381                max_effect_magnitude: 0.5,
382            },
383            labels: HashMap::new(),
384        })?;
385
386        Ok(())
387    }
388}
389
390impl Default for EnvironmentManager {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    fn make_dev_env() -> Environment {
401        Environment {
402            id: "dev".into(),
403            name: "Development".into(),
404            class: EnvironmentClass::Development,
405            governance: GovernanceScope {
406                risk_threshold: 0.9,
407                ..Default::default()
408            },
409            labels: HashMap::new(),
410        }
411    }
412
413    #[test]
414    fn environment_class_risk_threshold() {
415        assert!((EnvironmentClass::Development.risk_threshold() - 0.9).abs() < f64::EPSILON);
416        assert!((EnvironmentClass::Staging.risk_threshold() - 0.6).abs() < f64::EPSILON);
417        assert!((EnvironmentClass::Production.risk_threshold() - 0.3).abs() < f64::EPSILON);
418        let custom = EnvironmentClass::Custom {
419            name: "test".into(),
420            risk_threshold: 0.75,
421        };
422        assert!((custom.risk_threshold() - 0.75).abs() < f64::EPSILON);
423    }
424
425    #[test]
426    fn environment_class_display() {
427        assert_eq!(EnvironmentClass::Development.to_string(), "development");
428        assert_eq!(EnvironmentClass::Production.to_string(), "production");
429        assert_eq!(
430            EnvironmentClass::Custom {
431                name: "qa".into(),
432                risk_threshold: 0.5
433            }
434            .to_string(),
435            "custom(qa)"
436        );
437    }
438
439    #[test]
440    fn governance_scope_default() {
441        let scope = GovernanceScope::default();
442        assert!((scope.risk_threshold - 0.6).abs() < f64::EPSILON);
443        assert!(!scope.human_approval_required);
444        assert!(scope.active_branches.legislative);
445        assert!(scope.active_branches.executive);
446        assert!(!scope.active_branches.judicial);
447    }
448
449    #[test]
450    fn governance_scope_serde_roundtrip() {
451        let scope = GovernanceScope {
452            risk_threshold: 0.3,
453            human_approval_required: true,
454            active_branches: GovernanceBranches {
455                legislative: true,
456                executive: true,
457                judicial: true,
458            },
459            audit_level: AuditLevel::PerActionWithEffects,
460            learning_mode: LearningMode::Exploit,
461            max_effect_magnitude: 0.5,
462        };
463        let json = serde_json::to_string(&scope).unwrap();
464        let restored: GovernanceScope = serde_json::from_str(&json).unwrap();
465        assert!((restored.risk_threshold - 0.3).abs() < f64::EPSILON);
466        assert!(restored.human_approval_required);
467        assert_eq!(restored.learning_mode, LearningMode::Exploit);
468    }
469
470    #[test]
471    fn register_and_list() {
472        let mgr = EnvironmentManager::new();
473        mgr.register(make_dev_env()).unwrap();
474        let list = mgr.list();
475        assert_eq!(list.len(), 1);
476    }
477
478    #[test]
479    fn register_duplicate_fails() {
480        let mgr = EnvironmentManager::new();
481        mgr.register(make_dev_env()).unwrap();
482        assert!(matches!(
483            mgr.register(make_dev_env()),
484            Err(EnvironmentError::AlreadyExists { .. })
485        ));
486    }
487
488    #[test]
489    fn invalid_risk_threshold() {
490        let mgr = EnvironmentManager::new();
491        let mut env = make_dev_env();
492        env.governance.risk_threshold = 1.5;
493        assert!(matches!(
494            mgr.register(env),
495            Err(EnvironmentError::InvalidRiskThreshold { .. })
496        ));
497    }
498
499    #[test]
500    fn set_active() {
501        let mgr = EnvironmentManager::new();
502        mgr.register(make_dev_env()).unwrap();
503        mgr.set_active("dev").unwrap();
504        assert_eq!(mgr.active_id().as_deref(), Some("dev"));
505        let active = mgr.active().unwrap();
506        assert_eq!(active.name, "Development");
507    }
508
509    #[test]
510    fn set_active_nonexistent_fails() {
511        let mgr = EnvironmentManager::new();
512        assert!(matches!(
513            mgr.set_active("nope"),
514            Err(EnvironmentError::NotFound { .. })
515        ));
516    }
517
518    #[test]
519    fn remove_environment() {
520        let mgr = EnvironmentManager::new();
521        mgr.register(make_dev_env()).unwrap();
522        let removed = mgr.remove("dev").unwrap();
523        assert_eq!(removed.name, "Development");
524        assert!(mgr.is_empty());
525    }
526
527    #[test]
528    fn remove_active_clears_active() {
529        let mgr = EnvironmentManager::new();
530        mgr.register(make_dev_env()).unwrap();
531        mgr.set_active("dev").unwrap();
532        mgr.remove("dev").unwrap();
533        assert!(mgr.active_id().is_none());
534    }
535
536    #[test]
537    fn create_standard_set() {
538        let mgr = EnvironmentManager::new();
539        mgr.create_standard_set().unwrap();
540        assert_eq!(mgr.len(), 3);
541
542        let dev = mgr.get("dev").unwrap();
543        assert!((dev.governance.risk_threshold - 0.9).abs() < f64::EPSILON);
544        assert_eq!(dev.governance.learning_mode, LearningMode::Explore);
545
546        let prod = mgr.get("prod").unwrap();
547        assert!((prod.governance.risk_threshold - 0.3).abs() < f64::EPSILON);
548        assert!(prod.governance.human_approval_required);
549        assert_eq!(prod.governance.learning_mode, LearningMode::Exploit);
550    }
551
552    #[test]
553    fn environment_serde_roundtrip() {
554        let env = make_dev_env();
555        let json = serde_json::to_string(&env).unwrap();
556        let restored: Environment = serde_json::from_str(&json).unwrap();
557        assert_eq!(restored.id, "dev");
558        assert_eq!(restored.class, EnvironmentClass::Development);
559    }
560
561    #[test]
562    fn environment_error_display() {
563        let err = EnvironmentError::NotFound { id: "prod".into() };
564        assert!(err.to_string().contains("prod"));
565
566        let err = EnvironmentError::InvalidRiskThreshold { value: 1.5 };
567        assert!(err.to_string().contains("1.5"));
568    }
569
570    #[test]
571    fn audit_level_default() {
572        assert_eq!(AuditLevel::default(), AuditLevel::SessionSummary);
573    }
574
575    #[test]
576    fn learning_mode_default() {
577        assert_eq!(LearningMode::default(), LearningMode::Explore);
578    }
579}