1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct OllamaConfig {
159 pub base_url: String,
160 pub model: String,
161}
162
163#[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#[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![
200 "agent_browser_cli".to_string(),
201 "claude_code".to_string(),
202 "codex".to_string(),
203 "gemini_cli".to_string(),
204 ]
205}
206
207fn default_external_backend_audit_log_path() -> String {
208 ".claw/control/external-backends-audit.jsonl".to_string()
209}
210
211fn default_external_backend_env_allowlist() -> Vec<String> {
212 vec![
213 "PATH".to_string(),
214 "HOME".to_string(),
215 "USER".to_string(),
216 "USERNAME".to_string(),
217 "TMPDIR".to_string(),
218 "TMP".to_string(),
219 "TEMP".to_string(),
220 "LANG".to_string(),
221 "LC_ALL".to_string(),
222 "SSL_CERT_FILE".to_string(),
223 "SSL_CERT_DIR".to_string(),
224 "XDG_RUNTIME_DIR".to_string(),
225 "XDG_CACHE_HOME".to_string(),
226 "XDG_CONFIG_HOME".to_string(),
227 ]
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct MemoryConfig {
233 pub core_memory_max_tokens: usize,
234 pub core_memory_max_entries: usize,
235 pub embedding_concurrency: usize,
236 pub dedupe_cosine_threshold: f32,
237 pub decay_half_life_days: f64,
238 pub ttl: MemoryTtlConfig,
239 pub consolidation: ConsolidationConfig,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct MemoryTtlConfig {
245 pub episodic_days: u64,
247 pub semantic_days: u64,
249 pub procedural_days: u64,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ConsolidationConfig {
256 pub enabled: bool,
257 pub threshold_entries: usize,
258 pub schedule_interval_hours: u64,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct SchedulerConfig {
264 pub poll_interval_ms: u64,
265 pub lease_duration_secs: u64,
266 pub max_retries: u32,
267 pub base_retry_delay_secs: u64,
268 pub max_retry_delay_secs: u64,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct SecurityConfig {
274 pub require_auth: bool,
275 pub origin_validation: bool,
276 #[serde(default)]
277 pub control_api_token_env: Option<String>,
278 #[serde(default)]
279 pub trusted_proxy_token_env: Option<String>,
280 pub prompt_injection_defense: bool,
281 pub skill_signature_required: bool,
282 pub skill_verifying_key: Option<String>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct SidecarConfig {
288 pub grpc_port: u16,
289 pub python_path: String,
290 #[serde(default)]
291 pub role: SidecarRole,
292 pub auto_start: bool,
293 pub restart_on_crash: bool,
294}
295
296impl SidecarConfig {
297 pub fn supports_compat_dispatch(&self) -> bool {
298 matches!(self.role, SidecarRole::Compatibility)
299 }
300
301 pub fn supports_experimental_lane(&self) -> bool {
302 matches!(self.role, SidecarRole::Experimental)
303 }
304
305 pub fn is_disabled(&self) -> bool {
306 matches!(self.role, SidecarRole::Disabled)
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
312#[serde(rename_all = "snake_case")]
313pub enum SidecarRole {
314 #[default]
316 Compatibility,
317 Experimental,
319 Disabled,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ObservabilityConfig {
326 pub langsmith_enabled: bool,
327 pub tracing_enabled: bool,
328 pub metrics_enabled: bool,
329 pub metrics_port: u16,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, Default)]
334pub struct VoiceConfig {
335 #[serde(default)]
336 pub enabled: bool,
337 #[serde(default)]
338 pub wake_word: VoiceWakeWordConfig,
339 #[serde(default)]
340 pub stt: VoiceSttRuntimeConfig,
341 #[serde(default)]
342 pub tts: VoiceTtsRuntimeConfig,
343 #[serde(default)]
344 pub talk_mode: VoiceTalkModeRuntimeConfig,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct VoiceWakeWordConfig {
350 #[serde(default = "default_true")]
351 pub enabled: bool,
352 #[serde(default)]
353 pub model_path: Option<String>,
354 #[serde(default = "default_wake_word_sensitivity")]
355 pub sensitivity: f32,
356}
357
358impl Default for VoiceWakeWordConfig {
359 fn default() -> Self {
360 Self {
361 enabled: true,
362 model_path: None,
363 sensitivity: default_wake_word_sensitivity(),
364 }
365 }
366}
367
368fn default_wake_word_sensitivity() -> f32 {
369 0.7
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct VoiceSttRuntimeConfig {
375 #[serde(default = "default_voice_stt_provider")]
376 pub provider: String,
377 #[serde(default = "default_voice_stt_model")]
378 pub model: String,
379 #[serde(default = "default_voice_language")]
380 pub language: String,
381 #[serde(default)]
382 pub api_base_url: Option<String>,
383 #[serde(default)]
384 pub api_key_env: Option<String>,
385 #[serde(default)]
386 pub prompt: Option<String>,
387 #[serde(default)]
388 pub transcribe_inbound_notes: bool,
389 #[serde(default)]
390 pub download_dir: Option<String>,
391 #[serde(default = "default_voice_max_audio_bytes")]
392 pub max_audio_bytes: usize,
393 #[serde(default = "default_voice_timeout_secs")]
394 pub timeout_secs: u64,
395}
396
397impl Default for VoiceSttRuntimeConfig {
398 fn default() -> Self {
399 Self {
400 provider: default_voice_stt_provider(),
401 model: default_voice_stt_model(),
402 language: default_voice_language(),
403 api_base_url: None,
404 api_key_env: None,
405 prompt: None,
406 transcribe_inbound_notes: false,
407 download_dir: None,
408 max_audio_bytes: default_voice_max_audio_bytes(),
409 timeout_secs: default_voice_timeout_secs(),
410 }
411 }
412}
413
414fn default_voice_stt_provider() -> String {
415 "openai".to_string()
416}
417
418fn default_voice_stt_model() -> String {
419 "whisper-1".to_string()
420}
421
422fn default_voice_language() -> String {
423 "auto".to_string()
424}
425
426fn default_voice_max_audio_bytes() -> usize {
427 25 * 1024 * 1024
428}
429
430fn default_voice_timeout_secs() -> u64 {
431 60
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct VoiceTtsRuntimeConfig {
437 #[serde(default = "default_voice_tts_provider")]
438 pub provider: String,
439 #[serde(default = "default_voice_tts_model")]
440 pub model: String,
441 #[serde(default = "default_voice_tts_voice")]
442 pub voice: String,
443 #[serde(default)]
444 pub api_base_url: Option<String>,
445 #[serde(default)]
446 pub api_key_env: Option<String>,
447}
448
449impl Default for VoiceTtsRuntimeConfig {
450 fn default() -> Self {
451 Self {
452 provider: default_voice_tts_provider(),
453 model: default_voice_tts_model(),
454 voice: default_voice_tts_voice(),
455 api_base_url: None,
456 api_key_env: None,
457 }
458 }
459}
460
461fn default_voice_tts_provider() -> String {
462 "openai".to_string()
463}
464
465fn default_voice_tts_model() -> String {
466 "tts-1".to_string()
467}
468
469fn default_voice_tts_voice() -> String {
470 "alloy".to_string()
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct VoiceTalkModeRuntimeConfig {
476 #[serde(default = "default_true")]
477 pub enabled: bool,
478 #[serde(default = "default_talk_timeout_secs")]
479 pub timeout_secs: u64,
480}
481
482impl Default for VoiceTalkModeRuntimeConfig {
483 fn default() -> Self {
484 Self {
485 enabled: true,
486 timeout_secs: default_talk_timeout_secs(),
487 }
488 }
489}
490
491fn default_talk_timeout_secs() -> u64 {
492 30
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct ChannelRuntimeConfig {
498 #[serde(default = "default_true")]
499 pub health_monitor_enabled: bool,
500 #[serde(default = "default_channel_probe_interval_secs")]
501 pub probe_interval_secs: u64,
502 #[serde(default)]
503 pub auto_restart_on_failure: bool,
504 #[serde(default = "default_channel_failure_threshold")]
505 pub failure_threshold: usize,
506}
507
508impl Default for ChannelRuntimeConfig {
509 fn default() -> Self {
510 Self {
511 health_monitor_enabled: true,
512 probe_interval_secs: default_channel_probe_interval_secs(),
513 auto_restart_on_failure: false,
514 failure_threshold: default_channel_failure_threshold(),
515 }
516 }
517}
518
519fn default_channel_probe_interval_secs() -> u64 {
520 300
521}
522
523fn default_channel_failure_threshold() -> usize {
524 3
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct ChannelsConfig {
530 #[serde(default)]
531 pub runtime: ChannelRuntimeConfig,
532 pub telegram: TelegramConfig,
533 pub discord: DiscordConfig,
534 pub slack: SlackConfig,
535 pub whatsapp: WhatsAppConfig,
536 pub teams: TeamsConfig,
537 pub mattermost: MattermostConfig,
538 pub google_chat: GoogleChatConfig,
539 pub google_meet: GoogleMeetConfig,
540 pub gmail_pubsub: GmailPubSubConfig,
541 pub signal: SignalConfig,
542 pub matrix: MatrixConfig,
543 pub x: XConfig,
544 pub twilio: TwilioConfig,
545 pub meta: MetaConfig,
546 pub imessage: IMessageConfig,
547 pub line: LineConfig,
548 pub viber: ViberConfig,
549 pub wechat: WeChatConfig,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct MattermostConfig {
555 pub enabled: bool,
556 pub server_url: String,
558 pub bot_token: String,
560 pub webhook_path: String,
562 pub webhook_token: Option<String>,
564 pub bot_username: Option<String>,
566 pub allowlist: Vec<String>,
568 pub allowed_channels: Vec<String>,
570 pub rate_limit_requests_per_second: u32,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct SkillsConfig {
577 pub registry_url: Option<String>,
579 pub auto_update: Option<bool>,
581 pub skill_dirs: Option<Vec<String>>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct TelegramConfig {
588 pub enabled: bool,
589 pub token: String,
590 #[serde(default)]
591 pub api_base_url: Option<String>,
592 pub mode: TelegramMode,
593 pub webhook_url: Option<String>,
594 pub webhook_port: Option<u16>,
595 pub allowed_users: Vec<String>,
596 pub rate_limit_per_second: u32,
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(rename_all = "snake_case")]
602pub enum TelegramMode {
603 Polling,
604 Webhook,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct DiscordConfig {
610 pub enabled: bool,
611 pub token: String,
612 pub application_id: String,
613 #[serde(default)]
614 pub interaction_public_key: Option<String>,
615 #[serde(default)]
616 pub api_base_url: Option<String>,
617 #[serde(default)]
618 pub attachment_download_dir: Option<String>,
619 pub rate_limit_requests_per_second: u32,
620 pub allowed_guilds: Vec<String>,
621 pub allowed_channels: Vec<String>,
622 pub dm_enabled: bool,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct SlackConfig {
628 pub enabled: bool,
629 pub token: String,
630 #[serde(default)]
631 pub api_base_url: Option<String>,
632 pub app_token: Option<String>,
633 pub signing_secret: Option<String>,
634 pub mode: SlackMode,
635 pub socket_mode: bool,
636 pub rate_limit_requests_per_second: u32,
637 pub allowed_workspaces: Vec<String>,
638 pub app_home_enabled: bool,
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(rename_all = "snake_case")]
644pub enum SlackMode {
645 Http,
646 SocketMode,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct WhatsAppConfig {
652 pub enabled: bool,
653 pub session_path: String,
655 pub pairing_mode: bool,
657 pub allowlist: Vec<String>,
659 pub webhook_url: Option<String>,
661 pub bridge_path: String,
663 pub rate_limit_per_second: u32,
665 pub max_reconnect_attempts: u32,
667 pub reconnect_delay_secs: u64,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct TeamsConfig {
674 pub enabled: bool,
675 pub app_id: String,
677 pub app_password: String,
679 pub tenant_id: Option<String>,
681 pub webhook_path: String,
683 pub allowlist: Vec<String>,
685 pub group_policy: TeamsGroupPolicy,
687 pub rate_limit_requests_per_second: u32,
689 pub adaptive_cards_enabled: bool,
691 #[serde(default)]
693 pub attachment_download_dir: Option<String>,
694}
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "snake_case")]
699pub enum TeamsGroupPolicy {
700 Mention,
702 Open,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct GoogleChatConfig {
709 pub enabled: bool,
710 pub service_account_key: String,
712 pub project_id: String,
714 pub webhook_url: Option<String>,
716 pub pubsub_subscription: Option<String>,
718 pub allowlist: Vec<String>,
720 pub allowed_spaces: Vec<String>,
722 pub rate_limit_requests_per_second: u32,
724 pub cards_enabled: bool,
726 #[serde(default)]
728 pub attachment_download_dir: Option<String>,
729 pub response_mode: GoogleChatResponseMode,
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
735#[serde(rename_all = "snake_case")]
736pub enum GoogleChatResponseMode {
737 Mention,
739 Open,
741 SlashCommands,
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct GoogleMeetConfig {
748 pub enabled: bool,
749 pub service_account_key_path: String,
751 pub delegated_user_email: String,
753 pub webhook_path: String,
755 pub allowed_spaces: Vec<String>,
757 pub rate_limit_requests_per_second: u32,
759 pub api_base_url: Option<String>,
761 pub oauth_token_url: Option<String>,
763 pub additional_scopes: Vec<String>,
765 pub hydrate_transcript_events: bool,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct GmailPubSubConfig {
772 pub enabled: bool,
773 pub project_id: String,
775 pub subscription_name: String,
777 pub topic_name: Option<String>,
779 pub service_account_key_path: String,
781 pub user_email: String,
783 pub label_filters: Vec<String>,
785 pub query_filter: Option<String>,
787 pub auto_reply: bool,
789 pub max_history_fetch: u32,
791 pub rate_limit_requests_per_second: u32,
793 pub api_base_url: Option<String>,
795 pub oauth_token_url: Option<String>,
797}
798
799#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct SignalConfig {
802 pub enabled: bool,
803 pub phone_number: String,
805 pub data_dir: PathBuf,
807 pub allowlist: Vec<String>,
809 pub allowed_groups: Vec<String>,
811 pub signal_cli_path: Option<PathBuf>,
813 pub use_libsignal: bool,
815 pub rate_limit_per_minute: u32,
817 pub require_allowlist: bool,
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct MatrixConfig {
824 pub enabled: bool,
825 pub homeserver: String,
827 pub user_id: String,
829 pub access_token: Option<String>,
831 pub password: Option<String>,
833 pub device_id: Option<String>,
835 pub data_dir: String,
837 pub allowlist: Vec<String>,
839 pub room_allowlist: Vec<String>,
841 pub auto_join_rooms: bool,
843 pub enable_encryption: bool,
845 pub rate_limit_per_second: u32,
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
851pub struct XConfig {
852 pub enabled: bool,
853 pub bearer_token: String,
855 pub api_key: String,
857 pub api_secret: String,
859 pub access_token: String,
861 pub access_token_secret: String,
863 pub bot_user_id: String,
865 pub allowlist: Vec<String>,
867 pub respond_to_mentions: bool,
869 pub respond_to_dms: bool,
871 pub max_tweet_length: usize,
873 pub mention_poll_interval_secs: u64,
875 pub dm_poll_interval_secs: u64,
877 pub rate_limit_per_minute: u32,
879}
880
881#[derive(Debug, Clone, Serialize, Deserialize)]
883pub struct TwilioConfig {
884 pub enabled: bool,
885 pub account_sid: String,
887 pub auth_token: String,
889 pub phone_number: String,
891 pub webhook_url: Option<String>,
893 pub allowlist: Vec<String>,
895 pub max_message_length: usize,
897 pub enable_mms: bool,
899 pub rate_limit_per_second: u32,
901}
902
903#[derive(Debug, Clone, Serialize, Deserialize)]
905pub struct LineConfig {
906 pub enabled: bool,
907 pub channel_access_token: String,
909 pub channel_secret: String,
911 pub webhook_path: String,
913 pub allowlist: Vec<String>,
915 pub rate_limit_per_second: u32,
917 pub enable_rich_menu: bool,
919 pub enable_quick_replies: bool,
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
925pub struct ViberConfig {
926 pub enabled: bool,
927 pub auth_token: String,
929 pub webhook_url: Option<String>,
931 pub webhook_path: String,
933 pub allowlist: Vec<String>,
935 pub rate_limit_per_minute: u32,
937 pub allow_broadcast: bool,
939 pub welcome_message: Option<String>,
941 pub enable_keyboards: bool,
943}
944
945#[derive(Debug, Clone, Serialize, Deserialize)]
947pub struct WeChatConfig {
948 pub enabled: bool,
949 pub app_type: String,
951 pub corp_id: String,
954 pub corp_secret: String,
956 pub agent_id: String,
958 pub app_id: String,
961 pub app_secret: String,
963 pub token: String,
965 pub encoding_aes_key: Option<String>,
967 pub webhook_path: String,
969 pub allowlist: Vec<String>,
971 pub rate_limit_per_second: u32,
973 pub enable_encryption: bool,
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct MetaConfig {
980 pub enabled: bool,
981 pub app_id: String,
983 pub app_secret: String,
985 pub page_access_token: String,
987 pub verify_token: String,
989 pub webhook_path: String,
991 pub page_id: String,
993 pub instagram_account_id: Option<String>,
995 pub allowlist: Vec<String>,
997 pub respond_to_messenger: bool,
999 pub respond_to_instagram: bool,
1001 pub show_typing_indicator: bool,
1003 pub rate_limit_per_second: u32,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub struct IMessageConfig {
1010 pub enabled: bool,
1011 pub bridge_mode: IMessageBridgeMode,
1013 pub allowlist: Vec<String>,
1015 pub enable_tapbacks: bool,
1017 pub enable_typing_indicator: bool,
1019}
1020
1021#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1023#[serde(rename_all = "snake_case", tag = "mode")]
1024pub enum IMessageBridgeMode {
1025 #[serde(rename = "bluebubbles", alias = "blue_bubbles")]
1027 BlueBubbles {
1028 server_url: String,
1030 password: String,
1032 },
1033 #[default]
1035 #[serde(rename = "macos_direct", alias = "mac_o_s_direct")]
1036 MacOSDirect,
1037 PrivateApi,
1039}
1040
1041impl AppConfig {
1042 pub fn load() -> Result<Self, config::ConfigError> {
1047 let config = config::Config::builder()
1048 .add_source(config::File::with_name("config/default").required(false))
1049 .add_source(
1050 config::Environment::with_prefix("OPENRUSTCLAW")
1051 .separator("__")
1052 .try_parsing(true),
1053 )
1054 .build()?;
1055
1056 config.try_deserialize()
1057 }
1058
1059 pub fn load_from(path: &str) -> Result<Self, config::ConfigError> {
1061 let config = config::Config::builder()
1062 .add_source(config::File::with_name(path))
1063 .add_source(
1064 config::Environment::with_prefix("OPENRUSTCLAW")
1065 .separator("__")
1066 .try_parsing(true),
1067 )
1068 .build()?;
1069
1070 config.try_deserialize()
1071 }
1072}
1073
1074impl Default for AppConfig {
1075 fn default() -> Self {
1076 Self {
1077 gateway: GatewayConfig {
1078 network_mode: default_gateway_network_mode(),
1079 host: "127.0.0.1".to_string(),
1080 port: 18789,
1081 allowed_origins: vec![
1082 "http://localhost:3000".to_string(),
1083 "http://127.0.0.1:3000".to_string(),
1084 ],
1085 },
1086 database: DatabaseConfig {
1087 url: "sqlite://data/openrustclaw.db".to_string(),
1088 wal_mode: true,
1089 max_connections: 10,
1090 },
1091 providers: ProvidersConfig {
1092 default_provider: "anthropic".to_string(),
1093 fallback_chain: vec![
1094 "anthropic".to_string(),
1095 "openai".to_string(),
1096 "openrouter".to_string(),
1097 ],
1098 control_plane_provider: Some("openrouter".to_string()),
1099 control_plane_fallback_chain: vec!["ollama".to_string(), "anthropic".to_string()],
1100 anthropic: AnthropicConfig {
1101 model: "claude-sonnet-4-20250514".to_string(),
1102 api_key_env: None,
1103 api_version: "2023-06-01".to_string(),
1104 strict_tools: true,
1105 streaming_tool_deltas: true,
1106 },
1107 openai: OpenAiConfig {
1108 model: "gpt-4o".to_string(),
1109 codex_model: default_codex_model(),
1110 api_key_env: None,
1111 use_responses_api: true,
1112 strict_tools: true,
1113 },
1114 openrouter: OpenRouterConfig {
1115 model: "anthropic/claude-sonnet-4".to_string(),
1116 api_key_env: None,
1117 route_strategy: "quality".to_string(),
1118 },
1119 ollama: OllamaConfig {
1120 base_url: "http://localhost:11434".to_string(),
1121 model: "llama3.1".to_string(),
1122 },
1123 gemini: GeminiConfig::default(),
1124 },
1125 external_backends: ExternalBackendsConfig {
1126 allowed_backends: default_allowed_external_backends(),
1127 allow_local_cli_wrappers: true,
1128 allow_cloud_agent_execution: false,
1129 audit_log_path: default_external_backend_audit_log_path(),
1130 command_env_allowlist: default_external_backend_env_allowlist(),
1131 },
1132 memory: MemoryConfig {
1133 core_memory_max_tokens: 500,
1134 core_memory_max_entries: 20,
1135 embedding_concurrency: 4,
1136 dedupe_cosine_threshold: 0.92,
1137 decay_half_life_days: 30.0,
1138 ttl: MemoryTtlConfig {
1139 episodic_days: 90,
1140 semantic_days: 0,
1141 procedural_days: 0,
1142 },
1143 consolidation: ConsolidationConfig {
1144 enabled: true,
1145 threshold_entries: 1000,
1146 schedule_interval_hours: 24,
1147 },
1148 },
1149 session_routing: SessionRoutingConfig {
1150 direct_strategy: "shared_main".to_string(),
1151 group_strategy: "isolated".to_string(),
1152 thread_overrides_channel: true,
1153 pairing_approval_required: false,
1154 default_group_activation: "mention".to_string(),
1155 default_send_mode: "blocks".to_string(),
1156 default_chunk_chars: 1600,
1157 default_chunk_delay_ms: 250,
1158 },
1159 scheduler: SchedulerConfig {
1160 poll_interval_ms: 1000,
1161 lease_duration_secs: 60,
1162 max_retries: 3,
1163 base_retry_delay_secs: 5,
1164 max_retry_delay_secs: 300,
1165 },
1166 security: SecurityConfig {
1167 require_auth: true,
1168 origin_validation: true,
1169 control_api_token_env: None,
1170 trusted_proxy_token_env: None,
1171 prompt_injection_defense: true,
1172 skill_signature_required: false,
1173 skill_verifying_key: None,
1174 },
1175 sidecar: SidecarConfig {
1176 grpc_port: 50051,
1177 python_path: "python3".to_string(),
1178 role: SidecarRole::Compatibility,
1179 auto_start: false,
1180 restart_on_crash: true,
1181 },
1182 observability: ObservabilityConfig {
1183 langsmith_enabled: false,
1184 tracing_enabled: true,
1185 metrics_enabled: true,
1186 metrics_port: 9090,
1187 },
1188 voice: VoiceConfig::default(),
1189 channels: ChannelsConfig {
1190 runtime: ChannelRuntimeConfig::default(),
1191 telegram: TelegramConfig {
1192 enabled: false,
1193 token: String::new(),
1194 api_base_url: None,
1195 mode: TelegramMode::Polling,
1196 webhook_url: None,
1197 webhook_port: None,
1198 allowed_users: Vec::new(),
1199 rate_limit_per_second: 30,
1200 },
1201 discord: DiscordConfig {
1202 enabled: false,
1203 token: String::new(),
1204 application_id: String::new(),
1205 interaction_public_key: None,
1206 api_base_url: None,
1207 attachment_download_dir: None,
1208 rate_limit_requests_per_second: 5,
1209 allowed_guilds: Vec::new(),
1210 allowed_channels: Vec::new(),
1211 dm_enabled: true,
1212 },
1213 slack: SlackConfig {
1214 enabled: false,
1215 token: String::new(),
1216 api_base_url: None,
1217 app_token: None,
1218 signing_secret: None,
1219 mode: SlackMode::SocketMode,
1220 socket_mode: true,
1221 rate_limit_requests_per_second: 10,
1222 allowed_workspaces: Vec::new(),
1223 app_home_enabled: true,
1224 },
1225 whatsapp: WhatsAppConfig {
1226 enabled: false,
1227 session_path: "./data/whatsapp-session".to_string(),
1228 pairing_mode: false,
1229 allowlist: Vec::new(),
1230 webhook_url: None,
1231 bridge_path: "./crates/channels/baileys-bridge/index.js".to_string(),
1232 rate_limit_per_second: 10,
1233 max_reconnect_attempts: 10,
1234 reconnect_delay_secs: 5,
1235 },
1236 teams: TeamsConfig {
1237 enabled: false,
1238 app_id: String::new(),
1239 app_password: String::new(),
1240 tenant_id: None,
1241 webhook_path: "/webhooks/teams".to_string(),
1242 allowlist: Vec::new(),
1243 group_policy: TeamsGroupPolicy::Mention,
1244 rate_limit_requests_per_second: 10,
1245 adaptive_cards_enabled: true,
1246 attachment_download_dir: None,
1247 },
1248 mattermost: MattermostConfig {
1249 enabled: false,
1250 server_url: String::new(),
1251 bot_token: String::new(),
1252 webhook_path: "/webhooks/mattermost".to_string(),
1253 webhook_token: None,
1254 bot_username: None,
1255 allowlist: Vec::new(),
1256 allowed_channels: Vec::new(),
1257 rate_limit_requests_per_second: 10,
1258 },
1259 google_chat: GoogleChatConfig {
1260 enabled: false,
1261 service_account_key: String::new(),
1262 project_id: String::new(),
1263 webhook_url: None,
1264 pubsub_subscription: None,
1265 allowlist: Vec::new(),
1266 allowed_spaces: Vec::new(),
1267 rate_limit_requests_per_second: 10,
1268 cards_enabled: true,
1269 attachment_download_dir: None,
1270 response_mode: GoogleChatResponseMode::Mention,
1271 },
1272 google_meet: GoogleMeetConfig {
1273 enabled: false,
1274 service_account_key_path: String::new(),
1275 delegated_user_email: String::new(),
1276 webhook_path: "/webhooks/google-meet/events".to_string(),
1277 allowed_spaces: Vec::new(),
1278 rate_limit_requests_per_second: 5,
1279 api_base_url: None,
1280 oauth_token_url: None,
1281 additional_scopes: Vec::new(),
1282 hydrate_transcript_events: true,
1283 },
1284 gmail_pubsub: GmailPubSubConfig {
1285 enabled: false,
1286 project_id: String::new(),
1287 subscription_name: String::new(),
1288 topic_name: None,
1289 service_account_key_path: String::new(),
1290 user_email: String::new(),
1291 label_filters: vec!["INBOX".to_string(), "UNREAD".to_string()],
1292 query_filter: None,
1293 auto_reply: false,
1294 max_history_fetch: 100,
1295 rate_limit_requests_per_second: 10,
1296 api_base_url: None,
1297 oauth_token_url: None,
1298 },
1299 signal: SignalConfig {
1300 enabled: false,
1301 phone_number: String::new(),
1302 data_dir: std::path::PathBuf::from("./data/signal"),
1303 allowlist: Vec::new(),
1304 allowed_groups: Vec::new(),
1305 signal_cli_path: None,
1306 use_libsignal: false,
1307 rate_limit_per_minute: 20,
1308 require_allowlist: true,
1309 },
1310 matrix: MatrixConfig {
1311 enabled: false,
1312 homeserver: String::new(),
1313 user_id: String::new(),
1314 access_token: None,
1315 password: None,
1316 device_id: None,
1317 data_dir: "./data/matrix".to_string(),
1318 allowlist: Vec::new(),
1319 room_allowlist: Vec::new(),
1320 auto_join_rooms: true,
1321 enable_encryption: true,
1322 rate_limit_per_second: 10,
1323 },
1324 x: XConfig {
1325 enabled: false,
1326 bearer_token: String::new(),
1327 api_key: String::new(),
1328 api_secret: String::new(),
1329 access_token: String::new(),
1330 access_token_secret: String::new(),
1331 bot_user_id: String::new(),
1332 allowlist: Vec::new(),
1333 respond_to_mentions: true,
1334 respond_to_dms: true,
1335 max_tweet_length: 280,
1336 mention_poll_interval_secs: 60,
1337 dm_poll_interval_secs: 120,
1338 rate_limit_per_minute: 10,
1339 },
1340 twilio: TwilioConfig {
1341 enabled: false,
1342 account_sid: String::new(),
1343 auth_token: String::new(),
1344 phone_number: String::new(),
1345 webhook_url: None,
1346 allowlist: Vec::new(),
1347 max_message_length: 1600,
1348 enable_mms: true,
1349 rate_limit_per_second: 10,
1350 },
1351 meta: MetaConfig {
1352 enabled: false,
1353 app_id: String::new(),
1354 app_secret: String::new(),
1355 page_access_token: String::new(),
1356 verify_token: String::new(),
1357 webhook_path: "/webhooks/meta".to_string(),
1358 page_id: String::new(),
1359 instagram_account_id: None,
1360 allowlist: Vec::new(),
1361 respond_to_messenger: true,
1362 respond_to_instagram: true,
1363 show_typing_indicator: true,
1364 rate_limit_per_second: 10,
1365 },
1366 imessage: IMessageConfig {
1367 enabled: false,
1368 bridge_mode: IMessageBridgeMode::MacOSDirect,
1369 allowlist: Vec::new(),
1370 enable_tapbacks: true,
1371 enable_typing_indicator: false,
1372 },
1373 line: LineConfig {
1374 enabled: false,
1375 channel_access_token: String::new(),
1376 channel_secret: String::new(),
1377 webhook_path: "/webhook/line".to_string(),
1378 allowlist: Vec::new(),
1379 rate_limit_per_second: 1000,
1380 enable_rich_menu: true,
1381 enable_quick_replies: true,
1382 },
1383 viber: ViberConfig {
1384 enabled: false,
1385 auth_token: String::new(),
1386 webhook_url: None,
1387 webhook_path: "/webhook/viber".to_string(),
1388 allowlist: Vec::new(),
1389 rate_limit_per_minute: 300,
1390 allow_broadcast: false,
1391 welcome_message: None,
1392 enable_keyboards: true,
1393 },
1394 wechat: WeChatConfig {
1395 enabled: false,
1396 app_type: "work".to_string(),
1397 corp_id: String::new(),
1398 corp_secret: String::new(),
1399 agent_id: String::new(),
1400 app_id: String::new(),
1401 app_secret: String::new(),
1402 token: String::new(),
1403 encoding_aes_key: None,
1404 webhook_path: "/webhook/wechat".to_string(),
1405 allowlist: Vec::new(),
1406 rate_limit_per_second: 20,
1407 enable_encryption: false,
1408 },
1409 },
1410 skills: None,
1411 }
1412 }
1413}