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