1#![allow(missing_docs)]
2use cron::Schedule;
8use serde::{Deserialize, Serialize};
9use std::str::FromStr;
10
11use crate::email::{SmtpProvider, SmtpTls};
12use crate::types::Priority;
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct CronConfig {
17 #[serde(default)]
19 pub enabled: bool,
20 #[serde(default = "default_tick_interval")]
22 pub tick_interval_secs: u64,
23 #[serde(default)]
25 pub jobs: std::collections::HashMap<String, InlineCronJob>,
26}
27
28impl Default for CronConfig {
29 fn default() -> Self {
30 Self {
31 enabled: false,
32 tick_interval_secs: default_tick_interval(),
33 jobs: std::collections::HashMap::new(),
34 }
35 }
36}
37
38fn default_tick_interval() -> u64 {
39 60
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct InlineCronJob {
45 pub schedule: String,
47 pub goal: String,
49 #[serde(default)]
51 pub constraints: Vec<String>,
52 #[serde(default)]
54 pub acceptance_criteria: Vec<String>,
55 #[serde(default = "default_toolchain_inline")]
57 pub toolchain: String,
58 #[serde(default)]
60 pub priority: Priority,
61 #[serde(default = "default_true_inline")]
63 pub enabled: bool,
64}
65
66fn default_toolchain_inline() -> String {
67 "default".into()
68}
69
70fn default_true_inline() -> bool {
71 true
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct MemoryConfig {
77 #[serde(default = "default_true")]
79 pub enabled: bool,
80 #[serde(default = "default_max_recall")]
82 pub max_recall: usize,
83 #[serde(default = "default_true")]
85 pub auto_summarize: bool,
86 #[serde(default = "default_true")]
88 pub capture_compaction: bool,
89 #[serde(default)]
91 pub retention_days: u32,
92 #[serde(default = "default_true")]
94 pub cache_enabled: bool,
95 #[serde(default = "default_cache_ttl")]
97 pub cache_ttl_secs: u64,
98 #[serde(default = "default_cache_max_entries")]
100 pub cache_max_entries: usize,
101 #[serde(default)]
103 pub consolidation: ConsolidationConfig,
104 #[serde(default)]
106 pub sqlite: SqliteMemoryConfig,
107 #[serde(default)]
109 pub embedding: EmbeddingConfig,
110 #[serde(default)]
112 pub learning: LearningConfig,
113 #[serde(default)]
115 pub knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig,
116 #[serde(default)]
118 pub bridge: MemoryBridgeConfig,
119}
120
121fn default_true() -> bool {
122 true
123}
124
125fn default_max_recall() -> usize {
126 10
127}
128
129fn default_cache_ttl() -> u64 {
130 3600 }
132
133fn default_cache_max_entries() -> usize {
134 10000
135}
136
137impl Default for MemoryConfig {
138 fn default() -> Self {
139 Self {
140 enabled: true,
141 max_recall: 10,
142 auto_summarize: true,
143 capture_compaction: true,
144 retention_days: 0,
145 cache_enabled: true,
146 cache_ttl_secs: 3600,
147 cache_max_entries: 10000,
148 consolidation: ConsolidationConfig::default(),
149 sqlite: SqliteMemoryConfig::default(),
150 embedding: EmbeddingConfig::default(),
151 learning: LearningConfig::default(),
152 knowledge_dream: crate::knowledge_dream::KnowledgeDreamConfig::default(),
153 bridge: MemoryBridgeConfig::default(),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct SqliteMemoryConfig {
169 #[serde(default = "default_true")]
171 pub enabled: bool,
172 #[serde(default)]
175 pub path: String,
176 #[serde(default = "default_embedding_dim")]
180 pub embedding_dim: usize,
181 #[serde(default = "default_true")]
183 pub wal_mode: bool,
184}
185
186fn default_embedding_dim() -> usize {
187 256
188}
189
190impl Default for SqliteMemoryConfig {
191 fn default() -> Self {
192 Self {
193 enabled: true,
194 path: String::new(),
195 embedding_dim: 256,
196 wal_mode: true,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct EmbeddingConfig {
214 #[serde(default = "default_embedding_provider")]
216 pub provider: String,
217 #[serde(default = "default_embedding_dim")]
221 pub dimension: usize,
222 #[serde(default = "default_model_ttl")]
225 pub model_ttl_secs: u64,
226 #[serde(default)]
229 pub api_endpoint: String,
230 #[serde(default)]
233 pub api_key: String,
234 #[serde(default)]
237 pub api_model: String,
238}
239
240fn default_embedding_provider() -> String {
241 "tfidf".to_string()
244}
245
246fn default_model_ttl() -> u64 {
247 300 }
249
250impl Default for EmbeddingConfig {
251 fn default() -> Self {
252 Self {
253 provider: default_embedding_provider(),
254 dimension: default_embedding_dim(),
255 model_ttl_secs: default_model_ttl(),
256 api_endpoint: String::new(),
257 api_key: String::new(),
258 api_model: String::new(),
259 }
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct LearningConfig {
272 #[serde(default = "default_true")]
274 pub enabled: bool,
275 #[serde(default = "default_sona_mode")]
277 pub sona_mode: String,
278 #[serde(default = "default_distill_interval")]
280 pub distill_interval_hours: u64,
281 #[serde(default = "default_auto_promote_quality")]
283 pub auto_promote_quality: f32,
284 #[serde(default = "default_auto_promote_min_usage")]
286 pub auto_promote_min_usage: u32,
287}
288
289fn default_sona_mode() -> String {
290 "balanced".to_string()
291}
292
293fn default_distill_interval() -> u64 {
294 6
295}
296
297fn default_auto_promote_quality() -> f32 {
298 0.8
299}
300
301fn default_auto_promote_min_usage() -> u32 {
302 3
303}
304
305impl Default for LearningConfig {
306 fn default() -> Self {
307 Self {
308 enabled: true,
309 sona_mode: default_sona_mode(),
310 distill_interval_hours: default_distill_interval(),
311 auto_promote_quality: default_auto_promote_quality(),
312 auto_promote_min_usage: default_auto_promote_min_usage(),
313 }
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct MemoryBridgeConfig {
327 #[serde(default)]
329 pub sync_enabled: bool,
330 #[serde(default = "default_bridge_interval")]
332 pub interval_secs: u64,
333}
334
335fn default_bridge_interval() -> u64 {
336 3600
337}
338
339impl Default for MemoryBridgeConfig {
340 fn default() -> Self {
341 Self {
342 sync_enabled: false,
343 interval_secs: default_bridge_interval(),
344 }
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct ConsolidationConfig {
356 #[serde(default = "default_preset")]
360 pub preset: String,
361
362 #[serde(default = "default_true")]
364 pub dream_enabled: bool,
365 #[serde(default = "default_dream_interval")]
366 pub dream_interval_hours: u64,
367 #[serde(default = "default_dream_min_sessions")]
368 pub dream_min_sessions: u32,
369
370 #[serde(default = "default_hot_max")]
372 pub hot_max_entries: usize,
373 #[serde(default = "default_warm_max")]
374 pub warm_max_entries: usize,
375 #[serde(default = "default_cold_max")]
376 pub cold_max_entries: usize,
377 #[serde(default = "default_hot_token_budget")]
378 pub hot_token_budget: usize,
379
380 #[serde(default = "default_true")]
382 pub decay_enabled: bool,
383 #[serde(default = "default_one")]
384 pub decay_multiplier: f32,
385 #[serde(default = "default_decay_threshold")]
386 pub decay_threshold: f32,
387 #[serde(default = "default_retention_days")]
388 pub retention_days: u32,
389
390 #[serde(default = "default_true")]
392 pub auto_protection: bool,
393 #[serde(default = "default_protection_low_access")]
394 pub protection_low_access: u32,
395 #[serde(default = "default_protection_medium_access")]
396 pub protection_medium_access: u32,
397 #[serde(default = "default_protection_high_access")]
398 pub protection_high_access: u32,
399 #[serde(default = "default_protection_medium_sessions")]
400 pub protection_medium_sessions: u32,
401 #[serde(default = "default_protection_high_sessions")]
402 pub protection_high_sessions: u32,
403
404 #[serde(default = "default_true")]
406 pub auto_classification: bool,
407 #[serde(default = "default_type_promotion_threshold")]
408 pub type_promotion_repetitions: u32,
409
410 #[serde(default = "default_compaction_threshold")]
412 pub compaction_line_threshold: usize,
413 #[serde(default = "default_true")]
414 pub llm_compaction: bool,
415
416 #[serde(default)]
419 pub dream_model: Option<String>,
420
421 #[serde(default = "default_true")]
423 pub protection_demotion_enabled: bool,
424 #[serde(default = "default_demotion_stale_days")]
425 pub protection_demotion_stale_days: u32,
426 #[serde(default = "default_demotion_max_step")]
427 pub protection_demotion_max_step: u32,
428
429 #[serde(default = "default_true")]
431 pub proactive_recall: bool,
432 #[serde(default = "default_proactive_limit")]
433 pub proactive_recall_limit: usize,
434 #[serde(default = "default_proactive_threshold")]
435 pub proactive_recall_threshold: f32,
436}
437
438fn default_dream_interval() -> u64 {
439 24
440}
441fn default_dream_min_sessions() -> u32 {
442 5
443}
444fn default_hot_max() -> usize {
445 50
446}
447fn default_warm_max() -> usize {
448 500
449}
450fn default_cold_max() -> usize {
451 10_000
452}
453fn default_hot_token_budget() -> usize {
454 3_000
455}
456fn default_one() -> f32 {
457 1.0
458}
459fn default_decay_threshold() -> f32 {
460 0.05
461}
462fn default_retention_days() -> u32 {
463 90
464}
465fn default_protection_low_access() -> u32 {
466 2
467}
468fn default_protection_medium_access() -> u32 {
469 3
470}
471fn default_protection_high_access() -> u32 {
472 5
473}
474fn default_protection_medium_sessions() -> u32 {
475 2
476}
477fn default_protection_high_sessions() -> u32 {
478 3
479}
480fn default_type_promotion_threshold() -> u32 {
481 3
482}
483fn default_compaction_threshold() -> usize {
484 200
485}
486fn default_proactive_limit() -> usize {
487 5
488}
489fn default_proactive_threshold() -> f32 {
490 0.6
491}
492fn default_demotion_stale_days() -> u32 {
493 30
494}
495fn default_demotion_max_step() -> u32 {
496 1
497}
498
499fn default_preset() -> String {
500 "balanced".into()
501}
502
503impl Default for ConsolidationConfig {
504 fn default() -> Self {
505 Self {
506 preset: default_preset(),
507 dream_enabled: true,
508 dream_interval_hours: 24,
509 dream_min_sessions: 5,
510 hot_max_entries: 50,
511 warm_max_entries: 500,
512 cold_max_entries: 10_000,
513 hot_token_budget: 3_000,
514 decay_enabled: true,
515 decay_multiplier: 1.0,
516 decay_threshold: 0.05,
517 retention_days: 90,
518 auto_protection: true,
519 protection_low_access: 2,
520 protection_medium_access: 3,
521 protection_high_access: 5,
522 protection_medium_sessions: 2,
523 protection_high_sessions: 3,
524 auto_classification: true,
525 type_promotion_repetitions: 3,
526 compaction_line_threshold: 200,
527 llm_compaction: true,
528 dream_model: None,
529 protection_demotion_enabled: true,
530 protection_demotion_stale_days: 30,
531 protection_demotion_max_step: 1,
532 proactive_recall: true,
533 proactive_recall_limit: 5,
534 proactive_recall_threshold: 0.6,
535 }
536 }
537}
538
539impl ConsolidationConfig {
540 pub fn apply_preset(&mut self) {
544 let resolved = match self.preset.as_str() {
545 "conservative" => Self::conservative(),
546 "aggressive" => Self::aggressive(),
547 "custom" => return,
548 _ => Self::default(), };
550 *self = resolved;
551 }
552
553 fn conservative() -> Self {
555 Self {
556 preset: "conservative".into(),
557 dream_enabled: true,
558 dream_interval_hours: 48,
559 dream_min_sessions: 10,
560 hot_max_entries: 100,
561 warm_max_entries: 1000,
562 cold_max_entries: 50_000,
563 hot_token_budget: 5_000,
564 decay_enabled: true,
565 decay_multiplier: 0.8,
566 decay_threshold: 0.05,
567 retention_days: 365,
568 auto_protection: true,
569 protection_low_access: 3,
570 protection_medium_access: 5,
571 protection_high_access: 10,
572 protection_medium_sessions: 3,
573 protection_high_sessions: 5,
574 auto_classification: true,
575 type_promotion_repetitions: 5,
576 compaction_line_threshold: 300,
577 llm_compaction: true,
578 dream_model: None,
579 protection_demotion_enabled: true,
580 protection_demotion_stale_days: 90,
581 protection_demotion_max_step: 1,
582 proactive_recall: true,
583 proactive_recall_limit: 8,
584 proactive_recall_threshold: 0.5,
585 }
586 }
587
588 fn aggressive() -> Self {
590 Self {
591 preset: "aggressive".into(),
592 dream_enabled: true,
593 dream_interval_hours: 4,
594 dream_min_sessions: 2,
595 hot_max_entries: 20,
596 warm_max_entries: 100,
597 cold_max_entries: 1_000,
598 hot_token_budget: 2_000,
599 decay_enabled: true,
600 decay_multiplier: 1.0,
601 decay_threshold: 0.1,
602 retention_days: 30,
603 auto_protection: true,
604 protection_low_access: 1,
605 protection_medium_access: 2,
606 protection_high_access: 3,
607 protection_medium_sessions: 1,
608 protection_high_sessions: 2,
609 auto_classification: true,
610 type_promotion_repetitions: 2,
611 compaction_line_threshold: 150,
612 llm_compaction: true,
613 dream_model: None,
614 protection_demotion_enabled: true,
615 protection_demotion_stale_days: 14,
616 protection_demotion_max_step: 2,
617 proactive_recall: true,
618 proactive_recall_limit: 3,
619 proactive_recall_threshold: 0.7,
620 }
621 }
622}
623
624#[derive(Debug, Clone, Deserialize, Serialize, Default)]
626pub struct ChannelsConfig {
627 #[serde(default)]
630 pub enabled: Vec<String>,
631
632 #[serde(default)]
634 pub telegram: TelegramChannelConfig,
635}
636
637#[derive(Debug, Clone, Deserialize, Serialize)]
642pub struct SurfacesConfig {
643 #[serde(default = "default_surfaces_enabled")]
646 pub enabled: Vec<String>,
647}
648
649fn default_surfaces_enabled() -> Vec<String> {
650 vec!["web".to_string()]
651}
652
653impl Default for SurfacesConfig {
654 fn default() -> Self {
655 Self {
656 enabled: default_surfaces_enabled(),
657 }
658 }
659}
660
661#[derive(Debug, Clone, Deserialize, Serialize)]
663pub struct TelegramChannelConfig {
664 #[serde(default = "default_telegram_token_env")]
666 pub bot_token_env: String,
667 #[serde(default)]
669 pub allowed_users: Vec<i64>,
670 #[serde(default)]
672 pub session: TelegramSessionConfig,
673}
674
675fn default_telegram_token_env() -> String {
676 "TELEGRAM_BOT_TOKEN".to_string()
677}
678
679impl Default for TelegramChannelConfig {
680 fn default() -> Self {
681 Self {
682 bot_token_env: default_telegram_token_env(),
683 allowed_users: Vec::new(),
684 session: TelegramSessionConfig::default(),
685 }
686 }
687}
688#[derive(Debug, Clone, Serialize, Deserialize, Default)]
692pub struct RoleRoutingConfig {
693 #[serde(default)]
695 pub roles: std::collections::HashMap<String, String>,
696}
697
698#[derive(Debug, Clone, Deserialize, Serialize)]
700#[allow(clippy::derivable_impls)]
701pub struct EngineConfig {
702 #[serde(default)]
705 pub default_model: String,
706 #[serde(default, skip_serializing)]
710 pub api_key: Option<String>,
711 #[serde(default)]
714 pub provider_options: Option<oxi_sdk::ProviderOptions>,
715 #[serde(default)]
719 pub routing_enabled: bool,
720 #[serde(default)]
722 pub prefer_cost_efficient: bool,
723 #[serde(default)]
725 pub fallback_models: Vec<String>,
726 #[serde(default)]
728 pub excluded_models: Vec<String>,
729 #[serde(default)]
733 pub role_routing: RoleRoutingConfig,
734 #[serde(default)]
738 pub quick_ask_model: Option<String>,
739}
740
741#[allow(clippy::derivable_impls)]
742impl Default for EngineConfig {
743 fn default() -> Self {
744 Self {
745 default_model: String::new(),
746 api_key: None,
747 provider_options: None,
748 routing_enabled: false,
749 prefer_cost_efficient: false,
750 fallback_models: Vec::new(),
751 excluded_models: Vec::new(),
752 role_routing: RoleRoutingConfig::default(),
753 quick_ask_model: None,
754 }
755 }
756}
757
758#[derive(Debug, Clone, Deserialize, Serialize)]
760pub struct DaemonConfig {
761 #[serde(default = "default_pid_file")]
763 pub pid_file: String,
764 #[serde(default = "default_daemon_log_dir")]
766 pub log_dir: String,
767}
768
769fn default_pid_file() -> String {
770 dirs::home_dir()
771 .map(|h| format!("{}/.oxios/oxios.pid", h.display()))
772 .unwrap_or_else(|| "./oxios.pid".into())
773}
774
775fn default_daemon_log_dir() -> String {
776 dirs::home_dir()
777 .map(|h| format!("{}/.oxios/logs", h.display()))
778 .unwrap_or_else(|| "./logs".into())
779}
780
781impl Default for DaemonConfig {
782 fn default() -> Self {
783 Self {
784 pid_file: default_pid_file(),
785 log_dir: default_daemon_log_dir(),
786 }
787 }
788}
789
790#[derive(Debug, Clone, Deserialize, Serialize)]
792pub struct SessionConfig {
793 #[serde(default = "default_max_sessions")]
797 pub max_sessions: usize,
798
799 #[serde(default = "default_session_ttl_hours")]
803 pub ttl_hours: u64,
804
805 #[serde(default = "default_true")]
807 pub auto_prune: bool,
808}
809
810fn default_max_sessions() -> usize {
811 100
812}
813
814fn default_session_ttl_hours() -> u64 {
815 168 }
817
818impl Default for SessionConfig {
819 fn default() -> Self {
820 Self {
821 max_sessions: default_max_sessions(),
822 ttl_hours: default_session_ttl_hours(),
823 auto_prune: true,
824 }
825 }
826}
827
828#[derive(Debug, Clone, Deserialize, Serialize)]
832pub struct MountsConfig {
833 #[serde(default = "default_true")]
835 pub auto_promote_enabled: bool,
836 #[serde(default = "default_promote_threshold")]
838 pub auto_promote_threshold: usize,
839 #[serde(default = "default_promote_window_days")]
841 pub auto_promote_window_days: i64,
842 #[serde(default = "default_promote_interval_secs")]
844 pub auto_promote_interval_secs: u64,
845}
846
847fn default_promote_threshold() -> usize {
848 3
849}
850
851fn default_promote_window_days() -> i64 {
852 14
853}
854
855fn default_promote_interval_secs() -> u64 {
856 3600 }
858
859impl Default for MountsConfig {
860 fn default() -> Self {
861 Self {
862 auto_promote_enabled: true,
863 auto_promote_threshold: default_promote_threshold(),
864 auto_promote_window_days: default_promote_window_days(),
865 auto_promote_interval_secs: default_promote_interval_secs(),
866 }
867 }
868}
869
870#[derive(Debug, Clone, Deserialize, Serialize)]
872pub struct TelegramSessionConfig {
873 #[serde(default = "default_telegram_session_rotation_hours")]
876 pub rotation_hours: u64,
877
878 #[serde(default = "default_telegram_session_max_messages")]
881 pub max_messages: usize,
882}
883
884fn default_telegram_session_rotation_hours() -> u64 {
885 2 }
887
888fn default_telegram_session_max_messages() -> usize {
889 0 }
891
892impl Default for TelegramSessionConfig {
893 fn default() -> Self {
894 Self {
895 rotation_hours: default_telegram_session_rotation_hours(),
896 max_messages: default_telegram_session_max_messages(),
897 }
898 }
899}
900
901#[derive(Debug, Clone, Deserialize, Serialize, Default)]
903pub struct OxiosConfig {
904 pub kernel: KernelConfig,
906 #[serde(default)]
908 pub engine: EngineConfig,
909 #[serde(default)]
911 pub daemon: DaemonConfig,
912 #[serde(default)]
914 pub gateway: GatewayConfig,
915 #[serde(default)]
917 pub orchestrator: OrchestratorConfig,
918 #[serde(default)]
920 pub context: ContextConfig,
921 #[serde(default)]
923 pub security: SecurityConfig,
924 #[serde(default)]
926 pub persona: PersonaConfig,
927 #[serde(default)]
929 pub memory: MemoryConfig,
930 #[serde(default)]
932 pub cron: CronConfig,
933 #[serde(default)]
935 pub mcp: McpConfig,
936 #[serde(default)]
938 pub git: GitConfig,
939 #[serde(default)]
941 pub audit: AuditConfig,
942 #[serde(default)]
944 pub budget: BudgetConfig,
945 #[serde(default)]
947 pub exec: ExecConfig,
948 #[serde(default)]
950 pub resource_monitor: ResourceMonitorConfig,
951 #[serde(default)]
953 pub logging: LoggingConfig,
954 #[serde(default)]
956 pub channels: ChannelsConfig,
957 #[serde(default)]
959 pub surfaces: Option<SurfacesConfig>,
960 #[serde(default)]
962 pub browser: BrowserConfig,
963 #[serde(default)]
965 pub session: SessionConfig,
966 #[serde(default)]
968 pub mounts: MountsConfig,
969 #[serde(default)]
971 pub marketplace: MarketplaceConfig,
972 #[serde(default)]
974 pub calendar: CalendarConfig,
975 #[serde(default)]
977 pub email: EmailConfig,
978 #[serde(default)]
980 pub agent_log: AgentLogConfig,
981 #[serde(default)]
983 pub token_maxing: crate::token_maxing::TokenMaxingConfig,
984}
985
986#[derive(Debug, Clone, Deserialize, Serialize)]
988pub struct KernelConfig {
989 #[serde(default = "default_workspace")]
991 pub workspace: String,
992 #[serde(default = "default_event_bus_capacity")]
994 pub event_bus_capacity: usize,
995 #[serde(default = "default_max_agents")]
997 pub max_agents: usize,
998}
999
1000fn default_workspace() -> String {
1001 dirs_home().unwrap_or_else(|| ".".into())
1002}
1003
1004fn dirs_home() -> Option<String> {
1005 dirs::home_dir().map(|h| format!("{}/.oxios/workspace", h.display()))
1006}
1007
1008fn default_event_bus_capacity() -> usize {
1009 256
1010}
1011
1012fn default_max_agents() -> usize {
1013 10
1014}
1015
1016impl Default for KernelConfig {
1017 fn default() -> Self {
1018 Self {
1019 workspace: default_workspace(),
1020 event_bus_capacity: default_event_bus_capacity(),
1021 max_agents: 10,
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Clone, Deserialize, Serialize)]
1028pub struct GatewayConfig {
1029 #[serde(default = "default_gateway_host")]
1031 pub host: String,
1032 #[serde(default = "default_gateway_port")]
1034 pub port: u16,
1035 #[serde(default)]
1045 pub expose_api_docs: bool,
1046 #[serde(default = "default_response_timeout_secs")]
1050 pub response_timeout_secs: u64,
1051 #[serde(default)]
1053 pub reliability: GatewayReliabilityConfig,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1058pub struct GatewayReliabilityConfig {
1059 #[serde(default = "default_replay_buffer_size")]
1062 pub replay_buffer_size: usize,
1063 #[serde(default = "default_replay_ttl_secs")]
1065 pub replay_ttl_secs: u64,
1066}
1067
1068impl Default for GatewayReliabilityConfig {
1069 fn default() -> Self {
1070 Self {
1071 replay_buffer_size: default_replay_buffer_size(),
1072 replay_ttl_secs: default_replay_ttl_secs(),
1073 }
1074 }
1075}
1076
1077fn default_response_timeout_secs() -> u64 {
1078 120
1079}
1080fn default_replay_buffer_size() -> usize {
1081 512
1082}
1083fn default_replay_ttl_secs() -> u64 {
1084 60
1085}
1086
1087impl GatewayConfig {
1088 pub fn should_expose_api_docs(&self) -> bool {
1094 if !self.expose_api_docs {
1095 return false;
1096 }
1097 let h = self.host.trim();
1098 h == "127.0.0.1" || h == "::1" || h == "localhost" || h.starts_with("127.")
1099 }
1100}
1101
1102#[derive(Debug, Clone, Deserialize, Serialize)]
1104pub struct MarketplaceConfig {
1105 #[serde(default)]
1108 pub base_url: Option<String>,
1109 #[serde(default = "default_true")]
1111 pub enabled: bool,
1112 #[serde(default)]
1114 pub skills_sh: SkillsShConfig,
1115}
1116
1117#[derive(Debug, Clone, Deserialize, Serialize)]
1119pub struct SkillsShConfig {
1120 #[serde(default)]
1123 pub base_url: Option<String>,
1124 #[serde(default)]
1127 pub api_key: Option<String>,
1128 #[serde(default = "default_true")]
1130 pub enabled: bool,
1131}
1132
1133impl Default for MarketplaceConfig {
1134 fn default() -> Self {
1135 Self {
1136 base_url: Some("https://clawhub.ai".to_string()),
1137 enabled: true,
1138 skills_sh: SkillsShConfig::default(),
1139 }
1140 }
1141}
1142
1143impl Default for SkillsShConfig {
1144 fn default() -> Self {
1145 Self {
1146 base_url: None,
1147 api_key: None,
1148 enabled: true,
1149 }
1150 }
1151}
1152
1153#[derive(Debug, Clone, Deserialize, Serialize)]
1155pub struct CalendarConfig {
1156 #[serde(default = "default_true")]
1158 pub enabled: bool,
1159 #[serde(default = "default_calendar_timezone")]
1161 pub timezone: String,
1162 #[serde(default = "default_reminder_minutes")]
1164 pub default_reminder_minutes: Vec<u32>,
1165 #[serde(default)]
1167 pub alarm_channels: Vec<String>,
1168 #[serde(default = "default_journal_sync")]
1170 pub journal_sync: String,
1171 #[serde(default = "default_true")]
1173 pub system_calendar: bool,
1174 #[serde(default = "default_archive_days")]
1176 pub archive_after_days: u32,
1177}
1178
1179fn default_calendar_timezone() -> String {
1180 "Asia/Seoul".to_string()
1181}
1182
1183fn default_reminder_minutes() -> Vec<u32> {
1184 vec![15]
1185}
1186
1187fn default_journal_sync() -> String {
1188 "on_open".to_string()
1189}
1190
1191fn default_archive_days() -> u32 {
1192 365
1193}
1194
1195impl Default for CalendarConfig {
1196 fn default() -> Self {
1197 Self {
1198 enabled: true,
1199 timezone: default_calendar_timezone(),
1200 default_reminder_minutes: default_reminder_minutes(),
1201 alarm_channels: vec![],
1202 journal_sync: default_journal_sync(),
1203 system_calendar: true,
1204 archive_after_days: default_archive_days(),
1205 }
1206 }
1207}
1208
1209#[derive(Debug, Clone, Deserialize, Serialize)]
1214pub struct EmailConfig {
1215 #[serde(default)]
1217 pub enabled: bool,
1218 #[serde(default)]
1220 pub my_email: String,
1221 #[serde(default = "default_email_provider")]
1223 pub provider: SmtpProvider,
1224 #[serde(default)]
1226 pub host: String,
1227 #[serde(default)]
1229 pub port: u16,
1230 #[serde(default)]
1232 pub tls: Option<SmtpTls>,
1233 #[serde(default)]
1235 pub user: String,
1236 #[serde(default = "default_email_secret_ref")]
1239 pub secret_ref: String,
1240 #[serde(default = "default_rate_limit_emails")]
1242 pub rate_limit_per_hour: usize,
1243}
1244
1245fn default_email_provider() -> SmtpProvider {
1246 SmtpProvider::Gmail
1247}
1248
1249fn default_email_secret_ref() -> String {
1250 "email_smtp".to_string()
1251}
1252
1253fn default_rate_limit_emails() -> usize {
1254 10
1255}
1256
1257impl Default for EmailConfig {
1258 fn default() -> Self {
1259 Self {
1260 enabled: false,
1261 my_email: String::new(),
1262 provider: default_email_provider(),
1263 host: String::new(),
1264 port: 0,
1265 tls: None,
1266 user: String::new(),
1267 secret_ref: default_email_secret_ref(),
1268 rate_limit_per_hour: default_rate_limit_emails(),
1269 }
1270 }
1271}
1272
1273impl EmailConfig {
1274 pub fn provider(&self) -> SmtpProvider {
1276 self.provider
1277 }
1278}
1279
1280fn default_gateway_host() -> String {
1281 "127.0.0.1".into()
1282}
1283
1284fn default_gateway_port() -> u16 {
1285 4200
1286}
1287
1288impl Default for GatewayConfig {
1289 fn default() -> Self {
1290 Self {
1291 host: default_gateway_host(),
1292 port: default_gateway_port(),
1293 expose_api_docs: false,
1294 response_timeout_secs: default_response_timeout_secs(),
1295 reliability: GatewayReliabilityConfig::default(),
1296 }
1297 }
1298}
1299
1300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1305#[serde(rename_all = "lowercase")]
1306pub enum ExecMode {
1307 #[default]
1309 Structured,
1310 Shell,
1312}
1313
1314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1316#[serde(rename_all = "snake_case")]
1317#[derive(Default)]
1318pub enum AllowlistMode {
1319 Permissive,
1321 #[default]
1323 Enforced,
1324}
1325
1326#[derive(Debug, Clone, Deserialize, Serialize)]
1330pub struct ExecConfig {
1331 #[serde(default)]
1333 pub default_mode: ExecMode,
1334 #[serde(default = "default_false")]
1336 pub allow_shell_mode: bool,
1337 #[serde(default)]
1340 pub allowed_commands: Vec<String>,
1341 #[serde(default)]
1345 pub allowlist_mode: AllowlistMode,
1346 #[serde(default = "default_exec_timeout")]
1348 pub default_timeout_secs: u64,
1349 #[serde(default = "default_exec_max_timeout")]
1351 pub max_timeout_secs: u64,
1352}
1353
1354fn default_false() -> bool {
1355 false
1356}
1357
1358fn default_exec_timeout() -> u64 {
1359 120
1360}
1361
1362fn default_exec_max_timeout() -> u64 {
1363 600
1364}
1365
1366impl ExecConfig {
1367 pub fn is_binary_allowed(&self, name: &str) -> bool {
1374 match self.allowlist_mode {
1375 AllowlistMode::Permissive => {
1376 self.allowed_commands.is_empty() || self.allowed_commands.iter().any(|c| c == name)
1377 }
1378 AllowlistMode::Enforced => self.allowed_commands.iter().any(|c| c == name),
1379 }
1380 }
1381}
1382
1383impl Default for ExecConfig {
1384 fn default() -> Self {
1385 Self {
1386 default_mode: ExecMode::default(),
1387 allow_shell_mode: default_false(),
1388 allowed_commands: Vec::new(),
1389 allowlist_mode: AllowlistMode::default(),
1390 default_timeout_secs: default_exec_timeout(),
1391 max_timeout_secs: default_exec_max_timeout(),
1392 }
1393 }
1394}
1395
1396#[derive(Debug, Clone, Deserialize, Serialize)]
1398pub struct OrchestratorConfig {
1399 #[serde(default = "default_max_evolution_iterations")]
1402 pub max_evolution_iterations: u32,
1403
1404 #[serde(default = "default_min_evaluation_score")]
1407 pub min_evaluation_score: f64,
1408}
1409
1410fn default_max_evolution_iterations() -> u32 {
1411 3
1412}
1413
1414fn default_min_evaluation_score() -> f64 {
1415 0.8
1416}
1417
1418impl Default for OrchestratorConfig {
1419 fn default() -> Self {
1420 Self {
1421 max_evolution_iterations: default_max_evolution_iterations(),
1422 min_evaluation_score: default_min_evaluation_score(),
1423 }
1424 }
1425}
1426
1427#[derive(Debug, Clone, Serialize, Deserialize)]
1432pub struct IntentConfig {
1433 #[serde(default = "default_intent_max_retries")]
1437 pub max_retries: u32,
1438
1439 #[serde(default = "default_intent_score_threshold")]
1443 pub score_threshold: f64,
1444
1445 #[serde(default = "default_intent_max_clarify_rounds")]
1449 pub max_clarify_rounds: u32,
1450
1451 #[serde(default = "default_intent_enable_retry")]
1455 pub enable_retry: bool,
1456
1457 #[serde(default)]
1461 pub lightweight_model: Option<String>,
1462}
1463
1464fn default_intent_max_retries() -> u32 {
1465 2
1466}
1467
1468fn default_intent_score_threshold() -> f64 {
1469 0.7
1470}
1471
1472fn default_intent_max_clarify_rounds() -> u32 {
1473 3
1474}
1475
1476fn default_intent_enable_retry() -> bool {
1477 true
1478}
1479
1480impl Default for IntentConfig {
1481 fn default() -> Self {
1482 Self {
1483 max_retries: default_intent_max_retries(),
1484 score_threshold: default_intent_score_threshold(),
1485 max_clarify_rounds: default_intent_max_clarify_rounds(),
1486 enable_retry: default_intent_enable_retry(),
1487 lightweight_model: None,
1488 }
1489 }
1490}
1491
1492#[derive(Debug, Clone, Deserialize, Serialize)]
1494pub struct ContextConfig {
1495 #[serde(default = "default_active_limit")]
1497 pub active_limit_tokens: usize,
1498 #[serde(default = "default_cache_limit")]
1500 pub cache_limit_entries: usize,
1501}
1502
1503fn default_active_limit() -> usize {
1504 100_000
1505}
1506
1507fn default_cache_limit() -> usize {
1508 50
1509}
1510
1511impl Default for ContextConfig {
1512 fn default() -> Self {
1513 Self {
1514 active_limit_tokens: default_active_limit(),
1515 cache_limit_entries: default_cache_limit(),
1516 }
1517 }
1518}
1519
1520#[derive(Debug, Clone, Deserialize, Serialize)]
1522pub struct SecurityConfig {
1523 #[serde(default = "default_allowed_tools")]
1525 pub allowed_tools: Vec<String>,
1526 #[serde(default)]
1528 pub network_access: bool,
1529 #[serde(default = "default_max_exec_time")]
1531 pub max_execution_time_secs: u64,
1532 #[serde(default = "default_max_memory")]
1534 pub max_memory_mb: u64,
1535 #[serde(default)]
1537 pub can_fork: bool,
1538 #[serde(default = "default_max_audit")]
1540 pub max_audit_entries: usize,
1541 #[serde(default)]
1543 pub auth_enabled: bool,
1544 #[serde(default = "default_cors_origins")]
1546 pub cors_origins: Vec<String>,
1547 #[serde(default)]
1549 pub audit_log_path: Option<String>,
1550 #[serde(default = "default_rate_limit_per_minute")]
1552 pub rate_limit_per_minute: u32,
1553}
1554
1555fn default_allowed_tools() -> Vec<String> {
1556 vec![
1557 "read".to_string(),
1558 "write".to_string(),
1559 "edit".to_string(),
1560 "bash".to_string(),
1561 "grep".to_string(),
1562 "find".to_string(),
1563 "exec".to_string(),
1564 ]
1565}
1566
1567fn default_max_exec_time() -> u64 {
1568 300
1569}
1570
1571fn default_max_memory() -> u64 {
1572 512
1573}
1574
1575fn default_max_audit() -> usize {
1576 10_000
1577}
1578
1579fn default_rate_limit_per_minute() -> u32 {
1580 600
1584}
1585
1586fn default_cors_origins() -> Vec<String> {
1587 vec![
1592 "http://localhost:4200".to_string(),
1593 "http://127.0.0.1:4200".to_string(),
1594 "http://localhost:5173".to_string(),
1595 "http://127.0.0.1:5173".to_string(),
1596 ]
1597}
1598
1599impl Default for SecurityConfig {
1600 fn default() -> Self {
1601 Self {
1602 allowed_tools: default_allowed_tools(),
1603 network_access: false,
1604 max_execution_time_secs: default_max_exec_time(),
1605 max_memory_mb: default_max_memory(),
1606 can_fork: false,
1607 max_audit_entries: default_max_audit(),
1608 auth_enabled: false,
1609 cors_origins: default_cors_origins(),
1610 audit_log_path: None,
1611 rate_limit_per_minute: default_rate_limit_per_minute(),
1612 }
1613 }
1614}
1615
1616#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1621pub struct PersonaConfig {
1622 #[serde(default)]
1624 pub default_persona_id: Option<String>,
1625}
1626
1627#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1635pub struct McpConfig {
1636 #[serde(default)]
1638 pub servers: std::collections::HashMap<String, McpServerDef>,
1639}
1640
1641#[derive(Debug, Clone, Deserialize, Serialize)]
1643pub struct McpServerDef {
1644 pub command: String,
1646 #[serde(default)]
1648 pub args: Vec<String>,
1649 #[serde(default)]
1651 pub env: std::collections::HashMap<String, String>,
1652 #[serde(default = "default_mcp_enabled")]
1654 pub enabled: bool,
1655}
1656
1657fn default_mcp_enabled() -> bool {
1658 true
1659}
1660
1661#[derive(Debug, Clone, Deserialize, Serialize)]
1663pub struct GitConfig {
1664 #[serde(default = "default_true")]
1666 pub auto_commit: bool,
1667}
1668
1669impl Default for GitConfig {
1670 fn default() -> Self {
1671 Self { auto_commit: true }
1672 }
1673}
1674
1675#[derive(Debug, Clone, Deserialize, Serialize)]
1677pub struct AuditConfig {
1678 #[serde(default = "default_audit_max_entries")]
1680 pub max_entries: usize,
1681 #[serde(default = "default_true")]
1683 pub enabled: bool,
1684}
1685
1686fn default_audit_max_entries() -> usize {
1687 100_000
1688}
1689
1690impl Default for AuditConfig {
1691 fn default() -> Self {
1692 Self {
1693 max_entries: default_audit_max_entries(),
1694 enabled: true,
1695 }
1696 }
1697}
1698
1699#[derive(Debug, Clone, Deserialize, Serialize)]
1701pub struct BudgetConfig {
1702 #[serde(default)]
1704 pub default_token_budget: u64,
1705 #[serde(default)]
1707 pub default_calls_budget: u64,
1708 #[serde(default = "default_budget_window")]
1710 pub default_window_secs: u64,
1711 #[serde(default = "default_true")]
1713 pub enabled: bool,
1714 #[serde(default)]
1718 pub monthly_spend_limit_usd: Option<f64>,
1719}
1720
1721fn default_budget_window() -> u64 {
1722 3600
1723}
1724
1725impl Default for BudgetConfig {
1726 fn default() -> Self {
1727 Self {
1728 default_token_budget: 0,
1729 default_calls_budget: 0,
1730 default_window_secs: default_budget_window(),
1731 enabled: true,
1732 monthly_spend_limit_usd: None,
1733 }
1734 }
1735}
1736
1737#[derive(Debug, Clone, Deserialize, Serialize)]
1739pub struct ResourceMonitorConfig {
1740 #[serde(default = "default_rm_interval")]
1742 pub interval_secs: u64,
1743 #[serde(default = "default_rm_history_max")]
1745 pub history_max: usize,
1746 #[serde(default = "default_rm_cpu_threshold")]
1748 pub cpu_threshold: f32,
1749 #[serde(default = "default_rm_mem_threshold")]
1751 pub memory_threshold: f32,
1752 #[serde(default = "default_rm_load_threshold")]
1754 pub load_threshold: f32,
1755}
1756
1757fn default_rm_interval() -> u64 {
1758 60
1759}
1760
1761fn default_rm_history_max() -> usize {
1762 60
1763}
1764
1765fn default_rm_cpu_threshold() -> f32 {
1766 90.0
1767}
1768
1769fn default_rm_mem_threshold() -> f32 {
1770 90.0
1771}
1772
1773fn default_rm_load_threshold() -> f32 {
1774 8.0
1775}
1776
1777impl Default for ResourceMonitorConfig {
1778 fn default() -> Self {
1779 Self {
1780 interval_secs: default_rm_interval(),
1781 history_max: default_rm_history_max(),
1782 cpu_threshold: default_rm_cpu_threshold(),
1783 memory_threshold: default_rm_mem_threshold(),
1784 load_threshold: default_rm_load_threshold(),
1785 }
1786 }
1787}
1788
1789#[derive(Debug, Clone, Serialize, Deserialize)]
1791pub struct AgentLogConfig {
1792 #[serde(default = "default_agent_log_max_entries")]
1794 pub max_entries: usize,
1795 #[serde(default = "default_agent_log_ttl_hours")]
1797 pub ttl_hours: u64,
1798 #[serde(default = "default_agent_log_max_tool_calls")]
1800 pub max_tool_calls_per_agent: usize,
1801 #[serde(default = "default_agent_log_prune_batch")]
1803 pub prune_batch_size: usize,
1804 #[serde(default)]
1806 pub db_path: String,
1807}
1808
1809fn default_agent_log_max_entries() -> usize {
1810 10_000
1811}
1812fn default_agent_log_ttl_hours() -> u64 {
1813 720
1814}
1815fn default_agent_log_max_tool_calls() -> usize {
1816 500
1817}
1818fn default_agent_log_prune_batch() -> usize {
1819 100
1820}
1821
1822impl Default for AgentLogConfig {
1823 fn default() -> Self {
1824 Self {
1825 max_entries: 10_000,
1826 ttl_hours: 720,
1827 max_tool_calls_per_agent: 500,
1828 prune_batch_size: 100,
1829 db_path: String::new(),
1830 }
1831 }
1832}
1833
1834#[derive(Debug, Clone, Deserialize, Serialize)]
1836pub struct LoggingConfig {
1837 #[serde(default = "default_log_format")]
1839 pub format: String,
1840 #[serde(default)]
1842 pub level: Option<String>,
1843}
1844
1845fn default_log_format() -> String {
1846 "pretty".into()
1847}
1848
1849impl Default for LoggingConfig {
1850 fn default() -> Self {
1851 Self {
1852 format: default_log_format(),
1853 level: None,
1854 }
1855 }
1856}
1857
1858#[derive(Debug, Clone, Deserialize, Serialize)]
1864pub struct BrowserConfig {
1865 #[serde(default = "default_browser_enabled")]
1867 pub enabled: bool,
1868
1869 #[serde(default)]
1880 pub engine: serde_json::Value,
1881}
1882
1883fn default_browser_enabled() -> bool {
1884 true
1885}
1886
1887impl Default for BrowserConfig {
1888 fn default() -> Self {
1889 Self {
1890 enabled: true,
1891 engine: serde_json::json!({}),
1892 }
1893 }
1894}
1895
1896pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
1898 let content = std::fs::read_to_string(path)?;
1899 let config: OxiosConfig = toml::from_str(&content)?;
1900 let (errors, warnings) = config.validate();
1901 for w in warnings {
1902 tracing::warn!("config: {}", w);
1903 }
1904 if !errors.is_empty() {
1905 let msg = errors.join("; ");
1906 anyhow::bail!("Configuration validation failed: {msg}");
1907 }
1908 Ok(config)
1909}
1910
1911impl OxiosConfig {
1912 pub fn api_key(&self) -> Option<String> {
1914 self.engine.api_key.clone().filter(|k| !k.is_empty())
1915 }
1916
1917 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
1920 let mut errors = Vec::new();
1921 let mut warnings = Vec::new();
1922
1923 if self.kernel.max_agents == 0 {
1925 errors.push("kernel.max_agents must be > 0".into());
1926 }
1927 if self.kernel.workspace.is_empty() {
1928 errors.push("kernel.workspace must not be empty".into());
1929 }
1930
1931 if self.gateway.port == 0 {
1933 errors.push("gateway.port must be > 0".into());
1934 }
1935 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
1936 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
1937 }
1938
1939 for (name, job) in &self.cron.jobs {
1941 if job.schedule.is_empty() {
1942 errors.push(format!("cron.jobs.{name}: schedule is empty"));
1943 } else {
1944 let normalized = {
1946 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
1947 match fields.len() {
1948 5 => format!("0 {}", job.schedule),
1949 _ => job.schedule.clone(),
1950 }
1951 };
1952 if Schedule::from_str(&normalized).is_err() {
1953 errors.push(format!(
1954 "cron.jobs.{}: invalid cron expression '{}'",
1955 name, job.schedule
1956 ));
1957 }
1958 }
1959 if job.goal.is_empty() {
1960 errors.push(format!("cron.jobs.{name}: goal is empty"));
1961 }
1962 }
1963
1964 if self.security.max_execution_time_secs == 0 {
1966 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
1967 }
1968
1969 if self.audit.max_entries == 0 {
1971 warnings.push("audit.max_entries is 0 — audit will never prune".into());
1972 }
1973
1974 if self.budget.default_window_secs == 0 {
1976 warnings.push("budget.default_window_secs is 0 — no time window".into());
1977 }
1978
1979 if self.gateway.response_timeout_secs == 0 {
1981 errors.push("gateway.response_timeout_secs must be > 0".into());
1982 }
1983
1984 if self.engine.api_key.as_ref().is_some_and(|k| !k.is_empty()) {
1987 warnings.push(
1988 "engine.api_key is set in config — prefer the oxi auth store or env var to avoid storing a secret on disk"
1989 .into(),
1990 );
1991 }
1992
1993 for (name, server) in &self.mcp.servers {
1995 if server.command.trim().is_empty() {
1996 errors.push(format!("mcp.servers.{name}: command must not be empty"));
1997 }
1998 }
1999
2000 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
2002 {
2003 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
2004 }
2005
2006 if self.exec.default_timeout_secs == 0 {
2008 errors.push("exec.default_timeout_secs must be > 0".into());
2009 }
2010 if self.exec.max_timeout_secs == 0 {
2011 errors.push("exec.max_timeout_secs must be > 0".into());
2012 }
2013 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
2014 errors.push(format!(
2015 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
2016 self.exec.default_timeout_secs, self.exec.max_timeout_secs
2017 ));
2018 }
2019
2020 if self.resource_monitor.cpu_threshold > 100.0 {
2022 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
2023 }
2024 if self.resource_monitor.memory_threshold > 100.0 {
2025 errors.push("resource_monitor.memory_threshold must be <= 100".into());
2026 }
2027
2028 for name in &self.channels.enabled {
2030 let valid = ["cli", "telegram"];
2031 if !valid.contains(&name.as_str()) {
2032 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
2033 }
2034 }
2035 if self.channels.enabled.iter().any(|c| c == "web") {
2037 warnings.push(
2038 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
2039 );
2040 }
2041 if self.channels.enabled.iter().any(|c| c == "telegram")
2042 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
2043 {
2044 warnings.push(format!(
2045 "channels.telegram: {} env var not set — telegram channel will fail",
2046 self.channels.telegram.bot_token_env
2047 ));
2048 }
2049 for err in self.token_maxing.validate() {
2053 errors.push(err);
2054 }
2055
2056 (errors, warnings)
2057 }
2058}
2059
2060pub fn expand_home(path: &str) -> std::path::PathBuf {
2072 if let Some(rest) = path.strip_prefix("~/") {
2073 if let Ok(home) = std::env::var("HOME") {
2074 return std::path::PathBuf::from(format!("{home}/{rest}"));
2075 }
2076 if let Some(home) = dirs::home_dir() {
2077 return home.join(rest);
2078 }
2079 }
2080 std::path::PathBuf::from(path)
2081}
2082
2083#[cfg(test)]
2084mod tests {
2085 use super::*;
2086
2087 #[test]
2088 fn test_default_config_validates() {
2089 let config = OxiosConfig::default();
2090 let (errors, _warnings) = config.validate();
2091 assert!(
2092 errors.is_empty(),
2093 "Default config should have no errors: {:?}",
2094 errors
2095 );
2096 }
2097
2098 #[test]
2099 fn test_exec_config_default_allowed_commands() {
2100 let config = ExecConfig::default();
2101 assert!(config.allowed_commands.is_empty());
2103 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
2104 assert!(!config.is_binary_allowed("anything"));
2105 assert!(!config.is_binary_allowed("bash"));
2106 }
2107
2108 #[test]
2109 fn test_exec_config_permissive_mode() {
2110 let config = ExecConfig {
2111 allowlist_mode: AllowlistMode::Permissive,
2112 ..Default::default()
2113 };
2114 assert!(config.is_binary_allowed("anything"));
2116 assert!(config.is_binary_allowed("bash"));
2117 }
2118
2119 #[test]
2120 fn test_is_binary_allowed_with_allowlist() {
2121 let config = ExecConfig {
2122 allowed_commands: vec!["git".into(), "echo".into()],
2123 ..Default::default()
2124 };
2125 assert!(config.is_binary_allowed("git"));
2126 assert!(config.is_binary_allowed("echo"));
2127 assert!(!config.is_binary_allowed("bash"));
2128 assert!(!config.is_binary_allowed("rm"));
2129 assert!(!config.is_binary_allowed("sudo"));
2130 }
2131
2132 #[test]
2133 fn test_expand_home() {
2134 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
2136 let expanded = expand_home("~/projects/test");
2137 assert_eq!(
2138 expanded.to_str().unwrap(),
2139 format!("{}/projects/test", home)
2140 );
2141
2142 let abs = expand_home("/absolute/path");
2144 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
2145
2146 let bare = expand_home("~something");
2148 assert_eq!(bare, std::path::PathBuf::from("~something"));
2149 }
2150
2151 #[test]
2152 fn test_invalid_cron_expression() {
2153 let mut config = OxiosConfig::default();
2154 config.cron.enabled = true;
2155 config.cron.jobs.insert(
2156 "bad-job".to_string(),
2157 InlineCronJob {
2158 schedule: "not a valid cron".to_string(),
2159 goal: "Test goal".to_string(),
2160 constraints: vec![],
2161 acceptance_criteria: vec![],
2162 toolchain: "default".to_string(),
2163 priority: Priority::Normal,
2164 enabled: true,
2165 },
2166 );
2167
2168 let (errors, _warnings) = config.validate();
2169 assert!(
2170 !errors.is_empty(),
2171 "Expected validation error for invalid cron"
2172 );
2173 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
2174 assert!(
2175 has_cron_error,
2176 "Expected 'invalid cron expression' error, got: {:?}",
2177 errors
2178 );
2179 }
2180
2181 #[test]
2182 fn test_config_serialization_roundtrip() {
2183 let config = OxiosConfig::default();
2184
2185 let toml_str = toml::to_string(&config).expect("serialization should succeed");
2187
2188 let deserialized: OxiosConfig =
2190 toml::from_str(&toml_str).expect("deserialization should succeed");
2191
2192 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2194 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2195 assert_eq!(config.gateway.host, deserialized.gateway.host);
2196 assert_eq!(config.gateway.port, deserialized.gateway.port);
2197 assert_eq!(
2198 config.exec.default_timeout_secs,
2199 deserialized.exec.default_timeout_secs
2200 );
2201 assert_eq!(
2202 config.exec.max_timeout_secs,
2203 deserialized.exec.max_timeout_secs
2204 );
2205 }
2206
2207 #[test]
2208 fn test_exec_timeout_validation() {
2209 let mut config = OxiosConfig::default();
2210 config.exec.default_timeout_secs = 999;
2212 config.exec.max_timeout_secs = 100;
2213 let (errors, _warnings) = config.validate();
2214 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2215 assert!(
2216 has_error,
2217 "Expected timeout ordering error, got: {:?}",
2218 errors
2219 );
2220 }
2221
2222 #[test]
2223 fn test_zero_max_agents_error() {
2224 let mut config = OxiosConfig::default();
2225 config.kernel.max_agents = 0;
2226 let (errors, _warnings) = config.validate();
2227 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2228 }
2229
2230 #[test]
2235 fn test_default_config_matches_toml() {
2236 let from_rust = OxiosConfig::default();
2237
2238 let toml_str = include_str!("../../../share/default-config.toml");
2239 let from_toml: OxiosConfig =
2240 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2241
2242 assert_eq!(
2244 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2245 "kernel.max_agents 불일치: Rust={}, TOML={}",
2246 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2247 );
2248 assert_eq!(
2249 from_rust.gateway.host, from_toml.gateway.host,
2250 "gateway.host 불일치: Rust={}, TOML={}",
2251 from_rust.gateway.host, from_toml.gateway.host
2252 );
2253 assert_eq!(
2254 from_rust.gateway.port, from_toml.gateway.port,
2255 "gateway.port 불일치: Rust={}, TOML={}",
2256 from_rust.gateway.port, from_toml.gateway.port
2257 );
2258 assert_eq!(
2259 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2260 "kernel.event_bus_capacity 불일치"
2261 );
2262 assert_eq!(
2263 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2264 "memory.consolidation.preset 불일치"
2265 );
2266
2267 let (_, warnings) = from_toml.validate();
2269 for w in &warnings {
2270 eprintln!("default-config.toml 경고: {}", w);
2271 }
2272 }
2273
2274 #[test]
2277 fn test_gateway_should_expose_api_docs() {
2278 let cfg = GatewayConfig::default();
2280 assert!(!cfg.should_expose_api_docs());
2281
2282 let cfg = GatewayConfig {
2284 host: "0.0.0.0".into(),
2285 port: 4200,
2286 expose_api_docs: true,
2287 ..Default::default()
2288 };
2289 assert!(
2290 !cfg.should_expose_api_docs(),
2291 "public bind must not expose api docs even when opt-in is true"
2292 );
2293
2294 let cfg = GatewayConfig {
2296 host: "127.0.0.1".into(),
2297 port: 4200,
2298 expose_api_docs: true,
2299 ..Default::default()
2300 };
2301 assert!(cfg.should_expose_api_docs());
2302
2303 let cfg = GatewayConfig {
2305 host: "::1".into(),
2306 port: 4200,
2307 expose_api_docs: true,
2308 ..Default::default()
2309 };
2310 assert!(cfg.should_expose_api_docs());
2311
2312 let cfg = GatewayConfig {
2314 host: "localhost".into(),
2315 port: 4200,
2316 expose_api_docs: true,
2317 ..Default::default()
2318 };
2319 assert!(cfg.should_expose_api_docs());
2320
2321 let cfg = GatewayConfig {
2323 host: "127.0.0.1".into(),
2324 port: 4200,
2325 expose_api_docs: false,
2326 ..Default::default()
2327 };
2328 assert!(!cfg.should_expose_api_docs());
2329 }
2330}