use serde::{Deserialize, Serialize};
const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 1800;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AutonomyConfig {
#[serde(default)]
pub self_improve: SelfImprovementConfig,
#[serde(default)]
pub safety: SafetyConfig,
#[serde(default)]
pub git_workflow: GitWorkflowConfig,
#[serde(default)]
pub crash_recovery: CrashRecoveryConfig,
#[serde(default)]
pub gpio: GpioConfig,
#[serde(default)]
pub scheduler: SchedulerConfig,
#[serde(default)]
pub reactor: ReactorConfig,
#[serde(default)]
pub services: ServiceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelfImprovementConfig {
pub max_cycles: u32,
pub max_budget: f64,
pub dry_run: bool,
pub strategies: Vec<String>,
pub agent_iterations: u32,
pub max_diff_per_task: u32,
pub max_total_diff: u32,
pub create_prs: bool,
pub branch_prefix: String,
pub model: Option<String>,
pub provider: Option<String>,
pub circuit_breaker_threshold: u32,
}
impl Default for SelfImprovementConfig {
fn default() -> Self {
Self {
max_cycles: 10,
max_budget: 10.0,
dry_run: false,
strategies: Vec::new(),
agent_iterations: 25,
max_diff_per_task: 200,
max_total_diff: 1000,
create_prs: false,
branch_prefix: "self-improve/".to_string(),
model: None,
provider: None,
circuit_breaker_threshold: 3,
}
}
}
impl SelfImprovementConfig {
pub fn is_strategy_enabled(&self, name: &str) -> bool {
self.strategies.is_empty() || self.strategies.iter().any(|s| s == name)
}
}
#[derive(Debug, Clone)]
pub struct StrategyConfig {
pub repo_path: String,
pub max_tasks_per_strategy: usize,
}
impl Default for StrategyConfig {
fn default() -> Self {
Self {
repo_path: ".".to_string(),
max_tasks_per_strategy: 5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetyConfig {
pub max_total_cost: f64,
pub max_per_operation_cost: f64,
pub max_daily_operations: u32,
pub circuit_breaker_threshold: u32,
pub circuit_breaker_cooldown_secs: u64,
pub max_diff_per_task: u32,
pub max_total_diff: u32,
pub max_concurrent_agents: u32,
pub heartbeat_timeout_secs: u64,
pub allowed_paths: Vec<String>,
pub forbidden_paths: Vec<String>,
}
impl Default for SafetyConfig {
fn default() -> Self {
Self {
max_total_cost: 50.0,
max_per_operation_cost: 5.0,
max_daily_operations: 100,
circuit_breaker_threshold: 3,
circuit_breaker_cooldown_secs: 300,
max_diff_per_task: 200,
max_total_diff: 1000,
max_concurrent_agents: 5,
heartbeat_timeout_secs: DEFAULT_HEARTBEAT_TIMEOUT_SECS,
allowed_paths: Vec::new(),
forbidden_paths: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitWorkflowConfig {
pub branch_prefix: String,
pub auto_merge: bool,
pub merge_method: String,
pub min_confidence: f64,
#[serde(default)]
pub webhook: WebhookConfig,
}
impl Default for GitWorkflowConfig {
fn default() -> Self {
Self {
branch_prefix: "autonomy/".to_string(),
auto_merge: false,
merge_method: "squash".to_string(),
min_confidence: 0.7,
webhook: WebhookConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
pub listen_addr: String,
pub port: u16,
pub secret: Option<String>,
#[serde(default = "default_webhook_log_dir")]
pub log_dir: String,
#[serde(default = "default_webhook_keep_days")]
pub keep_days: u32,
#[serde(default)]
pub repos: std::collections::HashMap<String, WebhookRepoConfig>,
}
fn default_webhook_log_dir() -> String {
dirs::home_dir()
.map(|h| {
h.join(".brainwires")
.join("webhook-logs")
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| "/tmp/brainwires/webhook-logs".to_string())
}
fn default_webhook_keep_days() -> u32 {
30
}
impl Default for WebhookConfig {
fn default() -> Self {
Self {
listen_addr: "0.0.0.0".to_string(),
port: 3000,
secret: None,
log_dir: default_webhook_log_dir(),
keep_days: default_webhook_keep_days(),
repos: std::collections::HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookRepoConfig {
#[serde(default)]
pub events: Vec<String>,
#[serde(default)]
pub auto_investigate: bool,
#[serde(default)]
pub auto_fix: bool,
#[serde(default)]
pub auto_merge: bool,
#[serde(default)]
pub labels_filter: Vec<String>,
#[serde(default)]
pub post_commands: Vec<CommandConfig>,
}
impl Default for WebhookRepoConfig {
fn default() -> Self {
Self {
events: vec!["issues".to_string()],
auto_investigate: true,
auto_fix: false,
auto_merge: false,
labels_filter: Vec::new(),
post_commands: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandConfig {
pub cmd: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub working_dir: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrashRecoveryConfig {
pub max_fix_attempts: u32,
pub state_file: String,
pub enabled: bool,
}
impl Default for CrashRecoveryConfig {
fn default() -> Self {
Self {
max_fix_attempts: 3,
state_file: dirs::home_dir()
.map(|h| {
h.join(".brainwires")
.join("crash-recovery.json")
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| "/tmp/brainwires/crash-recovery.json".to_string()),
enabled: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpioConfig {
#[serde(default)]
pub allowed_pins: Vec<(u32, u32)>,
pub max_concurrent_pins: usize,
pub auto_release_timeout_secs: u64,
}
impl Default for GpioConfig {
fn default() -> Self {
Self {
allowed_pins: Vec::new(),
max_concurrent_pins: 4,
auto_release_timeout_secs: 300,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchedulerConfig {
pub max_concurrent_tasks: u32,
#[serde(default)]
pub tasks: Vec<ScheduledTaskDef>,
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self {
max_concurrent_tasks: 3,
tasks: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduledTaskDef {
pub id: String,
pub name: String,
pub cron_expression: String,
pub task_type: String,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_max_runtime")]
pub max_runtime_secs: u64,
}
fn default_true() -> bool {
true
}
fn default_max_runtime() -> u64 {
3600
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactorConfig {
pub max_events_per_minute: u32,
pub global_debounce_ms: u64,
pub max_watch_depth: u32,
#[serde(default)]
pub rules: Vec<ReactorRuleDef>,
}
impl Default for ReactorConfig {
fn default() -> Self {
Self {
max_events_per_minute: 60,
global_debounce_ms: 500,
max_watch_depth: 10,
rules: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactorRuleDef {
pub id: String,
pub name: String,
pub watch_paths: Vec<String>,
#[serde(default)]
pub patterns: Vec<String>,
#[serde(default)]
pub exclude_patterns: Vec<String>,
#[serde(default)]
pub event_types: Vec<String>,
#[serde(default = "default_debounce")]
pub debounce_ms: u64,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_debounce() -> u64 {
1000
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceConfig {
#[serde(default)]
pub allowed_services: Vec<String>,
#[serde(default)]
pub forbidden_services: Vec<String>,
#[serde(default = "default_true")]
pub read_only: bool,
#[serde(default)]
pub docker_socket_path: Option<String>,
}
impl Default for ServiceConfig {
fn default() -> Self {
Self {
allowed_services: Vec::new(),
forbidden_services: Vec::new(),
read_only: true,
docker_socket_path: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn autonomy_config_default_succeeds() {
let config = AutonomyConfig::default();
let _si: &SelfImprovementConfig = &config.self_improve;
let _safety: &SafetyConfig = &config.safety;
let _git: &GitWorkflowConfig = &config.git_workflow;
}
#[test]
fn self_improvement_config_default_has_sensible_values() {
let config = SelfImprovementConfig::default();
assert_eq!(config.max_cycles, 10);
assert!((config.max_budget - 10.0).abs() < f64::EPSILON);
assert!(!config.dry_run);
assert!(config.strategies.is_empty());
assert_eq!(config.agent_iterations, 25);
assert_eq!(config.circuit_breaker_threshold, 3);
assert_eq!(config.branch_prefix, "self-improve/");
assert!(config.model.is_none());
assert!(config.provider.is_none());
}
#[test]
fn safety_config_default_has_sensible_values() {
let config = SafetyConfig::default();
assert!((config.max_total_cost - 50.0).abs() < f64::EPSILON);
assert!((config.max_per_operation_cost - 5.0).abs() < f64::EPSILON);
assert_eq!(config.max_daily_operations, 100);
assert_eq!(config.circuit_breaker_threshold, 3);
assert_eq!(config.circuit_breaker_cooldown_secs, 300);
assert_eq!(config.max_concurrent_agents, 5);
assert_eq!(config.heartbeat_timeout_secs, 1800);
assert!(config.allowed_paths.is_empty());
assert!(config.forbidden_paths.is_empty());
}
#[test]
fn git_workflow_config_default_has_sensible_values() {
let config = GitWorkflowConfig::default();
assert_eq!(config.branch_prefix, "autonomy/");
assert!(!config.auto_merge);
assert_eq!(config.merge_method, "squash");
assert!((config.min_confidence - 0.7).abs() < f64::EPSILON);
assert_eq!(config.webhook.port, 3000);
}
#[test]
fn serde_roundtrip_autonomy_config() {
let config = AutonomyConfig::default();
let json = serde_json::to_string(&config).expect("serialize");
let deserialized: AutonomyConfig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(
deserialized.self_improve.max_cycles,
config.self_improve.max_cycles
);
assert_eq!(
deserialized.safety.max_total_cost,
config.safety.max_total_cost
);
assert_eq!(
deserialized.git_workflow.branch_prefix,
config.git_workflow.branch_prefix
);
}
#[test]
fn is_strategy_enabled_empty_list_enables_all() {
let config = SelfImprovementConfig::default();
assert!(config.is_strategy_enabled("clippy"));
assert!(config.is_strategy_enabled("anything"));
}
#[test]
fn is_strategy_enabled_specific_list() {
let config = SelfImprovementConfig {
strategies: vec!["clippy".to_string(), "todo".to_string()],
..Default::default()
};
assert!(config.is_strategy_enabled("clippy"));
assert!(config.is_strategy_enabled("todo"));
assert!(!config.is_strategy_enabled("dead_code"));
}
}