1use crate::env_helpers::default_enabled;
2use serde::{Deserialize, Serialize};
3
4#[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 #[default]
11 #[serde(alias = "ask", alias = "suggest")]
12 Default,
13 #[serde(alias = "acceptEdits", alias = "accept-edits", alias = "auto-approved")]
15 AcceptEdits,
16 #[serde(alias = "trusted_auto", alias = "trusted-auto")]
18 Auto,
19 Plan,
21 #[serde(alias = "dontAsk", alias = "dont-ask")]
23 DontAsk,
24 #[serde(
26 alias = "bypassPermissions",
27 alias = "bypass-permissions",
28 alias = "full-auto"
29 )]
30 BypassPermissions,
31}
32
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct PermissionsConfig {
37 #[serde(default)]
39 pub default_mode: PermissionMode,
40
41 #[serde(default)]
43 pub auto_mode: AutoModeConfig,
44
45 #[serde(default)]
47 pub allow: Vec<String>,
48
49 #[serde(default)]
51 pub ask: Vec<String>,
52
53 #[serde(default)]
55 pub deny: Vec<String>,
56
57 #[serde(default = "default_enabled")]
59 pub enabled: bool,
60
61 #[serde(default = "default_resolve_commands")]
63 pub resolve_commands: bool,
64
65 #[serde(default = "default_audit_enabled")]
67 pub audit_enabled: bool,
68
69 #[serde(default = "default_audit_directory")]
72 pub audit_directory: String,
73
74 #[serde(default = "default_log_allowed_commands")]
76 pub log_allowed_commands: bool,
77
78 #[serde(default = "default_log_denied_commands")]
80 pub log_denied_commands: bool,
81
82 #[serde(default = "default_log_permission_prompts")]
84 pub log_permission_prompts: bool,
85
86 #[serde(default = "default_cache_enabled")]
88 pub cache_enabled: bool,
89
90 #[serde(default = "default_cache_ttl_seconds")]
93 pub cache_ttl_seconds: u64,
94}
95
96#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct AutoModeConfig {
100 #[serde(default)]
102 pub model: String,
103
104 #[serde(default)]
106 pub probe_model: String,
107
108 #[serde(default = "default_auto_mode_max_consecutive_denials")]
110 pub max_consecutive_denials: u32,
111
112 #[serde(default = "default_auto_mode_max_total_denials")]
114 pub max_total_denials: u32,
115
116 #[serde(default = "default_auto_mode_drop_broad_allow_rules")]
118 pub drop_broad_allow_rules: bool,
119
120 #[serde(default = "default_auto_mode_block_rules")]
122 pub block_rules: Vec<String>,
123
124 #[serde(default = "default_auto_mode_allow_exceptions")]
126 pub allow_exceptions: Vec<String>,
127
128 #[serde(default)]
130 pub environment: AutoModeEnvironmentConfig,
131}
132
133#[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 }
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}