nexo_memory_snapshot/
config.rs1use std::path::PathBuf;
36use std::time::Duration;
37
38use serde::{Deserialize, Serialize};
39
40use crate::retention::RetentionConfig;
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct MemorySnapshotConfig {
45 #[serde(default = "default_enabled")]
46 pub enabled: bool,
47 #[serde(default = "default_root")]
48 pub root: PathBuf,
49 #[serde(default)]
50 pub auto_pre_dream: bool,
51 #[serde(default = "default_true")]
52 pub auto_pre_restore: bool,
53 #[serde(default)]
54 pub auto_pre_mutating_tool: bool,
55 #[serde(default = "default_lock_timeout_secs")]
56 pub lock_timeout_secs: u64,
57 #[serde(default = "default_true")]
58 pub redact_secrets_default: bool,
59 #[serde(default)]
60 pub encryption: EncryptionSection,
61 #[serde(default)]
62 pub retention: RetentionSection,
63 #[serde(default)]
64 pub events: EventsSection,
65}
66
67impl Default for MemorySnapshotConfig {
68 fn default() -> Self {
69 Self {
70 enabled: default_enabled(),
71 root: default_root(),
72 auto_pre_dream: false,
73 auto_pre_restore: true,
74 auto_pre_mutating_tool: false,
75 lock_timeout_secs: default_lock_timeout_secs(),
76 redact_secrets_default: true,
77 encryption: EncryptionSection::default(),
78 retention: RetentionSection::default(),
79 events: EventsSection::default(),
80 }
81 }
82}
83
84impl MemorySnapshotConfig {
85 pub fn lock_timeout(&self) -> Duration {
86 Duration::from_secs(self.lock_timeout_secs)
87 }
88
89 pub fn retention_runtime(&self) -> RetentionConfig {
90 RetentionConfig {
91 keep_count: self.retention.keep_count,
92 max_age_days: self.retention.max_age_days,
93 gc_interval_secs: self.retention.gc_interval_secs,
94 }
95 }
96
97 pub fn validate(&self) -> Result<(), String> {
102 if self.lock_timeout_secs == 0 {
103 return Err("memory.snapshot.lock_timeout_secs must be >= 1".into());
104 }
105 if self.retention.gc_interval_secs == 0 {
106 return Err("memory.snapshot.retention.gc_interval_secs must be >= 1".into());
107 }
108 if self.encryption.enabled && self.encryption.recipients.is_empty() {
109 return Err(
110 "memory.snapshot.encryption.enabled = true requires at least one recipient".into(),
111 );
112 }
113 Ok(())
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, Default)]
118#[serde(deny_unknown_fields, default)]
119pub struct EncryptionSection {
120 pub enabled: bool,
121 pub recipients: Vec<String>,
122 pub identity_path: Option<PathBuf>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(deny_unknown_fields, default)]
127pub struct RetentionSection {
128 pub keep_count: u32,
129 pub max_age_days: u32,
130 pub gc_interval_secs: u64,
131}
132
133impl Default for RetentionSection {
134 fn default() -> Self {
135 let r = RetentionConfig::default();
136 Self {
137 keep_count: r.keep_count,
138 max_age_days: r.max_age_days,
139 gc_interval_secs: r.gc_interval_secs,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(deny_unknown_fields, default)]
146pub struct EventsSection {
147 pub mutation_subject_prefix: String,
148 pub lifecycle_subject_prefix: String,
149 pub mutation_publish_enabled: bool,
150}
151
152impl Default for EventsSection {
153 fn default() -> Self {
154 Self {
155 mutation_subject_prefix: crate::events::MUTATION_SUBJECT_PREFIX.to_string(),
156 lifecycle_subject_prefix: crate::events::LIFECYCLE_SUBJECT_PREFIX.to_string(),
157 mutation_publish_enabled: true,
158 }
159 }
160}
161
162fn default_enabled() -> bool {
163 true
164}
165
166fn default_true() -> bool {
167 true
168}
169
170fn default_root() -> PathBuf {
171 PathBuf::from("./state")
172}
173
174fn default_lock_timeout_secs() -> u64 {
175 60
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn defaults_match_spec() {
184 let c = MemorySnapshotConfig::default();
185 assert!(c.enabled);
186 assert!(c.auto_pre_restore);
187 assert!(!c.auto_pre_dream);
188 assert!(!c.auto_pre_mutating_tool);
189 assert_eq!(c.lock_timeout_secs, 60);
190 assert!(c.redact_secrets_default);
191 assert_eq!(c.retention.keep_count, 30);
192 assert_eq!(c.retention.max_age_days, 90);
193 assert_eq!(c.retention.gc_interval_secs, 3600);
194 assert!(c.events.mutation_publish_enabled);
195 assert!(!c.encryption.enabled);
196 }
197
198 #[test]
199 fn parses_minimal_yaml_with_defaults() {
200 let c: MemorySnapshotConfig = serde_yaml::from_str("enabled: true\n").unwrap();
201 assert!(c.enabled);
202 assert_eq!(c.retention.keep_count, 30);
203 }
204
205 #[test]
206 fn parses_full_yaml_block() {
207 let yaml = r#"
208enabled: true
209root: /var/lib/nexo
210auto_pre_dream: true
211auto_pre_restore: false
212auto_pre_mutating_tool: true
213lock_timeout_secs: 30
214redact_secrets_default: false
215encryption:
216 enabled: true
217 recipients:
218 - age1abc
219 identity_path: /etc/nexo/snapshot.key
220retention:
221 keep_count: 5
222 max_age_days: 7
223 gc_interval_secs: 600
224events:
225 mutation_subject_prefix: "x.memory.mutated"
226 lifecycle_subject_prefix: "x.memory.snapshot"
227 mutation_publish_enabled: false
228"#;
229 let c: MemorySnapshotConfig = serde_yaml::from_str(yaml).unwrap();
230 assert_eq!(c.lock_timeout_secs, 30);
231 assert!(c.encryption.enabled);
232 assert_eq!(c.encryption.recipients, vec!["age1abc"]);
233 assert_eq!(c.retention.keep_count, 5);
234 assert_eq!(c.events.mutation_subject_prefix, "x.memory.mutated");
235 c.validate().unwrap();
236 }
237
238 #[test]
239 fn rejects_unknown_fields() {
240 let yaml = "enabled: true\nbogus_key: 1\n";
241 let r: Result<MemorySnapshotConfig, _> = serde_yaml::from_str(yaml);
242 assert!(r.is_err());
243 }
244
245 #[test]
246 fn validate_rejects_zero_lock_timeout() {
247 let c = MemorySnapshotConfig {
248 lock_timeout_secs: 0,
249 ..Default::default()
250 };
251 assert!(c.validate().is_err());
252 }
253
254 #[test]
255 fn validate_rejects_zero_gc_interval() {
256 let c = MemorySnapshotConfig {
257 retention: RetentionSection {
258 gc_interval_secs: 0,
259 ..Default::default()
260 },
261 ..Default::default()
262 };
263 assert!(c.validate().is_err());
264 }
265
266 #[test]
267 fn validate_rejects_encryption_enabled_without_recipients() {
268 let c = MemorySnapshotConfig {
269 encryption: EncryptionSection {
270 enabled: true,
271 recipients: Vec::new(),
272 identity_path: None,
273 },
274 ..Default::default()
275 };
276 assert!(c.validate().is_err());
277 }
278
279 #[test]
280 fn retention_runtime_round_trips_to_runtime_struct() {
281 let mut c = MemorySnapshotConfig::default();
282 c.retention.keep_count = 7;
283 c.retention.max_age_days = 14;
284 let r = c.retention_runtime();
285 assert_eq!(r.keep_count, 7);
286 assert_eq!(r.max_age_days, 14);
287 }
288}