Skip to main content

aa_core/
risk_tier.rs

1//! Risk tier classification for agents and policies.
2//!
3//! Mirrors the proto `RiskTier` enum defined in `proto/common.proto` so that
4//! pure-Rust code can reason about risk without a proto dependency.
5//!
6//! | Tier     | Proto value | Meaning |
7//! |----------|-------------|---------|
8//! | `Low`    | 1           | Log-only enforcement; no blocking. |
9//! | `Medium` | 2           | Block-and-optionally-approve enforcement. |
10//! | `High`   | 3           | Always block; human review mandatory. |
11//! | `Critical`| 4          | Immediate kill + incident escalation. |
12//!
13//! **Null-safe policy semantics**: when a registry lookup cannot resolve a
14//! risk tier (agent not registered yet, or `risk_tier = 0` / unspecified),
15//! the condition evaluates to `false` — no-match, not fail-safe trigger.
16
17/// Risk tier assigned to an agent or policy, in ascending severity order.
18///
19/// `PartialOrd` / `Ord` reflect severity: `Low < Medium < High < Critical`.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))]
23pub enum RiskTier {
24    /// Log-only enforcement; no blocking.
25    #[default]
26    Low,
27    /// Block-and-optionally-approve enforcement.
28    Medium,
29    /// Always block; human review mandatory.
30    High,
31    /// Immediate kill + incident escalation.
32    Critical,
33}
34
35impl RiskTier {
36    /// Convert from the proto integer value (1=Low … 4=Critical).
37    /// Returns `None` for 0 (UNSPECIFIED) and any out-of-range value.
38    pub fn from_proto_i32(v: i32) -> Option<Self> {
39        match v {
40            1 => Some(Self::Low),
41            2 => Some(Self::Medium),
42            3 => Some(Self::High),
43            4 => Some(Self::Critical),
44            _ => None,
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn ordering_reflects_severity() {
55        assert!(RiskTier::Low < RiskTier::Medium);
56        assert!(RiskTier::Medium < RiskTier::High);
57        assert!(RiskTier::High < RiskTier::Critical);
58    }
59
60    #[test]
61    fn default_is_low() {
62        assert_eq!(RiskTier::default(), RiskTier::Low);
63    }
64
65    #[test]
66    fn from_proto_i32_round_trips() {
67        assert_eq!(RiskTier::from_proto_i32(1), Some(RiskTier::Low));
68        assert_eq!(RiskTier::from_proto_i32(2), Some(RiskTier::Medium));
69        assert_eq!(RiskTier::from_proto_i32(3), Some(RiskTier::High));
70        assert_eq!(RiskTier::from_proto_i32(4), Some(RiskTier::Critical));
71        assert_eq!(RiskTier::from_proto_i32(0), None);
72        assert_eq!(RiskTier::from_proto_i32(99), None);
73    }
74
75    #[cfg(feature = "serde")]
76    #[test]
77    fn serde_round_trip() {
78        for tier in [RiskTier::Low, RiskTier::Medium, RiskTier::High, RiskTier::Critical] {
79            let json = serde_json::to_string(&tier).unwrap();
80            let back: RiskTier = serde_json::from_str(&json).unwrap();
81            assert_eq!(tier, back);
82        }
83    }
84}