Skip to main content

agent_code_lib/config/
schema.rs

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