doum_cli/system/
config.rs1use crate::system::paths::get_config_path;
2use anyhow::{Context, Result};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6
7#[cfg(unix)]
8use std::os::unix::fs::PermissionsExt;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Config {
13 pub llm: LLMConfig,
14 pub context: ContextConfig,
15 pub logging: LoggingConfig,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LLMConfig {
21 pub provider: String,
22 pub model: String,
23 pub timeout: u64,
24 pub max_retries: u32,
25 pub use_thinking: bool,
26 pub use_web_search: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ContextConfig {
32 pub max_lines: usize,
33 pub max_size_kb: usize,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LoggingConfig {
39 pub enabled: bool,
40 pub level: String,
41}
42
43fn 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 #[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
66pub fn load_config() -> Result<Config> {
68 let config_path = ensure_config()?;
69
70 if config_path.exists() {
71 validate_config(&config_path)?;
73
74 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
76
77 let config: Config = toml::from_str(&content).context("Failed to parse config file")?;
79
80 Ok(config)
81 } else {
82 let config = load_default_config()?;
84 save_config(&config)?;
85 Ok(config)
86 }
87}
88
89pub fn load_default_config() -> Result<Config> {
91 Ok(Config {
92 llm: LLMConfig {
93 provider: "openai".to_string(),
94 model: "gpt-5".to_string(),
95 timeout: 30,
96 max_retries: 3,
97 use_thinking: false,
98 use_web_search: true,
99 },
100 context: ContextConfig {
101 max_lines: 100,
102 max_size_kb: 50,
103 },
104 logging: LoggingConfig {
105 enabled: false,
106 level: "info".to_string(),
107 },
108 })
109}
110
111pub fn save_config(config: &Config) -> Result<()> {
113 let config_path = ensure_config()?;
114
115 let content = toml::to_string_pretty(config).context("Failed to serialize config")?;
117
118 fs::write(&config_path, content).context("Failed to write config file")?;
120
121 #[cfg(windows)]
123 {
124 }
127
128 #[cfg(unix)]
130 {
131 let metadata = fs::metadata(&config_path).context("File metadata read failed")?;
132 let mut permissions = metadata.permissions();
133 permissions.set_mode(0o600);
134 fs::set_permissions(&config_path, permissions).context("Failed to set file permissions")?;
135 }
136
137 Ok(())
138}
139
140fn validate_config(path: &PathBuf) -> Result<()> {
142 #[cfg(windows)]
143 {
144 let _ = path; }
148
149 #[cfg(unix)]
150 {
151 let metadata = fs::metadata(path).context("Failed to read file metadata")?;
152 let permissions = metadata.permissions();
153 let mode = permissions.mode() & 0o777;
154
155 if mode != 0o600 && mode != 0o400 {
157 anyhow::bail!(
158 "Insecure config file permissions: {:o} on {}. Expected 600 or 400.",
159 mode,
160 path.display()
161 );
162 }
163 }
164
165 Ok(())
166}