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}