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    /// Classifier-backed autonomous mode.
16    #[serde(alias = "trusted_auto", alias = "trusted-auto")]
17    Auto,
18    /// Read-only planning mode.
19    Plan,
20    /// Deny any action that is not explicitly allowed.
21    #[serde(alias = "dontAsk", alias = "dont-ask")]
22    DontAsk,
23    /// Skip prompts except protected writes and sandbox escalation prompts.
24    #[serde(
25        alias = "bypassPermissions",
26        alias = "bypass-permissions",
27        alias = "full-auto"
28    )]
29    BypassPermissions,
30}
31
32/// Permission system configuration - Controls command resolution, audit logging, and caching
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct PermissionsConfig {
36    /// Default unified permission mode for the current session.
37    #[serde(default)]
38    pub default_mode: PermissionMode,
39
40    /// Classifier-backed auto mode policy and environment settings.
41    #[serde(default)]
42    pub auto_mode: AutoModeConfig,
43
44    /// Rules that allow matching tool calls without prompting.
45    #[serde(default)]
46    pub allow: Vec<String>,
47
48    /// Rules that require an interactive prompt when they match.
49    #[serde(default)]
50    pub ask: Vec<String>,
51
52    /// Rules that deny matching tool calls.
53    #[serde(default)]
54    pub deny: Vec<String>,
55
56    /// Enable the enhanced permission system (resolver + audit logger + cache)
57    #[serde(default = "default_enabled")]
58    pub enabled: bool,
59
60    /// Enable command resolution to actual paths (helps identify suspicious commands)
61    #[serde(default = "default_resolve_commands")]
62    pub resolve_commands: bool,
63
64    /// Enable audit logging of all permission decisions
65    #[serde(default = "default_audit_enabled")]
66    pub audit_enabled: bool,
67
68    /// Directory for audit logs (created if not exists)
69    /// Defaults to ~/.vtcode/audit
70    #[serde(default = "default_audit_directory")]
71    pub audit_directory: String,
72
73    /// Log allowed commands to audit trail
74    #[serde(default = "default_log_allowed_commands")]
75    pub log_allowed_commands: bool,
76
77    /// Log denied commands to audit trail
78    #[serde(default = "default_log_denied_commands")]
79    pub log_denied_commands: bool,
80
81    /// Log permission prompts (when user is asked for confirmation)
82    #[serde(default = "default_log_permission_prompts")]
83    pub log_permission_prompts: bool,
84
85    /// Enable permission decision caching to avoid redundant evaluations
86    #[serde(default = "default_cache_enabled")]
87    pub cache_enabled: bool,
88
89    /// Cache time-to-live in seconds (how long to cache decisions)
90    /// Default: 300 seconds (5 minutes)
91    #[serde(default = "default_cache_ttl_seconds")]
92    pub cache_ttl_seconds: u64,
93}
94
95/// Classifier-backed auto mode configuration.
96#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct AutoModeConfig {
99    /// Optional model override for the transcript reviewer.
100    #[serde(default)]
101    pub model: String,
102
103    /// Optional model override for the prompt-injection probe.
104    #[serde(default)]
105    pub probe_model: String,
106
107    /// Maximum consecutive denials before auto mode falls back.
108    #[serde(default = "default_auto_mode_max_consecutive_denials")]
109    pub max_consecutive_denials: u32,
110
111    /// Maximum total denials before auto mode falls back.
112    #[serde(default = "default_auto_mode_max_total_denials")]
113    pub max_total_denials: u32,
114
115    /// Drop broad code-execution allow rules while auto mode is active.
116    #[serde(default = "default_auto_mode_drop_broad_allow_rules")]
117    pub drop_broad_allow_rules: bool,
118
119    /// Classifier block rules applied in stage 2 reasoning.
120    #[serde(default = "default_auto_mode_block_rules")]
121    pub block_rules: Vec<String>,
122
123    /// Narrow allow exceptions applied after block rules.
124    #[serde(default = "default_auto_mode_allow_exceptions")]
125    pub allow_exceptions: Vec<String>,
126
127    /// Trusted environment boundaries for the classifier.
128    #[serde(default)]
129    pub environment: AutoModeEnvironmentConfig,
130}
131
132/// Trust-boundary configuration for auto mode.
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134#[derive(Debug, Clone, Default, Deserialize, Serialize)]
135pub struct AutoModeEnvironmentConfig {
136    #[serde(default)]
137    pub trusted_paths: Vec<String>,
138
139    #[serde(default)]
140    pub trusted_domains: Vec<String>,
141
142    #[serde(default)]
143    pub trusted_git_hosts: Vec<String>,
144
145    #[serde(default)]
146    pub trusted_git_orgs: Vec<String>,
147
148    #[serde(default)]
149    pub trusted_services: Vec<String>,
150}
151
152impl Default for AutoModeConfig {
153    fn default() -> Self {
154        Self {
155            model: String::new(),
156            probe_model: String::new(),
157            max_consecutive_denials: default_auto_mode_max_consecutive_denials(),
158            max_total_denials: default_auto_mode_max_total_denials(),
159            drop_broad_allow_rules: default_auto_mode_drop_broad_allow_rules(),
160            block_rules: default_auto_mode_block_rules(),
161            allow_exceptions: default_auto_mode_allow_exceptions(),
162            environment: AutoModeEnvironmentConfig::default(),
163        }
164    }
165}
166
167#[inline]
168const fn default_enabled() -> bool {
169    true
170}
171
172#[inline]
173const fn default_resolve_commands() -> bool {
174    true
175}
176
177#[inline]
178const fn default_audit_enabled() -> bool {
179    true
180}
181
182const DEFAULT_AUDIT_DIR: &str = "~/.vtcode/audit";
183
184#[inline]
185fn default_audit_directory() -> String {
186    DEFAULT_AUDIT_DIR.into()
187}
188
189#[inline]
190const fn default_log_allowed_commands() -> bool {
191    true
192}
193
194#[inline]
195const fn default_log_denied_commands() -> bool {
196    true
197}
198
199#[inline]
200const fn default_log_permission_prompts() -> bool {
201    true
202}
203
204#[inline]
205const fn default_cache_enabled() -> bool {
206    true
207}
208
209#[inline]
210const fn default_cache_ttl_seconds() -> u64 {
211    300 // 5 minutes
212}
213
214#[inline]
215const fn default_auto_mode_max_consecutive_denials() -> u32 {
216    3
217}
218
219#[inline]
220const fn default_auto_mode_max_total_denials() -> u32 {
221    20
222}
223
224#[inline]
225const fn default_auto_mode_drop_broad_allow_rules() -> bool {
226    true
227}
228
229fn default_auto_mode_block_rules() -> Vec<String> {
230    vec![
231        "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(),
232        "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(),
233        "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(),
234        "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(),
235        "Block commands that bypass safety checks, disable verification, install persistence, or weaken the host security posture.".to_string(),
236        "Block destructive or production/shared-infrastructure actions unless the user explicitly authorized the target and the action.".to_string(),
237        "Block destructive actions against inferred, fuzzy-matched, or agent-selected targets when the user did not name the exact target.".to_string(),
238    ]
239}
240
241fn default_auto_mode_allow_exceptions() -> Vec<String> {
242    vec![
243        "Allow read-only tools and read-only browsing/search actions.".to_string(),
244        "Allow file edits and writes inside the current workspace when the path is not protected.".to_string(),
245        "Allow pushes only to the current session branch or configured git remotes inside the trusted environment.".to_string(),
246    ]
247}
248
249impl Default for PermissionsConfig {
250    fn default() -> Self {
251        Self {
252            default_mode: PermissionMode::default(),
253            auto_mode: AutoModeConfig::default(),
254            allow: Vec::new(),
255            ask: Vec::new(),
256            deny: Vec::new(),
257            enabled: default_enabled(),
258            resolve_commands: default_resolve_commands(),
259            audit_enabled: default_audit_enabled(),
260            audit_directory: default_audit_directory(),
261            log_allowed_commands: default_log_allowed_commands(),
262            log_denied_commands: default_log_denied_commands(),
263            log_permission_prompts: default_log_permission_prompts(),
264            cache_enabled: default_cache_enabled(),
265            cache_ttl_seconds: default_cache_ttl_seconds(),
266        }
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::{PermissionMode, PermissionsConfig};
273
274    #[test]
275    fn parses_claude_style_mode_aliases() {
276        let config: PermissionsConfig = toml::from_str(
277            r#"
278            default_mode = "acceptEdits"
279            "#,
280        )
281        .expect("permissions config");
282        assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
283
284        let config: PermissionsConfig = toml::from_str(
285            r#"
286            default_mode = "dontAsk"
287            "#,
288        )
289        .expect("permissions config");
290        assert_eq!(config.default_mode, PermissionMode::DontAsk);
291
292        let config: PermissionsConfig = toml::from_str(
293            r#"
294            default_mode = "bypassPermissions"
295            "#,
296        )
297        .expect("permissions config");
298        assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
299
300        let config: PermissionsConfig = toml::from_str(
301            r#"
302            default_mode = "auto"
303            "#,
304        )
305        .expect("permissions config");
306        assert_eq!(config.default_mode, PermissionMode::Auto);
307    }
308
309    #[test]
310    fn parses_legacy_mode_aliases() {
311        let config: PermissionsConfig = toml::from_str(
312            r#"
313            default_mode = "ask"
314            "#,
315        )
316        .expect("permissions config");
317        assert_eq!(config.default_mode, PermissionMode::Default);
318
319        let config: PermissionsConfig = toml::from_str(
320            r#"
321            default_mode = "auto-approved"
322            "#,
323        )
324        .expect("permissions config");
325        assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
326
327        let config: PermissionsConfig = toml::from_str(
328            r#"
329            default_mode = "full-auto"
330            "#,
331        )
332        .expect("permissions config");
333        assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
334
335        let config: PermissionsConfig = toml::from_str(
336            r#"
337            default_mode = "trusted_auto"
338            "#,
339        )
340        .expect("permissions config");
341        assert_eq!(config.default_mode, PermissionMode::Auto);
342    }
343
344    #[test]
345    fn parses_exact_tool_rules() {
346        let config: PermissionsConfig = toml::from_str(
347            r#"
348            allow = ["read_file", "unified_search"]
349            deny = ["unified_exec"]
350            "#,
351        )
352        .expect("permissions config");
353
354        assert_eq!(
355            config.allow,
356            vec!["read_file".to_string(), "unified_search".to_string()]
357        );
358        assert_eq!(config.deny, vec!["unified_exec".to_string()]);
359    }
360
361    #[test]
362    fn auto_mode_defaults_are_conservative() {
363        let config = PermissionsConfig::default();
364
365        assert_eq!(config.auto_mode.max_consecutive_denials, 3);
366        assert_eq!(config.auto_mode.max_total_denials, 20);
367        assert!(config.auto_mode.drop_broad_allow_rules);
368        assert!(!config.auto_mode.block_rules.is_empty());
369        assert!(!config.auto_mode.allow_exceptions.is_empty());
370        assert!(config.auto_mode.environment.trusted_paths.is_empty());
371    }
372}