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
19/// Re-entrancy guard to prevent Config::load → log → Config::load cycles.
20static LOADING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
21
22impl Config {
23    /// Load configuration from all sources, merging by priority.
24    pub fn load() -> Result<Config, ConfigError> {
25        // Re-entrancy guard.
26        if LOADING.swap(true, std::sync::atomic::Ordering::SeqCst) {
27            return Ok(Config::default());
28        }
29        let result = Self::load_inner();
30        LOADING.store(false, std::sync::atomic::Ordering::SeqCst);
31        result
32    }
33
34    fn load_inner() -> Result<Config, ConfigError> {
35        let mut config = Config::default();
36
37        // Layer 1: User-level config.
38        if let Some(path) = user_config_path()
39            && path.exists()
40        {
41            let content = std::fs::read_to_string(&path)
42                .map_err(|e| ConfigError::FileError(format!("{path:?}: {e}")))?;
43            let user_config: Config = toml::from_str(&content)?;
44            config.merge(user_config);
45        }
46
47        // Layer 2: Project-level config (walk up from cwd).
48        if let Some(path) = find_project_config() {
49            let content = std::fs::read_to_string(&path)
50                .map_err(|e| ConfigError::FileError(format!("{path:?}: {e}")))?;
51            let project_config: Config = toml::from_str(&content)?;
52            config.merge(project_config);
53        }
54
55        // Layer 3: Environment variables (applied in CLI parsing).
56
57        Ok(config)
58    }
59
60    /// Merge another config into this one. Non-default values from `other`
61    /// overwrite values in `self`.
62    fn merge(&mut self, other: Config) {
63        if !other.api.base_url.is_empty() {
64            self.api.base_url = other.api.base_url;
65        }
66        if !other.api.model.is_empty() {
67            self.api.model = other.api.model;
68        }
69        if other.api.api_key.is_some() {
70            self.api.api_key = other.api.api_key;
71        }
72        if other.api.max_output_tokens.is_some() {
73            self.api.max_output_tokens = other.api.max_output_tokens;
74        }
75        if other.permissions.default_mode != PermissionMode::Ask {
76            self.permissions.default_mode = other.permissions.default_mode;
77        }
78        if !other.permissions.rules.is_empty() {
79            self.permissions.rules.extend(other.permissions.rules);
80        }
81        // MCP servers merge by name (project overrides user).
82        for (name, entry) in other.mcp_servers {
83            self.mcp_servers.insert(name, entry);
84        }
85    }
86}
87
88/// Returns the user-level config file path.
89fn user_config_path() -> Option<PathBuf> {
90    dirs::config_dir().map(|d| d.join("agent-code").join("config.toml"))
91}
92
93/// Walk up from the current directory to find `.agent/settings.toml`.
94fn find_project_config() -> Option<PathBuf> {
95    let cwd = std::env::current_dir().ok()?;
96    find_config_in_ancestors(&cwd)
97}
98
99/// Watch config files for changes and reload when modified.
100/// Returns a handle that can be dropped to stop watching.
101pub fn watch_config(
102    on_reload: impl Fn(Config) + Send + 'static,
103) -> Option<std::thread::JoinHandle<()>> {
104    let user_path = user_config_path()?;
105    let project_path = find_project_config();
106
107    // Get initial mtimes.
108    let user_mtime = std::fs::metadata(&user_path)
109        .ok()
110        .and_then(|m| m.modified().ok());
111    let project_mtime = project_path
112        .as_ref()
113        .and_then(|p| std::fs::metadata(p).ok())
114        .and_then(|m| m.modified().ok());
115
116    Some(std::thread::spawn(move || {
117        let mut last_user = user_mtime;
118        let mut last_project = project_mtime;
119
120        loop {
121            std::thread::sleep(std::time::Duration::from_secs(5));
122
123            let cur_user = std::fs::metadata(&user_path)
124                .ok()
125                .and_then(|m| m.modified().ok());
126            let cur_project = project_path
127                .as_ref()
128                .and_then(|p| std::fs::metadata(p).ok())
129                .and_then(|m| m.modified().ok());
130
131            let changed = cur_user != last_user || cur_project != last_project;
132
133            if changed {
134                if let Ok(config) = Config::load() {
135                    tracing::info!("Config reloaded (file change detected)");
136                    on_reload(config);
137                }
138                last_user = cur_user;
139                last_project = cur_project;
140            }
141        }
142    }))
143}
144
145fn find_config_in_ancestors(start: &Path) -> Option<PathBuf> {
146    let mut dir = start.to_path_buf();
147    loop {
148        let candidate = dir.join(".agent").join("settings.toml");
149        if candidate.exists() {
150            return Some(candidate);
151        }
152        if !dir.pop() {
153            return None;
154        }
155    }
156}