use serde::Deserialize; /// Project-level configuration from .sunbeam/config.toml. #[derive(Debug, Default, Deserialize)] pub struct ProjectConfig { #[serde(default)] pub model: Option, #[serde(default)] pub tools: Option, } #[derive(Debug, Deserialize)] pub struct ModelConfig { pub name: Option, } #[derive(Debug, Default, Deserialize)] pub struct ToolPermissions { #[serde(default)] pub file_read: Option, #[serde(default)] pub file_write: Option, #[serde(default)] pub search_replace: Option, #[serde(default)] pub grep: Option, #[serde(default)] pub bash: Option, #[serde(default)] pub list_directory: Option, } /// Convenience wrapper with flattened fields. pub struct LoadedConfig { pub model_name: Option, pub file_read_perm: String, pub file_write_perm: String, pub search_replace_perm: String, pub grep_perm: String, pub bash_perm: String, pub list_directory_perm: String, } impl Default for LoadedConfig { fn default() -> Self { Self { model_name: None, file_read_perm: "always".into(), file_write_perm: "ask".into(), search_replace_perm: "ask".into(), grep_perm: "always".into(), bash_perm: "ask".into(), list_directory_perm: "always".into(), } } } /// Load project config from .sunbeam/config.toml. pub fn load_project_config(project_path: &str) -> LoadedConfig { let config_path = std::path::Path::new(project_path) .join(".sunbeam") .join("config.toml"); let raw = match std::fs::read_to_string(&config_path) { Ok(s) => s, Err(_) => return LoadedConfig::default(), }; let parsed: ProjectConfig = match toml::from_str(&raw) { Ok(c) => c, Err(e) => { eprintln!("warning: failed to parse .sunbeam/config.toml: {e}"); return LoadedConfig::default(); } }; let tools = parsed.tools.unwrap_or_default(); LoadedConfig { model_name: parsed.model.and_then(|m| m.name), file_read_perm: tools.file_read.unwrap_or_else(|| "always".into()), file_write_perm: tools.file_write.unwrap_or_else(|| "ask".into()), search_replace_perm: tools.search_replace.unwrap_or_else(|| "ask".into()), grep_perm: tools.grep.unwrap_or_else(|| "always".into()), bash_perm: tools.bash.unwrap_or_else(|| "ask".into()), list_directory_perm: tools.list_directory.unwrap_or_else(|| "always".into()), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let cfg = LoadedConfig::default(); assert_eq!(cfg.file_read_perm, "always"); assert_eq!(cfg.file_write_perm, "ask"); assert_eq!(cfg.bash_perm, "ask"); assert!(cfg.model_name.is_none()); } #[test] fn test_parse_config() { let toml = r#" [model] name = "devstral-2" [tools] file_read = "always" bash = "never" "#; let parsed: ProjectConfig = toml::from_str(toml).unwrap(); assert_eq!(parsed.model.unwrap().name.unwrap(), "devstral-2"); assert_eq!(parsed.tools.unwrap().bash.unwrap(), "never"); } }