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("AZURE_OPENAI_API_KEY"))
108            .or_else(|_| std::env::var("XAI_API_KEY"))
109            .or_else(|_| std::env::var("GOOGLE_API_KEY"))
110            .or_else(|_| std::env::var("DEEPSEEK_API_KEY"))
111            .or_else(|_| std::env::var("GROQ_API_KEY"))
112            .or_else(|_| std::env::var("MISTRAL_API_KEY"))
113            .or_else(|_| std::env::var("ZHIPU_API_KEY"))
114            .or_else(|_| std::env::var("TOGETHER_API_KEY"))
115            .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
116            .or_else(|_| std::env::var("COHERE_API_KEY"))
117            .or_else(|_| std::env::var("PERPLEXITY_API_KEY"))
118            .ok();
119
120        // Auto-detect base URL from which key is set.
121        // Check for cloud provider env vars first.
122        let use_bedrock = std::env::var("AGENT_CODE_USE_BEDROCK").is_ok()
123            || std::env::var("AWS_REGION").is_ok() && api_key.is_some();
124        let use_vertex = std::env::var("AGENT_CODE_USE_VERTEX").is_ok();
125        let use_azure = std::env::var("AZURE_OPENAI_ENDPOINT").is_ok()
126            || std::env::var("AZURE_OPENAI_API_KEY").is_ok();
127
128        let has_generic = std::env::var("AGENT_CODE_API_KEY").is_ok();
129        let base_url = if use_bedrock {
130            // AWS Bedrock — URL constructed from region.
131            let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
132            format!("https://bedrock-runtime.{region}.amazonaws.com")
133        } else if use_vertex {
134            // Google Vertex AI.
135            let project = std::env::var("GOOGLE_CLOUD_PROJECT").unwrap_or_default();
136            let location = std::env::var("GOOGLE_CLOUD_LOCATION")
137                .unwrap_or_else(|_| "us-central1".to_string());
138            format!(
139                "https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/anthropic/models"
140            )
141        } else if use_azure {
142            // Azure OpenAI — URL from AZURE_OPENAI_ENDPOINT or placeholder.
143            std::env::var("AZURE_OPENAI_ENDPOINT").unwrap_or_else(|_| {
144                "https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT"
145                    .to_string()
146            })
147        } else if has_generic {
148            // Generic key — default to OpenAI (default model is gpt-5.4).
149            "https://api.openai.com/v1".to_string()
150        } else if std::env::var("GOOGLE_API_KEY").is_ok() {
151            "https://generativelanguage.googleapis.com/v1beta/openai".to_string()
152        } else if std::env::var("DEEPSEEK_API_KEY").is_ok() {
153            "https://api.deepseek.com/v1".to_string()
154        } else if std::env::var("XAI_API_KEY").is_ok() {
155            "https://api.x.ai/v1".to_string()
156        } else if std::env::var("GROQ_API_KEY").is_ok() {
157            "https://api.groq.com/openai/v1".to_string()
158        } else if std::env::var("MISTRAL_API_KEY").is_ok() {
159            "https://api.mistral.ai/v1".to_string()
160        } else if std::env::var("TOGETHER_API_KEY").is_ok() {
161            "https://api.together.xyz/v1".to_string()
162        } else if std::env::var("OPENROUTER_API_KEY").is_ok() {
163            "https://openrouter.ai/api/v1".to_string()
164        } else if std::env::var("COHERE_API_KEY").is_ok() {
165            "https://api.cohere.com/v2".to_string()
166        } else if std::env::var("PERPLEXITY_API_KEY").is_ok() {
167            "https://api.perplexity.ai".to_string()
168        } else {
169            // Default to OpenAI (default model is gpt-5.4).
170            "https://api.openai.com/v1".to_string()
171        };
172
173        Self {
174            base_url,
175            model: "gpt-5.4".to_string(),
176            api_key,
177            max_output_tokens: Some(16384),
178            thinking: None,
179            effort: None,
180            max_cost_usd: None,
181            timeout_secs: 120,
182            max_retries: 3,
183        }
184    }
185}
186
187/// Permission system configuration.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(default)]
190pub struct PermissionsConfig {
191    /// Default permission mode for tools without explicit rules.
192    pub default_mode: PermissionMode,
193    /// Per-tool permission rules.
194    pub rules: Vec<PermissionRule>,
195}
196
197impl Default for PermissionsConfig {
198    fn default() -> Self {
199        Self {
200            default_mode: PermissionMode::Ask,
201            rules: Vec::new(),
202        }
203    }
204}
205
206/// Permission mode controlling how tool calls are authorized.
207///
208/// Set globally via `[permissions] default_mode` or per-tool via rules.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum PermissionMode {
212    /// Always allow without asking.
213    Allow,
214    /// Always deny.
215    Deny,
216    /// Ask the user interactively.
217    Ask,
218    /// Accept file edits automatically, ask for other mutations.
219    AcceptEdits,
220    /// Plan mode: read-only tools only.
221    Plan,
222}
223
224/// A single permission rule matching a tool and optional pattern.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PermissionRule {
227    /// Tool name to match.
228    pub tool: String,
229    /// Optional glob/regex pattern for the tool's input.
230    pub pattern: Option<String>,
231    /// Action to take when this rule matches.
232    pub action: PermissionMode,
233}
234
235/// UI configuration.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(default)]
238pub struct UiConfig {
239    /// Enable markdown rendering in output.
240    pub markdown: bool,
241    /// Enable syntax highlighting in code blocks.
242    pub syntax_highlight: bool,
243    /// Theme name.
244    pub theme: String,
245    /// Editing mode: "emacs" or "vi".
246    pub edit_mode: String,
247}
248
249impl Default for UiConfig {
250    fn default() -> Self {
251        Self {
252            markdown: true,
253            syntax_highlight: true,
254            theme: "dark".to_string(),
255            edit_mode: "emacs".to_string(),
256        }
257    }
258}
259
260/// Feature flags. All enabled by default — no artificial gates.
261/// Users can disable individual features in config.toml under [features].
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(default)]
264pub struct FeaturesConfig {
265    /// Track per-turn token usage and warn when approaching budget.
266    pub token_budget: bool,
267    /// Add co-author attribution line to git commits.
268    pub commit_attribution: bool,
269    /// Show a system reminder after context compaction.
270    pub compaction_reminders: bool,
271    /// Auto retry on capacity/overload errors in non-interactive mode.
272    pub unattended_retry: bool,
273    /// Enable /snip command to remove message ranges from history.
274    pub history_snip: bool,
275    /// Auto-detect system dark/light mode for theme.
276    pub auto_theme: bool,
277    /// Rich formatting for MCP tool output.
278    pub mcp_rich_output: bool,
279    /// Enable /fork command to branch conversation.
280    pub fork_conversation: bool,
281    /// Verification agent that checks completed tasks.
282    pub verification_agent: bool,
283    /// Background memory extraction after each turn.
284    pub extract_memories: bool,
285    /// Context collapse (snip old messages) when approaching limits.
286    pub context_collapse: bool,
287    /// Reactive auto-compaction when token budget is tight.
288    pub reactive_compact: bool,
289}
290
291impl Default for FeaturesConfig {
292    fn default() -> Self {
293        Self {
294            token_budget: true,
295            commit_attribution: true,
296            compaction_reminders: true,
297            unattended_retry: true,
298            history_snip: true,
299            auto_theme: true,
300            mcp_rich_output: true,
301            fork_conversation: true,
302            verification_agent: true,
303            extract_memories: true,
304            context_collapse: true,
305            reactive_compact: true,
306        }
307    }
308}
309
310// ---- Hook types (defined here so config has no runtime dependencies) ----
311
312/// Hook event types that can trigger user-defined actions.
313#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
314#[serde(rename_all = "snake_case")]
315pub enum HookEvent {
316    SessionStart,
317    SessionStop,
318    PreToolUse,
319    PostToolUse,
320    UserPromptSubmit,
321}
322
323/// A configured hook action.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325#[serde(tag = "type")]
326pub enum HookAction {
327    /// Run a shell command.
328    #[serde(rename = "shell")]
329    Shell { command: String },
330    /// Make an HTTP request.
331    #[serde(rename = "http")]
332    Http { url: String, method: Option<String> },
333}
334
335/// A hook definition binding an event to an action.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct HookDefinition {
338    pub event: HookEvent,
339    pub action: HookAction,
340    /// Optional tool name filter (for PreToolUse/PostToolUse).
341    pub tool_name: Option<String>,
342}