1pub mod agent_def;
36pub mod config;
37pub mod doctor;
38pub mod encrypted_store;
39pub mod home;
40pub mod hook_config;
41pub mod medic;
42pub mod project_def;
43pub mod secrets;
44pub mod sync;
45
46pub use agent_def::{AgentDef, AgentRegistry, ChannelBotConfig};
47pub use config::*;
48pub use doctor::{run_checks, Check, CheckStatus, DoctorReport};
49pub use encrypted_store::{default_config_path, EncryptedStore};
50pub use home::{
51 create_config_backup, enact_home, ensure_home_dirs, load_dotenv_from_home,
52 load_enact_md_context, resolve_config_file, write_env_secret, write_yaml_at_home,
53};
54pub use hook_config::{HookConfig, HookDecision, HookEvent, HookHandler, HooksConfig};
55pub use medic::{disallowed_top_level_keys, reference_yaml, REFERENCE_FILES};
56pub use project_def::{ProjectDef, ProjectRegistry, Task, TaskBoard};
57pub use secrets::SecretManager;
58pub use sync::{SyncManager, SyncStatus};
59
60use anyhow::{Context, Result};
61use std::path::PathBuf;
62use tracing::{debug, info};
63
64pub struct ConfigManager {
66 encrypted_store: EncryptedStore,
67 secrets: SecretManager,
68 sync_manager: Option<SyncManager>,
69}
70
71impl ConfigManager {
72 pub async fn new(config_path: impl Into<PathBuf>) -> Result<Self> {
77 let config_path = config_path.into();
78
79 if std::env::var("ENACT_USE_MOCK_SECRET_STORE").is_ok()
81 || std::env::var("CARGO_TARGET_TMPDIR").is_ok()
82 {
83 #[allow(clippy::needless_return)]
84 return Self::new_with_mock_secrets(config_path).await;
85 }
86
87 #[cfg(test)]
88 {
89 #[allow(clippy::needless_return)]
90 return Self::new_with_mock_secrets(config_path).await;
91 }
92
93 #[cfg(not(test))]
94 {
95 let secrets = SecretManager::new();
98 let encrypted_store =
113 EncryptedStore::new(&config_path).context("Failed to create encrypted store")?;
114
115 Ok(Self {
116 encrypted_store,
117 secrets,
118 sync_manager: None,
119 })
120 }
121 }
122
123 pub async fn new_with_mock_secrets(config_path: impl Into<PathBuf>) -> Result<Self> {
125 let config_path = config_path.into();
126 let mock_secrets = SecretManager::new_mock();
127 let encrypted_store = EncryptedStore::with_secrets(&config_path, mock_secrets.clone())
137 .context("Failed to create encrypted store")?;
138
139 Ok(Self {
140 encrypted_store,
141 secrets: mock_secrets,
142 sync_manager: None,
143 })
144 }
145
146 pub async fn with_sync(
148 config_path: impl Into<PathBuf>,
149 api_url: Option<String>,
150 tenant_id: Option<String>,
151 auto_sync: bool,
152 runtime_mode: RuntimeMode,
153 ) -> Result<Self> {
154 let mut manager = Self::new(config_path).await?;
155 manager.sync_manager = Some(SyncManager::new(
156 api_url,
157 tenant_id,
158 auto_sync,
159 runtime_mode,
160 ));
161
162 let mut config = manager.load().await?;
163 let mut changed = false;
164 if config.runtime.mode != runtime_mode {
165 config.runtime.mode = runtime_mode;
166 changed = true;
167 }
168
169 if matches!(runtime_mode, RuntimeMode::AirGapped) && config.runtime.allow_network {
170 config.runtime.allow_network = false;
171 changed = true;
172 }
173
174 if changed {
175 manager.save(&config).await?;
176 }
177
178 Ok(manager)
179 }
180
181 pub async fn load(&self) -> Result<Config> {
183 match self.encrypted_store.load()? {
184 Some(json) => {
185 let config: Config =
186 serde_json::from_str(&json).context("Failed to parse configuration")?;
187 debug!("Loaded configuration from encrypted store");
188 Ok(config)
189 }
190 None => {
191 debug!("No configuration found, using defaults");
192 Ok(Config::default())
193 }
194 }
195 }
196
197 pub async fn save(&self, config: &Config) -> Result<()> {
199 let json =
200 serde_json::to_string_pretty(config).context("Failed to serialize configuration")?;
201
202 self.encrypted_store.save(&json)?;
203
204 if let Some(ref sync_manager) = self.sync_manager {
206 if sync_manager.is_enabled() {
207 info!("Auto-syncing configuration to cloud");
208 if let Err(e) = sync_manager.sync_to_cloud(config).await {
209 tracing::warn!("Failed to sync configuration to cloud: {}", e);
210 }
211 }
212 }
213
214 Ok(())
215 }
216
217 pub async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
221 self.secrets.set(key, value)?;
222 debug!("Set secret: {}", key);
223 Ok(())
224 }
225
226 pub async fn get_secret(&self, key: &str) -> Result<Option<String>> {
228 self.secrets.get(key)
229 }
230
231 pub async fn delete_secret(&self, key: &str) -> Result<()> {
233 self.secrets.delete(key)?;
234 debug!("Deleted secret: {}", key);
235 Ok(())
236 }
237
238 pub async fn sync_from_cloud(&self) -> Result<Option<Config>> {
240 if let Some(ref sync_manager) = self.sync_manager {
241 sync_manager.sync_from_cloud().await
242 } else {
243 Ok(None)
244 }
245 }
246
247 pub async fn sync_to_cloud(&self, config: &Config) -> Result<Option<sync::SyncResponse>> {
249 if let Some(ref sync_manager) = self.sync_manager {
250 sync_manager.sync_to_cloud(config).await
251 } else {
252 Ok(None)
253 }
254 }
255
256 pub fn sync_status(&self) -> SyncStatus {
258 self.sync_manager
259 .as_ref()
260 .map(|m| m.status())
261 .unwrap_or(SyncStatus::Disabled)
262 }
263
264 pub fn config_path(&self) -> &std::path::Path {
266 self.encrypted_store.config_path()
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use tempfile::TempDir;
274
275 #[tokio::test]
276 async fn test_config_manager() {
277 let temp_dir = TempDir::new().unwrap();
278 let config_path = temp_dir.path().join("test_config.encrypted");
279 let manager = ConfigManager::new_with_mock_secrets(&config_path)
280 .await
281 .unwrap();
282
283 let config = manager.load().await.unwrap();
285 assert_eq!(config.runtime.mode, RuntimeMode::Local);
286
287 let mut config = Config::default();
289 config.runtime.mode = RuntimeMode::AirGapped;
290 manager.save(&config).await.unwrap();
291
292 let loaded = manager.load().await.unwrap();
293 assert_eq!(loaded.runtime.mode, RuntimeMode::AirGapped);
294 }
295
296 #[tokio::test]
297 async fn test_secret_management() {
298 let temp_dir = TempDir::new().unwrap();
299 let config_path = temp_dir.path().join("test_config.encrypted");
300 let manager = ConfigManager::new_with_mock_secrets(&config_path)
301 .await
302 .unwrap();
303
304 manager.set_secret("test.key", "test_value").await.unwrap();
306 let value = manager.get_secret("test.key").await.unwrap();
307 assert_eq!(value, Some("test_value".to_string()));
308
309 manager.delete_secret("test.key").await.unwrap();
311 let value = manager.get_secret("test.key").await.unwrap();
312 assert_eq!(value, None);
313 }
314}