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