use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyDecision {
Allow,
Deny(String),
RequiresApproval(String),
RateLimited { retry_after_secs: u64 },
}
impl PolicyDecision {
pub fn is_allowed(&self) -> bool {
matches!(self, PolicyDecision::Allow)
}
pub fn label(&self) -> &'static str {
match self {
PolicyDecision::Allow => "allow",
PolicyDecision::Deny(_) => "deny",
PolicyDecision::RequiresApproval(_) => "requires_approval",
PolicyDecision::RateLimited { .. } => "rate_limited",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
VerifiedPartner,
Trusted,
Standard,
Probationary,
Untrusted,
}
impl TrustTier {
pub fn from_score(score: u32) -> Self {
match score {
900..=1000 => TrustTier::VerifiedPartner,
700..=899 => TrustTier::Trusted,
500..=699 => TrustTier::Standard,
300..=499 => TrustTier::Probationary,
_ => TrustTier::Untrusted,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustScore {
pub agent_id: String,
pub score: u32,
pub tier: TrustTier,
pub interactions: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub seq: u64,
pub timestamp: String,
pub agent_id: String,
pub action: String,
pub decision: String,
pub previous_hash: String,
pub hash: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuditFilter {
pub agent_id: Option<String>,
pub action: Option<String>,
pub decision: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConflictResolutionStrategy {
DenyOverrides,
AllowOverrides,
#[default]
PriorityFirstMatch,
MostSpecificWins,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PolicyScope {
#[default]
Global,
Tenant,
Agent,
}
impl PolicyScope {
pub fn specificity(self) -> u32 {
match self {
PolicyScope::Global => 0,
PolicyScope::Tenant => 1,
PolicyScope::Agent => 2,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandidateDecision {
pub decision: PolicyDecision,
pub priority: u32,
pub scope: PolicyScope,
pub rule_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolutionResult {
pub winning_decision: PolicyDecision,
pub strategy_used: ConflictResolutionStrategy,
pub conflict_detected: bool,
pub candidates_evaluated: usize,
}
#[derive(Debug, Clone)]
pub struct GovernanceResult {
pub decision: PolicyDecision,
pub trust_score: TrustScore,
pub audit_entry: AuditEntry,
pub allowed: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_decision_is_allowed_allow() {
assert!(PolicyDecision::Allow.is_allowed());
}
#[test]
fn test_policy_decision_is_allowed_deny() {
assert!(!PolicyDecision::Deny("reason".to_string()).is_allowed());
}
#[test]
fn test_policy_decision_is_allowed_requires_approval() {
assert!(!PolicyDecision::RequiresApproval("reason".to_string()).is_allowed());
}
#[test]
fn test_policy_decision_is_allowed_rate_limited() {
assert!(!PolicyDecision::RateLimited {
retry_after_secs: 10
}
.is_allowed());
}
#[test]
fn test_policy_decision_label_allow() {
assert_eq!(PolicyDecision::Allow.label(), "allow");
}
#[test]
fn test_policy_decision_label_deny() {
assert_eq!(PolicyDecision::Deny("x".to_string()).label(), "deny");
}
#[test]
fn test_policy_decision_label_requires_approval() {
assert_eq!(
PolicyDecision::RequiresApproval("x".to_string()).label(),
"requires_approval"
);
}
#[test]
fn test_policy_decision_label_rate_limited() {
assert_eq!(
PolicyDecision::RateLimited {
retry_after_secs: 5
}
.label(),
"rate_limited"
);
}
#[test]
fn test_trust_tier_boundary_0() {
assert_eq!(TrustTier::from_score(0), TrustTier::Untrusted);
}
#[test]
fn test_trust_tier_boundary_299() {
assert_eq!(TrustTier::from_score(299), TrustTier::Untrusted);
}
#[test]
fn test_trust_tier_boundary_300() {
assert_eq!(TrustTier::from_score(300), TrustTier::Probationary);
}
#[test]
fn test_trust_tier_boundary_499() {
assert_eq!(TrustTier::from_score(499), TrustTier::Probationary);
}
#[test]
fn test_trust_tier_boundary_500() {
assert_eq!(TrustTier::from_score(500), TrustTier::Standard);
}
#[test]
fn test_trust_tier_boundary_699() {
assert_eq!(TrustTier::from_score(699), TrustTier::Standard);
}
#[test]
fn test_trust_tier_boundary_700() {
assert_eq!(TrustTier::from_score(700), TrustTier::Trusted);
}
#[test]
fn test_trust_tier_boundary_899() {
assert_eq!(TrustTier::from_score(899), TrustTier::Trusted);
}
#[test]
fn test_trust_tier_boundary_900() {
assert_eq!(TrustTier::from_score(900), TrustTier::VerifiedPartner);
}
#[test]
fn test_trust_tier_boundary_1000() {
assert_eq!(TrustTier::from_score(1000), TrustTier::VerifiedPartner);
}
#[test]
fn test_trust_score_serialization_roundtrip() {
let score = TrustScore {
agent_id: "agent-1".to_string(),
score: 750,
tier: TrustTier::Trusted,
interactions: 42,
};
let json = serde_json::to_string(&score).unwrap();
let deserialized: TrustScore = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.agent_id, "agent-1");
assert_eq!(deserialized.score, 750);
assert_eq!(deserialized.tier, TrustTier::Trusted);
assert_eq!(deserialized.interactions, 42);
}
#[test]
fn test_audit_entry_serialization_roundtrip() {
let entry = AuditEntry {
seq: 0,
timestamp: "2025-01-01T00:00:00Z".to_string(),
agent_id: "agent-1".to_string(),
action: "data.read".to_string(),
decision: "allow".to_string(),
previous_hash: "".to_string(),
hash: "abc123".to_string(),
};
let json = serde_json::to_string(&entry).unwrap();
let deserialized: AuditEntry = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.seq, 0);
assert_eq!(deserialized.agent_id, "agent-1");
assert_eq!(deserialized.action, "data.read");
assert_eq!(deserialized.decision, "allow");
assert_eq!(deserialized.hash, "abc123");
}
#[test]
fn test_policy_decision_serialization_allow() {
let d = PolicyDecision::Allow;
let json = serde_json::to_string(&d).unwrap();
let back: PolicyDecision = serde_json::from_str(&json).unwrap();
assert_eq!(back, PolicyDecision::Allow);
}
#[test]
fn test_policy_decision_serialization_deny() {
let d = PolicyDecision::Deny("blocked".to_string());
let json = serde_json::to_string(&d).unwrap();
let back: PolicyDecision = serde_json::from_str(&json).unwrap();
assert_eq!(back, PolicyDecision::Deny("blocked".to_string()));
}
#[test]
fn test_policy_decision_serialization_requires_approval() {
let d = PolicyDecision::RequiresApproval("needs review".to_string());
let json = serde_json::to_string(&d).unwrap();
let back: PolicyDecision = serde_json::from_str(&json).unwrap();
assert_eq!(
back,
PolicyDecision::RequiresApproval("needs review".to_string())
);
}
#[test]
fn test_policy_decision_serialization_rate_limited() {
let d = PolicyDecision::RateLimited {
retry_after_secs: 30,
};
let json = serde_json::to_string(&d).unwrap();
let back: PolicyDecision = serde_json::from_str(&json).unwrap();
assert_eq!(
back,
PolicyDecision::RateLimited {
retry_after_secs: 30
}
);
}
#[test]
fn test_governance_result_fields_populated() {
let result = GovernanceResult {
allowed: true,
decision: PolicyDecision::Allow,
trust_score: TrustScore {
agent_id: "agent-1".to_string(),
score: 500,
tier: TrustTier::Standard,
interactions: 0,
},
audit_entry: AuditEntry {
seq: 0,
timestamp: "2025-01-01T00:00:00Z".to_string(),
agent_id: "agent-1".to_string(),
action: "test".to_string(),
decision: "allow".to_string(),
previous_hash: "".to_string(),
hash: "abc".to_string(),
},
};
assert!(result.allowed);
assert_eq!(result.decision, PolicyDecision::Allow);
assert_eq!(result.trust_score.agent_id, "agent-1");
assert_eq!(result.audit_entry.action, "test");
}
#[test]
fn test_audit_filter_default_has_all_none() {
let filter = AuditFilter::default();
assert!(filter.agent_id.is_none());
assert!(filter.action.is_none());
assert!(filter.decision.is_none());
}
#[test]
fn test_trust_tier_partial_eq() {
assert_eq!(TrustTier::Standard, TrustTier::Standard);
assert_ne!(TrustTier::Standard, TrustTier::Trusted);
assert_eq!(TrustTier::VerifiedPartner, TrustTier::VerifiedPartner);
assert_ne!(TrustTier::Untrusted, TrustTier::Probationary);
}
}