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 (lowest priority file).
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 (overrides user config).
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 override file-based config.
56        // API key from env always wins over config files, because users
57        // expect `OPENAI_API_KEY=x agent` to use key x, even if a
58        // stale key exists in config.toml.
59        let env_api_key = resolve_api_key_from_env();
60        if env_api_key.is_some() {
61            config.api.api_key = env_api_key;
62        }
63
64        // Base URL from env overrides file config.
65        if let Ok(url) = std::env::var("AGENT_CODE_API_BASE_URL") {
66            config.api.base_url = url;
67        }
68
69        // Model from env overrides file config.
70        if let Ok(model) = std::env::var("AGENT_CODE_MODEL") {
71            config.api.model = model;
72        }
73
74        Ok(config)
75    }
76
77    /// Merge another config into this one. Non-default values from `other`
78    /// overwrite values in `self`.
79    fn merge(&mut self, other: Config) {
80        if !other.api.base_url.is_empty() {
81            self.api.base_url = other.api.base_url;
82        }
83        if !other.api.model.is_empty() {
84            self.api.model = other.api.model;
85        }
86        if other.api.api_key.is_some() {
87            self.api.api_key = other.api.api_key;
88        }
89        if other.api.max_output_tokens.is_some() {
90            self.api.max_output_tokens = other.api.max_output_tokens;
91        }
92        if other.permissions.default_mode != PermissionMode::Ask {
93            self.permissions.default_mode = other.permissions.default_mode;
94        }
95        if !other.permissions.rules.is_empty() {
96            self.permissions.rules.extend(other.permissions.rules);
97        }
98        // MCP servers merge by name (project overrides user).
99        for (name, entry) in other.mcp_servers {
100            self.mcp_servers.insert(name, entry);
101        }
102    }
103}
104
105/// Resolve API key from environment variables.
106///
107/// Checks each provider's env var in priority order. Returns the first
108/// one found, or None if no API key is set in the environment.
109fn resolve_api_key_from_env() -> Option<String> {
110    std::env::var("AGENT_CODE_API_KEY")
111        .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
112        .or_else(|_| std::env::var("OPENAI_API_KEY"))
113        .or_else(|_| std::env::var("XAI_API_KEY"))
114        .or_else(|_| std::env::var("GOOGLE_API_KEY"))
115        .or_else(|_| std::env::var("DEEPSEEK_API_KEY"))
116        .or_else(|_| std::env::var("GROQ_API_KEY"))
117        .or_else(|_| std::env::var("MISTRAL_API_KEY"))
118        .or_else(|_| std::env::var("ZHIPU_API_KEY"))
119        .or_else(|_| std::env::var("TOGETHER_API_KEY"))
120        .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
121        .or_else(|_| std::env::var("COHERE_API_KEY"))
122        .or_else(|_| std::env::var("PERPLEXITY_API_KEY"))
123        .ok()
124}
125
126/// Returns the user-level config file path.
127fn user_config_path() -> Option<PathBuf> {
128    dirs::config_dir().map(|d| d.join("agent-code").join("config.toml"))
129}
130
131/// Walk up from the current directory to find `.agent/settings.toml`.
132fn find_project_config() -> Option<PathBuf> {
133    let cwd = std::env::current_dir().ok()?;
134    find_config_in_ancestors(&cwd)
135}
136
137/// Watch config files for changes and reload when modified.
138/// Returns a handle that can be dropped to stop watching.
139pub fn watch_config(
140    on_reload: impl Fn(Config) + Send + 'static,
141) -> Option<std::thread::JoinHandle<()>> {
142    let user_path = user_config_path()?;
143    let project_path = find_project_config();
144
145    // Get initial mtimes.
146    let user_mtime = std::fs::metadata(&user_path)
147        .ok()
148        .and_then(|m| m.modified().ok());
149    let project_mtime = project_path
150        .as_ref()
151        .and_then(|p| std::fs::metadata(p).ok())
152        .and_then(|m| m.modified().ok());
153
154    Some(std::thread::spawn(move || {
155        let mut last_user = user_mtime;
156        let mut last_project = project_mtime;
157
158        loop {
159            std::thread::sleep(std::time::Duration::from_secs(5));
160
161            let cur_user = std::fs::metadata(&user_path)
162                .ok()
163                .and_then(|m| m.modified().ok());
164            let cur_project = project_path
165                .as_ref()
166                .and_then(|p| std::fs::metadata(p).ok())
167                .and_then(|m| m.modified().ok());
168
169            let changed = cur_user != last_user || cur_project != last_project;
170
171            if changed {
172                if let Ok(config) = Config::load() {
173                    tracing::info!("Config reloaded (file change detected)");
174                    on_reload(config);
175                }
176                last_user = cur_user;
177                last_project = cur_project;
178            }
179        }
180    }))
181}
182
183fn find_config_in_ancestors(start: &Path) -> Option<PathBuf> {
184    let mut dir = start.to_path_buf();
185    loop {
186        let candidate = dir.join(".agent").join("settings.toml");
187        if candidate.exists() {
188            return Some(candidate);
189        }
190        if !dir.pop() {
191            return None;
192        }
193    }
194}