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