use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct Config {
pub api: ApiConfig,
pub permissions: PermissionsConfig,
pub ui: UiConfig,
#[serde(default)]
pub features: FeaturesConfig,
#[serde(default)]
pub mcp_servers: std::collections::HashMap<String, McpServerEntry>,
#[serde(default)]
pub hooks: Vec<HookDefinition>,
#[serde(default)]
pub security: SecurityConfig,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SecurityConfig {
#[serde(default)]
pub additional_directories: Vec<String>,
#[serde(default)]
pub mcp_server_allowlist: Vec<String>,
#[serde(default)]
pub mcp_server_denylist: Vec<String>,
#[serde(default)]
pub disable_bypass_permissions: bool,
#[serde(default)]
pub env_allowlist: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerEntry {
pub command: Option<String>,
#[serde(default)]
pub args: Vec<String>,
pub url: Option<String>,
#[serde(default)]
pub env: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ApiConfig {
pub base_url: String,
pub model: String,
#[serde(skip_serializing)]
pub api_key: Option<String>,
pub max_output_tokens: Option<u32>,
pub thinking: Option<String>,
pub effort: Option<String>,
pub max_cost_usd: Option<f64>,
pub timeout_secs: u64,
pub max_retries: u32,
}
impl Default for ApiConfig {
fn default() -> Self {
let api_key = std::env::var("AGENT_CODE_API_KEY")
.or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
.or_else(|_| std::env::var("OPENAI_API_KEY"))
.or_else(|_| std::env::var("XAI_API_KEY"))
.or_else(|_| std::env::var("GOOGLE_API_KEY"))
.or_else(|_| std::env::var("DEEPSEEK_API_KEY"))
.or_else(|_| std::env::var("GROQ_API_KEY"))
.or_else(|_| std::env::var("MISTRAL_API_KEY"))
.or_else(|_| std::env::var("TOGETHER_API_KEY"))
.ok();
let use_bedrock = std::env::var("AGENT_CODE_USE_BEDROCK").is_ok()
|| std::env::var("AWS_REGION").is_ok() && api_key.is_some();
let use_vertex = std::env::var("AGENT_CODE_USE_VERTEX").is_ok();
let has_generic = std::env::var("AGENT_CODE_API_KEY").is_ok();
let base_url = if use_bedrock {
let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
format!("https://bedrock-runtime.{region}.amazonaws.com")
} else if use_vertex {
let project = std::env::var("GOOGLE_CLOUD_PROJECT").unwrap_or_default();
let location = std::env::var("GOOGLE_CLOUD_LOCATION")
.unwrap_or_else(|_| "us-central1".to_string());
format!(
"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/anthropic/models"
)
} else if has_generic {
"https://api.openai.com/v1".to_string()
} else if std::env::var("GOOGLE_API_KEY").is_ok() {
"https://generativelanguage.googleapis.com/v1beta/openai".to_string()
} else if std::env::var("DEEPSEEK_API_KEY").is_ok() {
"https://api.deepseek.com/v1".to_string()
} else if std::env::var("XAI_API_KEY").is_ok() {
"https://api.x.ai/v1".to_string()
} else if std::env::var("GROQ_API_KEY").is_ok() {
"https://api.groq.com/openai/v1".to_string()
} else if std::env::var("MISTRAL_API_KEY").is_ok() {
"https://api.mistral.ai/v1".to_string()
} else if std::env::var("TOGETHER_API_KEY").is_ok() {
"https://api.together.xyz/v1".to_string()
} else {
"https://api.openai.com/v1".to_string()
};
Self {
base_url,
model: "gpt-5.4".to_string(),
api_key,
max_output_tokens: Some(16384),
thinking: None,
effort: None,
max_cost_usd: None,
timeout_secs: 120,
max_retries: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PermissionsConfig {
pub default_mode: PermissionMode,
pub rules: Vec<PermissionRule>,
}
impl Default for PermissionsConfig {
fn default() -> Self {
Self {
default_mode: PermissionMode::Ask,
rules: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionMode {
Allow,
Deny,
Ask,
AcceptEdits,
Plan,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRule {
pub tool: String,
pub pattern: Option<String>,
pub action: PermissionMode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UiConfig {
pub markdown: bool,
pub syntax_highlight: bool,
pub theme: String,
pub edit_mode: String,
}
impl Default for UiConfig {
fn default() -> Self {
Self {
markdown: true,
syntax_highlight: true,
theme: "dark".to_string(),
edit_mode: "emacs".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FeaturesConfig {
pub token_budget: bool,
pub commit_attribution: bool,
pub compaction_reminders: bool,
pub unattended_retry: bool,
pub history_snip: bool,
pub auto_theme: bool,
pub mcp_rich_output: bool,
pub fork_conversation: bool,
pub verification_agent: bool,
pub extract_memories: bool,
pub context_collapse: bool,
pub reactive_compact: bool,
}
impl Default for FeaturesConfig {
fn default() -> Self {
Self {
token_budget: true,
commit_attribution: true,
compaction_reminders: true,
unattended_retry: true,
history_snip: true,
auto_theme: true,
mcp_rich_output: true,
fork_conversation: true,
verification_agent: true,
extract_memories: true,
context_collapse: true,
reactive_compact: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookEvent {
SessionStart,
SessionStop,
PreToolUse,
PostToolUse,
UserPromptSubmit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum HookAction {
#[serde(rename = "shell")]
Shell { command: String },
#[serde(rename = "http")]
Http { url: String, method: Option<String> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookDefinition {
pub event: HookEvent,
pub action: HookAction,
pub tool_name: Option<String>,
}