Skip to main content

vtcode_config/core/
permissions.rs

1use crate::env_helpers::default_enabled;
2use serde::{Deserialize, Serialize};
3
4/// Unified permission mode for authored policy evaluation.
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
7#[serde(rename_all = "snake_case")]
8pub enum PermissionMode {
9    /// Standard interactive behavior with prompts when policy requires them.
10    #[default]
11    #[serde(alias = "ask", alias = "suggest")]
12    Default,
13    /// Auto-allow built-in file mutations for the active session.
14    #[serde(alias = "acceptEdits", alias = "accept-edits", alias = "auto-approved")]
15    AcceptEdits,
16    /// Classifier-backed autonomous mode.
17    #[serde(alias = "trusted_auto", alias = "trusted-auto")]
18    Auto,
19    /// Read-only planning mode.
20    Plan,
21    /// Deny any action that is not explicitly allowed.
22    #[serde(alias = "dontAsk", alias = "dont-ask")]
23    DontAsk,
24    /// Skip prompts except protected writes and sandbox escalation prompts.
25    #[serde(
26        alias = "bypassPermissions",
27        alias = "bypass-permissions",
28        alias = "full-auto"
29    )]
30    BypassPermissions,
31}
32
33/// Permission system configuration - Controls command resolution, audit logging, and caching
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct PermissionsConfig {
37    /// Default unified permission mode for the current session.
38    #[serde(default)]
39    pub default_mode: PermissionMode,
40
41    /// Classifier-backed auto mode policy and environment settings.
42    #[serde(default)]
43    pub auto_mode: AutoModeConfig,
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/// Classifier-backed auto mode configuration.
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct AutoModeConfig {
100    /// Optional model override for the transcript reviewer.
101    #[serde(default)]
102    pub model: String,
103
104    /// Optional model override for the prompt-injection probe.
105    #[serde(default)]
106    pub probe_model: String,
107
108    /// Maximum consecutive denials before auto mode falls back.
109    #[serde(default = "default_auto_mode_max_consecutive_denials")]
110    pub max_consecutive_denials: u32,
111
112    /// Maximum total denials before auto mode falls back.
113    #[serde(default = "default_auto_mode_max_total_denials")]
114    pub max_total_denials: u32,
115
116    /// Drop broad code-execution allow rules while auto mode is active.
117    #[serde(default = "default_auto_mode_drop_broad_allow_rules")]
118    pub drop_broad_allow_rules: bool,
119
120    /// Classifier block rules applied in stage 2 reasoning.
121    #[serde(default = "default_auto_mode_block_rules")]
122    pub block_rules: Vec<String>,
123
124    /// Narrow allow exceptions applied after block rules.
125    #[serde(default = "default_auto_mode_allow_exceptions")]
126    pub allow_exceptions: Vec<String>,
127
128    /// Trusted environment boundaries for the classifier.
129    #[serde(default)]
130    pub environment: AutoModeEnvironmentConfig,
131}
132
133/// Trust-boundary configuration for auto mode.
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135#[derive(Debug, Clone, Default, Deserialize, Serialize)]
136pub struct AutoModeEnvironmentConfig {
137    #[serde(default)]
138    pub trusted_paths: Vec<String>,
139
140    #[serde(default)]
141    pub trusted_domains: Vec<String>,
142
143    #[serde(default)]
144    pub trusted_git_hosts: Vec<String>,
145
146    #[serde(default)]
147    pub trusted_git_orgs: Vec<String>,
148
149    #[serde(default)]
150    pub trusted_services: Vec<String>,
151}
152
153impl Default for AutoModeConfig {
154    fn default() -> Self {
155        Self {
156            model: String::new(),
157            probe_model: String::new(),
158            max_consecutive_denials: default_auto_mode_max_consecutive_denials(),
159            max_total_denials: default_auto_mode_max_total_denials(),
160            drop_broad_allow_rules: default_auto_mode_drop_broad_allow_rules(),
161            block_rules: default_auto_mode_block_rules(),
162            allow_exceptions: default_auto_mode_allow_exceptions(),
163            environment: AutoModeEnvironmentConfig::default(),
164        }
165    }
166}
167
168#[inline]
169const fn default_resolve_commands() -> bool {
170    default_enabled()
171}
172
173#[inline]
174const fn default_audit_enabled() -> bool {
175    default_enabled()
176}
177
178const DEFAULT_AUDIT_DIR: &str = "~/.vtcode/audit";
179
180#[inline]
181fn default_audit_directory() -> String {
182    DEFAULT_AUDIT_DIR.into()
183}
184
185#[inline]
186const fn default_log_allowed_commands() -> bool {
187    true
188}
189
190#[inline]
191const fn default_log_denied_commands() -> bool {
192    true
193}
194
195#[inline]
196const fn default_log_permission_prompts() -> bool {
197    true
198}
199
200#[inline]
201const fn default_cache_enabled() -> bool {
202    true
203}
204
205#[inline]
206const fn default_cache_ttl_seconds() -> u64 {
207    300 // 5 minutes
208}
209
210#[inline]
211const fn default_auto_mode_max_consecutive_denials() -> u32 {
212    3
213}
214
215#[inline]
216const fn default_auto_mode_max_total_denials() -> u32 {
217    20
218}
219
220#[inline]
221const fn default_auto_mode_drop_broad_allow_rules() -> bool {
222    true
223}
224
225fn default_auto_mode_block_rules() -> Vec<String> {
226    vec![
227        "Block destructive source-control actions such as force-pushes, direct pushes to protected branches, or remote branch deletion unless the user explicitly authorized that exact blast radius.".to_string(),
228        "Block remote code download-and-execute flows, including curl-or-wget pipes into interpreters or shells, and running code from freshly cloned external repositories.".to_string(),
229        "Block sending workspace contents, credentials, environment variables, or other sensitive data to external endpoints unless the user explicitly named both the destination and the data.".to_string(),
230        "Block credential hunting, secret discovery, or scanning config and environment stores for alternative tokens unless the user explicitly asked for that credential workflow.".to_string(),
231        "Block commands that bypass safety checks, disable verification, install persistence, or weaken the host security posture.".to_string(),
232        "Block destructive or production/shared-infrastructure actions unless the user explicitly authorized the target and the action.".to_string(),
233        "Block destructive actions against inferred, fuzzy-matched, or agent-selected targets when the user did not name the exact target.".to_string(),
234    ]
235}
236
237fn default_auto_mode_allow_exceptions() -> Vec<String> {
238    vec![
239        "Allow read-only tools and read-only browsing/search actions.".to_string(),
240        "Allow file edits and writes inside the current workspace when the path is not protected.".to_string(),
241        "Allow pushes only to the current session branch or configured git remotes inside the trusted environment.".to_string(),
242    ]
243}
244
245impl Default for PermissionsConfig {
246    fn default() -> Self {
247        Self {
248            default_mode: PermissionMode::default(),
249            auto_mode: AutoModeConfig::default(),
250            allow: Vec::new(),
251            ask: Vec::new(),
252            deny: Vec::new(),
253            enabled: default_enabled(),
254            resolve_commands: default_resolve_commands(),
255            audit_enabled: default_audit_enabled(),
256            audit_directory: default_audit_directory(),
257            log_allowed_commands: default_log_allowed_commands(),
258            log_denied_commands: default_log_denied_commands(),
259            log_permission_prompts: default_log_permission_prompts(),
260            cache_enabled: default_cache_enabled(),
261            cache_ttl_seconds: default_cache_ttl_seconds(),
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::{PermissionMode, PermissionsConfig};
269
270    #[test]
271    fn parses_claude_style_mode_aliases() {
272        let config: PermissionsConfig = toml::from_str(
273            r#"
274            default_mode = "acceptEdits"
275            "#,
276        )
277        .expect("permissions config");
278        assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
279
280        let config: PermissionsConfig = toml::from_str(
281            r#"
282            default_mode = "dontAsk"
283            "#,
284        )
285        .expect("permissions config");
286        assert_eq!(config.default_mode, PermissionMode::DontAsk);
287
288        let config: PermissionsConfig = toml::from_str(
289            r#"
290            default_mode = "bypassPermissions"
291            "#,
292        )
293        .expect("permissions config");
294        assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
295
296        let config: PermissionsConfig = toml::from_str(
297            r#"
298            default_mode = "auto"
299            "#,
300        )
301        .expect("permissions config");
302        assert_eq!(config.default_mode, PermissionMode::Auto);
303    }
304
305    #[test]
306    fn parses_legacy_mode_aliases() {
307        let config: PermissionsConfig = toml::from_str(
308            r#"
309            default_mode = "ask"
310            "#,
311        )
312        .expect("permissions config");
313        assert_eq!(config.default_mode, PermissionMode::Default);
314
315        let config: PermissionsConfig = toml::from_str(
316            r#"
317            default_mode = "auto-approved"
318            "#,
319        )
320        .expect("permissions config");
321        assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
322
323        let config: PermissionsConfig = toml::from_str(
324            r#"
325            default_mode = "full-auto"
326            "#,
327        )
328        .expect("permissions config");
329        assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
330
331        let config: PermissionsConfig = toml::from_str(
332            r#"
333            default_mode = "trusted_auto"
334            "#,
335        )
336        .expect("permissions config");
337        assert_eq!(config.default_mode, PermissionMode::Auto);
338    }
339
340    #[test]
341    fn parses_exact_tool_rules() {
342        let config: PermissionsConfig = toml::from_str(
343            r#"
344            allow = ["read_file", "unified_search"]
345            deny = ["unified_exec"]
346            "#,
347        )
348        .expect("permissions config");
349
350        assert_eq!(
351            config.allow,
352            vec!["read_file".to_string(), "unified_search".to_string()]
353        );
354        assert_eq!(config.deny, vec!["unified_exec".to_string()]);
355    }
356
357    #[test]
358    fn auto_mode_defaults_are_conservative() {
359        let config = PermissionsConfig::default();
360
361        assert_eq!(config.auto_mode.max_consecutive_denials, 3);
362        assert_eq!(config.auto_mode.max_total_denials, 20);
363        assert!(config.auto_mode.drop_broad_allow_rules);
364        assert!(!config.auto_mode.block_rules.is_empty());
365        assert!(!config.auto_mode.allow_exceptions.is_empty());
366        assert!(config.auto_mode.environment.trusted_paths.is_empty());
367    }
368}