Skip to main content

agent_governance/
types.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Shared types for the AgentMesh governance framework.
5
6use serde::{Deserialize, Serialize};
7
8/// The outcome of a policy evaluation.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum PolicyDecision {
12    /// Action is allowed.
13    Allow,
14    /// Action is denied with a reason.
15    Deny(String),
16    /// Action requires human approval.
17    RequiresApproval(String),
18    /// Action is rate-limited; retry after the given number of seconds.
19    RateLimited { retry_after_secs: u64 },
20}
21
22impl PolicyDecision {
23    /// Returns `true` if the decision permits the action.
24    pub fn is_allowed(&self) -> bool {
25        matches!(self, PolicyDecision::Allow)
26    }
27
28    /// Short label used in audit logs.
29    pub fn label(&self) -> &'static str {
30        match self {
31            PolicyDecision::Allow => "allow",
32            PolicyDecision::Deny(_) => "deny",
33            PolicyDecision::RequiresApproval(_) => "requires_approval",
34            PolicyDecision::RateLimited { .. } => "rate_limited",
35        }
36    }
37}
38
39/// Trust tier derived from a numeric score.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum TrustTier {
43    /// Score 900–1000.
44    VerifiedPartner,
45    /// Score 700–899.
46    Trusted,
47    /// Score 500–699.
48    Standard,
49    /// Score 300–499.
50    Probationary,
51    /// Score 0–299.
52    Untrusted,
53}
54
55impl TrustTier {
56    /// Derive the tier from a numeric score (0–1000).
57    pub fn from_score(score: u32) -> Self {
58        match score {
59            900..=1000 => TrustTier::VerifiedPartner,
60            700..=899 => TrustTier::Trusted,
61            500..=699 => TrustTier::Standard,
62            300..=499 => TrustTier::Probationary,
63            _ => TrustTier::Untrusted,
64        }
65    }
66}
67
68/// Snapshot of an agent's trust standing.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TrustScore {
71    pub agent_id: String,
72    pub score: u32,
73    pub tier: TrustTier,
74    pub interactions: u64,
75}
76
77/// A single immutable entry in the hash-chain audit log.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct AuditEntry {
80    pub seq: u64,
81    pub timestamp: String,
82    pub agent_id: String,
83    pub action: String,
84    pub decision: String,
85    pub previous_hash: String,
86    pub hash: String,
87}
88
89/// Filter for querying audit entries.
90#[derive(Debug, Default, Serialize, Deserialize)]
91pub struct AuditFilter {
92    pub agent_id: Option<String>,
93    pub action: Option<String>,
94    pub decision: Option<String>,
95}
96
97/// Conflict resolution strategy when multiple policy rules produce different decisions.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
99#[serde(rename_all = "snake_case")]
100pub enum ConflictResolutionStrategy {
101    /// Any deny decision overrides allows.
102    DenyOverrides,
103    /// Any allow decision overrides denies.
104    AllowOverrides,
105    /// The candidate with the highest priority wins.
106    #[default]
107    PriorityFirstMatch,
108    /// The most specific scope wins, with priority as tiebreaker.
109    MostSpecificWins,
110}
111
112/// The scope at which a policy rule applies.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
114#[serde(rename_all = "snake_case")]
115pub enum PolicyScope {
116    /// Applies to all tenants and agents.
117    #[default]
118    Global,
119    /// Applies to a specific tenant.
120    Tenant,
121    /// Applies to a specific agent.
122    Agent,
123}
124
125impl PolicyScope {
126    /// Returns a numeric specificity value (higher = more specific).
127    pub fn specificity(self) -> u32 {
128        match self {
129            PolicyScope::Global => 0,
130            PolicyScope::Tenant => 1,
131            PolicyScope::Agent => 2,
132        }
133    }
134}
135
136/// A candidate decision produced by a single policy rule evaluation.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct CandidateDecision {
139    pub decision: PolicyDecision,
140    pub priority: u32,
141    pub scope: PolicyScope,
142    pub rule_name: String,
143}
144
145/// Result of conflict resolution across multiple candidate decisions.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ResolutionResult {
148    pub winning_decision: PolicyDecision,
149    pub strategy_used: ConflictResolutionStrategy,
150    pub conflict_detected: bool,
151    pub candidates_evaluated: usize,
152}
153
154/// Result returned by [`AgentMeshClient::execute_with_governance`].
155#[derive(Debug, Clone)]
156pub struct GovernanceResult {
157    pub decision: PolicyDecision,
158    pub trust_score: TrustScore,
159    pub audit_entry: AuditEntry,
160    pub allowed: bool,
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_policy_decision_is_allowed_allow() {
169        assert!(PolicyDecision::Allow.is_allowed());
170    }
171
172    #[test]
173    fn test_policy_decision_is_allowed_deny() {
174        assert!(!PolicyDecision::Deny("reason".to_string()).is_allowed());
175    }
176
177    #[test]
178    fn test_policy_decision_is_allowed_requires_approval() {
179        assert!(!PolicyDecision::RequiresApproval("reason".to_string()).is_allowed());
180    }
181
182    #[test]
183    fn test_policy_decision_is_allowed_rate_limited() {
184        assert!(!PolicyDecision::RateLimited {
185            retry_after_secs: 10
186        }
187        .is_allowed());
188    }
189
190    #[test]
191    fn test_policy_decision_label_allow() {
192        assert_eq!(PolicyDecision::Allow.label(), "allow");
193    }
194
195    #[test]
196    fn test_policy_decision_label_deny() {
197        assert_eq!(PolicyDecision::Deny("x".to_string()).label(), "deny");
198    }
199
200    #[test]
201    fn test_policy_decision_label_requires_approval() {
202        assert_eq!(
203            PolicyDecision::RequiresApproval("x".to_string()).label(),
204            "requires_approval"
205        );
206    }
207
208    #[test]
209    fn test_policy_decision_label_rate_limited() {
210        assert_eq!(
211            PolicyDecision::RateLimited {
212                retry_after_secs: 5
213            }
214            .label(),
215            "rate_limited"
216        );
217    }
218
219    #[test]
220    fn test_trust_tier_boundary_0() {
221        assert_eq!(TrustTier::from_score(0), TrustTier::Untrusted);
222    }
223
224    #[test]
225    fn test_trust_tier_boundary_299() {
226        assert_eq!(TrustTier::from_score(299), TrustTier::Untrusted);
227    }
228
229    #[test]
230    fn test_trust_tier_boundary_300() {
231        assert_eq!(TrustTier::from_score(300), TrustTier::Probationary);
232    }
233
234    #[test]
235    fn test_trust_tier_boundary_499() {
236        assert_eq!(TrustTier::from_score(499), TrustTier::Probationary);
237    }
238
239    #[test]
240    fn test_trust_tier_boundary_500() {
241        assert_eq!(TrustTier::from_score(500), TrustTier::Standard);
242    }
243
244    #[test]
245    fn test_trust_tier_boundary_699() {
246        assert_eq!(TrustTier::from_score(699), TrustTier::Standard);
247    }
248
249    #[test]
250    fn test_trust_tier_boundary_700() {
251        assert_eq!(TrustTier::from_score(700), TrustTier::Trusted);
252    }
253
254    #[test]
255    fn test_trust_tier_boundary_899() {
256        assert_eq!(TrustTier::from_score(899), TrustTier::Trusted);
257    }
258
259    #[test]
260    fn test_trust_tier_boundary_900() {
261        assert_eq!(TrustTier::from_score(900), TrustTier::VerifiedPartner);
262    }
263
264    #[test]
265    fn test_trust_tier_boundary_1000() {
266        assert_eq!(TrustTier::from_score(1000), TrustTier::VerifiedPartner);
267    }
268
269    #[test]
270    fn test_trust_score_serialization_roundtrip() {
271        let score = TrustScore {
272            agent_id: "agent-1".to_string(),
273            score: 750,
274            tier: TrustTier::Trusted,
275            interactions: 42,
276        };
277        let json = serde_json::to_string(&score).unwrap();
278        let deserialized: TrustScore = serde_json::from_str(&json).unwrap();
279        assert_eq!(deserialized.agent_id, "agent-1");
280        assert_eq!(deserialized.score, 750);
281        assert_eq!(deserialized.tier, TrustTier::Trusted);
282        assert_eq!(deserialized.interactions, 42);
283    }
284
285    #[test]
286    fn test_audit_entry_serialization_roundtrip() {
287        let entry = AuditEntry {
288            seq: 0,
289            timestamp: "2025-01-01T00:00:00Z".to_string(),
290            agent_id: "agent-1".to_string(),
291            action: "data.read".to_string(),
292            decision: "allow".to_string(),
293            previous_hash: "".to_string(),
294            hash: "abc123".to_string(),
295        };
296        let json = serde_json::to_string(&entry).unwrap();
297        let deserialized: AuditEntry = serde_json::from_str(&json).unwrap();
298        assert_eq!(deserialized.seq, 0);
299        assert_eq!(deserialized.agent_id, "agent-1");
300        assert_eq!(deserialized.action, "data.read");
301        assert_eq!(deserialized.decision, "allow");
302        assert_eq!(deserialized.hash, "abc123");
303    }
304
305    #[test]
306    fn test_policy_decision_serialization_allow() {
307        let d = PolicyDecision::Allow;
308        let json = serde_json::to_string(&d).unwrap();
309        let back: PolicyDecision = serde_json::from_str(&json).unwrap();
310        assert_eq!(back, PolicyDecision::Allow);
311    }
312
313    #[test]
314    fn test_policy_decision_serialization_deny() {
315        let d = PolicyDecision::Deny("blocked".to_string());
316        let json = serde_json::to_string(&d).unwrap();
317        let back: PolicyDecision = serde_json::from_str(&json).unwrap();
318        assert_eq!(back, PolicyDecision::Deny("blocked".to_string()));
319    }
320
321    #[test]
322    fn test_policy_decision_serialization_requires_approval() {
323        let d = PolicyDecision::RequiresApproval("needs review".to_string());
324        let json = serde_json::to_string(&d).unwrap();
325        let back: PolicyDecision = serde_json::from_str(&json).unwrap();
326        assert_eq!(
327            back,
328            PolicyDecision::RequiresApproval("needs review".to_string())
329        );
330    }
331
332    #[test]
333    fn test_policy_decision_serialization_rate_limited() {
334        let d = PolicyDecision::RateLimited {
335            retry_after_secs: 30,
336        };
337        let json = serde_json::to_string(&d).unwrap();
338        let back: PolicyDecision = serde_json::from_str(&json).unwrap();
339        assert_eq!(
340            back,
341            PolicyDecision::RateLimited {
342                retry_after_secs: 30
343            }
344        );
345    }
346
347    #[test]
348    fn test_governance_result_fields_populated() {
349        let result = GovernanceResult {
350            allowed: true,
351            decision: PolicyDecision::Allow,
352            trust_score: TrustScore {
353                agent_id: "agent-1".to_string(),
354                score: 500,
355                tier: TrustTier::Standard,
356                interactions: 0,
357            },
358            audit_entry: AuditEntry {
359                seq: 0,
360                timestamp: "2025-01-01T00:00:00Z".to_string(),
361                agent_id: "agent-1".to_string(),
362                action: "test".to_string(),
363                decision: "allow".to_string(),
364                previous_hash: "".to_string(),
365                hash: "abc".to_string(),
366            },
367        };
368        assert!(result.allowed);
369        assert_eq!(result.decision, PolicyDecision::Allow);
370        assert_eq!(result.trust_score.agent_id, "agent-1");
371        assert_eq!(result.audit_entry.action, "test");
372    }
373
374    #[test]
375    fn test_audit_filter_default_has_all_none() {
376        let filter = AuditFilter::default();
377        assert!(filter.agent_id.is_none());
378        assert!(filter.action.is_none());
379        assert!(filter.decision.is_none());
380    }
381
382    #[test]
383    fn test_trust_tier_partial_eq() {
384        assert_eq!(TrustTier::Standard, TrustTier::Standard);
385        assert_ne!(TrustTier::Standard, TrustTier::Trusted);
386        assert_eq!(TrustTier::VerifiedPartner, TrustTier::VerifiedPartner);
387        assert_ne!(TrustTier::Untrusted, TrustTier::Probationary);
388    }
389}