Skip to main content

vtcode_config/core/
dotfile_protection.rs

1//! Dotfile protection configuration.
2//!
3//! Provides comprehensive protection for hidden configuration files (dotfiles)
4//! to prevent automatic or implicit modifications by AI agents or automated tools.
5
6use indexmap::IndexSet;
7use serde::{Deserialize, Serialize};
8
9/// Dotfile protection configuration.
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct DotfileProtectionConfig {
13    /// Enable dotfile protection globally.
14    #[serde(default = "default_true")]
15    pub enabled: bool,
16
17    /// Require explicit user confirmation for any dotfile modification.
18    #[serde(default = "default_true")]
19    pub require_explicit_confirmation: bool,
20
21    /// Enable immutable audit logging of all dotfile access attempts.
22    #[serde(default = "default_true")]
23    pub audit_logging_enabled: bool,
24
25    /// Path to the audit log file.
26    #[serde(default = "default_audit_log_path")]
27    pub audit_log_path: String,
28
29    /// Prevent cascading modifications (one dotfile change triggering others).
30    #[serde(default = "default_true")]
31    pub prevent_cascading_modifications: bool,
32
33    /// Create backup before any permitted modification.
34    #[serde(default = "default_true")]
35    pub create_backups: bool,
36
37    /// Directory for storing dotfile backups.
38    #[serde(default = "default_backup_dir")]
39    pub backup_directory: String,
40
41    /// Maximum number of backups to retain per file.
42    #[serde(default = "default_max_backups")]
43    pub max_backups_per_file: usize,
44
45    /// Preserve original file permissions and ownership.
46    #[serde(default = "default_true")]
47    pub preserve_permissions: bool,
48
49    /// Whitelisted dotfiles that can be modified (after secondary confirmation).
50    #[serde(default)]
51    pub whitelist: IndexSet<String>,
52
53    /// Additional dotfile patterns to protect (beyond defaults).
54    #[serde(default)]
55    pub additional_protected_patterns: Vec<String>,
56
57    /// Block modifications during automated operations.
58    #[serde(default = "default_true")]
59    pub block_during_automation: bool,
60
61    /// Operations that trigger extra protection.
62    #[serde(default = "default_blocked_operations")]
63    pub blocked_operations: Vec<String>,
64
65    /// Secondary authentication required for whitelisted files.
66    #[serde(default = "default_true")]
67    pub require_secondary_auth_for_whitelist: bool,
68}
69
70impl Default for DotfileProtectionConfig {
71    fn default() -> Self {
72        Self {
73            enabled: default_true(),
74            require_explicit_confirmation: default_true(),
75            audit_logging_enabled: default_true(),
76            audit_log_path: default_audit_log_path(),
77            prevent_cascading_modifications: default_true(),
78            create_backups: default_true(),
79            backup_directory: default_backup_dir(),
80            max_backups_per_file: default_max_backups(),
81            preserve_permissions: default_true(),
82            whitelist: IndexSet::new(),
83            additional_protected_patterns: Vec::new(),
84            block_during_automation: default_true(),
85            blocked_operations: default_blocked_operations(),
86            require_secondary_auth_for_whitelist: default_true(),
87        }
88    }
89}
90
91/// Default protected dotfile patterns.
92///
93/// These patterns match common configuration files that should never be
94/// modified automatically by AI agents or automated tools.
95pub const DEFAULT_PROTECTED_DOTFILES: &[&str] = &[
96    // Git configuration
97    ".gitignore",
98    ".gitattributes",
99    ".gitmodules",
100    ".gitconfig",
101    ".git-credentials",
102    // Editor configuration
103    ".editorconfig",
104    ".vscode/*",
105    ".idea/*",
106    ".cursor/*",
107    // Environment files
108    ".env",
109    ".env.local",
110    ".env.development",
111    ".env.production",
112    ".env.test",
113    ".env.*",
114    // Docker
115    ".dockerignore",
116    ".docker/*",
117    // Node.js/JavaScript
118    ".npmignore",
119    ".npmrc",
120    ".nvmrc",
121    ".yarnrc",
122    ".yarnrc.yml",
123    ".pnpmrc",
124    // Code formatting
125    ".prettierrc",
126    ".prettierrc.json",
127    ".prettierrc.yml",
128    ".prettierrc.yaml",
129    ".prettierrc.js",
130    ".prettierrc.cjs",
131    ".prettierignore",
132    // Linting
133    ".eslintrc",
134    ".eslintrc.json",
135    ".eslintrc.yml",
136    ".eslintrc.yaml",
137    ".eslintrc.js",
138    ".eslintrc.cjs",
139    ".eslintignore",
140    ".stylelintrc",
141    ".stylelintrc.json",
142    // Build tools
143    ".babelrc",
144    ".babelrc.json",
145    ".babelrc.js",
146    ".swcrc",
147    ".tsbuildinfo",
148    // Shell configuration
149    ".zshrc",
150    ".bashrc",
151    ".bash_profile",
152    ".bash_history",
153    ".bash_logout",
154    ".profile",
155    ".zprofile",
156    ".zshenv",
157    ".zsh_history",
158    ".shrc",
159    ".kshrc",
160    ".cshrc",
161    ".tcshrc",
162    ".fishrc",
163    ".config/fish/*",
164    // Editor configurations
165    ".vimrc",
166    ".vim/*",
167    ".nvim/*",
168    ".config/nvim/*",
169    ".emacs",
170    ".emacs.d/*",
171    ".nanorc",
172    // Terminal multiplexers
173    ".tmux.conf",
174    ".screenrc",
175    // SSH and security
176    ".ssh/*",
177    ".ssh/config",
178    ".ssh/known_hosts",
179    ".ssh/authorized_keys",
180    ".gnupg/*",
181    ".gpg/*",
182    // Cloud credentials
183    ".aws/*",
184    ".aws/config",
185    ".aws/credentials",
186    ".azure/*",
187    ".config/gcloud/*",
188    ".kube/*",
189    ".kube/config",
190    // Package managers and tools
191    ".cargo/*",
192    ".cargo/config.toml",
193    ".cargo/credentials.toml",
194    ".rustup/*",
195    ".gem/*",
196    ".bundle/*",
197    ".pip/*",
198    ".pypirc",
199    ".poetry/*",
200    ".pdm.toml",
201    ".python-version",
202    ".ruby-version",
203    ".node-version",
204    ".go-version",
205    ".tool-versions",
206    // Database
207    ".pgpass",
208    ".my.cnf",
209    ".mongorc.js",
210    ".rediscli_history",
211    // Misc configuration
212    ".netrc",
213    ".curlrc",
214    ".wgetrc",
215    ".htaccess",
216    ".htpasswd",
217    // VT Code specific
218    ".vtcode/*",
219    ".vtcodegitignore",
220    ".vtcode.toml",
221    // Claude/AI
222    ".claude/*",
223    ".claude.json",
224    ".agent/*",
225    // Other common dotfiles
226    ".inputrc",
227    ".dircolors",
228    ".mailrc",
229    ".gitkeep",
230    ".keep",
231];
232
233/// Operations that should never automatically modify dotfiles.
234fn default_blocked_operations() -> Vec<String> {
235    vec![
236        "dependency_installation".into(),
237        "code_formatting".into(),
238        "git_operations".into(),
239        "project_initialization".into(),
240        "build_operations".into(),
241        "test_execution".into(),
242        "linting".into(),
243        "auto_fix".into(),
244    ]
245}
246
247#[inline]
248const fn default_true() -> bool {
249    true
250}
251
252#[inline]
253fn default_audit_log_path() -> String {
254    "~/.vtcode/dotfile_audit.log".into()
255}
256
257#[inline]
258fn default_backup_dir() -> String {
259    "~/.vtcode/dotfile_backups".into()
260}
261
262#[inline]
263const fn default_max_backups() -> usize {
264    10
265}
266
267impl DotfileProtectionConfig {
268    /// Check if a file path matches a protected dotfile pattern.
269    pub fn is_protected(&self, path: &str) -> bool {
270        if !self.enabled {
271            return false;
272        }
273
274        let filename = std::path::Path::new(path)
275            .file_name()
276            .and_then(|n| n.to_str())
277            .unwrap_or(path);
278
279        // Check if it's a dotfile (starts with . or contains /. or is in a dotfile directory)
280        let is_dotfile = filename.starts_with('.')
281            || path.contains("/.")
282            || path.starts_with('.')
283            || Self::is_in_dotfile_directory(path);
284
285        if !is_dotfile {
286            return false;
287        }
288
289        // Check against default patterns
290        for pattern in DEFAULT_PROTECTED_DOTFILES {
291            if Self::matches_pattern(path, pattern) || Self::matches_pattern(filename, pattern) {
292                return true;
293            }
294        }
295
296        // Check against additional patterns
297        for pattern in &self.additional_protected_patterns {
298            if Self::matches_pattern(path, pattern) || Self::matches_pattern(filename, pattern) {
299                return true;
300            }
301        }
302
303        // Default: protect any file starting with .
304        filename.starts_with('.') || Self::is_in_dotfile_directory(path)
305    }
306
307    /// Check if a path is inside a dotfile directory like .ssh, .aws, etc.
308    fn is_in_dotfile_directory(path: &str) -> bool {
309        let components: Vec<&str> = path.split('/').collect();
310        for component in &components {
311            if component.starts_with('.')
312                && !component.is_empty()
313                && *component != "."
314                && *component != ".."
315            {
316                return true;
317            }
318        }
319        false
320    }
321
322    /// Check if a file is in the whitelist.
323    pub fn is_whitelisted(&self, path: &str) -> bool {
324        let filename = std::path::Path::new(path)
325            .file_name()
326            .and_then(|n| n.to_str())
327            .unwrap_or(path);
328
329        self.whitelist.contains(path) || self.whitelist.contains(filename)
330    }
331
332    /// Simple pattern matching with wildcard support.
333    fn matches_pattern(path: &str, pattern: &str) -> bool {
334        if pattern.contains('*') {
335            // Handle wildcard patterns
336            if pattern.ends_with("/*") {
337                let prefix = &pattern[..pattern.len() - 2];
338                path.starts_with(prefix)
339                    || path.contains(&format!("/{}/", prefix.trim_start_matches('.')))
340            } else if pattern.ends_with(".*") {
341                let prefix = &pattern[..pattern.len() - 1];
342                path.starts_with(prefix)
343            } else {
344                // Simple glob matching
345                let parts: Vec<&str> = pattern.split('*').collect();
346                if parts.len() == 2 {
347                    path.starts_with(parts[0]) && path.ends_with(parts[1])
348                } else {
349                    path == pattern
350                }
351            }
352        } else {
353            path == pattern || path.ends_with(&format!("/{}", pattern))
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_default_protection() {
364        let config = DotfileProtectionConfig::default();
365
366        // Should be protected
367        assert!(config.is_protected(".gitignore"));
368        assert!(config.is_protected(".env"));
369        assert!(config.is_protected(".env.local"));
370        assert!(config.is_protected(".bashrc"));
371        assert!(config.is_protected(".ssh/config"));
372        assert!(config.is_protected("/home/user/.npmrc"));
373
374        // Should not be protected (not dotfiles)
375        assert!(!config.is_protected("README.md"));
376        assert!(!config.is_protected("src/main.rs"));
377    }
378
379    #[test]
380    fn test_whitelist() {
381        let mut config = DotfileProtectionConfig::default();
382        config.whitelist.insert(".gitignore".into());
383
384        assert!(config.is_whitelisted(".gitignore"));
385        assert!(!config.is_whitelisted(".env"));
386    }
387
388    #[test]
389    fn test_disabled_protection() {
390        let mut config = DotfileProtectionConfig::default();
391        config.enabled = false;
392
393        assert!(!config.is_protected(".gitignore"));
394        assert!(!config.is_protected(".env"));
395    }
396
397    #[test]
398    fn test_pattern_matching() {
399        assert!(DotfileProtectionConfig::matches_pattern(
400            ".env.local",
401            ".env.*"
402        ));
403        assert!(DotfileProtectionConfig::matches_pattern(
404            ".env.production",
405            ".env.*"
406        ));
407        assert!(DotfileProtectionConfig::matches_pattern(
408            ".vscode/settings.json",
409            ".vscode/*"
410        ));
411    }
412}