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 allowed_tools: Vec<String>,
47
48 #[serde(default)]
50 pub disallowed_tools: Vec<String>,
51
52 #[serde(default)]
54 pub allow: Vec<String>,
55
56 #[serde(default)]
58 pub ask: Vec<String>,
59
60 #[serde(default)]
62 pub deny: Vec<String>,
63
64 #[serde(default = "default_enabled")]
66 pub enabled: bool,
67
68 #[serde(default = "default_resolve_commands")]
70 pub resolve_commands: bool,
71
72 #[serde(default = "default_audit_enabled")]
74 pub audit_enabled: bool,
75
76 #[serde(default = "default_audit_directory")]
79 pub audit_directory: String,
80
81 #[serde(default = "default_log_allowed_commands")]
83 pub log_allowed_commands: bool,
84
85 #[serde(default = "default_log_denied_commands")]
87 pub log_denied_commands: bool,
88
89 #[serde(default = "default_log_permission_prompts")]
91 pub log_permission_prompts: bool,
92
93 #[serde(default = "default_cache_enabled")]
95 pub cache_enabled: bool,
96
97 #[serde(default = "default_cache_ttl_seconds")]
100 pub cache_ttl_seconds: u64,
101}
102
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct AutoModeConfig {
107 #[serde(default)]
109 pub model: String,
110
111 #[serde(default)]
113 pub probe_model: String,
114
115 #[serde(default = "default_auto_mode_max_consecutive_denials")]
117 pub max_consecutive_denials: u32,
118
119 #[serde(default = "default_auto_mode_max_total_denials")]
121 pub max_total_denials: u32,
122
123 #[serde(default = "default_auto_mode_drop_broad_allow_rules")]
125 pub drop_broad_allow_rules: bool,
126
127 #[serde(default = "default_auto_mode_block_rules")]
129 pub block_rules: Vec<String>,
130
131 #[serde(default = "default_auto_mode_allow_exceptions")]
133 pub allow_exceptions: Vec<String>,
134
135 #[serde(default)]
137 pub environment: AutoModeEnvironmentConfig,
138}
139
140#[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 }
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}