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