Skip to main content

vtcode_config/core/
permissions.rs

1use serde::{Deserialize, Serialize};
2
3/// Unified permission mode for authored policy evaluation.
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum PermissionMode {
8    /// Standard interactive behavior with prompts when policy requires them.
9    #[default]
10    #[serde(alias = "ask", alias = "suggest")]
11    Default,
12    /// Auto-allow built-in file mutations for the active session.
13    #[serde(alias = "acceptEdits", alias = "accept-edits", alias = "auto-approved")]
14    AcceptEdits,
15    /// Read-only planning mode.
16    Plan,
17    /// Deny any action that is not explicitly allowed.
18    #[serde(alias = "dontAsk", alias = "dont-ask")]
19    DontAsk,
20    /// Skip prompts except protected writes and sandbox escalation prompts.
21    #[serde(
22        alias = "bypassPermissions",
23        alias = "bypass-permissions",
24        alias = "full-auto"
25    )]
26    BypassPermissions,
27}
28
29/// Permission system configuration - Controls command resolution, audit logging, and caching
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct PermissionsConfig {
33    /// Default unified permission mode for the current session.
34    #[serde(default)]
35    pub default_mode: PermissionMode,
36
37    /// Claude-style compatibility allow-list for exact tool ids.
38    #[serde(default)]
39    pub allowed_tools: Vec<String>,
40
41    /// Claude-style compatibility deny-list for exact tool ids.
42    #[serde(default)]
43    pub disallowed_tools: Vec<String>,
44
45    /// Rules that allow matching tool calls without prompting.
46    #[serde(default)]
47    pub allow: Vec<String>,
48
49    /// Rules that require an interactive prompt when they match.
50    #[serde(default)]
51    pub ask: Vec<String>,
52
53    /// Rules that deny matching tool calls.
54    #[serde(default)]
55    pub deny: Vec<String>,
56
57    /// Enable the enhanced permission system (resolver + audit logger + cache)
58    #[serde(default = "default_enabled")]
59    pub enabled: bool,
60
61    /// Enable command resolution to actual paths (helps identify suspicious commands)
62    #[serde(default = "default_resolve_commands")]
63    pub resolve_commands: bool,
64
65    /// Enable audit logging of all permission decisions
66    #[serde(default = "default_audit_enabled")]
67    pub audit_enabled: bool,
68
69    /// Directory for audit logs (created if not exists)
70    /// Defaults to ~/.vtcode/audit
71    #[serde(default = "default_audit_directory")]
72    pub audit_directory: String,
73
74    /// Log allowed commands to audit trail
75    #[serde(default = "default_log_allowed_commands")]
76    pub log_allowed_commands: bool,
77
78    /// Log denied commands to audit trail
79    #[serde(default = "default_log_denied_commands")]
80    pub log_denied_commands: bool,
81
82    /// Log permission prompts (when user is asked for confirmation)
83    #[serde(default = "default_log_permission_prompts")]
84    pub log_permission_prompts: bool,
85
86    /// Enable permission decision caching to avoid redundant evaluations
87    #[serde(default = "default_cache_enabled")]
88    pub cache_enabled: bool,
89
90    /// Cache time-to-live in seconds (how long to cache decisions)
91    /// Default: 300 seconds (5 minutes)
92    #[serde(default = "default_cache_ttl_seconds")]
93    pub cache_ttl_seconds: u64,
94}
95
96#[inline]
97const fn default_enabled() -> bool {
98    true
99}
100
101#[inline]
102const fn default_resolve_commands() -> bool {
103    true
104}
105
106#[inline]
107const fn default_audit_enabled() -> bool {
108    true
109}
110
111const DEFAULT_AUDIT_DIR: &str = "~/.vtcode/audit";
112
113#[inline]
114fn default_audit_directory() -> String {
115    DEFAULT_AUDIT_DIR.into()
116}
117
118#[inline]
119const fn default_log_allowed_commands() -> bool {
120    true
121}
122
123#[inline]
124const fn default_log_denied_commands() -> bool {
125    true
126}
127
128#[inline]
129const fn default_log_permission_prompts() -> bool {
130    true
131}
132
133#[inline]
134const fn default_cache_enabled() -> bool {
135    true
136}
137
138#[inline]
139const fn default_cache_ttl_seconds() -> u64 {
140    300 // 5 minutes
141}
142
143impl Default for PermissionsConfig {
144    fn default() -> Self {
145        Self {
146            default_mode: PermissionMode::default(),
147            allowed_tools: Vec::new(),
148            disallowed_tools: Vec::new(),
149            allow: Vec::new(),
150            ask: Vec::new(),
151            deny: Vec::new(),
152            enabled: default_enabled(),
153            resolve_commands: default_resolve_commands(),
154            audit_enabled: default_audit_enabled(),
155            audit_directory: default_audit_directory(),
156            log_allowed_commands: default_log_allowed_commands(),
157            log_denied_commands: default_log_denied_commands(),
158            log_permission_prompts: default_log_permission_prompts(),
159            cache_enabled: default_cache_enabled(),
160            cache_ttl_seconds: default_cache_ttl_seconds(),
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::{PermissionMode, PermissionsConfig};
168
169    #[test]
170    fn parses_claude_style_mode_aliases() {
171        let config: PermissionsConfig = toml::from_str(
172            r#"
173            default_mode = "acceptEdits"
174            "#,
175        )
176        .expect("permissions config");
177        assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
178
179        let config: PermissionsConfig = toml::from_str(
180            r#"
181            default_mode = "dontAsk"
182            "#,
183        )
184        .expect("permissions config");
185        assert_eq!(config.default_mode, PermissionMode::DontAsk);
186
187        let config: PermissionsConfig = toml::from_str(
188            r#"
189            default_mode = "bypassPermissions"
190            "#,
191        )
192        .expect("permissions config");
193        assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
194    }
195
196    #[test]
197    fn parses_legacy_mode_aliases() {
198        let config: PermissionsConfig = toml::from_str(
199            r#"
200            default_mode = "ask"
201            "#,
202        )
203        .expect("permissions config");
204        assert_eq!(config.default_mode, PermissionMode::Default);
205
206        let config: PermissionsConfig = toml::from_str(
207            r#"
208            default_mode = "auto-approved"
209            "#,
210        )
211        .expect("permissions config");
212        assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
213
214        let config: PermissionsConfig = toml::from_str(
215            r#"
216            default_mode = "full-auto"
217            "#,
218        )
219        .expect("permissions config");
220        assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
221    }
222
223    #[test]
224    fn parses_claude_compat_tool_lists() {
225        let config: PermissionsConfig = toml::from_str(
226            r#"
227            allowed_tools = ["read_file", "unified_search"]
228            disallowed_tools = ["unified_exec"]
229            "#,
230        )
231        .expect("permissions config");
232
233        assert_eq!(
234            config.allowed_tools,
235            vec!["read_file".to_string(), "unified_search".to_string()]
236        );
237        assert_eq!(config.disallowed_tools, vec!["unified_exec".to_string()]);
238    }
239}