Skip to main content

agent_diva_core/config/
schema.rs

1//! Configuration schema definitions
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Root configuration for agent-diva
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct Config {
9    /// Agent configuration
10    pub agents: AgentsConfig,
11    /// Channel configuration
12    pub channels: ChannelsConfig,
13    /// Provider configuration
14    pub providers: ProvidersConfig,
15    /// Gateway configuration
16    pub gateway: GatewayConfig,
17    /// Tools configuration
18    pub tools: ToolsConfig,
19    /// Logging configuration
20    #[serde(default)]
21    pub logging: LoggingConfig,
22}
23
24/// Logging configuration
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct LoggingConfig {
27    /// Default log level (trace, debug, info, warn, error)
28    #[serde(default = "default_log_level")]
29    pub level: String,
30    /// Log format (text, json)
31    #[serde(default = "default_log_format")]
32    pub format: String,
33    /// Directory for log files
34    #[serde(default = "default_log_dir")]
35    pub dir: String,
36    /// Module-specific overrides
37    #[serde(default)]
38    pub overrides: HashMap<String, String>,
39}
40
41fn default_log_level() -> String {
42    "info".to_string()
43}
44
45fn default_log_format() -> String {
46    "text".to_string()
47}
48
49fn default_log_dir() -> String {
50    "logs".to_string()
51}
52
53impl Default for LoggingConfig {
54    fn default() -> Self {
55        Self {
56            level: default_log_level(),
57            format: default_log_format(),
58            dir: default_log_dir(),
59            overrides: HashMap::new(),
60        }
61    }
62}
63
64/// Agent configuration
65#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66pub struct AgentsConfig {
67    /// Default agent settings
68    pub defaults: AgentDefaults,
69    /// Soul and identity behavior
70    #[serde(default)]
71    pub soul: AgentSoulConfig,
72}
73
74/// Default agent settings
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct AgentDefaults {
77    /// Workspace directory
78    pub workspace: String,
79    /// Explicit default provider selection
80    #[serde(default)]
81    pub provider: Option<String>,
82    /// Default model
83    pub model: String,
84    /// Maximum tokens
85    pub max_tokens: u32,
86    /// Temperature
87    pub temperature: f32,
88    /// Maximum tool iterations
89    pub max_tool_iterations: u32,
90    /// Optional reasoning effort for thinking-capable models (low/medium/high)
91    #[serde(default)]
92    pub reasoning_effort: Option<String>,
93}
94
95impl Default for AgentDefaults {
96    fn default() -> Self {
97        Self {
98            workspace: "~/.agent-diva/workspace".to_string(),
99            provider: Some("deepseek".to_string()),
100            model: "deepseek-chat".to_string(),
101            max_tokens: 8192,
102            temperature: 0.7,
103            max_tool_iterations: 20,
104            reasoning_effort: None,
105        }
106    }
107}
108
109/// Soul/identity settings
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct AgentSoulConfig {
112    /// Whether soul context injection is enabled.
113    #[serde(default = "default_true")]
114    pub enabled: bool,
115    /// Max characters loaded from each soul markdown file.
116    #[serde(default = "default_soul_max_chars")]
117    pub max_chars: usize,
118    /// Whether to notify user when soul files are updated.
119    #[serde(default = "default_true")]
120    pub notify_on_change: bool,
121    /// If true, BOOTSTRAP.md is only used until bootstrap is completed.
122    #[serde(default = "default_true")]
123    pub bootstrap_once: bool,
124    /// Rolling window in seconds for frequent soul-change hints.
125    #[serde(default = "default_soul_window_secs")]
126    pub frequent_change_window_secs: u64,
127    /// Minimum soul-changing turns in window to trigger hints.
128    #[serde(default = "default_soul_change_threshold")]
129    pub frequent_change_threshold: usize,
130    /// Add boundary confirmation hint when SOUL.md changes.
131    #[serde(default = "default_true")]
132    pub boundary_confirmation_hint: bool,
133}
134
135impl Default for AgentSoulConfig {
136    fn default() -> Self {
137        Self {
138            enabled: true,
139            max_chars: 4000,
140            notify_on_change: true,
141            bootstrap_once: true,
142            frequent_change_window_secs: 600,
143            frequent_change_threshold: 3,
144            boundary_confirmation_hint: true,
145        }
146    }
147}
148
149fn default_soul_max_chars() -> usize {
150    4000
151}
152
153fn default_soul_window_secs() -> u64 {
154    600
155}
156
157fn default_soul_change_threshold() -> usize {
158    3
159}
160
161/// Channel configuration
162#[derive(Debug, Clone, Serialize, Deserialize, Default)]
163pub struct ChannelsConfig {
164    #[serde(default)]
165    pub telegram: TelegramConfig,
166    #[serde(default)]
167    pub discord: DiscordConfig,
168    #[serde(default)]
169    pub whatsapp: WhatsAppConfig,
170    #[serde(default)]
171    pub feishu: FeishuConfig,
172    #[serde(default)]
173    pub dingtalk: DingTalkConfig,
174    #[serde(default)]
175    pub email: EmailConfig,
176    #[serde(default)]
177    pub slack: SlackConfig,
178    #[serde(default)]
179    pub qq: QQConfig,
180    #[serde(default)]
181    pub matrix: MatrixConfig,
182    #[serde(
183        default,
184        rename = "neuro-link",
185        alias = "neuro_link",
186        alias = "generic_pipe"
187    )]
188    pub neuro_link: NeuroLinkConfig,
189    #[serde(default)]
190    pub irc: IrcConfig,
191    #[serde(default)]
192    pub mattermost: MattermostConfig,
193    #[serde(default)]
194    pub nextcloud_talk: NextcloudTalkConfig,
195}
196
197/// Telegram channel configuration
198#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199pub struct TelegramConfig {
200    #[serde(default)]
201    pub enabled: bool,
202    #[serde(default)]
203    pub token: String,
204    #[serde(default)]
205    pub allow_from: Vec<String>,
206    #[serde(default)]
207    pub proxy: Option<String>,
208}
209
210/// Discord channel configuration
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct DiscordConfig {
213    #[serde(default)]
214    pub enabled: bool,
215    #[serde(default)]
216    pub token: String,
217    #[serde(default)]
218    pub allow_from: Vec<String>,
219    #[serde(default = "default_discord_gateway")]
220    pub gateway_url: String,
221    #[serde(default = "default_discord_intents")]
222    pub intents: u64,
223    /// When set, only guild messages from this server are handled (DMs are still allowed).
224    #[serde(default)]
225    pub guild_id: Option<String>,
226    /// In guild channels, require @mention of the bot (unless sender is in `group_reply_allowed_sender_ids`).
227    #[serde(default)]
228    pub mention_only: bool,
229    /// When true, process messages from other bots.
230    #[serde(default)]
231    pub listen_to_bots: bool,
232    /// User IDs that may trigger the bot in guild channels without @mention when `mention_only` is true.
233    #[serde(default)]
234    pub group_reply_allowed_sender_ids: Vec<String>,
235}
236
237fn default_discord_gateway() -> String {
238    "wss://gateway.discord.gg/?v=10&encoding=json".to_string()
239}
240
241fn default_discord_intents() -> u64 {
242    37377 // GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT
243}
244
245impl Default for DiscordConfig {
246    fn default() -> Self {
247        Self {
248            enabled: false,
249            token: String::new(),
250            allow_from: Vec::new(),
251            gateway_url: default_discord_gateway(),
252            intents: default_discord_intents(),
253            guild_id: None,
254            mention_only: false,
255            listen_to_bots: false,
256            group_reply_allowed_sender_ids: Vec::new(),
257        }
258    }
259}
260
261/// WhatsApp channel configuration
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct WhatsAppConfig {
264    #[serde(default)]
265    pub enabled: bool,
266    #[serde(default = "default_whatsapp_bridge")]
267    pub bridge_url: String,
268    #[serde(default)]
269    pub allow_from: Vec<String>,
270}
271
272fn default_whatsapp_bridge() -> String {
273    "ws://localhost:3001".to_string()
274}
275
276impl Default for WhatsAppConfig {
277    fn default() -> Self {
278        Self {
279            enabled: false,
280            bridge_url: default_whatsapp_bridge(),
281            allow_from: Vec::new(),
282        }
283    }
284}
285
286/// Feishu/Lark channel configuration
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288pub struct FeishuConfig {
289    #[serde(default)]
290    pub enabled: bool,
291    #[serde(default)]
292    pub app_id: String,
293    #[serde(default)]
294    pub app_secret: String,
295    #[serde(default)]
296    pub encrypt_key: String,
297    #[serde(default)]
298    pub verification_token: String,
299    #[serde(default)]
300    pub allow_from: Vec<String>,
301    /// Optional port for webhook mode (not used in WebSocket mode)
302    #[serde(default)]
303    pub port: Option<u16>,
304}
305
306/// DingTalk channel configuration
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct DingTalkConfig {
309    #[serde(default)]
310    pub enabled: bool,
311    #[serde(default)]
312    pub client_id: String,
313    #[serde(default)]
314    pub client_secret: String,
315    #[serde(default)]
316    pub robot_code: String,
317    #[serde(default = "default_dingtalk_policy")]
318    pub dm_policy: String,
319    #[serde(default = "default_dingtalk_policy")]
320    pub group_policy: String,
321    #[serde(default)]
322    pub allow_from: Vec<String>,
323}
324
325fn default_dingtalk_policy() -> String {
326    "open".to_string()
327}
328
329impl Default for DingTalkConfig {
330    fn default() -> Self {
331        Self {
332            enabled: false,
333            client_id: String::new(),
334            client_secret: String::new(),
335            robot_code: String::new(),
336            dm_policy: default_dingtalk_policy(),
337            group_policy: default_dingtalk_policy(),
338            allow_from: Vec::new(),
339        }
340    }
341}
342
343/// Email channel configuration
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct EmailConfig {
346    #[serde(default)]
347    pub enabled: bool,
348    #[serde(default)]
349    pub consent_granted: bool,
350    // IMAP settings
351    #[serde(default)]
352    pub imap_host: String,
353    #[serde(default = "default_imap_port")]
354    pub imap_port: u16,
355    #[serde(default)]
356    pub imap_username: String,
357    #[serde(default)]
358    pub imap_password: String,
359    #[serde(default = "default_imap_mailbox")]
360    pub imap_mailbox: String,
361    #[serde(default = "default_true")]
362    pub imap_use_ssl: bool,
363    // SMTP settings
364    #[serde(default)]
365    pub smtp_host: String,
366    #[serde(default = "default_smtp_port")]
367    pub smtp_port: u16,
368    #[serde(default)]
369    pub smtp_username: String,
370    #[serde(default)]
371    pub smtp_password: String,
372    #[serde(default = "default_true")]
373    pub smtp_use_tls: bool,
374    #[serde(default)]
375    pub smtp_use_ssl: bool,
376    #[serde(default)]
377    pub from_address: String,
378    // Behavior
379    #[serde(default = "default_true")]
380    pub auto_reply_enabled: bool,
381    #[serde(default = "default_poll_interval")]
382    pub poll_interval_seconds: u64,
383    #[serde(default = "default_true")]
384    pub mark_seen: bool,
385    #[serde(default = "default_max_body")]
386    pub max_body_chars: usize,
387    #[serde(default = "default_subject_prefix")]
388    pub subject_prefix: String,
389    #[serde(default)]
390    pub allow_from: Vec<String>,
391}
392
393fn default_imap_port() -> u16 {
394    993
395}
396fn default_imap_mailbox() -> String {
397    "INBOX".to_string()
398}
399fn default_smtp_port() -> u16 {
400    587
401}
402fn default_poll_interval() -> u64 {
403    30
404}
405fn default_max_body() -> usize {
406    12000
407}
408fn default_subject_prefix() -> String {
409    "Re: ".to_string()
410}
411fn default_true() -> bool {
412    true
413}
414
415impl Default for EmailConfig {
416    fn default() -> Self {
417        Self {
418            enabled: false,
419            consent_granted: false,
420            imap_host: String::new(),
421            imap_port: default_imap_port(),
422            imap_username: String::new(),
423            imap_password: String::new(),
424            imap_mailbox: default_imap_mailbox(),
425            imap_use_ssl: true,
426            smtp_host: String::new(),
427            smtp_port: default_smtp_port(),
428            smtp_username: String::new(),
429            smtp_password: String::new(),
430            smtp_use_tls: true,
431            smtp_use_ssl: false,
432            from_address: String::new(),
433            auto_reply_enabled: true,
434            poll_interval_seconds: default_poll_interval(),
435            mark_seen: true,
436            max_body_chars: default_max_body(),
437            subject_prefix: default_subject_prefix(),
438            allow_from: Vec::new(),
439        }
440    }
441}
442
443/// Slack channel configuration
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct SlackConfig {
446    #[serde(default)]
447    pub enabled: bool,
448    #[serde(default = "default_slack_mode")]
449    pub mode: String,
450    #[serde(default)]
451    pub webhook_path: String,
452    #[serde(default)]
453    pub bot_token: String,
454    #[serde(default)]
455    pub app_token: String,
456    #[serde(default = "default_true")]
457    pub user_token_read_only: bool,
458    #[serde(default = "default_slack_policy")]
459    pub group_policy: String,
460    #[serde(default)]
461    pub group_allow_from: Vec<String>,
462    #[serde(default)]
463    pub dm: SlackDMConfig,
464}
465
466fn default_slack_mode() -> String {
467    "socket".to_string()
468}
469fn default_slack_policy() -> String {
470    "mention".to_string()
471}
472
473impl Default for SlackConfig {
474    fn default() -> Self {
475        Self {
476            enabled: false,
477            mode: default_slack_mode(),
478            webhook_path: "/slack/events".to_string(),
479            bot_token: String::new(),
480            app_token: String::new(),
481            user_token_read_only: true,
482            group_policy: default_slack_policy(),
483            group_allow_from: Vec::new(),
484            dm: SlackDMConfig::default(),
485        }
486    }
487}
488
489/// Slack DM configuration
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct SlackDMConfig {
492    #[serde(default = "default_true")]
493    pub enabled: bool,
494    #[serde(default = "default_slack_dm_policy")]
495    pub policy: String,
496    #[serde(default)]
497    pub allow_from: Vec<String>,
498}
499
500fn default_slack_dm_policy() -> String {
501    "open".to_string()
502}
503
504impl Default for SlackDMConfig {
505    fn default() -> Self {
506        Self {
507            enabled: true,
508            policy: default_slack_dm_policy(),
509            allow_from: Vec::new(),
510        }
511    }
512}
513
514/// QQ channel configuration
515#[derive(Debug, Clone, Serialize, Deserialize, Default)]
516pub struct QQConfig {
517    #[serde(default)]
518    pub enabled: bool,
519    #[serde(default)]
520    pub app_id: String,
521    #[serde(default)]
522    pub secret: String,
523    #[serde(default)]
524    pub allow_from: Vec<String>,
525}
526
527/// Neuro-link (WebSocket server) channel configuration
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct NeuroLinkConfig {
530    #[serde(default)]
531    pub enabled: bool,
532    #[serde(default = "default_pipe_host")]
533    pub host: String,
534    #[serde(default = "default_pipe_port")]
535    pub port: u16,
536    #[serde(default)]
537    pub allow_from: Vec<String>,
538}
539
540fn default_pipe_host() -> String {
541    "0.0.0.0".to_string()
542}
543fn default_pipe_port() -> u16 {
544    9100
545}
546
547impl Default for NeuroLinkConfig {
548    fn default() -> Self {
549        Self {
550            enabled: false,
551            host: default_pipe_host(),
552            port: default_pipe_port(),
553            allow_from: Vec::new(),
554        }
555    }
556}
557
558/// Matrix channel configuration
559#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct MatrixConfig {
561    #[serde(default)]
562    pub enabled: bool,
563    #[serde(default = "default_matrix_homeserver")]
564    pub homeserver: String,
565    #[serde(default)]
566    pub user_id: String,
567    #[serde(default)]
568    pub access_token: String,
569    #[serde(default)]
570    pub device_id: String,
571    #[serde(default = "default_true")]
572    pub e2ee_enabled: bool,
573    #[serde(default = "default_matrix_media_limit")]
574    pub max_media_bytes: usize,
575    #[serde(default)]
576    pub allow_from: Vec<String>,
577    #[serde(default)]
578    pub group_allow_from: Vec<String>,
579    #[serde(default = "default_matrix_sync_timeout")]
580    pub sync_timeout_ms: u64,
581    #[serde(default = "default_matrix_sync_stop_grace")]
582    pub sync_stop_grace_seconds: u64,
583}
584
585fn default_matrix_homeserver() -> String {
586    "https://matrix.org".to_string()
587}
588fn default_matrix_media_limit() -> usize {
589    20 * 1024 * 1024
590}
591fn default_matrix_sync_timeout() -> u64 {
592    30_000
593}
594fn default_matrix_sync_stop_grace() -> u64 {
595    8
596}
597
598impl Default for MatrixConfig {
599    fn default() -> Self {
600        Self {
601            enabled: false,
602            homeserver: default_matrix_homeserver(),
603            user_id: String::new(),
604            access_token: String::new(),
605            device_id: String::new(),
606            e2ee_enabled: true,
607            max_media_bytes: default_matrix_media_limit(),
608            allow_from: Vec::new(),
609            group_allow_from: Vec::new(),
610            sync_timeout_ms: default_matrix_sync_timeout(),
611            sync_stop_grace_seconds: default_matrix_sync_stop_grace(),
612        }
613    }
614}
615
616/// IRC channel configuration
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct IrcConfig {
619    #[serde(default)]
620    pub enabled: bool,
621    #[serde(default)]
622    pub server: String,
623    #[serde(default = "default_irc_port")]
624    pub port: u16,
625    #[serde(default)]
626    pub nickname: String,
627    #[serde(default)]
628    pub username: String,
629    #[serde(default)]
630    pub channels: Vec<String>,
631    #[serde(default)]
632    pub server_password: Option<String>,
633    #[serde(default)]
634    pub nickserv_password: Option<String>,
635    #[serde(default)]
636    pub sasl_password: Option<String>,
637    #[serde(default = "default_true")]
638    pub use_tls: bool,
639    #[serde(default = "default_true")]
640    pub verify_tls: bool,
641    #[serde(default)]
642    pub allow_from: Vec<String>,
643}
644
645fn default_irc_port() -> u16 {
646    6697
647}
648
649impl Default for IrcConfig {
650    fn default() -> Self {
651        Self {
652            enabled: false,
653            server: String::new(),
654            port: default_irc_port(),
655            nickname: String::new(),
656            username: String::new(),
657            channels: Vec::new(),
658            server_password: None,
659            nickserv_password: None,
660            sasl_password: None,
661            use_tls: true,
662            verify_tls: true,
663            allow_from: Vec::new(),
664        }
665    }
666}
667
668/// Mattermost channel configuration
669#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct MattermostConfig {
671    #[serde(default)]
672    pub enabled: bool,
673    #[serde(default)]
674    pub base_url: String,
675    #[serde(default)]
676    pub bot_token: String,
677    #[serde(default)]
678    pub channel_id: String,
679    #[serde(default = "default_true")]
680    pub thread_replies: bool,
681    #[serde(default)]
682    pub mention_only: bool,
683    #[serde(default = "default_mm_poll_interval")]
684    pub poll_interval_seconds: u64,
685    #[serde(default)]
686    pub allow_from: Vec<String>,
687}
688
689fn default_mm_poll_interval() -> u64 {
690    3
691}
692
693impl Default for MattermostConfig {
694    fn default() -> Self {
695        Self {
696            enabled: false,
697            base_url: String::new(),
698            bot_token: String::new(),
699            channel_id: String::new(),
700            thread_replies: true,
701            mention_only: false,
702            poll_interval_seconds: default_mm_poll_interval(),
703            allow_from: Vec::new(),
704        }
705    }
706}
707
708/// Nextcloud Talk channel configuration
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct NextcloudTalkConfig {
711    #[serde(default)]
712    pub enabled: bool,
713    #[serde(default)]
714    pub base_url: String,
715    #[serde(default)]
716    pub app_token: String,
717    #[serde(default)]
718    pub room_token: String,
719    #[serde(default = "default_nc_poll_interval")]
720    pub poll_interval_seconds: u64,
721    #[serde(default)]
722    pub allow_from: Vec<String>,
723}
724
725fn default_nc_poll_interval() -> u64 {
726    5
727}
728
729impl Default for NextcloudTalkConfig {
730    fn default() -> Self {
731        Self {
732            enabled: false,
733            base_url: String::new(),
734            app_token: String::new(),
735            room_token: String::new(),
736            poll_interval_seconds: default_nc_poll_interval(),
737            allow_from: Vec::new(),
738        }
739    }
740}
741
742/// Provider configuration
743#[derive(Debug, Clone, Serialize, Deserialize, Default)]
744pub struct ProvidersConfig {
745    #[serde(default)]
746    pub anthropic: ProviderConfig,
747    #[serde(default)]
748    pub openai: ProviderConfig,
749    #[serde(default)]
750    pub openrouter: ProviderConfig,
751    #[serde(default)]
752    pub deepseek: ProviderConfig,
753    #[serde(default)]
754    pub groq: ProviderConfig,
755    #[serde(default)]
756    pub zhipu: ProviderConfig,
757    #[serde(default)]
758    pub dashscope: ProviderConfig,
759    #[serde(default)]
760    pub vllm: ProviderConfig,
761    #[serde(default)]
762    pub gemini: ProviderConfig,
763    #[serde(default)]
764    pub moonshot: ProviderConfig,
765    #[serde(default)]
766    pub minimax: ProviderConfig,
767    #[serde(default)]
768    pub aihubmix: ProviderConfig,
769    #[serde(default)]
770    pub custom: ProviderConfig,
771    #[serde(default)]
772    pub custom_providers: HashMap<String, CustomProviderConfig>,
773}
774
775/// Individual provider configuration
776#[derive(Debug, Clone, Serialize, Deserialize, Default)]
777pub struct ProviderConfig {
778    #[serde(default)]
779    pub api_key: String,
780    #[serde(default)]
781    pub api_base: Option<String>,
782    #[serde(default)]
783    pub extra_headers: Option<HashMap<String, String>>,
784    #[serde(default)]
785    pub custom_models: Vec<String>,
786}
787
788/// User-defined provider configuration.
789#[derive(Debug, Clone, Serialize, Deserialize, Default)]
790pub struct CustomProviderConfig {
791    #[serde(default)]
792    pub display_name: String,
793    #[serde(default = "default_custom_provider_api_type")]
794    pub api_type: String,
795    #[serde(default)]
796    pub api_key: String,
797    #[serde(default)]
798    pub api_base: Option<String>,
799    #[serde(default)]
800    pub default_model: Option<String>,
801    #[serde(default)]
802    pub models: Vec<String>,
803    #[serde(default)]
804    pub extra_headers: Option<HashMap<String, String>>,
805}
806
807fn default_custom_provider_api_type() -> String {
808    "openai".to_string()
809}
810
811impl ProvidersConfig {
812    pub const BUILTIN_PROVIDER_IDS: [&'static str; 13] = [
813        "anthropic",
814        "openai",
815        "openrouter",
816        "deepseek",
817        "groq",
818        "zhipu",
819        "dashscope",
820        "vllm",
821        "gemini",
822        "moonshot",
823        "minimax",
824        "aihubmix",
825        "custom",
826    ];
827
828    pub fn builtin_provider_names() -> &'static [&'static str] {
829        &Self::BUILTIN_PROVIDER_IDS
830    }
831
832    pub fn get(&self, name: &str) -> Option<&ProviderConfig> {
833        match name {
834            "anthropic" => Some(&self.anthropic),
835            "openai" => Some(&self.openai),
836            "openrouter" => Some(&self.openrouter),
837            "deepseek" => Some(&self.deepseek),
838            "groq" => Some(&self.groq),
839            "zhipu" => Some(&self.zhipu),
840            "dashscope" => Some(&self.dashscope),
841            "vllm" => Some(&self.vllm),
842            "gemini" => Some(&self.gemini),
843            "moonshot" => Some(&self.moonshot),
844            "minimax" => Some(&self.minimax),
845            "aihubmix" => Some(&self.aihubmix),
846            "custom" => Some(&self.custom),
847            _ => None,
848        }
849    }
850
851    pub fn get_mut(&mut self, name: &str) -> Option<&mut ProviderConfig> {
852        match name {
853            "anthropic" => Some(&mut self.anthropic),
854            "openai" => Some(&mut self.openai),
855            "openrouter" => Some(&mut self.openrouter),
856            "deepseek" => Some(&mut self.deepseek),
857            "groq" => Some(&mut self.groq),
858            "zhipu" => Some(&mut self.zhipu),
859            "dashscope" => Some(&mut self.dashscope),
860            "vllm" => Some(&mut self.vllm),
861            "gemini" => Some(&mut self.gemini),
862            "moonshot" => Some(&mut self.moonshot),
863            "minimax" => Some(&mut self.minimax),
864            "aihubmix" => Some(&mut self.aihubmix),
865            "custom" => Some(&mut self.custom),
866            _ => None,
867        }
868    }
869
870    pub fn get_custom(&self, name: &str) -> Option<&CustomProviderConfig> {
871        self.custom_providers.get(name)
872    }
873
874    pub fn get_custom_mut(&mut self, name: &str) -> Option<&mut CustomProviderConfig> {
875        self.custom_providers.get_mut(name)
876    }
877
878    pub fn is_builtin_provider(name: &str) -> bool {
879        Self::BUILTIN_PROVIDER_IDS.contains(&name)
880    }
881}
882
883/// Gateway configuration
884#[derive(Debug, Clone, Serialize, Deserialize)]
885pub struct GatewayConfig {
886    #[serde(default = "default_host")]
887    pub host: String,
888    #[serde(default = "default_port")]
889    pub port: u16,
890}
891
892fn default_host() -> String {
893    "0.0.0.0".to_string()
894}
895fn default_port() -> u16 {
896    3000
897}
898
899impl Default for GatewayConfig {
900    fn default() -> Self {
901        Self {
902            host: default_host(),
903            port: default_port(),
904        }
905    }
906}
907
908/// Tools configuration
909#[derive(Debug, Clone, Serialize, Deserialize, Default)]
910pub struct ToolsConfig {
911    #[serde(default)]
912    pub builtin: BuiltInToolsConfig,
913    #[serde(default)]
914    pub web: WebToolsConfig,
915    #[serde(default)]
916    pub exec: ExecToolConfig,
917    #[serde(default)]
918    pub restrict_to_workspace: bool,
919    #[serde(default, rename = "mcpServers", alias = "mcp_servers")]
920    pub mcp_servers: HashMap<String, MCPServerConfig>,
921    #[serde(default, rename = "mcpManager", alias = "mcp_manager")]
922    pub mcp_manager: MCPManagerConfig,
923}
924
925#[derive(Debug, Clone, Serialize, Deserialize)]
926pub struct BuiltInToolsConfig {
927    #[serde(default = "default_enabled")]
928    pub filesystem: bool,
929    #[serde(default = "default_enabled")]
930    pub shell: bool,
931    #[serde(default = "default_enabled")]
932    pub web_search: bool,
933    #[serde(default = "default_enabled")]
934    pub web_fetch: bool,
935    #[serde(default = "default_enabled")]
936    pub spawn: bool,
937    #[serde(default)]
938    pub cron: bool,
939    #[serde(default = "default_enabled")]
940    pub mcp: bool,
941    #[serde(default = "default_enabled")]
942    pub attachment: bool,
943}
944
945impl Default for BuiltInToolsConfig {
946    fn default() -> Self {
947        Self {
948            filesystem: true,
949            shell: true,
950            web_search: true,
951            web_fetch: true,
952            spawn: true,
953            cron: false,
954            mcp: true,
955            attachment: true,
956        }
957    }
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize, Default)]
961pub struct MCPManagerConfig {
962    #[serde(default)]
963    pub disabled_servers: Vec<String>,
964}
965
966impl ToolsConfig {
967    pub fn active_mcp_servers(&self) -> HashMap<String, MCPServerConfig> {
968        self.mcp_servers
969            .iter()
970            .filter(|(name, _)| !self.is_mcp_server_disabled(name))
971            .map(|(name, cfg)| (name.clone(), cfg.clone()))
972            .collect()
973    }
974
975    pub fn is_mcp_server_disabled(&self, name: &str) -> bool {
976        self.mcp_manager
977            .disabled_servers
978            .iter()
979            .any(|server| server == name)
980    }
981}
982
983/// MCP server connection configuration (stdio or HTTP)
984#[derive(Debug, Clone, Serialize, Deserialize, Default)]
985pub struct MCPServerConfig {
986    #[serde(default)]
987    pub command: String,
988    #[serde(default)]
989    pub args: Vec<String>,
990    #[serde(default)]
991    pub env: HashMap<String, String>,
992    #[serde(default)]
993    pub url: String,
994    /// Per-tool timeout in seconds (default: 30)
995    #[serde(default = "default_tool_timeout")]
996    pub tool_timeout: u64,
997}
998
999fn default_tool_timeout() -> u64 {
1000    30
1001}
1002
1003/// Web tools configuration
1004#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1005pub struct WebToolsConfig {
1006    #[serde(default)]
1007    pub search: WebSearchConfig,
1008    #[serde(default)]
1009    pub fetch: WebFetchConfig,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct WebFetchConfig {
1014    #[serde(default = "default_enabled")]
1015    pub enabled: bool,
1016}
1017
1018/// Web search configuration
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1020pub struct WebSearchConfig {
1021    #[serde(default = "default_search_provider")]
1022    pub provider: String,
1023    #[serde(default = "default_enabled")]
1024    pub enabled: bool,
1025    #[serde(default)]
1026    pub api_key: String,
1027    #[serde(default = "default_max_results")]
1028    pub max_results: u32,
1029}
1030
1031fn default_search_provider() -> String {
1032    "bocha".to_string()
1033}
1034
1035fn default_enabled() -> bool {
1036    true
1037}
1038
1039fn default_max_results() -> u32 {
1040    5
1041}
1042
1043impl Default for WebSearchConfig {
1044    fn default() -> Self {
1045        Self {
1046            provider: default_search_provider(),
1047            enabled: default_enabled(),
1048            api_key: String::new(),
1049            max_results: default_max_results(),
1050        }
1051    }
1052}
1053
1054impl Default for WebFetchConfig {
1055    fn default() -> Self {
1056        Self {
1057            enabled: default_enabled(),
1058        }
1059    }
1060}
1061
1062/// Exec tool configuration
1063#[derive(Debug, Clone, Serialize, Deserialize)]
1064pub struct ExecToolConfig {
1065    #[serde(default = "default_timeout")]
1066    pub timeout: u64,
1067}
1068
1069fn default_timeout() -> u64 {
1070    60
1071}
1072
1073impl Default for ExecToolConfig {
1074    fn default() -> Self {
1075        Self {
1076            timeout: default_timeout(),
1077        }
1078    }
1079}