Skip to main content

agent_code_lib/config/
mod.rs

1//! Configuration system.
2//!
3//! Configuration is loaded from multiple sources with the following
4//! priority (highest to lowest):
5//!
6//! 1. CLI flags and environment variables
7//! 2. Project-local settings (`.agent/settings.toml`)
8//! 3. User settings (`~/.config/agent-code/config.toml`)
9//!
10//! Each layer is merged into the final `Config` struct.
11
12mod schema;
13
14pub use schema::*;
15
16use crate::error::ConfigError;
17use std::path::{Path, PathBuf};
18
19impl Config {
20    /// Load configuration from all sources, merging by priority.
21    pub fn load() -> Result<Config, ConfigError> {
22        let mut config = Config::default();
23
24        // Layer 1: User-level config.
25        if let Some(path) = user_config_path()
26            && path.exists()
27        {
28            let content = std::fs::read_to_string(&path)
29                .map_err(|e| ConfigError::FileError(format!("{path:?}: {e}")))?;
30            let user_config: Config = toml::from_str(&content)?;
31            config.merge(user_config);
32        }
33
34        // Layer 2: Project-level config (walk up from cwd).
35        if let Some(path) = find_project_config() {
36            let content = std::fs::read_to_string(&path)
37                .map_err(|e| ConfigError::FileError(format!("{path:?}: {e}")))?;
38            let project_config: Config = toml::from_str(&content)?;
39            config.merge(project_config);
40        }
41
42        // Layer 3: Environment variables (applied in CLI parsing).
43
44        Ok(config)
45    }
46
47    /// Merge another config into this one. Non-default values from `other`
48    /// overwrite values in `self`.
49    fn merge(&mut self, other: Config) {
50        if !other.api.base_url.is_empty() {
51            self.api.base_url = other.api.base_url;
52        }
53        if !other.api.model.is_empty() {
54            self.api.model = other.api.model;
55        }
56        if other.api.api_key.is_some() {
57            self.api.api_key = other.api.api_key;
58        }
59        if other.api.max_output_tokens.is_some() {
60            self.api.max_output_tokens = other.api.max_output_tokens;
61        }
62        if other.permissions.default_mode != PermissionMode::Ask {
63            self.permissions.default_mode = other.permissions.default_mode;
64        }
65        if !other.permissions.rules.is_empty() {
66            self.permissions.rules.extend(other.permissions.rules);
67        }
68        // MCP servers merge by name (project overrides user).
69        for (name, entry) in other.mcp_servers {
70            self.mcp_servers.insert(name, entry);
71        }
72    }
73}
74
75/// Returns the user-level config file path.
76fn user_config_path() -> Option<PathBuf> {
77    dirs::config_dir().map(|d| d.join("agent-code").join("config.toml"))
78}
79
80/// Walk up from the current directory to find `.agent/settings.toml`.
81fn find_project_config() -> Option<PathBuf> {
82    let cwd = std::env::current_dir().ok()?;
83    find_config_in_ancestors(&cwd)
84}
85
86fn find_config_in_ancestors(start: &Path) -> Option<PathBuf> {
87    let mut dir = start.to_path_buf();
88    loop {
89        let candidate = dir.join(".agent").join("settings.toml");
90        if candidate.exists() {
91            return Some(candidate);
92        }
93        if !dir.pop() {
94            return None;
95        }
96    }
97}