agent_diva_core/security/
error.rs1use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Error, Debug, Clone)]
8pub enum SecurityError {
9 #[error("Path not allowed: {path}")]
11 PathNotAllowed { path: String },
12
13 #[error("Path escapes workspace: {resolved:?}")]
15 PathEscapesWorkspace { resolved: PathBuf },
16
17 #[error("Path contains forbidden component: {component}")]
19 ForbiddenComponent { component: String },
20
21 #[error("Rate limit exceeded: {count} actions in the last hour (max: {max})")]
23 RateLimitExceeded { count: usize, max: u32 },
24
25 #[error("Action budget exhausted")]
27 ActionBudgetExhausted,
28
29 #[error("Read-only mode: write operations are not allowed")]
31 ReadOnlyMode,
32
33 #[error("Symbolic links are not allowed: {path}")]
35 SymlinkNotAllowed { path: PathBuf },
36
37 #[error("Invalid path format: {reason}")]
39 InvalidPathFormat { reason: String },
40
41 #[error("File too large: {size} bytes (max: {max_size})")]
43 FileTooLarge { size: u64, max_size: u64 },
44
45 #[error("Forbidden file extension: {ext}")]
47 ForbiddenExtension { ext: String },
48}
49
50impl SecurityError {
51 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 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}