1use std::collections::HashMap;
19
20use serde::{Deserialize, Serialize};
21use tracing::debug;
22
23use crate::environment::{Environment, EnvironmentClass};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GovernanceRule {
28 pub id: String,
30
31 pub description: String,
33
34 pub branch: GovernanceBranch,
36
37 pub severity: RuleSeverity,
39
40 #[serde(default = "default_true")]
42 pub active: bool,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub reference_url: Option<String>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub sop_category: Option<String>,
51}
52
53impl GovernanceRule {
54 pub fn filter_by_category<'a>(rules: &'a [GovernanceRule], category: &str) -> Vec<&'a GovernanceRule> {
56 rules.iter().filter(|r| r.sop_category.as_deref() == Some(category)).collect()
57 }
58}
59
60fn default_true() -> bool {
61 true
62}
63
64#[non_exhaustive]
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub enum GovernanceBranch {
68 Legislative,
70 Executive,
72 Judicial,
74}
75
76impl std::fmt::Display for GovernanceBranch {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 GovernanceBranch::Legislative => write!(f, "legislative"),
80 GovernanceBranch::Executive => write!(f, "executive"),
81 GovernanceBranch::Judicial => write!(f, "judicial"),
82 }
83 }
84}
85
86#[non_exhaustive]
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
89pub enum RuleSeverity {
90 Advisory,
92 Warning,
94 Blocking,
96 Critical,
98}
99
100impl std::fmt::Display for RuleSeverity {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 match self {
103 RuleSeverity::Advisory => write!(f, "advisory"),
104 RuleSeverity::Warning => write!(f, "warning"),
105 RuleSeverity::Blocking => write!(f, "blocking"),
106 RuleSeverity::Critical => write!(f, "critical"),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct EffectVector {
118 #[serde(default)]
120 pub risk: f64,
121
122 #[serde(default)]
124 pub fairness: f64,
125
126 #[serde(default)]
128 pub privacy: f64,
129
130 #[serde(default)]
132 pub novelty: f64,
133
134 #[serde(default)]
136 pub security: f64,
137}
138
139impl EffectVector {
140 pub fn magnitude(&self) -> f64 {
142 (self.risk * self.risk
143 + self.fairness * self.fairness
144 + self.privacy * self.privacy
145 + self.novelty * self.novelty
146 + self.security * self.security)
147 .sqrt()
148 }
149
150 pub fn any_exceeds(&self, threshold: f64) -> bool {
152 self.risk > threshold
153 || self.fairness > threshold
154 || self.privacy > threshold
155 || self.novelty > threshold
156 || self.security > threshold
157 }
158
159 pub fn max_dimension(&self) -> f64 {
161 [
162 self.risk,
163 self.fairness,
164 self.privacy,
165 self.novelty,
166 self.security,
167 ]
168 .into_iter()
169 .fold(0.0_f64, f64::max)
170 }
171}
172
173#[non_exhaustive]
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub enum GovernanceDecision {
177 Permit,
179 PermitWithWarning(String),
181 EscalateToHuman(String),
183 Deny(String),
185}
186
187impl std::fmt::Display for GovernanceDecision {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 match self {
190 GovernanceDecision::Permit => write!(f, "permit"),
191 GovernanceDecision::PermitWithWarning(msg) => write!(f, "permit (warning: {msg})"),
192 GovernanceDecision::EscalateToHuman(msg) => write!(f, "escalate ({msg})"),
193 GovernanceDecision::Deny(reason) => write!(f, "deny: {reason}"),
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct GovernanceRequest {
201 pub agent_id: String,
203
204 pub action: String,
206
207 #[serde(default)]
209 pub effect: EffectVector,
210
211 #[serde(default)]
213 pub context: std::collections::HashMap<String, String>,
214
215 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub node_id: Option<String>,
218}
219
220impl GovernanceRequest {
221 pub fn new(agent_id: impl Into<String>, action: impl Into<String>) -> Self {
223 Self {
224 agent_id: agent_id.into(),
225 action: action.into(),
226 effect: EffectVector::default(),
227 context: std::collections::HashMap::new(),
228 node_id: None,
229 }
230 }
231
232 pub fn with_node_id(mut self, node_id: impl Into<String>) -> Self {
234 self.node_id = Some(node_id.into());
235 self
236 }
237
238 pub fn with_effect(mut self, effect: EffectVector) -> Self {
240 self.effect = effect;
241 self
242 }
243
244 pub fn with_tool_context(
259 mut self,
260 tool_name: impl Into<String>,
261 gate_action: impl Into<String>,
262 effect: EffectVector,
263 pid: u64,
264 ) -> Self {
265 self.context.insert("tool".into(), tool_name.into());
266 self.context.insert("gate_action".into(), gate_action.into());
267 self.context.insert("pid".into(), pid.to_string());
268 self.effect = effect;
269 self
270 }
271
272 pub fn with_context_entry(
274 mut self,
275 key: impl Into<String>,
276 value: impl Into<String>,
277 ) -> Self {
278 self.context.insert(key.into(), value.into());
279 self
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct GovernanceResult {
286 pub decision: GovernanceDecision,
288
289 pub evaluated_rules: Vec<String>,
291
292 pub effect: EffectVector,
294
295 pub threshold_exceeded: bool,
297}
298
299pub struct GovernanceEngine {
305 rules: Vec<GovernanceRule>,
306 risk_threshold: f64,
307 human_approval_required: bool,
308}
309
310impl GovernanceEngine {
311 pub fn new(risk_threshold: f64, human_approval_required: bool) -> Self {
313 Self {
314 rules: Vec::new(),
315 risk_threshold,
316 human_approval_required,
317 }
318 }
319
320 pub fn open() -> Self {
322 Self {
323 rules: Vec::new(),
324 risk_threshold: 1.0,
325 human_approval_required: false,
326 }
327 }
328
329 pub fn add_rule(&mut self, rule: GovernanceRule) {
331 debug!(rule_id = %rule.id, branch = %rule.branch, "adding governance rule");
332 self.rules.push(rule);
333 }
334
335 pub fn active_rules(&self) -> Vec<&GovernanceRule> {
337 self.rules.iter().filter(|r| r.active).collect()
338 }
339
340 pub fn rules_by_branch(&self, branch: &GovernanceBranch) -> Vec<&GovernanceRule> {
342 self.rules
343 .iter()
344 .filter(|r| r.active && &r.branch == branch)
345 .collect()
346 }
347
348 pub fn evaluate(&self, request: &GovernanceRequest) -> GovernanceResult {
358 let magnitude = request.effect.magnitude();
359 let threshold_exceeded = magnitude > self.risk_threshold;
360
361 let mut evaluated_rules = Vec::new();
362 let mut has_warning = false;
363 let mut has_blocking = false;
364 let mut blocking_reason = String::new();
365
366 for rule in self.active_rules() {
367 evaluated_rules.push(rule.id.clone());
368
369 match rule.severity {
370 RuleSeverity::Blocking | RuleSeverity::Critical => {
371 if threshold_exceeded {
372 has_blocking = true;
373 blocking_reason = format!(
374 "rule '{}': effect magnitude {magnitude:.2} > threshold {:.2}",
375 rule.id, self.risk_threshold
376 );
377 }
378 }
379 RuleSeverity::Warning => {
380 if threshold_exceeded {
381 has_warning = true;
382 }
383 }
384 RuleSeverity::Advisory => {}
385 }
386 }
387
388 let decision = if has_blocking {
389 if self.human_approval_required {
390 GovernanceDecision::EscalateToHuman(blocking_reason)
391 } else {
392 GovernanceDecision::Deny(blocking_reason)
393 }
394 } else if threshold_exceeded && has_warning {
395 GovernanceDecision::PermitWithWarning(format!(
396 "effect magnitude {magnitude:.2} approaches threshold {:.2}",
397 self.risk_threshold
398 ))
399 } else {
400 GovernanceDecision::Permit
401 };
402
403 GovernanceResult {
404 decision,
405 evaluated_rules,
406 effect: request.effect.clone(),
407 threshold_exceeded,
408 }
409 }
410
411 pub fn risk_threshold(&self) -> f64 {
413 self.risk_threshold
414 }
415
416 pub fn rule_count(&self) -> usize {
418 self.rules.len()
419 }
420
421 pub fn evaluate_in_environment(
433 &self,
434 request: &GovernanceRequest,
435 env: &Environment,
436 ) -> GovernanceResult {
437 let adjusted_threshold = match &env.class {
438 EnvironmentClass::Development => {
439 env.governance.risk_threshold
441 }
442 EnvironmentClass::Staging => {
443 env.governance.risk_threshold
445 }
446 EnvironmentClass::Production => {
447 env.governance.risk_threshold * 0.5
449 }
450 EnvironmentClass::Custom { risk_threshold, .. } => {
451 *risk_threshold
453 }
454 };
455
456 let mut result = self.evaluate(request);
458
459 let magnitude = request.effect.magnitude();
461 if magnitude > adjusted_threshold {
462 result.threshold_exceeded = true;
463 result.decision = GovernanceDecision::Deny(format!(
464 "effect magnitude {magnitude:.2} exceeds {} environment threshold {adjusted_threshold:.2}",
465 env.class,
466 ));
467 }
468
469 result
470 }
471
472 #[cfg(feature = "exochain")]
480 pub fn evaluate_logged(
481 &self,
482 request: &GovernanceRequest,
483 chain: Option<&crate::chain::ChainManager>,
484 ) -> GovernanceResult {
485 let result = self.evaluate(request);
486 if let Some(cm) = chain {
487 Self::chain_log_result(cm, request, &result);
488 }
489 result
490 }
491
492 #[cfg(feature = "exochain")]
494 pub fn evaluate_in_environment_logged(
495 &self,
496 request: &GovernanceRequest,
497 env: &Environment,
498 chain: Option<&crate::chain::ChainManager>,
499 ) -> GovernanceResult {
500 let result = self.evaluate_in_environment(request, env);
501 if let Some(cm) = chain {
502 Self::chain_log_result(cm, request, &result);
503 }
504 result
505 }
506
507 #[cfg(feature = "exochain")]
512 pub fn chain_log_result(
513 cm: &crate::chain::ChainManager,
514 request: &GovernanceRequest,
515 result: &GovernanceResult,
516 ) {
517 use crate::chain::{ChainLoggable, GovernanceDecisionEvent};
518
519 let decision_str = match &result.decision {
520 GovernanceDecision::Permit => "Permit".to_owned(),
521 GovernanceDecision::PermitWithWarning(_) => "PermitWithWarning".to_owned(),
522 GovernanceDecision::EscalateToHuman(_) => "EscalateToHuman".to_owned(),
523 GovernanceDecision::Deny(_) => "Deny".to_owned(),
524 };
525
526 let event = GovernanceDecisionEvent {
527 agent_id: request.agent_id.clone(),
528 action: request.action.clone(),
529 decision: decision_str,
530 effect_magnitude: request.effect.magnitude(),
531 threshold_exceeded: result.threshold_exceeded,
532 evaluated_rules: result.evaluated_rules.clone(),
533 timestamp: chrono::Utc::now(),
534 };
535 cm.append_loggable(&event);
536 }
537}
538
539#[non_exhaustive]
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub enum TrajectoryOutcome {
549 Success {
551 reward: f64,
553 },
554 Failure {
556 reason: String,
558 },
559 Pending,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct TrajectoryRecord {
566 pub agent_id: String,
568 pub action: String,
570 pub context: serde_json::Value,
572 pub outcome: TrajectoryOutcome,
574 pub timestamp: chrono::DateTime<chrono::Utc>,
576}
577
578pub struct TrajectoryRecorder {
584 records: Vec<TrajectoryRecord>,
585 max_records: usize,
586}
587
588impl TrajectoryRecorder {
589 pub fn new(max_records: usize) -> Self {
591 Self {
592 records: Vec::new(),
593 max_records,
594 }
595 }
596
597 pub fn record(&mut self, record: TrajectoryRecord) {
599 if self.records.len() >= self.max_records {
600 self.records.remove(0); }
602 self.records.push(record);
603 }
604
605 pub fn agent_trajectory(&self, agent_id: &str) -> Vec<&TrajectoryRecord> {
607 self.records
608 .iter()
609 .filter(|r| r.agent_id == agent_id)
610 .collect()
611 }
612
613 pub fn extract_patterns(&self) -> Vec<(String, usize)> {
616 let mut action_counts: HashMap<String, usize> = HashMap::new();
617 for record in &self.records {
618 if matches!(record.outcome, TrajectoryOutcome::Success { .. }) {
619 *action_counts.entry(record.action.clone()).or_default() += 1;
620 }
621 }
622 let mut patterns: Vec<_> = action_counts.into_iter().collect();
623 patterns.sort_by(|a, b| b.1.cmp(&a.1));
624 patterns
625 }
626
627 pub fn len(&self) -> usize {
629 self.records.len()
630 }
631
632 pub fn is_empty(&self) -> bool {
634 self.records.is_empty()
635 }
636}
637
638#[cfg(feature = "exochain")]
649impl GovernanceDecision {
650 pub fn to_rvf_policy_check(&self) -> rvf_types::witness::PolicyCheck {
656 match self {
657 GovernanceDecision::Permit => rvf_types::witness::PolicyCheck::Allowed,
658 GovernanceDecision::PermitWithWarning(_) => rvf_types::witness::PolicyCheck::Confirmed,
659 GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::PolicyCheck::Confirmed,
660 GovernanceDecision::Deny(_) => rvf_types::witness::PolicyCheck::Denied,
661 }
662 }
663}
664
665#[cfg(feature = "exochain")]
666impl GovernanceEngine {
667 pub fn to_rvf_mode(&self) -> rvf_types::witness::GovernanceMode {
673 if self.risk_threshold >= 1.0 {
674 rvf_types::witness::GovernanceMode::Autonomous
675 } else if self.human_approval_required {
676 rvf_types::witness::GovernanceMode::Approved
677 } else {
678 rvf_types::witness::GovernanceMode::Restricted
679 }
680 }
681
682 pub fn to_rvf_policy(&self) -> rvf_runtime::GovernancePolicy {
687 match self.to_rvf_mode() {
688 rvf_types::witness::GovernanceMode::Restricted => {
689 rvf_runtime::GovernancePolicy::restricted()
690 }
691 rvf_types::witness::GovernanceMode::Approved => {
692 rvf_runtime::GovernancePolicy::approved()
693 }
694 rvf_types::witness::GovernanceMode::Autonomous => {
695 rvf_runtime::GovernancePolicy::autonomous()
696 }
697 }
698 }
699}
700
701#[cfg(feature = "exochain")]
702impl GovernanceResult {
703 pub fn to_rvf_task_outcome(&self) -> rvf_types::witness::TaskOutcome {
708 match &self.decision {
709 GovernanceDecision::Permit | GovernanceDecision::PermitWithWarning(_) => {
710 rvf_types::witness::TaskOutcome::Solved
711 }
712 GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::TaskOutcome::Skipped,
713 GovernanceDecision::Deny(_) => rvf_types::witness::TaskOutcome::Failed,
714 }
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 fn make_rule(id: &str, severity: RuleSeverity, branch: GovernanceBranch) -> GovernanceRule {
723 GovernanceRule {
724 id: id.into(),
725 description: format!("Test rule {id}"),
726 branch,
727 severity,
728 active: true,
729 reference_url: None,
730 sop_category: None,
731 }
732 }
733
734 #[test]
735 fn effect_vector_magnitude() {
736 let v = EffectVector {
737 risk: 0.3,
738 fairness: 0.4,
739 privacy: 0.0,
740 novelty: 0.0,
741 security: 0.0,
742 };
743 assert!((v.magnitude() - 0.5).abs() < 0.001);
744 }
745
746 #[test]
747 fn effect_vector_zero() {
748 let v = EffectVector::default();
749 assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
750 }
751
752 #[test]
753 fn effect_any_exceeds() {
754 let v = EffectVector {
755 risk: 0.8,
756 ..Default::default()
757 };
758 assert!(v.any_exceeds(0.5));
759 assert!(!v.any_exceeds(0.9));
760 }
761
762 #[test]
763 fn effect_max_dimension() {
764 let v = EffectVector {
765 risk: 0.2,
766 fairness: 0.5,
767 privacy: 0.3,
768 novelty: 0.1,
769 security: 0.4,
770 };
771 assert!((v.max_dimension() - 0.5).abs() < f64::EPSILON);
772 }
773
774 #[test]
775 fn governance_branch_display() {
776 assert_eq!(GovernanceBranch::Legislative.to_string(), "legislative");
777 assert_eq!(GovernanceBranch::Executive.to_string(), "executive");
778 assert_eq!(GovernanceBranch::Judicial.to_string(), "judicial");
779 }
780
781 #[test]
782 fn rule_severity_ordering() {
783 assert!(RuleSeverity::Advisory < RuleSeverity::Warning);
784 assert!(RuleSeverity::Warning < RuleSeverity::Blocking);
785 assert!(RuleSeverity::Blocking < RuleSeverity::Critical);
786 }
787
788 #[test]
789 fn governance_decision_display() {
790 assert_eq!(GovernanceDecision::Permit.to_string(), "permit");
791 assert!(
792 GovernanceDecision::Deny("too risky".into())
793 .to_string()
794 .contains("too risky")
795 );
796 }
797
798 #[test]
799 fn open_engine_permits_everything() {
800 let engine = GovernanceEngine::open();
801 let request = GovernanceRequest {
802 agent_id: "agent-1".into(),
803 action: "deploy".into(),
804 effect: EffectVector {
805 risk: 0.9,
806 security: 0.9,
807 ..Default::default()
808 },
809 context: Default::default(),
810 node_id: None,
811 };
812 let result = engine.evaluate(&request);
813 assert_eq!(result.decision, GovernanceDecision::Permit);
814 }
815
816 #[test]
817 fn blocking_rule_denies() {
818 let mut engine = GovernanceEngine::new(0.5, false);
819 engine.add_rule(make_rule(
820 "security-check",
821 RuleSeverity::Blocking,
822 GovernanceBranch::Judicial,
823 ));
824
825 let request = GovernanceRequest {
826 agent_id: "agent-1".into(),
827 action: "deploy".into(),
828 effect: EffectVector {
829 risk: 0.6,
830 ..Default::default()
831 },
832 context: Default::default(),
833 node_id: None,
834 };
835 let result = engine.evaluate(&request);
836 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
837 assert!(result.threshold_exceeded);
838 }
839
840 #[test]
841 fn blocking_with_human_approval_escalates() {
842 let mut engine = GovernanceEngine::new(0.5, true);
843 engine.add_rule(make_rule(
844 "security-check",
845 RuleSeverity::Blocking,
846 GovernanceBranch::Judicial,
847 ));
848
849 let request = GovernanceRequest {
850 agent_id: "agent-1".into(),
851 action: "deploy".into(),
852 effect: EffectVector {
853 risk: 0.6,
854 ..Default::default()
855 },
856 context: Default::default(),
857 node_id: None,
858 };
859 let result = engine.evaluate(&request);
860 assert!(matches!(
861 result.decision,
862 GovernanceDecision::EscalateToHuman(_)
863 ));
864 }
865
866 #[test]
867 fn warning_rule_permits_with_warning() {
868 let mut engine = GovernanceEngine::new(0.5, false);
869 engine.add_rule(make_rule(
870 "risk-check",
871 RuleSeverity::Warning,
872 GovernanceBranch::Executive,
873 ));
874
875 let request = GovernanceRequest {
876 agent_id: "agent-1".into(),
877 action: "deploy".into(),
878 effect: EffectVector {
879 risk: 0.6,
880 ..Default::default()
881 },
882 context: Default::default(),
883 node_id: None,
884 };
885 let result = engine.evaluate(&request);
886 assert!(matches!(
887 result.decision,
888 GovernanceDecision::PermitWithWarning(_)
889 ));
890 }
891
892 #[test]
893 fn below_threshold_permits() {
894 let mut engine = GovernanceEngine::new(0.5, false);
895 engine.add_rule(make_rule(
896 "security-check",
897 RuleSeverity::Blocking,
898 GovernanceBranch::Judicial,
899 ));
900
901 let request = GovernanceRequest {
902 agent_id: "agent-1".into(),
903 action: "read".into(),
904 effect: EffectVector {
905 risk: 0.1,
906 ..Default::default()
907 },
908 context: Default::default(),
909 node_id: None,
910 };
911 let result = engine.evaluate(&request);
912 assert_eq!(result.decision, GovernanceDecision::Permit);
913 assert!(!result.threshold_exceeded);
914 }
915
916 #[test]
917 fn rules_by_branch() {
918 let mut engine = GovernanceEngine::new(0.5, false);
919 engine.add_rule(make_rule(
920 "r1",
921 RuleSeverity::Warning,
922 GovernanceBranch::Legislative,
923 ));
924 engine.add_rule(make_rule(
925 "r2",
926 RuleSeverity::Blocking,
927 GovernanceBranch::Judicial,
928 ));
929 engine.add_rule(make_rule(
930 "r3",
931 RuleSeverity::Advisory,
932 GovernanceBranch::Judicial,
933 ));
934
935 let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
936 assert_eq!(judicial.len(), 2);
937 let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
938 assert_eq!(legislative.len(), 1);
939 }
940
941 #[test]
942 fn inactive_rules_excluded() {
943 let mut engine = GovernanceEngine::new(0.5, false);
944 engine.add_rule(GovernanceRule {
945 id: "disabled".into(),
946 description: "Disabled rule".into(),
947 branch: GovernanceBranch::Judicial,
948 severity: RuleSeverity::Blocking,
949 active: false,
950 reference_url: None,
951 sop_category: None,
952 });
953 assert_eq!(engine.active_rules().len(), 0);
954 }
955
956 #[test]
957 fn governance_rule_serde_roundtrip() {
958 let rule = make_rule("sec-1", RuleSeverity::Critical, GovernanceBranch::Judicial);
959 let json = serde_json::to_string(&rule).unwrap();
960 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
961 assert_eq!(restored.id, "sec-1");
962 assert!(restored.active);
963 }
964
965 #[test]
966 fn governance_request_serde_roundtrip() {
967 let request = GovernanceRequest {
968 agent_id: "agent-1".into(),
969 action: "deploy".into(),
970 effect: EffectVector {
971 risk: 0.5,
972 privacy: 0.3,
973 ..Default::default()
974 },
975 context: std::collections::HashMap::from([("env".into(), "prod".into())]),
976 node_id: None,
977 };
978 let json = serde_json::to_string(&request).unwrap();
979 let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
980 assert_eq!(restored.agent_id, "agent-1");
981 assert!((restored.effect.risk - 0.5).abs() < f64::EPSILON);
982 assert!(restored.node_id.is_none());
983 }
984
985 #[test]
986 fn governance_request_with_node_id() {
987 let request = GovernanceRequest::new("agent-1", "deploy")
988 .with_node_id("node-42")
989 .with_effect(EffectVector { risk: 0.1, ..Default::default() });
990
991 assert_eq!(request.node_id.as_deref(), Some("node-42"));
992 assert_eq!(request.agent_id, "agent-1");
993
994 let json = serde_json::to_string(&request).unwrap();
996 assert!(json.contains("node-42"));
997 let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
998 assert_eq!(restored.node_id.as_deref(), Some("node-42"));
999 }
1000
1001 #[test]
1002 fn governance_request_without_node_id_deserializes() {
1003 let json = r#"{"agent_id":"a","action":"b","effect":{},"context":{}}"#;
1005 let request: GovernanceRequest = serde_json::from_str(json).unwrap();
1006 assert!(request.node_id.is_none());
1007 }
1008
1009 #[test]
1010 fn governance_request_builder() {
1011 let request = GovernanceRequest::new("agent-1", "deploy");
1012 assert_eq!(request.agent_id, "agent-1");
1013 assert_eq!(request.action, "deploy");
1014 assert!(request.node_id.is_none());
1015 assert!((request.effect.magnitude() - 0.0).abs() < f64::EPSILON);
1016 }
1017
1018 #[test]
1019 fn effect_vector_serde_roundtrip() {
1020 let v = EffectVector {
1021 risk: 0.1,
1022 fairness: 0.2,
1023 privacy: 0.3,
1024 novelty: 0.4,
1025 security: 0.5,
1026 };
1027 let json = serde_json::to_string(&v).unwrap();
1028 let restored: EffectVector = serde_json::from_str(&json).unwrap();
1029 assert!((restored.security - 0.5).abs() < f64::EPSILON);
1030 }
1031
1032 #[test]
1033 fn filter_rules_by_sop_category() {
1034 let rules = vec![
1035 GovernanceRule {
1036 id: "SOP-L001".into(),
1037 description: "test".into(),
1038 branch: GovernanceBranch::Legislative,
1039 severity: RuleSeverity::Blocking,
1040 active: true,
1041 reference_url: Some("https://example.com".into()),
1042 sop_category: Some("governance".into()),
1043 },
1044 GovernanceRule {
1045 id: "SOP-J001".into(),
1046 description: "test".into(),
1047 branch: GovernanceBranch::Judicial,
1048 severity: RuleSeverity::Blocking,
1049 active: true,
1050 reference_url: Some("https://example.com".into()),
1051 sop_category: Some("ethics".into()),
1052 },
1053 GovernanceRule {
1054 id: "GOV-001".into(),
1055 description: "test".into(),
1056 branch: GovernanceBranch::Judicial,
1057 severity: RuleSeverity::Blocking,
1058 active: true,
1059 reference_url: None,
1060 sop_category: None,
1061 },
1062 ];
1063 let ethics = GovernanceRule::filter_by_category(&rules, "ethics");
1064 assert_eq!(ethics.len(), 1);
1065 assert_eq!(ethics[0].id, "SOP-J001");
1066
1067 let governance = GovernanceRule::filter_by_category(&rules, "governance");
1068 assert_eq!(governance.len(), 1);
1069
1070 let none = GovernanceRule::filter_by_category(&rules, "nonexistent");
1071 assert!(none.is_empty());
1072 }
1073
1074 #[test]
1075 fn governance_rule_with_sop_serde() {
1076 let rule = GovernanceRule {
1077 id: "SOP-L001".into(),
1078 description: "test".into(),
1079 branch: GovernanceBranch::Legislative,
1080 severity: RuleSeverity::Blocking,
1081 active: true,
1082 reference_url: Some("https://example.com/sop".into()),
1083 sop_category: Some("governance".into()),
1084 };
1085 let json = serde_json::to_string(&rule).unwrap();
1086 assert!(json.contains("reference_url"));
1087 assert!(json.contains("sop_category"));
1088 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
1089 assert_eq!(restored.reference_url, Some("https://example.com/sop".into()));
1090 }
1091
1092 #[test]
1093 fn governance_rule_without_sop_backward_compat() {
1094 let json = r#"{"id":"GOV-001","description":"test","branch":"Judicial","severity":"Blocking","active":true}"#;
1096 let rule: GovernanceRule = serde_json::from_str(json).unwrap();
1097 assert!(rule.reference_url.is_none());
1098 assert!(rule.sop_category.is_none());
1099 }
1100
1101 fn make_sop_rule(
1106 id: &str,
1107 severity: RuleSeverity,
1108 branch: GovernanceBranch,
1109 category: Option<&str>,
1110 ) -> GovernanceRule {
1111 GovernanceRule {
1112 id: id.into(),
1113 description: format!("Genesis rule {id}"),
1114 branch,
1115 severity,
1116 active: true,
1117 reference_url: None,
1118 sop_category: category.map(|c| c.into()),
1119 }
1120 }
1121
1122 fn genesis_engine() -> GovernanceEngine {
1124 let mut engine = GovernanceEngine::new(0.7, false);
1125
1126 engine.add_rule(make_sop_rule("GOV-001", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
1129 engine.add_rule(make_sop_rule("GOV-002", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
1130 engine.add_rule(make_sop_rule("GOV-003", RuleSeverity::Warning, GovernanceBranch::Legislative, None));
1132 engine.add_rule(make_sop_rule("GOV-004", RuleSeverity::Advisory, GovernanceBranch::Executive, None));
1134 engine.add_rule(make_sop_rule("GOV-005", RuleSeverity::Warning, GovernanceBranch::Legislative, None));
1136 engine.add_rule(make_sop_rule("GOV-006", RuleSeverity::Blocking, GovernanceBranch::Executive, None));
1138 engine.add_rule(make_sop_rule("GOV-007", RuleSeverity::Advisory, GovernanceBranch::Judicial, None));
1140
1141 engine.add_rule(make_sop_rule("SOP-L001", RuleSeverity::Blocking, GovernanceBranch::Legislative, Some("governance")));
1143 engine.add_rule(make_sop_rule("SOP-L002", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
1144 engine.add_rule(make_sop_rule("SOP-L003", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("engineering")));
1145 engine.add_rule(make_sop_rule("SOP-L004", RuleSeverity::Advisory, GovernanceBranch::Legislative, Some("lifecycle")));
1146 engine.add_rule(make_sop_rule("SOP-L005", RuleSeverity::Blocking, GovernanceBranch::Legislative, Some("ethics")));
1147 engine.add_rule(make_sop_rule("SOP-L006", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
1148
1149 engine.add_rule(make_sop_rule("SOP-E001", RuleSeverity::Warning, GovernanceBranch::Executive, Some("engineering")));
1151 engine.add_rule(make_sop_rule("SOP-E002", RuleSeverity::Blocking, GovernanceBranch::Executive, Some("lifecycle")));
1152 engine.add_rule(make_sop_rule("SOP-E003", RuleSeverity::Warning, GovernanceBranch::Executive, Some("security")));
1153 engine.add_rule(make_sop_rule("SOP-E004", RuleSeverity::Advisory, GovernanceBranch::Executive, Some("lifecycle")));
1154 engine.add_rule(make_sop_rule("SOP-E005", RuleSeverity::Advisory, GovernanceBranch::Executive, Some("governance")));
1155
1156 engine.add_rule(make_sop_rule("SOP-J001", RuleSeverity::Blocking, GovernanceBranch::Judicial, Some("ethics")));
1158 engine.add_rule(make_sop_rule("SOP-J002", RuleSeverity::Warning, GovernanceBranch::Judicial, Some("ethics")));
1159 engine.add_rule(make_sop_rule("SOP-J003", RuleSeverity::Warning, GovernanceBranch::Judicial, Some("lifecycle")));
1160 engine.add_rule(make_sop_rule("SOP-J004", RuleSeverity::Advisory, GovernanceBranch::Judicial, Some("quality")));
1161
1162 engine
1163 }
1164
1165 #[test]
1166 fn genesis_has_22_rules() {
1167 let engine = genesis_engine();
1168 assert_eq!(engine.rule_count(), 22);
1169 }
1170
1171 #[test]
1172 fn genesis_high_risk_operation_blocked() {
1173 let engine = genesis_engine();
1174 let request = GovernanceRequest {
1175 agent_id: "agent-1".into(),
1176 action: "deploy-to-prod".into(),
1177 effect: EffectVector { risk: 0.9, ..Default::default() },
1178 context: Default::default(),
1179 node_id: None,
1180 };
1181 let result = engine.evaluate(&request);
1182 assert!(
1183 matches!(result.decision, GovernanceDecision::Deny(_)),
1184 "high-risk operation should be denied, got {:?}", result.decision,
1185 );
1186 assert!(result.threshold_exceeded);
1187 }
1188
1189 #[test]
1190 fn genesis_low_risk_operation_permitted() {
1191 let engine = genesis_engine();
1192 let request = GovernanceRequest {
1193 agent_id: "agent-1".into(),
1194 action: "read-file".into(),
1195 effect: EffectVector { risk: 0.1, ..Default::default() },
1196 context: Default::default(),
1197 node_id: None,
1198 };
1199 let result = engine.evaluate(&request);
1200 assert_eq!(result.decision, GovernanceDecision::Permit);
1201 assert!(!result.threshold_exceeded);
1202 }
1203
1204 #[test]
1205 fn genesis_privacy_violation_triggers_enforcement() {
1206 let engine = genesis_engine();
1207 let request = GovernanceRequest {
1208 agent_id: "agent-1".into(),
1209 action: "access-user-data".into(),
1210 effect: EffectVector { privacy: 0.8, ..Default::default() },
1211 context: Default::default(),
1212 node_id: None,
1213 };
1214 let result = engine.evaluate(&request);
1215 assert!(
1217 matches!(result.decision, GovernanceDecision::Deny(_) | GovernanceDecision::PermitWithWarning(_)),
1218 "privacy violation should trigger warning or deny, got {:?}", result.decision,
1219 );
1220 assert!(result.threshold_exceeded);
1221 }
1222
1223 #[test]
1224 fn genesis_security_sensitive_blocked() {
1225 let engine = genesis_engine();
1227 let request = GovernanceRequest {
1228 agent_id: "agent-1".into(),
1229 action: "modify-firewall".into(),
1230 effect: EffectVector { security: 0.9, risk: 0.5, ..Default::default() },
1231 context: Default::default(),
1232 node_id: None,
1233 };
1234 let result = engine.evaluate(&request);
1235 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1237 }
1238
1239 #[test]
1240 fn genesis_fairness_bias_blocked() {
1241 let engine = genesis_engine();
1243 let request = GovernanceRequest {
1244 agent_id: "ml-agent".into(),
1245 action: "evaluate-candidate".into(),
1246 effect: EffectVector { fairness: 0.9, ..Default::default() },
1247 context: Default::default(),
1248 node_id: None,
1249 };
1250 let result = engine.evaluate(&request);
1251 assert!(
1252 matches!(result.decision, GovernanceDecision::Deny(_)),
1253 "fairness violation should be blocked by SOP-J001, got {:?}", result.decision,
1254 );
1255 }
1256
1257 #[test]
1258 fn genesis_agent_spawn_blocked_when_risky() {
1259 let engine = genesis_engine();
1261 let request = GovernanceRequest {
1262 agent_id: "orchestrator".into(),
1263 action: "agent.spawn".into(),
1264 effect: EffectVector { risk: 0.8, novelty: 0.5, ..Default::default() },
1265 context: Default::default(),
1266 node_id: None,
1267 };
1268 let result = engine.evaluate(&request);
1269 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1271 }
1272
1273 #[test]
1274 fn genesis_agent_spawn_permitted_when_safe() {
1275 let engine = genesis_engine();
1277 let request = GovernanceRequest {
1278 agent_id: "orchestrator".into(),
1279 action: "agent.spawn".into(),
1280 effect: EffectVector { risk: 0.1, ..Default::default() },
1281 context: Default::default(),
1282 node_id: None,
1283 };
1284 let result = engine.evaluate(&request);
1285 assert_eq!(result.decision, GovernanceDecision::Permit);
1286 }
1287
1288 #[test]
1289 fn genesis_data_protection_blocks_high_privacy() {
1290 let engine = genesis_engine();
1292 let request = GovernanceRequest {
1293 agent_id: "data-agent".into(),
1294 action: "export-pii".into(),
1295 effect: EffectVector { privacy: 0.9, risk: 0.3, ..Default::default() },
1296 context: Default::default(),
1297 node_id: None,
1298 };
1299 let result = engine.evaluate(&request);
1300 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1302 }
1303
1304 #[test]
1305 fn genesis_with_human_approval_escalates() {
1306 let mut engine = GovernanceEngine::new(0.7, true);
1308 engine.add_rule(make_sop_rule("GOV-001", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
1309
1310 let request = GovernanceRequest {
1311 agent_id: "agent-1".into(),
1312 action: "high-risk-op".into(),
1313 effect: EffectVector { risk: 0.9, ..Default::default() },
1314 context: Default::default(),
1315 node_id: None,
1316 };
1317 let result = engine.evaluate(&request);
1318 assert!(
1319 matches!(result.decision, GovernanceDecision::EscalateToHuman(_)),
1320 "with human_approval, blocking should escalate, got {:?}", result.decision,
1321 );
1322 }
1323
1324 #[test]
1325 fn genesis_all_branches_represented() {
1326 let engine = genesis_engine();
1327 let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
1328 let executive = engine.rules_by_branch(&GovernanceBranch::Executive);
1329 let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
1330
1331 assert_eq!(legislative.len(), 8, "legislative should have 8 rules, got {}", legislative.len());
1333 assert_eq!(executive.len(), 7, "executive should have 7 rules, got {}", executive.len());
1335 assert_eq!(judicial.len(), 7, "judicial should have 7 rules, got {}", judicial.len());
1337 }
1338
1339 #[test]
1340 fn genesis_blocking_rules_count() {
1341 let engine = genesis_engine();
1342 let blocking_count = engine.active_rules().iter()
1343 .filter(|r| matches!(r.severity, RuleSeverity::Blocking | RuleSeverity::Critical))
1344 .count();
1345 assert_eq!(blocking_count, 7, "should have exactly 7 blocking rules");
1347 }
1348
1349 #[test]
1350 fn genesis_warning_rules_count() {
1351 let engine = genesis_engine();
1352 let warning_count = engine.active_rules().iter()
1353 .filter(|r| matches!(r.severity, RuleSeverity::Warning))
1354 .count();
1355 assert_eq!(warning_count, 9, "should have exactly 9 warning rules");
1358 }
1359
1360 #[test]
1361 fn genesis_advisory_rules_count() {
1362 let engine = genesis_engine();
1363 let advisory_count = engine.active_rules().iter()
1364 .filter(|r| matches!(r.severity, RuleSeverity::Advisory))
1365 .count();
1366 assert_eq!(advisory_count, 6, "should have exactly 6 advisory rules");
1368 }
1369
1370 #[test]
1371 fn genesis_moderate_risk_with_only_warnings_permits_with_warning() {
1372 let mut engine = GovernanceEngine::new(0.7, false);
1374 engine.add_rule(make_sop_rule("SOP-L002", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
1375 engine.add_rule(make_sop_rule("SOP-E001", RuleSeverity::Warning, GovernanceBranch::Executive, Some("engineering")));
1376
1377 let request = GovernanceRequest {
1378 agent_id: "dev-agent".into(),
1379 action: "write-code".into(),
1380 effect: EffectVector { risk: 0.5, novelty: 0.5, ..Default::default() },
1381 context: Default::default(),
1382 node_id: None,
1383 };
1384 let result = engine.evaluate(&request);
1385 assert!(
1387 matches!(result.decision, GovernanceDecision::PermitWithWarning(_)),
1388 "warning-only rules above threshold should PermitWithWarning, got {:?}", result.decision,
1389 );
1390 }
1391
1392 #[test]
1393 fn genesis_advisory_only_permits_above_threshold() {
1394 let mut engine = GovernanceEngine::new(0.7, false);
1396 engine.add_rule(make_sop_rule("GOV-004", RuleSeverity::Advisory, GovernanceBranch::Executive, None));
1397 engine.add_rule(make_sop_rule("GOV-007", RuleSeverity::Advisory, GovernanceBranch::Judicial, None));
1398
1399 let request = GovernanceRequest {
1400 agent_id: "agent-1".into(),
1401 action: "novel-action".into(),
1402 effect: EffectVector { novelty: 0.9, ..Default::default() },
1403 context: Default::default(),
1404 node_id: None,
1405 };
1406 let result = engine.evaluate(&request);
1407 assert_eq!(result.decision, GovernanceDecision::Permit,
1408 "advisory-only rules should still permit, got {:?}", result.decision);
1409 assert!(result.threshold_exceeded);
1410 }
1411
1412 #[test]
1413 fn genesis_evaluates_all_22_rules() {
1414 let engine = genesis_engine();
1415 let request = GovernanceRequest {
1416 agent_id: "agent-1".into(),
1417 action: "any-action".into(),
1418 effect: EffectVector { risk: 0.9, ..Default::default() },
1419 context: Default::default(),
1420 node_id: None,
1421 };
1422 let result = engine.evaluate(&request);
1423 assert_eq!(
1424 result.evaluated_rules.len(), 22,
1425 "all 22 rules should be evaluated, got {}", result.evaluated_rules.len(),
1426 );
1427 }
1428
1429 #[test]
1430 fn genesis_sop_categories_present() {
1431 let engine = genesis_engine();
1432 let all_rules = engine.active_rules();
1433 let categorized: Vec<_> = all_rules.iter()
1434 .filter(|r| r.sop_category.is_some())
1435 .collect();
1436 assert_eq!(categorized.len(), 15, "15 SOP rules should have categories");
1438
1439 let categories: std::collections::HashSet<_> = categorized.iter()
1440 .map(|r| r.sop_category.as_deref().unwrap())
1441 .collect();
1442 assert!(categories.contains("governance"));
1443 assert!(categories.contains("ethics"));
1444 assert!(categories.contains("engineering"));
1445 assert!(categories.contains("lifecycle"));
1446 assert!(categories.contains("security"));
1447 assert!(categories.contains("quality"));
1448 }
1449
1450 #[test]
1451 fn genesis_multi_dimension_high_effect_denied() {
1452 let engine = genesis_engine();
1454 let request = GovernanceRequest {
1455 agent_id: "agent-1".into(),
1456 action: "risky-novel-private".into(),
1457 effect: EffectVector {
1458 risk: 0.4,
1459 privacy: 0.4,
1460 novelty: 0.4,
1461 security: 0.4,
1462 fairness: 0.0,
1463 },
1464 context: Default::default(),
1465 node_id: None,
1466 };
1467 let result = engine.evaluate(&request);
1468 assert!(
1470 matches!(result.decision, GovernanceDecision::Deny(_)),
1471 "combined multi-dimension effect should exceed threshold and deny, got {:?}",
1472 result.decision,
1473 );
1474 assert!(result.threshold_exceeded);
1475 }
1476
1477 fn make_env(class: crate::environment::EnvironmentClass, risk_threshold: f64) -> crate::environment::Environment {
1480 crate::environment::Environment {
1481 id: format!("{class}"),
1482 name: format!("{class}"),
1483 class,
1484 governance: crate::environment::GovernanceScope {
1485 risk_threshold,
1486 ..Default::default()
1487 },
1488 labels: std::collections::HashMap::new(),
1489 }
1490 }
1491
1492 #[test]
1493 fn dev_environment_allows_higher_risk() {
1494 use crate::environment::EnvironmentClass;
1495
1496 let engine = GovernanceEngine::open();
1499
1500 let dev_env = make_env(EnvironmentClass::Development, 0.9);
1502
1503 let request = GovernanceRequest::new("agent-1", "deploy")
1505 .with_effect(EffectVector { risk: 0.8, ..Default::default() });
1506
1507 let result = engine.evaluate_in_environment(&request, &dev_env);
1508 assert_eq!(
1510 result.decision,
1511 GovernanceDecision::Permit,
1512 "dev should allow risk 0.8 with threshold 0.9, got {:?}",
1513 result.decision,
1514 );
1515 }
1516
1517 #[test]
1518 fn prod_environment_blocks_high_risk() {
1519 use crate::environment::EnvironmentClass;
1520
1521 let mut engine = GovernanceEngine::new(1.0, false);
1522 engine.add_rule(make_rule(
1523 "sec-check",
1524 RuleSeverity::Blocking,
1525 GovernanceBranch::Judicial,
1526 ));
1527
1528 let prod_env = make_env(EnvironmentClass::Production, 0.6);
1530
1531 let request = GovernanceRequest::new("agent-1", "deploy")
1533 .with_effect(EffectVector { risk: 0.4, ..Default::default() });
1534
1535 let result = engine.evaluate_in_environment(&request, &prod_env);
1536 assert!(
1538 matches!(result.decision, GovernanceDecision::Deny(_)),
1539 "prod should deny risk 0.4 with adjusted threshold 0.3, got {:?}",
1540 result.decision,
1541 );
1542 }
1543
1544 #[test]
1545 fn staging_uses_normal_thresholds() {
1546 use crate::environment::EnvironmentClass;
1547
1548 let mut engine = GovernanceEngine::new(1.0, false);
1549 engine.add_rule(make_rule(
1550 "sec-check",
1551 RuleSeverity::Blocking,
1552 GovernanceBranch::Judicial,
1553 ));
1554
1555 let staging_env = make_env(EnvironmentClass::Staging, 0.6);
1557
1558 let request = GovernanceRequest::new("agent-1", "test-deploy")
1560 .with_effect(EffectVector { risk: 0.5, ..Default::default() });
1561
1562 let result = engine.evaluate_in_environment(&request, &staging_env);
1563 assert_eq!(
1565 result.decision,
1566 GovernanceDecision::Permit,
1567 "staging should allow risk 0.5 with threshold 0.6, got {:?}",
1568 result.decision,
1569 );
1570
1571 let request2 = GovernanceRequest::new("agent-1", "test-deploy")
1573 .with_effect(EffectVector { risk: 0.7, ..Default::default() });
1574 let result2 = engine.evaluate_in_environment(&request2, &staging_env);
1575 assert!(
1577 matches!(result2.decision, GovernanceDecision::Deny(_)),
1578 "staging should deny risk 0.7 with threshold 0.6, got {:?}",
1579 result2.decision,
1580 );
1581 }
1582
1583 #[test]
1584 fn environment_governance_same_request_different_envs() {
1585 use crate::environment::EnvironmentClass;
1586
1587 let engine = GovernanceEngine::open();
1588
1589 let dev = make_env(EnvironmentClass::Development, 0.9);
1590 let prod = make_env(EnvironmentClass::Production, 0.6);
1591
1592 let request = GovernanceRequest::new("agent-1", "write-file")
1594 .with_effect(EffectVector { risk: 0.5, ..Default::default() });
1595
1596 let dev_result = engine.evaluate_in_environment(&request, &dev);
1597 let prod_result = engine.evaluate_in_environment(&request, &prod);
1598
1599 assert_eq!(dev_result.decision, GovernanceDecision::Permit);
1601 assert!(matches!(prod_result.decision, GovernanceDecision::Deny(_)));
1603 }
1604
1605 #[test]
1608 fn trajectory_records_and_retrieves() {
1609 use chrono::Utc;
1610 let mut recorder = TrajectoryRecorder::new(100);
1611 recorder.record(TrajectoryRecord {
1612 agent_id: "agent-1".into(),
1613 action: "tool.execute".into(),
1614 context: serde_json::json!({"tool": "read_file"}),
1615 outcome: TrajectoryOutcome::Success { reward: 1.0 },
1616 timestamp: Utc::now(),
1617 });
1618 assert_eq!(recorder.len(), 1);
1619 assert!(!recorder.is_empty());
1620 assert_eq!(recorder.agent_trajectory("agent-1").len(), 1);
1621 assert_eq!(recorder.agent_trajectory("agent-2").len(), 0);
1622 }
1623
1624 #[test]
1625 fn trajectory_extracts_patterns() {
1626 use chrono::Utc;
1627 let mut recorder = TrajectoryRecorder::new(100);
1628
1629 for _ in 0..5 {
1630 recorder.record(TrajectoryRecord {
1631 agent_id: "a".into(),
1632 action: "tool.execute".into(),
1633 context: serde_json::json!({}),
1634 outcome: TrajectoryOutcome::Success { reward: 1.0 },
1635 timestamp: Utc::now(),
1636 });
1637 }
1638 for _ in 0..3 {
1639 recorder.record(TrajectoryRecord {
1640 agent_id: "a".into(),
1641 action: "tool.read".into(),
1642 context: serde_json::json!({}),
1643 outcome: TrajectoryOutcome::Success { reward: 0.5 },
1644 timestamp: Utc::now(),
1645 });
1646 }
1647 recorder.record(TrajectoryRecord {
1648 agent_id: "a".into(),
1649 action: "tool.fail".into(),
1650 context: serde_json::json!({}),
1651 outcome: TrajectoryOutcome::Failure {
1652 reason: "err".into(),
1653 },
1654 timestamp: Utc::now(),
1655 });
1656
1657 let patterns = recorder.extract_patterns();
1658 assert_eq!(patterns[0].0, "tool.execute");
1659 assert_eq!(patterns[0].1, 5);
1660 assert_eq!(patterns[1].0, "tool.read");
1661 assert_eq!(patterns[1].1, 3);
1662 assert!(patterns.iter().all(|(a, _)| a != "tool.fail"));
1664 }
1665
1666 #[test]
1667 fn trajectory_max_records_eviction() {
1668 use chrono::Utc;
1669 let mut recorder = TrajectoryRecorder::new(3);
1670 for i in 0..5 {
1671 recorder.record(TrajectoryRecord {
1672 agent_id: format!("agent-{i}"),
1673 action: "act".into(),
1674 context: serde_json::json!({}),
1675 outcome: TrajectoryOutcome::Pending,
1676 timestamp: Utc::now(),
1677 });
1678 }
1679 assert_eq!(recorder.len(), 3);
1680 assert!(recorder.agent_trajectory("agent-0").is_empty());
1682 assert!(recorder.agent_trajectory("agent-1").is_empty());
1683 assert_eq!(recorder.agent_trajectory("agent-2").len(), 1);
1684 assert_eq!(recorder.agent_trajectory("agent-4").len(), 1);
1685 }
1686
1687 #[test]
1688 fn trajectory_empty_patterns() {
1689 let recorder = TrajectoryRecorder::new(10);
1690 assert!(recorder.is_empty());
1691 assert!(recorder.extract_patterns().is_empty());
1692 }
1693
1694 #[test]
1697 fn governance_branch_serde_roundtrip() {
1698 for branch in [
1699 GovernanceBranch::Legislative,
1700 GovernanceBranch::Executive,
1701 GovernanceBranch::Judicial,
1702 ] {
1703 let json = serde_json::to_string(&branch).unwrap();
1704 let restored: GovernanceBranch = serde_json::from_str(&json).unwrap();
1705 assert_eq!(restored, branch);
1706 }
1707 }
1708
1709 #[test]
1710 fn rule_severity_serde_roundtrip() {
1711 for severity in [
1712 RuleSeverity::Advisory,
1713 RuleSeverity::Warning,
1714 RuleSeverity::Blocking,
1715 RuleSeverity::Critical,
1716 ] {
1717 let json = serde_json::to_string(&severity).unwrap();
1718 let restored: RuleSeverity = serde_json::from_str(&json).unwrap();
1719 assert_eq!(restored, severity);
1720 }
1721 }
1722
1723 #[test]
1724 fn rule_severity_display() {
1725 assert_eq!(RuleSeverity::Advisory.to_string(), "advisory");
1726 assert_eq!(RuleSeverity::Warning.to_string(), "warning");
1727 assert_eq!(RuleSeverity::Blocking.to_string(), "blocking");
1728 assert_eq!(RuleSeverity::Critical.to_string(), "critical");
1729 }
1730
1731 #[test]
1732 fn governance_decision_serde_roundtrip_all_variants() {
1733 let variants = vec![
1734 GovernanceDecision::Permit,
1735 GovernanceDecision::PermitWithWarning("low risk".into()),
1736 GovernanceDecision::EscalateToHuman("needs review".into()),
1737 GovernanceDecision::Deny("policy violation".into()),
1738 ];
1739 for decision in variants {
1740 let json = serde_json::to_string(&decision).unwrap();
1741 let restored: GovernanceDecision = serde_json::from_str(&json).unwrap();
1742 assert_eq!(restored, decision);
1743 }
1744 }
1745
1746 #[test]
1747 fn governance_result_serde_roundtrip() {
1748 let result = GovernanceResult {
1749 decision: GovernanceDecision::Permit,
1750 evaluated_rules: vec!["rule-1".into(), "rule-2".into()],
1751 effect: EffectVector {
1752 risk: 0.3,
1753 ..Default::default()
1754 },
1755 threshold_exceeded: false,
1756 };
1757 let json = serde_json::to_string(&result).unwrap();
1758 let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
1759 assert_eq!(restored.decision, GovernanceDecision::Permit);
1760 assert_eq!(restored.evaluated_rules.len(), 2);
1761 assert!(!restored.threshold_exceeded);
1762 }
1763
1764 #[test]
1765 fn governance_result_deny_serde_roundtrip() {
1766 let result = GovernanceResult {
1767 decision: GovernanceDecision::Deny("threshold exceeded".into()),
1768 evaluated_rules: vec![],
1769 effect: EffectVector {
1770 risk: 0.9,
1771 security: 0.8,
1772 ..Default::default()
1773 },
1774 threshold_exceeded: true,
1775 };
1776 let json = serde_json::to_string(&result).unwrap();
1777 let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
1778 assert!(restored.threshold_exceeded);
1779 assert!(matches!(restored.decision, GovernanceDecision::Deny(_)));
1780 }
1781
1782 #[test]
1783 fn trajectory_outcome_serde_roundtrip() {
1784 let variants: Vec<TrajectoryOutcome> = vec![
1785 TrajectoryOutcome::Success { reward: 1.5 },
1786 TrajectoryOutcome::Failure {
1787 reason: "timeout".into(),
1788 },
1789 TrajectoryOutcome::Pending,
1790 ];
1791 for outcome in variants {
1792 let json = serde_json::to_string(&outcome).unwrap();
1793 let _restored: TrajectoryOutcome = serde_json::from_str(&json).unwrap();
1794 }
1795 }
1796
1797 #[test]
1798 fn trajectory_record_serde_roundtrip() {
1799 use chrono::Utc;
1800 let record = TrajectoryRecord {
1801 agent_id: "agent-1".into(),
1802 action: "tool.execute".into(),
1803 context: serde_json::json!({"tool": "read_file", "path": "/etc/hosts"}),
1804 outcome: TrajectoryOutcome::Success { reward: 1.0 },
1805 timestamp: Utc::now(),
1806 };
1807 let json = serde_json::to_string(&record).unwrap();
1808 let restored: TrajectoryRecord = serde_json::from_str(&json).unwrap();
1809 assert_eq!(restored.agent_id, "agent-1");
1810 assert_eq!(restored.action, "tool.execute");
1811 }
1812
1813 #[test]
1814 fn governance_rule_with_sop_fields_roundtrip() {
1815 let rule = GovernanceRule {
1816 id: "SOP-001".into(),
1817 description: "Do not access prod data".into(),
1818 branch: GovernanceBranch::Legislative,
1819 severity: RuleSeverity::Critical,
1820 active: true,
1821 reference_url: Some("https://sops.example.com/001".into()),
1822 sop_category: Some("data-access".into()),
1823 };
1824 let json = serde_json::to_string(&rule).unwrap();
1825 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
1826 assert_eq!(restored.reference_url.unwrap(), "https://sops.example.com/001");
1827 assert_eq!(restored.sop_category.unwrap(), "data-access");
1828 }
1829
1830 #[test]
1831 fn effect_vector_default_is_zero() {
1832 let v = EffectVector::default();
1833 assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
1834 assert!(!v.any_exceeds(0.0));
1835 }
1836
1837 #[test]
1838 fn effect_vector_max_dimension() {
1839 let v = EffectVector {
1840 risk: 0.1,
1841 fairness: 0.5,
1842 privacy: 0.3,
1843 novelty: 0.9,
1844 security: 0.2,
1845 };
1846 assert!((v.max_dimension() - 0.9).abs() < f64::EPSILON);
1847 }
1848
1849 #[cfg(feature = "exochain")]
1850 mod rvf_bridge_tests {
1851 use super::*;
1852
1853 #[test]
1854 fn decision_to_policy_check() {
1855 use rvf_types::witness::PolicyCheck;
1856
1857 assert_eq!(
1858 GovernanceDecision::Permit.to_rvf_policy_check(),
1859 PolicyCheck::Allowed,
1860 );
1861 assert_eq!(
1862 GovernanceDecision::PermitWithWarning("low risk".into()).to_rvf_policy_check(),
1863 PolicyCheck::Confirmed,
1864 );
1865 assert_eq!(
1866 GovernanceDecision::EscalateToHuman("needs review".into()).to_rvf_policy_check(),
1867 PolicyCheck::Confirmed,
1868 );
1869 assert_eq!(
1870 GovernanceDecision::Deny("blocked".into()).to_rvf_policy_check(),
1871 PolicyCheck::Denied,
1872 );
1873 }
1874
1875 #[test]
1876 fn open_engine_maps_to_autonomous() {
1877 use rvf_types::witness::GovernanceMode;
1878
1879 let engine = GovernanceEngine::open();
1880 assert_eq!(engine.to_rvf_mode(), GovernanceMode::Autonomous);
1881 }
1882
1883 #[test]
1884 fn strict_engine_maps_to_restricted() {
1885 use rvf_types::witness::GovernanceMode;
1886
1887 let engine = GovernanceEngine::new(0.5, false);
1888 assert_eq!(engine.to_rvf_mode(), GovernanceMode::Restricted);
1889 }
1890
1891 #[test]
1892 fn human_approval_maps_to_approved() {
1893 use rvf_types::witness::GovernanceMode;
1894
1895 let engine = GovernanceEngine::new(0.5, true);
1896 assert_eq!(engine.to_rvf_mode(), GovernanceMode::Approved);
1897 }
1898
1899 #[test]
1900 fn to_rvf_policy_mode_matches() {
1901 use rvf_types::witness::GovernanceMode;
1902
1903 let open = GovernanceEngine::open();
1904 let policy = open.to_rvf_policy();
1905 assert_eq!(policy.mode, GovernanceMode::Autonomous);
1906
1907 let strict = GovernanceEngine::new(0.3, false);
1908 let policy = strict.to_rvf_policy();
1909 assert_eq!(policy.mode, GovernanceMode::Restricted);
1910
1911 let human = GovernanceEngine::new(0.3, true);
1912 let policy = human.to_rvf_policy();
1913 assert_eq!(policy.mode, GovernanceMode::Approved);
1914 }
1915
1916 #[test]
1917 fn governance_result_to_task_outcome() {
1918 use rvf_types::witness::TaskOutcome;
1919
1920 let permit_result = GovernanceResult {
1921 decision: GovernanceDecision::Permit,
1922 evaluated_rules: vec![],
1923 effect: EffectVector::default(),
1924 threshold_exceeded: false,
1925 };
1926 assert_eq!(permit_result.to_rvf_task_outcome() as u8, TaskOutcome::Solved as u8);
1927
1928 let deny_result = GovernanceResult {
1929 decision: GovernanceDecision::Deny("blocked".into()),
1930 evaluated_rules: vec![],
1931 effect: EffectVector::default(),
1932 threshold_exceeded: true,
1933 };
1934 assert_eq!(deny_result.to_rvf_task_outcome() as u8, TaskOutcome::Failed as u8);
1935
1936 let escalate_result = GovernanceResult {
1937 decision: GovernanceDecision::EscalateToHuman("review".into()),
1938 evaluated_rules: vec![],
1939 effect: EffectVector::default(),
1940 threshold_exceeded: true,
1941 };
1942 assert_eq!(escalate_result.to_rvf_task_outcome() as u8, TaskOutcome::Skipped as u8);
1943 }
1944 }
1945
1946 #[cfg(feature = "exochain")]
1949 mod chain_logging_tests {
1950 use super::*;
1951 use std::sync::Arc;
1952
1953 #[test]
1954 fn evaluate_logged_records_permit() {
1955 let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
1956 let initial_len = cm.len();
1957
1958 let mut engine = GovernanceEngine::new(0.5, false);
1959 engine.add_rule(GovernanceRule {
1960 id: "sec".into(),
1961 description: "test".into(),
1962 branch: GovernanceBranch::Judicial,
1963 severity: RuleSeverity::Blocking,
1964 active: true,
1965 reference_url: None,
1966 sop_category: None,
1967 });
1968
1969 let request = GovernanceRequest {
1970 agent_id: "agent-1".into(),
1971 action: "tool.read".into(),
1972 effect: EffectVector { risk: 0.1, ..Default::default() },
1973 context: Default::default(),
1974 node_id: None,
1975 };
1976
1977 let result = engine.evaluate_logged(&request, Some(&cm));
1978 assert!(matches!(result.decision, GovernanceDecision::Permit));
1979 assert_eq!(cm.len(), initial_len + 1);
1980
1981 let events = cm.tail(1);
1982 assert_eq!(events[0].kind, "governance.permit");
1983 assert_eq!(events[0].source, "governance");
1984 }
1985
1986 #[test]
1987 fn evaluate_logged_records_deny() {
1988 let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
1989
1990 let mut engine = GovernanceEngine::new(0.5, false);
1991 engine.add_rule(GovernanceRule {
1992 id: "sec".into(),
1993 description: "test".into(),
1994 branch: GovernanceBranch::Judicial,
1995 severity: RuleSeverity::Blocking,
1996 active: true,
1997 reference_url: None,
1998 sop_category: None,
1999 });
2000
2001 let request = GovernanceRequest {
2002 agent_id: "agent-1".into(),
2003 action: "tool.exec".into(),
2004 effect: EffectVector { risk: 0.9, ..Default::default() },
2005 context: Default::default(),
2006 node_id: None,
2007 };
2008
2009 let result = engine.evaluate_logged(&request, Some(&cm));
2010 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
2011
2012 let events = cm.tail(1);
2013 assert_eq!(events[0].kind, "governance.deny");
2014 let payload = events[0].payload.as_ref().unwrap();
2015 assert_eq!(payload["agent_id"], "agent-1");
2016 assert!(payload["threshold_exceeded"].as_bool().unwrap());
2017 }
2018
2019 #[test]
2020 fn evaluate_logged_without_chain_still_works() {
2021 let engine = GovernanceEngine::new(0.5, false);
2022 let request = GovernanceRequest {
2023 agent_id: "agent-1".into(),
2024 action: "tool.read".into(),
2025 effect: EffectVector::default(),
2026 context: Default::default(),
2027 node_id: None,
2028 };
2029
2030 let result = engine.evaluate_logged(&request, None);
2032 assert!(matches!(result.decision, GovernanceDecision::Permit));
2033 }
2034
2035 #[test]
2036 fn chain_log_result_standalone() {
2037 let cm = crate::chain::ChainManager::new(0, 100);
2038 let initial_len = cm.len();
2039
2040 let mut engine = GovernanceEngine::new(0.5, true);
2041 engine.add_rule(GovernanceRule {
2042 id: "sec".into(),
2043 description: "test".into(),
2044 branch: GovernanceBranch::Judicial,
2045 severity: RuleSeverity::Blocking,
2046 active: true,
2047 reference_url: None,
2048 sop_category: None,
2049 });
2050
2051 let request = GovernanceRequest {
2052 agent_id: "agent-1".into(),
2053 action: "tool.exec".into(),
2054 effect: EffectVector { risk: 0.8, ..Default::default() },
2055 context: Default::default(),
2056 node_id: None,
2057 };
2058
2059 let result = engine.evaluate(&request);
2060 GovernanceEngine::chain_log_result(&cm, &request, &result);
2061 assert_eq!(cm.len(), initial_len + 1);
2062
2063 let events = cm.tail(1);
2064 assert_eq!(events[0].kind, "governance.defer");
2065 }
2066 }
2067}