1use serde::{Deserialize, Serialize};
4
5const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 1800;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct AutonomyConfig {
11 #[serde(default)]
13 pub self_improve: SelfImprovementConfig,
14 #[serde(default)]
16 pub safety: SafetyConfig,
17 #[serde(default)]
19 pub git_workflow: GitWorkflowConfig,
20 #[serde(default)]
22 pub crash_recovery: CrashRecoveryConfig,
23 #[serde(default)]
25 pub gpio: GpioConfig,
26 #[serde(default)]
28 pub scheduler: SchedulerConfig,
29 #[serde(default)]
31 pub reactor: ReactorConfig,
32 #[serde(default)]
34 pub services: ServiceConfig,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SelfImprovementConfig {
40 pub max_cycles: u32,
42 pub max_budget: f64,
44 pub dry_run: bool,
46 pub strategies: Vec<String>,
48 pub agent_iterations: u32,
50 pub max_diff_per_task: u32,
52 pub max_total_diff: u32,
54 pub create_prs: bool,
56 pub branch_prefix: String,
58 pub model: Option<String>,
60 pub provider: Option<String>,
62 pub circuit_breaker_threshold: u32,
64}
65
66impl Default for SelfImprovementConfig {
67 fn default() -> Self {
68 Self {
69 max_cycles: 10,
70 max_budget: 10.0,
71 dry_run: false,
72 strategies: Vec::new(),
73 agent_iterations: 25,
74 max_diff_per_task: 200,
75 max_total_diff: 1000,
76 create_prs: false,
77 branch_prefix: "self-improve/".to_string(),
78 model: None,
79 provider: None,
80 circuit_breaker_threshold: 3,
81 }
82 }
83}
84
85impl SelfImprovementConfig {
86 pub fn is_strategy_enabled(&self, name: &str) -> bool {
88 self.strategies.is_empty() || self.strategies.iter().any(|s| s == name)
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct StrategyConfig {
95 pub repo_path: String,
97 pub max_tasks_per_strategy: usize,
99}
100
101impl Default for StrategyConfig {
102 fn default() -> Self {
103 Self {
104 repo_path: ".".to_string(),
105 max_tasks_per_strategy: 5,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SafetyConfig {
116 pub max_total_cost: f64,
118 pub max_per_operation_cost: f64,
120 pub max_daily_operations: u32,
122 pub circuit_breaker_threshold: u32,
124 pub circuit_breaker_cooldown_secs: u64,
126 pub max_diff_per_task: u32,
128 pub max_total_diff: u32,
130 pub max_concurrent_agents: u32,
132 pub heartbeat_timeout_secs: u64,
134 pub allowed_paths: Vec<String>,
136 pub forbidden_paths: Vec<String>,
138}
139
140impl Default for SafetyConfig {
141 fn default() -> Self {
142 Self {
143 max_total_cost: 50.0,
144 max_per_operation_cost: 5.0,
145 max_daily_operations: 100,
146 circuit_breaker_threshold: 3,
147 circuit_breaker_cooldown_secs: 300,
148 max_diff_per_task: 200,
149 max_total_diff: 1000,
150 max_concurrent_agents: 5,
151 heartbeat_timeout_secs: DEFAULT_HEARTBEAT_TIMEOUT_SECS,
152 allowed_paths: Vec::new(),
153 forbidden_paths: Vec::new(),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct GitWorkflowConfig {
161 pub branch_prefix: String,
163 pub auto_merge: bool,
165 pub merge_method: String,
167 pub min_confidence: f64,
169 #[serde(default)]
171 pub webhook: WebhookConfig,
172}
173
174impl Default for GitWorkflowConfig {
175 fn default() -> Self {
176 Self {
177 branch_prefix: "autonomy/".to_string(),
178 auto_merge: false,
179 merge_method: "squash".to_string(),
180 min_confidence: 0.7,
181 webhook: WebhookConfig::default(),
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct WebhookConfig {
189 pub listen_addr: String,
191 pub port: u16,
193 pub secret: Option<String>,
195 #[serde(default = "default_webhook_log_dir")]
197 pub log_dir: String,
198 #[serde(default = "default_webhook_keep_days")]
200 pub keep_days: u32,
201 #[serde(default)]
203 pub repos: std::collections::HashMap<String, WebhookRepoConfig>,
204}
205
206fn default_webhook_log_dir() -> String {
207 dirs::home_dir()
208 .map(|h| {
209 h.join(".brainwires")
210 .join("webhook-logs")
211 .to_string_lossy()
212 .to_string()
213 })
214 .unwrap_or_else(|| "/tmp/brainwires/webhook-logs".to_string())
215}
216
217fn default_webhook_keep_days() -> u32 {
218 30
219}
220
221impl Default for WebhookConfig {
222 fn default() -> Self {
223 Self {
224 listen_addr: "0.0.0.0".to_string(),
225 port: 3000,
226 secret: None,
227 log_dir: default_webhook_log_dir(),
228 keep_days: default_webhook_keep_days(),
229 repos: std::collections::HashMap::new(),
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct WebhookRepoConfig {
237 #[serde(default)]
239 pub events: Vec<String>,
240 #[serde(default)]
242 pub auto_investigate: bool,
243 #[serde(default)]
245 pub auto_fix: bool,
246 #[serde(default)]
248 pub auto_merge: bool,
249 #[serde(default)]
251 pub labels_filter: Vec<String>,
252 #[serde(default)]
254 pub post_commands: Vec<CommandConfig>,
255}
256
257impl Default for WebhookRepoConfig {
258 fn default() -> Self {
259 Self {
260 events: vec!["issues".to_string()],
261 auto_investigate: true,
262 auto_fix: false,
263 auto_merge: false,
264 labels_filter: Vec::new(),
265 post_commands: Vec::new(),
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct CommandConfig {
273 pub cmd: String,
275 #[serde(default)]
277 pub args: Vec<String>,
278 #[serde(default)]
280 pub working_dir: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct CrashRecoveryConfig {
286 pub max_fix_attempts: u32,
288 pub state_file: String,
290 pub enabled: bool,
292}
293
294impl Default for CrashRecoveryConfig {
295 fn default() -> Self {
296 Self {
297 max_fix_attempts: 3,
298 state_file: dirs::home_dir()
299 .map(|h| {
300 h.join(".brainwires")
301 .join("crash-recovery.json")
302 .to_string_lossy()
303 .to_string()
304 })
305 .unwrap_or_else(|| "/tmp/brainwires/crash-recovery.json".to_string()),
306 enabled: true,
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct GpioConfig {
314 #[serde(default)]
316 pub allowed_pins: Vec<(u32, u32)>,
317 pub max_concurrent_pins: usize,
319 pub auto_release_timeout_secs: u64,
321}
322
323impl Default for GpioConfig {
324 fn default() -> Self {
325 Self {
326 allowed_pins: Vec::new(),
327 max_concurrent_pins: 4,
328 auto_release_timeout_secs: 300,
329 }
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct SchedulerConfig {
336 pub max_concurrent_tasks: u32,
338 #[serde(default)]
340 pub tasks: Vec<ScheduledTaskDef>,
341}
342
343impl Default for SchedulerConfig {
344 fn default() -> Self {
345 Self {
346 max_concurrent_tasks: 3,
347 tasks: Vec::new(),
348 }
349 }
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct ScheduledTaskDef {
355 pub id: String,
357 pub name: String,
359 pub cron_expression: String,
361 pub task_type: String,
363 #[serde(default = "default_true")]
365 pub enabled: bool,
366 #[serde(default = "default_max_runtime")]
368 pub max_runtime_secs: u64,
369}
370
371fn default_true() -> bool {
372 true
373}
374
375fn default_max_runtime() -> u64 {
376 3600
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct ReactorConfig {
382 pub max_events_per_minute: u32,
384 pub global_debounce_ms: u64,
386 pub max_watch_depth: u32,
388 #[serde(default)]
390 pub rules: Vec<ReactorRuleDef>,
391}
392
393impl Default for ReactorConfig {
394 fn default() -> Self {
395 Self {
396 max_events_per_minute: 60,
397 global_debounce_ms: 500,
398 max_watch_depth: 10,
399 rules: Vec::new(),
400 }
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct ReactorRuleDef {
407 pub id: String,
409 pub name: String,
411 pub watch_paths: Vec<String>,
413 #[serde(default)]
415 pub patterns: Vec<String>,
416 #[serde(default)]
418 pub exclude_patterns: Vec<String>,
419 #[serde(default)]
421 pub event_types: Vec<String>,
422 #[serde(default = "default_debounce")]
424 pub debounce_ms: u64,
425 #[serde(default = "default_true")]
427 pub enabled: bool,
428}
429
430fn default_debounce() -> u64 {
431 1000
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ServiceConfig {
437 #[serde(default)]
439 pub allowed_services: Vec<String>,
440 #[serde(default)]
442 pub forbidden_services: Vec<String>,
443 #[serde(default = "default_true")]
445 pub read_only: bool,
446 #[serde(default)]
448 pub docker_socket_path: Option<String>,
449}
450
451impl Default for ServiceConfig {
452 fn default() -> Self {
453 Self {
454 allowed_services: Vec::new(),
455 forbidden_services: Vec::new(),
456 read_only: true,
457 docker_socket_path: None,
458 }
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn autonomy_config_default_succeeds() {
468 let config = AutonomyConfig::default();
469 let _si: &SelfImprovementConfig = &config.self_improve;
471 let _safety: &SafetyConfig = &config.safety;
472 let _git: &GitWorkflowConfig = &config.git_workflow;
473 }
474
475 #[test]
476 fn self_improvement_config_default_has_sensible_values() {
477 let config = SelfImprovementConfig::default();
478 assert_eq!(config.max_cycles, 10);
479 assert!((config.max_budget - 10.0).abs() < f64::EPSILON);
480 assert!(!config.dry_run);
481 assert!(config.strategies.is_empty());
482 assert_eq!(config.agent_iterations, 25);
483 assert_eq!(config.circuit_breaker_threshold, 3);
484 assert_eq!(config.branch_prefix, "self-improve/");
485 assert!(config.model.is_none());
486 assert!(config.provider.is_none());
487 }
488
489 #[test]
490 fn safety_config_default_has_sensible_values() {
491 let config = SafetyConfig::default();
492 assert!((config.max_total_cost - 50.0).abs() < f64::EPSILON);
493 assert!((config.max_per_operation_cost - 5.0).abs() < f64::EPSILON);
494 assert_eq!(config.max_daily_operations, 100);
495 assert_eq!(config.circuit_breaker_threshold, 3);
496 assert_eq!(config.circuit_breaker_cooldown_secs, 300);
497 assert_eq!(config.max_concurrent_agents, 5);
498 assert_eq!(config.heartbeat_timeout_secs, 1800);
499 assert!(config.allowed_paths.is_empty());
500 assert!(config.forbidden_paths.is_empty());
501 }
502
503 #[test]
504 fn git_workflow_config_default_has_sensible_values() {
505 let config = GitWorkflowConfig::default();
506 assert_eq!(config.branch_prefix, "autonomy/");
507 assert!(!config.auto_merge);
508 assert_eq!(config.merge_method, "squash");
509 assert!((config.min_confidence - 0.7).abs() < f64::EPSILON);
510 assert_eq!(config.webhook.port, 3000);
511 }
512
513 #[test]
514 fn serde_roundtrip_autonomy_config() {
515 let config = AutonomyConfig::default();
516 let json = serde_json::to_string(&config).expect("serialize");
517 let deserialized: AutonomyConfig = serde_json::from_str(&json).expect("deserialize");
518 assert_eq!(
519 deserialized.self_improve.max_cycles,
520 config.self_improve.max_cycles
521 );
522 assert_eq!(
523 deserialized.safety.max_total_cost,
524 config.safety.max_total_cost
525 );
526 assert_eq!(
527 deserialized.git_workflow.branch_prefix,
528 config.git_workflow.branch_prefix
529 );
530 }
531
532 #[test]
533 fn is_strategy_enabled_empty_list_enables_all() {
534 let config = SelfImprovementConfig::default();
535 assert!(config.is_strategy_enabled("clippy"));
536 assert!(config.is_strategy_enabled("anything"));
537 }
538
539 #[test]
540 fn is_strategy_enabled_specific_list() {
541 let config = SelfImprovementConfig {
542 strategies: vec!["clippy".to_string(), "todo".to_string()],
543 ..Default::default()
544 };
545 assert!(config.is_strategy_enabled("clippy"));
546 assert!(config.is_strategy_enabled("todo"));
547 assert!(!config.is_strategy_enabled("dead_code"));
548 }
549}