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