Skip to main content

assay_core/
on_error.rs

1// on_error.rs - Fail-safe configuration for Assay
2//
3// This module defines the error handling policy for policy checks.
4// See docs/concepts/fail-safe.md for usage documentation.
5
6use serde::{Deserialize, Serialize};
7
8/// Error handling policy for policy checks.
9///
10/// Determines what happens when Assay encounters an error during evaluation
11/// (e.g., schema parse failure, network timeout, unexpected exception).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ErrorPolicy {
15    /// Fail-closed: Deny action on error (default, safer)
16    ///
17    /// Use when:
18    /// - Compliance requirements mandate fail-safe behavior
19    /// - Safety-critical environment
20    /// - False negatives are worse than false positives
21    #[default]
22    Block,
23
24    /// Fail-open: Permit action on error
25    ///
26    /// Use when:
27    /// - Availability is more important than enforcement
28    /// - Development/testing environment
29    /// - Other layers of defense exist
30    Allow,
31}
32
33impl ErrorPolicy {
34    /// Returns true if this policy blocks on error
35    pub fn blocks_on_error(&self) -> bool {
36        matches!(self, ErrorPolicy::Block)
37    }
38
39    /// Returns true if this policy allows on error
40    pub fn allows_on_error(&self) -> bool {
41        matches!(self, ErrorPolicy::Allow)
42    }
43
44    /// Apply the policy to an error, returning the appropriate TestStatus
45    pub fn apply_to_error(&self, error: &anyhow::Error) -> ErrorPolicyResult {
46        match self {
47            ErrorPolicy::Block => ErrorPolicyResult::Blocked {
48                reason: format!("Policy check error (fail-closed): {}", error),
49            },
50            ErrorPolicy::Allow => ErrorPolicyResult::Allowed {
51                warning: format!("Policy check error (fail-open): {}", error),
52            },
53        }
54    }
55}
56
57/// Result of applying an error policy
58#[derive(Debug, Clone)]
59pub enum ErrorPolicyResult {
60    /// Action was blocked due to error (fail-closed)
61    Blocked { reason: String },
62    /// Action was allowed despite error (fail-open)
63    Allowed { warning: String },
64}
65
66/// Logs a fail-safe trigger event in structured JSON format.
67///
68/// This provides Ops teams with a machine-readable audit trail when the
69/// fail-safe mechanism permits execution despite an error.
70pub fn log_fail_safe(reason: &str, config_path: Option<&str>) {
71    // SOTA: Use structured tracing instead of println
72    // This allows the binary (CLI or MCP server) to route logs to OTLP/Datadog
73    tracing::warn!(
74        event = "assay.failsafe.triggered",
75        reason = %reason,
76        config_path = %config_path.unwrap_or("none"),
77        action = "allowed",
78        "Fail-safe triggered: {}", reason
79    );
80}
81
82impl ErrorPolicyResult {
83    pub fn is_blocked(&self) -> bool {
84        matches!(self, ErrorPolicyResult::Blocked { .. })
85    }
86
87    pub fn is_allowed(&self) -> bool {
88        matches!(self, ErrorPolicyResult::Allowed { .. })
89    }
90
91    pub fn message(&self) -> &str {
92        match self {
93            ErrorPolicyResult::Blocked { reason } => reason,
94            ErrorPolicyResult::Allowed { warning } => warning,
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_default_is_block() {
105        assert_eq!(ErrorPolicy::default(), ErrorPolicy::Block);
106    }
107
108    #[test]
109    fn test_block_policy() {
110        let policy = ErrorPolicy::Block;
111        assert!(policy.blocks_on_error());
112        assert!(!policy.allows_on_error());
113
114        let error = anyhow::anyhow!("Schema parse failed");
115        let result = policy.apply_to_error(&error);
116        assert!(result.is_blocked());
117    }
118
119    #[test]
120    fn test_allow_policy() {
121        let policy = ErrorPolicy::Allow;
122        assert!(!policy.blocks_on_error());
123        assert!(policy.allows_on_error());
124
125        let error = anyhow::anyhow!("Network timeout");
126        let result = policy.apply_to_error(&error);
127        assert!(result.is_allowed());
128    }
129
130    #[test]
131    fn test_serde_roundtrip() {
132        let block: ErrorPolicy = serde_yaml::from_str("block").unwrap();
133        assert_eq!(block, ErrorPolicy::Block);
134
135        let allow: ErrorPolicy = serde_yaml::from_str("allow").unwrap();
136        assert_eq!(allow, ErrorPolicy::Allow);
137
138        // Test in struct context
139        #[derive(Deserialize)]
140        struct Config {
141            on_error: ErrorPolicy,
142        }
143
144        let config: Config = serde_yaml::from_str("on_error: block").unwrap();
145        assert_eq!(config.on_error, ErrorPolicy::Block);
146
147        let config: Config = serde_yaml::from_str("on_error: allow").unwrap();
148        assert_eq!(config.on_error, ErrorPolicy::Allow);
149    }
150}