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