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 120
1581}
1582
1583fn default_cors_origins() -> Vec<String> {
1584 vec![
1589 "http://localhost:4200".to_string(),
1590 "http://127.0.0.1:4200".to_string(),
1591 "http://localhost:5173".to_string(),
1592 "http://127.0.0.1:5173".to_string(),
1593 ]
1594}
1595
1596impl Default for SecurityConfig {
1597 fn default() -> Self {
1598 Self {
1599 allowed_tools: default_allowed_tools(),
1600 network_access: false,
1601 max_execution_time_secs: default_max_exec_time(),
1602 max_memory_mb: default_max_memory(),
1603 can_fork: false,
1604 max_audit_entries: default_max_audit(),
1605 auth_enabled: false,
1606 cors_origins: default_cors_origins(),
1607 audit_log_path: None,
1608 rate_limit_per_minute: default_rate_limit_per_minute(),
1609 }
1610 }
1611}
1612
1613#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1618pub struct PersonaConfig {
1619 #[serde(default)]
1621 pub default_persona_id: Option<String>,
1622}
1623
1624#[derive(Debug, Clone, Deserialize, Serialize, Default)]
1632pub struct McpConfig {
1633 #[serde(default)]
1635 pub servers: std::collections::HashMap<String, McpServerDef>,
1636}
1637
1638#[derive(Debug, Clone, Deserialize, Serialize)]
1640pub struct McpServerDef {
1641 pub command: String,
1643 #[serde(default)]
1645 pub args: Vec<String>,
1646 #[serde(default)]
1648 pub env: std::collections::HashMap<String, String>,
1649 #[serde(default = "default_mcp_enabled")]
1651 pub enabled: bool,
1652}
1653
1654fn default_mcp_enabled() -> bool {
1655 true
1656}
1657
1658#[derive(Debug, Clone, Deserialize, Serialize)]
1660pub struct GitConfig {
1661 #[serde(default = "default_true")]
1663 pub auto_commit: bool,
1664}
1665
1666impl Default for GitConfig {
1667 fn default() -> Self {
1668 Self { auto_commit: true }
1669 }
1670}
1671
1672#[derive(Debug, Clone, Deserialize, Serialize)]
1674pub struct AuditConfig {
1675 #[serde(default = "default_audit_max_entries")]
1677 pub max_entries: usize,
1678 #[serde(default = "default_true")]
1680 pub enabled: bool,
1681}
1682
1683fn default_audit_max_entries() -> usize {
1684 100_000
1685}
1686
1687impl Default for AuditConfig {
1688 fn default() -> Self {
1689 Self {
1690 max_entries: default_audit_max_entries(),
1691 enabled: true,
1692 }
1693 }
1694}
1695
1696#[derive(Debug, Clone, Deserialize, Serialize)]
1698pub struct BudgetConfig {
1699 #[serde(default)]
1701 pub default_token_budget: u64,
1702 #[serde(default)]
1704 pub default_calls_budget: u64,
1705 #[serde(default = "default_budget_window")]
1707 pub default_window_secs: u64,
1708 #[serde(default = "default_true")]
1710 pub enabled: bool,
1711 #[serde(default)]
1715 pub monthly_spend_limit_usd: Option<f64>,
1716}
1717
1718fn default_budget_window() -> u64 {
1719 3600
1720}
1721
1722impl Default for BudgetConfig {
1723 fn default() -> Self {
1724 Self {
1725 default_token_budget: 0,
1726 default_calls_budget: 0,
1727 default_window_secs: default_budget_window(),
1728 enabled: true,
1729 monthly_spend_limit_usd: None,
1730 }
1731 }
1732}
1733
1734#[derive(Debug, Clone, Deserialize, Serialize)]
1736pub struct ResourceMonitorConfig {
1737 #[serde(default = "default_rm_interval")]
1739 pub interval_secs: u64,
1740 #[serde(default = "default_rm_history_max")]
1742 pub history_max: usize,
1743 #[serde(default = "default_rm_cpu_threshold")]
1745 pub cpu_threshold: f32,
1746 #[serde(default = "default_rm_mem_threshold")]
1748 pub memory_threshold: f32,
1749 #[serde(default = "default_rm_load_threshold")]
1751 pub load_threshold: f32,
1752}
1753
1754fn default_rm_interval() -> u64 {
1755 60
1756}
1757
1758fn default_rm_history_max() -> usize {
1759 60
1760}
1761
1762fn default_rm_cpu_threshold() -> f32 {
1763 90.0
1764}
1765
1766fn default_rm_mem_threshold() -> f32 {
1767 90.0
1768}
1769
1770fn default_rm_load_threshold() -> f32 {
1771 8.0
1772}
1773
1774impl Default for ResourceMonitorConfig {
1775 fn default() -> Self {
1776 Self {
1777 interval_secs: default_rm_interval(),
1778 history_max: default_rm_history_max(),
1779 cpu_threshold: default_rm_cpu_threshold(),
1780 memory_threshold: default_rm_mem_threshold(),
1781 load_threshold: default_rm_load_threshold(),
1782 }
1783 }
1784}
1785
1786#[derive(Debug, Clone, Serialize, Deserialize)]
1788pub struct AgentLogConfig {
1789 #[serde(default = "default_agent_log_max_entries")]
1791 pub max_entries: usize,
1792 #[serde(default = "default_agent_log_ttl_hours")]
1794 pub ttl_hours: u64,
1795 #[serde(default = "default_agent_log_max_tool_calls")]
1797 pub max_tool_calls_per_agent: usize,
1798 #[serde(default = "default_agent_log_prune_batch")]
1800 pub prune_batch_size: usize,
1801 #[serde(default)]
1803 pub db_path: String,
1804}
1805
1806fn default_agent_log_max_entries() -> usize {
1807 10_000
1808}
1809fn default_agent_log_ttl_hours() -> u64 {
1810 720
1811}
1812fn default_agent_log_max_tool_calls() -> usize {
1813 500
1814}
1815fn default_agent_log_prune_batch() -> usize {
1816 100
1817}
1818
1819impl Default for AgentLogConfig {
1820 fn default() -> Self {
1821 Self {
1822 max_entries: 10_000,
1823 ttl_hours: 720,
1824 max_tool_calls_per_agent: 500,
1825 prune_batch_size: 100,
1826 db_path: String::new(),
1827 }
1828 }
1829}
1830
1831#[derive(Debug, Clone, Deserialize, Serialize)]
1833pub struct LoggingConfig {
1834 #[serde(default = "default_log_format")]
1836 pub format: String,
1837 #[serde(default)]
1839 pub level: Option<String>,
1840}
1841
1842fn default_log_format() -> String {
1843 "pretty".into()
1844}
1845
1846impl Default for LoggingConfig {
1847 fn default() -> Self {
1848 Self {
1849 format: default_log_format(),
1850 level: None,
1851 }
1852 }
1853}
1854
1855#[derive(Debug, Clone, Deserialize, Serialize)]
1861pub struct BrowserConfig {
1862 #[serde(default = "default_browser_enabled")]
1864 pub enabled: bool,
1865
1866 #[serde(default)]
1877 pub engine: serde_json::Value,
1878}
1879
1880fn default_browser_enabled() -> bool {
1881 true
1882}
1883
1884impl Default for BrowserConfig {
1885 fn default() -> Self {
1886 Self {
1887 enabled: true,
1888 engine: serde_json::json!({}),
1889 }
1890 }
1891}
1892
1893pub fn load_config(path: &std::path::Path) -> anyhow::Result<OxiosConfig> {
1895 let content = std::fs::read_to_string(path)?;
1896 let config: OxiosConfig = toml::from_str(&content)?;
1897 let (errors, warnings) = config.validate();
1898 for w in warnings {
1899 tracing::warn!("config: {}", w);
1900 }
1901 if !errors.is_empty() {
1902 let msg = errors.join("; ");
1903 anyhow::bail!("Configuration validation failed: {msg}");
1904 }
1905 Ok(config)
1906}
1907
1908impl OxiosConfig {
1909 pub fn api_key(&self) -> Option<String> {
1911 self.engine.api_key.clone().filter(|k| !k.is_empty())
1912 }
1913
1914 pub fn validate(&self) -> (Vec<String>, Vec<String>) {
1917 let mut errors = Vec::new();
1918 let mut warnings = Vec::new();
1919
1920 if self.kernel.max_agents == 0 {
1922 errors.push("kernel.max_agents must be > 0".into());
1923 }
1924 if self.kernel.workspace.is_empty() {
1925 errors.push("kernel.workspace must not be empty".into());
1926 }
1927
1928 if self.gateway.port == 0 {
1930 errors.push("gateway.port must be > 0".into());
1931 }
1932 if self.gateway.port < 1024 && self.gateway.host == "0.0.0.0" {
1933 warnings.push("Running on port <1024 as 0.0.0.0 may require root".into());
1934 }
1935
1936 for (name, job) in &self.cron.jobs {
1938 if job.schedule.is_empty() {
1939 errors.push(format!("cron.jobs.{name}: schedule is empty"));
1940 } else {
1941 let normalized = {
1943 let fields: Vec<&str> = job.schedule.split_whitespace().collect();
1944 match fields.len() {
1945 5 => format!("0 {}", job.schedule),
1946 _ => job.schedule.clone(),
1947 }
1948 };
1949 if Schedule::from_str(&normalized).is_err() {
1950 errors.push(format!(
1951 "cron.jobs.{}: invalid cron expression '{}'",
1952 name, job.schedule
1953 ));
1954 }
1955 }
1956 if job.goal.is_empty() {
1957 errors.push(format!("cron.jobs.{name}: goal is empty"));
1958 }
1959 }
1960
1961 if self.security.max_execution_time_secs == 0 {
1963 warnings.push("security.max_execution_time_secs is 0 — no timeout".into());
1964 }
1965
1966 if self.audit.max_entries == 0 {
1968 warnings.push("audit.max_entries is 0 — audit will never prune".into());
1969 }
1970
1971 if self.budget.default_window_secs == 0 {
1973 warnings.push("budget.default_window_secs is 0 — no time window".into());
1974 }
1975
1976 if self.gateway.response_timeout_secs == 0 {
1978 errors.push("gateway.response_timeout_secs must be > 0".into());
1979 }
1980
1981 if self.engine.api_key.as_ref().is_some_and(|k| !k.is_empty()) {
1984 warnings.push(
1985 "engine.api_key is set in config — prefer the oxi auth store or env var to avoid storing a secret on disk"
1986 .into(),
1987 );
1988 }
1989
1990 for (name, server) in &self.mcp.servers {
1992 if server.command.trim().is_empty() {
1993 errors.push(format!("mcp.servers.{name}: command must not be empty"));
1994 }
1995 }
1996
1997 if self.session.max_sessions == 0 && self.session.ttl_hours == 0 && self.session.auto_prune
1999 {
2000 warnings.push("session: auto_prune is enabled but both max_sessions and ttl_hours are 0 — nothing will be pruned".into());
2001 }
2002
2003 if self.exec.default_timeout_secs == 0 {
2005 errors.push("exec.default_timeout_secs must be > 0".into());
2006 }
2007 if self.exec.max_timeout_secs == 0 {
2008 errors.push("exec.max_timeout_secs must be > 0".into());
2009 }
2010 if self.exec.default_timeout_secs > self.exec.max_timeout_secs {
2011 errors.push(format!(
2012 "exec.default_timeout_secs ({}) must not exceed max_timeout_secs ({})",
2013 self.exec.default_timeout_secs, self.exec.max_timeout_secs
2014 ));
2015 }
2016
2017 if self.resource_monitor.cpu_threshold > 100.0 {
2019 errors.push("resource_monitor.cpu_threshold must be <= 100".into());
2020 }
2021 if self.resource_monitor.memory_threshold > 100.0 {
2022 errors.push("resource_monitor.memory_threshold must be <= 100".into());
2023 }
2024
2025 for name in &self.channels.enabled {
2027 let valid = ["cli", "telegram"];
2028 if !valid.contains(&name.as_str()) {
2029 warnings.push(format!("channels.enabled: unknown channel '{name}'"));
2030 }
2031 }
2032 if self.channels.enabled.iter().any(|c| c == "web") {
2034 warnings.push(
2035 "channels.enabled: 'web' should be listed under [surfaces], not [channels]".into(),
2036 );
2037 }
2038 if self.channels.enabled.iter().any(|c| c == "telegram")
2039 && std::env::var(&self.channels.telegram.bot_token_env).is_err()
2040 {
2041 warnings.push(format!(
2042 "channels.telegram: {} env var not set — telegram channel will fail",
2043 self.channels.telegram.bot_token_env
2044 ));
2045 }
2046 for err in self.token_maxing.validate() {
2050 errors.push(err);
2051 }
2052
2053 (errors, warnings)
2054 }
2055}
2056
2057pub fn expand_home(path: &str) -> std::path::PathBuf {
2069 if let Some(rest) = path.strip_prefix("~/") {
2070 if let Ok(home) = std::env::var("HOME") {
2071 return std::path::PathBuf::from(format!("{home}/{rest}"));
2072 }
2073 if let Some(home) = dirs::home_dir() {
2074 return home.join(rest);
2075 }
2076 }
2077 std::path::PathBuf::from(path)
2078}
2079
2080#[cfg(test)]
2081mod tests {
2082 use super::*;
2083
2084 #[test]
2085 fn test_default_config_validates() {
2086 let config = OxiosConfig::default();
2087 let (errors, _warnings) = config.validate();
2088 assert!(
2089 errors.is_empty(),
2090 "Default config should have no errors: {:?}",
2091 errors
2092 );
2093 }
2094
2095 #[test]
2096 fn test_exec_config_default_allowed_commands() {
2097 let config = ExecConfig::default();
2098 assert!(config.allowed_commands.is_empty());
2100 assert_eq!(config.allowlist_mode, AllowlistMode::Enforced);
2101 assert!(!config.is_binary_allowed("anything"));
2102 assert!(!config.is_binary_allowed("bash"));
2103 }
2104
2105 #[test]
2106 fn test_exec_config_permissive_mode() {
2107 let config = ExecConfig {
2108 allowlist_mode: AllowlistMode::Permissive,
2109 ..Default::default()
2110 };
2111 assert!(config.is_binary_allowed("anything"));
2113 assert!(config.is_binary_allowed("bash"));
2114 }
2115
2116 #[test]
2117 fn test_is_binary_allowed_with_allowlist() {
2118 let config = ExecConfig {
2119 allowed_commands: vec!["git".into(), "echo".into()],
2120 ..Default::default()
2121 };
2122 assert!(config.is_binary_allowed("git"));
2123 assert!(config.is_binary_allowed("echo"));
2124 assert!(!config.is_binary_allowed("bash"));
2125 assert!(!config.is_binary_allowed("rm"));
2126 assert!(!config.is_binary_allowed("sudo"));
2127 }
2128
2129 #[test]
2130 fn test_expand_home() {
2131 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp/testhome".into());
2133 let expanded = expand_home("~/projects/test");
2134 assert_eq!(
2135 expanded.to_str().unwrap(),
2136 format!("{}/projects/test", home)
2137 );
2138
2139 let abs = expand_home("/absolute/path");
2141 assert_eq!(abs, std::path::PathBuf::from("/absolute/path"));
2142
2143 let bare = expand_home("~something");
2145 assert_eq!(bare, std::path::PathBuf::from("~something"));
2146 }
2147
2148 #[test]
2149 fn test_invalid_cron_expression() {
2150 let mut config = OxiosConfig::default();
2151 config.cron.enabled = true;
2152 config.cron.jobs.insert(
2153 "bad-job".to_string(),
2154 InlineCronJob {
2155 schedule: "not a valid cron".to_string(),
2156 goal: "Test goal".to_string(),
2157 constraints: vec![],
2158 acceptance_criteria: vec![],
2159 toolchain: "default".to_string(),
2160 priority: Priority::Normal,
2161 enabled: true,
2162 },
2163 );
2164
2165 let (errors, _warnings) = config.validate();
2166 assert!(
2167 !errors.is_empty(),
2168 "Expected validation error for invalid cron"
2169 );
2170 let has_cron_error = errors.iter().any(|e| e.contains("invalid cron expression"));
2171 assert!(
2172 has_cron_error,
2173 "Expected 'invalid cron expression' error, got: {:?}",
2174 errors
2175 );
2176 }
2177
2178 #[test]
2179 fn test_config_serialization_roundtrip() {
2180 let config = OxiosConfig::default();
2181
2182 let toml_str = toml::to_string(&config).expect("serialization should succeed");
2184
2185 let deserialized: OxiosConfig =
2187 toml::from_str(&toml_str).expect("deserialization should succeed");
2188
2189 assert_eq!(config.kernel.max_agents, deserialized.kernel.max_agents);
2191 assert_eq!(config.kernel.workspace, deserialized.kernel.workspace);
2192 assert_eq!(config.gateway.host, deserialized.gateway.host);
2193 assert_eq!(config.gateway.port, deserialized.gateway.port);
2194 assert_eq!(
2195 config.exec.default_timeout_secs,
2196 deserialized.exec.default_timeout_secs
2197 );
2198 assert_eq!(
2199 config.exec.max_timeout_secs,
2200 deserialized.exec.max_timeout_secs
2201 );
2202 }
2203
2204 #[test]
2205 fn test_exec_timeout_validation() {
2206 let mut config = OxiosConfig::default();
2207 config.exec.default_timeout_secs = 999;
2209 config.exec.max_timeout_secs = 100;
2210 let (errors, _warnings) = config.validate();
2211 let has_error = errors.iter().any(|e| e.contains("must not exceed"));
2212 assert!(
2213 has_error,
2214 "Expected timeout ordering error, got: {:?}",
2215 errors
2216 );
2217 }
2218
2219 #[test]
2220 fn test_zero_max_agents_error() {
2221 let mut config = OxiosConfig::default();
2222 config.kernel.max_agents = 0;
2223 let (errors, _warnings) = config.validate();
2224 assert!(errors.iter().any(|e| e.contains("max_agents must be > 0")));
2225 }
2226
2227 #[test]
2232 fn test_default_config_matches_toml() {
2233 let from_rust = OxiosConfig::default();
2234
2235 let toml_str = include_str!("../../../share/default-config.toml");
2236 let from_toml: OxiosConfig =
2237 toml::from_str(toml_str).expect("share/default-config.toml이 유효하지 않습니다");
2238
2239 assert_eq!(
2241 from_rust.kernel.max_agents, from_toml.kernel.max_agents,
2242 "kernel.max_agents 불일치: Rust={}, TOML={}",
2243 from_rust.kernel.max_agents, from_toml.kernel.max_agents
2244 );
2245 assert_eq!(
2246 from_rust.gateway.host, from_toml.gateway.host,
2247 "gateway.host 불일치: Rust={}, TOML={}",
2248 from_rust.gateway.host, from_toml.gateway.host
2249 );
2250 assert_eq!(
2251 from_rust.gateway.port, from_toml.gateway.port,
2252 "gateway.port 불일치: Rust={}, TOML={}",
2253 from_rust.gateway.port, from_toml.gateway.port
2254 );
2255 assert_eq!(
2256 from_rust.kernel.event_bus_capacity, from_toml.kernel.event_bus_capacity,
2257 "kernel.event_bus_capacity 불일치"
2258 );
2259 assert_eq!(
2260 from_rust.memory.consolidation.preset, from_toml.memory.consolidation.preset,
2261 "memory.consolidation.preset 불일치"
2262 );
2263
2264 let (_, warnings) = from_toml.validate();
2266 for w in &warnings {
2267 eprintln!("default-config.toml 경고: {}", w);
2268 }
2269 }
2270
2271 #[test]
2274 fn test_gateway_should_expose_api_docs() {
2275 let cfg = GatewayConfig::default();
2277 assert!(!cfg.should_expose_api_docs());
2278
2279 let cfg = GatewayConfig {
2281 host: "0.0.0.0".into(),
2282 port: 4200,
2283 expose_api_docs: true,
2284 ..Default::default()
2285 };
2286 assert!(
2287 !cfg.should_expose_api_docs(),
2288 "public bind must not expose api docs even when opt-in is true"
2289 );
2290
2291 let cfg = GatewayConfig {
2293 host: "127.0.0.1".into(),
2294 port: 4200,
2295 expose_api_docs: true,
2296 ..Default::default()
2297 };
2298 assert!(cfg.should_expose_api_docs());
2299
2300 let cfg = GatewayConfig {
2302 host: "::1".into(),
2303 port: 4200,
2304 expose_api_docs: true,
2305 ..Default::default()
2306 };
2307 assert!(cfg.should_expose_api_docs());
2308
2309 let cfg = GatewayConfig {
2311 host: "localhost".into(),
2312 port: 4200,
2313 expose_api_docs: true,
2314 ..Default::default()
2315 };
2316 assert!(cfg.should_expose_api_docs());
2317
2318 let cfg = GatewayConfig {
2320 host: "127.0.0.1".into(),
2321 port: 4200,
2322 expose_api_docs: false,
2323 ..Default::default()
2324 };
2325 assert!(!cfg.should_expose_api_docs());
2326 }
2327}