doum_cli/system/
config.rs

1use crate::llm::Provider;
2use crate::system::paths::get_config_path;
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8#[cfg(unix)]
9use std::os::unix::fs::PermissionsExt;
10
11/// Entire application configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Config {
14    pub llm: LLMConfig,
15    pub context: ContextConfig,
16    pub logging: LoggingConfig,
17}
18
19/// Configuration for LLM API
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct LLMConfig {
22    pub provider: Provider,
23    pub model: String,
24    pub timeout: u64,
25    pub use_thinking: bool,
26    pub use_web_search: bool,
27}
28
29/// Context management settings
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ContextConfig {
32    pub max_lines: usize,
33    pub max_size_kb: usize,
34}
35
36/// Configuration for logging
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LoggingConfig {
39    pub enabled: bool,
40    pub level: String,
41}
42
43/// Ensure configuration directory and return config file path
44fn ensure_config() -> Result<PathBuf> {
45    let config_path = get_config_path()?;
46
47    if let Some(parent) = config_path.parent()
48        && !parent.exists()
49    {
50        fs::create_dir_all(parent).context("Failed to create config directory")?;
51
52        // Set directory permissions to 700 on Unix
53        #[cfg(unix)]
54        {
55            let metadata = fs::metadata(parent).context("Failed to read directory metadata")?;
56            let mut permissions = metadata.permissions();
57            permissions.set_mode(0o700);
58            fs::set_permissions(parent, permissions)
59                .context("Failed to set directory permissions")?;
60        }
61    }
62
63    Ok(config_path)
64}
65
66/// load configuration from file or create default
67pub fn load_config() -> Result<Config> {
68    let config_path = ensure_config()?;
69
70    if config_path.exists() {
71        // Read and parse existing config file
72        let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
73        let config: Config = toml::from_str(&content).context("Failed to parse config file")?;
74        Ok(config)
75    } else {
76        // If config file doesn't exist, create default
77        let config = load_default_config()?;
78        save_config(&config)?;
79        Ok(config)
80    }
81}
82
83/// Load default configuration
84pub fn load_default_config() -> Result<Config> {
85    Ok(Config {
86        llm: LLMConfig {
87            provider: Provider::OpenAI,
88            model: "gpt-5".to_string(),
89            timeout: 30,
90            use_thinking: false,
91            use_web_search: true,
92        },
93        context: ContextConfig {
94            max_lines: 100,
95            max_size_kb: 50,
96        },
97        logging: LoggingConfig {
98            enabled: true,
99            level: "info".to_string(),
100        },
101    })
102}
103
104/// Save configuration to file with secure permissions
105pub fn save_config(config: &Config) -> Result<()> {
106    // Get config path and write file
107    let config_path = ensure_config()?;
108    let content = toml::to_string_pretty(config).context("Failed to serialize config")?;
109    fs::write(&config_path, content).context("Failed to write config file")?;
110
111    // if Windows, set ACLs for the user only
112    #[cfg(windows)]
113    {
114        // In Windows, basic file permissions are usually sufficient
115        // Additional ACL settings can be implemented if needed
116    }
117
118    // if Unix, set file permissions to 600
119    #[cfg(unix)]
120    {
121        let metadata = fs::metadata(&config_path).context("File metadata read failed")?;
122        let mut permissions = metadata.permissions();
123        permissions.set_mode(0o600);
124        fs::set_permissions(&config_path, permissions).context("Failed to set file permissions")?;
125    }
126
127    Ok(())
128}