Skip to main content

agent_code_lib/config/
schema.rs

1//! Configuration schema definitions.
2
3use serde::{Deserialize, Serialize};
4
5/// Top-level configuration for the agent.
6///
7/// Loaded from three layers (highest priority first):
8/// 1. CLI flags and environment variables
9/// 2. Project config (`.agent/settings.toml`)
10/// 3. User config (`~/.config/agent-code/config.toml`)
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(default)]
13#[derive(Default)]
14pub struct Config {
15    pub api: ApiConfig,
16    pub permissions: PermissionsConfig,
17    pub ui: UiConfig,
18    /// Feature flags — all enabled by default.
19    #[serde(default)]
20    pub features: FeaturesConfig,
21    /// MCP server configurations.
22    #[serde(default)]
23    pub mcp_servers: std::collections::HashMap<String, McpServerEntry>,
24    /// Lifecycle hooks.
25    #[serde(default)]
26    pub hooks: Vec<HookDefinition>,
27    /// Security and enterprise settings.
28    #[serde(default)]
29    pub security: SecurityConfig,
30}
31
32/// Security and enterprise configuration.
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34#[serde(default)]
35pub struct SecurityConfig {
36    /// Additional directories the agent can access (beyond cwd).
37    #[serde(default)]
38    pub additional_directories: Vec<String>,
39    /// MCP server allowlist. If non-empty, only listed servers can connect.
40    #[serde(default)]
41    pub mcp_server_allowlist: Vec<String>,
42    /// MCP server denylist. Listed servers are blocked from connecting.
43    #[serde(default)]
44    pub mcp_server_denylist: Vec<String>,
45    /// Disable the --dangerously-skip-permissions flag.
46    #[serde(default)]
47    pub disable_bypass_permissions: bool,
48    /// Restrict which environment variables the agent can read.
49    #[serde(default)]
50    pub env_allowlist: Vec<String>,
51    /// Disable inline shell execution within skill templates.
52    #[serde(default)]
53    pub disable_skill_shell_execution: bool,
54}
55
56/// Entry for a configured MCP server.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct McpServerEntry {
59    /// Command to run (for stdio transport).
60    pub command: Option<String>,
61    /// Arguments for the command.
62    #[serde(default)]
63    pub args: Vec<String>,
64    /// URL (for SSE transport).
65    pub url: Option<String>,
66    /// Environment variables for the server process.
67    #[serde(default)]
68    pub env: std::collections::HashMap<String, String>,
69}
70
71/// API connection settings.
72///
73/// Configures the LLM provider: base URL, model, API key, timeouts,
74/// cost limits, and thinking mode. The API key is resolved from
75/// multiple sources (env vars, config file, CLI flag).
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(default)]
78pub struct ApiConfig {
79    /// Base URL for the LLM API.
80    pub base_url: String,
81    /// Model identifier.
82    pub model: String,
83    /// API key. Resolved from (in order): config, AGENT_CODE_API_KEY,
84    /// ANTHROPIC_API_KEY, OPENAI_API_KEY env vars.
85    #[serde(skip_serializing)]
86    pub api_key: Option<String>,
87    /// Maximum output tokens per response.
88    pub max_output_tokens: Option<u32>,
89    /// Thinking mode: "enabled", "disabled", or "adaptive".
90    pub thinking: Option<String>,
91    /// Effort level: "low", "medium", "high".
92    pub effort: Option<String>,
93    /// Maximum spend per session in USD.
94    pub max_cost_usd: Option<f64>,
95    /// Request timeout in seconds.
96    pub timeout_secs: u64,
97    /// Maximum retry attempts for transient errors.
98    pub max_retries: u32,
99}
100
101impl Default for ApiConfig {
102    fn default() -> Self {
103        // Resolve API key from multiple environment variables.
104        let api_key = std::env::var("AGENT_CODE_API_KEY")
105            .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
106            .or_else(|_| std::env::var("OPENAI_API_KEY"))
107            .or_else(|_| std::env::var("XAI_API_KEY"))
108            .or_else(|_| std::env::var("GOOGLE_API_KEY"))
109            .or_else(|_| std::env::var("DEEPSEEK_API_KEY"))
110            .or_else(|_| std::env::var("GROQ_API_KEY"))
111            .or_else(|_| std::env::var("MISTRAL_API_KEY"))
112            .or_else(|_| std::env::var("ZHIPU_API_KEY"))
113            .or_else(|_| std::env::var("TOGETHER_API_KEY"))
114            .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
115            .or_else(|_| std::env::var("COHERE_API_KEY"))
116            .or_else(|_| std::env::var("PERPLEXITY_API_KEY"))
117            .ok();
118
119        // Auto-detect base URL from which key is set.
120        // Check for cloud provider env vars first.
121        let use_bedrock = std::env::var("AGENT_CODE_USE_BEDROCK").is_ok()
122            || std::env::var("AWS_REGION").is_ok() && api_key.is_some();
123        let use_vertex = std::env::var("AGENT_CODE_USE_VERTEX").is_ok();
124
125        let has_generic = std::env::var("AGENT_CODE_API_KEY").is_ok();
126        let base_url = if use_bedrock {
127            // AWS Bedrock — URL constructed from region.
128            let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
129            format!("https://bedrock-runtime.{region}.amazonaws.com")
130        } else if use_vertex {
131            // Google Vertex AI.
132            let project = std::env::var("GOOGLE_CLOUD_PROJECT").unwrap_or_default();
133            let location = std::env::var("GOOGLE_CLOUD_LOCATION")
134                .unwrap_or_else(|_| "us-central1".to_string());
135            format!(
136                "https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/anthropic/models"
137            )
138        } else if has_generic {
139            // Generic key — default to OpenAI (default model is gpt-5.4).
140            "https://api.openai.com/v1".to_string()
141        } else if std::env::var("GOOGLE_API_KEY").is_ok() {
142            "https://generativelanguage.googleapis.com/v1beta/openai".to_string()
143        } else if std::env::var("DEEPSEEK_API_KEY").is_ok() {
144            "https://api.deepseek.com/v1".to_string()
145        } else if std::env::var("XAI_API_KEY").is_ok() {
146            "https://api.x.ai/v1".to_string()
147        } else if std::env::var("GROQ_API_KEY").is_ok() {
148            "https://api.groq.com/openai/v1".to_string()
149        } else if std::env::var("MISTRAL_API_KEY").is_ok() {
150            "https://api.mistral.ai/v1".to_string()
151        } else if std::env::var("TOGETHER_API_KEY").is_ok() {
152            "https://api.together.xyz/v1".to_string()
153        } else if std::env::var("OPENROUTER_API_KEY").is_ok() {
154            "https://openrouter.ai/api/v1".to_string()
155        } else if std::env::var("COHERE_API_KEY").is_ok() {
156            "https://api.cohere.com/v2".to_string()
157        } else if std::env::var("PERPLEXITY_API_KEY").is_ok() {
158            "https://api.perplexity.ai".to_string()
159        } else {
160            // Default to OpenAI (default model is gpt-5.4).
161            "https://api.openai.com/v1".to_string()
162        };
163
164        Self {
165            base_url,
166            model: "gpt-5.4".to_string(),
167            api_key,
168            max_output_tokens: Some(16384),
169            thinking: None,
170            effort: None,
171            max_cost_usd: None,
172            timeout_secs: 120,
173            max_retries: 3,
174        }
175    }
176}
177
178/// Permission system configuration.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(default)]
181pub struct PermissionsConfig {
182    /// Default permission mode for tools without explicit rules.
183    pub default_mode: PermissionMode,
184    /// Per-tool permission rules.
185    pub rules: Vec<PermissionRule>,
186}
187
188impl Default for PermissionsConfig {
189    fn default() -> Self {
190        Self {
191            default_mode: PermissionMode::Ask,
192            rules: Vec::new(),
193        }
194    }
195}
196
197/// Permission mode controlling how tool calls are authorized.
198///
199/// Set globally via `[permissions] default_mode` or per-tool via rules.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "snake_case")]
202pub enum PermissionMode {
203    /// Always allow without asking.
204    Allow,
205    /// Always deny.
206    Deny,
207    /// Ask the user interactively.
208    Ask,
209    /// Accept file edits automatically, ask for other mutations.
210    AcceptEdits,
211    /// Plan mode: read-only tools only.
212    Plan,
213}
214
215/// A single permission rule matching a tool and optional pattern.
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct PermissionRule {
218    /// Tool name to match.
219    pub tool: String,
220    /// Optional glob/regex pattern for the tool's input.
221    pub pattern: Option<String>,
222    /// Action to take when this rule matches.
223    pub action: PermissionMode,
224}
225
226/// UI configuration.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(default)]
229pub struct UiConfig {
230    /// Enable markdown rendering in output.
231    pub markdown: bool,
232    /// Enable syntax highlighting in code blocks.
233    pub syntax_highlight: bool,
234    /// Theme name.
235    pub theme: String,
236    /// Editing mode: "emacs" or "vi".
237    pub edit_mode: String,
238}
239
240impl Default for UiConfig {
241    fn default() -> Self {
242        Self {
243            markdown: true,
244            syntax_highlight: true,
245            theme: "dark".to_string(),
246            edit_mode: "emacs".to_string(),
247        }
248    }
249}
250
251/// Feature flags. All enabled by default — no artificial gates.
252/// Users can disable individual features in config.toml under [features].
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(default)]
255pub struct FeaturesConfig {
256    /// Track per-turn token usage and warn when approaching budget.
257    pub token_budget: bool,
258    /// Add co-author attribution line to git commits.
259    pub commit_attribution: bool,
260    /// Show a system reminder after context compaction.
261    pub compaction_reminders: bool,
262    /// Auto retry on capacity/overload errors in non-interactive mode.
263    pub unattended_retry: bool,
264    /// Enable /snip command to remove message ranges from history.
265    pub history_snip: bool,
266    /// Auto-detect system dark/light mode for theme.
267    pub auto_theme: bool,
268    /// Rich formatting for MCP tool output.
269    pub mcp_rich_output: bool,
270    /// Enable /fork command to branch conversation.
271    pub fork_conversation: bool,
272    /// Verification agent that checks completed tasks.
273    pub verification_agent: bool,
274    /// Background memory extraction after each turn.
275    pub extract_memories: bool,
276    /// Context collapse (snip old messages) when approaching limits.
277    pub context_collapse: bool,
278    /// Reactive auto-compaction when token budget is tight.
279    pub reactive_compact: bool,
280}
281
282impl Default for FeaturesConfig {
283    fn default() -> Self {
284        Self {
285            token_budget: true,
286            commit_attribution: true,
287            compaction_reminders: true,
288            unattended_retry: true,
289            history_snip: true,
290            auto_theme: true,
291            mcp_rich_output: true,
292            fork_conversation: true,
293            verification_agent: true,
294            extract_memories: true,
295            context_collapse: true,
296            reactive_compact: true,
297        }
298    }
299}
300
301// ---- Hook types (defined here so config has no runtime dependencies) ----
302
303/// Hook event types that can trigger user-defined actions.
304#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
305#[serde(rename_all = "snake_case")]
306pub enum HookEvent {
307    SessionStart,
308    SessionStop,
309    PreToolUse,
310    PostToolUse,
311    UserPromptSubmit,
312}
313
314/// A configured hook action.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(tag = "type")]
317pub enum HookAction {
318    /// Run a shell command.
319    #[serde(rename = "shell")]
320    Shell { command: String },
321    /// Make an HTTP request.
322    #[serde(rename = "http")]
323    Http { url: String, method: Option<String> },
324}
325
326/// A hook definition binding an event to an action.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct HookDefinition {
329    pub event: HookEvent,
330    pub action: HookAction,
331    /// Optional tool name filter (for PreToolUse/PostToolUse).
332    pub tool_name: Option<String>,
333}