Skip to main content

guardrail_core/
decision.rs

1//! Decision types returned by pipeline stages.
2
3use crate::request::GuardrailRequest;
4
5/// The outcome of a single stage evaluation.
6///
7/// Stages return a `Decision` to indicate whether the request should proceed,
8/// be modified, or be stopped entirely.
9#[derive(Debug, Clone, PartialEq)]
10pub enum Decision {
11    /// Request is clean; proceed to the next stage.
12    Allow,
13    /// Request contained sensitive content; `mutated` holds the sanitized version.
14    Redact {
15        /// Human-readable reason for the redaction.
16        reason: String,
17        /// The sanitized request to pass to subsequent stages.
18        mutated: GuardrailRequest,
19        /// Machine-readable list of entity-type names that were redacted
20        /// (e.g. `["email", "phone"]`), for structured logging and the
21        /// audit trail. Mirrors how [`Decision::Block`] pairs a
22        /// human-readable `reason` with a machine-readable `code`. Empty
23        /// for stages that redact without a typed entity taxonomy (e.g. a
24        /// custom stage that redacts based on a free-form policy match).
25        entities: Vec<String>,
26    },
27    /// Request must be blocked; do not forward to the upstream.
28    Block {
29        /// Human-readable reason for blocking.
30        reason: String,
31        /// Machine-readable block code for programmatic handling.
32        code: BlockCode,
33    },
34}
35
36impl Decision {
37    /// Returns `true` if the request should be forwarded upstream (`Allow` or `Redact`).
38    ///
39    /// # Examples
40    ///
41    /// ```rust
42    /// use guardrail_core::decision::{Decision, BlockCode};
43    ///
44    /// assert!(Decision::Allow.should_forward());
45    /// assert!(!Decision::Block {
46    ///     reason: "injection".into(),
47    ///     code: BlockCode::PromptInjection
48    /// }.should_forward());
49    /// ```
50    pub fn should_forward(&self) -> bool {
51        !matches!(self, Decision::Block { .. })
52    }
53
54    /// Returns the name of this decision variant as a static string, for logging.
55    pub fn name(&self) -> &'static str {
56        match self {
57            Decision::Allow => "allow",
58            Decision::Redact { .. } => "redact",
59            Decision::Block { .. } => "block",
60        }
61    }
62}
63
64/// Machine-readable codes for blocked requests.
65///
66/// Used in the JSON error response body and audit log.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum BlockCode {
69    /// A prompt injection pattern was detected.
70    PromptInjection,
71    /// Toxic or harmful content was detected.
72    Toxicity,
73    /// A user-defined policy rule was violated.
74    PolicyViolation,
75    /// The request was rate-limited.
76    RateLimit,
77    /// A custom block code defined by a user stage.
78    Custom(String),
79}
80
81impl BlockCode {
82    /// Returns the snake_case string representation for use in JSON/logs.
83    ///
84    /// # Examples
85    ///
86    /// ```rust
87    /// use guardrail_core::decision::BlockCode;
88    ///
89    /// assert_eq!(BlockCode::PromptInjection.as_str(), "prompt_injection");
90    /// assert_eq!(BlockCode::Custom("my_rule".into()).as_str(), "my_rule");
91    /// ```
92    pub fn as_str(&self) -> &str {
93        match self {
94            BlockCode::PromptInjection => "prompt_injection",
95            BlockCode::Toxicity => "toxicity",
96            BlockCode::PolicyViolation => "policy_violation",
97            BlockCode::RateLimit => "rate_limit",
98            BlockCode::Custom(s) => s.as_str(),
99        }
100    }
101}
102
103impl std::fmt::Display for BlockCode {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}", self.as_str())
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_should_forward_allow() {
115        assert!(Decision::Allow.should_forward());
116    }
117
118    #[test]
119    fn test_should_forward_block() {
120        let d = Decision::Block {
121            reason: "test".into(),
122            code: BlockCode::PromptInjection,
123        };
124        assert!(!d.should_forward());
125    }
126
127    #[test]
128    fn test_block_code_as_str() {
129        assert_eq!(BlockCode::PromptInjection.as_str(), "prompt_injection");
130        assert_eq!(BlockCode::Toxicity.as_str(), "toxicity");
131        assert_eq!(BlockCode::PolicyViolation.as_str(), "policy_violation");
132        assert_eq!(BlockCode::RateLimit.as_str(), "rate_limit");
133        assert_eq!(BlockCode::Custom("foo_bar".into()).as_str(), "foo_bar");
134    }
135
136    #[test]
137    fn test_decision_name() {
138        assert_eq!(Decision::Allow.name(), "allow");
139        assert_eq!(
140            Decision::Block {
141                reason: "x".into(),
142                code: BlockCode::Toxicity
143            }
144            .name(),
145            "block"
146        );
147    }
148}