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