agent_diva_core/security/
config.rs1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SecurityLevel {
10 Permissive,
12 #[default]
14 Standard,
15 Strict,
17 Paranoid,
19}
20
21impl SecurityLevel {
22 pub fn default_max_actions_per_hour(&self) -> u32 {
24 match self {
25 Self::Permissive => 1000,
26 Self::Standard => 100,
27 Self::Strict => 50,
28 Self::Paranoid => 20,
29 }
30 }
31
32 pub fn default_workspace_only(&self) -> bool {
34 match self {
35 Self::Permissive => false,
36 Self::Standard | Self::Strict | Self::Paranoid => true,
37 }
38 }
39
40 pub fn is_read_only(&self) -> bool {
42 matches!(self, Self::Paranoid)
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(default)]
49pub struct SecurityConfig {
50 pub level: SecurityLevel,
52
53 pub workspace_only: bool,
55
56 pub max_actions_per_hour: u32,
58
59 pub forbidden_paths: Vec<String>,
61
62 pub allowed_roots: Vec<PathBuf>,
64
65 pub forbidden_extensions: Vec<String>,
67
68 pub read_only: Option<bool>,
70
71 pub max_file_size: u64,
73
74 pub allow_symlinks: bool,
76}
77
78impl Default for SecurityConfig {
79 fn default() -> Self {
80 Self {
81 level: SecurityLevel::default(),
82 workspace_only: true,
83 max_actions_per_hour: 100,
84 forbidden_paths: vec![
85 "/etc".to_string(),
86 "/root".to_string(),
87 "/sys".to_string(),
88 "/proc".to_string(),
89 "~/.ssh".to_string(),
90 "~/.gnupg".to_string(),
91 "~/.aws".to_string(),
92 ],
93 allowed_roots: Vec::new(),
94 forbidden_extensions: vec![
95 ".exe".to_string(),
96 ".dll".to_string(),
97 ".bat".to_string(),
98 ".cmd".to_string(),
99 ".sh".to_string(),
100 ],
101 read_only: None,
102 max_file_size: 10 * 1024 * 1024, allow_symlinks: false,
104 }
105 }
106}
107
108impl SecurityConfig {
109 pub fn from_level(level: SecurityLevel) -> Self {
111 Self {
112 level,
113 workspace_only: level.default_workspace_only(),
114 max_actions_per_hour: level.default_max_actions_per_hour(),
115 read_only: Some(level.is_read_only()),
116 ..Self::default()
117 }
118 }
119
120 pub fn is_read_only(&self) -> bool {
122 self.read_only.unwrap_or_else(|| self.level.is_read_only())
123 }
124
125 pub fn validate(&self) -> Result<(), String> {
127 if self.max_actions_per_hour == 0 {
128 return Err("max_actions_per_hour must be greater than 0".to_string());
129 }
130
131 for path in &self.forbidden_paths {
132 if path.contains('\0') {
133 return Err(format!("Forbidden path contains null byte: {}", path));
134 }
135 }
136
137 Ok(())
138 }
139
140 pub fn merge(&mut self, other: SecurityConfig) {
142 if other.level != SecurityLevel::default() {
143 self.level = other.level;
144 }
145 if !other.forbidden_paths.is_empty() {
146 self.forbidden_paths = other.forbidden_paths;
147 }
148 if !other.allowed_roots.is_empty() {
149 self.allowed_roots = other.allowed_roots;
150 }
151 if other.max_file_size != 0 {
152 self.max_file_size = other.max_file_size;
153 }
154 if other.read_only.is_some() {
155 self.read_only = other.read_only;
156 }
157 if other.max_actions_per_hour != SecurityLevel::default().default_max_actions_per_hour() {
160 self.max_actions_per_hour = other.max_actions_per_hour;
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_security_level_defaults() {
171 assert_eq!(
172 SecurityLevel::Permissive.default_max_actions_per_hour(),
173 1000
174 );
175 assert_eq!(SecurityLevel::Standard.default_max_actions_per_hour(), 100);
176 assert_eq!(SecurityLevel::Strict.default_max_actions_per_hour(), 50);
177 assert_eq!(SecurityLevel::Paranoid.default_max_actions_per_hour(), 20);
178
179 assert!(!SecurityLevel::Permissive.default_workspace_only());
180 assert!(SecurityLevel::Standard.default_workspace_only());
181 assert!(SecurityLevel::Paranoid.is_read_only());
182 }
183
184 #[test]
185 fn test_config_from_level() {
186 let config = SecurityConfig::from_level(SecurityLevel::Strict);
187 assert_eq!(config.max_actions_per_hour, 50);
188 assert!(config.workspace_only);
189 assert!(!config.is_read_only());
190
191 let config = SecurityConfig::from_level(SecurityLevel::Paranoid);
192 assert!(config.is_read_only());
193 }
194
195 #[test]
196 fn test_config_validation() {
197 let mut config = SecurityConfig::default();
198 assert!(config.validate().is_ok());
199
200 config.max_actions_per_hour = 0;
201 assert!(config.validate().is_err());
202 }
203
204 #[test]
205 fn test_config_merge() {
206 let mut base = SecurityConfig::default();
207 let other = SecurityConfig {
208 level: SecurityLevel::Strict,
209 max_actions_per_hour: 50,
210 ..Default::default()
211 };
212
213 base.merge(other);
214 assert_eq!(base.level, SecurityLevel::Strict);
215 assert_eq!(base.max_actions_per_hour, 50);
216 }
217}