use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::environment::{Environment, EnvironmentClass};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceRule {
pub id: String,
pub description: String,
pub branch: GovernanceBranch,
pub severity: RuleSeverity,
#[serde(default = "default_true")]
pub active: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reference_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sop_category: Option<String>,
}
impl GovernanceRule {
pub fn filter_by_category<'a>(rules: &'a [GovernanceRule], category: &str) -> Vec<&'a GovernanceRule> {
rules.iter().filter(|r| r.sop_category.as_deref() == Some(category)).collect()
}
}
fn default_true() -> bool {
true
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GovernanceBranch {
Legislative,
Executive,
Judicial,
}
impl std::fmt::Display for GovernanceBranch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GovernanceBranch::Legislative => write!(f, "legislative"),
GovernanceBranch::Executive => write!(f, "executive"),
GovernanceBranch::Judicial => write!(f, "judicial"),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RuleSeverity {
Advisory,
Warning,
Blocking,
Critical,
}
impl std::fmt::Display for RuleSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuleSeverity::Advisory => write!(f, "advisory"),
RuleSeverity::Warning => write!(f, "warning"),
RuleSeverity::Blocking => write!(f, "blocking"),
RuleSeverity::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EffectVector {
#[serde(default)]
pub risk: f64,
#[serde(default)]
pub fairness: f64,
#[serde(default)]
pub privacy: f64,
#[serde(default)]
pub novelty: f64,
#[serde(default)]
pub security: f64,
}
impl EffectVector {
pub fn magnitude(&self) -> f64 {
(self.risk * self.risk
+ self.fairness * self.fairness
+ self.privacy * self.privacy
+ self.novelty * self.novelty
+ self.security * self.security)
.sqrt()
}
pub fn any_exceeds(&self, threshold: f64) -> bool {
self.risk > threshold
|| self.fairness > threshold
|| self.privacy > threshold
|| self.novelty > threshold
|| self.security > threshold
}
pub fn max_dimension(&self) -> f64 {
[
self.risk,
self.fairness,
self.privacy,
self.novelty,
self.security,
]
.into_iter()
.fold(0.0_f64, f64::max)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GovernanceDecision {
Permit,
PermitWithWarning(String),
EscalateToHuman(String),
Deny(String),
}
impl std::fmt::Display for GovernanceDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GovernanceDecision::Permit => write!(f, "permit"),
GovernanceDecision::PermitWithWarning(msg) => write!(f, "permit (warning: {msg})"),
GovernanceDecision::EscalateToHuman(msg) => write!(f, "escalate ({msg})"),
GovernanceDecision::Deny(reason) => write!(f, "deny: {reason}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceRequest {
pub agent_id: String,
pub action: String,
#[serde(default)]
pub effect: EffectVector,
#[serde(default)]
pub context: std::collections::HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_id: Option<String>,
}
impl GovernanceRequest {
pub fn new(agent_id: impl Into<String>, action: impl Into<String>) -> Self {
Self {
agent_id: agent_id.into(),
action: action.into(),
effect: EffectVector::default(),
context: std::collections::HashMap::new(),
node_id: None,
}
}
pub fn with_node_id(mut self, node_id: impl Into<String>) -> Self {
self.node_id = Some(node_id.into());
self
}
pub fn with_effect(mut self, effect: EffectVector) -> Self {
self.effect = effect;
self
}
pub fn with_tool_context(
mut self,
tool_name: impl Into<String>,
gate_action: impl Into<String>,
effect: EffectVector,
pid: u64,
) -> Self {
self.context.insert("tool".into(), tool_name.into());
self.context.insert("gate_action".into(), gate_action.into());
self.context.insert("pid".into(), pid.to_string());
self.effect = effect;
self
}
pub fn with_context_entry(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.context.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceResult {
pub decision: GovernanceDecision,
pub evaluated_rules: Vec<String>,
pub effect: EffectVector,
pub threshold_exceeded: bool,
}
pub struct GovernanceEngine {
rules: Vec<GovernanceRule>,
risk_threshold: f64,
human_approval_required: bool,
}
impl GovernanceEngine {
pub fn new(risk_threshold: f64, human_approval_required: bool) -> Self {
Self {
rules: Vec::new(),
risk_threshold,
human_approval_required,
}
}
pub fn open() -> Self {
Self {
rules: Vec::new(),
risk_threshold: 1.0,
human_approval_required: false,
}
}
pub fn add_rule(&mut self, rule: GovernanceRule) {
debug!(rule_id = %rule.id, branch = %rule.branch, "adding governance rule");
self.rules.push(rule);
}
pub fn active_rules(&self) -> Vec<&GovernanceRule> {
self.rules.iter().filter(|r| r.active).collect()
}
pub fn rules_by_branch(&self, branch: &GovernanceBranch) -> Vec<&GovernanceRule> {
self.rules
.iter()
.filter(|r| r.active && &r.branch == branch)
.collect()
}
pub fn evaluate(&self, request: &GovernanceRequest) -> GovernanceResult {
let magnitude = request.effect.magnitude();
let threshold_exceeded = magnitude > self.risk_threshold;
let mut evaluated_rules = Vec::new();
let mut has_warning = false;
let mut has_blocking = false;
let mut blocking_reason = String::new();
for rule in self.active_rules() {
evaluated_rules.push(rule.id.clone());
match rule.severity {
RuleSeverity::Blocking | RuleSeverity::Critical => {
if threshold_exceeded {
has_blocking = true;
blocking_reason = format!(
"rule '{}': effect magnitude {magnitude:.2} > threshold {:.2}",
rule.id, self.risk_threshold
);
}
}
RuleSeverity::Warning => {
if threshold_exceeded {
has_warning = true;
}
}
RuleSeverity::Advisory => {}
}
}
let decision = if has_blocking {
if self.human_approval_required {
GovernanceDecision::EscalateToHuman(blocking_reason)
} else {
GovernanceDecision::Deny(blocking_reason)
}
} else if threshold_exceeded && has_warning {
GovernanceDecision::PermitWithWarning(format!(
"effect magnitude {magnitude:.2} approaches threshold {:.2}",
self.risk_threshold
))
} else {
GovernanceDecision::Permit
};
GovernanceResult {
decision,
evaluated_rules,
effect: request.effect.clone(),
threshold_exceeded,
}
}
pub fn risk_threshold(&self) -> f64 {
self.risk_threshold
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn evaluate_in_environment(
&self,
request: &GovernanceRequest,
env: &Environment,
) -> GovernanceResult {
let adjusted_threshold = match &env.class {
EnvironmentClass::Development => {
env.governance.risk_threshold
}
EnvironmentClass::Staging => {
env.governance.risk_threshold
}
EnvironmentClass::Production => {
env.governance.risk_threshold * 0.5
}
EnvironmentClass::Custom { risk_threshold, .. } => {
*risk_threshold
}
};
let mut result = self.evaluate(request);
let magnitude = request.effect.magnitude();
if magnitude > adjusted_threshold {
result.threshold_exceeded = true;
result.decision = GovernanceDecision::Deny(format!(
"effect magnitude {magnitude:.2} exceeds {} environment threshold {adjusted_threshold:.2}",
env.class,
));
}
result
}
#[cfg(feature = "exochain")]
pub fn evaluate_logged(
&self,
request: &GovernanceRequest,
chain: Option<&crate::chain::ChainManager>,
) -> GovernanceResult {
let result = self.evaluate(request);
if let Some(cm) = chain {
Self::chain_log_result(cm, request, &result);
}
result
}
#[cfg(feature = "exochain")]
pub fn evaluate_in_environment_logged(
&self,
request: &GovernanceRequest,
env: &Environment,
chain: Option<&crate::chain::ChainManager>,
) -> GovernanceResult {
let result = self.evaluate_in_environment(request, env);
if let Some(cm) = chain {
Self::chain_log_result(cm, request, &result);
}
result
}
#[cfg(feature = "exochain")]
pub fn chain_log_result(
cm: &crate::chain::ChainManager,
request: &GovernanceRequest,
result: &GovernanceResult,
) {
use crate::chain::{ChainLoggable, GovernanceDecisionEvent};
let decision_str = match &result.decision {
GovernanceDecision::Permit => "Permit".to_owned(),
GovernanceDecision::PermitWithWarning(_) => "PermitWithWarning".to_owned(),
GovernanceDecision::EscalateToHuman(_) => "EscalateToHuman".to_owned(),
GovernanceDecision::Deny(_) => "Deny".to_owned(),
};
let event = GovernanceDecisionEvent {
agent_id: request.agent_id.clone(),
action: request.action.clone(),
decision: decision_str,
effect_magnitude: request.effect.magnitude(),
threshold_exceeded: result.threshold_exceeded,
evaluated_rules: result.evaluated_rules.clone(),
timestamp: chrono::Utc::now(),
};
cm.append_loggable(&event);
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TrajectoryOutcome {
Success {
reward: f64,
},
Failure {
reason: String,
},
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectoryRecord {
pub agent_id: String,
pub action: String,
pub context: serde_json::Value,
pub outcome: TrajectoryOutcome,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
pub struct TrajectoryRecorder {
records: Vec<TrajectoryRecord>,
max_records: usize,
}
impl TrajectoryRecorder {
pub fn new(max_records: usize) -> Self {
Self {
records: Vec::new(),
max_records,
}
}
pub fn record(&mut self, record: TrajectoryRecord) {
if self.records.len() >= self.max_records {
self.records.remove(0); }
self.records.push(record);
}
pub fn agent_trajectory(&self, agent_id: &str) -> Vec<&TrajectoryRecord> {
self.records
.iter()
.filter(|r| r.agent_id == agent_id)
.collect()
}
pub fn extract_patterns(&self) -> Vec<(String, usize)> {
let mut action_counts: HashMap<String, usize> = HashMap::new();
for record in &self.records {
if matches!(record.outcome, TrajectoryOutcome::Success { .. }) {
*action_counts.entry(record.action.clone()).or_default() += 1;
}
}
let mut patterns: Vec<_> = action_counts.into_iter().collect();
patterns.sort_by(|a, b| b.1.cmp(&a.1));
patterns
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
#[cfg(feature = "exochain")]
impl GovernanceDecision {
pub fn to_rvf_policy_check(&self) -> rvf_types::witness::PolicyCheck {
match self {
GovernanceDecision::Permit => rvf_types::witness::PolicyCheck::Allowed,
GovernanceDecision::PermitWithWarning(_) => rvf_types::witness::PolicyCheck::Confirmed,
GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::PolicyCheck::Confirmed,
GovernanceDecision::Deny(_) => rvf_types::witness::PolicyCheck::Denied,
}
}
}
#[cfg(feature = "exochain")]
impl GovernanceEngine {
pub fn to_rvf_mode(&self) -> rvf_types::witness::GovernanceMode {
if self.risk_threshold >= 1.0 {
rvf_types::witness::GovernanceMode::Autonomous
} else if self.human_approval_required {
rvf_types::witness::GovernanceMode::Approved
} else {
rvf_types::witness::GovernanceMode::Restricted
}
}
pub fn to_rvf_policy(&self) -> rvf_runtime::GovernancePolicy {
match self.to_rvf_mode() {
rvf_types::witness::GovernanceMode::Restricted => {
rvf_runtime::GovernancePolicy::restricted()
}
rvf_types::witness::GovernanceMode::Approved => {
rvf_runtime::GovernancePolicy::approved()
}
rvf_types::witness::GovernanceMode::Autonomous => {
rvf_runtime::GovernancePolicy::autonomous()
}
}
}
}
#[cfg(feature = "exochain")]
impl GovernanceResult {
pub fn to_rvf_task_outcome(&self) -> rvf_types::witness::TaskOutcome {
match &self.decision {
GovernanceDecision::Permit | GovernanceDecision::PermitWithWarning(_) => {
rvf_types::witness::TaskOutcome::Solved
}
GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::TaskOutcome::Skipped,
GovernanceDecision::Deny(_) => rvf_types::witness::TaskOutcome::Failed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rule(id: &str, severity: RuleSeverity, branch: GovernanceBranch) -> GovernanceRule {
GovernanceRule {
id: id.into(),
description: format!("Test rule {id}"),
branch,
severity,
active: true,
reference_url: None,
sop_category: None,
}
}
#[test]
fn effect_vector_magnitude() {
let v = EffectVector {
risk: 0.3,
fairness: 0.4,
privacy: 0.0,
novelty: 0.0,
security: 0.0,
};
assert!((v.magnitude() - 0.5).abs() < 0.001);
}
#[test]
fn effect_vector_zero() {
let v = EffectVector::default();
assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn effect_any_exceeds() {
let v = EffectVector {
risk: 0.8,
..Default::default()
};
assert!(v.any_exceeds(0.5));
assert!(!v.any_exceeds(0.9));
}
#[test]
fn effect_max_dimension() {
let v = EffectVector {
risk: 0.2,
fairness: 0.5,
privacy: 0.3,
novelty: 0.1,
security: 0.4,
};
assert!((v.max_dimension() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn governance_branch_display() {
assert_eq!(GovernanceBranch::Legislative.to_string(), "legislative");
assert_eq!(GovernanceBranch::Executive.to_string(), "executive");
assert_eq!(GovernanceBranch::Judicial.to_string(), "judicial");
}
#[test]
fn rule_severity_ordering() {
assert!(RuleSeverity::Advisory < RuleSeverity::Warning);
assert!(RuleSeverity::Warning < RuleSeverity::Blocking);
assert!(RuleSeverity::Blocking < RuleSeverity::Critical);
}
#[test]
fn governance_decision_display() {
assert_eq!(GovernanceDecision::Permit.to_string(), "permit");
assert!(
GovernanceDecision::Deny("too risky".into())
.to_string()
.contains("too risky")
);
}
#[test]
fn open_engine_permits_everything() {
let engine = GovernanceEngine::open();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "deploy".into(),
effect: EffectVector {
risk: 0.9,
security: 0.9,
..Default::default()
},
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert_eq!(result.decision, GovernanceDecision::Permit);
}
#[test]
fn blocking_rule_denies() {
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(make_rule(
"security-check",
RuleSeverity::Blocking,
GovernanceBranch::Judicial,
));
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "deploy".into(),
effect: EffectVector {
risk: 0.6,
..Default::default()
},
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
assert!(result.threshold_exceeded);
}
#[test]
fn blocking_with_human_approval_escalates() {
let mut engine = GovernanceEngine::new(0.5, true);
engine.add_rule(make_rule(
"security-check",
RuleSeverity::Blocking,
GovernanceBranch::Judicial,
));
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "deploy".into(),
effect: EffectVector {
risk: 0.6,
..Default::default()
},
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(matches!(
result.decision,
GovernanceDecision::EscalateToHuman(_)
));
}
#[test]
fn warning_rule_permits_with_warning() {
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(make_rule(
"risk-check",
RuleSeverity::Warning,
GovernanceBranch::Executive,
));
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "deploy".into(),
effect: EffectVector {
risk: 0.6,
..Default::default()
},
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(matches!(
result.decision,
GovernanceDecision::PermitWithWarning(_)
));
}
#[test]
fn below_threshold_permits() {
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(make_rule(
"security-check",
RuleSeverity::Blocking,
GovernanceBranch::Judicial,
));
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "read".into(),
effect: EffectVector {
risk: 0.1,
..Default::default()
},
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert_eq!(result.decision, GovernanceDecision::Permit);
assert!(!result.threshold_exceeded);
}
#[test]
fn rules_by_branch() {
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(make_rule(
"r1",
RuleSeverity::Warning,
GovernanceBranch::Legislative,
));
engine.add_rule(make_rule(
"r2",
RuleSeverity::Blocking,
GovernanceBranch::Judicial,
));
engine.add_rule(make_rule(
"r3",
RuleSeverity::Advisory,
GovernanceBranch::Judicial,
));
let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
assert_eq!(judicial.len(), 2);
let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
assert_eq!(legislative.len(), 1);
}
#[test]
fn inactive_rules_excluded() {
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(GovernanceRule {
id: "disabled".into(),
description: "Disabled rule".into(),
branch: GovernanceBranch::Judicial,
severity: RuleSeverity::Blocking,
active: false,
reference_url: None,
sop_category: None,
});
assert_eq!(engine.active_rules().len(), 0);
}
#[test]
fn governance_rule_serde_roundtrip() {
let rule = make_rule("sec-1", RuleSeverity::Critical, GovernanceBranch::Judicial);
let json = serde_json::to_string(&rule).unwrap();
let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, "sec-1");
assert!(restored.active);
}
#[test]
fn governance_request_serde_roundtrip() {
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "deploy".into(),
effect: EffectVector {
risk: 0.5,
privacy: 0.3,
..Default::default()
},
context: std::collections::HashMap::from([("env".into(), "prod".into())]),
node_id: None,
};
let json = serde_json::to_string(&request).unwrap();
let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
assert_eq!(restored.agent_id, "agent-1");
assert!((restored.effect.risk - 0.5).abs() < f64::EPSILON);
assert!(restored.node_id.is_none());
}
#[test]
fn governance_request_with_node_id() {
let request = GovernanceRequest::new("agent-1", "deploy")
.with_node_id("node-42")
.with_effect(EffectVector { risk: 0.1, ..Default::default() });
assert_eq!(request.node_id.as_deref(), Some("node-42"));
assert_eq!(request.agent_id, "agent-1");
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("node-42"));
let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
assert_eq!(restored.node_id.as_deref(), Some("node-42"));
}
#[test]
fn governance_request_without_node_id_deserializes() {
let json = r#"{"agent_id":"a","action":"b","effect":{},"context":{}}"#;
let request: GovernanceRequest = serde_json::from_str(json).unwrap();
assert!(request.node_id.is_none());
}
#[test]
fn governance_request_builder() {
let request = GovernanceRequest::new("agent-1", "deploy");
assert_eq!(request.agent_id, "agent-1");
assert_eq!(request.action, "deploy");
assert!(request.node_id.is_none());
assert!((request.effect.magnitude() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn effect_vector_serde_roundtrip() {
let v = EffectVector {
risk: 0.1,
fairness: 0.2,
privacy: 0.3,
novelty: 0.4,
security: 0.5,
};
let json = serde_json::to_string(&v).unwrap();
let restored: EffectVector = serde_json::from_str(&json).unwrap();
assert!((restored.security - 0.5).abs() < f64::EPSILON);
}
#[test]
fn filter_rules_by_sop_category() {
let rules = vec![
GovernanceRule {
id: "SOP-L001".into(),
description: "test".into(),
branch: GovernanceBranch::Legislative,
severity: RuleSeverity::Blocking,
active: true,
reference_url: Some("https://example.com".into()),
sop_category: Some("governance".into()),
},
GovernanceRule {
id: "SOP-J001".into(),
description: "test".into(),
branch: GovernanceBranch::Judicial,
severity: RuleSeverity::Blocking,
active: true,
reference_url: Some("https://example.com".into()),
sop_category: Some("ethics".into()),
},
GovernanceRule {
id: "GOV-001".into(),
description: "test".into(),
branch: GovernanceBranch::Judicial,
severity: RuleSeverity::Blocking,
active: true,
reference_url: None,
sop_category: None,
},
];
let ethics = GovernanceRule::filter_by_category(&rules, "ethics");
assert_eq!(ethics.len(), 1);
assert_eq!(ethics[0].id, "SOP-J001");
let governance = GovernanceRule::filter_by_category(&rules, "governance");
assert_eq!(governance.len(), 1);
let none = GovernanceRule::filter_by_category(&rules, "nonexistent");
assert!(none.is_empty());
}
#[test]
fn governance_rule_with_sop_serde() {
let rule = GovernanceRule {
id: "SOP-L001".into(),
description: "test".into(),
branch: GovernanceBranch::Legislative,
severity: RuleSeverity::Blocking,
active: true,
reference_url: Some("https://example.com/sop".into()),
sop_category: Some("governance".into()),
};
let json = serde_json::to_string(&rule).unwrap();
assert!(json.contains("reference_url"));
assert!(json.contains("sop_category"));
let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
assert_eq!(restored.reference_url, Some("https://example.com/sop".into()));
}
#[test]
fn governance_rule_without_sop_backward_compat() {
let json = r#"{"id":"GOV-001","description":"test","branch":"Judicial","severity":"Blocking","active":true}"#;
let rule: GovernanceRule = serde_json::from_str(json).unwrap();
assert!(rule.reference_url.is_none());
assert!(rule.sop_category.is_none());
}
fn make_sop_rule(
id: &str,
severity: RuleSeverity,
branch: GovernanceBranch,
category: Option<&str>,
) -> GovernanceRule {
GovernanceRule {
id: id.into(),
description: format!("Genesis rule {id}"),
branch,
severity,
active: true,
reference_url: None,
sop_category: category.map(|c| c.into()),
}
}
fn genesis_engine() -> GovernanceEngine {
let mut engine = GovernanceEngine::new(0.7, false);
engine.add_rule(make_sop_rule("GOV-001", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
engine.add_rule(make_sop_rule("GOV-002", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
engine.add_rule(make_sop_rule("GOV-003", RuleSeverity::Warning, GovernanceBranch::Legislative, None));
engine.add_rule(make_sop_rule("GOV-004", RuleSeverity::Advisory, GovernanceBranch::Executive, None));
engine.add_rule(make_sop_rule("GOV-005", RuleSeverity::Warning, GovernanceBranch::Legislative, None));
engine.add_rule(make_sop_rule("GOV-006", RuleSeverity::Blocking, GovernanceBranch::Executive, None));
engine.add_rule(make_sop_rule("GOV-007", RuleSeverity::Advisory, GovernanceBranch::Judicial, None));
engine.add_rule(make_sop_rule("SOP-L001", RuleSeverity::Blocking, GovernanceBranch::Legislative, Some("governance")));
engine.add_rule(make_sop_rule("SOP-L002", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
engine.add_rule(make_sop_rule("SOP-L003", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("engineering")));
engine.add_rule(make_sop_rule("SOP-L004", RuleSeverity::Advisory, GovernanceBranch::Legislative, Some("lifecycle")));
engine.add_rule(make_sop_rule("SOP-L005", RuleSeverity::Blocking, GovernanceBranch::Legislative, Some("ethics")));
engine.add_rule(make_sop_rule("SOP-L006", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
engine.add_rule(make_sop_rule("SOP-E001", RuleSeverity::Warning, GovernanceBranch::Executive, Some("engineering")));
engine.add_rule(make_sop_rule("SOP-E002", RuleSeverity::Blocking, GovernanceBranch::Executive, Some("lifecycle")));
engine.add_rule(make_sop_rule("SOP-E003", RuleSeverity::Warning, GovernanceBranch::Executive, Some("security")));
engine.add_rule(make_sop_rule("SOP-E004", RuleSeverity::Advisory, GovernanceBranch::Executive, Some("lifecycle")));
engine.add_rule(make_sop_rule("SOP-E005", RuleSeverity::Advisory, GovernanceBranch::Executive, Some("governance")));
engine.add_rule(make_sop_rule("SOP-J001", RuleSeverity::Blocking, GovernanceBranch::Judicial, Some("ethics")));
engine.add_rule(make_sop_rule("SOP-J002", RuleSeverity::Warning, GovernanceBranch::Judicial, Some("ethics")));
engine.add_rule(make_sop_rule("SOP-J003", RuleSeverity::Warning, GovernanceBranch::Judicial, Some("lifecycle")));
engine.add_rule(make_sop_rule("SOP-J004", RuleSeverity::Advisory, GovernanceBranch::Judicial, Some("quality")));
engine
}
#[test]
fn genesis_has_22_rules() {
let engine = genesis_engine();
assert_eq!(engine.rule_count(), 22);
}
#[test]
fn genesis_high_risk_operation_blocked() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "deploy-to-prod".into(),
effect: EffectVector { risk: 0.9, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(
matches!(result.decision, GovernanceDecision::Deny(_)),
"high-risk operation should be denied, got {:?}", result.decision,
);
assert!(result.threshold_exceeded);
}
#[test]
fn genesis_low_risk_operation_permitted() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "read-file".into(),
effect: EffectVector { risk: 0.1, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert_eq!(result.decision, GovernanceDecision::Permit);
assert!(!result.threshold_exceeded);
}
#[test]
fn genesis_privacy_violation_triggers_enforcement() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "access-user-data".into(),
effect: EffectVector { privacy: 0.8, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(
matches!(result.decision, GovernanceDecision::Deny(_) | GovernanceDecision::PermitWithWarning(_)),
"privacy violation should trigger warning or deny, got {:?}", result.decision,
);
assert!(result.threshold_exceeded);
}
#[test]
fn genesis_security_sensitive_blocked() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "modify-firewall".into(),
effect: EffectVector { security: 0.9, risk: 0.5, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
}
#[test]
fn genesis_fairness_bias_blocked() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "ml-agent".into(),
action: "evaluate-candidate".into(),
effect: EffectVector { fairness: 0.9, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(
matches!(result.decision, GovernanceDecision::Deny(_)),
"fairness violation should be blocked by SOP-J001, got {:?}", result.decision,
);
}
#[test]
fn genesis_agent_spawn_blocked_when_risky() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "orchestrator".into(),
action: "agent.spawn".into(),
effect: EffectVector { risk: 0.8, novelty: 0.5, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
}
#[test]
fn genesis_agent_spawn_permitted_when_safe() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "orchestrator".into(),
action: "agent.spawn".into(),
effect: EffectVector { risk: 0.1, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert_eq!(result.decision, GovernanceDecision::Permit);
}
#[test]
fn genesis_data_protection_blocks_high_privacy() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "data-agent".into(),
action: "export-pii".into(),
effect: EffectVector { privacy: 0.9, risk: 0.3, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
}
#[test]
fn genesis_with_human_approval_escalates() {
let mut engine = GovernanceEngine::new(0.7, true);
engine.add_rule(make_sop_rule("GOV-001", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "high-risk-op".into(),
effect: EffectVector { risk: 0.9, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(
matches!(result.decision, GovernanceDecision::EscalateToHuman(_)),
"with human_approval, blocking should escalate, got {:?}", result.decision,
);
}
#[test]
fn genesis_all_branches_represented() {
let engine = genesis_engine();
let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
let executive = engine.rules_by_branch(&GovernanceBranch::Executive);
let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
assert_eq!(legislative.len(), 8, "legislative should have 8 rules, got {}", legislative.len());
assert_eq!(executive.len(), 7, "executive should have 7 rules, got {}", executive.len());
assert_eq!(judicial.len(), 7, "judicial should have 7 rules, got {}", judicial.len());
}
#[test]
fn genesis_blocking_rules_count() {
let engine = genesis_engine();
let blocking_count = engine.active_rules().iter()
.filter(|r| matches!(r.severity, RuleSeverity::Blocking | RuleSeverity::Critical))
.count();
assert_eq!(blocking_count, 7, "should have exactly 7 blocking rules");
}
#[test]
fn genesis_warning_rules_count() {
let engine = genesis_engine();
let warning_count = engine.active_rules().iter()
.filter(|r| matches!(r.severity, RuleSeverity::Warning))
.count();
assert_eq!(warning_count, 9, "should have exactly 9 warning rules");
}
#[test]
fn genesis_advisory_rules_count() {
let engine = genesis_engine();
let advisory_count = engine.active_rules().iter()
.filter(|r| matches!(r.severity, RuleSeverity::Advisory))
.count();
assert_eq!(advisory_count, 6, "should have exactly 6 advisory rules");
}
#[test]
fn genesis_moderate_risk_with_only_warnings_permits_with_warning() {
let mut engine = GovernanceEngine::new(0.7, false);
engine.add_rule(make_sop_rule("SOP-L002", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
engine.add_rule(make_sop_rule("SOP-E001", RuleSeverity::Warning, GovernanceBranch::Executive, Some("engineering")));
let request = GovernanceRequest {
agent_id: "dev-agent".into(),
action: "write-code".into(),
effect: EffectVector { risk: 0.5, novelty: 0.5, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(
matches!(result.decision, GovernanceDecision::PermitWithWarning(_)),
"warning-only rules above threshold should PermitWithWarning, got {:?}", result.decision,
);
}
#[test]
fn genesis_advisory_only_permits_above_threshold() {
let mut engine = GovernanceEngine::new(0.7, false);
engine.add_rule(make_sop_rule("GOV-004", RuleSeverity::Advisory, GovernanceBranch::Executive, None));
engine.add_rule(make_sop_rule("GOV-007", RuleSeverity::Advisory, GovernanceBranch::Judicial, None));
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "novel-action".into(),
effect: EffectVector { novelty: 0.9, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert_eq!(result.decision, GovernanceDecision::Permit,
"advisory-only rules should still permit, got {:?}", result.decision);
assert!(result.threshold_exceeded);
}
#[test]
fn genesis_evaluates_all_22_rules() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "any-action".into(),
effect: EffectVector { risk: 0.9, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert_eq!(
result.evaluated_rules.len(), 22,
"all 22 rules should be evaluated, got {}", result.evaluated_rules.len(),
);
}
#[test]
fn genesis_sop_categories_present() {
let engine = genesis_engine();
let all_rules = engine.active_rules();
let categorized: Vec<_> = all_rules.iter()
.filter(|r| r.sop_category.is_some())
.collect();
assert_eq!(categorized.len(), 15, "15 SOP rules should have categories");
let categories: std::collections::HashSet<_> = categorized.iter()
.map(|r| r.sop_category.as_deref().unwrap())
.collect();
assert!(categories.contains("governance"));
assert!(categories.contains("ethics"));
assert!(categories.contains("engineering"));
assert!(categories.contains("lifecycle"));
assert!(categories.contains("security"));
assert!(categories.contains("quality"));
}
#[test]
fn genesis_multi_dimension_high_effect_denied() {
let engine = genesis_engine();
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "risky-novel-private".into(),
effect: EffectVector {
risk: 0.4,
privacy: 0.4,
novelty: 0.4,
security: 0.4,
fairness: 0.0,
},
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
assert!(
matches!(result.decision, GovernanceDecision::Deny(_)),
"combined multi-dimension effect should exceed threshold and deny, got {:?}",
result.decision,
);
assert!(result.threshold_exceeded);
}
fn make_env(class: crate::environment::EnvironmentClass, risk_threshold: f64) -> crate::environment::Environment {
crate::environment::Environment {
id: format!("{class}"),
name: format!("{class}"),
class,
governance: crate::environment::GovernanceScope {
risk_threshold,
..Default::default()
},
labels: std::collections::HashMap::new(),
}
}
#[test]
fn dev_environment_allows_higher_risk() {
use crate::environment::EnvironmentClass;
let engine = GovernanceEngine::open();
let dev_env = make_env(EnvironmentClass::Development, 0.9);
let request = GovernanceRequest::new("agent-1", "deploy")
.with_effect(EffectVector { risk: 0.8, ..Default::default() });
let result = engine.evaluate_in_environment(&request, &dev_env);
assert_eq!(
result.decision,
GovernanceDecision::Permit,
"dev should allow risk 0.8 with threshold 0.9, got {:?}",
result.decision,
);
}
#[test]
fn prod_environment_blocks_high_risk() {
use crate::environment::EnvironmentClass;
let mut engine = GovernanceEngine::new(1.0, false);
engine.add_rule(make_rule(
"sec-check",
RuleSeverity::Blocking,
GovernanceBranch::Judicial,
));
let prod_env = make_env(EnvironmentClass::Production, 0.6);
let request = GovernanceRequest::new("agent-1", "deploy")
.with_effect(EffectVector { risk: 0.4, ..Default::default() });
let result = engine.evaluate_in_environment(&request, &prod_env);
assert!(
matches!(result.decision, GovernanceDecision::Deny(_)),
"prod should deny risk 0.4 with adjusted threshold 0.3, got {:?}",
result.decision,
);
}
#[test]
fn staging_uses_normal_thresholds() {
use crate::environment::EnvironmentClass;
let mut engine = GovernanceEngine::new(1.0, false);
engine.add_rule(make_rule(
"sec-check",
RuleSeverity::Blocking,
GovernanceBranch::Judicial,
));
let staging_env = make_env(EnvironmentClass::Staging, 0.6);
let request = GovernanceRequest::new("agent-1", "test-deploy")
.with_effect(EffectVector { risk: 0.5, ..Default::default() });
let result = engine.evaluate_in_environment(&request, &staging_env);
assert_eq!(
result.decision,
GovernanceDecision::Permit,
"staging should allow risk 0.5 with threshold 0.6, got {:?}",
result.decision,
);
let request2 = GovernanceRequest::new("agent-1", "test-deploy")
.with_effect(EffectVector { risk: 0.7, ..Default::default() });
let result2 = engine.evaluate_in_environment(&request2, &staging_env);
assert!(
matches!(result2.decision, GovernanceDecision::Deny(_)),
"staging should deny risk 0.7 with threshold 0.6, got {:?}",
result2.decision,
);
}
#[test]
fn environment_governance_same_request_different_envs() {
use crate::environment::EnvironmentClass;
let engine = GovernanceEngine::open();
let dev = make_env(EnvironmentClass::Development, 0.9);
let prod = make_env(EnvironmentClass::Production, 0.6);
let request = GovernanceRequest::new("agent-1", "write-file")
.with_effect(EffectVector { risk: 0.5, ..Default::default() });
let dev_result = engine.evaluate_in_environment(&request, &dev);
let prod_result = engine.evaluate_in_environment(&request, &prod);
assert_eq!(dev_result.decision, GovernanceDecision::Permit);
assert!(matches!(prod_result.decision, GovernanceDecision::Deny(_)));
}
#[test]
fn trajectory_records_and_retrieves() {
use chrono::Utc;
let mut recorder = TrajectoryRecorder::new(100);
recorder.record(TrajectoryRecord {
agent_id: "agent-1".into(),
action: "tool.execute".into(),
context: serde_json::json!({"tool": "read_file"}),
outcome: TrajectoryOutcome::Success { reward: 1.0 },
timestamp: Utc::now(),
});
assert_eq!(recorder.len(), 1);
assert!(!recorder.is_empty());
assert_eq!(recorder.agent_trajectory("agent-1").len(), 1);
assert_eq!(recorder.agent_trajectory("agent-2").len(), 0);
}
#[test]
fn trajectory_extracts_patterns() {
use chrono::Utc;
let mut recorder = TrajectoryRecorder::new(100);
for _ in 0..5 {
recorder.record(TrajectoryRecord {
agent_id: "a".into(),
action: "tool.execute".into(),
context: serde_json::json!({}),
outcome: TrajectoryOutcome::Success { reward: 1.0 },
timestamp: Utc::now(),
});
}
for _ in 0..3 {
recorder.record(TrajectoryRecord {
agent_id: "a".into(),
action: "tool.read".into(),
context: serde_json::json!({}),
outcome: TrajectoryOutcome::Success { reward: 0.5 },
timestamp: Utc::now(),
});
}
recorder.record(TrajectoryRecord {
agent_id: "a".into(),
action: "tool.fail".into(),
context: serde_json::json!({}),
outcome: TrajectoryOutcome::Failure {
reason: "err".into(),
},
timestamp: Utc::now(),
});
let patterns = recorder.extract_patterns();
assert_eq!(patterns[0].0, "tool.execute");
assert_eq!(patterns[0].1, 5);
assert_eq!(patterns[1].0, "tool.read");
assert_eq!(patterns[1].1, 3);
assert!(patterns.iter().all(|(a, _)| a != "tool.fail"));
}
#[test]
fn trajectory_max_records_eviction() {
use chrono::Utc;
let mut recorder = TrajectoryRecorder::new(3);
for i in 0..5 {
recorder.record(TrajectoryRecord {
agent_id: format!("agent-{i}"),
action: "act".into(),
context: serde_json::json!({}),
outcome: TrajectoryOutcome::Pending,
timestamp: Utc::now(),
});
}
assert_eq!(recorder.len(), 3);
assert!(recorder.agent_trajectory("agent-0").is_empty());
assert!(recorder.agent_trajectory("agent-1").is_empty());
assert_eq!(recorder.agent_trajectory("agent-2").len(), 1);
assert_eq!(recorder.agent_trajectory("agent-4").len(), 1);
}
#[test]
fn trajectory_empty_patterns() {
let recorder = TrajectoryRecorder::new(10);
assert!(recorder.is_empty());
assert!(recorder.extract_patterns().is_empty());
}
#[test]
fn governance_branch_serde_roundtrip() {
for branch in [
GovernanceBranch::Legislative,
GovernanceBranch::Executive,
GovernanceBranch::Judicial,
] {
let json = serde_json::to_string(&branch).unwrap();
let restored: GovernanceBranch = serde_json::from_str(&json).unwrap();
assert_eq!(restored, branch);
}
}
#[test]
fn rule_severity_serde_roundtrip() {
for severity in [
RuleSeverity::Advisory,
RuleSeverity::Warning,
RuleSeverity::Blocking,
RuleSeverity::Critical,
] {
let json = serde_json::to_string(&severity).unwrap();
let restored: RuleSeverity = serde_json::from_str(&json).unwrap();
assert_eq!(restored, severity);
}
}
#[test]
fn rule_severity_display() {
assert_eq!(RuleSeverity::Advisory.to_string(), "advisory");
assert_eq!(RuleSeverity::Warning.to_string(), "warning");
assert_eq!(RuleSeverity::Blocking.to_string(), "blocking");
assert_eq!(RuleSeverity::Critical.to_string(), "critical");
}
#[test]
fn governance_decision_serde_roundtrip_all_variants() {
let variants = vec![
GovernanceDecision::Permit,
GovernanceDecision::PermitWithWarning("low risk".into()),
GovernanceDecision::EscalateToHuman("needs review".into()),
GovernanceDecision::Deny("policy violation".into()),
];
for decision in variants {
let json = serde_json::to_string(&decision).unwrap();
let restored: GovernanceDecision = serde_json::from_str(&json).unwrap();
assert_eq!(restored, decision);
}
}
#[test]
fn governance_result_serde_roundtrip() {
let result = GovernanceResult {
decision: GovernanceDecision::Permit,
evaluated_rules: vec!["rule-1".into(), "rule-2".into()],
effect: EffectVector {
risk: 0.3,
..Default::default()
},
threshold_exceeded: false,
};
let json = serde_json::to_string(&result).unwrap();
let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
assert_eq!(restored.decision, GovernanceDecision::Permit);
assert_eq!(restored.evaluated_rules.len(), 2);
assert!(!restored.threshold_exceeded);
}
#[test]
fn governance_result_deny_serde_roundtrip() {
let result = GovernanceResult {
decision: GovernanceDecision::Deny("threshold exceeded".into()),
evaluated_rules: vec![],
effect: EffectVector {
risk: 0.9,
security: 0.8,
..Default::default()
},
threshold_exceeded: true,
};
let json = serde_json::to_string(&result).unwrap();
let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
assert!(restored.threshold_exceeded);
assert!(matches!(restored.decision, GovernanceDecision::Deny(_)));
}
#[test]
fn trajectory_outcome_serde_roundtrip() {
let variants: Vec<TrajectoryOutcome> = vec![
TrajectoryOutcome::Success { reward: 1.5 },
TrajectoryOutcome::Failure {
reason: "timeout".into(),
},
TrajectoryOutcome::Pending,
];
for outcome in variants {
let json = serde_json::to_string(&outcome).unwrap();
let _restored: TrajectoryOutcome = serde_json::from_str(&json).unwrap();
}
}
#[test]
fn trajectory_record_serde_roundtrip() {
use chrono::Utc;
let record = TrajectoryRecord {
agent_id: "agent-1".into(),
action: "tool.execute".into(),
context: serde_json::json!({"tool": "read_file", "path": "/etc/hosts"}),
outcome: TrajectoryOutcome::Success { reward: 1.0 },
timestamp: Utc::now(),
};
let json = serde_json::to_string(&record).unwrap();
let restored: TrajectoryRecord = serde_json::from_str(&json).unwrap();
assert_eq!(restored.agent_id, "agent-1");
assert_eq!(restored.action, "tool.execute");
}
#[test]
fn governance_rule_with_sop_fields_roundtrip() {
let rule = GovernanceRule {
id: "SOP-001".into(),
description: "Do not access prod data".into(),
branch: GovernanceBranch::Legislative,
severity: RuleSeverity::Critical,
active: true,
reference_url: Some("https://sops.example.com/001".into()),
sop_category: Some("data-access".into()),
};
let json = serde_json::to_string(&rule).unwrap();
let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
assert_eq!(restored.reference_url.unwrap(), "https://sops.example.com/001");
assert_eq!(restored.sop_category.unwrap(), "data-access");
}
#[test]
fn effect_vector_default_is_zero() {
let v = EffectVector::default();
assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
assert!(!v.any_exceeds(0.0));
}
#[test]
fn effect_vector_max_dimension() {
let v = EffectVector {
risk: 0.1,
fairness: 0.5,
privacy: 0.3,
novelty: 0.9,
security: 0.2,
};
assert!((v.max_dimension() - 0.9).abs() < f64::EPSILON);
}
#[cfg(feature = "exochain")]
mod rvf_bridge_tests {
use super::*;
#[test]
fn decision_to_policy_check() {
use rvf_types::witness::PolicyCheck;
assert_eq!(
GovernanceDecision::Permit.to_rvf_policy_check(),
PolicyCheck::Allowed,
);
assert_eq!(
GovernanceDecision::PermitWithWarning("low risk".into()).to_rvf_policy_check(),
PolicyCheck::Confirmed,
);
assert_eq!(
GovernanceDecision::EscalateToHuman("needs review".into()).to_rvf_policy_check(),
PolicyCheck::Confirmed,
);
assert_eq!(
GovernanceDecision::Deny("blocked".into()).to_rvf_policy_check(),
PolicyCheck::Denied,
);
}
#[test]
fn open_engine_maps_to_autonomous() {
use rvf_types::witness::GovernanceMode;
let engine = GovernanceEngine::open();
assert_eq!(engine.to_rvf_mode(), GovernanceMode::Autonomous);
}
#[test]
fn strict_engine_maps_to_restricted() {
use rvf_types::witness::GovernanceMode;
let engine = GovernanceEngine::new(0.5, false);
assert_eq!(engine.to_rvf_mode(), GovernanceMode::Restricted);
}
#[test]
fn human_approval_maps_to_approved() {
use rvf_types::witness::GovernanceMode;
let engine = GovernanceEngine::new(0.5, true);
assert_eq!(engine.to_rvf_mode(), GovernanceMode::Approved);
}
#[test]
fn to_rvf_policy_mode_matches() {
use rvf_types::witness::GovernanceMode;
let open = GovernanceEngine::open();
let policy = open.to_rvf_policy();
assert_eq!(policy.mode, GovernanceMode::Autonomous);
let strict = GovernanceEngine::new(0.3, false);
let policy = strict.to_rvf_policy();
assert_eq!(policy.mode, GovernanceMode::Restricted);
let human = GovernanceEngine::new(0.3, true);
let policy = human.to_rvf_policy();
assert_eq!(policy.mode, GovernanceMode::Approved);
}
#[test]
fn governance_result_to_task_outcome() {
use rvf_types::witness::TaskOutcome;
let permit_result = GovernanceResult {
decision: GovernanceDecision::Permit,
evaluated_rules: vec![],
effect: EffectVector::default(),
threshold_exceeded: false,
};
assert_eq!(permit_result.to_rvf_task_outcome() as u8, TaskOutcome::Solved as u8);
let deny_result = GovernanceResult {
decision: GovernanceDecision::Deny("blocked".into()),
evaluated_rules: vec![],
effect: EffectVector::default(),
threshold_exceeded: true,
};
assert_eq!(deny_result.to_rvf_task_outcome() as u8, TaskOutcome::Failed as u8);
let escalate_result = GovernanceResult {
decision: GovernanceDecision::EscalateToHuman("review".into()),
evaluated_rules: vec![],
effect: EffectVector::default(),
threshold_exceeded: true,
};
assert_eq!(escalate_result.to_rvf_task_outcome() as u8, TaskOutcome::Skipped as u8);
}
}
#[cfg(feature = "exochain")]
mod chain_logging_tests {
use super::*;
use std::sync::Arc;
#[test]
fn evaluate_logged_records_permit() {
let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
let initial_len = cm.len();
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(GovernanceRule {
id: "sec".into(),
description: "test".into(),
branch: GovernanceBranch::Judicial,
severity: RuleSeverity::Blocking,
active: true,
reference_url: None,
sop_category: None,
});
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "tool.read".into(),
effect: EffectVector { risk: 0.1, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate_logged(&request, Some(&cm));
assert!(matches!(result.decision, GovernanceDecision::Permit));
assert_eq!(cm.len(), initial_len + 1);
let events = cm.tail(1);
assert_eq!(events[0].kind, "governance.permit");
assert_eq!(events[0].source, "governance");
}
#[test]
fn evaluate_logged_records_deny() {
let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
let mut engine = GovernanceEngine::new(0.5, false);
engine.add_rule(GovernanceRule {
id: "sec".into(),
description: "test".into(),
branch: GovernanceBranch::Judicial,
severity: RuleSeverity::Blocking,
active: true,
reference_url: None,
sop_category: None,
});
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "tool.exec".into(),
effect: EffectVector { risk: 0.9, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate_logged(&request, Some(&cm));
assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
let events = cm.tail(1);
assert_eq!(events[0].kind, "governance.deny");
let payload = events[0].payload.as_ref().unwrap();
assert_eq!(payload["agent_id"], "agent-1");
assert!(payload["threshold_exceeded"].as_bool().unwrap());
}
#[test]
fn evaluate_logged_without_chain_still_works() {
let engine = GovernanceEngine::new(0.5, false);
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "tool.read".into(),
effect: EffectVector::default(),
context: Default::default(),
node_id: None,
};
let result = engine.evaluate_logged(&request, None);
assert!(matches!(result.decision, GovernanceDecision::Permit));
}
#[test]
fn chain_log_result_standalone() {
let cm = crate::chain::ChainManager::new(0, 100);
let initial_len = cm.len();
let mut engine = GovernanceEngine::new(0.5, true);
engine.add_rule(GovernanceRule {
id: "sec".into(),
description: "test".into(),
branch: GovernanceBranch::Judicial,
severity: RuleSeverity::Blocking,
active: true,
reference_url: None,
sop_category: None,
});
let request = GovernanceRequest {
agent_id: "agent-1".into(),
action: "tool.exec".into(),
effect: EffectVector { risk: 0.8, ..Default::default() },
context: Default::default(),
node_id: None,
};
let result = engine.evaluate(&request);
GovernanceEngine::chain_log_result(&cm, &request, &result);
assert_eq!(cm.len(), initial_len + 1);
let events = cm.tail(1);
assert_eq!(events[0].kind, "governance.defer");
}
}
}