1#![allow(missing_docs)]
2use cron::Schedule;
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11use crate::email::{SmtpProvider, SmtpTls};
12use crate::types::Priority;
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct CronConfig {
17 #[serde(default)]
19 pub enabled: bool,
20 #[serde(default = "default_tick_interval")]
22 pub tick_interval_secs: u64,
23 #[serde(default)]
25 pub jobs: std::collections::HashMap<String, InlineCronJob>,
26}
27
28impl Default for CronConfig {
29 fn default() -> Self {
30 Self {
31 enabled: false,
32 tick_interval_secs: default_tick_interval(),
33 jobs: std::collections::HashMap::new(),
34 }
35 }
36}
37
38fn default_tick_interval() -> u64 {
39 60
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct InlineCronJob {
45 pub schedule: String,
47 pub goal: String,
49 #[serde(default)]
51 pub constraints: Vec<String>,
52 #[serde(default)]
54 pub acceptance_criteria: Vec<String>,
55 #[serde(default = "default_toolchain_inline")]
57 pub toolchain: String,
58 #[serde(default)]
60 pub priority: Priority,
61 #[serde(default = "default_true_inline")]
63 pub enabled: bool,
64}
65
66fn default_toolchain_inline() -> String {
67 "default".into()
68}
69
70fn default_true_inline() -> bool {
71 true
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct MemoryConfig {
77 #[serde(default = "default_true")]
79 pub enabled: bool,
80 #[serde(default = "default_max_recall")]
82 pub max_recall: usize,
83 #[serde(default = "default_true")]
85 pub auto_summarize: bool,
86 #[serde(default = "default_true")]
88 pub capture_compaction: bool,
89 #[serde(default)]
91 pub retention_days: u32,
92 #[serde(default = "default_true")]
94 pub cache_enabled: bool,
95 #[serde(default = "default_cache_ttl")]
97 pub cache_ttl_secs: u64,
98 #[serde(default = "default_cache_max_entries")]
100 pub cache_max_entries: usize,
101 #[serde(default)]
103 pub consolidation: ConsolidationConfig,
104 #[serde(default)]
106 pub sqlite: SqliteMemoryConfig,
107 #[serde(default)]
109 pub embedding: EmbeddingConfig,
110 #[serde(default)]
112 pub learning: LearningConfig,
113 #[serde(default)]
115 pub knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig,
116 #[serde(default)]
118 pub bridge: MemoryBridgeConfig,
119}
120
121fn default_true() -> bool {
122 true
123}
124
125fn default_max_recall() -> usize {
126 10
127}
128
129fn default_cache_ttl() -> u64 {
130 3600 }
132
133fn default_cache_max_entries() -> usize {
134 10000
135}
136
137impl Default for MemoryConfig {
138 fn default() -> Self {
139 Self {
140 enabled: true,
141 max_recall: 10,
142 auto_summarize: true,
143 capture_compaction: true,
144 retention_days: 0,
145 cache_enabled: true,
146 cache_ttl_secs: 3600,
147 cache_max_entries: 10000,
148 consolidation: ConsolidationConfig::default(),
149 sqlite: SqliteMemoryConfig::default(),
150 embedding: EmbeddingConfig::default(),
151 learning: LearningConfig::default(),
152 knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig::default(),
153 bridge: MemoryBridgeConfig::default(),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct SqliteMemoryConfig {
169 #[serde(default = "default_true")]
171 pub enabled: bool,
172 #[serde(default)]
175 pub path: String,
176 #[serde(default = "default_embedding_dim")]
180 pub embedding_dim: usize,
181 #[serde(default = "default_true")]
183 pub wal_mode: bool,
184}
185
186fn default_embedding_dim() -> usize {
187 256
188}
189
190impl Default for SqliteMemoryConfig {
191 fn default() -> Self {
192 Self {
193 enabled: true,
194 path: String::new(),
195 embedding_dim: 256,
196 wal_mode: true,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct EmbeddingConfig {
214 #[serde(default = "default_embedding_provider")]
216 pub provider: String,
217 #[serde(default = "default_embedding_dim")]
221 pub dimension: usize,
222 #[serde(default = "default_model_ttl")]
225 pub model_ttl_secs: u64,
226 #[serde(default)]
229 pub api_endpoint: String,
230 #[serde(default)]
233 pub api_key: String,
234 #[serde(default)]
237 pub api_model: String,
238}
239
240fn default_embedding_provider() -> String {
241 "tfidf".to_string()
244}
245
246fn default_model_ttl() -> u64 {
247 300 }
249
250impl Default for EmbeddingConfig {
251 fn default() -> Self {
252 Self {
253 provider: default_embedding_provider(),
254 dimension: default_embedding_dim(),
255 model_ttl_secs: default_model_ttl(),
256 api_endpoint: String::new(),
257 api_key: String::new(),
258 api_model: String::new(),
259 }
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct LearningConfig {
272 #[serde(default = "default_true")]
274 pub enabled: bool,
275 #[serde(default = "default_sona_mode")]
277 pub sona_mode: String,
278 #[serde(default = "default_distill_interval")]
280 pub distill_interval_hours: u64,
281 #[serde(default = "default_auto_promote_quality")]
283 pub auto_promote_quality: f32,
284 #[serde(default = "default_auto_promote_min_usage")]
286 pub auto_promote_min_usage: u32,
287}
288
289fn default_sona_mode() -> String {
290 "balanced".to_string()
291}
292
293fn default_distill_interval() -> u64 {
294 6
295}
296
297fn default_auto_promote_quality() -> f32 {
298 0.8
299}
300
301fn default_auto_promote_min_usage() -> u32 {
302 3
303}
304
305impl Default for LearningConfig {
306 fn default() -> Self {
307 Self {
308 enabled: true,
309 sona_mode: default_sona_mode(),
310 distill_interval_hours: default_distill_interval(),
311 auto_promote_quality: default_auto_promote_quality(),
312 auto_promote_min_usage: default_auto_promote_min_usage(),
313 }
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct MemoryBridgeConfig {
327 #[serde(default)]
329 pub sync_enabled: bool,
330 #[serde(default = "default_bridge_interval")]
332 pub interval_secs: u64,
333}
334
335fn default_bridge_interval() -> u64 {
336 3600
337}
338
339impl Default for MemoryBridgeConfig {
340 fn default() -> Self {
341 Self {
342 sync_enabled: false,
343 interval_secs: default_bridge_interval(),
344 }
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct ConsolidationConfig {
356 #[serde(default = "default_preset")]
360 pub preset: String,
361
362 #[serde(default = "default_true")]
364 pub dream_enabled: bool,
365 #[serde(default = "default_dream_interval")]
366 pub dream_interval_hours: u64,
367 #[serde(default = "default_dream_min_sessions")]
368 pub dream_min_sessions: u32,
369
370 #[serde(default = "default_hot_max")]
372 pub hot_max_entries: usize,
373 #[serde(default = "default_warm_max")]
374 pub warm_max_entries: usize,
375 #[serde(default = "default_cold_max")]
376 pub cold_max_entries: usize,
377 #[serde(default = "default_hot_token_budget")]
378 pub hot_token_budget: usize,
379
380 #[serde(default = "default_true")]
382 pub decay_enabled: bool,
383 #[serde(default = "default_one")]
384 pub decay_multiplier: f32,
385 #[serde(default = "default_decay_threshold")]
386 pub decay_threshold: f32,
387 #[serde(default = "default_retention_days")]
388 pub retention_days: u32,
389
390 #[serde(default = "default_true")]
392 pub auto_protection: bool,
393 #[serde(default = "default_protection_low_access")]
394 pub protection_low_access: u32,
395 #[serde(default = "default_protection_medium_access")]
396 pub protection_medium_access: u32,
397 #[serde(default = "default_protection_high_access")]
398 pub protection_high_access: u32,
399 #[serde(default = "default_protection_medium_sessions")]
400 pub protection_medium_sessions: u32,
401 #[serde(default = "default_protection_high_sessions")]
402 pub protection_high_sessions: u32,
403
404 #[serde(default = "default_true")]
406 pub auto_classification: bool,
407 #[serde(default = "default_type_promotion_threshold")]
408 pub type_promotion_repetitions: u32,
409
410 #[serde(default = "default_compaction_threshold")]
412 pub compaction_line_threshold: usize,
413 #[serde(default = "default_true")]
414 pub llm_compaction: bool,
415
416 #[serde(default)]
419 pub dream_model: Option<String>,
420
421 #[serde(default = "default_true")]
423 pub protection_demotion_enabled: bool,
424 #[serde(default = "default_demotion_stale_days")]
425 pub protection_demotion_stale_days: u32,
426 #[serde(default = "default_demotion_max_step")]
427 pub protection_demotion_max_step: u32,
428
429 #[serde(default = "default_true")]
431 pub proactive_recall: bool,
432 #[serde(default = "default_proactive_limit")]
433 pub proactive_recall_limit: usize,
434 #[serde(default = "default_proactive_threshold")]
435 pub proactive_recall_threshold: f32,
436}
437
438fn default_dream_interval() -> u64 {
439 24
440}
441fn default_dream_min_sessions() -> u32 {
442 5
443}
444fn default_hot_max() -> usize {
445 50
446}
447fn default_warm_max() -> usize {
448 500
449}
450fn default_cold_max() -> usize {
451 10_000
452}
453fn default_hot_token_budget() -> usize {
454 3_000
455}
456fn default_one() -> f32 {
457 1.0
458}
459fn default_decay_threshold() -> f32 {
460 0.05
461}
462fn default_retention_days() -> u32 {
463 90
464}
465fn default_protection_low_access() -> u32 {
466 2
467}
468fn default_protection_medium_access() -> u32 {
469 3
470}
471fn default_protection_high_access() -> u32 {
472 5
473}
474fn default_protection_medium_sessions() -> u32 {
475 2
476}
477fn default_protection_high_sessions() -> u32 {
478 3
479}
480fn default_type_promotion_threshold() -> u32 {
481 3
482}
483fn default_compaction_threshold() -> usize {
484 200
485}
486fn default_proactive_limit() -> usize {
487 5
488}
489fn default_proactive_threshold() -> f32 {
490 0.6
491}
492fn default_demotion_stale_days() -> u32 {
493 30
494}
495fn default_demotion_max_step() -> u32 {
496 1
497}
498
499fn default_preset() -> String {
500 "balanced".into()
501}
502
503impl Default for ConsolidationConfig {
504 fn default() -> Self {
505 Self {
506 preset: default_preset(),
507 dream_enabled: true,
508 dream_interval_hours: 24,
509 dream_min_sessions: 5,
510 hot_max_entries: 50,
511 warm_max_entries: 500,
512 cold_max_entries: 10_000,
513 hot_token_budget: 3_000,
514 decay_enabled: true,
515 decay_multiplier: 1.0,
516 decay_threshold: 0.05,
517 retention_days: 90,
518 auto_protection: true,
519 protection_low_access: 2,
520 protection_medium_access: 3,
521 protection_high_access: 5,
522 protection_medium_sessions: 2,
523 protection_high_sessions: 3,
524 auto_classification: true,
525 type_promotion_repetitions: 3,
526 compaction_line_threshold: 200,
527 llm_compaction: true,
528 dream_model: None,
529 protection_demotion_enabled: true,
530 protection_demotion_stale_days: 30,
531 protection_demotion_max_step: 1,
532 proactive_recall: true,
533 proactive_recall_limit: 5,
534 proactive_recall_threshold: 0.6,
535 }
536 }
537}
538
539impl ConsolidationConfig {
540 pub fn apply_preset(&mut self) {
544 let resolved = match self.preset.as_str() {
545 "conservative" => Self::conservative(),
546 "aggressive" => Self::aggressive(),
547 "custom" => return,
548 _ => Self::default(), };
550 *self = resolved;
551 }
552
553 fn conservative() -> Self {
555 Self {
556 preset: "conservative".into(),
557 dream_enabled: true,
558 dream_interval_hours: 48,
559 dream_min_sessions: 10,
560 hot_max_entries: 100,
561 warm_max_entries: 1000,
562 cold_max_entries: 50_000,
563 hot_token_budget: 5_000,
564 decay_enabled: true,
565 decay_multiplier: 0.8,
566 decay_threshold: 0.05,
567 retention_days: 365,
568 auto_protection: true,
569 protection_low_access: 3,
570 protection_medium_access: 5,
571 protection_high_access: 10,
572 protection_medium_sessions: 3,
573 protection_high_sessions: 5,
574 auto_classification: true,
575 type_promotion_repetitions: 5,
576 compaction_line_threshold: 300,
577 llm_compaction: true,
578 dream_model: None,
579 protection_demotion_enabled: true,
580 protection_demotion_stale_days: 90,
581 protection_demotion_max_step: 1,
582 proactive_recall: true,
583 proactive_recall_limit: 8,
584 proactive_recall_threshold: 0.5,
585 }
586 }
587
588 fn aggressive() -> Self {
590 Self {
591 preset: "aggressive".into(),
592 dream_enabled: true,
593 dream_interval_hours: 4,
594 dream_min_sessions: 2,
595 hot_max_entries: 20,
596 warm_max_entries: 100,
597 cold_max_entries: 1_000,
598 hot_token_budget: 2_000,
599 decay_enabled: true,
600 decay_multiplier: 1.0,
601 decay_threshold: 0.1,
602 retention_days: 30,
603 auto_protection: true,
604 protection_low_access: 1,
605 protection_medium_access: 2,
606 protection_high_access: 3,
607 protection_medium_sessions: 1,
608 protection_high_sessions: 2,
609 auto_classification: true,
610 type_promotion_repetitions: 2,
611 compaction_line_threshold: 150,
612 llm_compaction: true,
613 dream_model: None,
614 protection_demotion_enabled: true,
615 protection_demotion_stale_days: 14,
616 protection_demotion_max_step: 2,
617 proactive_recall: true,
618 proactive_recall_limit: 3,
619 proactive_recall_threshold: 0.7,
620 }
621 }
622}
623
624#[derive(Debug, Clone, Deserialize, Serialize, Default)]
626pub struct ChannelsConfig {
627 #[serde(default)]
630 pub enabled: Vec<String>,
631
632 #[serde(default)]
634 pub telegram: TelegramChannelConfig,
635}
636
637#[derive(Debug, Clone, Deserialize, Serialize)]
642pub struct SurfacesConfig {
643 #[serde(default = "default_surfaces_enabled")]
646 pub enabled: Vec<String>,
647}
648
649fn default_surfaces_enabled() -> Vec<String> {
650 vec!["web".to_string()]
651}
652
653impl Default for SurfacesConfig {
654 fn default() -> Self {
655 Self {
656 enabled: default_surfaces_enabled(),
657 }
658 }
659}
660
661#[derive(Debug, Clone, Deserialize, Serialize)]
663pub struct TelegramChannelConfig {
664 #[serde(default = "default_telegram_token_env")]
666 pub bot_token_env: String,
667 #[serde(default)]
669 pub allowed_users: Vec<i64>,
670 #[serde(default)]
672 pub session: TelegramSessionConfig,
673}
674
675fn default_telegram_token_env() -> String {
676 "TELEGRAM_BOT_TOKEN".to_string()
677}
678
679impl Default for TelegramChannelConfig {
680 fn default() -> Self {
681 Self {
682 bot_token_env: default_telegram_token_env(),
683 allowed_users: Vec::new(),
684 session: TelegramSessionConfig::default(),
685 }
686 }
687}
688#[derive(Debug, Clone, Serialize, Deserialize, Default)]
692pub struct RoleRoutingConfig {
693 #[serde(default)]
695 pub roles: std::collections::HashMap<String, String>,
696}
697
698#[derive(Debug, Clone, Deserialize, Serialize)]
700#[allow(clippy::derivable_impls)]
701pub struct EngineConfig {
702 #[serde(default)]
705 pub default_model: String,
706 #[serde(default, skip_serializing)]
710 pub api_key: Option<String>,
711 #[serde(default)]
714 pub provider_options: Option<oxi_sdk::ProviderOptions>,
715 #[serde(default)]
719 pub routing_enabled: bool,
720 #[serde(default)]
722 pub prefer_cost_efficient: bool,
723 #[serde(default)]
725 pub fallback_models: Vec<String>,
726 #[serde(default)]
728 pub excluded_models: Vec<String>,
729 #[serde(default)]
733 pub role_routing: RoleRoutingConfig,
734 #[serde(default)]
738 pub quick_ask_model: Option<String>,
739}
740
741#[allow(clippy::derivable_impls)]
742impl Default for EngineConfig {
743 fn default() -> Self {
744 Self {
745 default_model: String::new(),
746 api_key: None,
747 provider_options: None,
748 routing_enabled: false,
749 prefer_cost_efficient: false,
750 fallback_models: Vec::new(),
751 excluded_models: Vec::new(),
752 role_routing: RoleRoutingConfig::default(),
753 quick_ask_model: None,
754 }
755 }
756}
757
758#[derive(Debug, Clone, Deserialize, Serialize)]
760pub struct DaemonConfig {
761 #[serde(default = "default_pid_file")]
763 pub pid_file: String,
764 #[serde(default = "default_daemon_log_dir")]
766 pub log_dir: String,
767}
768
769fn default_pid_file() -> String {
770 dirs::home_dir()
771 .map(|h| format!("{}/.oxios/oxios.pid", h.display()))
772 .unwrap_or_else(|| "./oxios.pid".into())
773}
774
775fn default_daemon_log_dir() -> String {
776 dirs::home_dir()
777 .map(|h| format!("{}/.oxios/logs", h.display()))
778 .unwrap_or_else(|| "./logs".into())
779}
780
781impl Default for DaemonConfig {
782 fn default() -> Self {
783 Self {
784 pid_file: default_pid_file(),
785 log_dir: default_daemon_log_dir(),
786 }
787 }
788}
789
790#[derive(Debug, Clone, Deserialize, Serialize)]
792pub struct SessionConfig {
793 #[serde(default = "default_max_sessions")]
797 pub max_sessions: usize,
798
799 #[serde(default = "default_session_ttl_hours")]
803 pub ttl_hours: u64,
804
805 #[serde(default = "default_true")]
807 pub auto_prune: bool,
808}
809
810fn default_max_sessions() -> usize {
811 100
812}
813
814fn default_session_ttl_hours() -> u64 {
815 168 }
817
818impl Default for SessionConfig {
819 fn default() -> Self {
820 Self {
821 max_sessions: default_max_sessions(),
822 ttl_hours: default_session_ttl_hours(),
823 auto_prune: true,
824 }
825 }
826}
827
828#[derive(Debug, Clone, Deserialize, Serialize)]
832pub struct MountsConfig {
833 #[serde(default = "default_true")]
835 pub auto_promote_enabled: bool,
836 #[serde(default = "default_promote_threshold")]
838 pub auto_promote_threshold: usize,
839 #[serde(default = "default_promote_window_days")]
841 pub auto_promote_window_days: i64,
842 #[serde(default = "default_promote_interval_secs")]
844 pub auto_promote_interval_secs: u64,
845}
846
847fn default_promote_threshold() -> usize {
848 3
849}
850
851fn default_promote_window_days() -> i64 {
852 14
853}
854
855fn default_promote_interval_secs() -> u64 {
856 3600 }
858
859impl Default for MountsConfig {
860 fn default() -> Self {
861 Self {
862 auto_promote_enabled: true,
863 auto_promote_threshold: default_promote_threshold(),
864 auto_promote_window_days: default_promote_window_days(),
865 auto_promote_interval_secs: default_promote_interval_secs(),
866 }
867 }
868}
869
870#[derive(Debug, Clone, Deserialize, Serialize)]
872pub struct TelegramSessionConfig {
873 #[serde(default = "default_telegram_session_rotation_hours")]
876 pub rotation_hours: u64,
877
878 #[serde(default = "default_telegram_session_max_messages")]
881 pub max_messages: usize,
882}
883
884fn default_telegram_session_rotation_hours() -> u64 {
885 2 }
887
888fn default_telegram_session_max_messages() -> usize {
889 0 }
891
892impl Default for TelegramSessionConfig {
893 fn default() -> Self {
894 Self {
895 rotation_hours: default_telegram_session_rotation_hours(),
896 max_messages: default_telegram_session_max_messages(),
897 }
898 }
899}
900
901#[derive(Debug, Clone, Deserialize, Serialize, Default)]
905pub struct SystemAgentItem {
906 #[serde(default)]
908 pub model: String,
909 #[serde(default = "default_true")]
911 pub enabled: bool,
912 #[serde(default)]
914 pub context_limit: Option<u32>,
915 #[serde(default)]
917 pub custom_prompt: Option<String>,
918}
919
920#[derive(Debug, Clone, Deserialize, Serialize, Default)]
923pub struct SystemAgentsConfig {
924 #[serde(default)]
926 pub topic: SystemAgentItem,
927 #[serde(default)]
929 pub generation_topic: SystemAgentItem,
930 #[serde(default)]
932 pub translation: SystemAgentItem,
933 #[serde(default)]
935 pub history_compress: SystemAgentItem,
936 #[serde(default)]
938 pub agent_meta: SystemAgentItem,
939 #[serde(default)]
941 pub follow_up_action: SystemAgentItem,
942 #[serde(default)]
944 pub input_completion: SystemAgentItem,
945 #[serde(default)]
947 pub prompt_rewrite: SystemAgentItem,
948 #[serde(default)]
950 pub memory_analysis: SystemAgentItem,
951 #[serde(default)]
953 pub memory_embedding: SystemAgentItem,
954 #[serde(default)]
956 pub memory_persona_writer: SystemAgentItem,
957}
958
959impl SystemAgentsConfig {
960 pub fn model_for_task(&self, task: &str) -> Option<String> {
962 let item = match task {
963 "topic" => &self.topic,
964 "generation_topic" => &self.generation_topic,
965 "translation" => &self.translation,
966 "history_compress" => &self.history_compress,
967 "agent_meta" => &self.agent_meta,
968 "follow_up_action" => &self.follow_up_action,
969 "input_completion" => &self.input_completion,
970 "prompt_rewrite" => &self.prompt_rewrite,
971 "memory_analysis" => &self.memory_analysis,
972 "memory_embedding" => &self.memory_embedding,
973 "memory_persona_writer" => &self.memory_persona_writer,
974 _ => return None,
975 };
976 if !item.enabled || item.model.is_empty() {
977 None
978 } else {
979 Some(item.model.clone())
980 }
981 }
982
983 pub fn is_enabled(&self, task: &str) -> bool {
985 self.model_for_task(task).is_some()
986 || match task {
987 "topic" => self.topic.enabled,
988 "generation_topic" => self.generation_topic.enabled,
989 "translation" => self.translation.enabled,
990 "history_compress" => self.history_compress.enabled,
991 "agent_meta" => self.agent_meta.enabled,
992 "follow_up_action" => self.follow_up_action.enabled,
993 "input_completion" => self.input_completion.enabled,
994 "prompt_rewrite" => self.prompt_rewrite.enabled,
995 "memory_analysis" => self.memory_analysis.enabled,
996 "memory_embedding" => self.memory_embedding.enabled,
997 "memory_persona_writer" => self.memory_persona_writer.enabled,
998 _ => false,
999 }
1000 }
1001}
1002
1003#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1004pub struct OxiosConfig {
1005 pub kernel: KernelConfig,
1007 #[serde(default)]
1009 pub engine: EngineConfig,
1010 #[serde(default)]
1012 pub daemon: DaemonConfig,
1013 #[serde(default)]
1015 pub gateway: GatewayConfig,
1016 #[serde(default)]
1018 pub orchestrator: OrchestratorConfig,
1019 #[serde(default)]
1021 pub intent: IntentConfig,
1022 #[serde(default)]
1024 pub system_agents: SystemAgentsConfig,
1025 #[serde(default)]
1027 pub context: ContextConfig,
1028 #[serde(default)]
1030 pub security: SecurityConfig,
1031 #[serde(default)]
1033 pub persona: PersonaConfig,
1034 #[serde(default)]
1036 pub memory: MemoryConfig,
1037 #[serde(default)]
1039 pub cron: CronConfig,
1040 #[serde(default)]
1042 pub mcp: McpConfig,
1043 #[serde(default)]
1045 pub git: GitConfig,
1046 #[serde(default)]
1048 pub audit: AuditConfig,
1049 #[serde(default)]
1051 pub budget: BudgetConfig,
1052 #[serde(default)]
1054 pub exec: ExecConfig,
1055 #[serde(default)]
1057 pub resource_monitor: ResourceMonitorConfig,
1058 #[serde(default)]
1060 pub logging: LoggingConfig,
1061 #[serde(default)]
1063 pub channels: ChannelsConfig,
1064 #[serde(default)]
1066 pub surfaces: Option<SurfacesConfig>,
1067 #[serde(default)]
1069 pub browser: BrowserConfig,
1070 #[serde(default)]
1072 pub session: SessionConfig,
1073 #[serde(default)]
1075 pub mounts: MountsConfig,
1076 #[serde(default)]
1078 pub marketplace: MarketplaceConfig,
1079 #[serde(default)]
1081 pub calendar: CalendarConfig,
1082 #[serde(default)]
1084 pub email: EmailConfig,
1085 #[serde(default)]
1087 pub agent_log: AgentLogConfig,
1088 #[serde(default)]
1090 pub token_maxing: crate::token_maxing::TokenMaxingConfig,
1091 #[serde(default)]
1093 pub image_gen: ImageGenConfig,
1094}
1095
1096#[derive(Debug, Clone, Deserialize, Serialize)]
1104pub struct ImageGenConfig {
1105 #[serde(default)]
1107 pub enabled: bool,
1108 #[serde(default = "default_image_gen_provider")]
1110 pub provider: String,
1111 #[serde(default = "default_image_gen_base_url")]
1113 pub base_url: String,
1114 #[serde(default)]
1117 pub default_model: String,
1118 #[serde(default = "default_image_gen_num")]
1120 pub default_num: u8,
1121}
1122
1123impl Default for ImageGenConfig {
1124 fn default() -> Self {
1125 Self {
1126 enabled: false,
1127 provider: default_image_gen_provider(),
1128 base_url: default_image_gen_base_url(),
1129 default_model: String::new(),
1130 default_num: default_image_gen_num(),
1131 }
1132 }
1133}
1134
1135fn default_image_gen_provider() -> String {
1136 "openai".into()
1137}
1138
1139fn default_image_gen_base_url() -> String {
1140 "https://api.openai.com/v1".into()
1141}
1142
1143fn default_image_gen_num() -> u8 {
1144 1
1145}
1146
1147#[derive(Debug, Clone, Deserialize, Serialize)]
1149pub struct KernelConfig {
1150 #[serde(default = "default_workspace")]
1152 pub workspace: String,
1153 #[serde(default = "default_event_bus_capacity")]
1155 pub event_bus_capacity: usize,
1156 #[serde(default = "default_max_agents")]
1158 pub max_agents: usize,
1159}
1160
1161fn default_workspace() -> String {
1162 dirs_home().unwrap_or_else(|| ".".into())
1163}
1164
1165fn dirs_home() -> Option<String> {
1166 dirs::home_dir().map(|h| format!("{}/.oxios/workspace", h.display()))
1167}
1168
1169fn default_event_bus_capacity() -> usize {
1170 256
1171}
1172
1173fn default_max_agents() -> usize {
1174 10
1175}
1176
1177impl Default for KernelConfig {
1178 fn default() -> Self {
1179 Self {
1180 workspace: default_workspace(),
1181 event_bus_capacity: default_event_bus_capacity(),
1182 max_agents: 10,
1183 }
1184 }
1185}
1186
1187#[derive(Debug, Clone, Deserialize, Serialize)]
1189pub struct GatewayConfig {
1190 #[serde(default = "default_gateway_host")]
1192 pub host: String,
1193 #[serde(default = "default_gateway_port")]
1195 pub port: u16,
1196 #[serde(default)]
1206 pub expose_api_docs: bool,
1207 #[serde(default = "default_response_timeout_secs")]
1211 pub response_timeout_secs: u64,
1212 #[serde(default)]
1214 pub reliability: GatewayReliabilityConfig,
1215}
1216
1217#[derive(Debug, Clone, Serialize, Deserialize)]
1219pub struct GatewayReliabilityConfig {
1220 #[serde(default = "default_replay_buffer_size")]
1223 pub replay_buffer_size: usize,
1224 #[serde(default = "default_replay_ttl_secs")]
1226 pub replay_ttl_secs: u64,
1227}
1228
1229impl Default for GatewayReliabilityConfig {
1230 fn default() -> Self {
1231 Self {
1232 replay_buffer_size: default_replay_buffer_size(),
1233 replay_ttl_secs: default_replay_ttl_secs(),
1234 }
1235 }
1236}
1237
1238fn default_response_timeout_secs() -> u64 {
1239 120
1240}
1241fn default_replay_buffer_size() -> usize {
1242 512
1243}
1244fn default_replay_ttl_secs() -> u64 {
1245 60
1246}
1247
1248impl GatewayConfig {
1249 pub fn should_expose_api_docs(&self) -> bool {
1255 if !self.expose_api_docs {
1256 return false;
1257 }
1258 let h = self.host.trim();
1259 h == "127.0.0.1" || h == "::1" || h == "localhost" || h.starts_with("127.")
1260 }
1261}
1262
1263#[derive(Debug, Clone, Deserialize, Serialize)]
1265pub struct MarketplaceConfig {
1266 #[serde(default)]
1269 pub base_url: Option<String>,
1270 #[serde(default = "default_true")]
1272 pub enabled: bool,
1273 #[serde(default)]
1275 pub skills_sh: SkillsShConfig,
1276}
1277
1278#[derive(Debug, Clone, Deserialize, Serialize)]
1280pub struct SkillsShConfig {
1281 #[serde(default)]
1284 pub base_url: Option<String>,
1285 #[serde(default)]
1288 pub api_key: Option<String>,
1289 #[serde(default = "default_true")]
1291 pub enabled: bool,
1292}
1293
1294impl Default for MarketplaceConfig {
1295 fn default() -> Self {
1296 Self {
1297 base_url: Some("https://clawhub.ai".to_string()),
1298 enabled: true,
1299 skills_sh: SkillsShConfig::default(),
1300 }
1301 }
1302}
1303
1304impl Default for SkillsShConfig {
1305 fn default() -> Self {
1306 Self {
1307 base_url: None,
1308 api_key: None,
1309 enabled: true,
1310 }
1311 }
1312}
1313
1314#[derive(Debug, Clone, Deserialize, Serialize)]
1316pub struct CalendarConfig {
1317 #[serde(default = "default_true")]
1319 pub enabled: bool,
1320 #[serde(default = "default_calendar_timezone")]
1322 pub timezone: String,
1323 #[serde(default = "default_reminder_minutes")]
1325 pub default_reminder_minutes: Vec<u32>,
1326 #[serde(default)]
1328 pub alarm_channels: Vec<String>,
1329 #[serde(default = "default_journal_sync")]
1331 pub journal_sync: String,
1332 #[serde(default = "default_true")]
1334 pub system_calendar: bool,
1335 #[serde(default = "default_archive_days")]
1337 pub archive_after_days: u32,
1338}
1339
1340fn default_calendar_timezone() -> String {
1341 "Asia/Seoul".to_string()
1342}
1343
1344fn default_reminder_minutes() -> Vec<u32> {
1345 vec![15]
1346}
1347
1348fn default_journal_sync() -> String {
1349 "on_open".to_string()
1350}
1351
1352fn default_archive_days() -> u32 {
1353 365
1354}
1355
1356impl Default for CalendarConfig {
1357 fn default() -> Self {
1358 Self {
1359 enabled: true,
1360 timezone: default_calendar_timezone(),
1361 default_reminder_minutes: default_reminder_minutes(),
1362 alarm_channels: vec![],
1363 journal_sync: default_journal_sync(),
1364 system_calendar: true,
1365 archive_after_days: default_archive_days(),
1366 }
1367 }
1368}
1369
1370#[derive(Debug, Clone, Deserialize, Serialize)]
1375pub struct EmailConfig {
1376 #[serde(default)]
1378 pub enabled: bool,
1379 #[serde(default)]
1381 pub my_email: String,
1382 #[serde(default = "default_email_provider")]
1384 pub provider: SmtpProvider,
1385 #[serde(default)]
1387 pub host: String,
1388 #[serde(default)]
1390 pub port: u16,
1391 #[serde(default)]
1393 pub tls: Option<SmtpTls>,
1394 #[serde(default)]
1396 pub user: String,
1397 #[serde(default = "default_email_secret_ref")]
1400 pub secret_ref: String,
1401 #[serde(default = "default_rate_limit_emails")]
1403 pub rate_limit_per_hour: usize,
1404}
1405
1406fn default_email_provider() -> SmtpProvider {
1407 SmtpProvider::Gmail
1408}
1409
1410fn default_email_secret_ref() -> String {
1411 "email_smtp".to_string()
1412}
1413
1414fn default_rate_limit_emails() -> usize {
1415 10
1416}
1417
1418impl Default for EmailConfig {
1419 fn default() -> Self {
1420 Self {
1421 enabled: false,
1422 my_email: String::new(),
1423 provider: default_email_provider(),
1424 host: String::new(),
1425 port: 0,
1426 tls: None,
1427 user: String::new(),
1428 secret_ref: default_email_secret_ref(),
1429 rate_limit_per_hour: default_rate_limit_emails(),
1430 }
1431 }
1432}
1433
1434impl EmailConfig {
1435 pub fn provider(&self) -> SmtpProvider {
1437 self.provider
1438 }
1439}
1440
1441fn default_gateway_host() -> String {
1442 "127.0.0.1".into()
1443}
1444
1445fn default_gateway_port() -> u16 {
1446 4200
1447}
1448
1449impl Default for GatewayConfig {
1450 fn default() -> Self {
1451 Self {
1452 host: default_gateway_host(),
1453 port: default_gateway_port(),
1454 expose_api_docs: false,
1455 response_timeout_secs: default_response_timeout_secs(),
1456 reliability: GatewayReliabilityConfig::default(),
1457 }
1458 }
1459}
1460
1461#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1466#[serde(rename_all = "lowercase")]
1467pub enum ExecMode {
1468 #[default]
1470 Structured,
1471 Shell,
1473}
1474
1475#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1477#[serde(rename_all = "snake_case")]
1478#[derive(Default)]
1479pub enum AllowlistMode {
1480 Permissive,
1482 #[default]
1484 Enforced,
1485}
1486
1487#[derive(Debug, Clone, Deserialize, Serialize)]
1491pub struct ExecConfig {
1492 #[serde(default)]
1494 pub default_mode: ExecMode,
1495 #[serde(default = "default_false")]
1497 pub allow_shell_mode: bool,
1498 #[serde(default)]
1501 pub allowed_commands: Vec<String>,
1502 #[serde(default)]
1506 pub allowlist_mode: AllowlistMode,
1507 #[serde(default = "default_exec_timeout")]
1509 pub default_timeout_secs: u64,
1510 #[serde(default = "default_exec_max_timeout")]
1512 pub max_timeout_secs: u64,
1513}
1514
1515fn default_false() -> bool {
1516 false
1517}
1518
1519fn default_exec_timeout() -> u64 {
1520 120
1521}
1522
1523fn default_exec_max_timeout() -> u64 {
1524 600
1525}
1526
1527impl ExecConfig {
1528 pub fn is_binary_allowed(&self, name: &str) -> bool {
1535 match self.allowlist_mode {
1536 AllowlistMode::Permissive => {
1537 self.allowed_commands.is_empty() || self.allowed_commands.iter().any(|c| c == name)
1538 }
1539 AllowlistMode::Enforced => self.allowed_commands.iter().any(|c| c == name),
1540 }
1541 }
1542}
1543
1544impl Default for ExecConfig {
1545 fn default() -> Self {
1546 Self {
1547 default_mode: ExecMode::default(),
1548 allow_shell_mode: default_false(),
1549 allowed_commands: Vec::new(),
1550 allowlist_mode: AllowlistMode::default(),
1551 default_timeout_secs: default_exec_timeout(),
1552 max_timeout_secs: default_exec_max_timeout(),
1553 }
1554 }
1555}
1556
1557#[derive(Debug, Clone, Deserialize, Serialize)]
1559pub struct OrchestratorConfig {
1560 #[serde(default = "default_max_evolution_iterations")]
1563 pub max_evolution_iterations: u32,
1564
1565 #[serde(default = "default_min_evaluation_score")]
1568 pub min_evaluation_score: f64,
1569}
1570
1571fn default_max_evolution_iterations() -> u32 {
1572 3
1573}
1574
1575fn default_min_evaluation_score() -> f64 {
1576 0.8
1577}
1578
1579impl Default for OrchestratorConfig {
1580 fn default() -> Self {
1581 Self {
1582 max_evolution_iterations: default_max_evolution_iterations(),
1583 min_evaluation_score: default_min_evaluation_score(),
1584 }
1585 }
1586}
1587
1588#[derive(Debug, Clone, Serialize, Deserialize)]
1593pub struct IntentConfig {
1594 #[serde(default = "default_intent_max_retries")]
1598 pub max_retries: u32,
1599
1600 #[serde(default = "default_intent_score_threshold")]
1604 pub score_threshold: f64,
1605
1606 #[serde(default = "default_intent_max_clarify_rounds")]
1610 pub max_clarify_rounds: u32,
1611
1612 #[serde(default = "default_intent_enable_retry")]
1616 pub enable_retry: bool,
1617
1618 #[serde(default)]
1622 pub lightweight_model: Option<String>,
1623}
1624
1625fn default_intent_max_retries() -> u32 {
1626 2
1627}
1628
1629fn default_intent_score_threshold() -> f64 {
1630 0.7
1631}
1632
1633fn default_intent_max_clarify_rounds() -> u32 {
1634 3
1635}
1636
1637fn default_intent_enable_retry() -> bool {
1638 true
1639}
1640
1641impl Default for IntentConfig {
1642 fn default() -> Self {
1643 Self {
1644 max_retries: default_intent_max_retries(),
1645 score_threshold: default_intent_score_threshold(),
1646 max_clarify_rounds: default_intent_max_clarify_rounds(),
1647 enable_retry: default_intent_enable_retry(),
1648 lightweight_model: None,
1649 }
1650 }
1651}
1652
1653#[derive(Debug, Clone, Deserialize, Serialize)]
1655pub struct ContextConfig {
1656 #[serde(default = "default_active_limit")]
1658 pub active_limit_tokens: usize,
1659 #[serde(default = "default_cache_limit")]
1661 pub cache_limit_entries: usize,
1662}
1663
1664fn default_active_limit() -> usize {
1665 100_000
1666}
1667
1668fn default_cache_limit() -> usize {
1669 50
1670}
1671
1672impl Default for ContextConfig {
1673 fn default() -> Self {
1674 Self {
1675 active_limit_tokens: default_active_limit(),
1676 cache_limit_entries: default_cache_limit(),
1677 }
1678 }
1679}
1680
1681#[derive(Debug, Clone, Deserialize, Serialize)]
1683pub struct SecurityConfig {
1684 #[serde(default = "default_allowed_tools")]
1686 pub allowed_tools: Vec<String>,
1687 #[serde(default)]
1689 pub network_access: bool,
1690 #[serde(default = "default_max_exec_time")]
1692 pub max_execution_time_secs: u64,
1693 #[serde(default = "default_max_memory")]
1695 pub max_memory_mb: u64,
1696 #[serde(default)]
1698 pub can_fork: bool,
1699 #[serde(default = "default_max_audit")]
1701 pub max_audit_entries: usize,
1702 #[serde(default)]
1704 pub auth_enabled: bool,
1705 #[serde(default = "default_cors_origins")]
1707 pub cors_origins: Vec<String>,
1708 #[serde(default)]
1710 pub audit_log_path: Option<String>,
1711 #[serde(default = "default_rate_limit_per_minute")]
1713 pub rate_limit_per_minute: u32,
1714}
1715
1716fn default_allowed_tools() -> Vec<String> {
1717 vec![
1718 "read".to_string(),
1719 "write".to_string(),
1720 "edit".to_string(),
1721 "bash".to_string(),
1722 "grep".to_string(),
1723 "find".to_string(),
1724 "exec".to_string(),
1725 ]
1726}
1727
1728fn default_max_exec_time() -> u64 {
1729 300
1730}
1731
1732fn default_max_memory() -> u64 {
1733 512
1734}
1735
1736fn default_max_audit() -> usize {
1737 10_000
1738}
1739
1740fn default_rate_limit_per_minute() -> u32 {
1741 600
1745}
1746
1747fn default_cors_origins() -> Vec<String> {
1748 vec![
1753 "http://localhost:4200".to_string(),
1754 "http://127.0.0.1:4200".to_string(),
1755 "http://localhost:5173".to_string(),
1756 "http://127.0.0.1:5173".to_string(),
1757 ]
1758}
1759
1760impl Default for SecurityConfig {
1761 fn default() -> Self {
1762 Self {
1763 allowed_tools: default_allowed_tools(),
1764 network_access: false,
1765 max_execution_time_secs: default_max_exec_time(),
1766 max_memory_mb: default_max_memory(),
1767 can_fork: false,
1768 max_audit_entries: default_max_audit(),
1769 auth_enabled: false,
1770 cors_origins: default_cors_origins(),
1771 audit_log_path: None,
1772 rate_limit_per_minute: default_rate_limit_per_minute(),
1773 }
1774 }
1775}
1776
1777#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1782pub struct PersonaConfig {
1783 #[serde(default)]
1785 pub default_persona_id: Option<String>,
1786}
1787
1788#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1796pub struct McpConfig {
1797 #[serde(default)]
1799 pub servers: std::collections::HashMap<String, McpServerDef>,
1800}
1801
1802#[derive(Debug, Clone, Deserialize, Serialize)]
1804pub struct McpServerDef {
1805 pub command: String,
1807 #[serde(default)]
1809 pub args: Vec<String>,
1810 #[serde(default)]
1812 pub env: std::collections::HashMap<String, String>,
1813 #[serde(default = "default_mcp_enabled")]
1815 pub enabled: bool,
1816}
1817
1818fn default_mcp_enabled() -> bool {
1819 true
1820}
1821
1822#[derive(Debug, Clone, Deserialize, Serialize)]
1824pub struct GitConfig {
1825 #[serde(default = "default_true")]
1827 pub auto_commit: bool,
1828}
1829
1830impl Default for GitConfig {
1831 fn default() -> Self {
1832 Self { auto_commit: true }
1833 }
1834}
1835
1836#[derive(Debug, Clone, Deserialize, Serialize)]
1838pub struct AuditConfig {
1839 #[serde(default = "default_audit_max_entries")]
1841 pub max_entries: usize,
1842 #[serde(default = "default_true")]
1844 pub enabled: bool,
1845}
1846
1847fn default_audit_max_entries() -> usize {
1848 100_000
1849}
1850
1851impl Default for AuditConfig {
1852 fn default() -> Self {
1853 Self {
1854 max_entries: default_audit_max_entries(),
1855 enabled: true,
1856 }
1857 }
1858}
1859
1860#[derive(Debug, Clone, Deserialize, Serialize)]
1862pub struct BudgetConfig {
1863 #[serde(default)]
1865 pub default_token_budget: u64,
1866 #[serde(default)]
1868 pub default_calls_budget: u64,
1869 #[serde(default = "default_budget_window")]
1871 pub default_window_secs: u64,
1872 #[serde(default = "default_true")]
1874 pub enabled: bool,
1875 #[serde(default)]
1879 pub monthly_spend_limit_usd: Option<f64>,
1880}
1881
1882fn default_budget_window() -> u64 {
1883 3600
1884}
1885
1886impl Default for BudgetConfig {
1887 fn default() -> Self {
1888 Self {
1889 default_token_budget: 0,
1890 default_calls_budget: 0,
1891 default_window_secs: default_budget_window(),
1892 enabled: true,
1893 monthly_spend_limit_usd: None,
1894 }
1895 }
1896}
1897
1898#[derive(Debug, Clone, Deserialize, Serialize)]
1900pub struct ResourceMonitorConfig {
1901 #[serde(default = "default_rm_interval")]
1903 pub interval_secs: u64,
1904 #[serde(default = "default_rm_history_max")]
1906 pub history_max: usize,
1907 #[serde(default = "default_rm_cpu_threshold")]
1909 pub cpu_threshold: f32,
1910 #[serde(default = "default_rm_mem_threshold")]
1912 pub memory_threshold: f32,
1913 #[serde(default = "default_rm_load_threshold")]
1915 pub load_threshold: f32,
1916}
1917
1918fn default_rm_interval() -> u64 {
1919 60
1920}
1921
1922fn default_rm_history_max() -> usize {
1923 60
1924}
1925
1926fn default_rm_cpu_threshold() -> f32 {
1927 90.0
1928}
1929
1930fn default_rm_mem_threshold() -> f32 {
1931 90.0
1932}
1933
1934fn default_rm_load_threshold() -> f32 {
1935 8.0
1936}
1937
1938impl Default for ResourceMonitorConfig {
1939 fn default() -> Self {
1940 Self {
1941 interval_secs: default_rm_interval(),
1942 history_max: default_rm_history_max(),
1943 cpu_threshold: default_rm_cpu_threshold(),
1944 memory_threshold: default_rm_mem_threshold(),
1945 load_threshold: default_rm_load_threshold(),
1946 }
1947 }
1948}
1949
1950#[derive(Debug, Clone, Serialize, Deserialize)]
1952pub struct AgentLogConfig {
1953 #[serde(default = "default_agent_log_max_entries")]
1955 pub max_entries: usize,
1956 #[serde(default = "default_agent_log_ttl_hours")]
1958 pub ttl_hours: u64,
1959 #[serde(default = "default_agent_log_max_tool_calls")]
1961 pub max_tool_calls_per_agent: usize,
1962 #[serde(default = "default_agent_log_prune_batch")]
1964 pub prune_batch_size: usize,
1965 #[serde(default)]
1967 pub db_path: String,
1968}
1969
1970fn default_agent_log_max_entries() -> usize {
1971 10_000
1972}
1973fn default_agent_log_ttl_hours() -> u64 {
1974 720
1975}
1976fn default_agent_log_max_tool_calls() -> usize {
1977 500
1978}
1979fn default_agent_log_prune_batch() -> usize {
1980 100
1981}
1982
1983impl Default for AgentLogConfig {
1984 fn default() -> Self {
1985 Self {
1986 max_entries: 10_000,
1987 ttl_hours: 720,
1988 max_tool_calls_per_agent: 500,
1989 prune_batch_size: 100,
1990 db_path: String::new(),
1991 }
1992 }
1993}
1994
1995#[derive(Debug, Clone, Deserialize, Serialize)]
1997pub struct LoggingConfig {
1998 #[serde(default = "default_log_format")]
2000 pub format: String,
2001 #[serde(default)]
2003 pub level: Option<String>,
2004}
2005
2006fn default_log_format() -> String {
2007 "pretty".into()
2008}
2009
2010impl Default for LoggingConfig {
2011 fn default() -> Self {
2012 Self {
2013 format: default_log_format(),
2014 level: None,
2015 }
2016 }
2017}
2018
2019#[derive(Debug, Clone, Deserialize, Serialize)]
2025pub struct BrowserConfig {
2026 #[serde(default = "default_browser_enabled")]
2028 pub enabled: bool,
2029
2030 #[serde(default)]
2041 pub engine: serde_json::Value,
2042}
2043
2044fn default_browser_enabled() -> bool {
2045 true
2046}
2047
2048impl Default for BrowserConfig {
2049 fn default() -> Self {
2050 Self {
2051 enabled: true,
2052 engine: serde_json::json!({}),
2053 }
2054 }
2055}
2056
2057pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
2059 let content = std::fs::read_to_string(path)?;
2060 let config: OxiosConfig = toml::from_str(&content)?;
2061 let (errors, warnings) = config.validate();
2062 for w in warnings {
2063 tracing::warn!("config: {}", w);
2064 }
2065 if !errors.is_empty() {
2066 let msg = errors.join("; ");
2067 anyhow::bail!("Configuration validation failed: {msg}");
2068 }
2069 Ok(config)
2070}
2071
2072impl OxiosConfig {
2073 pub fn api_key(&self) -> Option<String> {
2075 self.engine.api_key.clone().filter(|k| !k.is_empty())
2076 }
2077
2078 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
2081 let mut errors = Vec::new();
2082 let mut warnings = Vec::new();
2083
2084 if self.kernel.max_agents == 0 {
2086 errors.push("kernel.max_agents must be > 0".into());
2087 }
2088 if self.kernel.workspace.is_empty() {
2089 errors.push("kernel.workspace must not be empty".into());
2090 }
2091
2092 if self.gateway.port == 0 {
2094 errors.push("gateway.port must be > 0".into());
2095 }
2096 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
2097 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
2098 }
2099
2100 for (name, job) in &self.cron.jobs {
2102 if job.schedule.is_empty() {
2103 errors.push(format!("cron.jobs.{name}: schedule is empty"));
2104 } else {
2105 let normalized = {
2107 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
2108 match fields.len() {
2109 5 => format!("0 {}", job.schedule),
2110 _ => job.schedule.clone(),
2111 }
2112 };
2113 if Schedule::from_str(&normalized).is_err() {
2114 errors.push(format!(
2115 "cron.jobs.{}: invalid cron expression '{}'",
2116 name, job.schedule
2117 ));
2118 }
2119 }
2120 if job.goal.is_empty() {
2121 errors.push(format!("cron.jobs.{name}: goal is empty"));
2122 }
2123 }
2124
2125 if self.security.max_execution_time_secs == 0 {
2127 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
2128 }
2129
2130 if self.audit.max_entries == 0 {
2132 warnings.push("audit.max_entries is 0 — audit will never prune".into());
2133 }
2134
2135 if self.budget.default_window_secs == 0 {
2137 warnings.push("budget.default_window_secs is 0 — no time window".into());
2138 }
2139
2140 if self.gateway.response_timeout_secs == 0 {
2142 errors.push("gateway.response_timeout_secs must be > 0".into());
2143 }
2144
2145 if self.engine.api_key.as_ref().is_some_and(|k| !k.is_empty()) {
2148 warnings.push(
2149 "engine.api_key is set in config — prefer the oxi auth store or env var to avoid storing a secret on disk"
2150 .into(),
2151 );
2152 }
2153
2154 for (name, server) in &self.mcp.servers {
2156 if server.command.trim().is_empty() {
2157 errors.push(format!("mcp.servers.{name}: command must not be empty"));
2158 }
2159 }
2160
2161 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
2163 {
2164 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
2165 }
2166
2167 if self.exec.default_timeout_secs == 0 {
2169 errors.push("exec.default_timeout_secs must be > 0".into());
2170 }
2171 if self.exec.max_timeout_secs == 0 {
2172 errors.push("exec.max_timeout_secs must be > 0".into());
2173 }
2174 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
2175 errors.push(format!(
2176 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
2177 self.exec.default_timeout_secs, self.exec.max_timeout_secs
2178 ));
2179 }
2180
2181 if self.resource_monitor.cpu_threshold > 100.0 {
2183 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
2184 }
2185 if self.resource_monitor.memory_threshold > 100.0 {
2186 errors.push("resource_monitor.memory_threshold must be <= 100".into());
2187 }
2188
2189 for name in &self.channels.enabled {
2191 let valid = ["cli", "telegram"];
2192 if !valid.contains(&name.as_str()) {
2193 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
2194 }
2195 }
2196 if self.channels.enabled.iter().any(|c| c == "web") {
2198 warnings.push(
2199 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
2200 );
2201 }
2202 if self.channels.enabled.iter().any(|c| c == "telegram")
2203 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
2204 {
2205 warnings.push(format!(
2206 "channels.telegram: {} env var not set — telegram channel will fail",
2207 self.channels.telegram.bot_token_env
2208 ));
2209 }
2210 for err in self.token_maxing.validate() {
2214 errors.push(err);
2215 }
2216
2217 (errors, warnings)
2218 }
2219}
2220
2221pub fn expand_home(path: &str) -> std::path::PathBuf {
2233 if let Some(rest) = path.strip_prefix("~/") {
2234 if let Ok(home) = std::env::var("HOME") {
2235 return std::path::PathBuf::from(format!("{home}/{rest}"));
2236 }
2237 if let Some(home) = dirs::home_dir() {
2238 return home.join(rest);
2239 }
2240 }
2241 std::path::PathBuf::from(path)
2242}
2243
2244#[cfg(test)]
2245mod tests {
2246 use super::*;
2247
2248 #[test]
2249 fn test_default_config_validates() {
2250 let config = OxiosConfig::default();
2251 let (errors, _warnings) = config.validate();
2252 assert!(
2253 errors.is_empty(),
2254 "Default config should have no errors: {:?}",
2255 errors
2256 );
2257 }
2258
2259 #[test]
2260 fn test_exec_config_default_allowed_commands() {
2261 let config = ExecConfig::default();
2262 assert!(config.allowed_commands.is_empty());
2264 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
2265 assert!(!config.is_binary_allowed("anything"));
2266 assert!(!config.is_binary_allowed("bash"));
2267 }
2268
2269 #[test]
2270 fn test_exec_config_permissive_mode() {
2271 let config = ExecConfig {
2272 allowlist_mode: AllowlistMode::Permissive,
2273 ..Default::default()
2274 };
2275 assert!(config.is_binary_allowed("anything"));
2277 assert!(config.is_binary_allowed("bash"));
2278 }
2279
2280 #[test]
2281 fn test_is_binary_allowed_with_allowlist() {
2282 let config = ExecConfig {
2283 allowed_commands: vec!["git".into(), "echo".into()],
2284 ..Default::default()
2285 };
2286 assert!(config.is_binary_allowed("git"));
2287 assert!(config.is_binary_allowed("echo"));
2288 assert!(!config.is_binary_allowed("bash"));
2289 assert!(!config.is_binary_allowed("rm"));
2290 assert!(!config.is_binary_allowed("sudo"));
2291 }
2292
2293 #[test]
2294 fn test_expand_home() {
2295 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
2297 let expanded = expand_home("~/projects/test");
2298 assert_eq!(
2299 expanded.to_str().unwrap(),
2300 format!("{}/projects/test", home)
2301 );
2302
2303 let abs = expand_home("/absolute/path");
2305 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
2306
2307 let bare = expand_home("~something");
2309 assert_eq!(bare, std::path::PathBuf::from("~something"));
2310 }
2311
2312 #[test]
2313 fn test_invalid_cron_expression() {
2314 let mut config = OxiosConfig::default();
2315 config.cron.enabled = true;
2316 config.cron.jobs.insert(
2317 "bad-job".to_string(),
2318 InlineCronJob {
2319 schedule: "not a valid cron".to_string(),
2320 goal: "Test goal".to_string(),
2321 constraints: vec![],
2322 acceptance_criteria: vec![],
2323 toolchain: "default".to_string(),
2324 priority: Priority::Normal,
2325 enabled: true,
2326 },
2327 );
2328
2329 let (errors, _warnings) = config.validate();
2330 assert!(
2331 !errors.is_empty(),
2332 "Expected validation error for invalid cron"
2333 );
2334 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
2335 assert!(
2336 has_cron_error,
2337 "Expected 'invalid cron expression' error, got: {:?}",
2338 errors
2339 );
2340 }
2341
2342 #[test]
2343 fn test_config_serialization_roundtrip() {
2344 let config = OxiosConfig::default();
2345
2346 let toml_str = toml::to_string(&config).expect("serialization should succeed");
2348
2349 let deserialized: OxiosConfig =
2351 toml::from_str(&toml_str).expect("deserialization should succeed");
2352
2353 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2355 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2356 assert_eq!(config.gateway.host, deserialized.gateway.host);
2357 assert_eq!(config.gateway.port, deserialized.gateway.port);
2358 assert_eq!(
2359 config.exec.default_timeout_secs,
2360 deserialized.exec.default_timeout_secs
2361 );
2362 assert_eq!(
2363 config.exec.max_timeout_secs,
2364 deserialized.exec.max_timeout_secs
2365 );
2366 }
2367
2368 #[test]
2369 fn test_exec_timeout_validation() {
2370 let mut config = OxiosConfig::default();
2371 config.exec.default_timeout_secs = 999;
2373 config.exec.max_timeout_secs = 100;
2374 let (errors, _warnings) = config.validate();
2375 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2376 assert!(
2377 has_error,
2378 "Expected timeout ordering error, got: {:?}",
2379 errors
2380 );
2381 }
2382
2383 #[test]
2384 fn test_zero_max_agents_error() {
2385 let mut config = OxiosConfig::default();
2386 config.kernel.max_agents = 0;
2387 let (errors, _warnings) = config.validate();
2388 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2389 }
2390
2391 #[test]
2396 fn test_default_config_matches_toml() {
2397 let from_rust = OxiosConfig::default();
2398
2399 let toml_str = include_str!("../../../share/default-config.toml");
2400 let from_toml: OxiosConfig =
2401 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2402
2403 assert_eq!(
2405 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2406 "kernel.max_agents 불일치: Rust={}, TOML={}",
2407 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2408 );
2409 assert_eq!(
2410 from_rust.gateway.host, from_toml.gateway.host,
2411 "gateway.host 불일치: Rust={}, TOML={}",
2412 from_rust.gateway.host, from_toml.gateway.host
2413 );
2414 assert_eq!(
2415 from_rust.gateway.port, from_toml.gateway.port,
2416 "gateway.port 불일치: Rust={}, TOML={}",
2417 from_rust.gateway.port, from_toml.gateway.port
2418 );
2419 assert_eq!(
2420 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2421 "kernel.event_bus_capacity 불일치"
2422 );
2423 assert_eq!(
2424 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2425 "memory.consolidation.preset 불일치"
2426 );
2427
2428 let (_, warnings) = from_toml.validate();
2430 for w in &warnings {
2431 eprintln!("default-config.toml 경고: {}", w);
2432 }
2433 }
2434
2435 #[test]
2438 fn test_gateway_should_expose_api_docs() {
2439 let cfg = GatewayConfig::default();
2441 assert!(!cfg.should_expose_api_docs());
2442
2443 let cfg = GatewayConfig {
2445 host: "0.0.0.0".into(),
2446 port: 4200,
2447 expose_api_docs: true,
2448 ..Default::default()
2449 };
2450 assert!(
2451 !cfg.should_expose_api_docs(),
2452 "public bind must not expose api docs even when opt-in is true"
2453 );
2454
2455 let cfg = GatewayConfig {
2457 host: "127.0.0.1".into(),
2458 port: 4200,
2459 expose_api_docs: true,
2460 ..Default::default()
2461 };
2462 assert!(cfg.should_expose_api_docs());
2463
2464 let cfg = GatewayConfig {
2466 host: "::1".into(),
2467 port: 4200,
2468 expose_api_docs: true,
2469 ..Default::default()
2470 };
2471 assert!(cfg.should_expose_api_docs());
2472
2473 let cfg = GatewayConfig {
2475 host: "localhost".into(),
2476 port: 4200,
2477 expose_api_docs: true,
2478 ..Default::default()
2479 };
2480 assert!(cfg.should_expose_api_docs());
2481
2482 let cfg = GatewayConfig {
2484 host: "127.0.0.1".into(),
2485 port: 4200,
2486 expose_api_docs: false,
2487 ..Default::default()
2488 };
2489 assert!(!cfg.should_expose_api_docs());
2490 }
2491}