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