Skip to main content

openrustclaw_core/
config.rs

1//! Configuration structs for OpenRustClaw.
2//!
3//! Maps to `config/default.toml` and environment variable overrides.
4//! Uses the `config` crate for layered configuration loading.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9/// Root configuration for the entire OpenRustClaw system.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AppConfig {
12    pub gateway: GatewayConfig,
13    pub database: DatabaseConfig,
14    pub providers: ProvidersConfig,
15    #[serde(default)]
16    pub external_backends: ExternalBackendsConfig,
17    pub memory: MemoryConfig,
18    pub session_routing: SessionRoutingConfig,
19    pub scheduler: SchedulerConfig,
20    pub security: SecurityConfig,
21    pub sidecar: SidecarConfig,
22    pub observability: ObservabilityConfig,
23    pub channels: ChannelsConfig,
24    pub voice: VoiceConfig,
25    pub skills: Option<SkillsConfig>,
26}
27
28/// Session-routing policy configuration.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SessionRoutingConfig {
31    #[serde(default = "default_direct_strategy")]
32    pub direct_strategy: String,
33    #[serde(default = "default_group_strategy")]
34    pub group_strategy: String,
35    #[serde(default = "default_thread_overrides_channel")]
36    pub thread_overrides_channel: bool,
37    #[serde(default = "default_pairing_approval_required")]
38    pub pairing_approval_required: bool,
39    #[serde(default = "default_group_activation_mode")]
40    pub default_group_activation: String,
41    #[serde(default = "default_send_mode")]
42    pub default_send_mode: String,
43    #[serde(default = "default_chunk_chars")]
44    pub default_chunk_chars: usize,
45    #[serde(default)]
46    pub default_chunk_delay_ms: u64,
47}
48
49fn default_direct_strategy() -> String {
50    "shared_main".to_string()
51}
52
53fn default_group_strategy() -> String {
54    "isolated".to_string()
55}
56
57fn default_thread_overrides_channel() -> bool {
58    true
59}
60
61fn default_pairing_approval_required() -> bool {
62    false
63}
64
65fn default_group_activation_mode() -> String {
66    "mention".to_string()
67}
68
69fn default_send_mode() -> String {
70    "blocks".to_string()
71}
72
73fn default_chunk_chars() -> usize {
74    1600
75}
76
77fn default_true() -> bool {
78    true
79}
80
81/// Gateway (WebSocket server) configuration.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct GatewayConfig {
84    #[serde(default = "default_gateway_network_mode")]
85    pub network_mode: String,
86    pub host: String,
87    pub port: u16,
88    pub allowed_origins: Vec<String>,
89}
90
91fn default_gateway_network_mode() -> String {
92    "loopback".to_string()
93}
94
95/// Database configuration.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct DatabaseConfig {
98    pub url: String,
99    pub wal_mode: bool,
100    pub max_connections: u32,
101}
102
103/// Provider configuration container.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ProvidersConfig {
106    pub default_provider: String,
107    pub fallback_chain: Vec<String>,
108    #[serde(default)]
109    pub control_plane_provider: Option<String>,
110    #[serde(default)]
111    pub control_plane_fallback_chain: Vec<String>,
112    pub anthropic: AnthropicConfig,
113    pub openai: OpenAiConfig,
114    pub openrouter: OpenRouterConfig,
115    pub ollama: OllamaConfig,
116    #[serde(default)]
117    pub gemini: GeminiConfig,
118}
119
120/// Anthropic provider configuration.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct AnthropicConfig {
123    pub model: String,
124    #[serde(default)]
125    pub api_key_env: Option<String>,
126    pub api_version: String,
127    pub strict_tools: bool,
128    pub streaming_tool_deltas: bool,
129}
130
131/// OpenAI provider configuration.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct OpenAiConfig {
134    pub model: String,
135    #[serde(default = "default_codex_model")]
136    pub codex_model: String,
137    #[serde(default)]
138    pub api_key_env: Option<String>,
139    pub use_responses_api: bool,
140    pub strict_tools: bool,
141}
142
143fn default_codex_model() -> String {
144    "gpt-5.3-codex".to_string()
145}
146
147/// OpenRouter provider configuration.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct OpenRouterConfig {
150    pub model: String,
151    #[serde(default)]
152    pub api_key_env: Option<String>,
153    pub route_strategy: String,
154}
155
156/// Ollama (local model) configuration.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct OllamaConfig {
159    pub base_url: String,
160    pub model: String,
161}
162
163/// Gemini provider configuration.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct GeminiConfig {
166    pub model: String,
167    #[serde(default)]
168    pub api_key_env: Option<String>,
169    #[serde(default)]
170    pub base_url: Option<String>,
171}
172
173impl Default for GeminiConfig {
174    fn default() -> Self {
175        Self {
176            model: "gemini-1.5-pro".to_string(),
177            api_key_env: Some("GEMINI_API_KEY".to_string()),
178            base_url: None,
179        }
180    }
181}
182
183/// Governance policy for optional external execution backends.
184#[derive(Debug, Clone, Serialize, Deserialize, Default)]
185pub struct ExternalBackendsConfig {
186    #[serde(default = "default_allowed_external_backends")]
187    pub allowed_backends: Vec<String>,
188    #[serde(default = "default_true")]
189    pub allow_local_cli_wrappers: bool,
190    #[serde(default)]
191    pub allow_cloud_agent_execution: bool,
192    #[serde(default = "default_external_backend_audit_log_path")]
193    pub audit_log_path: String,
194    #[serde(default = "default_external_backend_env_allowlist")]
195    pub command_env_allowlist: Vec<String>,
196}
197
198fn default_allowed_external_backends() -> Vec<String> {
199    vec!["agent_browser_cli".to_string()]
200}
201
202fn default_external_backend_audit_log_path() -> String {
203    ".claw/control/external-backends-audit.jsonl".to_string()
204}
205
206fn default_external_backend_env_allowlist() -> Vec<String> {
207    vec![
208        "PATH".to_string(),
209        "HOME".to_string(),
210        "USER".to_string(),
211        "USERNAME".to_string(),
212        "TMPDIR".to_string(),
213        "TMP".to_string(),
214        "TEMP".to_string(),
215        "LANG".to_string(),
216        "LC_ALL".to_string(),
217        "SSL_CERT_FILE".to_string(),
218        "SSL_CERT_DIR".to_string(),
219        "XDG_RUNTIME_DIR".to_string(),
220        "XDG_CACHE_HOME".to_string(),
221        "XDG_CONFIG_HOME".to_string(),
222    ]
223}
224
225/// Memory system configuration.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct MemoryConfig {
228    pub core_memory_max_tokens: usize,
229    pub core_memory_max_entries: usize,
230    pub embedding_concurrency: usize,
231    pub dedupe_cosine_threshold: f32,
232    pub decay_half_life_days: f64,
233    pub ttl: MemoryTtlConfig,
234    pub consolidation: ConsolidationConfig,
235}
236
237/// Memory TTL (time-to-live) configuration.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct MemoryTtlConfig {
240    /// Days until episodic memories expire (0 = never).
241    pub episodic_days: u64,
242    /// Days until semantic memories expire (0 = never).
243    pub semantic_days: u64,
244    /// Days until procedural memories expire (0 = never).
245    pub procedural_days: u64,
246}
247
248/// Memory consolidation configuration.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ConsolidationConfig {
251    pub enabled: bool,
252    pub threshold_entries: usize,
253    pub schedule_interval_hours: u64,
254}
255
256/// Scheduler configuration.
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct SchedulerConfig {
259    pub poll_interval_ms: u64,
260    pub lease_duration_secs: u64,
261    pub max_retries: u32,
262    pub base_retry_delay_secs: u64,
263    pub max_retry_delay_secs: u64,
264}
265
266/// Security configuration.
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct SecurityConfig {
269    pub require_auth: bool,
270    pub origin_validation: bool,
271    #[serde(default)]
272    pub control_api_token_env: Option<String>,
273    #[serde(default)]
274    pub trusted_proxy_token_env: Option<String>,
275    pub prompt_injection_defense: bool,
276    pub skill_signature_required: bool,
277    pub skill_verifying_key: Option<String>,
278}
279
280/// Python sidecar configuration.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct SidecarConfig {
283    pub grpc_port: u16,
284    pub python_path: String,
285    #[serde(default)]
286    pub role: SidecarRole,
287    pub auto_start: bool,
288    pub restart_on_crash: bool,
289}
290
291impl SidecarConfig {
292    pub fn supports_compat_dispatch(&self) -> bool {
293        matches!(self.role, SidecarRole::Compatibility)
294    }
295
296    pub fn supports_experimental_lane(&self) -> bool {
297        matches!(self.role, SidecarRole::Experimental)
298    }
299
300    pub fn is_disabled(&self) -> bool {
301        matches!(self.role, SidecarRole::Disabled)
302    }
303}
304
305/// The allowed role of the optional Python sidecar.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
307#[serde(rename_all = "snake_case")]
308pub enum SidecarRole {
309    /// Optional bounded compatibility bridge for legacy workflow execution only.
310    #[default]
311    Compatibility,
312    /// Experimental LangGraph lane for prototyping and evaluation only.
313    Experimental,
314    /// Fully disabled; the sidecar should not be started or used for dispatch.
315    Disabled,
316}
317
318/// Observability configuration.
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ObservabilityConfig {
321    pub langsmith_enabled: bool,
322    pub tracing_enabled: bool,
323    pub metrics_enabled: bool,
324    pub metrics_port: u16,
325}
326
327/// Voice runtime configuration.
328#[derive(Debug, Clone, Serialize, Deserialize, Default)]
329pub struct VoiceConfig {
330    #[serde(default)]
331    pub enabled: bool,
332    #[serde(default)]
333    pub wake_word: VoiceWakeWordConfig,
334    #[serde(default)]
335    pub stt: VoiceSttRuntimeConfig,
336    #[serde(default)]
337    pub tts: VoiceTtsRuntimeConfig,
338    #[serde(default)]
339    pub talk_mode: VoiceTalkModeRuntimeConfig,
340}
341
342/// Wake-word configuration for voice flows.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct VoiceWakeWordConfig {
345    #[serde(default = "default_true")]
346    pub enabled: bool,
347    #[serde(default)]
348    pub model_path: Option<String>,
349    #[serde(default = "default_wake_word_sensitivity")]
350    pub sensitivity: f32,
351}
352
353impl Default for VoiceWakeWordConfig {
354    fn default() -> Self {
355        Self {
356            enabled: true,
357            model_path: None,
358            sensitivity: default_wake_word_sensitivity(),
359        }
360    }
361}
362
363fn default_wake_word_sensitivity() -> f32 {
364    0.7
365}
366
367/// Speech-to-text runtime configuration.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct VoiceSttRuntimeConfig {
370    #[serde(default = "default_voice_stt_provider")]
371    pub provider: String,
372    #[serde(default = "default_voice_stt_model")]
373    pub model: String,
374    #[serde(default = "default_voice_language")]
375    pub language: String,
376    #[serde(default)]
377    pub api_base_url: Option<String>,
378    #[serde(default)]
379    pub api_key_env: Option<String>,
380    #[serde(default)]
381    pub prompt: Option<String>,
382    #[serde(default)]
383    pub transcribe_inbound_notes: bool,
384    #[serde(default)]
385    pub download_dir: Option<String>,
386    #[serde(default = "default_voice_max_audio_bytes")]
387    pub max_audio_bytes: usize,
388    #[serde(default = "default_voice_timeout_secs")]
389    pub timeout_secs: u64,
390}
391
392impl Default for VoiceSttRuntimeConfig {
393    fn default() -> Self {
394        Self {
395            provider: default_voice_stt_provider(),
396            model: default_voice_stt_model(),
397            language: default_voice_language(),
398            api_base_url: None,
399            api_key_env: None,
400            prompt: None,
401            transcribe_inbound_notes: false,
402            download_dir: None,
403            max_audio_bytes: default_voice_max_audio_bytes(),
404            timeout_secs: default_voice_timeout_secs(),
405        }
406    }
407}
408
409fn default_voice_stt_provider() -> String {
410    "openai".to_string()
411}
412
413fn default_voice_stt_model() -> String {
414    "whisper-1".to_string()
415}
416
417fn default_voice_language() -> String {
418    "auto".to_string()
419}
420
421fn default_voice_max_audio_bytes() -> usize {
422    25 * 1024 * 1024
423}
424
425fn default_voice_timeout_secs() -> u64 {
426    60
427}
428
429/// Text-to-speech runtime configuration.
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct VoiceTtsRuntimeConfig {
432    #[serde(default = "default_voice_tts_provider")]
433    pub provider: String,
434    #[serde(default = "default_voice_tts_model")]
435    pub model: String,
436    #[serde(default = "default_voice_tts_voice")]
437    pub voice: String,
438    #[serde(default)]
439    pub api_base_url: Option<String>,
440    #[serde(default)]
441    pub api_key_env: Option<String>,
442}
443
444impl Default for VoiceTtsRuntimeConfig {
445    fn default() -> Self {
446        Self {
447            provider: default_voice_tts_provider(),
448            model: default_voice_tts_model(),
449            voice: default_voice_tts_voice(),
450            api_base_url: None,
451            api_key_env: None,
452        }
453    }
454}
455
456fn default_voice_tts_provider() -> String {
457    "openai".to_string()
458}
459
460fn default_voice_tts_model() -> String {
461    "tts-1".to_string()
462}
463
464fn default_voice_tts_voice() -> String {
465    "alloy".to_string()
466}
467
468/// Continuous talk-mode configuration.
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct VoiceTalkModeRuntimeConfig {
471    #[serde(default = "default_true")]
472    pub enabled: bool,
473    #[serde(default = "default_talk_timeout_secs")]
474    pub timeout_secs: u64,
475}
476
477impl Default for VoiceTalkModeRuntimeConfig {
478    fn default() -> Self {
479        Self {
480            enabled: true,
481            timeout_secs: default_talk_timeout_secs(),
482        }
483    }
484}
485
486fn default_talk_timeout_secs() -> u64 {
487    30
488}
489
490/// Channel runtime resilience configuration.
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct ChannelRuntimeConfig {
493    #[serde(default = "default_true")]
494    pub health_monitor_enabled: bool,
495    #[serde(default = "default_channel_probe_interval_secs")]
496    pub probe_interval_secs: u64,
497    #[serde(default)]
498    pub auto_restart_on_failure: bool,
499    #[serde(default = "default_channel_failure_threshold")]
500    pub failure_threshold: usize,
501}
502
503impl Default for ChannelRuntimeConfig {
504    fn default() -> Self {
505        Self {
506            health_monitor_enabled: true,
507            probe_interval_secs: default_channel_probe_interval_secs(),
508            auto_restart_on_failure: false,
509            failure_threshold: default_channel_failure_threshold(),
510        }
511    }
512}
513
514fn default_channel_probe_interval_secs() -> u64 {
515    300
516}
517
518fn default_channel_failure_threshold() -> usize {
519    3
520}
521
522/// Channel integrations configuration.
523#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct ChannelsConfig {
525    #[serde(default)]
526    pub runtime: ChannelRuntimeConfig,
527    pub telegram: TelegramConfig,
528    pub discord: DiscordConfig,
529    pub slack: SlackConfig,
530    pub whatsapp: WhatsAppConfig,
531    pub teams: TeamsConfig,
532    pub mattermost: MattermostConfig,
533    pub google_chat: GoogleChatConfig,
534    pub google_meet: GoogleMeetConfig,
535    pub gmail_pubsub: GmailPubSubConfig,
536    pub signal: SignalConfig,
537    pub matrix: MatrixConfig,
538    pub x: XConfig,
539    pub twilio: TwilioConfig,
540    pub meta: MetaConfig,
541    pub imessage: IMessageConfig,
542    pub line: LineConfig,
543    pub viber: ViberConfig,
544    pub wechat: WeChatConfig,
545}
546
547/// Mattermost bot / slash-command configuration.
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct MattermostConfig {
550    pub enabled: bool,
551    /// Mattermost server base URL (e.g. "<https://chat.example.com>")
552    pub server_url: String,
553    /// Personal access token or bot token for REST API calls
554    pub bot_token: String,
555    /// Webhook path for incoming slash commands / outgoing webhooks
556    pub webhook_path: String,
557    /// Optional shared token used to validate incoming slash-command / webhook payloads
558    pub webhook_token: Option<String>,
559    /// Optional bot username used for mention detection
560    pub bot_username: Option<String>,
561    /// Allowed Mattermost user IDs or usernames
562    pub allowlist: Vec<String>,
563    /// Allowed Mattermost channel IDs (empty = all)
564    pub allowed_channels: Vec<String>,
565    /// Rate limit for outgoing REST API requests per second
566    pub rate_limit_requests_per_second: u32,
567}
568
569/// Skills/Plugins configuration.
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct SkillsConfig {
572    /// ClawHub registry URL
573    pub registry_url: Option<String>,
574    /// Auto-update skills on startup
575    pub auto_update: Option<bool>,
576    /// Additional skill directories
577    pub skill_dirs: Option<Vec<String>>,
578}
579
580/// Telegram bot configuration.
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct TelegramConfig {
583    pub enabled: bool,
584    pub token: String,
585    #[serde(default)]
586    pub api_base_url: Option<String>,
587    pub mode: TelegramMode,
588    pub webhook_url: Option<String>,
589    pub webhook_port: Option<u16>,
590    pub allowed_users: Vec<String>,
591    pub rate_limit_per_second: u32,
592}
593
594/// Telegram connection mode.
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(rename_all = "snake_case")]
597pub enum TelegramMode {
598    Polling,
599    Webhook,
600}
601
602/// Discord bot configuration.
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct DiscordConfig {
605    pub enabled: bool,
606    pub token: String,
607    pub application_id: String,
608    #[serde(default)]
609    pub interaction_public_key: Option<String>,
610    #[serde(default)]
611    pub api_base_url: Option<String>,
612    #[serde(default)]
613    pub attachment_download_dir: Option<String>,
614    pub rate_limit_requests_per_second: u32,
615    pub allowed_guilds: Vec<String>,
616    pub allowed_channels: Vec<String>,
617    pub dm_enabled: bool,
618}
619
620/// Slack app configuration.
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct SlackConfig {
623    pub enabled: bool,
624    pub token: String,
625    #[serde(default)]
626    pub api_base_url: Option<String>,
627    pub app_token: Option<String>,
628    pub signing_secret: Option<String>,
629    pub mode: SlackMode,
630    pub socket_mode: bool,
631    pub rate_limit_requests_per_second: u32,
632    pub allowed_workspaces: Vec<String>,
633    pub app_home_enabled: bool,
634}
635
636/// Slack connection mode.
637#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "snake_case")]
639pub enum SlackMode {
640    Http,
641    SocketMode,
642}
643
644/// WhatsApp Web configuration.
645#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct WhatsAppConfig {
647    pub enabled: bool,
648    /// Path to store session credentials
649    pub session_path: String,
650    /// Use pairing code instead of QR code
651    pub pairing_mode: bool,
652    /// Allowed phone numbers for DMs (empty = all allowed)
653    pub allowlist: Vec<String>,
654    /// Optional webhook URL for notifications
655    pub webhook_url: Option<String>,
656    /// Path to the Baileys bridge script
657    pub bridge_path: String,
658    /// Rate limit for outgoing messages per second
659    pub rate_limit_per_second: u32,
660    /// Maximum reconnection attempts
661    pub max_reconnect_attempts: u32,
662    /// Initial reconnection delay in seconds (increases with backoff)
663    pub reconnect_delay_secs: u64,
664}
665
666/// Microsoft Teams bot configuration.
667#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct TeamsConfig {
669    pub enabled: bool,
670    /// Microsoft App ID (from Azure Bot registration)
671    pub app_id: String,
672    /// Microsoft App Password (client secret)
673    pub app_password: String,
674    /// Tenant ID for single-tenant apps (None for multi-tenant)
675    pub tenant_id: Option<String>,
676    /// Webhook path for incoming messages
677    pub webhook_path: String,
678    /// List of allowed user emails or AAD object IDs
679    pub allowlist: Vec<String>,
680    /// Group policy for mentions
681    pub group_policy: TeamsGroupPolicy,
682    /// Rate limit for sending messages
683    pub rate_limit_requests_per_second: u32,
684    /// Enable Adaptive Cards support
685    pub adaptive_cards_enabled: bool,
686    /// Optional local download root for inbound Teams attachments
687    #[serde(default)]
688    pub attachment_download_dir: Option<String>,
689}
690
691/// Group policy for Microsoft Teams mentions.
692#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
693#[serde(rename_all = "snake_case")]
694pub enum TeamsGroupPolicy {
695    /// Bot responds only when @mentioned
696    Mention,
697    /// Bot responds to all messages in the channel
698    Open,
699}
700
701/// Google Chat bot configuration.
702#[derive(Debug, Clone, Serialize, Deserialize)]
703pub struct GoogleChatConfig {
704    pub enabled: bool,
705    /// Path to service account JSON key file
706    pub service_account_key: String,
707    /// Google Cloud project ID
708    pub project_id: String,
709    /// Webhook URL for receiving messages (if using HTTP push)
710    pub webhook_url: Option<String>,
711    /// Pub/Sub subscription name (if using Pub/Sub)
712    pub pubsub_subscription: Option<String>,
713    /// List of allowed user emails or Google Workspace user IDs
714    pub allowlist: Vec<String>,
715    /// List of allowed space IDs (empty = all spaces)
716    pub allowed_spaces: Vec<String>,
717    /// Rate limit for sending messages
718    pub rate_limit_requests_per_second: u32,
719    /// Enable Card-based responses
720    pub cards_enabled: bool,
721    /// Optional local download root for inbound Google Chat attachments
722    #[serde(default)]
723    pub attachment_download_dir: Option<String>,
724    /// Response mode for the bot
725    pub response_mode: GoogleChatResponseMode,
726}
727
728/// Response mode for Google Chat bot.
729#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
730#[serde(rename_all = "snake_case")]
731pub enum GoogleChatResponseMode {
732    /// Bot responds only when @mentioned
733    Mention,
734    /// Bot responds to all messages in the space
735    Open,
736    /// Bot only responds to slash commands
737    SlashCommands,
738}
739
740/// Google Meet integration configuration.
741#[derive(Debug, Clone, Serialize, Deserialize)]
742pub struct GoogleMeetConfig {
743    pub enabled: bool,
744    /// Path to service account JSON key file, or `token:<value>`, or `env:VAR`
745    pub service_account_key_path: String,
746    /// Delegated Google Workspace user email for Meet API access
747    pub delegated_user_email: String,
748    /// Webhook path for Google Workspace Events / Pub/Sub push delivery
749    pub webhook_path: String,
750    /// Optional space allowlist (empty = all spaces)
751    pub allowed_spaces: Vec<String>,
752    /// Rate limit for Meet API requests per second
753    pub rate_limit_requests_per_second: u32,
754    /// Optional override for the Google Meet API base URL
755    pub api_base_url: Option<String>,
756    /// Optional override for the Google OAuth token URL
757    pub oauth_token_url: Option<String>,
758    /// Additional OAuth scopes to request beyond the built-in Meet defaults
759    pub additional_scopes: Vec<String>,
760    /// Whether transcript events should hydrate transcript entries eagerly
761    pub hydrate_transcript_events: bool,
762}
763
764/// Gmail Pub/Sub configuration.
765#[derive(Debug, Clone, Serialize, Deserialize)]
766pub struct GmailPubSubConfig {
767    pub enabled: bool,
768    /// Google Cloud project ID
769    pub project_id: String,
770    /// Pub/Sub subscription name
771    pub subscription_name: String,
772    /// Optional fully qualified Pub/Sub topic name for Gmail watch notifications
773    pub topic_name: Option<String>,
774    /// Path to service account JSON key file
775    pub service_account_key_path: String,
776    /// Gmail user email address
777    pub user_email: String,
778    /// Label filters (e.g., ["INBOX", "UNREAD"])
779    pub label_filters: Vec<String>,
780    /// Optional query filter (e.g., "from:github.com")
781    pub query_filter: Option<String>,
782    /// Enable auto-reply functionality
783    pub auto_reply: bool,
784    /// Maximum number of history records to fetch per notification
785    pub max_history_fetch: u32,
786    /// Rate limit for Gmail API requests per second
787    pub rate_limit_requests_per_second: u32,
788    /// Optional override for the Gmail API base URL
789    pub api_base_url: Option<String>,
790    /// Optional override for the Google OAuth token URL
791    pub oauth_token_url: Option<String>,
792}
793
794/// Signal messenger configuration.
795#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct SignalConfig {
797    pub enabled: bool,
798    /// Bot's phone number (E.164 format, e.g., +1234567890)
799    pub phone_number: String,
800    /// Path to signal-cli data directory
801    pub data_dir: PathBuf,
802    /// Allowed phone numbers (empty = allow all)
803    pub allowlist: Vec<String>,
804    /// Allowed group IDs (empty = allow all)
805    pub allowed_groups: Vec<String>,
806    /// Path to signal-cli binary (None = use system PATH)
807    pub signal_cli_path: Option<PathBuf>,
808    /// Use native libsignal-client instead of signal-cli
809    pub use_libsignal: bool,
810    /// Rate limit for outgoing messages per minute
811    pub rate_limit_per_minute: u32,
812    /// Require allowlist for security
813    pub require_allowlist: bool,
814}
815
816/// Matrix protocol configuration.
817#[derive(Debug, Clone, Serialize, Deserialize)]
818pub struct MatrixConfig {
819    pub enabled: bool,
820    /// Matrix homeserver URL (e.g., "<https://matrix.org>")
821    pub homeserver: String,
822    /// Matrix user ID (e.g., "@bot:matrix.org")
823    pub user_id: String,
824    /// Access token for authentication (preferred over password)
825    pub access_token: Option<String>,
826    /// Password for authentication (if access_token not provided)
827    pub password: Option<String>,
828    /// Device ID for the session
829    pub device_id: Option<String>,
830    /// Directory to store Matrix client data (encryption keys, sync tokens)
831    pub data_dir: String,
832    /// List of allowed MXIDs (empty = allow all)
833    pub allowlist: Vec<String>,
834    /// List of allowed room IDs (empty = allow all)
835    pub room_allowlist: Vec<String>,
836    /// Automatically join rooms when invited
837    pub auto_join_rooms: bool,
838    /// Enable end-to-end encryption
839    pub enable_encryption: bool,
840    /// Rate limit for sending messages per second
841    pub rate_limit_per_second: u32,
842}
843
844/// X (Twitter) API v2 configuration.
845#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct XConfig {
847    pub enabled: bool,
848    /// Bearer token for X API v2
849    pub bearer_token: String,
850    /// API key (for user context endpoints)
851    pub api_key: String,
852    /// API secret (for user context endpoints)
853    pub api_secret: String,
854    /// Access token (for user context endpoints)
855    pub access_token: String,
856    /// Access token secret (for user context endpoints)
857    pub access_token_secret: String,
858    /// Bot user ID (numeric string)
859    pub bot_user_id: String,
860    /// List of allowed usernames (without @, empty = all allowed)
861    pub allowlist: Vec<String>,
862    /// Respond to mentions
863    pub respond_to_mentions: bool,
864    /// Respond to DMs
865    pub respond_to_dms: bool,
866    /// Maximum tweet length (default: 280)
867    pub max_tweet_length: usize,
868    /// Polling interval for mentions in seconds
869    pub mention_poll_interval_secs: u64,
870    /// Polling interval for DMs in seconds
871    pub dm_poll_interval_secs: u64,
872    /// Rate limit for sending tweets per minute
873    pub rate_limit_per_minute: u32,
874}
875
876/// Twilio SMS/MMS configuration.
877#[derive(Debug, Clone, Serialize, Deserialize)]
878pub struct TwilioConfig {
879    pub enabled: bool,
880    /// Twilio Account SID
881    pub account_sid: String,
882    /// Twilio Auth Token
883    pub auth_token: String,
884    /// Twilio phone number (E.164 format, e.g., +1234567890)
885    pub phone_number: String,
886    /// Optional webhook URL for receiving messages
887    pub webhook_url: Option<String>,
888    /// List of allowed phone numbers (empty = all allowed)
889    pub allowlist: Vec<String>,
890    /// Maximum message length (default 1600, Twilio's limit)
891    pub max_message_length: usize,
892    /// Enable MMS support
893    pub enable_mms: bool,
894    /// Rate limit for sending messages per second
895    pub rate_limit_per_second: u32,
896}
897
898/// LINE Messaging API configuration.
899#[derive(Debug, Clone, Serialize, Deserialize)]
900pub struct LineConfig {
901    pub enabled: bool,
902    /// LINE Channel Access Token
903    pub channel_access_token: String,
904    /// LINE Channel Secret (for webhook signature verification)
905    pub channel_secret: String,
906    /// Webhook path for receiving events
907    pub webhook_path: String,
908    /// List of allowed user IDs (empty = all allowed)
909    pub allowlist: Vec<String>,
910    /// Rate limit for sending messages per second (default: 1000)
911    pub rate_limit_per_second: u32,
912    /// Enable rich menu support
913    pub enable_rich_menu: bool,
914    /// Enable quick replies
915    pub enable_quick_replies: bool,
916}
917
918/// Viber Bot API configuration.
919#[derive(Debug, Clone, Serialize, Deserialize)]
920pub struct ViberConfig {
921    pub enabled: bool,
922    /// Viber Bot Authentication Token
923    pub auth_token: String,
924    /// Webhook URL for receiving callbacks (optional)
925    pub webhook_url: Option<String>,
926    /// Webhook path for receiving callbacks
927    pub webhook_path: String,
928    /// List of allowed user IDs (empty = all allowed)
929    pub allowlist: Vec<String>,
930    /// Rate limit for sending messages per minute (default: 300)
931    pub rate_limit_per_minute: u32,
932    /// Allow broadcast messages (admin only)
933    pub allow_broadcast: bool,
934    /// Welcome message for new conversations
935    pub welcome_message: Option<String>,
936    /// Enable keyboard support
937    pub enable_keyboards: bool,
938}
939
940/// WeChat configuration (supports both Work and Official Accounts).
941#[derive(Debug, Clone, Serialize, Deserialize)]
942pub struct WeChatConfig {
943    pub enabled: bool,
944    /// App type: "work" for WeChat Work, "official_account" for Official Accounts
945    pub app_type: String,
946    // WeChat Work fields
947    /// WeChat Work Corp ID
948    pub corp_id: String,
949    /// WeChat Work Corp Secret
950    pub corp_secret: String,
951    /// WeChat Work Agent ID
952    pub agent_id: String,
953    // Official Account fields
954    /// WeChat Official Account App ID
955    pub app_id: String,
956    /// WeChat Official Account App Secret
957    pub app_secret: String,
958    /// Token for webhook signature verification (Official Accounts)
959    pub token: String,
960    /// Encoding AES key for message encryption (optional)
961    pub encoding_aes_key: Option<String>,
962    /// Webhook path for receiving messages
963    pub webhook_path: String,
964    /// List of allowed user IDs (empty = all allowed)
965    pub allowlist: Vec<String>,
966    /// Rate limit for API requests per second (default: 20)
967    pub rate_limit_per_second: u32,
968    /// Enable message encryption
969    pub enable_encryption: bool,
970}
971
972/// Meta (Messenger & Instagram) configuration.
973#[derive(Debug, Clone, Serialize, Deserialize)]
974pub struct MetaConfig {
975    pub enabled: bool,
976    /// Meta App ID
977    pub app_id: String,
978    /// Meta App Secret
979    pub app_secret: String,
980    /// Page Access Token for Messenger
981    pub page_access_token: String,
982    /// Webhook verify token
983    pub verify_token: String,
984    /// Webhook path for receiving messages
985    pub webhook_path: String,
986    /// Facebook Page ID
987    pub page_id: String,
988    /// Instagram Account ID (optional, for Instagram Direct)
989    pub instagram_account_id: Option<String>,
990    /// List of allowed sender IDs (empty = all allowed)
991    pub allowlist: Vec<String>,
992    /// Respond to Messenger messages
993    pub respond_to_messenger: bool,
994    /// Respond to Instagram Direct messages
995    pub respond_to_instagram: bool,
996    /// Show typing indicator while processing
997    pub show_typing_indicator: bool,
998    /// Rate limit for sending messages per second
999    pub rate_limit_per_second: u32,
1000}
1001
1002/// iMessage configuration.
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub struct IMessageConfig {
1005    pub enabled: bool,
1006    /// Bridge mode for iMessage integration
1007    pub bridge_mode: IMessageBridgeMode,
1008    /// List of allowed phone numbers or Apple IDs (empty = all allowed)
1009    pub allowlist: Vec<String>,
1010    /// Enable tapback/reaction support
1011    pub enable_tapbacks: bool,
1012    /// Enable typing indicator support
1013    pub enable_typing_indicator: bool,
1014}
1015
1016/// Bridge mode for iMessage integration.
1017#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1018#[serde(rename_all = "snake_case", tag = "mode")]
1019pub enum IMessageBridgeMode {
1020    /// BlueBubbles server (works remotely)
1021    #[serde(rename = "bluebubbles", alias = "blue_bubbles")]
1022    BlueBubbles {
1023        /// BlueBubbles server URL
1024        server_url: String,
1025        /// BlueBubbles API password
1026        password: String,
1027    },
1028    /// Direct macOS AppleScript (local only, requires macOS)
1029    #[default]
1030    #[serde(rename = "macos_direct", alias = "mac_o_s_direct")]
1031    MacOSDirect,
1032    /// macOS Messages.app private API (advanced)
1033    PrivateApi,
1034}
1035
1036impl AppConfig {
1037    /// Load configuration from default.toml, then overlay environment variables.
1038    ///
1039    /// Environment variables use the prefix `OPENRUSTCLAW_` with `__` as separator.
1040    /// Example: `OPENRUSTCLAW_GATEWAY__PORT=8080`
1041    pub fn load() -> Result<Self, config::ConfigError> {
1042        let config = config::Config::builder()
1043            .add_source(config::File::with_name("config/default").required(false))
1044            .add_source(
1045                config::Environment::with_prefix("OPENRUSTCLAW")
1046                    .separator("__")
1047                    .try_parsing(true),
1048            )
1049            .build()?;
1050
1051        config.try_deserialize()
1052    }
1053
1054    /// Load from a specific config file path.
1055    pub fn load_from(path: &str) -> Result<Self, config::ConfigError> {
1056        let config = config::Config::builder()
1057            .add_source(config::File::with_name(path))
1058            .add_source(
1059                config::Environment::with_prefix("OPENRUSTCLAW")
1060                    .separator("__")
1061                    .try_parsing(true),
1062            )
1063            .build()?;
1064
1065        config.try_deserialize()
1066    }
1067}
1068
1069impl Default for AppConfig {
1070    fn default() -> Self {
1071        Self {
1072            gateway: GatewayConfig {
1073                network_mode: default_gateway_network_mode(),
1074                host: "127.0.0.1".to_string(),
1075                port: 18789,
1076                allowed_origins: vec![
1077                    "http://localhost:3000".to_string(),
1078                    "http://127.0.0.1:3000".to_string(),
1079                ],
1080            },
1081            database: DatabaseConfig {
1082                url: "sqlite://data/openrustclaw.db".to_string(),
1083                wal_mode: true,
1084                max_connections: 10,
1085            },
1086            providers: ProvidersConfig {
1087                default_provider: "anthropic".to_string(),
1088                fallback_chain: vec![
1089                    "anthropic".to_string(),
1090                    "openai".to_string(),
1091                    "openrouter".to_string(),
1092                ],
1093                control_plane_provider: Some("openrouter".to_string()),
1094                control_plane_fallback_chain: vec!["ollama".to_string(), "anthropic".to_string()],
1095                anthropic: AnthropicConfig {
1096                    model: "claude-sonnet-4-20250514".to_string(),
1097                    api_key_env: None,
1098                    api_version: "2023-06-01".to_string(),
1099                    strict_tools: true,
1100                    streaming_tool_deltas: true,
1101                },
1102                openai: OpenAiConfig {
1103                    model: "gpt-4o".to_string(),
1104                    codex_model: default_codex_model(),
1105                    api_key_env: None,
1106                    use_responses_api: true,
1107                    strict_tools: true,
1108                },
1109                openrouter: OpenRouterConfig {
1110                    model: "anthropic/claude-sonnet-4".to_string(),
1111                    api_key_env: None,
1112                    route_strategy: "quality".to_string(),
1113                },
1114                ollama: OllamaConfig {
1115                    base_url: "http://localhost:11434".to_string(),
1116                    model: "llama3.1".to_string(),
1117                },
1118                gemini: GeminiConfig::default(),
1119            },
1120            external_backends: ExternalBackendsConfig {
1121                allowed_backends: default_allowed_external_backends(),
1122                allow_local_cli_wrappers: true,
1123                allow_cloud_agent_execution: false,
1124                audit_log_path: default_external_backend_audit_log_path(),
1125                command_env_allowlist: default_external_backend_env_allowlist(),
1126            },
1127            memory: MemoryConfig {
1128                core_memory_max_tokens: 500,
1129                core_memory_max_entries: 20,
1130                embedding_concurrency: 4,
1131                dedupe_cosine_threshold: 0.92,
1132                decay_half_life_days: 30.0,
1133                ttl: MemoryTtlConfig {
1134                    episodic_days: 90,
1135                    semantic_days: 0,
1136                    procedural_days: 0,
1137                },
1138                consolidation: ConsolidationConfig {
1139                    enabled: true,
1140                    threshold_entries: 1000,
1141                    schedule_interval_hours: 24,
1142                },
1143            },
1144            session_routing: SessionRoutingConfig {
1145                direct_strategy: "shared_main".to_string(),
1146                group_strategy: "isolated".to_string(),
1147                thread_overrides_channel: true,
1148                pairing_approval_required: false,
1149                default_group_activation: "mention".to_string(),
1150                default_send_mode: "blocks".to_string(),
1151                default_chunk_chars: 1600,
1152                default_chunk_delay_ms: 250,
1153            },
1154            scheduler: SchedulerConfig {
1155                poll_interval_ms: 1000,
1156                lease_duration_secs: 60,
1157                max_retries: 3,
1158                base_retry_delay_secs: 5,
1159                max_retry_delay_secs: 300,
1160            },
1161            security: SecurityConfig {
1162                require_auth: true,
1163                origin_validation: true,
1164                control_api_token_env: None,
1165                trusted_proxy_token_env: None,
1166                prompt_injection_defense: true,
1167                skill_signature_required: false,
1168                skill_verifying_key: None,
1169            },
1170            sidecar: SidecarConfig {
1171                grpc_port: 50051,
1172                python_path: "python3".to_string(),
1173                role: SidecarRole::Compatibility,
1174                auto_start: false,
1175                restart_on_crash: true,
1176            },
1177            observability: ObservabilityConfig {
1178                langsmith_enabled: false,
1179                tracing_enabled: true,
1180                metrics_enabled: true,
1181                metrics_port: 9090,
1182            },
1183            voice: VoiceConfig::default(),
1184            channels: ChannelsConfig {
1185                runtime: ChannelRuntimeConfig::default(),
1186                telegram: TelegramConfig {
1187                    enabled: false,
1188                    token: String::new(),
1189                    api_base_url: None,
1190                    mode: TelegramMode::Polling,
1191                    webhook_url: None,
1192                    webhook_port: None,
1193                    allowed_users: Vec::new(),
1194                    rate_limit_per_second: 30,
1195                },
1196                discord: DiscordConfig {
1197                    enabled: false,
1198                    token: String::new(),
1199                    application_id: String::new(),
1200                    interaction_public_key: None,
1201                    api_base_url: None,
1202                    attachment_download_dir: None,
1203                    rate_limit_requests_per_second: 5,
1204                    allowed_guilds: Vec::new(),
1205                    allowed_channels: Vec::new(),
1206                    dm_enabled: true,
1207                },
1208                slack: SlackConfig {
1209                    enabled: false,
1210                    token: String::new(),
1211                    api_base_url: None,
1212                    app_token: None,
1213                    signing_secret: None,
1214                    mode: SlackMode::SocketMode,
1215                    socket_mode: true,
1216                    rate_limit_requests_per_second: 10,
1217                    allowed_workspaces: Vec::new(),
1218                    app_home_enabled: true,
1219                },
1220                whatsapp: WhatsAppConfig {
1221                    enabled: false,
1222                    session_path: "./data/whatsapp-session".to_string(),
1223                    pairing_mode: false,
1224                    allowlist: Vec::new(),
1225                    webhook_url: None,
1226                    bridge_path: "./crates/channels/baileys-bridge/index.js".to_string(),
1227                    rate_limit_per_second: 10,
1228                    max_reconnect_attempts: 10,
1229                    reconnect_delay_secs: 5,
1230                },
1231                teams: TeamsConfig {
1232                    enabled: false,
1233                    app_id: String::new(),
1234                    app_password: String::new(),
1235                    tenant_id: None,
1236                    webhook_path: "/webhooks/teams".to_string(),
1237                    allowlist: Vec::new(),
1238                    group_policy: TeamsGroupPolicy::Mention,
1239                    rate_limit_requests_per_second: 10,
1240                    adaptive_cards_enabled: true,
1241                    attachment_download_dir: None,
1242                },
1243                mattermost: MattermostConfig {
1244                    enabled: false,
1245                    server_url: String::new(),
1246                    bot_token: String::new(),
1247                    webhook_path: "/webhooks/mattermost".to_string(),
1248                    webhook_token: None,
1249                    bot_username: None,
1250                    allowlist: Vec::new(),
1251                    allowed_channels: Vec::new(),
1252                    rate_limit_requests_per_second: 10,
1253                },
1254                google_chat: GoogleChatConfig {
1255                    enabled: false,
1256                    service_account_key: String::new(),
1257                    project_id: String::new(),
1258                    webhook_url: None,
1259                    pubsub_subscription: None,
1260                    allowlist: Vec::new(),
1261                    allowed_spaces: Vec::new(),
1262                    rate_limit_requests_per_second: 10,
1263                    cards_enabled: true,
1264                    attachment_download_dir: None,
1265                    response_mode: GoogleChatResponseMode::Mention,
1266                },
1267                google_meet: GoogleMeetConfig {
1268                    enabled: false,
1269                    service_account_key_path: String::new(),
1270                    delegated_user_email: String::new(),
1271                    webhook_path: "/webhooks/google-meet/events".to_string(),
1272                    allowed_spaces: Vec::new(),
1273                    rate_limit_requests_per_second: 5,
1274                    api_base_url: None,
1275                    oauth_token_url: None,
1276                    additional_scopes: Vec::new(),
1277                    hydrate_transcript_events: true,
1278                },
1279                gmail_pubsub: GmailPubSubConfig {
1280                    enabled: false,
1281                    project_id: String::new(),
1282                    subscription_name: String::new(),
1283                    topic_name: None,
1284                    service_account_key_path: String::new(),
1285                    user_email: String::new(),
1286                    label_filters: vec!["INBOX".to_string(), "UNREAD".to_string()],
1287                    query_filter: None,
1288                    auto_reply: false,
1289                    max_history_fetch: 100,
1290                    rate_limit_requests_per_second: 10,
1291                    api_base_url: None,
1292                    oauth_token_url: None,
1293                },
1294                signal: SignalConfig {
1295                    enabled: false,
1296                    phone_number: String::new(),
1297                    data_dir: std::path::PathBuf::from("./data/signal"),
1298                    allowlist: Vec::new(),
1299                    allowed_groups: Vec::new(),
1300                    signal_cli_path: None,
1301                    use_libsignal: false,
1302                    rate_limit_per_minute: 20,
1303                    require_allowlist: true,
1304                },
1305                matrix: MatrixConfig {
1306                    enabled: false,
1307                    homeserver: String::new(),
1308                    user_id: String::new(),
1309                    access_token: None,
1310                    password: None,
1311                    device_id: None,
1312                    data_dir: "./data/matrix".to_string(),
1313                    allowlist: Vec::new(),
1314                    room_allowlist: Vec::new(),
1315                    auto_join_rooms: true,
1316                    enable_encryption: true,
1317                    rate_limit_per_second: 10,
1318                },
1319                x: XConfig {
1320                    enabled: false,
1321                    bearer_token: String::new(),
1322                    api_key: String::new(),
1323                    api_secret: String::new(),
1324                    access_token: String::new(),
1325                    access_token_secret: String::new(),
1326                    bot_user_id: String::new(),
1327                    allowlist: Vec::new(),
1328                    respond_to_mentions: true,
1329                    respond_to_dms: true,
1330                    max_tweet_length: 280,
1331                    mention_poll_interval_secs: 60,
1332                    dm_poll_interval_secs: 120,
1333                    rate_limit_per_minute: 10,
1334                },
1335                twilio: TwilioConfig {
1336                    enabled: false,
1337                    account_sid: String::new(),
1338                    auth_token: String::new(),
1339                    phone_number: String::new(),
1340                    webhook_url: None,
1341                    allowlist: Vec::new(),
1342                    max_message_length: 1600,
1343                    enable_mms: true,
1344                    rate_limit_per_second: 10,
1345                },
1346                meta: MetaConfig {
1347                    enabled: false,
1348                    app_id: String::new(),
1349                    app_secret: String::new(),
1350                    page_access_token: String::new(),
1351                    verify_token: String::new(),
1352                    webhook_path: "/webhooks/meta".to_string(),
1353                    page_id: String::new(),
1354                    instagram_account_id: None,
1355                    allowlist: Vec::new(),
1356                    respond_to_messenger: true,
1357                    respond_to_instagram: true,
1358                    show_typing_indicator: true,
1359                    rate_limit_per_second: 10,
1360                },
1361                imessage: IMessageConfig {
1362                    enabled: false,
1363                    bridge_mode: IMessageBridgeMode::MacOSDirect,
1364                    allowlist: Vec::new(),
1365                    enable_tapbacks: true,
1366                    enable_typing_indicator: false,
1367                },
1368                line: LineConfig {
1369                    enabled: false,
1370                    channel_access_token: String::new(),
1371                    channel_secret: String::new(),
1372                    webhook_path: "/webhook/line".to_string(),
1373                    allowlist: Vec::new(),
1374                    rate_limit_per_second: 1000,
1375                    enable_rich_menu: true,
1376                    enable_quick_replies: true,
1377                },
1378                viber: ViberConfig {
1379                    enabled: false,
1380                    auth_token: String::new(),
1381                    webhook_url: None,
1382                    webhook_path: "/webhook/viber".to_string(),
1383                    allowlist: Vec::new(),
1384                    rate_limit_per_minute: 300,
1385                    allow_broadcast: false,
1386                    welcome_message: None,
1387                    enable_keyboards: true,
1388                },
1389                wechat: WeChatConfig {
1390                    enabled: false,
1391                    app_type: "work".to_string(),
1392                    corp_id: String::new(),
1393                    corp_secret: String::new(),
1394                    agent_id: String::new(),
1395                    app_id: String::new(),
1396                    app_secret: String::new(),
1397                    token: String::new(),
1398                    encoding_aes_key: None,
1399                    webhook_path: "/webhook/wechat".to_string(),
1400                    allowlist: Vec::new(),
1401                    rate_limit_per_second: 20,
1402                    enable_encryption: false,
1403                },
1404            },
1405            skills: None,
1406        }
1407    }
1408}