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}
1092
1093#[derive(Debug, Clone, Deserialize, Serialize)]
1095pub struct KernelConfig {
1096 #[serde(default = "default_workspace")]
1098 pub workspace: String,
1099 #[serde(default = "default_event_bus_capacity")]
1101 pub event_bus_capacity: usize,
1102 #[serde(default = "default_max_agents")]
1104 pub max_agents: usize,
1105}
1106
1107fn default_workspace() -> String {
1108 dirs_home().unwrap_or_else(|| ".".into())
1109}
1110
1111fn dirs_home() -> Option<String> {
1112 dirs::home_dir().map(|h| format!("{}/.oxios/workspace", h.display()))
1113}
1114
1115fn default_event_bus_capacity() -> usize {
1116 256
1117}
1118
1119fn default_max_agents() -> usize {
1120 10
1121}
1122
1123impl Default for KernelConfig {
1124 fn default() -> Self {
1125 Self {
1126 workspace: default_workspace(),
1127 event_bus_capacity: default_event_bus_capacity(),
1128 max_agents: 10,
1129 }
1130 }
1131}
1132
1133#[derive(Debug, Clone, Deserialize, Serialize)]
1135pub struct GatewayConfig {
1136 #[serde(default = "default_gateway_host")]
1138 pub host: String,
1139 #[serde(default = "default_gateway_port")]
1141 pub port: u16,
1142 #[serde(default)]
1152 pub expose_api_docs: bool,
1153 #[serde(default = "default_response_timeout_secs")]
1157 pub response_timeout_secs: u64,
1158 #[serde(default)]
1160 pub reliability: GatewayReliabilityConfig,
1161}
1162
1163#[derive(Debug, Clone, Serialize, Deserialize)]
1165pub struct GatewayReliabilityConfig {
1166 #[serde(default = "default_replay_buffer_size")]
1169 pub replay_buffer_size: usize,
1170 #[serde(default = "default_replay_ttl_secs")]
1172 pub replay_ttl_secs: u64,
1173}
1174
1175impl Default for GatewayReliabilityConfig {
1176 fn default() -> Self {
1177 Self {
1178 replay_buffer_size: default_replay_buffer_size(),
1179 replay_ttl_secs: default_replay_ttl_secs(),
1180 }
1181 }
1182}
1183
1184fn default_response_timeout_secs() -> u64 {
1185 120
1186}
1187fn default_replay_buffer_size() -> usize {
1188 512
1189}
1190fn default_replay_ttl_secs() -> u64 {
1191 60
1192}
1193
1194impl GatewayConfig {
1195 pub fn should_expose_api_docs(&self) -> bool {
1201 if !self.expose_api_docs {
1202 return false;
1203 }
1204 let h = self.host.trim();
1205 h == "127.0.0.1" || h == "::1" || h == "localhost" || h.starts_with("127.")
1206 }
1207}
1208
1209#[derive(Debug, Clone, Deserialize, Serialize)]
1211pub struct MarketplaceConfig {
1212 #[serde(default)]
1215 pub base_url: Option<String>,
1216 #[serde(default = "default_true")]
1218 pub enabled: bool,
1219 #[serde(default)]
1221 pub skills_sh: SkillsShConfig,
1222}
1223
1224#[derive(Debug, Clone, Deserialize, Serialize)]
1226pub struct SkillsShConfig {
1227 #[serde(default)]
1230 pub base_url: Option<String>,
1231 #[serde(default)]
1234 pub api_key: Option<String>,
1235 #[serde(default = "default_true")]
1237 pub enabled: bool,
1238}
1239
1240impl Default for MarketplaceConfig {
1241 fn default() -> Self {
1242 Self {
1243 base_url: Some("https://clawhub.ai".to_string()),
1244 enabled: true,
1245 skills_sh: SkillsShConfig::default(),
1246 }
1247 }
1248}
1249
1250impl Default for SkillsShConfig {
1251 fn default() -> Self {
1252 Self {
1253 base_url: None,
1254 api_key: None,
1255 enabled: true,
1256 }
1257 }
1258}
1259
1260#[derive(Debug, Clone, Deserialize, Serialize)]
1262pub struct CalendarConfig {
1263 #[serde(default = "default_true")]
1265 pub enabled: bool,
1266 #[serde(default = "default_calendar_timezone")]
1268 pub timezone: String,
1269 #[serde(default = "default_reminder_minutes")]
1271 pub default_reminder_minutes: Vec<u32>,
1272 #[serde(default)]
1274 pub alarm_channels: Vec<String>,
1275 #[serde(default = "default_journal_sync")]
1277 pub journal_sync: String,
1278 #[serde(default = "default_true")]
1280 pub system_calendar: bool,
1281 #[serde(default = "default_archive_days")]
1283 pub archive_after_days: u32,
1284}
1285
1286fn default_calendar_timezone() -> String {
1287 "Asia/Seoul".to_string()
1288}
1289
1290fn default_reminder_minutes() -> Vec<u32> {
1291 vec![15]
1292}
1293
1294fn default_journal_sync() -> String {
1295 "on_open".to_string()
1296}
1297
1298fn default_archive_days() -> u32 {
1299 365
1300}
1301
1302impl Default for CalendarConfig {
1303 fn default() -> Self {
1304 Self {
1305 enabled: true,
1306 timezone: default_calendar_timezone(),
1307 default_reminder_minutes: default_reminder_minutes(),
1308 alarm_channels: vec![],
1309 journal_sync: default_journal_sync(),
1310 system_calendar: true,
1311 archive_after_days: default_archive_days(),
1312 }
1313 }
1314}
1315
1316#[derive(Debug, Clone, Deserialize, Serialize)]
1321pub struct EmailConfig {
1322 #[serde(default)]
1324 pub enabled: bool,
1325 #[serde(default)]
1327 pub my_email: String,
1328 #[serde(default = "default_email_provider")]
1330 pub provider: SmtpProvider,
1331 #[serde(default)]
1333 pub host: String,
1334 #[serde(default)]
1336 pub port: u16,
1337 #[serde(default)]
1339 pub tls: Option<SmtpTls>,
1340 #[serde(default)]
1342 pub user: String,
1343 #[serde(default = "default_email_secret_ref")]
1346 pub secret_ref: String,
1347 #[serde(default = "default_rate_limit_emails")]
1349 pub rate_limit_per_hour: usize,
1350}
1351
1352fn default_email_provider() -> SmtpProvider {
1353 SmtpProvider::Gmail
1354}
1355
1356fn default_email_secret_ref() -> String {
1357 "email_smtp".to_string()
1358}
1359
1360fn default_rate_limit_emails() -> usize {
1361 10
1362}
1363
1364impl Default for EmailConfig {
1365 fn default() -> Self {
1366 Self {
1367 enabled: false,
1368 my_email: String::new(),
1369 provider: default_email_provider(),
1370 host: String::new(),
1371 port: 0,
1372 tls: None,
1373 user: String::new(),
1374 secret_ref: default_email_secret_ref(),
1375 rate_limit_per_hour: default_rate_limit_emails(),
1376 }
1377 }
1378}
1379
1380impl EmailConfig {
1381 pub fn provider(&self) -> SmtpProvider {
1383 self.provider
1384 }
1385}
1386
1387fn default_gateway_host() -> String {
1388 "127.0.0.1".into()
1389}
1390
1391fn default_gateway_port() -> u16 {
1392 4200
1393}
1394
1395impl Default for GatewayConfig {
1396 fn default() -> Self {
1397 Self {
1398 host: default_gateway_host(),
1399 port: default_gateway_port(),
1400 expose_api_docs: false,
1401 response_timeout_secs: default_response_timeout_secs(),
1402 reliability: GatewayReliabilityConfig::default(),
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1412#[serde(rename_all = "lowercase")]
1413pub enum ExecMode {
1414 #[default]
1416 Structured,
1417 Shell,
1419}
1420
1421#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1423#[serde(rename_all = "snake_case")]
1424#[derive(Default)]
1425pub enum AllowlistMode {
1426 Permissive,
1428 #[default]
1430 Enforced,
1431}
1432
1433#[derive(Debug, Clone, Deserialize, Serialize)]
1437pub struct ExecConfig {
1438 #[serde(default)]
1440 pub default_mode: ExecMode,
1441 #[serde(default = "default_false")]
1443 pub allow_shell_mode: bool,
1444 #[serde(default)]
1447 pub allowed_commands: Vec<String>,
1448 #[serde(default)]
1452 pub allowlist_mode: AllowlistMode,
1453 #[serde(default = "default_exec_timeout")]
1455 pub default_timeout_secs: u64,
1456 #[serde(default = "default_exec_max_timeout")]
1458 pub max_timeout_secs: u64,
1459}
1460
1461fn default_false() -> bool {
1462 false
1463}
1464
1465fn default_exec_timeout() -> u64 {
1466 120
1467}
1468
1469fn default_exec_max_timeout() -> u64 {
1470 600
1471}
1472
1473impl ExecConfig {
1474 pub fn is_binary_allowed(&self, name: &str) -> bool {
1481 match self.allowlist_mode {
1482 AllowlistMode::Permissive => {
1483 self.allowed_commands.is_empty() || self.allowed_commands.iter().any(|c| c == name)
1484 }
1485 AllowlistMode::Enforced => self.allowed_commands.iter().any(|c| c == name),
1486 }
1487 }
1488}
1489
1490impl Default for ExecConfig {
1491 fn default() -> Self {
1492 Self {
1493 default_mode: ExecMode::default(),
1494 allow_shell_mode: default_false(),
1495 allowed_commands: Vec::new(),
1496 allowlist_mode: AllowlistMode::default(),
1497 default_timeout_secs: default_exec_timeout(),
1498 max_timeout_secs: default_exec_max_timeout(),
1499 }
1500 }
1501}
1502
1503#[derive(Debug, Clone, Deserialize, Serialize)]
1505pub struct OrchestratorConfig {
1506 #[serde(default = "default_max_evolution_iterations")]
1509 pub max_evolution_iterations: u32,
1510
1511 #[serde(default = "default_min_evaluation_score")]
1514 pub min_evaluation_score: f64,
1515}
1516
1517fn default_max_evolution_iterations() -> u32 {
1518 3
1519}
1520
1521fn default_min_evaluation_score() -> f64 {
1522 0.8
1523}
1524
1525impl Default for OrchestratorConfig {
1526 fn default() -> Self {
1527 Self {
1528 max_evolution_iterations: default_max_evolution_iterations(),
1529 min_evaluation_score: default_min_evaluation_score(),
1530 }
1531 }
1532}
1533
1534#[derive(Debug, Clone, Serialize, Deserialize)]
1539pub struct IntentConfig {
1540 #[serde(default = "default_intent_max_retries")]
1544 pub max_retries: u32,
1545
1546 #[serde(default = "default_intent_score_threshold")]
1550 pub score_threshold: f64,
1551
1552 #[serde(default = "default_intent_max_clarify_rounds")]
1556 pub max_clarify_rounds: u32,
1557
1558 #[serde(default = "default_intent_enable_retry")]
1562 pub enable_retry: bool,
1563
1564 #[serde(default)]
1568 pub lightweight_model: Option<String>,
1569}
1570
1571fn default_intent_max_retries() -> u32 {
1572 2
1573}
1574
1575fn default_intent_score_threshold() -> f64 {
1576 0.7
1577}
1578
1579fn default_intent_max_clarify_rounds() -> u32 {
1580 3
1581}
1582
1583fn default_intent_enable_retry() -> bool {
1584 true
1585}
1586
1587impl Default for IntentConfig {
1588 fn default() -> Self {
1589 Self {
1590 max_retries: default_intent_max_retries(),
1591 score_threshold: default_intent_score_threshold(),
1592 max_clarify_rounds: default_intent_max_clarify_rounds(),
1593 enable_retry: default_intent_enable_retry(),
1594 lightweight_model: None,
1595 }
1596 }
1597}
1598
1599#[derive(Debug, Clone, Deserialize, Serialize)]
1601pub struct ContextConfig {
1602 #[serde(default = "default_active_limit")]
1604 pub active_limit_tokens: usize,
1605 #[serde(default = "default_cache_limit")]
1607 pub cache_limit_entries: usize,
1608}
1609
1610fn default_active_limit() -> usize {
1611 100_000
1612}
1613
1614fn default_cache_limit() -> usize {
1615 50
1616}
1617
1618impl Default for ContextConfig {
1619 fn default() -> Self {
1620 Self {
1621 active_limit_tokens: default_active_limit(),
1622 cache_limit_entries: default_cache_limit(),
1623 }
1624 }
1625}
1626
1627#[derive(Debug, Clone, Deserialize, Serialize)]
1629pub struct SecurityConfig {
1630 #[serde(default = "default_allowed_tools")]
1632 pub allowed_tools: Vec<String>,
1633 #[serde(default)]
1635 pub network_access: bool,
1636 #[serde(default = "default_max_exec_time")]
1638 pub max_execution_time_secs: u64,
1639 #[serde(default = "default_max_memory")]
1641 pub max_memory_mb: u64,
1642 #[serde(default)]
1644 pub can_fork: bool,
1645 #[serde(default = "default_max_audit")]
1647 pub max_audit_entries: usize,
1648 #[serde(default)]
1650 pub auth_enabled: bool,
1651 #[serde(default = "default_cors_origins")]
1653 pub cors_origins: Vec<String>,
1654 #[serde(default)]
1656 pub audit_log_path: Option<String>,
1657 #[serde(default = "default_rate_limit_per_minute")]
1659 pub rate_limit_per_minute: u32,
1660}
1661
1662fn default_allowed_tools() -> Vec<String> {
1663 vec![
1664 "read".to_string(),
1665 "write".to_string(),
1666 "edit".to_string(),
1667 "bash".to_string(),
1668 "grep".to_string(),
1669 "find".to_string(),
1670 "exec".to_string(),
1671 ]
1672}
1673
1674fn default_max_exec_time() -> u64 {
1675 300
1676}
1677
1678fn default_max_memory() -> u64 {
1679 512
1680}
1681
1682fn default_max_audit() -> usize {
1683 10_000
1684}
1685
1686fn default_rate_limit_per_minute() -> u32 {
1687 600
1691}
1692
1693fn default_cors_origins() -> Vec<String> {
1694 vec![
1699 "http://localhost:4200".to_string(),
1700 "http://127.0.0.1:4200".to_string(),
1701 "http://localhost:5173".to_string(),
1702 "http://127.0.0.1:5173".to_string(),
1703 ]
1704}
1705
1706impl Default for SecurityConfig {
1707 fn default() -> Self {
1708 Self {
1709 allowed_tools: default_allowed_tools(),
1710 network_access: false,
1711 max_execution_time_secs: default_max_exec_time(),
1712 max_memory_mb: default_max_memory(),
1713 can_fork: false,
1714 max_audit_entries: default_max_audit(),
1715 auth_enabled: false,
1716 cors_origins: default_cors_origins(),
1717 audit_log_path: None,
1718 rate_limit_per_minute: default_rate_limit_per_minute(),
1719 }
1720 }
1721}
1722
1723#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1728pub struct PersonaConfig {
1729 #[serde(default)]
1731 pub default_persona_id: Option<String>,
1732}
1733
1734#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1742pub struct McpConfig {
1743 #[serde(default)]
1745 pub servers: std::collections::HashMap<String, McpServerDef>,
1746}
1747
1748#[derive(Debug, Clone, Deserialize, Serialize)]
1750pub struct McpServerDef {
1751 pub command: String,
1753 #[serde(default)]
1755 pub args: Vec<String>,
1756 #[serde(default)]
1758 pub env: std::collections::HashMap<String, String>,
1759 #[serde(default = "default_mcp_enabled")]
1761 pub enabled: bool,
1762}
1763
1764fn default_mcp_enabled() -> bool {
1765 true
1766}
1767
1768#[derive(Debug, Clone, Deserialize, Serialize)]
1770pub struct GitConfig {
1771 #[serde(default = "default_true")]
1773 pub auto_commit: bool,
1774}
1775
1776impl Default for GitConfig {
1777 fn default() -> Self {
1778 Self { auto_commit: true }
1779 }
1780}
1781
1782#[derive(Debug, Clone, Deserialize, Serialize)]
1784pub struct AuditConfig {
1785 #[serde(default = "default_audit_max_entries")]
1787 pub max_entries: usize,
1788 #[serde(default = "default_true")]
1790 pub enabled: bool,
1791}
1792
1793fn default_audit_max_entries() -> usize {
1794 100_000
1795}
1796
1797impl Default for AuditConfig {
1798 fn default() -> Self {
1799 Self {
1800 max_entries: default_audit_max_entries(),
1801 enabled: true,
1802 }
1803 }
1804}
1805
1806#[derive(Debug, Clone, Deserialize, Serialize)]
1808pub struct BudgetConfig {
1809 #[serde(default)]
1811 pub default_token_budget: u64,
1812 #[serde(default)]
1814 pub default_calls_budget: u64,
1815 #[serde(default = "default_budget_window")]
1817 pub default_window_secs: u64,
1818 #[serde(default = "default_true")]
1820 pub enabled: bool,
1821 #[serde(default)]
1825 pub monthly_spend_limit_usd: Option<f64>,
1826}
1827
1828fn default_budget_window() -> u64 {
1829 3600
1830}
1831
1832impl Default for BudgetConfig {
1833 fn default() -> Self {
1834 Self {
1835 default_token_budget: 0,
1836 default_calls_budget: 0,
1837 default_window_secs: default_budget_window(),
1838 enabled: true,
1839 monthly_spend_limit_usd: None,
1840 }
1841 }
1842}
1843
1844#[derive(Debug, Clone, Deserialize, Serialize)]
1846pub struct ResourceMonitorConfig {
1847 #[serde(default = "default_rm_interval")]
1849 pub interval_secs: u64,
1850 #[serde(default = "default_rm_history_max")]
1852 pub history_max: usize,
1853 #[serde(default = "default_rm_cpu_threshold")]
1855 pub cpu_threshold: f32,
1856 #[serde(default = "default_rm_mem_threshold")]
1858 pub memory_threshold: f32,
1859 #[serde(default = "default_rm_load_threshold")]
1861 pub load_threshold: f32,
1862}
1863
1864fn default_rm_interval() -> u64 {
1865 60
1866}
1867
1868fn default_rm_history_max() -> usize {
1869 60
1870}
1871
1872fn default_rm_cpu_threshold() -> f32 {
1873 90.0
1874}
1875
1876fn default_rm_mem_threshold() -> f32 {
1877 90.0
1878}
1879
1880fn default_rm_load_threshold() -> f32 {
1881 8.0
1882}
1883
1884impl Default for ResourceMonitorConfig {
1885 fn default() -> Self {
1886 Self {
1887 interval_secs: default_rm_interval(),
1888 history_max: default_rm_history_max(),
1889 cpu_threshold: default_rm_cpu_threshold(),
1890 memory_threshold: default_rm_mem_threshold(),
1891 load_threshold: default_rm_load_threshold(),
1892 }
1893 }
1894}
1895
1896#[derive(Debug, Clone, Serialize, Deserialize)]
1898pub struct AgentLogConfig {
1899 #[serde(default = "default_agent_log_max_entries")]
1901 pub max_entries: usize,
1902 #[serde(default = "default_agent_log_ttl_hours")]
1904 pub ttl_hours: u64,
1905 #[serde(default = "default_agent_log_max_tool_calls")]
1907 pub max_tool_calls_per_agent: usize,
1908 #[serde(default = "default_agent_log_prune_batch")]
1910 pub prune_batch_size: usize,
1911 #[serde(default)]
1913 pub db_path: String,
1914}
1915
1916fn default_agent_log_max_entries() -> usize {
1917 10_000
1918}
1919fn default_agent_log_ttl_hours() -> u64 {
1920 720
1921}
1922fn default_agent_log_max_tool_calls() -> usize {
1923 500
1924}
1925fn default_agent_log_prune_batch() -> usize {
1926 100
1927}
1928
1929impl Default for AgentLogConfig {
1930 fn default() -> Self {
1931 Self {
1932 max_entries: 10_000,
1933 ttl_hours: 720,
1934 max_tool_calls_per_agent: 500,
1935 prune_batch_size: 100,
1936 db_path: String::new(),
1937 }
1938 }
1939}
1940
1941#[derive(Debug, Clone, Deserialize, Serialize)]
1943pub struct LoggingConfig {
1944 #[serde(default = "default_log_format")]
1946 pub format: String,
1947 #[serde(default)]
1949 pub level: Option<String>,
1950}
1951
1952fn default_log_format() -> String {
1953 "pretty".into()
1954}
1955
1956impl Default for LoggingConfig {
1957 fn default() -> Self {
1958 Self {
1959 format: default_log_format(),
1960 level: None,
1961 }
1962 }
1963}
1964
1965#[derive(Debug, Clone, Deserialize, Serialize)]
1971pub struct BrowserConfig {
1972 #[serde(default = "default_browser_enabled")]
1974 pub enabled: bool,
1975
1976 #[serde(default)]
1987 pub engine: serde_json::Value,
1988}
1989
1990fn default_browser_enabled() -> bool {
1991 true
1992}
1993
1994impl Default for BrowserConfig {
1995 fn default() -> Self {
1996 Self {
1997 enabled: true,
1998 engine: serde_json::json!({}),
1999 }
2000 }
2001}
2002
2003pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
2005 let content = std::fs::read_to_string(path)?;
2006 let config: OxiosConfig = toml::from_str(&content)?;
2007 let (errors, warnings) = config.validate();
2008 for w in warnings {
2009 tracing::warn!("config: {}", w);
2010 }
2011 if !errors.is_empty() {
2012 let msg = errors.join("; ");
2013 anyhow::bail!("Configuration validation failed: {msg}");
2014 }
2015 Ok(config)
2016}
2017
2018impl OxiosConfig {
2019 pub fn api_key(&self) -> Option<String> {
2021 self.engine.api_key.clone().filter(|k| !k.is_empty())
2022 }
2023
2024 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
2027 let mut errors = Vec::new();
2028 let mut warnings = Vec::new();
2029
2030 if self.kernel.max_agents == 0 {
2032 errors.push("kernel.max_agents must be > 0".into());
2033 }
2034 if self.kernel.workspace.is_empty() {
2035 errors.push("kernel.workspace must not be empty".into());
2036 }
2037
2038 if self.gateway.port == 0 {
2040 errors.push("gateway.port must be > 0".into());
2041 }
2042 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
2043 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
2044 }
2045
2046 for (name, job) in &self.cron.jobs {
2048 if job.schedule.is_empty() {
2049 errors.push(format!("cron.jobs.{name}: schedule is empty"));
2050 } else {
2051 let normalized = {
2053 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
2054 match fields.len() {
2055 5 => format!("0 {}", job.schedule),
2056 _ => job.schedule.clone(),
2057 }
2058 };
2059 if Schedule::from_str(&normalized).is_err() {
2060 errors.push(format!(
2061 "cron.jobs.{}: invalid cron expression '{}'",
2062 name, job.schedule
2063 ));
2064 }
2065 }
2066 if job.goal.is_empty() {
2067 errors.push(format!("cron.jobs.{name}: goal is empty"));
2068 }
2069 }
2070
2071 if self.security.max_execution_time_secs == 0 {
2073 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
2074 }
2075
2076 if self.audit.max_entries == 0 {
2078 warnings.push("audit.max_entries is 0 — audit will never prune".into());
2079 }
2080
2081 if self.budget.default_window_secs == 0 {
2083 warnings.push("budget.default_window_secs is 0 — no time window".into());
2084 }
2085
2086 if self.gateway.response_timeout_secs == 0 {
2088 errors.push("gateway.response_timeout_secs must be > 0".into());
2089 }
2090
2091 if self.engine.api_key.as_ref().is_some_and(|k| !k.is_empty()) {
2094 warnings.push(
2095 "engine.api_key is set in config — prefer the oxi auth store or env var to avoid storing a secret on disk"
2096 .into(),
2097 );
2098 }
2099
2100 for (name, server) in &self.mcp.servers {
2102 if server.command.trim().is_empty() {
2103 errors.push(format!("mcp.servers.{name}: command must not be empty"));
2104 }
2105 }
2106
2107 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
2109 {
2110 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
2111 }
2112
2113 if self.exec.default_timeout_secs == 0 {
2115 errors.push("exec.default_timeout_secs must be > 0".into());
2116 }
2117 if self.exec.max_timeout_secs == 0 {
2118 errors.push("exec.max_timeout_secs must be > 0".into());
2119 }
2120 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
2121 errors.push(format!(
2122 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
2123 self.exec.default_timeout_secs, self.exec.max_timeout_secs
2124 ));
2125 }
2126
2127 if self.resource_monitor.cpu_threshold > 100.0 {
2129 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
2130 }
2131 if self.resource_monitor.memory_threshold > 100.0 {
2132 errors.push("resource_monitor.memory_threshold must be <= 100".into());
2133 }
2134
2135 for name in &self.channels.enabled {
2137 let valid = ["cli", "telegram"];
2138 if !valid.contains(&name.as_str()) {
2139 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
2140 }
2141 }
2142 if self.channels.enabled.iter().any(|c| c == "web") {
2144 warnings.push(
2145 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
2146 );
2147 }
2148 if self.channels.enabled.iter().any(|c| c == "telegram")
2149 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
2150 {
2151 warnings.push(format!(
2152 "channels.telegram: {} env var not set — telegram channel will fail",
2153 self.channels.telegram.bot_token_env
2154 ));
2155 }
2156 for err in self.token_maxing.validate() {
2160 errors.push(err);
2161 }
2162
2163 (errors, warnings)
2164 }
2165}
2166
2167pub fn expand_home(path: &str) -> std::path::PathBuf {
2179 if let Some(rest) = path.strip_prefix("~/") {
2180 if let Ok(home) = std::env::var("HOME") {
2181 return std::path::PathBuf::from(format!("{home}/{rest}"));
2182 }
2183 if let Some(home) = dirs::home_dir() {
2184 return home.join(rest);
2185 }
2186 }
2187 std::path::PathBuf::from(path)
2188}
2189
2190#[cfg(test)]
2191mod tests {
2192 use super::*;
2193
2194 #[test]
2195 fn test_default_config_validates() {
2196 let config = OxiosConfig::default();
2197 let (errors, _warnings) = config.validate();
2198 assert!(
2199 errors.is_empty(),
2200 "Default config should have no errors: {:?}",
2201 errors
2202 );
2203 }
2204
2205 #[test]
2206 fn test_exec_config_default_allowed_commands() {
2207 let config = ExecConfig::default();
2208 assert!(config.allowed_commands.is_empty());
2210 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
2211 assert!(!config.is_binary_allowed("anything"));
2212 assert!(!config.is_binary_allowed("bash"));
2213 }
2214
2215 #[test]
2216 fn test_exec_config_permissive_mode() {
2217 let config = ExecConfig {
2218 allowlist_mode: AllowlistMode::Permissive,
2219 ..Default::default()
2220 };
2221 assert!(config.is_binary_allowed("anything"));
2223 assert!(config.is_binary_allowed("bash"));
2224 }
2225
2226 #[test]
2227 fn test_is_binary_allowed_with_allowlist() {
2228 let config = ExecConfig {
2229 allowed_commands: vec!["git".into(), "echo".into()],
2230 ..Default::default()
2231 };
2232 assert!(config.is_binary_allowed("git"));
2233 assert!(config.is_binary_allowed("echo"));
2234 assert!(!config.is_binary_allowed("bash"));
2235 assert!(!config.is_binary_allowed("rm"));
2236 assert!(!config.is_binary_allowed("sudo"));
2237 }
2238
2239 #[test]
2240 fn test_expand_home() {
2241 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
2243 let expanded = expand_home("~/projects/test");
2244 assert_eq!(
2245 expanded.to_str().unwrap(),
2246 format!("{}/projects/test", home)
2247 );
2248
2249 let abs = expand_home("/absolute/path");
2251 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
2252
2253 let bare = expand_home("~something");
2255 assert_eq!(bare, std::path::PathBuf::from("~something"));
2256 }
2257
2258 #[test]
2259 fn test_invalid_cron_expression() {
2260 let mut config = OxiosConfig::default();
2261 config.cron.enabled = true;
2262 config.cron.jobs.insert(
2263 "bad-job".to_string(),
2264 InlineCronJob {
2265 schedule: "not a valid cron".to_string(),
2266 goal: "Test goal".to_string(),
2267 constraints: vec![],
2268 acceptance_criteria: vec![],
2269 toolchain: "default".to_string(),
2270 priority: Priority::Normal,
2271 enabled: true,
2272 },
2273 );
2274
2275 let (errors, _warnings) = config.validate();
2276 assert!(
2277 !errors.is_empty(),
2278 "Expected validation error for invalid cron"
2279 );
2280 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
2281 assert!(
2282 has_cron_error,
2283 "Expected 'invalid cron expression' error, got: {:?}",
2284 errors
2285 );
2286 }
2287
2288 #[test]
2289 fn test_config_serialization_roundtrip() {
2290 let config = OxiosConfig::default();
2291
2292 let toml_str = toml::to_string(&config).expect("serialization should succeed");
2294
2295 let deserialized: OxiosConfig =
2297 toml::from_str(&toml_str).expect("deserialization should succeed");
2298
2299 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2301 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2302 assert_eq!(config.gateway.host, deserialized.gateway.host);
2303 assert_eq!(config.gateway.port, deserialized.gateway.port);
2304 assert_eq!(
2305 config.exec.default_timeout_secs,
2306 deserialized.exec.default_timeout_secs
2307 );
2308 assert_eq!(
2309 config.exec.max_timeout_secs,
2310 deserialized.exec.max_timeout_secs
2311 );
2312 }
2313
2314 #[test]
2315 fn test_exec_timeout_validation() {
2316 let mut config = OxiosConfig::default();
2317 config.exec.default_timeout_secs = 999;
2319 config.exec.max_timeout_secs = 100;
2320 let (errors, _warnings) = config.validate();
2321 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2322 assert!(
2323 has_error,
2324 "Expected timeout ordering error, got: {:?}",
2325 errors
2326 );
2327 }
2328
2329 #[test]
2330 fn test_zero_max_agents_error() {
2331 let mut config = OxiosConfig::default();
2332 config.kernel.max_agents = 0;
2333 let (errors, _warnings) = config.validate();
2334 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2335 }
2336
2337 #[test]
2342 fn test_default_config_matches_toml() {
2343 let from_rust = OxiosConfig::default();
2344
2345 let toml_str = include_str!("../../../share/default-config.toml");
2346 let from_toml: OxiosConfig =
2347 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2348
2349 assert_eq!(
2351 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2352 "kernel.max_agents 불일치: Rust={}, TOML={}",
2353 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2354 );
2355 assert_eq!(
2356 from_rust.gateway.host, from_toml.gateway.host,
2357 "gateway.host 불일치: Rust={}, TOML={}",
2358 from_rust.gateway.host, from_toml.gateway.host
2359 );
2360 assert_eq!(
2361 from_rust.gateway.port, from_toml.gateway.port,
2362 "gateway.port 불일치: Rust={}, TOML={}",
2363 from_rust.gateway.port, from_toml.gateway.port
2364 );
2365 assert_eq!(
2366 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2367 "kernel.event_bus_capacity 불일치"
2368 );
2369 assert_eq!(
2370 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2371 "memory.consolidation.preset 불일치"
2372 );
2373
2374 let (_, warnings) = from_toml.validate();
2376 for w in &warnings {
2377 eprintln!("default-config.toml 경고: {}", w);
2378 }
2379 }
2380
2381 #[test]
2384 fn test_gateway_should_expose_api_docs() {
2385 let cfg = GatewayConfig::default();
2387 assert!(!cfg.should_expose_api_docs());
2388
2389 let cfg = GatewayConfig {
2391 host: "0.0.0.0".into(),
2392 port: 4200,
2393 expose_api_docs: true,
2394 ..Default::default()
2395 };
2396 assert!(
2397 !cfg.should_expose_api_docs(),
2398 "public bind must not expose api docs even when opt-in is true"
2399 );
2400
2401 let cfg = GatewayConfig {
2403 host: "127.0.0.1".into(),
2404 port: 4200,
2405 expose_api_docs: true,
2406 ..Default::default()
2407 };
2408 assert!(cfg.should_expose_api_docs());
2409
2410 let cfg = GatewayConfig {
2412 host: "::1".into(),
2413 port: 4200,
2414 expose_api_docs: true,
2415 ..Default::default()
2416 };
2417 assert!(cfg.should_expose_api_docs());
2418
2419 let cfg = GatewayConfig {
2421 host: "localhost".into(),
2422 port: 4200,
2423 expose_api_docs: true,
2424 ..Default::default()
2425 };
2426 assert!(cfg.should_expose_api_docs());
2427
2428 let cfg = GatewayConfig {
2430 host: "127.0.0.1".into(),
2431 port: 4200,
2432 expose_api_docs: false,
2433 ..Default::default()
2434 };
2435 assert!(!cfg.should_expose_api_docs());
2436 }
2437}