Skip to main content

ai_agent/utils/settings/
mod.rs

1// Source: ~/claudecode/openclaudecode/src/utils/settings/settings.ts
2//! Settings file I/O for reading/writing Claude Code configuration.
3//!
4//! Translated from TypeScript settings.ts
5//! Provides get_settings_for_source() and update_settings_for_source()
6//! for reading and persisting settings to JSON files.
7
8use std::path::{Path, PathBuf};
9
10use serde_json::{Map, Value};
11
12pub mod permission_validation;
13pub mod settings_cache;
14pub mod tool_validation_config;
15pub mod validation;
16
17// Re-export from MCP types for use in settings validation
18pub use crate::services::mcp::ConfigScope;
19
20#[cfg(test)]
21#[path = "tests/settings_tests.rs"]
22mod settings_tests;
23
24/// Editable setting sources that can be read from and written to disk.
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum EditableSettingSource {
27    /// ~/.ai/settings.json
28    UserSettings,
29    /// <cwd>/.ai/settings.json
30    ProjectSettings,
31    /// <cwd>/.ai/settings.local.json
32    LocalSettings,
33}
34
35/// All setting sources including non-editable ones
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum SettingSource {
38    UserSettings,
39    ProjectSettings,
40    LocalSettings,
41    PolicySettings,
42    FlagSettings,
43}
44
45/// Get the file path for a given settings source.
46/// Returns None for sources without a file path.
47pub fn get_settings_file_path_for_source(source: &EditableSettingSource) -> Option<PathBuf> {
48    match source {
49        EditableSettingSource::UserSettings => {
50            dirs::home_dir().map(|home| home.join(".ai").join("settings.json"))
51        }
52        EditableSettingSource::ProjectSettings => {
53            std::env::current_dir().ok().map(|cwd| cwd.join(".ai").join("settings.json"))
54        }
55        EditableSettingSource::LocalSettings => {
56            std::env::current_dir().ok().map(|cwd| cwd.join(".ai").join("settings.local.json"))
57        }
58    }
59}
60
61/// Read settings JSON from a file. Returns None if file doesn't exist or can't be read.
62pub fn read_settings_file(path: &Path) -> Option<Value> {
63    let content = std::fs::read_to_string(path).ok()?;
64    if content.trim().is_empty() {
65        return Some(Value::Object(serde_json::Map::new()));
66    }
67    serde_json::from_str(&content).ok()
68}
69
70/// Deep merge two JSON values. `overlay` is merged into `base`.
71/// Arrays in `overlay` replace arrays in `base` entirely.
72/// `null` values in `overlay` delete keys from `base`.
73fn deep_merge(base: &Value, overlay: &Value) -> Value {
74    match (base, overlay) {
75        (Value::Object(base_map), Value::Object(overlay_map)) => {
76            let mut result = base_map.clone();
77            for (key, overlay_val) in overlay_map {
78                if overlay_val.is_null() {
79                    result.remove(key);
80                } else {
81                    let base_val = result.get(key);
82                    result.insert(
83                        key.clone(),
84                        match base_val {
85                            Some(b) => deep_merge(b, overlay_val),
86                            None => overlay_val.clone(),
87                        },
88                    );
89                }
90            }
91            Value::Object(result)
92        }
93        (_, Value::Array(overlay_arr)) => overlay.clone(),
94        (_, other) => other.clone(),
95    }
96}
97
98/// Get settings for a source by reading from disk.
99/// Returns None if the settings file doesn't exist or is invalid.
100pub fn get_settings_for_source(source: &EditableSettingSource) -> Option<Value> {
101    let path = get_settings_file_path_for_source(source)?;
102    read_settings_file(&path)
103}
104
105/// Merge `settings` into the existing settings file for `source`.
106/// Creates the directory and file if they don't exist.
107/// Returns Err on I/O or JSON parse errors.
108pub fn update_settings_for_source(
109    source: &EditableSettingSource,
110    settings: &Value,
111) -> Result<(), String> {
112    let file_path =
113        get_settings_file_path_for_source(source).ok_or("Cannot determine settings path")?;
114
115    // Create directory if needed
116    if let Some(parent) = file_path.parent() {
117        std::fs::create_dir_all(parent)
118            .map_err(|e| format!("Failed to create settings directory: {}", e))?;
119    }
120
121    // Read existing settings
122    let existing = read_settings_file(&file_path).unwrap_or(Value::Object(serde_json::Map::new()));
123
124    // Deep merge new settings into existing
125    let merged = deep_merge(&existing, settings);
126
127    // Write back
128    let json_str = serde_json::to_string_pretty(&merged)
129        .map_err(|e| format!("Failed to serialize settings: {}", e))?;
130
131    std::fs::write(&file_path, json_str + "\n")
132        .map_err(|e| format!("Failed to write settings file: {}", e))?;
133
134    Ok(())
135}
136
137/// Add permission rules to settings file.
138/// This is the core persistence function called by persist_permission_update.
139pub fn add_permission_rules_to_settings(
140    rules: &[String],
141    behavior: &str, // "allow", "deny", or "ask"
142    source: &EditableSettingSource,
143) -> Result<(), String> {
144    let existing = get_settings_for_source(source).unwrap_or(Value::Object(serde_json::Map::new()));
145
146    // Get current permission rules for this behavior
147    let current_rules: Vec<String> = existing
148        .get("permissions")
149        .and_then(|p| p.get(behavior))
150        .and_then(|r| r.as_array())
151        .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
152        .unwrap_or_default();
153
154    // Add new rules, avoiding duplicates
155    let mut all_rules = current_rules;
156    for rule in rules {
157        if !all_rules.contains(rule) {
158            all_rules.push(rule.clone());
159        }
160    }
161
162    // Build settings to merge
163    let mut perms = serde_json::Map::new();
164    perms.insert(
165        behavior.to_string(),
166        Value::Array(all_rules.into_iter().map(Value::String).collect()),
167    );
168    let settings = Value::Object(
169        [("permissions".to_string(), Value::Object(perms))]
170            .into_iter()
171            .collect::<Map<_, _>>(),
172    );
173
174    update_settings_for_source(source, &settings)
175}
176
177/// Remove permission rules from settings file.
178pub fn remove_permission_rules_from_settings(
179    rules: &[String],
180    behavior: &str,
181    source: &EditableSettingSource,
182) -> Result<(), String> {
183    let current_rules: Vec<String> = match get_settings_for_source(source) {
184        Some(s) => s.get("permissions")
185            .and_then(|p| p.get(behavior))
186            .and_then(|r| r.as_array())
187            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
188            .unwrap_or_default(),
189        None => Vec::new(),
190    };
191
192    let rules_to_remove: std::collections::HashSet<&str> = rules.iter().map(|s| s.as_str()).collect();
193    let filtered: Vec<String> =
194        current_rules.into_iter().filter(|r| !rules_to_remove.contains(r.as_str())).collect();
195
196    let mut perms = serde_json::Map::new();
197    perms.insert(
198        behavior.to_string(),
199        Value::Array(filtered.into_iter().map(Value::String).collect()),
200    );
201    let settings = Value::Object(
202        [("permissions".to_string(), Value::Object(perms))]
203            .into_iter()
204            .collect::<Map<_, _>>(),
205    );
206
207    update_settings_for_source(source, &settings)
208}
209
210/// Replace all permission rules for a behavior in settings file.
211pub fn replace_permission_rules_in_settings(
212    rules: &[String],
213    behavior: &str,
214    source: &EditableSettingSource,
215) -> Result<(), String> {
216    let mut perms = serde_json::Map::new();
217    perms.insert(
218        behavior.to_string(),
219        Value::Array(rules.iter().map(|r| Value::String(r.clone())).collect()),
220    );
221    let settings = Value::Object(
222        [("permissions".to_string(), Value::Object(perms))]
223            .into_iter()
224            .collect::<Map<_, _>>(),
225    );
226
227    update_settings_for_source(source, &settings)
228}
229
230/// Manage additional directories in settings
231pub fn add_directories_to_settings(
232    directories: &[String],
233    source: &EditableSettingSource,
234) -> Result<(), String> {
235    let current_dirs: Vec<String> = match get_settings_for_source(source) {
236        Some(s) => s.get("permissions")
237            .and_then(|p| p.get("additionalDirectories"))
238            .and_then(|r| r.as_array())
239            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
240            .unwrap_or_default(),
241        None => Vec::new(),
242    };
243
244    let existing: std::collections::HashSet<String> = current_dirs.iter().cloned().collect();
245    let mut all_dirs = current_dirs;
246    for dir in directories {
247        if !existing.contains(dir) {
248            all_dirs.push(dir.clone());
249        }
250    }
251
252    let mut perms = serde_json::Map::new();
253    perms.insert(
254        "additionalDirectories".to_string(),
255        Value::Array(all_dirs.into_iter().map(Value::String).collect()),
256    );
257    let settings = Value::Object(
258        [("permissions".to_string(), Value::Object(perms))]
259            .into_iter()
260            .collect::<Map<_, _>>(),
261    );
262
263    update_settings_for_source(source, &settings)
264}
265
266/// Remove directories from settings
267pub fn remove_directories_from_settings(
268    directories: &[String],
269    source: &EditableSettingSource,
270) -> Result<(), String> {
271    let current_dirs: Vec<String> = match get_settings_for_source(source) {
272        Some(s) => s.get("permissions")
273            .and_then(|p| p.get("additionalDirectories"))
274            .and_then(|r| r.as_array())
275            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
276            .unwrap_or_default(),
277        None => Vec::new(),
278    };
279
280    let dirs_to_remove: std::collections::HashSet<&str> =
281        directories.iter().map(|s| s.as_str()).collect();
282    let filtered: Vec<String> =
283        current_dirs.into_iter().filter(|d| !dirs_to_remove.contains(d.as_str())).collect();
284
285    let mut perms = serde_json::Map::new();
286    perms.insert(
287        "additionalDirectories".to_string(),
288        Value::Array(filtered.into_iter().map(Value::String).collect()),
289    );
290    let settings = Value::Object(
291        [("permissions".to_string(), Value::Object(perms))]
292            .into_iter()
293            .collect::<Map<_, _>>(),
294    );
295
296    update_settings_for_source(source, &settings)
297}
298
299/// Set permission mode in settings
300pub fn set_permission_mode_in_settings(
301    mode: &str,
302    source: &EditableSettingSource,
303) -> Result<(), String> {
304    let mut perms = serde_json::Map::new();
305    perms.insert("defaultMode".to_string(), Value::String(mode.to_string()));
306    let settings = Value::Object(
307        [("permissions".to_string(), Value::Object(perms))]
308            .into_iter()
309            .collect::<Map<_, _>>(),
310    );
311
312    update_settings_for_source(source, &settings)
313}