1use serde::{Deserialize, Serialize};
5
6use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
7
8fn default_true() -> bool {
9 true
10}
11
12fn default_timeout() -> u64 {
13 30
14}
15
16fn default_confirm_patterns() -> Vec<String> {
17 vec![
18 "rm ".into(),
19 "git push -f".into(),
20 "git push --force".into(),
21 "drop table".into(),
22 "drop database".into(),
23 "truncate ".into(),
24 "$(".into(),
25 "`".into(),
26 "<(".into(),
27 ">(".into(),
28 "<<<".into(),
29 "eval ".into(),
30 ]
31}
32
33fn default_audit_destination() -> String {
34 "stdout".into()
35}
36
37fn default_overflow_threshold() -> usize {
38 50_000
39}
40
41fn default_retention_days() -> u64 {
42 7
43}
44
45fn default_max_overflow_bytes() -> usize {
46 10 * 1024 * 1024 }
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct OverflowConfig {
52 #[serde(default = "default_overflow_threshold")]
53 pub threshold: usize,
54 #[serde(default = "default_retention_days")]
55 pub retention_days: u64,
56 #[serde(default = "default_max_overflow_bytes")]
58 pub max_overflow_bytes: usize,
59}
60
61impl Default for OverflowConfig {
62 fn default() -> Self {
63 Self {
64 threshold: default_overflow_threshold(),
65 retention_days: default_retention_days(),
66 max_overflow_bytes: default_max_overflow_bytes(),
67 }
68 }
69}
70
71fn default_anomaly_window() -> usize {
72 10
73}
74
75fn default_anomaly_error_threshold() -> f64 {
76 0.5
77}
78
79fn default_anomaly_critical_threshold() -> f64 {
80 0.8
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize)]
85pub struct AnomalyConfig {
86 #[serde(default)]
87 pub enabled: bool,
88 #[serde(default = "default_anomaly_window")]
89 pub window_size: usize,
90 #[serde(default = "default_anomaly_error_threshold")]
91 pub error_threshold: f64,
92 #[serde(default = "default_anomaly_critical_threshold")]
93 pub critical_threshold: f64,
94}
95
96impl Default for AnomalyConfig {
97 fn default() -> Self {
98 Self {
99 enabled: false,
100 window_size: default_anomaly_window(),
101 error_threshold: default_anomaly_error_threshold(),
102 critical_threshold: default_anomaly_critical_threshold(),
103 }
104 }
105}
106
107#[derive(Debug, Deserialize, Serialize)]
109pub struct ToolsConfig {
110 #[serde(default = "default_true")]
111 pub enabled: bool,
112 #[serde(default = "default_true")]
113 pub summarize_output: bool,
114 #[serde(default)]
115 pub shell: ShellConfig,
116 #[serde(default)]
117 pub scrape: ScrapeConfig,
118 #[serde(default)]
119 pub audit: AuditConfig,
120 #[serde(default)]
121 pub permissions: Option<PermissionsConfig>,
122 #[serde(default)]
123 pub filters: crate::filter::FilterConfig,
124 #[serde(default)]
125 pub overflow: OverflowConfig,
126 #[serde(default)]
127 pub anomaly: AnomalyConfig,
128}
129
130impl ToolsConfig {
131 #[must_use]
133 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
134 let policy = if let Some(ref perms) = self.permissions {
135 PermissionPolicy::from(perms.clone())
136 } else {
137 PermissionPolicy::from_legacy(
138 &self.shell.blocked_commands,
139 &self.shell.confirm_patterns,
140 )
141 };
142 policy.with_autonomy(autonomy_level)
143 }
144}
145
146#[derive(Debug, Deserialize, Serialize)]
148pub struct ShellConfig {
149 #[serde(default = "default_timeout")]
150 pub timeout: u64,
151 #[serde(default)]
152 pub blocked_commands: Vec<String>,
153 #[serde(default)]
154 pub allowed_commands: Vec<String>,
155 #[serde(default)]
156 pub allowed_paths: Vec<String>,
157 #[serde(default = "default_true")]
158 pub allow_network: bool,
159 #[serde(default = "default_confirm_patterns")]
160 pub confirm_patterns: Vec<String>,
161}
162
163#[derive(Debug, Deserialize, Serialize)]
165pub struct AuditConfig {
166 #[serde(default)]
167 pub enabled: bool,
168 #[serde(default = "default_audit_destination")]
169 pub destination: String,
170}
171
172impl Default for ToolsConfig {
173 fn default() -> Self {
174 Self {
175 enabled: true,
176 summarize_output: true,
177 shell: ShellConfig::default(),
178 scrape: ScrapeConfig::default(),
179 audit: AuditConfig::default(),
180 permissions: None,
181 filters: crate::filter::FilterConfig::default(),
182 overflow: OverflowConfig::default(),
183 anomaly: AnomalyConfig::default(),
184 }
185 }
186}
187
188impl Default for ShellConfig {
189 fn default() -> Self {
190 Self {
191 timeout: default_timeout(),
192 blocked_commands: Vec::new(),
193 allowed_commands: Vec::new(),
194 allowed_paths: Vec::new(),
195 allow_network: true,
196 confirm_patterns: default_confirm_patterns(),
197 }
198 }
199}
200
201impl Default for AuditConfig {
202 fn default() -> Self {
203 Self {
204 enabled: false,
205 destination: default_audit_destination(),
206 }
207 }
208}
209
210fn default_scrape_timeout() -> u64 {
211 15
212}
213
214fn default_max_body_bytes() -> usize {
215 4_194_304
216}
217
218#[derive(Debug, Deserialize, Serialize)]
220pub struct ScrapeConfig {
221 #[serde(default = "default_scrape_timeout")]
222 pub timeout: u64,
223 #[serde(default = "default_max_body_bytes")]
224 pub max_body_bytes: usize,
225}
226
227impl Default for ScrapeConfig {
228 fn default() -> Self {
229 Self {
230 timeout: default_scrape_timeout(),
231 max_body_bytes: default_max_body_bytes(),
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn deserialize_default_config() {
242 let toml_str = r#"
243 enabled = true
244
245 [shell]
246 timeout = 60
247 blocked_commands = ["rm -rf /", "sudo"]
248 "#;
249
250 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
251 assert!(config.enabled);
252 assert_eq!(config.shell.timeout, 60);
253 assert_eq!(config.shell.blocked_commands.len(), 2);
254 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
255 assert_eq!(config.shell.blocked_commands[1], "sudo");
256 }
257
258 #[test]
259 fn empty_blocked_commands() {
260 let toml_str = r"
261 [shell]
262 timeout = 30
263 ";
264
265 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
266 assert!(config.enabled);
267 assert_eq!(config.shell.timeout, 30);
268 assert!(config.shell.blocked_commands.is_empty());
269 }
270
271 #[test]
272 fn default_tools_config() {
273 let config = ToolsConfig::default();
274 assert!(config.enabled);
275 assert!(config.summarize_output);
276 assert_eq!(config.shell.timeout, 30);
277 assert!(config.shell.blocked_commands.is_empty());
278 assert!(!config.audit.enabled);
279 }
280
281 #[test]
282 fn tools_summarize_output_default_true() {
283 let config = ToolsConfig::default();
284 assert!(config.summarize_output);
285 }
286
287 #[test]
288 fn tools_summarize_output_parsing() {
289 let toml_str = r"
290 summarize_output = true
291 ";
292 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
293 assert!(config.summarize_output);
294 }
295
296 #[test]
297 fn default_shell_config() {
298 let config = ShellConfig::default();
299 assert_eq!(config.timeout, 30);
300 assert!(config.blocked_commands.is_empty());
301 assert!(config.allowed_paths.is_empty());
302 assert!(config.allow_network);
303 assert!(!config.confirm_patterns.is_empty());
304 }
305
306 #[test]
307 fn deserialize_omitted_fields_use_defaults() {
308 let toml_str = "";
309 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
310 assert!(config.enabled);
311 assert_eq!(config.shell.timeout, 30);
312 assert!(config.shell.blocked_commands.is_empty());
313 assert!(config.shell.allow_network);
314 assert!(!config.shell.confirm_patterns.is_empty());
315 assert_eq!(config.scrape.timeout, 15);
316 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
317 assert!(!config.audit.enabled);
318 assert_eq!(config.audit.destination, "stdout");
319 assert!(config.summarize_output);
320 }
321
322 #[test]
323 fn default_scrape_config() {
324 let config = ScrapeConfig::default();
325 assert_eq!(config.timeout, 15);
326 assert_eq!(config.max_body_bytes, 4_194_304);
327 }
328
329 #[test]
330 fn deserialize_scrape_config() {
331 let toml_str = r"
332 [scrape]
333 timeout = 30
334 max_body_bytes = 2097152
335 ";
336
337 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
338 assert_eq!(config.scrape.timeout, 30);
339 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
340 }
341
342 #[test]
343 fn tools_config_default_includes_scrape() {
344 let config = ToolsConfig::default();
345 assert_eq!(config.scrape.timeout, 15);
346 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
347 }
348
349 #[test]
350 fn deserialize_allowed_commands() {
351 let toml_str = r#"
352 [shell]
353 timeout = 30
354 allowed_commands = ["curl", "wget"]
355 "#;
356
357 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
358 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
359 }
360
361 #[test]
362 fn default_allowed_commands_empty() {
363 let config = ShellConfig::default();
364 assert!(config.allowed_commands.is_empty());
365 }
366
367 #[test]
368 fn deserialize_shell_security_fields() {
369 let toml_str = r#"
370 [shell]
371 allowed_paths = ["/tmp", "/home/user"]
372 allow_network = false
373 confirm_patterns = ["rm ", "drop table"]
374 "#;
375
376 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
377 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
378 assert!(!config.shell.allow_network);
379 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
380 }
381
382 #[test]
383 fn deserialize_audit_config() {
384 let toml_str = r#"
385 [audit]
386 enabled = true
387 destination = "/var/log/zeph-audit.log"
388 "#;
389
390 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
391 assert!(config.audit.enabled);
392 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
393 }
394
395 #[test]
396 fn default_audit_config() {
397 let config = AuditConfig::default();
398 assert!(!config.enabled);
399 assert_eq!(config.destination, "stdout");
400 }
401
402 #[test]
403 fn permission_policy_from_legacy_fields() {
404 let config = ToolsConfig {
405 shell: ShellConfig {
406 blocked_commands: vec!["sudo".to_owned()],
407 confirm_patterns: vec!["rm ".to_owned()],
408 ..ShellConfig::default()
409 },
410 ..ToolsConfig::default()
411 };
412 let policy = config.permission_policy(AutonomyLevel::Supervised);
413 assert_eq!(
414 policy.check("bash", "sudo apt"),
415 crate::permissions::PermissionAction::Deny
416 );
417 assert_eq!(
418 policy.check("bash", "rm file"),
419 crate::permissions::PermissionAction::Ask
420 );
421 }
422
423 #[test]
424 fn permission_policy_from_explicit_config() {
425 let toml_str = r#"
426 [permissions]
427 [[permissions.bash]]
428 pattern = "*sudo*"
429 action = "deny"
430 "#;
431 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
432 let policy = config.permission_policy(AutonomyLevel::Supervised);
433 assert_eq!(
434 policy.check("bash", "sudo rm"),
435 crate::permissions::PermissionAction::Deny
436 );
437 }
438
439 #[test]
440 fn permission_policy_default_uses_legacy() {
441 let config = ToolsConfig::default();
442 assert!(config.permissions.is_none());
443 let policy = config.permission_policy(AutonomyLevel::Supervised);
444 assert!(!config.shell.confirm_patterns.is_empty());
446 assert!(policy.rules().contains_key("bash"));
447 }
448
449 #[test]
450 fn deserialize_overflow_config_full() {
451 let toml_str = r"
452 [overflow]
453 threshold = 100000
454 retention_days = 14
455 max_overflow_bytes = 5242880
456 ";
457 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
458 assert_eq!(config.overflow.threshold, 100_000);
459 assert_eq!(config.overflow.retention_days, 14);
460 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
461 }
462
463 #[test]
464 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
465 let toml_str = r#"
467 [overflow]
468 threshold = 75000
469 dir = "/tmp/overflow"
470 "#;
471 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
472 assert_eq!(config.overflow.threshold, 75_000);
473 }
474
475 #[test]
476 fn deserialize_overflow_config_partial_uses_defaults() {
477 let toml_str = r"
478 [overflow]
479 threshold = 75000
480 ";
481 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
482 assert_eq!(config.overflow.threshold, 75_000);
483 assert_eq!(config.overflow.retention_days, 7);
484 }
485
486 #[test]
487 fn deserialize_overflow_config_omitted_uses_defaults() {
488 let config: ToolsConfig = toml::from_str("").unwrap();
489 assert_eq!(config.overflow.threshold, 50_000);
490 assert_eq!(config.overflow.retention_days, 7);
491 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
492 }
493}