use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeterministicConfig {
#[serde(default)]
pub blocklist: Vec<String>,
#[serde(default)]
pub auto_approve: Vec<String>,
#[serde(default = "default_rate_limit")]
pub max_tool_calls_per_minute: u32,
#[serde(default = "default_token_budget")]
pub token_budget_limit: u64,
}
fn default_rate_limit() -> u32 {
120
}
fn default_token_budget() -> u64 {
200_000
}
impl Default for DeterministicConfig {
fn default() -> Self {
Self {
blocklist: Vec::new(),
auto_approve: Vec::new(),
max_tool_calls_per_minute: default_rate_limit(),
token_budget_limit: default_token_budget(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
struct OverseerSection {
#[serde(default)]
enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LlmConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_llm_model")]
pub model: String,
#[serde(default = "default_api_key_env")]
pub api_key_env: String,
}
fn default_llm_model() -> String {
"meta-llama/llama-3.1-8b-instruct:free".to_string()
}
fn default_api_key_env() -> String {
"OPENROUTER_API_KEY".to_string()
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
enabled: false,
model: default_llm_model(),
api_key_env: default_api_key_env(),
}
}
}
#[derive(Debug, Default, Deserialize)]
struct OverseerToml {
#[serde(default)]
overseer: OverseerSection,
#[serde(default)]
deterministic: DeterministicConfig,
#[serde(default)]
llm: LlmConfig,
#[serde(default)]
auto_responses: HashMap<String, String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverseerConfig {
pub enabled: bool,
pub deterministic: DeterministicConfig,
#[serde(default)]
pub llm: LlmConfig,
pub auto_responses: HashMap<String, String>,
}
impl OverseerConfig {
pub fn load_from(path: &Path) -> Self {
let raw = match std::fs::read_to_string(path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Self::default(),
Err(e) => {
tracing::warn!(
"failed to read overseer config {}: {e}; using defaults",
path.display()
);
return Self::default();
}
};
match toml::from_str::<OverseerToml>(&raw) {
Ok(parsed) => Self {
enabled: parsed.overseer.enabled,
deterministic: parsed.deterministic,
llm: parsed.llm,
auto_responses: parsed.auto_responses,
},
Err(e) => {
tracing::warn!(
"malformed overseer config {}: {e}; using defaults",
path.display()
);
Self::default()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_disabled() {
let cfg = OverseerConfig::default();
assert!(!cfg.enabled);
assert!(cfg.auto_responses.is_empty());
}
#[test]
fn default_deterministic_is_sane() {
let d = DeterministicConfig::default();
assert_eq!(d.max_tool_calls_per_minute, 120);
assert_eq!(d.token_budget_limit, 200_000);
assert!(d.blocklist.is_empty());
}
#[test]
fn config_loads_from_toml() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("overseer.toml");
let mut file = std::fs::File::create(&path).unwrap();
writeln!(
file,
"[overseer]\nenabled = true\n\n\
[deterministic]\nblocklist = [\"rm -rf /\"]\nauto_approve = [\"ls\"]\n\
max_tool_calls_per_minute = 30\ntoken_budget_limit = 1000\n\n\
[auto_responses]\n\"shall i proceed\" = \"yes, proceed\""
)
.unwrap();
let cfg = OverseerConfig::load_from(&path);
assert!(cfg.enabled);
assert_eq!(cfg.deterministic.blocklist, vec!["rm -rf /".to_string()]);
assert_eq!(cfg.deterministic.auto_approve, vec!["ls".to_string()]);
assert_eq!(cfg.deterministic.max_tool_calls_per_minute, 30);
assert_eq!(cfg.deterministic.token_budget_limit, 1000);
assert_eq!(
cfg.auto_responses
.get("shall i proceed")
.map(String::as_str),
Some("yes, proceed")
);
}
#[test]
fn load_from_missing_file_is_default() {
let dir = tempfile::tempdir().unwrap();
let cfg = OverseerConfig::load_from(&dir.path().join("absent.toml"));
assert_eq!(cfg, OverseerConfig::default());
}
#[test]
fn load_from_malformed_file_is_default() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("overseer.toml");
let mut file = std::fs::File::create(&path).unwrap();
writeln!(file, "this is = not valid = toml [[[").unwrap();
let cfg = OverseerConfig::load_from(&path);
assert_eq!(cfg, OverseerConfig::default());
}
#[test]
fn default_llm_is_disabled() {
let llm = LlmConfig::default();
assert!(!llm.enabled);
assert_eq!(llm.api_key_env, "OPENROUTER_API_KEY");
assert!(llm.model.contains("llama") || llm.model.contains("haiku"));
}
#[test]
fn llm_config_loads_from_toml() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("overseer.toml");
let mut file = std::fs::File::create(&path).unwrap();
writeln!(
file,
"[overseer]\nenabled = true\n\n\
[llm]\nenabled = true\nmodel = \"claude-haiku-4-5\"\n\
api_key_env = \"MY_KEY\""
)
.unwrap();
let cfg = OverseerConfig::load_from(&path);
assert!(cfg.llm.enabled);
assert_eq!(cfg.llm.model, "claude-haiku-4-5");
assert_eq!(cfg.llm.api_key_env, "MY_KEY");
}
#[test]
fn llm_table_uses_field_defaults() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("overseer.toml");
let mut file = std::fs::File::create(&path).unwrap();
writeln!(file, "[llm]\nenabled = true").unwrap();
let cfg = OverseerConfig::load_from(&path);
assert!(cfg.llm.enabled);
assert_eq!(cfg.llm.api_key_env, "OPENROUTER_API_KEY");
}
#[test]
fn partial_toml_uses_field_defaults() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("overseer.toml");
let mut file = std::fs::File::create(&path).unwrap();
writeln!(file, "[overseer]\nenabled = true").unwrap();
let cfg = OverseerConfig::load_from(&path);
assert!(cfg.enabled);
assert_eq!(cfg.deterministic, DeterministicConfig::default());
}
}