1#![allow(missing_docs)]
2use cron::Schedule;
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11use crate::email::{SmtpProvider, SmtpTls};
12use crate::types::Priority;
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct CronConfig {
17 #[serde(default)]
19 pub enabled: bool,
20 #[serde(default = "default_tick_interval")]
22 pub tick_interval_secs: u64,
23 #[serde(default)]
25 pub jobs: std::collections::HashMap<String, InlineCronJob>,
26}
27
28impl Default for CronConfig {
29 fn default() -> Self {
30 Self {
31 enabled: false,
32 tick_interval_secs: default_tick_interval(),
33 jobs: std::collections::HashMap::new(),
34 }
35 }
36}
37
38fn default_tick_interval() -> u64 {
39 60
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct InlineCronJob {
45 pub schedule: String,
47 pub goal: String,
49 #[serde(default)]
51 pub constraints: Vec<String>,
52 #[serde(default)]
54 pub acceptance_criteria: Vec<String>,
55 #[serde(default = "default_toolchain_inline")]
57 pub toolchain: String,
58 #[serde(default)]
60 pub priority: Priority,
61 #[serde(default = "default_true_inline")]
63 pub enabled: bool,
64}
65
66fn default_toolchain_inline() -> String {
67 "default".into()
68}
69
70fn default_true_inline() -> bool {
71 true
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct MemoryConfig {
77 #[serde(default = "default_true")]
79 pub enabled: bool,
80 #[serde(default = "default_max_recall")]
82 pub max_recall: usize,
83 #[serde(default = "default_true")]
85 pub auto_summarize: bool,
86 #[serde(default = "default_true")]
88 pub capture_compaction: bool,
89 #[serde(default)]
91 pub retention_days: u32,
92 #[serde(default = "default_true")]
94 pub cache_enabled: bool,
95 #[serde(default = "default_cache_ttl")]
97 pub cache_ttl_secs: u64,
98 #[serde(default = "default_cache_max_entries")]
100 pub cache_max_entries: usize,
101 #[serde(default)]
103 pub consolidation: ConsolidationConfig,
104 #[serde(default)]
106 pub sqlite: SqliteMemoryConfig,
107 #[serde(default)]
109 pub embedding: EmbeddingConfig,
110 #[serde(default)]
112 pub learning: LearningConfig,
113 #[serde(default)]
115 pub knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig,
116 #[serde(default)]
118 pub bridge: MemoryBridgeConfig,
119}
120
121fn default_true() -> bool {
122 true
123}
124
125fn default_max_recall() -> usize {
126 10
127}
128
129fn default_cache_ttl() -> u64 {
130 3600 }
132
133fn default_cache_max_entries() -> usize {
134 10000
135}
136
137impl Default for MemoryConfig {
138 fn default() -> Self {
139 Self {
140 enabled: true,
141 max_recall: 10,
142 auto_summarize: true,
143 capture_compaction: true,
144 retention_days: 0,
145 cache_enabled: true,
146 cache_ttl_secs: 3600,
147 cache_max_entries: 10000,
148 consolidation: ConsolidationConfig::default(),
149 sqlite: SqliteMemoryConfig::default(),
150 embedding: EmbeddingConfig::default(),
151 learning: LearningConfig::default(),
152 knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig::default(),
153 bridge: MemoryBridgeConfig::default(),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct SqliteMemoryConfig {
169 #[serde(default = "default_true")]
171 pub enabled: bool,
172 #[serde(default)]
175 pub path: String,
176 #[serde(default = "default_embedding_dim")]
180 pub embedding_dim: usize,
181 #[serde(default = "default_true")]
183 pub wal_mode: bool,
184}
185
186fn default_embedding_dim() -> usize {
187 256
188}
189
190impl Default for SqliteMemoryConfig {
191 fn default() -> Self {
192 Self {
193 enabled: true,
194 path: String::new(),
195 embedding_dim: 256,
196 wal_mode: true,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct EmbeddingConfig {
213 #[serde(default = "default_embedding_provider")]
215 pub provider: String,
216 #[serde(default = "default_embedding_dim")]
219 pub dimension: usize,
220 #[serde(default = "default_model_ttl")]
223 pub model_ttl_secs: u64,
224}
225
226fn default_embedding_provider() -> String {
227 "gguf".to_string()
228}
229
230fn default_model_ttl() -> u64 {
231 300 }
233
234impl Default for EmbeddingConfig {
235 fn default() -> Self {
236 Self {
237 provider: default_embedding_provider(),
238 dimension: default_embedding_dim(),
239 model_ttl_secs: default_model_ttl(),
240 }
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct LearningConfig {
253 #[serde(default = "default_true")]
255 pub enabled: bool,
256 #[serde(default = "default_sona_mode")]
258 pub sona_mode: String,
259 #[serde(default = "default_distill_interval")]
261 pub distill_interval_hours: u64,
262 #[serde(default = "default_auto_promote_quality")]
264 pub auto_promote_quality: f32,
265 #[serde(default = "default_auto_promote_min_usage")]
267 pub auto_promote_min_usage: u32,
268}
269
270fn default_sona_mode() -> String {
271 "balanced".to_string()
272}
273
274fn default_distill_interval() -> u64 {
275 6
276}
277
278fn default_auto_promote_quality() -> f32 {
279 0.8
280}
281
282fn default_auto_promote_min_usage() -> u32 {
283 3
284}
285
286impl Default for LearningConfig {
287 fn default() -> Self {
288 Self {
289 enabled: true,
290 sona_mode: default_sona_mode(),
291 distill_interval_hours: default_distill_interval(),
292 auto_promote_quality: default_auto_promote_quality(),
293 auto_promote_min_usage: default_auto_promote_min_usage(),
294 }
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct MemoryBridgeConfig {
308 #[serde(default)]
310 pub sync_enabled: bool,
311 #[serde(default = "default_bridge_interval")]
313 pub interval_secs: u64,
314}
315
316fn default_bridge_interval() -> u64 {
317 3600
318}
319
320impl Default for MemoryBridgeConfig {
321 fn default() -> Self {
322 Self {
323 sync_enabled: false,
324 interval_secs: default_bridge_interval(),
325 }
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct ConsolidationConfig {
337 #[serde(default = "default_preset")]
341 pub preset: String,
342
343 #[serde(default = "default_true")]
345 pub dream_enabled: bool,
346 #[serde(default = "default_dream_interval")]
347 pub dream_interval_hours: u64,
348 #[serde(default = "default_dream_min_sessions")]
349 pub dream_min_sessions: u32,
350
351 #[serde(default = "default_hot_max")]
353 pub hot_max_entries: usize,
354 #[serde(default = "default_warm_max")]
355 pub warm_max_entries: usize,
356 #[serde(default = "default_cold_max")]
357 pub cold_max_entries: usize,
358 #[serde(default = "default_hot_token_budget")]
359 pub hot_token_budget: usize,
360
361 #[serde(default = "default_true")]
363 pub decay_enabled: bool,
364 #[serde(default = "default_one")]
365 pub decay_multiplier: f32,
366 #[serde(default = "default_decay_threshold")]
367 pub decay_threshold: f32,
368 #[serde(default = "default_retention_days")]
369 pub retention_days: u32,
370
371 #[serde(default = "default_true")]
373 pub auto_protection: bool,
374 #[serde(default = "default_protection_low_access")]
375 pub protection_low_access: u32,
376 #[serde(default = "default_protection_medium_access")]
377 pub protection_medium_access: u32,
378 #[serde(default = "default_protection_high_access")]
379 pub protection_high_access: u32,
380 #[serde(default = "default_protection_medium_sessions")]
381 pub protection_medium_sessions: u32,
382 #[serde(default = "default_protection_high_sessions")]
383 pub protection_high_sessions: u32,
384
385 #[serde(default = "default_true")]
387 pub auto_classification: bool,
388 #[serde(default = "default_type_promotion_threshold")]
389 pub type_promotion_repetitions: u32,
390
391 #[serde(default = "default_compaction_threshold")]
393 pub compaction_line_threshold: usize,
394 #[serde(default = "default_true")]
395 pub llm_compaction: bool,
396
397 #[serde(default)]
400 pub dream_model: Option<String>,
401
402 #[serde(default = "default_true")]
404 pub protection_demotion_enabled: bool,
405 #[serde(default = "default_demotion_stale_days")]
406 pub protection_demotion_stale_days: u32,
407 #[serde(default = "default_demotion_max_step")]
408 pub protection_demotion_max_step: u32,
409
410 #[serde(default = "default_true")]
412 pub proactive_recall: bool,
413 #[serde(default = "default_proactive_limit")]
414 pub proactive_recall_limit: usize,
415 #[serde(default = "default_proactive_threshold")]
416 pub proactive_recall_threshold: f32,
417}
418
419fn default_dream_interval() -> u64 {
420 24
421}
422fn default_dream_min_sessions() -> u32 {
423 5
424}
425fn default_hot_max() -> usize {
426 50
427}
428fn default_warm_max() -> usize {
429 500
430}
431fn default_cold_max() -> usize {
432 10_000
433}
434fn default_hot_token_budget() -> usize {
435 3_000
436}
437fn default_one() -> f32 {
438 1.0
439}
440fn default_decay_threshold() -> f32 {
441 0.05
442}
443fn default_retention_days() -> u32 {
444 90
445}
446fn default_protection_low_access() -> u32 {
447 2
448}
449fn default_protection_medium_access() -> u32 {
450 3
451}
452fn default_protection_high_access() -> u32 {
453 5
454}
455fn default_protection_medium_sessions() -> u32 {
456 2
457}
458fn default_protection_high_sessions() -> u32 {
459 3
460}
461fn default_type_promotion_threshold() -> u32 {
462 3
463}
464fn default_compaction_threshold() -> usize {
465 200
466}
467fn default_proactive_limit() -> usize {
468 5
469}
470fn default_proactive_threshold() -> f32 {
471 0.6
472}
473fn default_demotion_stale_days() -> u32 {
474 30
475}
476fn default_demotion_max_step() -> u32 {
477 1
478}
479
480fn default_preset() -> String {
481 "balanced".into()
482}
483
484impl Default for ConsolidationConfig {
485 fn default() -> Self {
486 Self {
487 preset: default_preset(),
488 dream_enabled: true,
489 dream_interval_hours: 24,
490 dream_min_sessions: 5,
491 hot_max_entries: 50,
492 warm_max_entries: 500,
493 cold_max_entries: 10_000,
494 hot_token_budget: 3_000,
495 decay_enabled: true,
496 decay_multiplier: 1.0,
497 decay_threshold: 0.05,
498 retention_days: 90,
499 auto_protection: true,
500 protection_low_access: 2,
501 protection_medium_access: 3,
502 protection_high_access: 5,
503 protection_medium_sessions: 2,
504 protection_high_sessions: 3,
505 auto_classification: true,
506 type_promotion_repetitions: 3,
507 compaction_line_threshold: 200,
508 llm_compaction: true,
509 dream_model: None,
510 protection_demotion_enabled: true,
511 protection_demotion_stale_days: 30,
512 protection_demotion_max_step: 1,
513 proactive_recall: true,
514 proactive_recall_limit: 5,
515 proactive_recall_threshold: 0.6,
516 }
517 }
518}
519
520impl ConsolidationConfig {
521 pub fn apply_preset(&mut self) {
525 let resolved = match self.preset.as_str() {
526 "conservative" => Self::conservative(),
527 "aggressive" => Self::aggressive(),
528 "custom" => return,
529 _ => Self::default(), };
531 *self = resolved;
532 }
533
534 fn conservative() -> Self {
536 Self {
537 preset: "conservative".into(),
538 dream_enabled: true,
539 dream_interval_hours: 48,
540 dream_min_sessions: 10,
541 hot_max_entries: 100,
542 warm_max_entries: 1000,
543 cold_max_entries: 50_000,
544 hot_token_budget: 5_000,
545 decay_enabled: true,
546 decay_multiplier: 0.8,
547 decay_threshold: 0.05,
548 retention_days: 365,
549 auto_protection: true,
550 protection_low_access: 3,
551 protection_medium_access: 5,
552 protection_high_access: 10,
553 protection_medium_sessions: 3,
554 protection_high_sessions: 5,
555 auto_classification: true,
556 type_promotion_repetitions: 5,
557 compaction_line_threshold: 300,
558 llm_compaction: true,
559 dream_model: None,
560 protection_demotion_enabled: true,
561 protection_demotion_stale_days: 90,
562 protection_demotion_max_step: 1,
563 proactive_recall: true,
564 proactive_recall_limit: 8,
565 proactive_recall_threshold: 0.5,
566 }
567 }
568
569 fn aggressive() -> Self {
571 Self {
572 preset: "aggressive".into(),
573 dream_enabled: true,
574 dream_interval_hours: 4,
575 dream_min_sessions: 2,
576 hot_max_entries: 20,
577 warm_max_entries: 100,
578 cold_max_entries: 1_000,
579 hot_token_budget: 2_000,
580 decay_enabled: true,
581 decay_multiplier: 1.0,
582 decay_threshold: 0.1,
583 retention_days: 30,
584 auto_protection: true,
585 protection_low_access: 1,
586 protection_medium_access: 2,
587 protection_high_access: 3,
588 protection_medium_sessions: 1,
589 protection_high_sessions: 2,
590 auto_classification: true,
591 type_promotion_repetitions: 2,
592 compaction_line_threshold: 150,
593 llm_compaction: true,
594 dream_model: None,
595 protection_demotion_enabled: true,
596 protection_demotion_stale_days: 14,
597 protection_demotion_max_step: 2,
598 proactive_recall: true,
599 proactive_recall_limit: 3,
600 proactive_recall_threshold: 0.7,
601 }
602 }
603}
604
605#[derive(Debug, Clone, Deserialize, Serialize, Default)]
607pub struct ChannelsConfig {
608 #[serde(default)]
611 pub enabled: Vec<String>,
612
613 #[serde(default)]
615 pub telegram: TelegramChannelConfig,
616}
617
618#[derive(Debug, Clone, Deserialize, Serialize)]
623pub struct SurfacesConfig {
624 #[serde(default = "default_surfaces_enabled")]
627 pub enabled: Vec<String>,
628}
629
630fn default_surfaces_enabled() -> Vec<String> {
631 vec!["web".to_string()]
632}
633
634impl Default for SurfacesConfig {
635 fn default() -> Self {
636 Self {
637 enabled: default_surfaces_enabled(),
638 }
639 }
640}
641
642#[derive(Debug, Clone, Deserialize, Serialize)]
644pub struct TelegramChannelConfig {
645 #[serde(default = "default_telegram_token_env")]
647 pub bot_token_env: String,
648 #[serde(default)]
650 pub allowed_users: Vec<i64>,
651 #[serde(default)]
653 pub session: TelegramSessionConfig,
654}
655
656fn default_telegram_token_env() -> String {
657 "TELEGRAM_BOT_TOKEN".to_string()
658}
659
660impl Default for TelegramChannelConfig {
661 fn default() -> Self {
662 Self {
663 bot_token_env: default_telegram_token_env(),
664 allowed_users: Vec::new(),
665 session: TelegramSessionConfig::default(),
666 }
667 }
668}
669#[derive(Debug, Clone, Serialize, Deserialize, Default)]
673pub struct RoleRoutingConfig {
674 #[serde(default)]
676 pub roles: std::collections::HashMap<String, String>,
677}
678
679#[derive(Debug, Clone, Deserialize, Serialize)]
681#[allow(clippy::derivable_impls)]
682pub struct EngineConfig {
683 #[serde(default)]
686 pub default_model: String,
687 #[serde(default, skip_serializing)]
691 pub api_key: Option<String>,
692 #[serde(default)]
695 pub provider_options: Option<oxi_sdk::ProviderOptions>,
696 #[serde(default)]
700 pub routing_enabled: bool,
701 #[serde(default)]
703 pub prefer_cost_efficient: bool,
704 #[serde(default)]
706 pub fallback_models: Vec<String>,
707 #[serde(default)]
709 pub excluded_models: Vec<String>,
710 #[serde(default)]
714 pub role_routing: RoleRoutingConfig,
715 #[serde(default)]
719 pub quick_ask_model: Option<String>,
720}
721
722#[allow(clippy::derivable_impls)]
723impl Default for EngineConfig {
724 fn default() -> Self {
725 Self {
726 default_model: String::new(),
727 api_key: None,
728 provider_options: None,
729 routing_enabled: false,
730 prefer_cost_efficient: false,
731 fallback_models: Vec::new(),
732 excluded_models: Vec::new(),
733 role_routing: RoleRoutingConfig::default(),
734 quick_ask_model: None,
735 }
736 }
737}
738
739#[derive(Debug, Clone, Deserialize, Serialize)]
741pub struct DaemonConfig {
742 #[serde(default = "default_pid_file")]
744 pub pid_file: String,
745 #[serde(default = "default_daemon_log_dir")]
747 pub log_dir: String,
748}
749
750fn default_pid_file() -> String {
751 dirs::home_dir()
752 .map(|h| format!("{}/.oxios/oxios.pid", h.display()))
753 .unwrap_or_else(|| "./oxios.pid".into())
754}
755
756fn default_daemon_log_dir() -> String {
757 dirs::home_dir()
758 .map(|h| format!("{}/.oxios/logs", h.display()))
759 .unwrap_or_else(|| "./logs".into())
760}
761
762impl Default for DaemonConfig {
763 fn default() -> Self {
764 Self {
765 pid_file: default_pid_file(),
766 log_dir: default_daemon_log_dir(),
767 }
768 }
769}
770
771#[derive(Debug, Clone, Deserialize, Serialize)]
773pub struct SessionConfig {
774 #[serde(default = "default_max_sessions")]
778 pub max_sessions: usize,
779
780 #[serde(default = "default_session_ttl_hours")]
784 pub ttl_hours: u64,
785
786 #[serde(default = "default_true")]
788 pub auto_prune: bool,
789}
790
791fn default_max_sessions() -> usize {
792 100
793}
794
795fn default_session_ttl_hours() -> u64 {
796 168 }
798
799impl Default for SessionConfig {
800 fn default() -> Self {
801 Self {
802 max_sessions: default_max_sessions(),
803 ttl_hours: default_session_ttl_hours(),
804 auto_prune: true,
805 }
806 }
807}
808
809#[derive(Debug, Clone, Deserialize, Serialize)]
813pub struct MountsConfig {
814 #[serde(default = "default_true")]
816 pub auto_promote_enabled: bool,
817 #[serde(default = "default_promote_threshold")]
819 pub auto_promote_threshold: usize,
820 #[serde(default = "default_promote_window_days")]
822 pub auto_promote_window_days: i64,
823 #[serde(default = "default_promote_interval_secs")]
825 pub auto_promote_interval_secs: u64,
826}
827
828fn default_promote_threshold() -> usize {
829 3
830}
831
832fn default_promote_window_days() -> i64 {
833 14
834}
835
836fn default_promote_interval_secs() -> u64 {
837 3600 }
839
840impl Default for MountsConfig {
841 fn default() -> Self {
842 Self {
843 auto_promote_enabled: true,
844 auto_promote_threshold: default_promote_threshold(),
845 auto_promote_window_days: default_promote_window_days(),
846 auto_promote_interval_secs: default_promote_interval_secs(),
847 }
848 }
849}
850
851#[derive(Debug, Clone, Deserialize, Serialize)]
853pub struct TelegramSessionConfig {
854 #[serde(default = "default_telegram_session_rotation_hours")]
857 pub rotation_hours: u64,
858
859 #[serde(default = "default_telegram_session_max_messages")]
862 pub max_messages: usize,
863}
864
865fn default_telegram_session_rotation_hours() -> u64 {
866 2 }
868
869fn default_telegram_session_max_messages() -> usize {
870 0 }
872
873impl Default for TelegramSessionConfig {
874 fn default() -> Self {
875 Self {
876 rotation_hours: default_telegram_session_rotation_hours(),
877 max_messages: default_telegram_session_max_messages(),
878 }
879 }
880}
881
882#[derive(Debug, Clone, Deserialize, Serialize, Default)]
884pub struct OxiosConfig {
885 pub kernel: KernelConfig,
887 #[serde(default)]
889 pub engine: EngineConfig,
890 #[serde(default)]
892 pub daemon: DaemonConfig,
893 #[serde(default)]
895 pub gateway: GatewayConfig,
896 #[serde(default)]
898 pub orchestrator: OrchestratorConfig,
899 #[serde(default)]
901 pub context: ContextConfig,
902 #[serde(default)]
904 pub security: SecurityConfig,
905 #[serde(default)]
907 pub persona: PersonaConfig,
908 #[serde(default)]
910 pub memory: MemoryConfig,
911 #[serde(default)]
913 pub cron: CronConfig,
914 #[serde(default)]
916 pub mcp: McpConfig,
917 #[serde(default)]
919 pub git: GitConfig,
920 #[serde(default)]
922 pub audit: AuditConfig,
923 #[serde(default)]
925 pub budget: BudgetConfig,
926 #[serde(default)]
928 pub exec: ExecConfig,
929 #[serde(default)]
931 pub resource_monitor: ResourceMonitorConfig,
932 #[serde(default)]
934 pub logging: LoggingConfig,
935 #[serde(default)]
937 pub channels: ChannelsConfig,
938 #[serde(default)]
940 pub surfaces: Option<SurfacesConfig>,
941 #[serde(default)]
943 pub browser: BrowserConfig,
944 #[serde(default)]
946 pub session: SessionConfig,
947 #[serde(default)]
949 pub mounts: MountsConfig,
950 #[serde(default)]
952 pub marketplace: MarketplaceConfig,
953 #[serde(default)]
955 pub calendar: CalendarConfig,
956 #[serde(default)]
958 pub email: EmailConfig,
959 #[serde(default)]
961 pub agent_log: AgentLogConfig,
962 #[serde(default)]
964 pub token_maxing: crate::token_maxing::TokenMaxingConfig,
965}
966
967#[derive(Debug, Clone, Deserialize, Serialize)]
969pub struct KernelConfig {
970 #[serde(default = "default_workspace")]
972 pub workspace: String,
973 #[serde(default = "default_event_bus_capacity")]
975 pub event_bus_capacity: usize,
976 #[serde(default = "default_max_agents")]
978 pub max_agents: usize,
979}
980
981fn default_workspace() -> String {
982 dirs_home().unwrap_or_else(|| ".".into())
983}
984
985fn dirs_home() -> Option<String> {
986 dirs::home_dir().map(|h| format!("{}/.oxios/workspace", h.display()))
987}
988
989fn default_event_bus_capacity() -> usize {
990 256
991}
992
993fn default_max_agents() -> usize {
994 10
995}
996
997impl Default for KernelConfig {
998 fn default() -> Self {
999 Self {
1000 workspace: default_workspace(),
1001 event_bus_capacity: default_event_bus_capacity(),
1002 max_agents: 10,
1003 }
1004 }
1005}
1006
1007#[derive(Debug, Clone, Deserialize, Serialize)]
1009pub struct GatewayConfig {
1010 #[serde(default = "default_gateway_host")]
1012 pub host: String,
1013 #[serde(default = "default_gateway_port")]
1015 pub port: u16,
1016 #[serde(default)]
1026 pub expose_api_docs: bool,
1027 #[serde(default = "default_response_timeout_secs")]
1031 pub response_timeout_secs: u64,
1032 #[serde(default)]
1034 pub reliability: GatewayReliabilityConfig,
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1039pub struct GatewayReliabilityConfig {
1040 #[serde(default = "default_replay_buffer_size")]
1043 pub replay_buffer_size: usize,
1044 #[serde(default = "default_replay_ttl_secs")]
1046 pub replay_ttl_secs: u64,
1047}
1048
1049impl Default for GatewayReliabilityConfig {
1050 fn default() -> Self {
1051 Self {
1052 replay_buffer_size: default_replay_buffer_size(),
1053 replay_ttl_secs: default_replay_ttl_secs(),
1054 }
1055 }
1056}
1057
1058fn default_response_timeout_secs() -> u64 {
1059 120
1060}
1061fn default_replay_buffer_size() -> usize {
1062 512
1063}
1064fn default_replay_ttl_secs() -> u64 {
1065 60
1066}
1067
1068impl GatewayConfig {
1069 pub fn should_expose_api_docs(&self) -> bool {
1075 if !self.expose_api_docs {
1076 return false;
1077 }
1078 let h = self.host.trim();
1079 h == "127.0.0.1" || h == "::1" || h == "localhost" || h.starts_with("127.")
1080 }
1081}
1082
1083#[derive(Debug, Clone, Deserialize, Serialize)]
1085pub struct MarketplaceConfig {
1086 #[serde(default)]
1089 pub base_url: Option<String>,
1090 #[serde(default = "default_true")]
1092 pub enabled: bool,
1093 #[serde(default)]
1095 pub skills_sh: SkillsShConfig,
1096}
1097
1098#[derive(Debug, Clone, Deserialize, Serialize)]
1100pub struct SkillsShConfig {
1101 #[serde(default)]
1104 pub base_url: Option<String>,
1105 #[serde(default)]
1108 pub api_key: Option<String>,
1109 #[serde(default = "default_true")]
1111 pub enabled: bool,
1112}
1113
1114impl Default for MarketplaceConfig {
1115 fn default() -> Self {
1116 Self {
1117 base_url: Some("https://clawhub.ai".to_string()),
1118 enabled: true,
1119 skills_sh: SkillsShConfig::default(),
1120 }
1121 }
1122}
1123
1124impl Default for SkillsShConfig {
1125 fn default() -> Self {
1126 Self {
1127 base_url: None,
1128 api_key: None,
1129 enabled: true,
1130 }
1131 }
1132}
1133
1134#[derive(Debug, Clone, Deserialize, Serialize)]
1136pub struct CalendarConfig {
1137 #[serde(default = "default_true")]
1139 pub enabled: bool,
1140 #[serde(default = "default_calendar_timezone")]
1142 pub timezone: String,
1143 #[serde(default = "default_reminder_minutes")]
1145 pub default_reminder_minutes: Vec<u32>,
1146 #[serde(default)]
1148 pub alarm_channels: Vec<String>,
1149 #[serde(default = "default_journal_sync")]
1151 pub journal_sync: String,
1152 #[serde(default = "default_true")]
1154 pub system_calendar: bool,
1155 #[serde(default = "default_archive_days")]
1157 pub archive_after_days: u32,
1158}
1159
1160fn default_calendar_timezone() -> String {
1161 "Asia/Seoul".to_string()
1162}
1163
1164fn default_reminder_minutes() -> Vec<u32> {
1165 vec![15]
1166}
1167
1168fn default_journal_sync() -> String {
1169 "on_open".to_string()
1170}
1171
1172fn default_archive_days() -> u32 {
1173 365
1174}
1175
1176impl Default for CalendarConfig {
1177 fn default() -> Self {
1178 Self {
1179 enabled: true,
1180 timezone: default_calendar_timezone(),
1181 default_reminder_minutes: default_reminder_minutes(),
1182 alarm_channels: vec![],
1183 journal_sync: default_journal_sync(),
1184 system_calendar: true,
1185 archive_after_days: default_archive_days(),
1186 }
1187 }
1188}
1189
1190#[derive(Debug, Clone, Deserialize, Serialize)]
1195pub struct EmailConfig {
1196 #[serde(default)]
1198 pub enabled: bool,
1199 #[serde(default)]
1201 pub my_email: String,
1202 #[serde(default = "default_email_provider")]
1204 pub provider: SmtpProvider,
1205 #[serde(default)]
1207 pub host: String,
1208 #[serde(default)]
1210 pub port: u16,
1211 #[serde(default)]
1213 pub tls: Option<SmtpTls>,
1214 #[serde(default)]
1216 pub user: String,
1217 #[serde(default = "default_email_secret_ref")]
1220 pub secret_ref: String,
1221 #[serde(default = "default_rate_limit_emails")]
1223 pub rate_limit_per_hour: usize,
1224}
1225
1226fn default_email_provider() -> SmtpProvider {
1227 SmtpProvider::Gmail
1228}
1229
1230fn default_email_secret_ref() -> String {
1231 "email_smtp".to_string()
1232}
1233
1234fn default_rate_limit_emails() -> usize {
1235 10
1236}
1237
1238impl Default for EmailConfig {
1239 fn default() -> Self {
1240 Self {
1241 enabled: false,
1242 my_email: String::new(),
1243 provider: default_email_provider(),
1244 host: String::new(),
1245 port: 0,
1246 tls: None,
1247 user: String::new(),
1248 secret_ref: default_email_secret_ref(),
1249 rate_limit_per_hour: default_rate_limit_emails(),
1250 }
1251 }
1252}
1253
1254impl EmailConfig {
1255 pub fn provider(&self) -> SmtpProvider {
1257 self.provider
1258 }
1259}
1260
1261fn default_gateway_host() -> String {
1262 "127.0.0.1".into()
1263}
1264
1265fn default_gateway_port() -> u16 {
1266 4200
1267}
1268
1269impl Default for GatewayConfig {
1270 fn default() -> Self {
1271 Self {
1272 host: default_gateway_host(),
1273 port: default_gateway_port(),
1274 expose_api_docs: false,
1275 response_timeout_secs: default_response_timeout_secs(),
1276 reliability: GatewayReliabilityConfig::default(),
1277 }
1278 }
1279}
1280
1281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1286#[serde(rename_all = "lowercase")]
1287pub enum ExecMode {
1288 #[default]
1290 Structured,
1291 Shell,
1293}
1294
1295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1297#[serde(rename_all = "snake_case")]
1298#[derive(Default)]
1299pub enum AllowlistMode {
1300 Permissive,
1302 #[default]
1304 Enforced,
1305}
1306
1307#[derive(Debug, Clone, Deserialize, Serialize)]
1311pub struct ExecConfig {
1312 #[serde(default)]
1314 pub default_mode: ExecMode,
1315 #[serde(default = "default_false")]
1317 pub allow_shell_mode: bool,
1318 #[serde(default)]
1321 pub allowed_commands: Vec<String>,
1322 #[serde(default)]
1326 pub allowlist_mode: AllowlistMode,
1327 #[serde(default = "default_exec_timeout")]
1329 pub default_timeout_secs: u64,
1330 #[serde(default = "default_exec_max_timeout")]
1332 pub max_timeout_secs: u64,
1333}
1334
1335fn default_false() -> bool {
1336 false
1337}
1338
1339fn default_exec_timeout() -> u64 {
1340 120
1341}
1342
1343fn default_exec_max_timeout() -> u64 {
1344 600
1345}
1346
1347impl ExecConfig {
1348 pub fn is_binary_allowed(&self, name: &str) -> bool {
1355 match self.allowlist_mode {
1356 AllowlistMode::Permissive => {
1357 self.allowed_commands.is_empty() || self.allowed_commands.iter().any(|c| c == name)
1358 }
1359 AllowlistMode::Enforced => self.allowed_commands.iter().any(|c| c == name),
1360 }
1361 }
1362}
1363
1364impl Default for ExecConfig {
1365 fn default() -> Self {
1366 Self {
1367 default_mode: ExecMode::default(),
1368 allow_shell_mode: default_false(),
1369 allowed_commands: Vec::new(),
1370 allowlist_mode: AllowlistMode::default(),
1371 default_timeout_secs: default_exec_timeout(),
1372 max_timeout_secs: default_exec_max_timeout(),
1373 }
1374 }
1375}
1376
1377#[derive(Debug, Clone, Deserialize, Serialize)]
1379pub struct OrchestratorConfig {
1380 #[serde(default = "default_max_evolution_iterations")]
1383 pub max_evolution_iterations: u32,
1384
1385 #[serde(default = "default_min_evaluation_score")]
1388 pub min_evaluation_score: f64,
1389}
1390
1391fn default_max_evolution_iterations() -> u32 {
1392 3
1393}
1394
1395fn default_min_evaluation_score() -> f64 {
1396 0.8
1397}
1398
1399impl Default for OrchestratorConfig {
1400 fn default() -> Self {
1401 Self {
1402 max_evolution_iterations: default_max_evolution_iterations(),
1403 min_evaluation_score: default_min_evaluation_score(),
1404 }
1405 }
1406}
1407
1408#[derive(Debug, Clone, Serialize, Deserialize)]
1413pub struct IntentConfig {
1414 #[serde(default = "default_intent_max_retries")]
1418 pub max_retries: u32,
1419
1420 #[serde(default = "default_intent_score_threshold")]
1424 pub score_threshold: f64,
1425
1426 #[serde(default = "default_intent_max_clarify_rounds")]
1430 pub max_clarify_rounds: u32,
1431
1432 #[serde(default = "default_intent_enable_retry")]
1436 pub enable_retry: bool,
1437
1438 #[serde(default)]
1442 pub lightweight_model: Option<String>,
1443}
1444
1445fn default_intent_max_retries() -> u32 {
1446 2
1447}
1448
1449fn default_intent_score_threshold() -> f64 {
1450 0.7
1451}
1452
1453fn default_intent_max_clarify_rounds() -> u32 {
1454 3
1455}
1456
1457fn default_intent_enable_retry() -> bool {
1458 true
1459}
1460
1461impl Default for IntentConfig {
1462 fn default() -> Self {
1463 Self {
1464 max_retries: default_intent_max_retries(),
1465 score_threshold: default_intent_score_threshold(),
1466 max_clarify_rounds: default_intent_max_clarify_rounds(),
1467 enable_retry: default_intent_enable_retry(),
1468 lightweight_model: None,
1469 }
1470 }
1471}
1472
1473#[derive(Debug, Clone, Deserialize, Serialize)]
1475pub struct ContextConfig {
1476 #[serde(default = "default_active_limit")]
1478 pub active_limit_tokens: usize,
1479 #[serde(default = "default_cache_limit")]
1481 pub cache_limit_entries: usize,
1482}
1483
1484fn default_active_limit() -> usize {
1485 100_000
1486}
1487
1488fn default_cache_limit() -> usize {
1489 50
1490}
1491
1492impl Default for ContextConfig {
1493 fn default() -> Self {
1494 Self {
1495 active_limit_tokens: default_active_limit(),
1496 cache_limit_entries: default_cache_limit(),
1497 }
1498 }
1499}
1500
1501#[derive(Debug, Clone, Deserialize, Serialize)]
1503pub struct SecurityConfig {
1504 #[serde(default = "default_allowed_tools")]
1506 pub allowed_tools: Vec<String>,
1507 #[serde(default)]
1509 pub network_access: bool,
1510 #[serde(default = "default_max_exec_time")]
1512 pub max_execution_time_secs: u64,
1513 #[serde(default = "default_max_memory")]
1515 pub max_memory_mb: u64,
1516 #[serde(default)]
1518 pub can_fork: bool,
1519 #[serde(default = "default_max_audit")]
1521 pub max_audit_entries: usize,
1522 #[serde(default)]
1524 pub auth_enabled: bool,
1525 #[serde(default = "default_cors_origins")]
1527 pub cors_origins: Vec<String>,
1528 #[serde(default)]
1530 pub audit_log_path: Option<String>,
1531 #[serde(default = "default_rate_limit_per_minute")]
1533 pub rate_limit_per_minute: u32,
1534}
1535
1536fn default_allowed_tools() -> Vec<String> {
1537 vec![
1538 "read".to_string(),
1539 "write".to_string(),
1540 "edit".to_string(),
1541 "bash".to_string(),
1542 "grep".to_string(),
1543 "find".to_string(),
1544 "exec".to_string(),
1545 ]
1546}
1547
1548fn default_max_exec_time() -> u64 {
1549 300
1550}
1551
1552fn default_max_memory() -> u64 {
1553 512
1554}
1555
1556fn default_max_audit() -> usize {
1557 10_000
1558}
1559
1560fn default_rate_limit_per_minute() -> u32 {
1561 120
1562}
1563
1564fn default_cors_origins() -> Vec<String> {
1565 vec![
1570 "http://localhost:4200".to_string(),
1571 "http://127.0.0.1:4200".to_string(),
1572 "http://localhost:5173".to_string(),
1573 "http://127.0.0.1:5173".to_string(),
1574 ]
1575}
1576
1577impl Default for SecurityConfig {
1578 fn default() -> Self {
1579 Self {
1580 allowed_tools: default_allowed_tools(),
1581 network_access: false,
1582 max_execution_time_secs: default_max_exec_time(),
1583 max_memory_mb: default_max_memory(),
1584 can_fork: false,
1585 max_audit_entries: default_max_audit(),
1586 auth_enabled: false,
1587 cors_origins: default_cors_origins(),
1588 audit_log_path: None,
1589 rate_limit_per_minute: default_rate_limit_per_minute(),
1590 }
1591 }
1592}
1593
1594#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1599pub struct PersonaConfig {
1600 #[serde(default)]
1602 pub default_persona_id: Option<String>,
1603}
1604
1605#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1613pub struct McpConfig {
1614 #[serde(default)]
1616 pub servers: std::collections::HashMap<String, McpServerDef>,
1617}
1618
1619#[derive(Debug, Clone, Deserialize, Serialize)]
1621pub struct McpServerDef {
1622 pub command: String,
1624 #[serde(default)]
1626 pub args: Vec<String>,
1627 #[serde(default)]
1629 pub env: std::collections::HashMap<String, String>,
1630 #[serde(default = "default_mcp_enabled")]
1632 pub enabled: bool,
1633}
1634
1635fn default_mcp_enabled() -> bool {
1636 true
1637}
1638
1639#[derive(Debug, Clone, Deserialize, Serialize)]
1641pub struct GitConfig {
1642 #[serde(default = "default_true")]
1644 pub auto_commit: bool,
1645}
1646
1647impl Default for GitConfig {
1648 fn default() -> Self {
1649 Self { auto_commit: true }
1650 }
1651}
1652
1653#[derive(Debug, Clone, Deserialize, Serialize)]
1655pub struct AuditConfig {
1656 #[serde(default = "default_audit_max_entries")]
1658 pub max_entries: usize,
1659 #[serde(default = "default_true")]
1661 pub enabled: bool,
1662}
1663
1664fn default_audit_max_entries() -> usize {
1665 100_000
1666}
1667
1668impl Default for AuditConfig {
1669 fn default() -> Self {
1670 Self {
1671 max_entries: default_audit_max_entries(),
1672 enabled: true,
1673 }
1674 }
1675}
1676
1677#[derive(Debug, Clone, Deserialize, Serialize)]
1679pub struct BudgetConfig {
1680 #[serde(default)]
1682 pub default_token_budget: u64,
1683 #[serde(default)]
1685 pub default_calls_budget: u64,
1686 #[serde(default = "default_budget_window")]
1688 pub default_window_secs: u64,
1689 #[serde(default = "default_true")]
1691 pub enabled: bool,
1692 #[serde(default)]
1696 pub monthly_spend_limit_usd: Option<f64>,
1697}
1698
1699fn default_budget_window() -> u64 {
1700 3600
1701}
1702
1703impl Default for BudgetConfig {
1704 fn default() -> Self {
1705 Self {
1706 default_token_budget: 0,
1707 default_calls_budget: 0,
1708 default_window_secs: default_budget_window(),
1709 enabled: true,
1710 monthly_spend_limit_usd: None,
1711 }
1712 }
1713}
1714
1715#[derive(Debug, Clone, Deserialize, Serialize)]
1717pub struct ResourceMonitorConfig {
1718 #[serde(default = "default_rm_interval")]
1720 pub interval_secs: u64,
1721 #[serde(default = "default_rm_history_max")]
1723 pub history_max: usize,
1724 #[serde(default = "default_rm_cpu_threshold")]
1726 pub cpu_threshold: f32,
1727 #[serde(default = "default_rm_mem_threshold")]
1729 pub memory_threshold: f32,
1730 #[serde(default = "default_rm_load_threshold")]
1732 pub load_threshold: f32,
1733}
1734
1735fn default_rm_interval() -> u64 {
1736 60
1737}
1738
1739fn default_rm_history_max() -> usize {
1740 60
1741}
1742
1743fn default_rm_cpu_threshold() -> f32 {
1744 90.0
1745}
1746
1747fn default_rm_mem_threshold() -> f32 {
1748 90.0
1749}
1750
1751fn default_rm_load_threshold() -> f32 {
1752 8.0
1753}
1754
1755impl Default for ResourceMonitorConfig {
1756 fn default() -> Self {
1757 Self {
1758 interval_secs: default_rm_interval(),
1759 history_max: default_rm_history_max(),
1760 cpu_threshold: default_rm_cpu_threshold(),
1761 memory_threshold: default_rm_mem_threshold(),
1762 load_threshold: default_rm_load_threshold(),
1763 }
1764 }
1765}
1766
1767#[derive(Debug, Clone, Serialize, Deserialize)]
1769pub struct AgentLogConfig {
1770 #[serde(default = "default_agent_log_max_entries")]
1772 pub max_entries: usize,
1773 #[serde(default = "default_agent_log_ttl_hours")]
1775 pub ttl_hours: u64,
1776 #[serde(default = "default_agent_log_max_tool_calls")]
1778 pub max_tool_calls_per_agent: usize,
1779 #[serde(default = "default_agent_log_prune_batch")]
1781 pub prune_batch_size: usize,
1782 #[serde(default)]
1784 pub db_path: String,
1785}
1786
1787fn default_agent_log_max_entries() -> usize {
1788 10_000
1789}
1790fn default_agent_log_ttl_hours() -> u64 {
1791 720
1792}
1793fn default_agent_log_max_tool_calls() -> usize {
1794 500
1795}
1796fn default_agent_log_prune_batch() -> usize {
1797 100
1798}
1799
1800impl Default for AgentLogConfig {
1801 fn default() -> Self {
1802 Self {
1803 max_entries: 10_000,
1804 ttl_hours: 720,
1805 max_tool_calls_per_agent: 500,
1806 prune_batch_size: 100,
1807 db_path: String::new(),
1808 }
1809 }
1810}
1811
1812#[derive(Debug, Clone, Deserialize, Serialize)]
1814pub struct LoggingConfig {
1815 #[serde(default = "default_log_format")]
1817 pub format: String,
1818 #[serde(default)]
1820 pub level: Option<String>,
1821}
1822
1823fn default_log_format() -> String {
1824 "pretty".into()
1825}
1826
1827impl Default for LoggingConfig {
1828 fn default() -> Self {
1829 Self {
1830 format: default_log_format(),
1831 level: None,
1832 }
1833 }
1834}
1835
1836#[derive(Debug, Clone, Deserialize, Serialize)]
1842pub struct BrowserConfig {
1843 #[serde(default = "default_browser_enabled")]
1845 pub enabled: bool,
1846
1847 #[serde(default)]
1858 pub engine: serde_json::Value,
1859}
1860
1861fn default_browser_enabled() -> bool {
1862 true
1863}
1864
1865impl Default for BrowserConfig {
1866 fn default() -> Self {
1867 Self {
1868 enabled: true,
1869 engine: serde_json::json!({}),
1870 }
1871 }
1872}
1873
1874pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
1876 let content = std::fs::read_to_string(path)?;
1877 let config: OxiosConfig = toml::from_str(&content)?;
1878 let (errors, warnings) = config.validate();
1879 for w in warnings {
1880 tracing::warn!("config: {}", w);
1881 }
1882 if !errors.is_empty() {
1883 let msg = errors.join("; ");
1884 anyhow::bail!("Configuration validation failed: {msg}");
1885 }
1886 Ok(config)
1887}
1888
1889impl OxiosConfig {
1890 pub fn api_key(&self) -> Option<String> {
1892 self.engine.api_key.clone().filter(|k| !k.is_empty())
1893 }
1894
1895 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
1898 let mut errors = Vec::new();
1899 let mut warnings = Vec::new();
1900
1901 if self.kernel.max_agents == 0 {
1903 errors.push("kernel.max_agents must be > 0".into());
1904 }
1905 if self.kernel.workspace.is_empty() {
1906 errors.push("kernel.workspace must not be empty".into());
1907 }
1908
1909 if self.gateway.port == 0 {
1911 errors.push("gateway.port must be > 0".into());
1912 }
1913 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
1914 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
1915 }
1916
1917 for (name, job) in &self.cron.jobs {
1919 if job.schedule.is_empty() {
1920 errors.push(format!("cron.jobs.{name}: schedule is empty"));
1921 } else {
1922 let normalized = {
1924 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
1925 match fields.len() {
1926 5 => format!("0 {}", job.schedule),
1927 _ => job.schedule.clone(),
1928 }
1929 };
1930 if Schedule::from_str(&normalized).is_err() {
1931 errors.push(format!(
1932 "cron.jobs.{}: invalid cron expression '{}'",
1933 name, job.schedule
1934 ));
1935 }
1936 }
1937 if job.goal.is_empty() {
1938 errors.push(format!("cron.jobs.{name}: goal is empty"));
1939 }
1940 }
1941
1942 if self.security.max_execution_time_secs == 0 {
1944 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
1945 }
1946
1947 if self.audit.max_entries == 0 {
1949 warnings.push("audit.max_entries is 0 — audit will never prune".into());
1950 }
1951
1952 if self.budget.default_window_secs == 0 {
1954 warnings.push("budget.default_window_secs is 0 — no time window".into());
1955 }
1956
1957 if self.gateway.response_timeout_secs == 0 {
1959 errors.push("gateway.response_timeout_secs must be > 0".into());
1960 }
1961
1962 if self.engine.api_key.as_ref().is_some_and(|k| !k.is_empty()) {
1965 warnings.push(
1966 "engine.api_key is set in config — prefer the oxi auth store or env var to avoid storing a secret on disk"
1967 .into(),
1968 );
1969 }
1970
1971 for (name, server) in &self.mcp.servers {
1973 if server.command.trim().is_empty() {
1974 errors.push(format!("mcp.servers.{name}: command must not be empty"));
1975 }
1976 }
1977
1978 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
1980 {
1981 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
1982 }
1983
1984 if self.exec.default_timeout_secs == 0 {
1986 errors.push("exec.default_timeout_secs must be > 0".into());
1987 }
1988 if self.exec.max_timeout_secs == 0 {
1989 errors.push("exec.max_timeout_secs must be > 0".into());
1990 }
1991 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
1992 errors.push(format!(
1993 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
1994 self.exec.default_timeout_secs, self.exec.max_timeout_secs
1995 ));
1996 }
1997
1998 if self.resource_monitor.cpu_threshold > 100.0 {
2000 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
2001 }
2002 if self.resource_monitor.memory_threshold > 100.0 {
2003 errors.push("resource_monitor.memory_threshold must be <= 100".into());
2004 }
2005
2006 for name in &self.channels.enabled {
2008 let valid = ["cli", "telegram"];
2009 if !valid.contains(&name.as_str()) {
2010 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
2011 }
2012 }
2013 if self.channels.enabled.iter().any(|c| c == "web") {
2015 warnings.push(
2016 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
2017 );
2018 }
2019 if self.channels.enabled.iter().any(|c| c == "telegram")
2020 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
2021 {
2022 warnings.push(format!(
2023 "channels.telegram: {} env var not set — telegram channel will fail",
2024 self.channels.telegram.bot_token_env
2025 ));
2026 }
2027 for err in self.token_maxing.validate() {
2031 errors.push(err);
2032 }
2033
2034 (errors, warnings)
2035 }
2036}
2037
2038pub fn expand_home(path: &str) -> std::path::PathBuf {
2050 if let Some(rest) = path.strip_prefix("~/") {
2051 if let Ok(home) = std::env::var("HOME") {
2052 return std::path::PathBuf::from(format!("{home}/{rest}"));
2053 }
2054 if let Some(home) = dirs::home_dir() {
2055 return home.join(rest);
2056 }
2057 }
2058 std::path::PathBuf::from(path)
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063 use super::*;
2064
2065 #[test]
2066 fn test_default_config_validates() {
2067 let config = OxiosConfig::default();
2068 let (errors, _warnings) = config.validate();
2069 assert!(
2070 errors.is_empty(),
2071 "Default config should have no errors: {:?}",
2072 errors
2073 );
2074 }
2075
2076 #[test]
2077 fn test_exec_config_default_allowed_commands() {
2078 let config = ExecConfig::default();
2079 assert!(config.allowed_commands.is_empty());
2081 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
2082 assert!(!config.is_binary_allowed("anything"));
2083 assert!(!config.is_binary_allowed("bash"));
2084 }
2085
2086 #[test]
2087 fn test_exec_config_permissive_mode() {
2088 let config = ExecConfig {
2089 allowlist_mode: AllowlistMode::Permissive,
2090 ..Default::default()
2091 };
2092 assert!(config.is_binary_allowed("anything"));
2094 assert!(config.is_binary_allowed("bash"));
2095 }
2096
2097 #[test]
2098 fn test_is_binary_allowed_with_allowlist() {
2099 let config = ExecConfig {
2100 allowed_commands: vec!["git".into(), "echo".into()],
2101 ..Default::default()
2102 };
2103 assert!(config.is_binary_allowed("git"));
2104 assert!(config.is_binary_allowed("echo"));
2105 assert!(!config.is_binary_allowed("bash"));
2106 assert!(!config.is_binary_allowed("rm"));
2107 assert!(!config.is_binary_allowed("sudo"));
2108 }
2109
2110 #[test]
2111 fn test_expand_home() {
2112 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
2114 let expanded = expand_home("~/projects/test");
2115 assert_eq!(
2116 expanded.to_str().unwrap(),
2117 format!("{}/projects/test", home)
2118 );
2119
2120 let abs = expand_home("/absolute/path");
2122 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
2123
2124 let bare = expand_home("~something");
2126 assert_eq!(bare, std::path::PathBuf::from("~something"));
2127 }
2128
2129 #[test]
2130 fn test_invalid_cron_expression() {
2131 let mut config = OxiosConfig::default();
2132 config.cron.enabled = true;
2133 config.cron.jobs.insert(
2134 "bad-job".to_string(),
2135 InlineCronJob {
2136 schedule: "not a valid cron".to_string(),
2137 goal: "Test goal".to_string(),
2138 constraints: vec![],
2139 acceptance_criteria: vec![],
2140 toolchain: "default".to_string(),
2141 priority: Priority::Normal,
2142 enabled: true,
2143 },
2144 );
2145
2146 let (errors, _warnings) = config.validate();
2147 assert!(
2148 !errors.is_empty(),
2149 "Expected validation error for invalid cron"
2150 );
2151 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
2152 assert!(
2153 has_cron_error,
2154 "Expected 'invalid cron expression' error, got: {:?}",
2155 errors
2156 );
2157 }
2158
2159 #[test]
2160 fn test_config_serialization_roundtrip() {
2161 let config = OxiosConfig::default();
2162
2163 let toml_str = toml::to_string(&config).expect("serialization should succeed");
2165
2166 let deserialized: OxiosConfig =
2168 toml::from_str(&toml_str).expect("deserialization should succeed");
2169
2170 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2172 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2173 assert_eq!(config.gateway.host, deserialized.gateway.host);
2174 assert_eq!(config.gateway.port, deserialized.gateway.port);
2175 assert_eq!(
2176 config.exec.default_timeout_secs,
2177 deserialized.exec.default_timeout_secs
2178 );
2179 assert_eq!(
2180 config.exec.max_timeout_secs,
2181 deserialized.exec.max_timeout_secs
2182 );
2183 }
2184
2185 #[test]
2186 fn test_exec_timeout_validation() {
2187 let mut config = OxiosConfig::default();
2188 config.exec.default_timeout_secs = 999;
2190 config.exec.max_timeout_secs = 100;
2191 let (errors, _warnings) = config.validate();
2192 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2193 assert!(
2194 has_error,
2195 "Expected timeout ordering error, got: {:?}",
2196 errors
2197 );
2198 }
2199
2200 #[test]
2201 fn test_zero_max_agents_error() {
2202 let mut config = OxiosConfig::default();
2203 config.kernel.max_agents = 0;
2204 let (errors, _warnings) = config.validate();
2205 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2206 }
2207
2208 #[test]
2213 fn test_default_config_matches_toml() {
2214 let from_rust = OxiosConfig::default();
2215
2216 let toml_str = include_str!("../../../share/default-config.toml");
2217 let from_toml: OxiosConfig =
2218 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2219
2220 assert_eq!(
2222 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2223 "kernel.max_agents 불일치: Rust={}, TOML={}",
2224 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2225 );
2226 assert_eq!(
2227 from_rust.gateway.host, from_toml.gateway.host,
2228 "gateway.host 불일치: Rust={}, TOML={}",
2229 from_rust.gateway.host, from_toml.gateway.host
2230 );
2231 assert_eq!(
2232 from_rust.gateway.port, from_toml.gateway.port,
2233 "gateway.port 불일치: Rust={}, TOML={}",
2234 from_rust.gateway.port, from_toml.gateway.port
2235 );
2236 assert_eq!(
2237 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2238 "kernel.event_bus_capacity 불일치"
2239 );
2240 assert_eq!(
2241 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2242 "memory.consolidation.preset 불일치"
2243 );
2244
2245 let (_, warnings) = from_toml.validate();
2247 for w in &warnings {
2248 eprintln!("default-config.toml 경고: {}", w);
2249 }
2250 }
2251
2252 #[test]
2255 fn test_gateway_should_expose_api_docs() {
2256 let cfg = GatewayConfig::default();
2258 assert!(!cfg.should_expose_api_docs());
2259
2260 let cfg = GatewayConfig {
2262 host: "0.0.0.0".into(),
2263 port: 4200,
2264 expose_api_docs: true,
2265 ..Default::default()
2266 };
2267 assert!(
2268 !cfg.should_expose_api_docs(),
2269 "public bind must not expose api docs even when opt-in is true"
2270 );
2271
2272 let cfg = GatewayConfig {
2274 host: "127.0.0.1".into(),
2275 port: 4200,
2276 expose_api_docs: true,
2277 ..Default::default()
2278 };
2279 assert!(cfg.should_expose_api_docs());
2280
2281 let cfg = GatewayConfig {
2283 host: "::1".into(),
2284 port: 4200,
2285 expose_api_docs: true,
2286 ..Default::default()
2287 };
2288 assert!(cfg.should_expose_api_docs());
2289
2290 let cfg = GatewayConfig {
2292 host: "localhost".into(),
2293 port: 4200,
2294 expose_api_docs: true,
2295 ..Default::default()
2296 };
2297 assert!(cfg.should_expose_api_docs());
2298
2299 let cfg = GatewayConfig {
2301 host: "127.0.0.1".into(),
2302 port: 4200,
2303 expose_api_docs: false,
2304 ..Default::default()
2305 };
2306 assert!(!cfg.should_expose_api_docs());
2307 }
2308}