1use serde::{Deserialize, Serialize};
2
3#[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 #[default]
10 #[serde(alias = "ask", alias = "suggest")]
11 Default,
12 #[serde(alias = "acceptEdits", alias = "accept-edits", alias = "auto-approved")]
14 AcceptEdits,
15 #[serde(alias = "trusted_auto", alias = "trusted-auto")]
17 Auto,
18 Plan,
20 #[serde(alias = "dontAsk", alias = "dont-ask")]
22 DontAsk,
23 #[serde(
25 alias = "bypassPermissions",
26 alias = "bypass-permissions",
27 alias = "full-auto"
28 )]
29 BypassPermissions,
30}
31
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct PermissionsConfig {
36 #[serde(default)]
38 pub default_mode: PermissionMode,
39
40 #[serde(default)]
42 pub auto_mode: AutoModeConfig,
43
44 #[serde(default)]
46 pub allow: Vec<String>,
47
48 #[serde(default)]
50 pub ask: Vec<String>,
51
52 #[serde(default)]
54 pub deny: Vec<String>,
55
56 #[serde(default = "default_enabled")]
58 pub enabled: bool,
59
60 #[serde(default = "default_resolve_commands")]
62 pub resolve_commands: bool,
63
64 #[serde(default = "default_audit_enabled")]
66 pub audit_enabled: bool,
67
68 #[serde(default = "default_audit_directory")]
71 pub audit_directory: String,
72
73 #[serde(default = "default_log_allowed_commands")]
75 pub log_allowed_commands: bool,
76
77 #[serde(default = "default_log_denied_commands")]
79 pub log_denied_commands: bool,
80
81 #[serde(default = "default_log_permission_prompts")]
83 pub log_permission_prompts: bool,
84
85 #[serde(default = "default_cache_enabled")]
87 pub cache_enabled: bool,
88
89 #[serde(default = "default_cache_ttl_seconds")]
92 pub cache_ttl_seconds: u64,
93}
94
95#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct AutoModeConfig {
99 #[serde(default)]
101 pub model: String,
102
103 #[serde(default)]
105 pub probe_model: String,
106
107 #[serde(default = "default_auto_mode_max_consecutive_denials")]
109 pub max_consecutive_denials: u32,
110
111 #[serde(default = "default_auto_mode_max_total_denials")]
113 pub max_total_denials: u32,
114
115 #[serde(default = "default_auto_mode_drop_broad_allow_rules")]
117 pub drop_broad_allow_rules: bool,
118
119 #[serde(default = "default_auto_mode_block_rules")]
121 pub block_rules: Vec<String>,
122
123 #[serde(default = "default_auto_mode_allow_exceptions")]
125 pub allow_exceptions: Vec<String>,
126
127 #[serde(default)]
129 pub environment: AutoModeEnvironmentConfig,
130}
131
132#[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 }
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}