Skip to main content

fd_policy/
decision.rs

1//! Policy decisions
2
3use fd_core::{PolicyDecisionId, PolicyRuleId};
4use serde::{Deserialize, Serialize};
5
6use crate::trace::DecisionTrace;
7
8/// The outcome of a policy evaluation
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PolicyDecision {
11    /// Unique ID for this decision (for audit trail)
12    pub id: PolicyDecisionId,
13
14    /// The decision outcome
15    pub kind: PolicyDecisionKind,
16
17    /// Human-readable explanation
18    pub reason: String,
19
20    /// The rule that triggered this decision (if any)
21    pub rule_id: Option<PolicyRuleId>,
22
23    /// Additional context
24    #[serde(default)]
25    pub metadata: serde_json::Value,
26
27    /// Audit-grade explanation of how this decision was reached: every
28    /// matched verdict, which one fired, and which were overridden by
29    /// precedence. `None` for legacy code paths that haven't been wired
30    /// through the conflict resolver yet — additive on the wire.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub trace: Option<DecisionTrace>,
33}
34
35/// The kind of policy decision
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum PolicyDecisionKind {
39    /// Action is allowed to proceed
40    Allow,
41
42    /// Action is denied
43    Deny,
44
45    /// Action requires human approval before proceeding
46    RequiresApproval,
47
48    /// Action is allowed but with warnings
49    AllowWithWarning,
50}
51
52impl PolicyDecision {
53    pub fn allow(reason: impl Into<String>) -> Self {
54        Self {
55            id: PolicyDecisionId::new(),
56            kind: PolicyDecisionKind::Allow,
57            reason: reason.into(),
58            rule_id: None,
59            metadata: serde_json::Value::Null,
60            trace: None,
61        }
62    }
63
64    pub fn deny(reason: impl Into<String>) -> Self {
65        Self {
66            id: PolicyDecisionId::new(),
67            kind: PolicyDecisionKind::Deny,
68            reason: reason.into(),
69            rule_id: None,
70            metadata: serde_json::Value::Null,
71            trace: None,
72        }
73    }
74
75    pub fn requires_approval(reason: impl Into<String>) -> Self {
76        Self {
77            id: PolicyDecisionId::new(),
78            kind: PolicyDecisionKind::RequiresApproval,
79            reason: reason.into(),
80            rule_id: None,
81            metadata: serde_json::Value::Null,
82            trace: None,
83        }
84    }
85
86    pub fn with_rule(mut self, rule_id: PolicyRuleId) -> Self {
87        self.rule_id = Some(rule_id);
88        self
89    }
90
91    /// Attach an explanation trace produced by
92    /// [`crate::precedence::resolve_conflicts`] +
93    /// [`crate::trace::DecisionTrace::from_resolution`].
94    pub fn with_trace(mut self, trace: DecisionTrace) -> Self {
95        self.trace = Some(trace);
96        self
97    }
98
99    pub fn is_allowed(&self) -> bool {
100        matches!(
101            self.kind,
102            PolicyDecisionKind::Allow | PolicyDecisionKind::AllowWithWarning
103        )
104    }
105
106    pub fn is_denied(&self) -> bool {
107        matches!(self.kind, PolicyDecisionKind::Deny)
108    }
109
110    pub fn needs_approval(&self) -> bool {
111        matches!(self.kind, PolicyDecisionKind::RequiresApproval)
112    }
113}