use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct Config {
pub agents: AgentConfig,
pub channels: ChannelsConfig,
pub providers: ProvidersConfig,
pub gateway: GatewayConfig,
pub tools: ToolsConfig,
pub memory: MemoryConfig,
pub heartbeat: HeartbeatConfig,
pub skills: SkillsConfig,
pub runtime: RuntimeConfig,
pub container_agent: ContainerAgentConfig,
pub swarm: SwarmConfig,
pub approval: crate::tools::approval::ApprovalConfig,
pub plugins: crate::plugins::types::PluginConfig,
pub telemetry: crate::utils::telemetry::TelemetryConfig,
pub cost: crate::utils::cost::CostConfig,
pub batch: crate::batch::BatchConfig,
pub hooks: crate::hooks::HooksConfig,
pub safety: crate::safety::SafetyConfig,
pub compaction: CompactionConfig,
pub mcp: McpConfig,
pub routines: RoutinesConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CompactionConfig {
pub enabled: bool,
pub context_limit: usize,
pub threshold: f64,
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
enabled: false,
context_limit: 100_000,
threshold: 0.80,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct McpConfig {
pub servers: Vec<McpServerConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
pub name: String,
pub url: String,
#[serde(default = "default_mcp_timeout")]
pub timeout_secs: u64,
}
fn default_mcp_timeout() -> u64 {
30
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RoutinesConfig {
pub enabled: bool,
pub cron_interval_secs: u64,
pub max_concurrent: usize,
}
impl Default for RoutinesConfig {
fn default() -> Self {
Self {
enabled: false,
cron_interval_secs: 60,
max_concurrent: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[derive(Default)]
pub struct AgentConfig {
pub defaults: AgentDefaults,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentDefaults {
pub workspace: String,
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
pub max_tool_iterations: u32,
pub agent_timeout_secs: u64,
pub message_queue_mode: MessageQueueMode,
pub streaming: bool,
pub token_budget: u64,
}
const COMPILE_TIME_DEFAULT_MODEL: &str = match option_env!("ZEPTOCLAW_DEFAULT_MODEL") {
Some(v) => v,
None => "claude-sonnet-4-5-20250929",
};
impl Default for AgentDefaults {
fn default() -> Self {
Self {
workspace: "~/.zeptoclaw/workspace".to_string(),
model: COMPILE_TIME_DEFAULT_MODEL.to_string(),
max_tokens: 8192,
temperature: 0.7,
max_tool_iterations: 20,
agent_timeout_secs: 300,
message_queue_mode: MessageQueueMode::default(),
streaming: false,
token_budget: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageQueueMode {
#[default]
Collect,
Followup,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ChannelsConfig {
pub telegram: Option<TelegramConfig>,
pub discord: Option<DiscordConfig>,
pub slack: Option<SlackConfig>,
pub whatsapp: Option<WhatsAppConfig>,
pub feishu: Option<FeishuConfig>,
pub maixcam: Option<MaixCamConfig>,
pub qq: Option<QQConfig>,
pub dingtalk: Option<DingTalkConfig>,
pub webhook: Option<WebhookConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_webhook_bind_address")]
pub bind_address: String,
#[serde(default = "default_webhook_port")]
pub port: u16,
#[serde(default = "default_webhook_path")]
pub path: String,
#[serde(default)]
pub auth_token: Option<String>,
#[serde(default)]
pub allow_from: Vec<String>,
}
fn default_webhook_bind_address() -> String {
"127.0.0.1".to_string()
}
fn default_webhook_port() -> u16 {
9876
}
fn default_webhook_path() -> String {
"/webhook".to_string()
}
impl Default for WebhookConfig {
fn default() -> Self {
Self {
enabled: false,
bind_address: default_webhook_bind_address(),
port: default_webhook_port(),
path: default_webhook_path(),
auth_token: None,
allow_from: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TelegramConfig {
#[serde(default)]
pub enabled: bool,
pub token: String,
#[serde(default)]
pub allow_from: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DiscordConfig {
#[serde(default)]
pub enabled: bool,
pub token: String,
#[serde(default)]
pub allow_from: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SlackConfig {
#[serde(default)]
pub enabled: bool,
pub bot_token: String,
pub app_token: String,
#[serde(default)]
pub allow_from: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhatsAppConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_whatsapp_bridge_url")]
pub bridge_url: String,
#[serde(default)]
pub allow_from: Vec<String>,
#[serde(default = "default_bridge_managed")]
pub bridge_managed: bool,
}
fn default_whatsapp_bridge_url() -> String {
"ws://localhost:3001".to_string()
}
fn default_bridge_managed() -> bool {
true
}
impl Default for WhatsAppConfig {
fn default() -> Self {
Self {
enabled: false,
bridge_url: default_whatsapp_bridge_url(),
allow_from: Vec::new(),
bridge_managed: default_bridge_managed(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FeishuConfig {
#[serde(default)]
pub enabled: bool,
pub app_id: String,
pub app_secret: String,
#[serde(default)]
pub encrypt_key: String,
#[serde(default)]
pub verification_token: String,
#[serde(default)]
pub allow_from: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaixCamConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_maixcam_host")]
pub host: String,
#[serde(default = "default_maixcam_port")]
pub port: u16,
#[serde(default)]
pub allow_from: Vec<String>,
}
fn default_maixcam_host() -> String {
"0.0.0.0".to_string()
}
fn default_maixcam_port() -> u16 {
18790
}
impl Default for MaixCamConfig {
fn default() -> Self {
Self {
enabled: false,
host: default_maixcam_host(),
port: default_maixcam_port(),
allow_from: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QQConfig {
#[serde(default)]
pub enabled: bool,
pub app_id: String,
pub app_secret: String,
#[serde(default)]
pub allow_from: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DingTalkConfig {
#[serde(default)]
pub enabled: bool,
pub client_id: String,
pub client_secret: String,
#[serde(default)]
pub allow_from: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ProvidersConfig {
pub anthropic: Option<ProviderConfig>,
pub openai: Option<ProviderConfig>,
pub openrouter: Option<ProviderConfig>,
pub groq: Option<ProviderConfig>,
pub zhipu: Option<ProviderConfig>,
pub vllm: Option<ProviderConfig>,
pub gemini: Option<ProviderConfig>,
pub ollama: Option<ProviderConfig>,
pub retry: RetryConfig,
pub fallback: FallbackConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProviderConfig {
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub api_base: Option<String>,
#[serde(default)]
pub auth_method: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RetryConfig {
pub enabled: bool,
pub max_retries: u32,
pub base_delay_ms: u64,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
enabled: true,
max_retries: 3,
base_delay_ms: 1_000,
max_delay_ms: 30_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FallbackConfig {
pub enabled: bool,
pub provider: Option<String>,
}
impl Default for FallbackConfig {
fn default() -> Self {
Self {
enabled: true,
provider: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GatewayConfig {
pub host: String,
pub port: u16,
}
impl Default for GatewayConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8080,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ToolsConfig {
pub web: WebToolsConfig,
pub whatsapp: WhatsAppToolConfig,
pub google_sheets: GoogleSheetsToolConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct WebToolsConfig {
pub search: WebSearchConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WebSearchConfig {
#[serde(default)]
pub api_key: Option<String>,
pub max_results: u32,
}
impl Default for WebSearchConfig {
fn default() -> Self {
Self {
api_key: None,
max_results: 5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WhatsAppToolConfig {
#[serde(default)]
pub business_account_id: Option<String>,
#[serde(default)]
pub phone_number_id: Option<String>,
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub webhook_verify_token: Option<String>,
pub default_language: String,
}
impl Default for WhatsAppToolConfig {
fn default() -> Self {
Self {
business_account_id: None,
phone_number_id: None,
access_token: None,
webhook_verify_token: None,
default_language: "ms".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct GoogleSheetsToolConfig {
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub service_account_base64: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MemoryBackend {
#[serde(rename = "none")]
Disabled,
#[default]
Builtin,
Qmd,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MemoryCitationsMode {
#[default]
Auto,
On,
Off,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct MemoryConfig {
pub backend: MemoryBackend,
pub citations: MemoryCitationsMode,
pub include_default_memory: bool,
pub max_results: u32,
pub min_score: f32,
pub max_snippet_chars: u32,
#[serde(default)]
pub extra_paths: Vec<String>,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
backend: MemoryBackend::Builtin,
citations: MemoryCitationsMode::Auto,
include_default_memory: true,
max_results: 6,
min_score: 0.2,
max_snippet_chars: 700,
extra_paths: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HeartbeatConfig {
pub enabled: bool,
pub interval_secs: u64,
#[serde(default)]
pub file_path: Option<String>,
}
impl Default for HeartbeatConfig {
fn default() -> Self {
Self {
enabled: false,
interval_secs: 30 * 60,
file_path: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SkillsConfig {
pub enabled: bool,
#[serde(default)]
pub workspace_dir: Option<String>,
#[serde(default)]
pub always_load: Vec<String>,
#[serde(default)]
pub disabled: Vec<String>,
}
impl Default for SkillsConfig {
fn default() -> Self {
Self {
enabled: true,
workspace_dir: None,
always_load: Vec::new(),
disabled: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SwarmConfig {
pub enabled: bool,
pub max_depth: u32,
pub max_concurrent: u32,
pub roles: std::collections::HashMap<String, SwarmRole>,
}
impl Default for SwarmConfig {
fn default() -> Self {
Self {
enabled: true,
max_depth: 1,
max_concurrent: 3,
roles: std::collections::HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SwarmRole {
pub system_prompt: String,
pub tools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RuntimeType {
#[default]
Native,
Docker,
#[serde(rename = "apple")]
AppleContainer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RuntimeConfig {
pub runtime_type: RuntimeType,
pub allow_fallback_to_native: bool,
#[serde(default = "default_mount_allowlist_path")]
pub mount_allowlist_path: String,
pub docker: DockerConfig,
pub apple: AppleContainerConfig,
}
fn default_mount_allowlist_path() -> String {
"~/.zeptoclaw/mount-allowlist.json".to_string()
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
runtime_type: RuntimeType::Native,
allow_fallback_to_native: false,
mount_allowlist_path: default_mount_allowlist_path(),
docker: DockerConfig::default(),
apple: AppleContainerConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DockerConfig {
pub image: String,
pub extra_mounts: Vec<String>,
pub memory_limit: Option<String>,
pub cpu_limit: Option<String>,
pub network: String,
}
impl Default for DockerConfig {
fn default() -> Self {
Self {
image: "alpine:latest".to_string(),
extra_mounts: Vec::new(),
memory_limit: Some("512m".to_string()),
cpu_limit: Some("1.0".to_string()),
network: "none".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct AppleContainerConfig {
pub image: String,
pub extra_mounts: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ContainerAgentBackend {
#[default]
Auto,
Docker,
#[cfg(target_os = "macos")]
#[serde(rename = "apple")]
Apple,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ContainerAgentConfig {
pub backend: ContainerAgentBackend,
pub image: String,
pub docker_binary: Option<String>,
pub memory_limit: Option<String>,
pub cpu_limit: Option<String>,
pub timeout_secs: u64,
pub network: String,
pub extra_mounts: Vec<String>,
pub max_concurrent: usize,
}
impl Default for ContainerAgentConfig {
fn default() -> Self {
Self {
backend: ContainerAgentBackend::Auto,
image: "zeptoclaw:latest".to_string(),
docker_binary: None,
memory_limit: Some("1g".to_string()),
cpu_limit: Some("2.0".to_string()),
timeout_secs: 300,
network: "none".to_string(),
extra_mounts: Vec::new(),
max_concurrent: 5,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swarm_config_defaults() {
let config = SwarmConfig::default();
assert!(config.enabled);
assert_eq!(config.max_depth, 1);
assert_eq!(config.max_concurrent, 3);
assert!(config.roles.is_empty());
}
#[test]
fn test_swarm_config_deserialize() {
let json = r#"{
"enabled": true,
"roles": {
"researcher": {
"system_prompt": "You are a researcher.",
"tools": ["web_search", "web_fetch"]
}
}
}"#;
let config: SwarmConfig = serde_json::from_str(json).unwrap();
assert!(config.enabled);
assert_eq!(config.roles.len(), 1);
let role = config.roles.get("researcher").unwrap();
assert_eq!(role.tools, vec!["web_search", "web_fetch"]);
}
#[test]
fn test_swarm_role_defaults() {
let role = SwarmRole::default();
assert!(role.system_prompt.is_empty());
assert!(role.tools.is_empty());
}
#[test]
fn test_streaming_defaults_to_false() {
let defaults = AgentDefaults::default();
assert!(!defaults.streaming);
}
#[test]
fn test_streaming_config_deserialize() {
let json = r#"{"streaming": true}"#;
let defaults: AgentDefaults = serde_json::from_str(json).unwrap();
assert!(defaults.streaming);
}
#[test]
fn test_config_with_swarm_deserialize() {
let json = r#"{
"swarm": {
"enabled": false,
"max_depth": 2
}
}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert!(!config.swarm.enabled);
assert_eq!(config.swarm.max_depth, 2);
}
}