Skip to main content

agent_diva_core/security/
error.rs

1//! Security-related error types
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Errors that can occur during security policy enforcement
7#[derive(Error, Debug, Clone)]
8pub enum SecurityError {
9    /// Path is not allowed by security policy
10    #[error("Path not allowed: {path}")]
11    PathNotAllowed { path: String },
12
13    /// Resolved path escapes allowed workspace
14    #[error("Path escapes workspace: {resolved:?}")]
15    PathEscapesWorkspace { resolved: PathBuf },
16
17    /// Path contains forbidden component (e.g., parent dir)
18    #[error("Path contains forbidden component: {component}")]
19    ForbiddenComponent { component: String },
20
21    /// Rate limit exceeded
22    #[error("Rate limit exceeded: {count} actions in the last hour (max: {max})")]
23    RateLimitExceeded { count: usize, max: u32 },
24
25    /// Action budget exhausted
26    #[error("Action budget exhausted")]
27    ActionBudgetExhausted,
28
29    /// Read-only mode
30    #[error("Read-only mode: write operations are not allowed")]
31    ReadOnlyMode,
32
33    /// Path is a symbolic link (when not allowed)
34    #[error("Symbolic links are not allowed: {path}")]
35    SymlinkNotAllowed { path: PathBuf },
36
37    /// Invalid path format
38    #[error("Invalid path format: {reason}")]
39    InvalidPathFormat { reason: String },
40
41    /// File too large
42    #[error("File too large: {size} bytes (max: {max_size})")]
43    FileTooLarge { size: u64, max_size: u64 },
44
45    /// Forbidden file extension
46    #[error("Forbidden file extension: {ext}")]
47    ForbiddenExtension { ext: String },
48}
49
50impl SecurityError {
51    /// Get a user-friendly error message
52    pub fn user_message(&self) -> String {
53        match self {
54            Self::PathNotAllowed { path } => {
55                format!("Access to '{}' is not allowed by security policy", path)
56            }
57            Self::PathEscapesWorkspace { .. } => {
58                "The specified path is outside the allowed workspace".to_string()
59            }
60            Self::ForbiddenComponent { component } => {
61                format!("Path contains forbidden component: {}", component)
62            }
63            Self::RateLimitExceeded { count, max } => {
64                format!(
65                    "Too many file operations ({} in the last hour, max: {}). Please try again later.",
66                    count, max
67                )
68            }
69            Self::ActionBudgetExhausted => {
70                "Action budget exhausted. Please try again later.".to_string()
71            }
72            Self::ReadOnlyMode => "Write operations are disabled in read-only mode".to_string(),
73            Self::SymlinkNotAllowed { path } => {
74                format!("Symbolic links are not allowed: {}", path.display())
75            }
76            Self::InvalidPathFormat { reason } => {
77                format!("Invalid path: {}", reason)
78            }
79            Self::FileTooLarge { size, max_size } => {
80                format!("File too large ({} bytes, max: {} bytes)", size, max_size)
81            }
82            Self::ForbiddenExtension { ext } => {
83                format!("Files with extension '{}' are not allowed", ext)
84            }
85        }
86    }
87
88    /// Check if this error is retryable
89    pub fn is_retryable(&self) -> bool {
90        matches!(
91            self,
92            Self::RateLimitExceeded { .. } | Self::ActionBudgetExhausted
93        )
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_error_messages() {
103        let err = SecurityError::PathNotAllowed {
104            path: "/etc/passwd".to_string(),
105        };
106        assert!(err.user_message().contains("not allowed"));
107
108        let err = SecurityError::RateLimitExceeded {
109            count: 150,
110            max: 100,
111        };
112        assert!(err.user_message().contains("Too many"));
113    }
114
115    #[test]
116    fn test_is_retryable() {
117        assert!(SecurityError::RateLimitExceeded { count: 1, max: 0 }.is_retryable());
118
119        assert!(!SecurityError::PathNotAllowed {
120            path: "/test".to_string()
121        }
122        .is_retryable());
123    }
124}