1#![allow(missing_docs)]
2use cron::Schedule;
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11use crate::email::{SmtpProvider, SmtpTls};
12use crate::scheduler::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 bridge: MemoryBridgeConfig,
116}
117
118fn default_true() -> bool {
119 true
120}
121
122fn default_max_recall() -> usize {
123 10
124}
125
126fn default_cache_ttl() -> u64 {
127 3600 }
129
130fn default_cache_max_entries() -> usize {
131 10000
132}
133
134impl Default for MemoryConfig {
135 fn default() -> Self {
136 Self {
137 enabled: true,
138 max_recall: 10,
139 auto_summarize: true,
140 capture_compaction: true,
141 retention_days: 0,
142 cache_enabled: true,
143 cache_ttl_secs: 3600,
144 cache_max_entries: 10000,
145 consolidation: ConsolidationConfig::default(),
146 sqlite: SqliteMemoryConfig::default(),
147 embedding: EmbeddingConfig::default(),
148 learning: LearningConfig::default(),
149 bridge: MemoryBridgeConfig::default(),
150 }
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct SqliteMemoryConfig {
165 #[serde(default = "default_true")]
167 pub enabled: bool,
168 #[serde(default)]
171 pub path: String,
172 #[serde(default = "default_embedding_dim")]
176 pub embedding_dim: usize,
177 #[serde(default = "default_true")]
179 pub wal_mode: bool,
180}
181
182fn default_embedding_dim() -> usize {
183 256
184}
185
186impl Default for SqliteMemoryConfig {
187 fn default() -> Self {
188 Self {
189 enabled: true,
190 path: String::new(),
191 embedding_dim: 256,
192 wal_mode: true,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct EmbeddingConfig {
209 #[serde(default = "default_embedding_provider")]
211 pub provider: String,
212 #[serde(default = "default_embedding_dim")]
215 pub dimension: usize,
216 #[serde(default = "default_model_ttl")]
219 pub model_ttl_secs: u64,
220}
221
222fn default_embedding_provider() -> String {
223 "gguf".to_string()
224}
225
226fn default_model_ttl() -> u64 {
227 300 }
229
230impl Default for EmbeddingConfig {
231 fn default() -> Self {
232 Self {
233 provider: default_embedding_provider(),
234 dimension: default_embedding_dim(),
235 model_ttl_secs: default_model_ttl(),
236 }
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct LearningConfig {
249 #[serde(default = "default_true")]
251 pub enabled: bool,
252 #[serde(default = "default_sona_mode")]
254 pub sona_mode: String,
255 #[serde(default = "default_distill_interval")]
257 pub distill_interval_hours: u64,
258 #[serde(default = "default_auto_promote_quality")]
260 pub auto_promote_quality: f32,
261 #[serde(default = "default_auto_promote_min_usage")]
263 pub auto_promote_min_usage: u32,
264}
265
266fn default_sona_mode() -> String {
267 "balanced".to_string()
268}
269
270fn default_distill_interval() -> u64 {
271 6
272}
273
274fn default_auto_promote_quality() -> f32 {
275 0.8
276}
277
278fn default_auto_promote_min_usage() -> u32 {
279 3
280}
281
282impl Default for LearningConfig {
283 fn default() -> Self {
284 Self {
285 enabled: true,
286 sona_mode: default_sona_mode(),
287 distill_interval_hours: default_distill_interval(),
288 auto_promote_quality: default_auto_promote_quality(),
289 auto_promote_min_usage: default_auto_promote_min_usage(),
290 }
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct MemoryBridgeConfig {
304 #[serde(default)]
306 pub sync_enabled: bool,
307 #[serde(default = "default_bridge_interval")]
309 pub interval_secs: u64,
310}
311
312fn default_bridge_interval() -> u64 {
313 3600
314}
315
316impl Default for MemoryBridgeConfig {
317 fn default() -> Self {
318 Self {
319 sync_enabled: false,
320 interval_secs: default_bridge_interval(),
321 }
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct ConsolidationConfig {
333 #[serde(default = "default_preset")]
337 pub preset: String,
338
339 #[serde(default = "default_true")]
341 pub dream_enabled: bool,
342 #[serde(default = "default_dream_interval")]
343 pub dream_interval_hours: u64,
344 #[serde(default = "default_dream_min_sessions")]
345 pub dream_min_sessions: u32,
346
347 #[serde(default = "default_hot_max")]
349 pub hot_max_entries: usize,
350 #[serde(default = "default_warm_max")]
351 pub warm_max_entries: usize,
352 #[serde(default = "default_cold_max")]
353 pub cold_max_entries: usize,
354 #[serde(default = "default_hot_token_budget")]
355 pub hot_token_budget: usize,
356
357 #[serde(default = "default_true")]
359 pub decay_enabled: bool,
360 #[serde(default = "default_one")]
361 pub decay_multiplier: f32,
362 #[serde(default = "default_decay_threshold")]
363 pub decay_threshold: f32,
364 #[serde(default = "default_retention_days")]
365 pub retention_days: u32,
366
367 #[serde(default = "default_true")]
369 pub auto_protection: bool,
370 #[serde(default = "default_protection_low_access")]
371 pub protection_low_access: u32,
372 #[serde(default = "default_protection_medium_access")]
373 pub protection_medium_access: u32,
374 #[serde(default = "default_protection_high_access")]
375 pub protection_high_access: u32,
376 #[serde(default = "default_protection_medium_sessions")]
377 pub protection_medium_sessions: u32,
378 #[serde(default = "default_protection_high_sessions")]
379 pub protection_high_sessions: u32,
380
381 #[serde(default = "default_true")]
383 pub auto_classification: bool,
384 #[serde(default = "default_type_promotion_threshold")]
385 pub type_promotion_repetitions: u32,
386
387 #[serde(default = "default_compaction_threshold")]
389 pub compaction_line_threshold: usize,
390 #[serde(default = "default_true")]
391 pub llm_compaction: bool,
392
393 #[serde(default)]
396 pub dream_model: Option<String>,
397
398 #[serde(default = "default_true")]
400 pub protection_demotion_enabled: bool,
401 #[serde(default = "default_demotion_stale_days")]
402 pub protection_demotion_stale_days: u32,
403 #[serde(default = "default_demotion_max_step")]
404 pub protection_demotion_max_step: u32,
405
406 #[serde(default = "default_true")]
408 pub proactive_recall: bool,
409 #[serde(default = "default_proactive_limit")]
410 pub proactive_recall_limit: usize,
411 #[serde(default = "default_proactive_threshold")]
412 pub proactive_recall_threshold: f32,
413}
414
415fn default_dream_interval() -> u64 {
416 24
417}
418fn default_dream_min_sessions() -> u32 {
419 5
420}
421fn default_hot_max() -> usize {
422 50
423}
424fn default_warm_max() -> usize {
425 500
426}
427fn default_cold_max() -> usize {
428 10_000
429}
430fn default_hot_token_budget() -> usize {
431 3_000
432}
433fn default_one() -> f32 {
434 1.0
435}
436fn default_decay_threshold() -> f32 {
437 0.05
438}
439fn default_retention_days() -> u32 {
440 90
441}
442fn default_protection_low_access() -> u32 {
443 2
444}
445fn default_protection_medium_access() -> u32 {
446 3
447}
448fn default_protection_high_access() -> u32 {
449 5
450}
451fn default_protection_medium_sessions() -> u32 {
452 2
453}
454fn default_protection_high_sessions() -> u32 {
455 3
456}
457fn default_type_promotion_threshold() -> u32 {
458 3
459}
460fn default_compaction_threshold() -> usize {
461 200
462}
463fn default_proactive_limit() -> usize {
464 5
465}
466fn default_proactive_threshold() -> f32 {
467 0.6
468}
469fn default_demotion_stale_days() -> u32 {
470 30
471}
472fn default_demotion_max_step() -> u32 {
473 1
474}
475
476fn default_preset() -> String {
477 "balanced".into()
478}
479
480impl Default for ConsolidationConfig {
481 fn default() -> Self {
482 Self {
483 preset: default_preset(),
484 dream_enabled: true,
485 dream_interval_hours: 24,
486 dream_min_sessions: 5,
487 hot_max_entries: 50,
488 warm_max_entries: 500,
489 cold_max_entries: 10_000,
490 hot_token_budget: 3_000,
491 decay_enabled: true,
492 decay_multiplier: 1.0,
493 decay_threshold: 0.05,
494 retention_days: 90,
495 auto_protection: true,
496 protection_low_access: 2,
497 protection_medium_access: 3,
498 protection_high_access: 5,
499 protection_medium_sessions: 2,
500 protection_high_sessions: 3,
501 auto_classification: true,
502 type_promotion_repetitions: 3,
503 compaction_line_threshold: 200,
504 llm_compaction: true,
505 dream_model: None,
506 protection_demotion_enabled: true,
507 protection_demotion_stale_days: 30,
508 protection_demotion_max_step: 1,
509 proactive_recall: true,
510 proactive_recall_limit: 5,
511 proactive_recall_threshold: 0.6,
512 }
513 }
514}
515
516impl ConsolidationConfig {
517 pub fn apply_preset(&mut self) {
521 let resolved = match self.preset.as_str() {
522 "conservative" => Self::conservative(),
523 "aggressive" => Self::aggressive(),
524 "custom" => return,
525 _ => Self::default(), };
527 *self = resolved;
528 }
529
530 fn conservative() -> Self {
532 Self {
533 preset: "conservative".into(),
534 dream_enabled: true,
535 dream_interval_hours: 48,
536 dream_min_sessions: 10,
537 hot_max_entries: 100,
538 warm_max_entries: 1000,
539 cold_max_entries: 50_000,
540 hot_token_budget: 5_000,
541 decay_enabled: true,
542 decay_multiplier: 0.8,
543 decay_threshold: 0.05,
544 retention_days: 365,
545 auto_protection: true,
546 protection_low_access: 3,
547 protection_medium_access: 5,
548 protection_high_access: 10,
549 protection_medium_sessions: 3,
550 protection_high_sessions: 5,
551 auto_classification: true,
552 type_promotion_repetitions: 5,
553 compaction_line_threshold: 300,
554 llm_compaction: true,
555 dream_model: None,
556 protection_demotion_enabled: true,
557 protection_demotion_stale_days: 90,
558 protection_demotion_max_step: 1,
559 proactive_recall: true,
560 proactive_recall_limit: 8,
561 proactive_recall_threshold: 0.5,
562 }
563 }
564
565 fn aggressive() -> Self {
567 Self {
568 preset: "aggressive".into(),
569 dream_enabled: true,
570 dream_interval_hours: 4,
571 dream_min_sessions: 2,
572 hot_max_entries: 20,
573 warm_max_entries: 100,
574 cold_max_entries: 1_000,
575 hot_token_budget: 2_000,
576 decay_enabled: true,
577 decay_multiplier: 1.0,
578 decay_threshold: 0.1,
579 retention_days: 30,
580 auto_protection: true,
581 protection_low_access: 1,
582 protection_medium_access: 2,
583 protection_high_access: 3,
584 protection_medium_sessions: 1,
585 protection_high_sessions: 2,
586 auto_classification: true,
587 type_promotion_repetitions: 2,
588 compaction_line_threshold: 150,
589 llm_compaction: true,
590 dream_model: None,
591 protection_demotion_enabled: true,
592 protection_demotion_stale_days: 14,
593 protection_demotion_max_step: 2,
594 proactive_recall: true,
595 proactive_recall_limit: 3,
596 proactive_recall_threshold: 0.7,
597 }
598 }
599}
600
601#[derive(Debug, Clone, Deserialize, Serialize, Default)]
603pub struct ChannelsConfig {
604 #[serde(default)]
607 pub enabled: Vec<String>,
608
609 #[serde(default)]
611 pub telegram: TelegramChannelConfig,
612}
613
614#[derive(Debug, Clone, Deserialize, Serialize)]
619pub struct SurfacesConfig {
620 #[serde(default = "default_surfaces_enabled")]
623 pub enabled: Vec<String>,
624}
625
626fn default_surfaces_enabled() -> Vec<String> {
627 vec!["web".to_string()]
628}
629
630impl Default for SurfacesConfig {
631 fn default() -> Self {
632 Self {
633 enabled: default_surfaces_enabled(),
634 }
635 }
636}
637
638#[derive(Debug, Clone, Deserialize, Serialize)]
640pub struct TelegramChannelConfig {
641 #[serde(default = "default_telegram_token_env")]
643 pub bot_token_env: String,
644 #[serde(default)]
646 pub allowed_users: Vec<i64>,
647 #[serde(default)]
649 pub session: TelegramSessionConfig,
650}
651
652fn default_telegram_token_env() -> String {
653 "TELEGRAM_BOT_TOKEN".to_string()
654}
655
656impl Default for TelegramChannelConfig {
657 fn default() -> Self {
658 Self {
659 bot_token_env: default_telegram_token_env(),
660 allowed_users: Vec::new(),
661 session: TelegramSessionConfig::default(),
662 }
663 }
664}
665
666#[derive(Debug, Clone, Deserialize, Serialize)]
668#[allow(clippy::derivable_impls)]
669pub struct EngineConfig {
670 #[serde(default)]
673 pub default_model: String,
674 #[serde(default, skip_serializing)]
678 pub api_key: Option<String>,
679 #[serde(default)]
682 pub provider_options: Option<oxi_sdk::ProviderOptions>,
683 #[serde(default)]
687 pub routing_enabled: bool,
688 #[serde(default)]
690 pub prefer_cost_efficient: bool,
691 #[serde(default)]
693 pub fallback_models: Vec<String>,
694 #[serde(default)]
696 pub excluded_models: Vec<String>,
697}
698
699#[allow(clippy::derivable_impls)]
700impl Default for EngineConfig {
701 fn default() -> Self {
702 Self {
703 default_model: String::new(),
704 api_key: None,
705 provider_options: None,
706 routing_enabled: false,
707 prefer_cost_efficient: false,
708 fallback_models: Vec::new(),
709 excluded_models: Vec::new(),
710 }
711 }
712}
713
714#[derive(Debug, Clone, Deserialize, Serialize)]
716pub struct DaemonConfig {
717 #[serde(default = "default_pid_file")]
719 pub pid_file: String,
720 #[serde(default = "default_daemon_log_dir")]
722 pub log_dir: String,
723}
724
725fn default_pid_file() -> String {
726 dirs::home_dir()
727 .map(|h| format!("{}/.oxios/oxios.pid", h.display()))
728 .unwrap_or_else(|| "./oxios.pid".into())
729}
730
731fn default_daemon_log_dir() -> String {
732 dirs::home_dir()
733 .map(|h| format!("{}/.oxios/logs", h.display()))
734 .unwrap_or_else(|| "./logs".into())
735}
736
737impl Default for DaemonConfig {
738 fn default() -> Self {
739 Self {
740 pid_file: default_pid_file(),
741 log_dir: default_daemon_log_dir(),
742 }
743 }
744}
745
746#[derive(Debug, Clone, Deserialize, Serialize)]
748pub struct SessionConfig {
749 #[serde(default = "default_max_sessions")]
753 pub max_sessions: usize,
754
755 #[serde(default = "default_session_ttl_hours")]
759 pub ttl_hours: u64,
760
761 #[serde(default = "default_true")]
763 pub auto_prune: bool,
764}
765
766fn default_max_sessions() -> usize {
767 100
768}
769
770fn default_session_ttl_hours() -> u64 {
771 168 }
773
774impl Default for SessionConfig {
775 fn default() -> Self {
776 Self {
777 max_sessions: default_max_sessions(),
778 ttl_hours: default_session_ttl_hours(),
779 auto_prune: true,
780 }
781 }
782}
783
784#[derive(Debug, Clone, Deserialize, Serialize)]
786pub struct TelegramSessionConfig {
787 #[serde(default = "default_telegram_session_rotation_hours")]
790 pub rotation_hours: u64,
791
792 #[serde(default = "default_telegram_session_max_messages")]
795 pub max_messages: usize,
796}
797
798fn default_telegram_session_rotation_hours() -> u64 {
799 2 }
801
802fn default_telegram_session_max_messages() -> usize {
803 0 }
805
806impl Default for TelegramSessionConfig {
807 fn default() -> Self {
808 Self {
809 rotation_hours: default_telegram_session_rotation_hours(),
810 max_messages: default_telegram_session_max_messages(),
811 }
812 }
813}
814
815#[derive(Debug, Clone, Deserialize, Serialize, Default)]
817pub struct OxiosConfig {
818 pub kernel: KernelConfig,
820 #[serde(default)]
822 pub engine: EngineConfig,
823 #[serde(default)]
825 pub daemon: DaemonConfig,
826 #[serde(default)]
828 pub gateway: GatewayConfig,
829 #[serde(default)]
831 pub scheduler: SchedulerConfig,
832 #[serde(default)]
834 pub orchestrator: OrchestratorConfig,
835 #[serde(default)]
837 pub context: ContextConfig,
838 #[serde(default)]
840 pub security: SecurityConfig,
841 #[serde(default)]
843 pub persona: PersonaConfig,
844 #[serde(default)]
846 pub memory: MemoryConfig,
847 #[serde(default)]
849 pub cron: CronConfig,
850 #[serde(default)]
852 pub mcp: McpConfig,
853 #[serde(default)]
855 pub git: GitConfig,
856 #[serde(default)]
858 pub audit: AuditConfig,
859 #[serde(default)]
861 pub budget: BudgetConfig,
862 #[serde(default)]
864 pub exec: ExecConfig,
865 #[serde(default)]
867 pub resource_monitor: ResourceMonitorConfig,
868 #[serde(default)]
870 pub otel: OtelConfig,
871 #[serde(default)]
873 pub logging: LoggingConfig,
874 #[serde(default)]
876 pub channels: ChannelsConfig,
877 #[serde(default)]
879 pub surfaces: Option<SurfacesConfig>,
880 #[serde(default)]
882 pub browser: BrowserConfig,
883 #[serde(default)]
885 pub session: SessionConfig,
886 #[serde(default)]
888 pub marketplace: MarketplaceConfig,
889 #[serde(default)]
891 pub calendar: CalendarConfig,
892 #[serde(default)]
894 pub email: EmailConfig,
895}
896
897#[derive(Debug, Clone, Deserialize, Serialize)]
899pub struct KernelConfig {
900 #[serde(default = "default_workspace")]
902 pub workspace: String,
903 #[serde(default = "default_event_bus_capacity")]
905 pub event_bus_capacity: usize,
906 #[serde(default = "default_max_agents")]
908 pub max_agents: usize,
909}
910
911fn default_workspace() -> String {
912 dirs_home().unwrap_or_else(|| ".".into())
913}
914
915fn dirs_home() -> Option<String> {
916 dirs::home_dir().map(|h| format!("{}/.oxios/workspace", h.display()))
917}
918
919fn default_event_bus_capacity() -> usize {
920 256
921}
922
923fn default_max_agents() -> usize {
924 10
925}
926
927impl Default for KernelConfig {
928 fn default() -> Self {
929 Self {
930 workspace: default_workspace(),
931 event_bus_capacity: default_event_bus_capacity(),
932 max_agents: 10,
933 }
934 }
935}
936
937#[derive(Debug, Clone, Deserialize, Serialize)]
939pub struct GatewayConfig {
940 #[serde(default = "default_gateway_host")]
942 pub host: String,
943 #[serde(default = "default_gateway_port")]
945 pub port: u16,
946 #[serde(default)]
956 pub expose_api_docs: bool,
957}
958
959impl GatewayConfig {
960 pub fn should_expose_api_docs(&self) -> bool {
966 if !self.expose_api_docs {
967 return false;
968 }
969 let h = self.host.trim();
970 h == "127.0.0.1" || h == "::1" || h == "localhost" || h.starts_with("127.")
971 }
972}
973
974#[derive(Debug, Clone, Deserialize, Serialize)]
976pub struct MarketplaceConfig {
977 #[serde(default)]
980 pub base_url: Option<String>,
981 #[serde(default = "default_true")]
983 pub enabled: bool,
984 #[serde(default)]
986 pub skills_sh: SkillsShConfig,
987}
988
989#[derive(Debug, Clone, Deserialize, Serialize)]
991pub struct SkillsShConfig {
992 #[serde(default)]
995 pub base_url: Option<String>,
996 #[serde(default)]
999 pub api_key: Option<String>,
1000 #[serde(default = "default_true")]
1002 pub enabled: bool,
1003}
1004
1005impl Default for MarketplaceConfig {
1006 fn default() -> Self {
1007 Self {
1008 base_url: Some("https://clawhub.ai".to_string()),
1009 enabled: true,
1010 skills_sh: SkillsShConfig::default(),
1011 }
1012 }
1013}
1014
1015impl Default for SkillsShConfig {
1016 fn default() -> Self {
1017 Self {
1018 base_url: None,
1019 api_key: None,
1020 enabled: true,
1021 }
1022 }
1023}
1024
1025#[derive(Debug, Clone, Deserialize, Serialize)]
1027pub struct CalendarConfig {
1028 #[serde(default)]
1030 pub enabled: bool,
1031 #[serde(default = "default_calendar_timezone")]
1033 pub timezone: String,
1034 #[serde(default = "default_reminder_minutes")]
1036 pub default_reminder_minutes: Vec<u32>,
1037 #[serde(default)]
1039 pub alarm_channels: Vec<String>,
1040 #[serde(default = "default_journal_sync")]
1042 pub journal_sync: String,
1043 #[serde(default = "default_true")]
1045 pub system_calendar: bool,
1046 #[serde(default = "default_archive_days")]
1048 pub archive_after_days: u32,
1049}
1050
1051fn default_calendar_timezone() -> String {
1052 "Asia/Seoul".to_string()
1053}
1054
1055fn default_reminder_minutes() -> Vec<u32> {
1056 vec![15]
1057}
1058
1059fn default_journal_sync() -> String {
1060 "on_open".to_string()
1061}
1062
1063fn default_archive_days() -> u32 {
1064 365
1065}
1066
1067impl Default for CalendarConfig {
1068 fn default() -> Self {
1069 Self {
1070 enabled: false,
1071 timezone: default_calendar_timezone(),
1072 default_reminder_minutes: default_reminder_minutes(),
1073 alarm_channels: vec![],
1074 journal_sync: default_journal_sync(),
1075 system_calendar: true,
1076 archive_after_days: default_archive_days(),
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone, Deserialize, Serialize)]
1086pub struct EmailConfig {
1087 #[serde(default)]
1089 pub enabled: bool,
1090 #[serde(default)]
1092 pub my_email: String,
1093 #[serde(default = "default_email_provider")]
1095 pub provider: SmtpProvider,
1096 #[serde(default)]
1098 pub host: String,
1099 #[serde(default)]
1101 pub port: u16,
1102 #[serde(default)]
1104 pub tls: Option<SmtpTls>,
1105 #[serde(default)]
1107 pub user: String,
1108 #[serde(default = "default_email_secret_ref")]
1111 pub secret_ref: String,
1112 #[serde(default = "default_rate_limit_emails")]
1114 pub rate_limit_per_hour: usize,
1115}
1116
1117fn default_email_provider() -> SmtpProvider {
1118 SmtpProvider::Gmail
1119}
1120
1121fn default_email_secret_ref() -> String {
1122 "email_smtp".to_string()
1123}
1124
1125fn default_rate_limit_emails() -> usize {
1126 10
1127}
1128
1129impl Default for EmailConfig {
1130 fn default() -> Self {
1131 Self {
1132 enabled: false,
1133 my_email: String::new(),
1134 provider: default_email_provider(),
1135 host: String::new(),
1136 port: 0,
1137 tls: None,
1138 user: String::new(),
1139 secret_ref: default_email_secret_ref(),
1140 rate_limit_per_hour: default_rate_limit_emails(),
1141 }
1142 }
1143}
1144
1145impl EmailConfig {
1146 pub fn provider(&self) -> SmtpProvider {
1148 self.provider
1149 }
1150}
1151
1152fn default_gateway_host() -> String {
1153 "127.0.0.1".into()
1154}
1155
1156fn default_gateway_port() -> u16 {
1157 4200
1158}
1159
1160impl Default for GatewayConfig {
1161 fn default() -> Self {
1162 Self {
1163 host: default_gateway_host(),
1164 port: default_gateway_port(),
1165 expose_api_docs: false,
1166 }
1167 }
1168}
1169
1170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1175#[serde(rename_all = "lowercase")]
1176pub enum ExecMode {
1177 #[default]
1179 Structured,
1180 Shell,
1182}
1183
1184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1186#[serde(rename_all = "snake_case")]
1187#[derive(Default)]
1188pub enum AllowlistMode {
1189 Permissive,
1191 #[default]
1193 Enforced,
1194}
1195
1196#[derive(Debug, Clone, Deserialize, Serialize)]
1200pub struct ExecConfig {
1201 #[serde(default)]
1203 pub default_mode: ExecMode,
1204 #[serde(default = "default_false")]
1206 pub allow_shell_mode: bool,
1207 #[serde(default)]
1210 pub allowed_commands: Vec<String>,
1211 #[serde(default)]
1215 pub allowlist_mode: AllowlistMode,
1216 #[serde(default = "default_exec_timeout")]
1218 pub default_timeout_secs: u64,
1219 #[serde(default = "default_exec_max_timeout")]
1221 pub max_timeout_secs: u64,
1222}
1223
1224fn default_false() -> bool {
1225 false
1226}
1227
1228fn default_exec_timeout() -> u64 {
1229 120
1230}
1231
1232fn default_exec_max_timeout() -> u64 {
1233 600
1234}
1235
1236impl ExecConfig {
1237 pub fn is_binary_allowed(&self, name: &str) -> bool {
1244 match self.allowlist_mode {
1245 AllowlistMode::Permissive => {
1246 self.allowed_commands.is_empty() || self.allowed_commands.iter().any(|c| c == name)
1247 }
1248 AllowlistMode::Enforced => self.allowed_commands.iter().any(|c| c == name),
1249 }
1250 }
1251}
1252
1253impl Default for ExecConfig {
1254 fn default() -> Self {
1255 Self {
1256 default_mode: ExecMode::default(),
1257 allow_shell_mode: default_false(),
1258 allowed_commands: Vec::new(),
1259 allowlist_mode: AllowlistMode::default(),
1260 default_timeout_secs: default_exec_timeout(),
1261 max_timeout_secs: default_exec_max_timeout(),
1262 }
1263 }
1264}
1265
1266#[derive(Debug, Clone, Deserialize, Serialize)]
1268pub struct SchedulerConfig {
1269 #[serde(default = "default_max_concurrent")]
1271 pub max_concurrent: usize,
1272 #[serde(default = "default_rate_limit")]
1274 pub rate_limit_per_minute: u32,
1275 #[serde(default = "default_zombie_timeout")]
1277 pub zombie_timeout_secs: u64,
1278}
1279
1280fn default_max_concurrent() -> usize {
1281 5
1282}
1283
1284fn default_rate_limit() -> u32 {
1285 60
1286}
1287
1288fn default_zombie_timeout() -> u64 {
1289 300
1290}
1291
1292impl Default for SchedulerConfig {
1293 fn default() -> Self {
1294 Self {
1295 max_concurrent: default_max_concurrent(),
1296 rate_limit_per_minute: default_rate_limit(),
1297 zombie_timeout_secs: default_zombie_timeout(),
1298 }
1299 }
1300}
1301
1302#[derive(Debug, Clone, Deserialize, Serialize)]
1304pub struct OrchestratorConfig {
1305 #[serde(default = "default_max_evolution_iterations")]
1308 pub max_evolution_iterations: u32,
1309
1310 #[serde(default = "default_min_evaluation_score")]
1313 pub min_evaluation_score: f64,
1314
1315 #[serde(default = "default_true")]
1317 pub eval_cache_enabled: bool,
1318}
1319
1320fn default_max_evolution_iterations() -> u32 {
1321 3
1322}
1323
1324fn default_min_evaluation_score() -> f64 {
1325 0.8
1326}
1327
1328impl Default for OrchestratorConfig {
1329 fn default() -> Self {
1330 Self {
1331 max_evolution_iterations: default_max_evolution_iterations(),
1332 min_evaluation_score: default_min_evaluation_score(),
1333 eval_cache_enabled: true,
1334 }
1335 }
1336}
1337
1338#[derive(Debug, Clone, Deserialize, Serialize)]
1340pub struct ContextConfig {
1341 #[serde(default = "default_active_limit")]
1343 pub active_limit_tokens: usize,
1344 #[serde(default = "default_cache_limit")]
1346 pub cache_limit_entries: usize,
1347}
1348
1349fn default_active_limit() -> usize {
1350 100_000
1351}
1352
1353fn default_cache_limit() -> usize {
1354 50
1355}
1356
1357impl Default for ContextConfig {
1358 fn default() -> Self {
1359 Self {
1360 active_limit_tokens: default_active_limit(),
1361 cache_limit_entries: default_cache_limit(),
1362 }
1363 }
1364}
1365
1366#[derive(Debug, Clone, Deserialize, Serialize)]
1368pub struct SecurityConfig {
1369 #[serde(default = "default_allowed_tools")]
1371 pub allowed_tools: Vec<String>,
1372 #[serde(default)]
1374 pub network_access: bool,
1375 #[serde(default = "default_max_exec_time")]
1377 pub max_execution_time_secs: u64,
1378 #[serde(default = "default_max_memory")]
1380 pub max_memory_mb: u64,
1381 #[serde(default)]
1383 pub can_fork: bool,
1384 #[serde(default = "default_max_audit")]
1386 pub max_audit_entries: usize,
1387 #[serde(default)]
1389 pub auth_enabled: bool,
1390 #[serde(default = "default_cors_origins")]
1392 pub cors_origins: Vec<String>,
1393 #[serde(default)]
1395 pub audit_log_path: Option<String>,
1396 #[serde(default = "default_rate_limit_per_minute")]
1398 pub rate_limit_per_minute: u32,
1399}
1400
1401fn default_allowed_tools() -> Vec<String> {
1402 vec![
1403 "read".to_string(),
1404 "write".to_string(),
1405 "edit".to_string(),
1406 "bash".to_string(),
1407 "grep".to_string(),
1408 "find".to_string(),
1409 "exec".to_string(),
1410 ]
1411}
1412
1413fn default_max_exec_time() -> u64 {
1414 300
1415}
1416
1417fn default_max_memory() -> u64 {
1418 512
1419}
1420
1421fn default_max_audit() -> usize {
1422 10_000
1423}
1424
1425fn default_rate_limit_per_minute() -> u32 {
1426 120
1427}
1428
1429fn default_cors_origins() -> Vec<String> {
1430 vec!["http://localhost:4200".to_string()]
1431}
1432
1433impl Default for SecurityConfig {
1434 fn default() -> Self {
1435 Self {
1436 allowed_tools: default_allowed_tools(),
1437 network_access: false,
1438 max_execution_time_secs: default_max_exec_time(),
1439 max_memory_mb: default_max_memory(),
1440 can_fork: false,
1441 max_audit_entries: default_max_audit(),
1442 auth_enabled: false,
1443 cors_origins: default_cors_origins(),
1444 audit_log_path: None,
1445 rate_limit_per_minute: default_rate_limit_per_minute(),
1446 }
1447 }
1448}
1449
1450#[derive(Debug, Clone, Deserialize, Serialize)]
1452pub struct PersonaConfig {
1453 #[serde(default)]
1455 pub default_persona_id: Option<String>,
1456 #[serde(default = "default_max_concurrent_personas")]
1458 pub max_concurrent_personas: usize,
1459}
1460
1461fn default_max_concurrent_personas() -> usize {
1462 5
1463}
1464
1465impl Default for PersonaConfig {
1466 fn default() -> Self {
1467 Self {
1468 default_persona_id: Some("dev".to_string()),
1469 max_concurrent_personas: default_max_concurrent_personas(),
1470 }
1471 }
1472}
1473
1474#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1482pub struct McpConfig {
1483 #[serde(default)]
1485 pub servers: std::collections::HashMap<String, McpServerDef>,
1486}
1487
1488#[derive(Debug, Clone, Deserialize, Serialize)]
1490pub struct McpServerDef {
1491 pub command: String,
1493 #[serde(default)]
1495 pub args: Vec<String>,
1496 #[serde(default)]
1498 pub env: std::collections::HashMap<String, String>,
1499 #[serde(default = "default_mcp_enabled")]
1501 pub enabled: bool,
1502}
1503
1504fn default_mcp_enabled() -> bool {
1505 true
1506}
1507
1508#[derive(Debug, Clone, Deserialize, Serialize)]
1510pub struct GitConfig {
1511 #[serde(default = "default_true")]
1513 pub auto_commit: bool,
1514}
1515
1516impl Default for GitConfig {
1517 fn default() -> Self {
1518 Self { auto_commit: true }
1519 }
1520}
1521
1522#[derive(Debug, Clone, Deserialize, Serialize)]
1524pub struct AuditConfig {
1525 #[serde(default = "default_audit_max_entries")]
1527 pub max_entries: usize,
1528 #[serde(default = "default_true")]
1530 pub enabled: bool,
1531}
1532
1533fn default_audit_max_entries() -> usize {
1534 100_000
1535}
1536
1537impl Default for AuditConfig {
1538 fn default() -> Self {
1539 Self {
1540 max_entries: default_audit_max_entries(),
1541 enabled: true,
1542 }
1543 }
1544}
1545
1546#[derive(Debug, Clone, Deserialize, Serialize)]
1548pub struct BudgetConfig {
1549 #[serde(default)]
1551 pub default_token_budget: u64,
1552 #[serde(default)]
1554 pub default_calls_budget: u64,
1555 #[serde(default = "default_budget_window")]
1557 pub default_window_secs: u64,
1558 #[serde(default = "default_true")]
1560 pub enabled: bool,
1561}
1562
1563fn default_budget_window() -> u64 {
1564 3600
1565}
1566
1567impl Default for BudgetConfig {
1568 fn default() -> Self {
1569 Self {
1570 default_token_budget: 0,
1571 default_calls_budget: 0,
1572 default_window_secs: default_budget_window(),
1573 enabled: true,
1574 }
1575 }
1576}
1577
1578#[derive(Debug, Clone, Deserialize, Serialize)]
1580pub struct ResourceMonitorConfig {
1581 #[serde(default = "default_rm_interval")]
1583 pub interval_secs: u64,
1584 #[serde(default = "default_rm_history_max")]
1586 pub history_max: usize,
1587 #[serde(default = "default_rm_cpu_threshold")]
1589 pub cpu_threshold: f32,
1590 #[serde(default = "default_rm_mem_threshold")]
1592 pub memory_threshold: f32,
1593 #[serde(default = "default_rm_load_threshold")]
1595 pub load_threshold: f32,
1596}
1597
1598fn default_rm_interval() -> u64 {
1599 60
1600}
1601
1602fn default_rm_history_max() -> usize {
1603 60
1604}
1605
1606fn default_rm_cpu_threshold() -> f32 {
1607 90.0
1608}
1609
1610fn default_rm_mem_threshold() -> f32 {
1611 90.0
1612}
1613
1614fn default_rm_load_threshold() -> f32 {
1615 8.0
1616}
1617
1618impl Default for ResourceMonitorConfig {
1619 fn default() -> Self {
1620 Self {
1621 interval_secs: default_rm_interval(),
1622 history_max: default_rm_history_max(),
1623 cpu_threshold: default_rm_cpu_threshold(),
1624 memory_threshold: default_rm_mem_threshold(),
1625 load_threshold: default_rm_load_threshold(),
1626 }
1627 }
1628}
1629
1630#[derive(Debug, Clone, Deserialize, Serialize)]
1632pub struct OtelConfig {
1633 #[serde(default)]
1635 pub enabled: bool,
1636 #[serde(default = "default_otel_endpoint")]
1638 pub endpoint: String,
1639 #[serde(default = "default_otel_service_name")]
1641 pub service_name: String,
1642 #[serde(default = "default_otel_sampling_ratio")]
1644 pub sampling_ratio: f64,
1645}
1646
1647fn default_otel_endpoint() -> String {
1648 "http://localhost:4317".into()
1649}
1650
1651fn default_otel_service_name() -> String {
1652 "oxios".into()
1653}
1654
1655fn default_otel_sampling_ratio() -> f64 {
1656 1.0
1657}
1658
1659impl Default for OtelConfig {
1660 fn default() -> Self {
1661 Self {
1662 enabled: false,
1663 endpoint: default_otel_endpoint(),
1664 service_name: default_otel_service_name(),
1665 sampling_ratio: default_otel_sampling_ratio(),
1666 }
1667 }
1668}
1669
1670#[derive(Debug, Clone, Deserialize, Serialize)]
1672pub struct LoggingConfig {
1673 #[serde(default = "default_log_format")]
1675 pub format: String,
1676 #[serde(default)]
1678 pub level: Option<String>,
1679}
1680
1681fn default_log_format() -> String {
1682 "pretty".into()
1683}
1684
1685impl Default for LoggingConfig {
1686 fn default() -> Self {
1687 Self {
1688 format: default_log_format(),
1689 level: None,
1690 }
1691 }
1692}
1693
1694#[derive(Debug, Clone, Deserialize, Serialize)]
1700pub struct BrowserConfig {
1701 #[serde(default = "default_browser_enabled")]
1703 pub enabled: bool,
1704
1705 #[serde(default)]
1716 pub engine: serde_json::Value,
1717}
1718
1719fn default_browser_enabled() -> bool {
1720 true
1721}
1722
1723impl Default for BrowserConfig {
1724 fn default() -> Self {
1725 Self {
1726 enabled: true,
1727 engine: serde_json::json!({}),
1728 }
1729 }
1730}
1731
1732pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
1734 let content = std::fs::read_to_string(path)?;
1735 let config: OxiosConfig = toml::from_str(&content)?;
1736 let (errors, warnings) = config.validate();
1737 for w in warnings {
1738 tracing::warn!("config: {}", w);
1739 }
1740 if !errors.is_empty() {
1741 let msg = errors.join("; ");
1742 anyhow::bail!("Configuration validation failed: {msg}");
1743 }
1744 Ok(config)
1745}
1746
1747impl OxiosConfig {
1748 pub fn api_key(&self) -> Option<String> {
1750 self.engine.api_key.clone().filter(|k| !k.is_empty())
1751 }
1752
1753 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
1756 let mut errors = Vec::new();
1757 let mut warnings = Vec::new();
1758
1759 if self.kernel.max_agents == 0 {
1761 errors.push("kernel.max_agents must be > 0".into());
1762 }
1763 if self.kernel.workspace.is_empty() {
1764 errors.push("kernel.workspace must not be empty".into());
1765 }
1766
1767 if self.gateway.port == 0 {
1769 errors.push("gateway.port must be > 0".into());
1770 }
1771 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
1772 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
1773 }
1774
1775 if self.scheduler.max_concurrent == 0 {
1777 warnings.push("scheduler.max_concurrent is 0 — no tasks will run".into());
1778 }
1779 if self.scheduler.zombie_timeout_secs == 0 {
1780 errors.push("scheduler.zombie_timeout_secs must be > 0".into());
1781 }
1782
1783 for (name, job) in &self.cron.jobs {
1785 if job.schedule.is_empty() {
1786 errors.push(format!("cron.jobs.{name}: schedule is empty"));
1787 } else {
1788 let normalized = {
1790 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
1791 match fields.len() {
1792 5 => format!("0 {}", job.schedule),
1793 _ => job.schedule.clone(),
1794 }
1795 };
1796 if Schedule::from_str(&normalized).is_err() {
1797 errors.push(format!(
1798 "cron.jobs.{}: invalid cron expression '{}'",
1799 name, job.schedule
1800 ));
1801 }
1802 }
1803 if job.goal.is_empty() {
1804 errors.push(format!("cron.jobs.{name}: goal is empty"));
1805 }
1806 }
1807
1808 if self.security.max_execution_time_secs == 0 {
1810 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
1811 }
1812
1813 if self.audit.max_entries == 0 {
1815 warnings.push("audit.max_entries is 0 — audit will never prune".into());
1816 }
1817
1818 if self.budget.default_window_secs == 0 {
1820 warnings.push("budget.default_window_secs is 0 — no time window".into());
1821 }
1822
1823 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
1825 {
1826 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
1827 }
1828
1829 if self.exec.default_timeout_secs == 0 {
1831 errors.push("exec.default_timeout_secs must be > 0".into());
1832 }
1833 if self.exec.max_timeout_secs == 0 {
1834 errors.push("exec.max_timeout_secs must be > 0".into());
1835 }
1836 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
1837 errors.push(format!(
1838 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
1839 self.exec.default_timeout_secs, self.exec.max_timeout_secs
1840 ));
1841 }
1842
1843 if self.resource_monitor.cpu_threshold > 100.0 {
1845 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
1846 }
1847 if self.resource_monitor.memory_threshold > 100.0 {
1848 errors.push("resource_monitor.memory_threshold must be <= 100".into());
1849 }
1850
1851 for name in &self.channels.enabled {
1853 let valid = ["cli", "telegram"];
1854 if !valid.contains(&name.as_str()) {
1855 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
1856 }
1857 }
1858 if self.channels.enabled.iter().any(|c| c == "web") {
1860 warnings.push(
1861 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
1862 );
1863 }
1864 if self.channels.enabled.iter().any(|c| c == "telegram")
1865 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
1866 {
1867 warnings.push(format!(
1868 "channels.telegram: {} env var not set — telegram channel will fail",
1869 self.channels.telegram.bot_token_env
1870 ));
1871 }
1872
1873 (errors, warnings)
1874 }
1875}
1876
1877pub fn expand_home(path: &str) -> std::path::PathBuf {
1881 if let Some(rest) = path.strip_prefix("~/") {
1882 if let Ok(home) = std::env::var("HOME") {
1883 return std::path::PathBuf::from(format!("{home}/{rest}"));
1884 }
1885 }
1886 std::path::PathBuf::from(path)
1887}
1888
1889#[cfg(test)]
1890mod tests {
1891 use super::*;
1892
1893 #[test]
1894 fn test_default_config_validates() {
1895 let config = OxiosConfig::default();
1896 let (errors, _warnings) = config.validate();
1897 assert!(
1898 errors.is_empty(),
1899 "Default config should have no errors: {:?}",
1900 errors
1901 );
1902 }
1903
1904 #[test]
1905 fn test_exec_config_default_allowed_commands() {
1906 let config = ExecConfig::default();
1907 assert!(config.allowed_commands.is_empty());
1909 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
1910 assert!(!config.is_binary_allowed("anything"));
1911 assert!(!config.is_binary_allowed("bash"));
1912 }
1913
1914 #[test]
1915 fn test_exec_config_permissive_mode() {
1916 let config = ExecConfig {
1917 allowlist_mode: AllowlistMode::Permissive,
1918 ..Default::default()
1919 };
1920 assert!(config.is_binary_allowed("anything"));
1922 assert!(config.is_binary_allowed("bash"));
1923 }
1924
1925 #[test]
1926 fn test_is_binary_allowed_with_allowlist() {
1927 let config = ExecConfig {
1928 allowed_commands: vec!["git".into(), "echo".into()],
1929 ..Default::default()
1930 };
1931 assert!(config.is_binary_allowed("git"));
1932 assert!(config.is_binary_allowed("echo"));
1933 assert!(!config.is_binary_allowed("bash"));
1934 assert!(!config.is_binary_allowed("rm"));
1935 assert!(!config.is_binary_allowed("sudo"));
1936 }
1937
1938 #[test]
1939 fn test_expand_home() {
1940 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
1942 let expanded = expand_home("~/projects/test");
1943 assert_eq!(
1944 expanded.to_str().unwrap(),
1945 format!("{}/projects/test", home)
1946 );
1947
1948 let abs = expand_home("/absolute/path");
1950 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
1951
1952 let bare = expand_home("~something");
1954 assert_eq!(bare, std::path::PathBuf::from("~something"));
1955 }
1956
1957 #[test]
1958 fn test_invalid_cron_expression() {
1959 let mut config = OxiosConfig::default();
1960 config.cron.enabled = true;
1961 config.cron.jobs.insert(
1962 "bad-job".to_string(),
1963 InlineCronJob {
1964 schedule: "not a valid cron".to_string(),
1965 goal: "Test goal".to_string(),
1966 constraints: vec![],
1967 acceptance_criteria: vec![],
1968 toolchain: "default".to_string(),
1969 priority: Priority::Normal,
1970 enabled: true,
1971 },
1972 );
1973
1974 let (errors, _warnings) = config.validate();
1975 assert!(
1976 !errors.is_empty(),
1977 "Expected validation error for invalid cron"
1978 );
1979 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
1980 assert!(
1981 has_cron_error,
1982 "Expected 'invalid cron expression' error, got: {:?}",
1983 errors
1984 );
1985 }
1986
1987 #[test]
1988 fn test_config_serialization_roundtrip() {
1989 let config = OxiosConfig::default();
1990
1991 let toml_str = toml::to_string(&config).expect("serialization should succeed");
1993
1994 let deserialized: OxiosConfig =
1996 toml::from_str(&toml_str).expect("deserialization should succeed");
1997
1998 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2000 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2001 assert_eq!(config.gateway.host, deserialized.gateway.host);
2002 assert_eq!(config.gateway.port, deserialized.gateway.port);
2003 assert_eq!(
2004 config.exec.default_timeout_secs,
2005 deserialized.exec.default_timeout_secs
2006 );
2007 assert_eq!(
2008 config.exec.max_timeout_secs,
2009 deserialized.exec.max_timeout_secs
2010 );
2011 }
2012
2013 #[test]
2014 fn test_exec_timeout_validation() {
2015 let mut config = OxiosConfig::default();
2016 config.exec.default_timeout_secs = 999;
2018 config.exec.max_timeout_secs = 100;
2019 let (errors, _warnings) = config.validate();
2020 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2021 assert!(
2022 has_error,
2023 "Expected timeout ordering error, got: {:?}",
2024 errors
2025 );
2026 }
2027
2028 #[test]
2029 fn test_zero_max_agents_error() {
2030 let mut config = OxiosConfig::default();
2031 config.kernel.max_agents = 0;
2032 let (errors, _warnings) = config.validate();
2033 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2034 }
2035
2036 #[test]
2041 fn test_default_config_matches_toml() {
2042 let from_rust = OxiosConfig::default();
2043
2044 let toml_str = include_str!("../../../share/default-config.toml");
2045 let from_toml: OxiosConfig =
2046 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2047
2048 assert_eq!(
2050 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2051 "kernel.max_agents 불일치: Rust={}, TOML={}",
2052 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2053 );
2054 assert_eq!(
2055 from_rust.gateway.host, from_toml.gateway.host,
2056 "gateway.host 불일치: Rust={}, TOML={}",
2057 from_rust.gateway.host, from_toml.gateway.host
2058 );
2059 assert_eq!(
2060 from_rust.gateway.port, from_toml.gateway.port,
2061 "gateway.port 불일치: Rust={}, TOML={}",
2062 from_rust.gateway.port, from_toml.gateway.port
2063 );
2064 assert_eq!(
2065 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2066 "kernel.event_bus_capacity 불일치"
2067 );
2068 assert_eq!(
2069 from_rust.scheduler.max_concurrent, from_toml.scheduler.max_concurrent,
2070 "scheduler.max_concurrent 불일치"
2071 );
2072 assert_eq!(
2073 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2074 "memory.consolidation.preset 불일치"
2075 );
2076
2077 let (_, warnings) = from_toml.validate();
2079 for w in &warnings {
2080 eprintln!("default-config.toml 경고: {}", w);
2081 }
2082 }
2083
2084 #[test]
2087 fn test_gateway_should_expose_api_docs() {
2088 let cfg = GatewayConfig::default();
2090 assert!(!cfg.should_expose_api_docs());
2091
2092 let cfg = GatewayConfig {
2094 host: "0.0.0.0".into(),
2095 port: 4200,
2096 expose_api_docs: true,
2097 };
2098 assert!(
2099 !cfg.should_expose_api_docs(),
2100 "public bind must not expose api docs even when opt-in is true"
2101 );
2102
2103 let cfg = GatewayConfig {
2105 host: "127.0.0.1".into(),
2106 port: 4200,
2107 expose_api_docs: true,
2108 };
2109 assert!(cfg.should_expose_api_docs());
2110
2111 let cfg = GatewayConfig {
2113 host: "::1".into(),
2114 port: 4200,
2115 expose_api_docs: true,
2116 };
2117 assert!(cfg.should_expose_api_docs());
2118
2119 let cfg = GatewayConfig {
2121 host: "localhost".into(),
2122 port: 4200,
2123 expose_api_docs: true,
2124 };
2125 assert!(cfg.should_expose_api_docs());
2126
2127 let cfg = GatewayConfig {
2129 host: "127.0.0.1".into(),
2130 port: 4200,
2131 expose_api_docs: false,
2132 };
2133 assert!(!cfg.should_expose_api_docs());
2134 }
2135}