1#![allow(missing_docs)]
2use cron::Schedule;
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11use crate::approval::ApprovalConfig;
12use crate::email::{SmtpProvider, SmtpTls};
13use crate::types::Priority;
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct CronConfig {
18 #[serde(default)]
20 pub enabled: bool,
21 #[serde(default = "default_tick_interval")]
23 pub tick_interval_secs: u64,
24 #[serde(default)]
26 pub jobs: std::collections::HashMap<String, InlineCronJob>,
27}
28
29impl Default for CronConfig {
30 fn default() -> Self {
31 Self {
32 enabled: false,
33 tick_interval_secs: default_tick_interval(),
34 jobs: std::collections::HashMap::new(),
35 }
36 }
37}
38
39fn default_tick_interval() -> u64 {
40 60
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
45pub struct InlineCronJob {
46 pub schedule: String,
48 pub goal: String,
50 #[serde(default)]
52 pub constraints: Vec<String>,
53 #[serde(default)]
55 pub acceptance_criteria: Vec<String>,
56 #[serde(default = "default_toolchain_inline")]
58 pub toolchain: String,
59 #[serde(default)]
61 pub priority: Priority,
62 #[serde(default = "default_true_inline")]
64 pub enabled: bool,
65}
66
67fn default_toolchain_inline() -> String {
68 "default".into()
69}
70
71fn default_true_inline() -> bool {
72 true
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct MemoryConfig {
78 #[serde(default = "default_true")]
80 pub enabled: bool,
81 #[serde(default = "default_max_recall")]
83 pub max_recall: usize,
84 #[serde(default = "default_true")]
86 pub auto_summarize: bool,
87 #[serde(default = "default_true")]
89 pub capture_compaction: bool,
90 #[serde(default)]
92 pub retention_days: u32,
93 #[serde(default = "default_true")]
95 pub cache_enabled: bool,
96 #[serde(default = "default_cache_ttl")]
98 pub cache_ttl_secs: u64,
99 #[serde(default = "default_cache_max_entries")]
101 pub cache_max_entries: usize,
102 #[serde(default)]
104 pub consolidation: ConsolidationConfig,
105 #[serde(default)]
107 pub sqlite: SqliteMemoryConfig,
108 #[serde(default)]
110 pub embedding: EmbeddingConfig,
111 #[serde(default)]
113 pub learning: LearningConfig,
114 #[serde(default)]
116 pub knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig,
117 #[serde(default)]
119 pub bridge: MemoryBridgeConfig,
120}
121
122fn default_true() -> bool {
123 true
124}
125
126fn default_max_recall() -> usize {
127 10
128}
129
130fn default_cache_ttl() -> u64 {
131 3600 }
133
134fn default_cache_max_entries() -> usize {
135 10000
136}
137
138impl Default for MemoryConfig {
139 fn default() -> Self {
140 Self {
141 enabled: true,
142 max_recall: 10,
143 auto_summarize: true,
144 capture_compaction: true,
145 retention_days: 0,
146 cache_enabled: true,
147 cache_ttl_secs: 3600,
148 cache_max_entries: 10000,
149 consolidation: ConsolidationConfig::default(),
150 sqlite: SqliteMemoryConfig::default(),
151 embedding: EmbeddingConfig::default(),
152 learning: LearningConfig::default(),
153 knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig::default(),
154 bridge: MemoryBridgeConfig::default(),
155 }
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct SqliteMemoryConfig {
170 #[serde(default = "default_true")]
172 pub enabled: bool,
173 #[serde(default)]
176 pub path: String,
177 #[serde(default = "default_embedding_dim")]
181 pub embedding_dim: usize,
182 #[serde(default = "default_true")]
184 pub wal_mode: bool,
185}
186
187fn default_embedding_dim() -> usize {
188 256
189}
190
191impl Default for SqliteMemoryConfig {
192 fn default() -> Self {
193 Self {
194 enabled: true,
195 path: String::new(),
196 embedding_dim: 256,
197 wal_mode: true,
198 }
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct EmbeddingConfig {
215 #[serde(default = "default_embedding_provider")]
217 pub provider: String,
218 #[serde(default = "default_embedding_dim")]
222 pub dimension: usize,
223 #[serde(default = "default_model_ttl")]
226 pub model_ttl_secs: u64,
227 #[serde(default)]
230 pub api_endpoint: String,
231 #[serde(default)]
234 pub api_key: String,
235 #[serde(default)]
238 pub api_model: String,
239}
240
241fn default_embedding_provider() -> String {
242 "tfidf".to_string()
245}
246
247fn default_model_ttl() -> u64 {
248 300 }
250
251impl Default for EmbeddingConfig {
252 fn default() -> Self {
253 Self {
254 provider: default_embedding_provider(),
255 dimension: default_embedding_dim(),
256 model_ttl_secs: default_model_ttl(),
257 api_endpoint: String::new(),
258 api_key: String::new(),
259 api_model: String::new(),
260 }
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct LearningConfig {
273 #[serde(default = "default_true")]
275 pub enabled: bool,
276 #[serde(default = "default_sona_mode")]
278 pub sona_mode: String,
279 #[serde(default = "default_distill_interval")]
281 pub distill_interval_hours: u64,
282 #[serde(default = "default_auto_promote_quality")]
284 pub auto_promote_quality: f32,
285 #[serde(default = "default_auto_promote_min_usage")]
287 pub auto_promote_min_usage: u32,
288}
289
290fn default_sona_mode() -> String {
291 "balanced".to_string()
292}
293
294fn default_distill_interval() -> u64 {
295 6
296}
297
298fn default_auto_promote_quality() -> f32 {
299 0.8
300}
301
302fn default_auto_promote_min_usage() -> u32 {
303 3
304}
305
306impl Default for LearningConfig {
307 fn default() -> Self {
308 Self {
309 enabled: true,
310 sona_mode: default_sona_mode(),
311 distill_interval_hours: default_distill_interval(),
312 auto_promote_quality: default_auto_promote_quality(),
313 auto_promote_min_usage: default_auto_promote_min_usage(),
314 }
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct MemoryBridgeConfig {
328 #[serde(default)]
330 pub sync_enabled: bool,
331 #[serde(default = "default_bridge_interval")]
333 pub interval_secs: u64,
334}
335
336fn default_bridge_interval() -> u64 {
337 3600
338}
339
340impl Default for MemoryBridgeConfig {
341 fn default() -> Self {
342 Self {
343 sync_enabled: false,
344 interval_secs: default_bridge_interval(),
345 }
346 }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ConsolidationConfig {
357 #[serde(default = "default_preset")]
361 pub preset: String,
362
363 #[serde(default = "default_true")]
365 pub dream_enabled: bool,
366 #[serde(default = "default_dream_interval")]
367 pub dream_interval_hours: u64,
368 #[serde(default = "default_dream_min_sessions")]
369 pub dream_min_sessions: u32,
370
371 #[serde(default = "default_hot_max")]
373 pub hot_max_entries: usize,
374 #[serde(default = "default_warm_max")]
375 pub warm_max_entries: usize,
376 #[serde(default = "default_cold_max")]
377 pub cold_max_entries: usize,
378 #[serde(default = "default_hot_token_budget")]
379 pub hot_token_budget: usize,
380
381 #[serde(default = "default_true")]
383 pub decay_enabled: bool,
384 #[serde(default = "default_one")]
385 pub decay_multiplier: f32,
386 #[serde(default = "default_decay_threshold")]
387 pub decay_threshold: f32,
388 #[serde(default = "default_retention_days")]
389 pub retention_days: u32,
390
391 #[serde(default = "default_true")]
393 pub auto_protection: bool,
394 #[serde(default = "default_protection_low_access")]
395 pub protection_low_access: u32,
396 #[serde(default = "default_protection_medium_access")]
397 pub protection_medium_access: u32,
398 #[serde(default = "default_protection_high_access")]
399 pub protection_high_access: u32,
400 #[serde(default = "default_protection_medium_sessions")]
401 pub protection_medium_sessions: u32,
402 #[serde(default = "default_protection_high_sessions")]
403 pub protection_high_sessions: u32,
404
405 #[serde(default = "default_true")]
407 pub auto_classification: bool,
408 #[serde(default = "default_type_promotion_threshold")]
409 pub type_promotion_repetitions: u32,
410
411 #[serde(default = "default_compaction_threshold")]
413 pub compaction_line_threshold: usize,
414 #[serde(default = "default_true")]
415 pub llm_compaction: bool,
416
417 #[serde(default)]
420 pub dream_model: Option<String>,
421
422 #[serde(default = "default_true")]
424 pub protection_demotion_enabled: bool,
425 #[serde(default = "default_demotion_stale_days")]
426 pub protection_demotion_stale_days: u32,
427 #[serde(default = "default_demotion_max_step")]
428 pub protection_demotion_max_step: u32,
429
430 #[serde(default = "default_true")]
432 pub proactive_recall: bool,
433 #[serde(default = "default_proactive_limit")]
434 pub proactive_recall_limit: usize,
435 #[serde(default = "default_proactive_threshold")]
436 pub proactive_recall_threshold: f32,
437}
438
439fn default_dream_interval() -> u64 {
440 24
441}
442fn default_dream_min_sessions() -> u32 {
443 5
444}
445fn default_hot_max() -> usize {
446 50
447}
448fn default_warm_max() -> usize {
449 500
450}
451fn default_cold_max() -> usize {
452 10_000
453}
454fn default_hot_token_budget() -> usize {
455 3_000
456}
457fn default_one() -> f32 {
458 1.0
459}
460fn default_decay_threshold() -> f32 {
461 0.05
462}
463fn default_retention_days() -> u32 {
464 90
465}
466fn default_protection_low_access() -> u32 {
467 2
468}
469fn default_protection_medium_access() -> u32 {
470 3
471}
472fn default_protection_high_access() -> u32 {
473 5
474}
475fn default_protection_medium_sessions() -> u32 {
476 2
477}
478fn default_protection_high_sessions() -> u32 {
479 3
480}
481fn default_type_promotion_threshold() -> u32 {
482 3
483}
484fn default_compaction_threshold() -> usize {
485 200
486}
487fn default_proactive_limit() -> usize {
488 5
489}
490fn default_proactive_threshold() -> f32 {
491 0.6
492}
493fn default_demotion_stale_days() -> u32 {
494 30
495}
496fn default_demotion_max_step() -> u32 {
497 1
498}
499
500fn default_preset() -> String {
501 "balanced".into()
502}
503
504impl Default for ConsolidationConfig {
505 fn default() -> Self {
506 Self {
507 preset: default_preset(),
508 dream_enabled: true,
509 dream_interval_hours: 24,
510 dream_min_sessions: 5,
511 hot_max_entries: 50,
512 warm_max_entries: 500,
513 cold_max_entries: 10_000,
514 hot_token_budget: 3_000,
515 decay_enabled: true,
516 decay_multiplier: 1.0,
517 decay_threshold: 0.05,
518 retention_days: 90,
519 auto_protection: true,
520 protection_low_access: 2,
521 protection_medium_access: 3,
522 protection_high_access: 5,
523 protection_medium_sessions: 2,
524 protection_high_sessions: 3,
525 auto_classification: true,
526 type_promotion_repetitions: 3,
527 compaction_line_threshold: 200,
528 llm_compaction: true,
529 dream_model: None,
530 protection_demotion_enabled: true,
531 protection_demotion_stale_days: 30,
532 protection_demotion_max_step: 1,
533 proactive_recall: true,
534 proactive_recall_limit: 5,
535 proactive_recall_threshold: 0.6,
536 }
537 }
538}
539
540impl ConsolidationConfig {
541 pub fn apply_preset(&mut self) {
545 let resolved = match self.preset.as_str() {
546 "conservative" => Self::conservative(),
547 "aggressive" => Self::aggressive(),
548 "custom" => return,
549 _ => Self::default(), };
551 *self = resolved;
552 }
553
554 fn conservative() -> Self {
556 Self {
557 preset: "conservative".into(),
558 dream_enabled: true,
559 dream_interval_hours: 48,
560 dream_min_sessions: 10,
561 hot_max_entries: 100,
562 warm_max_entries: 1000,
563 cold_max_entries: 50_000,
564 hot_token_budget: 5_000,
565 decay_enabled: true,
566 decay_multiplier: 0.8,
567 decay_threshold: 0.05,
568 retention_days: 365,
569 auto_protection: true,
570 protection_low_access: 3,
571 protection_medium_access: 5,
572 protection_high_access: 10,
573 protection_medium_sessions: 3,
574 protection_high_sessions: 5,
575 auto_classification: true,
576 type_promotion_repetitions: 5,
577 compaction_line_threshold: 300,
578 llm_compaction: true,
579 dream_model: None,
580 protection_demotion_enabled: true,
581 protection_demotion_stale_days: 90,
582 protection_demotion_max_step: 1,
583 proactive_recall: true,
584 proactive_recall_limit: 8,
585 proactive_recall_threshold: 0.5,
586 }
587 }
588
589 fn aggressive() -> Self {
591 Self {
592 preset: "aggressive".into(),
593 dream_enabled: true,
594 dream_interval_hours: 4,
595 dream_min_sessions: 2,
596 hot_max_entries: 20,
597 warm_max_entries: 100,
598 cold_max_entries: 1_000,
599 hot_token_budget: 2_000,
600 decay_enabled: true,
601 decay_multiplier: 1.0,
602 decay_threshold: 0.1,
603 retention_days: 30,
604 auto_protection: true,
605 protection_low_access: 1,
606 protection_medium_access: 2,
607 protection_high_access: 3,
608 protection_medium_sessions: 1,
609 protection_high_sessions: 2,
610 auto_classification: true,
611 type_promotion_repetitions: 2,
612 compaction_line_threshold: 150,
613 llm_compaction: true,
614 dream_model: None,
615 protection_demotion_enabled: true,
616 protection_demotion_stale_days: 14,
617 protection_demotion_max_step: 2,
618 proactive_recall: true,
619 proactive_recall_limit: 3,
620 proactive_recall_threshold: 0.7,
621 }
622 }
623}
624
625#[derive(Debug, Clone, Deserialize, Serialize, Default)]
627pub struct ChannelsConfig {
628 #[serde(default)]
631 pub enabled: Vec<String>,
632
633 #[serde(default)]
635 pub telegram: TelegramChannelConfig,
636}
637
638#[derive(Debug, Clone, Deserialize, Serialize)]
643pub struct SurfacesConfig {
644 #[serde(default = "default_surfaces_enabled")]
647 pub enabled: Vec<String>,
648}
649
650fn default_surfaces_enabled() -> Vec<String> {
651 vec!["web".to_string()]
652}
653
654impl Default for SurfacesConfig {
655 fn default() -> Self {
656 Self {
657 enabled: default_surfaces_enabled(),
658 }
659 }
660}
661
662#[derive(Debug, Clone, Deserialize, Serialize)]
664pub struct TelegramChannelConfig {
665 #[serde(default = "default_telegram_token_env")]
667 pub bot_token_env: String,
668 #[serde(default)]
670 pub allowed_users: Vec<i64>,
671 #[serde(default)]
673 pub session: TelegramSessionConfig,
674}
675
676fn default_telegram_token_env() -> String {
677 "TELEGRAM_BOT_TOKEN".to_string()
678}
679
680impl Default for TelegramChannelConfig {
681 fn default() -> Self {
682 Self {
683 bot_token_env: default_telegram_token_env(),
684 allowed_users: Vec::new(),
685 session: TelegramSessionConfig::default(),
686 }
687 }
688}
689#[derive(Debug, Clone, Serialize, Deserialize, Default)]
693pub struct RoleRoutingConfig {
694 #[serde(default)]
696 pub roles: std::collections::HashMap<String, String>,
697}
698
699#[derive(Debug, Clone, Deserialize, Serialize)]
701#[allow(clippy::derivable_impls)]
702pub struct EngineConfig {
703 #[serde(default)]
706 pub default_model: String,
707 #[serde(default, skip_serializing)]
711 pub api_key: Option<String>,
712 #[serde(default)]
715 pub provider_options: Option<oxi_sdk::ProviderOptions>,
716 #[serde(default)]
720 pub routing_enabled: bool,
721 #[serde(default)]
723 pub prefer_cost_efficient: bool,
724 #[serde(default)]
726 pub fallback_models: Vec<String>,
727 #[serde(default)]
729 pub excluded_models: Vec<String>,
730 #[serde(default)]
734 pub role_routing: RoleRoutingConfig,
735 #[serde(default)]
739 pub quick_ask_model: Option<String>,
740}
741
742#[allow(clippy::derivable_impls)]
743impl Default for EngineConfig {
744 fn default() -> Self {
745 Self {
746 default_model: String::new(),
747 api_key: None,
748 provider_options: None,
749 routing_enabled: false,
750 prefer_cost_efficient: false,
751 fallback_models: Vec::new(),
752 excluded_models: Vec::new(),
753 role_routing: RoleRoutingConfig::default(),
754 quick_ask_model: None,
755 }
756 }
757}
758
759#[derive(Debug, Clone, Deserialize, Serialize)]
761pub struct DaemonConfig {
762 #[serde(default = "default_pid_file")]
764 pub pid_file: String,
765 #[serde(default = "default_daemon_log_dir")]
767 pub log_dir: String,
768}
769
770fn default_pid_file() -> String {
771 dirs::home_dir()
772 .map(|h| format!("{}/.oxios/oxios.pid", h.display()))
773 .unwrap_or_else(|| "./oxios.pid".into())
774}
775
776fn default_daemon_log_dir() -> String {
777 dirs::home_dir()
778 .map(|h| format!("{}/.oxios/logs", h.display()))
779 .unwrap_or_else(|| "./logs".into())
780}
781
782impl Default for DaemonConfig {
783 fn default() -> Self {
784 Self {
785 pid_file: default_pid_file(),
786 log_dir: default_daemon_log_dir(),
787 }
788 }
789}
790
791#[derive(Debug, Clone, Deserialize, Serialize)]
793pub struct SessionConfig {
794 #[serde(default = "default_max_sessions")]
798 pub max_sessions: usize,
799
800 #[serde(default = "default_session_ttl_hours")]
804 pub ttl_hours: u64,
805
806 #[serde(default = "default_true")]
808 pub auto_prune: bool,
809}
810
811fn default_max_sessions() -> usize {
812 100
813}
814
815fn default_session_ttl_hours() -> u64 {
816 168 }
818
819impl Default for SessionConfig {
820 fn default() -> Self {
821 Self {
822 max_sessions: default_max_sessions(),
823 ttl_hours: default_session_ttl_hours(),
824 auto_prune: true,
825 }
826 }
827}
828
829#[derive(Debug, Clone, Deserialize, Serialize)]
833pub struct MountsConfig {
834 #[serde(default = "default_true")]
836 pub auto_promote_enabled: bool,
837 #[serde(default = "default_promote_threshold")]
839 pub auto_promote_threshold: usize,
840 #[serde(default = "default_promote_window_days")]
842 pub auto_promote_window_days: i64,
843 #[serde(default = "default_promote_interval_secs")]
845 pub auto_promote_interval_secs: u64,
846}
847
848fn default_promote_threshold() -> usize {
849 3
850}
851
852fn default_promote_window_days() -> i64 {
853 14
854}
855
856fn default_promote_interval_secs() -> u64 {
857 3600 }
859
860impl Default for MountsConfig {
861 fn default() -> Self {
862 Self {
863 auto_promote_enabled: true,
864 auto_promote_threshold: default_promote_threshold(),
865 auto_promote_window_days: default_promote_window_days(),
866 auto_promote_interval_secs: default_promote_interval_secs(),
867 }
868 }
869}
870
871#[derive(Debug, Clone, Deserialize, Serialize)]
873pub struct TelegramSessionConfig {
874 #[serde(default = "default_telegram_session_rotation_hours")]
877 pub rotation_hours: u64,
878
879 #[serde(default = "default_telegram_session_max_messages")]
882 pub max_messages: usize,
883}
884
885fn default_telegram_session_rotation_hours() -> u64 {
886 2 }
888
889fn default_telegram_session_max_messages() -> usize {
890 0 }
892
893impl Default for TelegramSessionConfig {
894 fn default() -> Self {
895 Self {
896 rotation_hours: default_telegram_session_rotation_hours(),
897 max_messages: default_telegram_session_max_messages(),
898 }
899 }
900}
901
902#[derive(Debug, Clone, Deserialize, Serialize, Default)]
906pub struct SystemAgentItem {
907 #[serde(default)]
909 pub model: String,
910 #[serde(default = "default_true")]
912 pub enabled: bool,
913 #[serde(default)]
915 pub context_limit: Option<u32>,
916 #[serde(default)]
918 pub custom_prompt: Option<String>,
919}
920
921#[derive(Debug, Clone, Deserialize, Serialize, Default)]
924pub struct SystemAgentsConfig {
925 #[serde(default)]
927 pub topic: SystemAgentItem,
928 #[serde(default)]
930 pub generation_topic: SystemAgentItem,
931 #[serde(default)]
933 pub translation: SystemAgentItem,
934 #[serde(default)]
936 pub history_compress: SystemAgentItem,
937 #[serde(default)]
939 pub agent_meta: SystemAgentItem,
940 #[serde(default)]
942 pub follow_up_action: SystemAgentItem,
943 #[serde(default)]
945 pub input_completion: SystemAgentItem,
946 #[serde(default)]
948 pub prompt_rewrite: SystemAgentItem,
949 #[serde(default)]
951 pub memory_analysis: SystemAgentItem,
952 #[serde(default)]
954 pub memory_embedding: SystemAgentItem,
955 #[serde(default)]
957 pub memory_persona_writer: SystemAgentItem,
958}
959
960impl SystemAgentsConfig {
961 pub fn model_for_task(&self, task: &str) -> Option<String> {
963 let item = match task {
964 "topic" => &self.topic,
965 "generation_topic" => &self.generation_topic,
966 "translation" => &self.translation,
967 "history_compress" => &self.history_compress,
968 "agent_meta" => &self.agent_meta,
969 "follow_up_action" => &self.follow_up_action,
970 "input_completion" => &self.input_completion,
971 "prompt_rewrite" => &self.prompt_rewrite,
972 "memory_analysis" => &self.memory_analysis,
973 "memory_embedding" => &self.memory_embedding,
974 "memory_persona_writer" => &self.memory_persona_writer,
975 _ => return None,
976 };
977 if !item.enabled || item.model.is_empty() {
978 None
979 } else {
980 Some(item.model.clone())
981 }
982 }
983
984 pub fn is_enabled(&self, task: &str) -> bool {
986 self.model_for_task(task).is_some()
987 || match task {
988 "topic" => self.topic.enabled,
989 "generation_topic" => self.generation_topic.enabled,
990 "translation" => self.translation.enabled,
991 "history_compress" => self.history_compress.enabled,
992 "agent_meta" => self.agent_meta.enabled,
993 "follow_up_action" => self.follow_up_action.enabled,
994 "input_completion" => self.input_completion.enabled,
995 "prompt_rewrite" => self.prompt_rewrite.enabled,
996 "memory_analysis" => self.memory_analysis.enabled,
997 "memory_embedding" => self.memory_embedding.enabled,
998 "memory_persona_writer" => self.memory_persona_writer.enabled,
999 _ => false,
1000 }
1001 }
1002}
1003
1004#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1005pub struct OxiosConfig {
1006 pub kernel: KernelConfig,
1008 #[serde(default)]
1010 pub engine: EngineConfig,
1011 #[serde(default)]
1013 pub daemon: DaemonConfig,
1014 #[serde(default)]
1016 pub gateway: GatewayConfig,
1017 #[serde(default)]
1019 pub orchestrator: OrchestratorConfig,
1020 #[serde(default)]
1022 pub intent: IntentConfig,
1023 #[serde(default)]
1025 pub system_agents: SystemAgentsConfig,
1026 #[serde(default)]
1028 pub context: ContextConfig,
1029 #[serde(default)]
1031 pub security: SecurityConfig,
1032 #[serde(default)]
1034 pub persona: PersonaConfig,
1035 #[serde(default)]
1037 pub memory: MemoryConfig,
1038 #[serde(default)]
1040 pub cron: CronConfig,
1041 #[serde(default)]
1043 pub mcp: McpConfig,
1044 #[serde(default)]
1046 pub git: GitConfig,
1047 #[serde(default)]
1049 pub audit: AuditConfig,
1050 #[serde(default)]
1052 pub budget: BudgetConfig,
1053 #[serde(default)]
1055 pub exec: ExecConfig,
1056 #[serde(default)]
1058 pub resource_monitor: ResourceMonitorConfig,
1059 #[serde(default)]
1061 pub logging: LoggingConfig,
1062 #[serde(default)]
1064 pub channels: ChannelsConfig,
1065 #[serde(default)]
1067 pub surfaces: Option<SurfacesConfig>,
1068 #[serde(default)]
1070 pub browser: BrowserConfig,
1071 #[serde(default)]
1073 pub session: SessionConfig,
1074 #[serde(default)]
1076 pub mounts: MountsConfig,
1077 #[serde(default)]
1079 pub marketplace: MarketplaceConfig,
1080 #[serde(default)]
1082 pub calendar: CalendarConfig,
1083 #[serde(default)]
1085 pub email: EmailConfig,
1086 #[serde(default)]
1088 pub agent_log: AgentLogConfig,
1089 #[serde(default)]
1091 pub token_maxing: crate::token_maxing::TokenMaxingConfig,
1092 #[serde(default)]
1094 pub image_gen: ImageGenConfig,
1095}
1096
1097#[derive(Debug, Clone, Deserialize, Serialize)]
1105pub struct ImageGenConfig {
1106 #[serde(default)]
1108 pub enabled: bool,
1109 #[serde(default = "default_image_gen_provider")]
1111 pub provider: String,
1112 #[serde(default = "default_image_gen_base_url")]
1114 pub base_url: String,
1115 #[serde(default)]
1118 pub default_model: String,
1119 #[serde(default = "default_image_gen_num")]
1121 pub default_num: u8,
1122}
1123
1124impl Default for ImageGenConfig {
1125 fn default() -> Self {
1126 Self {
1127 enabled: false,
1128 provider: default_image_gen_provider(),
1129 base_url: default_image_gen_base_url(),
1130 default_model: String::new(),
1131 default_num: default_image_gen_num(),
1132 }
1133 }
1134}
1135
1136fn default_image_gen_provider() -> String {
1137 "openai".into()
1138}
1139
1140fn default_image_gen_base_url() -> String {
1141 "https://api.openai.com/v1".into()
1142}
1143
1144fn default_image_gen_num() -> u8 {
1145 1
1146}
1147
1148#[derive(Debug, Clone, Deserialize, Serialize)]
1150pub struct KernelConfig {
1151 #[serde(default = "default_workspace")]
1153 pub workspace: String,
1154 #[serde(default = "default_event_bus_capacity")]
1156 pub event_bus_capacity: usize,
1157 #[serde(default = "default_max_agents")]
1159 pub max_agents: usize,
1160}
1161
1162fn default_workspace() -> String {
1163 dirs_home().unwrap_or_else(|| ".".into())
1164}
1165
1166fn dirs_home() -> Option<String> {
1167 dirs::home_dir().map(|h| format!("{}/.oxios/workspace", h.display()))
1168}
1169
1170fn default_event_bus_capacity() -> usize {
1171 256
1172}
1173
1174fn default_max_agents() -> usize {
1175 10
1176}
1177
1178impl Default for KernelConfig {
1179 fn default() -> Self {
1180 Self {
1181 workspace: default_workspace(),
1182 event_bus_capacity: default_event_bus_capacity(),
1183 max_agents: 10,
1184 }
1185 }
1186}
1187
1188#[derive(Debug, Clone, Deserialize, Serialize)]
1190pub struct GatewayConfig {
1191 #[serde(default = "default_gateway_host")]
1193 pub host: String,
1194 #[serde(default = "default_gateway_port")]
1196 pub port: u16,
1197 #[serde(default)]
1207 pub expose_api_docs: bool,
1208 #[serde(default = "default_response_timeout_secs")]
1212 pub response_timeout_secs: u64,
1213 #[serde(default)]
1215 pub reliability: GatewayReliabilityConfig,
1216}
1217
1218#[derive(Debug, Clone, Serialize, Deserialize)]
1220pub struct GatewayReliabilityConfig {
1221 #[serde(default = "default_replay_buffer_size")]
1224 pub replay_buffer_size: usize,
1225 #[serde(default = "default_replay_ttl_secs")]
1227 pub replay_ttl_secs: u64,
1228}
1229
1230impl Default for GatewayReliabilityConfig {
1231 fn default() -> Self {
1232 Self {
1233 replay_buffer_size: default_replay_buffer_size(),
1234 replay_ttl_secs: default_replay_ttl_secs(),
1235 }
1236 }
1237}
1238
1239fn default_response_timeout_secs() -> u64 {
1240 120
1241}
1242fn default_replay_buffer_size() -> usize {
1243 512
1244}
1245fn default_replay_ttl_secs() -> u64 {
1246 60
1247}
1248
1249impl GatewayConfig {
1250 pub fn should_expose_api_docs(&self) -> bool {
1256 if !self.expose_api_docs {
1257 return false;
1258 }
1259 let h = self.host.trim();
1260 h == "127.0.0.1" || h == "::1" || h == "localhost" || h.starts_with("127.")
1261 }
1262}
1263
1264#[derive(Debug, Clone, Deserialize, Serialize)]
1266pub struct MarketplaceConfig {
1267 #[serde(default)]
1270 pub base_url: Option<String>,
1271 #[serde(default = "default_true")]
1273 pub enabled: bool,
1274 #[serde(default)]
1276 pub skills_sh: SkillsShConfig,
1277}
1278
1279#[derive(Debug, Clone, Deserialize, Serialize)]
1281pub struct SkillsShConfig {
1282 #[serde(default)]
1285 pub base_url: Option<String>,
1286 #[serde(default)]
1289 pub api_key: Option<String>,
1290 #[serde(default = "default_true")]
1292 pub enabled: bool,
1293}
1294
1295impl Default for MarketplaceConfig {
1296 fn default() -> Self {
1297 Self {
1298 base_url: Some("https://clawhub.ai".to_string()),
1299 enabled: true,
1300 skills_sh: SkillsShConfig::default(),
1301 }
1302 }
1303}
1304
1305impl Default for SkillsShConfig {
1306 fn default() -> Self {
1307 Self {
1308 base_url: None,
1309 api_key: None,
1310 enabled: true,
1311 }
1312 }
1313}
1314
1315#[derive(Debug, Clone, Deserialize, Serialize)]
1317pub struct CalendarConfig {
1318 #[serde(default = "default_true")]
1320 pub enabled: bool,
1321 #[serde(default = "default_calendar_timezone")]
1323 pub timezone: String,
1324 #[serde(default = "default_reminder_minutes")]
1326 pub default_reminder_minutes: Vec<u32>,
1327 #[serde(default)]
1329 pub alarm_channels: Vec<String>,
1330 #[serde(default = "default_journal_sync")]
1332 pub journal_sync: String,
1333 #[serde(default = "default_true")]
1335 pub system_calendar: bool,
1336 #[serde(default = "default_archive_days")]
1338 pub archive_after_days: u32,
1339}
1340
1341fn default_calendar_timezone() -> String {
1342 "Asia/Seoul".to_string()
1343}
1344
1345fn default_reminder_minutes() -> Vec<u32> {
1346 vec![15]
1347}
1348
1349fn default_journal_sync() -> String {
1350 "on_open".to_string()
1351}
1352
1353fn default_archive_days() -> u32 {
1354 365
1355}
1356
1357impl Default for CalendarConfig {
1358 fn default() -> Self {
1359 Self {
1360 enabled: true,
1361 timezone: default_calendar_timezone(),
1362 default_reminder_minutes: default_reminder_minutes(),
1363 alarm_channels: vec![],
1364 journal_sync: default_journal_sync(),
1365 system_calendar: true,
1366 archive_after_days: default_archive_days(),
1367 }
1368 }
1369}
1370
1371#[derive(Debug, Clone, Deserialize, Serialize)]
1376pub struct EmailConfig {
1377 #[serde(default)]
1379 pub enabled: bool,
1380 #[serde(default)]
1382 pub my_email: String,
1383 #[serde(default = "default_email_provider")]
1385 pub provider: SmtpProvider,
1386 #[serde(default)]
1388 pub host: String,
1389 #[serde(default)]
1391 pub port: u16,
1392 #[serde(default)]
1394 pub tls: Option<SmtpTls>,
1395 #[serde(default)]
1397 pub user: String,
1398 #[serde(default = "default_email_secret_ref")]
1401 pub secret_ref: String,
1402 #[serde(default = "default_rate_limit_emails")]
1404 pub rate_limit_per_hour: usize,
1405}
1406
1407fn default_email_provider() -> SmtpProvider {
1408 SmtpProvider::Gmail
1409}
1410
1411fn default_email_secret_ref() -> String {
1412 "email_smtp".to_string()
1413}
1414
1415fn default_rate_limit_emails() -> usize {
1416 10
1417}
1418
1419impl Default for EmailConfig {
1420 fn default() -> Self {
1421 Self {
1422 enabled: false,
1423 my_email: String::new(),
1424 provider: default_email_provider(),
1425 host: String::new(),
1426 port: 0,
1427 tls: None,
1428 user: String::new(),
1429 secret_ref: default_email_secret_ref(),
1430 rate_limit_per_hour: default_rate_limit_emails(),
1431 }
1432 }
1433}
1434
1435impl EmailConfig {
1436 pub fn provider(&self) -> SmtpProvider {
1438 self.provider
1439 }
1440}
1441
1442fn default_gateway_host() -> String {
1443 "127.0.0.1".into()
1444}
1445
1446fn default_gateway_port() -> u16 {
1447 4200
1448}
1449
1450impl Default for GatewayConfig {
1451 fn default() -> Self {
1452 Self {
1453 host: default_gateway_host(),
1454 port: default_gateway_port(),
1455 expose_api_docs: false,
1456 response_timeout_secs: default_response_timeout_secs(),
1457 reliability: GatewayReliabilityConfig::default(),
1458 }
1459 }
1460}
1461
1462#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1467#[serde(rename_all = "lowercase")]
1468pub enum ExecMode {
1469 #[default]
1471 Structured,
1472 Shell,
1474}
1475
1476#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1478#[serde(rename_all = "snake_case")]
1479#[derive(Default)]
1480pub enum AllowlistMode {
1481 Permissive,
1483 #[default]
1485 Enforced,
1486}
1487
1488#[derive(Debug, Clone, Deserialize, Serialize)]
1492pub struct ExecConfig {
1493 #[serde(default)]
1495 pub default_mode: ExecMode,
1496 #[serde(default = "default_false")]
1498 pub allow_shell_mode: bool,
1499 #[serde(default)]
1502 pub allowed_commands: Vec<String>,
1503 #[serde(default)]
1507 pub allowlist_mode: AllowlistMode,
1508 #[serde(default = "default_exec_timeout")]
1510 pub default_timeout_secs: u64,
1511 #[serde(default = "default_exec_max_timeout")]
1513 pub max_timeout_secs: u64,
1514}
1515
1516fn default_false() -> bool {
1517 false
1518}
1519
1520fn default_exec_timeout() -> u64 {
1521 120
1522}
1523
1524fn default_exec_max_timeout() -> u64 {
1525 600
1526}
1527
1528impl ExecConfig {
1529 pub fn is_binary_allowed(&self, name: &str) -> bool {
1536 match self.allowlist_mode {
1537 AllowlistMode::Permissive => {
1538 self.allowed_commands.is_empty() || self.allowed_commands.iter().any(|c| c == name)
1539 }
1540 AllowlistMode::Enforced => self.allowed_commands.iter().any(|c| c == name),
1541 }
1542 }
1543}
1544
1545impl Default for ExecConfig {
1546 fn default() -> Self {
1547 Self {
1548 default_mode: ExecMode::default(),
1549 allow_shell_mode: default_false(),
1550 allowed_commands: Vec::new(),
1551 allowlist_mode: AllowlistMode::default(),
1552 default_timeout_secs: default_exec_timeout(),
1553 max_timeout_secs: default_exec_max_timeout(),
1554 }
1555 }
1556}
1557
1558#[derive(Debug, Clone, Deserialize, Serialize)]
1560pub struct OrchestratorConfig {
1561 #[serde(default = "default_max_evolution_iterations")]
1564 pub max_evolution_iterations: u32,
1565
1566 #[serde(default = "default_min_evaluation_score")]
1569 pub min_evaluation_score: f64,
1570}
1571
1572fn default_max_evolution_iterations() -> u32 {
1573 3
1574}
1575
1576fn default_min_evaluation_score() -> f64 {
1577 0.8
1578}
1579
1580impl Default for OrchestratorConfig {
1581 fn default() -> Self {
1582 Self {
1583 max_evolution_iterations: default_max_evolution_iterations(),
1584 min_evaluation_score: default_min_evaluation_score(),
1585 }
1586 }
1587}
1588
1589#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct IntentConfig {
1595 #[serde(default = "default_intent_max_retries")]
1599 pub max_retries: u32,
1600
1601 #[serde(default = "default_intent_score_threshold")]
1605 pub score_threshold: f64,
1606
1607 #[serde(default = "default_intent_max_clarify_rounds")]
1611 pub max_clarify_rounds: u32,
1612
1613 #[serde(default = "default_intent_enable_retry")]
1617 pub enable_retry: bool,
1618
1619 #[serde(default)]
1623 pub lightweight_model: Option<String>,
1624}
1625
1626fn default_intent_max_retries() -> u32 {
1627 2
1628}
1629
1630fn default_intent_score_threshold() -> f64 {
1631 0.7
1632}
1633
1634fn default_intent_max_clarify_rounds() -> u32 {
1635 3
1636}
1637
1638fn default_intent_enable_retry() -> bool {
1639 true
1640}
1641
1642impl Default for IntentConfig {
1643 fn default() -> Self {
1644 Self {
1645 max_retries: default_intent_max_retries(),
1646 score_threshold: default_intent_score_threshold(),
1647 max_clarify_rounds: default_intent_max_clarify_rounds(),
1648 enable_retry: default_intent_enable_retry(),
1649 lightweight_model: None,
1650 }
1651 }
1652}
1653
1654#[derive(Debug, Clone, Deserialize, Serialize)]
1656pub struct ContextConfig {
1657 #[serde(default = "default_active_limit")]
1659 pub active_limit_tokens: usize,
1660 #[serde(default = "default_cache_limit")]
1662 pub cache_limit_entries: usize,
1663}
1664
1665fn default_active_limit() -> usize {
1666 100_000
1667}
1668
1669fn default_cache_limit() -> usize {
1670 50
1671}
1672
1673impl Default for ContextConfig {
1674 fn default() -> Self {
1675 Self {
1676 active_limit_tokens: default_active_limit(),
1677 cache_limit_entries: default_cache_limit(),
1678 }
1679 }
1680}
1681
1682#[derive(Debug, Clone, Deserialize, Serialize)]
1684pub struct SecurityConfig {
1685 #[serde(default = "default_allowed_tools")]
1687 pub allowed_tools: Vec<String>,
1688 #[serde(default)]
1690 pub network_access: bool,
1691 #[serde(default = "default_max_exec_time")]
1693 pub max_execution_time_secs: u64,
1694 #[serde(default = "default_max_memory")]
1696 pub max_memory_mb: u64,
1697 #[serde(default)]
1699 pub can_fork: bool,
1700 #[serde(default)]
1702 pub approval: ApprovalConfig,
1703 #[serde(default = "default_max_audit")]
1705 pub max_audit_entries: usize,
1706 #[serde(default)]
1708 pub auth_enabled: bool,
1709 #[serde(default = "default_cors_origins")]
1711 pub cors_origins: Vec<String>,
1712 #[serde(default)]
1714 pub audit_log_path: Option<String>,
1715 #[serde(default = "default_rate_limit_per_minute")]
1717 pub rate_limit_per_minute: u32,
1718}
1719
1720fn default_allowed_tools() -> Vec<String> {
1721 vec![
1722 "read".to_string(),
1723 "write".to_string(),
1724 "edit".to_string(),
1725 "bash".to_string(),
1726 "grep".to_string(),
1727 "find".to_string(),
1728 "exec".to_string(),
1729 ]
1730}
1731
1732fn default_max_exec_time() -> u64 {
1733 300
1734}
1735
1736fn default_max_memory() -> u64 {
1737 512
1738}
1739
1740fn default_max_audit() -> usize {
1741 10_000
1742}
1743
1744fn default_rate_limit_per_minute() -> u32 {
1745 600
1749}
1750
1751fn default_cors_origins() -> Vec<String> {
1752 vec![
1757 "http://localhost:4200".to_string(),
1758 "http://127.0.0.1:4200".to_string(),
1759 "http://localhost:5173".to_string(),
1760 "http://127.0.0.1:5173".to_string(),
1761 ]
1762}
1763
1764impl Default for SecurityConfig {
1765 fn default() -> Self {
1766 Self {
1767 allowed_tools: default_allowed_tools(),
1768 network_access: false,
1769 max_execution_time_secs: default_max_exec_time(),
1770 max_memory_mb: default_max_memory(),
1771 can_fork: false,
1772 approval: ApprovalConfig::default(),
1773 max_audit_entries: default_max_audit(),
1774 auth_enabled: false,
1775 cors_origins: default_cors_origins(),
1776 audit_log_path: None,
1777 rate_limit_per_minute: default_rate_limit_per_minute(),
1778 }
1779 }
1780}
1781
1782#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1787pub struct PersonaConfig {
1788 #[serde(default)]
1790 pub default_persona_id: Option<String>,
1791}
1792
1793#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1801pub struct McpConfig {
1802 #[serde(default)]
1804 pub servers: std::collections::HashMap<String, McpServerDef>,
1805}
1806
1807#[derive(Debug, Clone, Deserialize, Serialize)]
1809pub struct McpServerDef {
1810 pub command: String,
1812 #[serde(default)]
1814 pub args: Vec<String>,
1815 #[serde(default)]
1817 pub env: std::collections::HashMap<String, String>,
1818 #[serde(default = "default_mcp_enabled")]
1820 pub enabled: bool,
1821}
1822
1823fn default_mcp_enabled() -> bool {
1824 true
1825}
1826
1827#[derive(Debug, Clone, Deserialize, Serialize)]
1829pub struct GitConfig {
1830 #[serde(default = "default_true")]
1832 pub auto_commit: bool,
1833}
1834
1835impl Default for GitConfig {
1836 fn default() -> Self {
1837 Self { auto_commit: true }
1838 }
1839}
1840
1841#[derive(Debug, Clone, Deserialize, Serialize)]
1843pub struct AuditConfig {
1844 #[serde(default = "default_audit_max_entries")]
1846 pub max_entries: usize,
1847 #[serde(default = "default_true")]
1849 pub enabled: bool,
1850}
1851
1852fn default_audit_max_entries() -> usize {
1853 100_000
1854}
1855
1856impl Default for AuditConfig {
1857 fn default() -> Self {
1858 Self {
1859 max_entries: default_audit_max_entries(),
1860 enabled: true,
1861 }
1862 }
1863}
1864
1865#[derive(Debug, Clone, Deserialize, Serialize)]
1867pub struct BudgetConfig {
1868 #[serde(default)]
1870 pub default_token_budget: u64,
1871 #[serde(default)]
1873 pub default_calls_budget: u64,
1874 #[serde(default = "default_budget_window")]
1876 pub default_window_secs: u64,
1877 #[serde(default = "default_true")]
1879 pub enabled: bool,
1880 #[serde(default)]
1884 pub monthly_spend_limit_usd: Option<f64>,
1885}
1886
1887fn default_budget_window() -> u64 {
1888 3600
1889}
1890
1891impl Default for BudgetConfig {
1892 fn default() -> Self {
1893 Self {
1894 default_token_budget: 0,
1895 default_calls_budget: 0,
1896 default_window_secs: default_budget_window(),
1897 enabled: true,
1898 monthly_spend_limit_usd: None,
1899 }
1900 }
1901}
1902
1903#[derive(Debug, Clone, Deserialize, Serialize)]
1905pub struct ResourceMonitorConfig {
1906 #[serde(default = "default_rm_interval")]
1908 pub interval_secs: u64,
1909 #[serde(default = "default_rm_history_max")]
1911 pub history_max: usize,
1912 #[serde(default = "default_rm_cpu_threshold")]
1914 pub cpu_threshold: f32,
1915 #[serde(default = "default_rm_mem_threshold")]
1917 pub memory_threshold: f32,
1918 #[serde(default = "default_rm_load_threshold")]
1920 pub load_threshold: f32,
1921}
1922
1923fn default_rm_interval() -> u64 {
1924 60
1925}
1926
1927fn default_rm_history_max() -> usize {
1928 60
1929}
1930
1931fn default_rm_cpu_threshold() -> f32 {
1932 90.0
1933}
1934
1935fn default_rm_mem_threshold() -> f32 {
1936 90.0
1937}
1938
1939fn default_rm_load_threshold() -> f32 {
1940 8.0
1941}
1942
1943impl Default for ResourceMonitorConfig {
1944 fn default() -> Self {
1945 Self {
1946 interval_secs: default_rm_interval(),
1947 history_max: default_rm_history_max(),
1948 cpu_threshold: default_rm_cpu_threshold(),
1949 memory_threshold: default_rm_mem_threshold(),
1950 load_threshold: default_rm_load_threshold(),
1951 }
1952 }
1953}
1954
1955#[derive(Debug, Clone, Serialize, Deserialize)]
1957pub struct AgentLogConfig {
1958 #[serde(default = "default_agent_log_max_entries")]
1960 pub max_entries: usize,
1961 #[serde(default = "default_agent_log_ttl_hours")]
1963 pub ttl_hours: u64,
1964 #[serde(default = "default_agent_log_max_tool_calls")]
1966 pub max_tool_calls_per_agent: usize,
1967 #[serde(default = "default_agent_log_prune_batch")]
1969 pub prune_batch_size: usize,
1970 #[serde(default)]
1972 pub db_path: String,
1973}
1974
1975fn default_agent_log_max_entries() -> usize {
1976 10_000
1977}
1978fn default_agent_log_ttl_hours() -> u64 {
1979 720
1980}
1981fn default_agent_log_max_tool_calls() -> usize {
1982 500
1983}
1984fn default_agent_log_prune_batch() -> usize {
1985 100
1986}
1987
1988impl Default for AgentLogConfig {
1989 fn default() -> Self {
1990 Self {
1991 max_entries: 10_000,
1992 ttl_hours: 720,
1993 max_tool_calls_per_agent: 500,
1994 prune_batch_size: 100,
1995 db_path: String::new(),
1996 }
1997 }
1998}
1999
2000#[derive(Debug, Clone, Deserialize, Serialize)]
2002pub struct LoggingConfig {
2003 #[serde(default = "default_log_format")]
2005 pub format: String,
2006 #[serde(default)]
2008 pub level: Option<String>,
2009}
2010
2011fn default_log_format() -> String {
2012 "pretty".into()
2013}
2014
2015impl Default for LoggingConfig {
2016 fn default() -> Self {
2017 Self {
2018 format: default_log_format(),
2019 level: None,
2020 }
2021 }
2022}
2023
2024#[derive(Debug, Clone, Deserialize, Serialize)]
2030pub struct BrowserConfig {
2031 #[serde(default = "default_browser_enabled")]
2033 pub enabled: bool,
2034
2035 #[serde(default)]
2046 pub engine: serde_json::Value,
2047}
2048
2049fn default_browser_enabled() -> bool {
2050 true
2051}
2052
2053impl Default for BrowserConfig {
2054 fn default() -> Self {
2055 Self {
2056 enabled: true,
2057 engine: serde_json::json!({}),
2058 }
2059 }
2060}
2061
2062pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
2064 let content = std::fs::read_to_string(path)?;
2065 let config: OxiosConfig = toml::from_str(&content)?;
2066 let (errors, warnings) = config.validate();
2067 for w in warnings {
2068 tracing::warn!("config: {}", w);
2069 }
2070 if !errors.is_empty() {
2071 let msg = errors.join("; ");
2072 anyhow::bail!("Configuration validation failed: {msg}");
2073 }
2074 Ok(config)
2075}
2076
2077impl OxiosConfig {
2078 pub fn api_key(&self) -> Option<String> {
2080 self.engine.api_key.clone().filter(|k| !k.is_empty())
2081 }
2082
2083 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
2086 let mut errors = Vec::new();
2087 let mut warnings = Vec::new();
2088
2089 if self.kernel.max_agents == 0 {
2091 errors.push("kernel.max_agents must be > 0".into());
2092 }
2093 if self.kernel.workspace.is_empty() {
2094 errors.push("kernel.workspace must not be empty".into());
2095 }
2096
2097 if self.gateway.port == 0 {
2099 errors.push("gateway.port must be > 0".into());
2100 }
2101 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
2102 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
2103 }
2104
2105 for (name, job) in &self.cron.jobs {
2107 if job.schedule.is_empty() {
2108 errors.push(format!("cron.jobs.{name}: schedule is empty"));
2109 } else {
2110 let normalized = {
2112 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
2113 match fields.len() {
2114 5 => format!("0 {}", job.schedule),
2115 _ => job.schedule.clone(),
2116 }
2117 };
2118 if Schedule::from_str(&normalized).is_err() {
2119 errors.push(format!(
2120 "cron.jobs.{}: invalid cron expression '{}'",
2121 name, job.schedule
2122 ));
2123 }
2124 }
2125 if job.goal.is_empty() {
2126 errors.push(format!("cron.jobs.{name}: goal is empty"));
2127 }
2128 }
2129
2130 if self.security.max_execution_time_secs == 0 {
2132 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
2133 }
2134
2135 if self.audit.max_entries == 0 {
2137 warnings.push("audit.max_entries is 0 — audit will never prune".into());
2138 }
2139
2140 if self.budget.default_window_secs == 0 {
2142 warnings.push("budget.default_window_secs is 0 — no time window".into());
2143 }
2144
2145 if self.gateway.response_timeout_secs == 0 {
2147 errors.push("gateway.response_timeout_secs must be > 0".into());
2148 }
2149
2150 if self.engine.api_key.as_ref().is_some_and(|k| !k.is_empty()) {
2153 warnings.push(
2154 "engine.api_key is set in config — prefer the oxi auth store or env var to avoid storing a secret on disk"
2155 .into(),
2156 );
2157 }
2158
2159 for (name, server) in &self.mcp.servers {
2161 if server.command.trim().is_empty() {
2162 errors.push(format!("mcp.servers.{name}: command must not be empty"));
2163 }
2164 }
2165
2166 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
2168 {
2169 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
2170 }
2171
2172 if self.exec.default_timeout_secs == 0 {
2174 errors.push("exec.default_timeout_secs must be > 0".into());
2175 }
2176 if self.exec.max_timeout_secs == 0 {
2177 errors.push("exec.max_timeout_secs must be > 0".into());
2178 }
2179 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
2180 errors.push(format!(
2181 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
2182 self.exec.default_timeout_secs, self.exec.max_timeout_secs
2183 ));
2184 }
2185
2186 if self.resource_monitor.cpu_threshold > 100.0 {
2188 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
2189 }
2190 if self.resource_monitor.memory_threshold > 100.0 {
2191 errors.push("resource_monitor.memory_threshold must be <= 100".into());
2192 }
2193
2194 for name in &self.channels.enabled {
2196 let valid = ["cli", "telegram"];
2197 if !valid.contains(&name.as_str()) {
2198 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
2199 }
2200 }
2201 if self.channels.enabled.iter().any(|c| c == "web") {
2203 warnings.push(
2204 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
2205 );
2206 }
2207 if self.channels.enabled.iter().any(|c| c == "telegram")
2208 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
2209 {
2210 warnings.push(format!(
2211 "channels.telegram: {} env var not set — telegram channel will fail",
2212 self.channels.telegram.bot_token_env
2213 ));
2214 }
2215 for err in self.token_maxing.validate() {
2219 errors.push(err);
2220 }
2221
2222 (errors, warnings)
2223 }
2224}
2225
2226pub fn expand_home(path: &str) -> std::path::PathBuf {
2238 if let Some(rest) = path.strip_prefix("~/") {
2239 if let Ok(home) = std::env::var("HOME") {
2240 return std::path::PathBuf::from(format!("{home}/{rest}"));
2241 }
2242 if let Some(home) = dirs::home_dir() {
2243 return home.join(rest);
2244 }
2245 }
2246 std::path::PathBuf::from(path)
2247}
2248
2249#[cfg(test)]
2250mod tests {
2251 use super::*;
2252
2253 #[test]
2254 fn test_default_config_validates() {
2255 let config = OxiosConfig::default();
2256 let (errors, _warnings) = config.validate();
2257 assert!(
2258 errors.is_empty(),
2259 "Default config should have no errors: {:?}",
2260 errors
2261 );
2262 }
2263
2264 #[test]
2265 fn security_config_parses_approval_section() {
2266 let toml = r#"
2267[kernel]
2268
2269[security.approval]
2270mode = "auto-run"
2271allow_list = ["exec:curl", "web_search"]
2272
2273[security.approval.tool_overrides]
2274exec = "always"
2275"#;
2276 let cfg: OxiosConfig = toml::from_str(toml).unwrap();
2277 assert_eq!(
2278 cfg.security.approval.mode,
2279 crate::approval::ApprovalMode::AutoRun
2280 );
2281 assert_eq!(
2282 cfg.security.approval.allow_list,
2283 vec!["exec:curl", "web_search"]
2284 );
2285 assert_eq!(
2286 cfg.security.approval.tool_overrides.get("exec"),
2287 Some(&crate::approval::ToolPolicy::Always)
2288 );
2289 }
2290
2291 #[test]
2292 fn security_config_defaults_approval_to_manual() {
2293 let cfg = OxiosConfig::default();
2294 assert_eq!(
2295 cfg.security.approval.mode,
2296 crate::approval::ApprovalMode::Manual
2297 );
2298 assert!(cfg.security.approval.allow_list.is_empty());
2299 }
2300
2301 #[test]
2302 fn test_exec_config_default_allowed_commands() {
2303 let config = ExecConfig::default();
2304 assert!(config.allowed_commands.is_empty());
2306 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
2307 assert!(!config.is_binary_allowed("anything"));
2308 assert!(!config.is_binary_allowed("bash"));
2309 }
2310
2311 #[test]
2312 fn test_exec_config_permissive_mode() {
2313 let config = ExecConfig {
2314 allowlist_mode: AllowlistMode::Permissive,
2315 ..Default::default()
2316 };
2317 assert!(config.is_binary_allowed("anything"));
2319 assert!(config.is_binary_allowed("bash"));
2320 }
2321
2322 #[test]
2323 fn test_is_binary_allowed_with_allowlist() {
2324 let config = ExecConfig {
2325 allowed_commands: vec!["git".into(), "echo".into()],
2326 ..Default::default()
2327 };
2328 assert!(config.is_binary_allowed("git"));
2329 assert!(config.is_binary_allowed("echo"));
2330 assert!(!config.is_binary_allowed("bash"));
2331 assert!(!config.is_binary_allowed("rm"));
2332 assert!(!config.is_binary_allowed("sudo"));
2333 }
2334
2335 #[test]
2336 fn test_expand_home() {
2337 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
2339 let expanded = expand_home("~/projects/test");
2340 assert_eq!(
2341 expanded.to_str().unwrap(),
2342 format!("{}/projects/test", home)
2343 );
2344
2345 let abs = expand_home("/absolute/path");
2347 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
2348
2349 let bare = expand_home("~something");
2351 assert_eq!(bare, std::path::PathBuf::from("~something"));
2352 }
2353
2354 #[test]
2355 fn test_invalid_cron_expression() {
2356 let mut config = OxiosConfig::default();
2357 config.cron.enabled = true;
2358 config.cron.jobs.insert(
2359 "bad-job".to_string(),
2360 InlineCronJob {
2361 schedule: "not a valid cron".to_string(),
2362 goal: "Test goal".to_string(),
2363 constraints: vec![],
2364 acceptance_criteria: vec![],
2365 toolchain: "default".to_string(),
2366 priority: Priority::Normal,
2367 enabled: true,
2368 },
2369 );
2370
2371 let (errors, _warnings) = config.validate();
2372 assert!(
2373 !errors.is_empty(),
2374 "Expected validation error for invalid cron"
2375 );
2376 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
2377 assert!(
2378 has_cron_error,
2379 "Expected 'invalid cron expression' error, got: {:?}",
2380 errors
2381 );
2382 }
2383
2384 #[test]
2385 fn test_config_serialization_roundtrip() {
2386 let config = OxiosConfig::default();
2387
2388 let toml_str = toml::to_string(&config).expect("serialization should succeed");
2390
2391 let deserialized: OxiosConfig =
2393 toml::from_str(&toml_str).expect("deserialization should succeed");
2394
2395 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2397 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2398 assert_eq!(config.gateway.host, deserialized.gateway.host);
2399 assert_eq!(config.gateway.port, deserialized.gateway.port);
2400 assert_eq!(
2401 config.exec.default_timeout_secs,
2402 deserialized.exec.default_timeout_secs
2403 );
2404 assert_eq!(
2405 config.exec.max_timeout_secs,
2406 deserialized.exec.max_timeout_secs
2407 );
2408 }
2409
2410 #[test]
2411 fn test_exec_timeout_validation() {
2412 let mut config = OxiosConfig::default();
2413 config.exec.default_timeout_secs = 999;
2415 config.exec.max_timeout_secs = 100;
2416 let (errors, _warnings) = config.validate();
2417 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2418 assert!(
2419 has_error,
2420 "Expected timeout ordering error, got: {:?}",
2421 errors
2422 );
2423 }
2424
2425 #[test]
2426 fn test_zero_max_agents_error() {
2427 let mut config = OxiosConfig::default();
2428 config.kernel.max_agents = 0;
2429 let (errors, _warnings) = config.validate();
2430 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2431 }
2432
2433 #[test]
2438 fn test_default_config_matches_toml() {
2439 let from_rust = OxiosConfig::default();
2440
2441 let toml_str = include_str!("../../../share/default-config.toml");
2442 let from_toml: OxiosConfig =
2443 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2444
2445 assert_eq!(
2447 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2448 "kernel.max_agents 불일치: Rust={}, TOML={}",
2449 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2450 );
2451 assert_eq!(
2452 from_rust.gateway.host, from_toml.gateway.host,
2453 "gateway.host 불일치: Rust={}, TOML={}",
2454 from_rust.gateway.host, from_toml.gateway.host
2455 );
2456 assert_eq!(
2457 from_rust.gateway.port, from_toml.gateway.port,
2458 "gateway.port 불일치: Rust={}, TOML={}",
2459 from_rust.gateway.port, from_toml.gateway.port
2460 );
2461 assert_eq!(
2462 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2463 "kernel.event_bus_capacity 불일치"
2464 );
2465 assert_eq!(
2466 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2467 "memory.consolidation.preset 불일치"
2468 );
2469
2470 let (_, warnings) = from_toml.validate();
2472 for w in &warnings {
2473 eprintln!("default-config.toml 경고: {}", w);
2474 }
2475 }
2476
2477 #[test]
2480 fn test_gateway_should_expose_api_docs() {
2481 let cfg = GatewayConfig::default();
2483 assert!(!cfg.should_expose_api_docs());
2484
2485 let cfg = GatewayConfig {
2487 host: "0.0.0.0".into(),
2488 port: 4200,
2489 expose_api_docs: true,
2490 ..Default::default()
2491 };
2492 assert!(
2493 !cfg.should_expose_api_docs(),
2494 "public bind must not expose api docs even when opt-in is true"
2495 );
2496
2497 let cfg = GatewayConfig {
2499 host: "127.0.0.1".into(),
2500 port: 4200,
2501 expose_api_docs: true,
2502 ..Default::default()
2503 };
2504 assert!(cfg.should_expose_api_docs());
2505
2506 let cfg = GatewayConfig {
2508 host: "::1".into(),
2509 port: 4200,
2510 expose_api_docs: true,
2511 ..Default::default()
2512 };
2513 assert!(cfg.should_expose_api_docs());
2514
2515 let cfg = GatewayConfig {
2517 host: "localhost".into(),
2518 port: 4200,
2519 expose_api_docs: true,
2520 ..Default::default()
2521 };
2522 assert!(cfg.should_expose_api_docs());
2523
2524 let cfg = GatewayConfig {
2526 host: "127.0.0.1".into(),
2527 port: 4200,
2528 expose_api_docs: false,
2529 ..Default::default()
2530 };
2531 assert!(!cfg.should_expose_api_docs());
2532 }
2533}