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}