use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum PermissionMode {
#[default]
#[serde(alias = "ask", alias = "suggest")]
Default,
#[serde(alias = "acceptEdits", alias = "accept-edits", alias = "auto-approved")]
AcceptEdits,
#[serde(alias = "trusted_auto", alias = "trusted-auto")]
Auto,
Plan,
#[serde(alias = "dontAsk", alias = "dont-ask")]
DontAsk,
#[serde(
alias = "bypassPermissions",
alias = "bypass-permissions",
alias = "full-auto"
)]
BypassPermissions,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PermissionsConfig {
#[serde(default)]
pub default_mode: PermissionMode,
#[serde(default)]
pub auto_mode: AutoModeConfig,
#[serde(default)]
pub allow: Vec<String>,
#[serde(default)]
pub ask: Vec<String>,
#[serde(default)]
pub deny: Vec<String>,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_resolve_commands")]
pub resolve_commands: bool,
#[serde(default = "default_audit_enabled")]
pub audit_enabled: bool,
#[serde(default = "default_audit_directory")]
pub audit_directory: String,
#[serde(default = "default_log_allowed_commands")]
pub log_allowed_commands: bool,
#[serde(default = "default_log_denied_commands")]
pub log_denied_commands: bool,
#[serde(default = "default_log_permission_prompts")]
pub log_permission_prompts: bool,
#[serde(default = "default_cache_enabled")]
pub cache_enabled: bool,
#[serde(default = "default_cache_ttl_seconds")]
pub cache_ttl_seconds: u64,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AutoModeConfig {
#[serde(default)]
pub model: String,
#[serde(default)]
pub probe_model: String,
#[serde(default = "default_auto_mode_max_consecutive_denials")]
pub max_consecutive_denials: u32,
#[serde(default = "default_auto_mode_max_total_denials")]
pub max_total_denials: u32,
#[serde(default = "default_auto_mode_drop_broad_allow_rules")]
pub drop_broad_allow_rules: bool,
#[serde(default = "default_auto_mode_block_rules")]
pub block_rules: Vec<String>,
#[serde(default = "default_auto_mode_allow_exceptions")]
pub allow_exceptions: Vec<String>,
#[serde(default)]
pub environment: AutoModeEnvironmentConfig,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AutoModeEnvironmentConfig {
#[serde(default)]
pub trusted_paths: Vec<String>,
#[serde(default)]
pub trusted_domains: Vec<String>,
#[serde(default)]
pub trusted_git_hosts: Vec<String>,
#[serde(default)]
pub trusted_git_orgs: Vec<String>,
#[serde(default)]
pub trusted_services: Vec<String>,
}
impl Default for AutoModeConfig {
fn default() -> Self {
Self {
model: String::new(),
probe_model: String::new(),
max_consecutive_denials: default_auto_mode_max_consecutive_denials(),
max_total_denials: default_auto_mode_max_total_denials(),
drop_broad_allow_rules: default_auto_mode_drop_broad_allow_rules(),
block_rules: default_auto_mode_block_rules(),
allow_exceptions: default_auto_mode_allow_exceptions(),
environment: AutoModeEnvironmentConfig::default(),
}
}
}
#[inline]
const fn default_enabled() -> bool {
true
}
#[inline]
const fn default_resolve_commands() -> bool {
true
}
#[inline]
const fn default_audit_enabled() -> bool {
true
}
const DEFAULT_AUDIT_DIR: &str = "~/.vtcode/audit";
#[inline]
fn default_audit_directory() -> String {
DEFAULT_AUDIT_DIR.into()
}
#[inline]
const fn default_log_allowed_commands() -> bool {
true
}
#[inline]
const fn default_log_denied_commands() -> bool {
true
}
#[inline]
const fn default_log_permission_prompts() -> bool {
true
}
#[inline]
const fn default_cache_enabled() -> bool {
true
}
#[inline]
const fn default_cache_ttl_seconds() -> u64 {
300 }
#[inline]
const fn default_auto_mode_max_consecutive_denials() -> u32 {
3
}
#[inline]
const fn default_auto_mode_max_total_denials() -> u32 {
20
}
#[inline]
const fn default_auto_mode_drop_broad_allow_rules() -> bool {
true
}
fn default_auto_mode_block_rules() -> Vec<String> {
vec![
"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(),
"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(),
"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(),
"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(),
"Block commands that bypass safety checks, disable verification, install persistence, or weaken the host security posture.".to_string(),
"Block destructive or production/shared-infrastructure actions unless the user explicitly authorized the target and the action.".to_string(),
"Block destructive actions against inferred, fuzzy-matched, or agent-selected targets when the user did not name the exact target.".to_string(),
]
}
fn default_auto_mode_allow_exceptions() -> Vec<String> {
vec![
"Allow read-only tools and read-only browsing/search actions.".to_string(),
"Allow file edits and writes inside the current workspace when the path is not protected.".to_string(),
"Allow pushes only to the current session branch or configured git remotes inside the trusted environment.".to_string(),
]
}
impl Default for PermissionsConfig {
fn default() -> Self {
Self {
default_mode: PermissionMode::default(),
auto_mode: AutoModeConfig::default(),
allow: Vec::new(),
ask: Vec::new(),
deny: Vec::new(),
enabled: default_enabled(),
resolve_commands: default_resolve_commands(),
audit_enabled: default_audit_enabled(),
audit_directory: default_audit_directory(),
log_allowed_commands: default_log_allowed_commands(),
log_denied_commands: default_log_denied_commands(),
log_permission_prompts: default_log_permission_prompts(),
cache_enabled: default_cache_enabled(),
cache_ttl_seconds: default_cache_ttl_seconds(),
}
}
}
#[cfg(test)]
mod tests {
use super::{PermissionMode, PermissionsConfig};
#[test]
fn parses_claude_style_mode_aliases() {
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "acceptEdits"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "dontAsk"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::DontAsk);
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "bypassPermissions"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "auto"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::Auto);
}
#[test]
fn parses_legacy_mode_aliases() {
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "ask"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::Default);
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "auto-approved"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::AcceptEdits);
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "full-auto"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::BypassPermissions);
let config: PermissionsConfig = toml::from_str(
r#"
default_mode = "trusted_auto"
"#,
)
.expect("permissions config");
assert_eq!(config.default_mode, PermissionMode::Auto);
}
#[test]
fn parses_exact_tool_rules() {
let config: PermissionsConfig = toml::from_str(
r#"
allow = ["read_file", "unified_search"]
deny = ["unified_exec"]
"#,
)
.expect("permissions config");
assert_eq!(
config.allow,
vec!["read_file".to_string(), "unified_search".to_string()]
);
assert_eq!(config.deny, vec!["unified_exec".to_string()]);
}
#[test]
fn auto_mode_defaults_are_conservative() {
let config = PermissionsConfig::default();
assert_eq!(config.auto_mode.max_consecutive_denials, 3);
assert_eq!(config.auto_mode.max_total_denials, 20);
assert!(config.auto_mode.drop_broad_allow_rules);
assert!(!config.auto_mode.block_rules.is_empty());
assert!(!config.auto_mode.allow_exceptions.is_empty());
assert!(config.auto_mode.environment.trusted_paths.is_empty());
}
}