use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PermissionBehavior {
Allow,
Deny,
#[default]
Ask,
}
impl PermissionBehavior {
pub fn as_str(&self) -> &'static str {
match self {
PermissionBehavior::Allow => "allow",
PermissionBehavior::Deny => "deny",
PermissionBehavior::Ask => "ask",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PermissionMode {
#[default]
Default,
AcceptEdits,
Bypass,
DontAsk,
Plan,
Auto,
Bubble,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionRuleSource {
UserSettings,
ProjectSettings,
LocalSettings,
CliArg,
Session,
Policy,
FlagSettings,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PermissionRule {
pub source: PermissionRuleSource,
pub behavior: PermissionBehavior,
pub tool_name: String,
pub rule_content: Option<String>,
}
impl PermissionRule {
pub fn new(tool_name: &str, behavior: PermissionBehavior) -> Self {
Self {
source: PermissionRuleSource::UserSettings,
behavior,
tool_name: tool_name.to_string(),
rule_content: None,
}
}
pub fn with_content(tool_name: &str, behavior: PermissionBehavior, content: &str) -> Self {
Self {
source: PermissionRuleSource::UserSettings,
behavior,
tool_name: tool_name.to_string(),
rule_content: Some(content.to_string()),
}
}
pub fn allow(tool_name: &str) -> Self {
Self::new(tool_name, PermissionBehavior::Allow)
}
pub fn deny(tool_name: &str) -> Self {
Self::new(tool_name, PermissionBehavior::Deny)
}
pub fn ask(tool_name: &str) -> Self {
Self::new(tool_name, PermissionBehavior::Ask)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionMetadata {
pub tool_name: String,
pub description: Option<String>,
pub input: Option<serde_json::Value>,
pub cwd: Option<String>,
}
impl PermissionMetadata {
pub fn new(tool_name: &str) -> Self {
Self {
tool_name: tool_name.to_string(),
description: None,
input: None,
cwd: None,
}
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = Some(description.to_string());
self
}
pub fn with_input(mut self, input: serde_json::Value) -> Self {
self.input = Some(input);
self
}
pub fn with_cwd(mut self, cwd: &str) -> Self {
self.cwd = Some(cwd.to_string());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PermissionDecisionReason {
Rule { rule: PermissionRule },
Mode { mode: PermissionMode },
Hook {
hook_name: String,
reason: Option<String>,
},
SandboxOverride { reason: String },
SafetyCheck { reason: String },
Other { reason: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionAllowDecision {
pub behavior: PermissionBehavior,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_input: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_modified: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision_reason: Option<PermissionDecisionReason>,
}
impl PermissionAllowDecision {
pub fn new() -> Self {
Self {
behavior: PermissionBehavior::Allow,
updated_input: None,
user_modified: None,
decision_reason: None,
}
}
pub fn with_reason(mut self, reason: PermissionDecisionReason) -> Self {
self.decision_reason = Some(reason);
self
}
}
impl Default for PermissionAllowDecision {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionAskDecision {
pub behavior: PermissionBehavior,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_input: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision_reason: Option<PermissionDecisionReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked_path: Option<String>,
}
impl PermissionAskDecision {
pub fn new(message: &str) -> Self {
Self {
behavior: PermissionBehavior::Ask,
message: message.to_string(),
updated_input: None,
decision_reason: None,
blocked_path: None,
}
}
pub fn with_reason(mut self, reason: PermissionDecisionReason) -> Self {
self.decision_reason = Some(reason);
self
}
pub fn with_blocked_path(mut self, path: &str) -> Self {
self.blocked_path = Some(path.to_string());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionDenyDecision {
pub behavior: PermissionBehavior,
pub message: String,
pub decision_reason: PermissionDecisionReason,
}
impl PermissionDenyDecision {
pub fn new(message: &str, reason: PermissionDecisionReason) -> Self {
Self {
behavior: PermissionBehavior::Deny,
message: message.to_string(),
decision_reason: reason,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "behavior", rename_all = "lowercase")]
pub enum PermissionDecision {
Allow(PermissionAllowDecision),
Ask(PermissionAskDecision),
Deny(PermissionDenyDecision),
}
impl PermissionDecision {
pub fn is_allowed(&self) -> bool {
matches!(self, PermissionDecision::Allow(_))
}
pub fn is_denied(&self) -> bool {
matches!(self, PermissionDecision::Deny(_))
}
pub fn is_ask(&self) -> bool {
matches!(self, PermissionDecision::Ask(_))
}
pub fn message(&self) -> Option<&str> {
match self {
PermissionDecision::Allow(_) => None,
PermissionDecision::Ask(d) => Some(&d.message),
PermissionDecision::Deny(d) => Some(&d.message),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "behavior", rename_all = "lowercase")]
pub enum PermissionResult {
Allow(PermissionAllowDecision),
Ask(PermissionAskDecision),
Deny(PermissionDenyDecision),
Passthrough {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
decision_reason: Option<PermissionDecisionReason>,
},
}
impl PermissionResult {
pub fn to_decision(self) -> Option<PermissionDecision> {
match self {
PermissionResult::Allow(d) => Some(PermissionDecision::Allow(d)),
PermissionResult::Ask(d) => Some(PermissionDecision::Ask(d)),
PermissionResult::Deny(d) => Some(PermissionDecision::Deny(d)),
PermissionResult::Passthrough { .. } => None,
}
}
pub fn is_allowed(&self) -> bool {
matches!(
self,
PermissionResult::Allow(_) | PermissionResult::Passthrough { .. }
)
}
pub fn is_denied(&self) -> bool {
matches!(self, PermissionResult::Deny(_))
}
pub fn is_ask(&self) -> bool {
matches!(self, PermissionResult::Ask(_))
}
pub fn message(&self) -> Option<&str> {
match self {
PermissionResult::Allow(_) => None,
PermissionResult::Ask(d) => Some(&d.message),
PermissionResult::Deny(d) => Some(&d.message),
PermissionResult::Passthrough { message, .. } => Some(message),
}
}
}
pub struct PermissionContext {
pub mode: PermissionMode,
pub allow_rules: Vec<PermissionRule>,
pub deny_rules: Vec<PermissionRule>,
pub ask_rules: Vec<PermissionRule>,
pub denial_tracking: std::sync::RwLock<crate::utils::permissions::denial_tracking::DenialTrackingState>,
}
impl std::fmt::Debug for PermissionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionContext")
.field("mode", &self.mode)
.field("allow_rules", &self.allow_rules)
.field("deny_rules", &self.deny_rules)
.field("ask_rules", &self.ask_rules)
.finish_non_exhaustive()
}
}
impl Clone for PermissionContext {
fn clone(&self) -> Self {
let dt = self.denial_tracking.read().map(|dt| *dt).unwrap_or_default();
Self {
mode: self.mode,
allow_rules: self.allow_rules.clone(),
deny_rules: self.deny_rules.clone(),
ask_rules: self.ask_rules.clone(),
denial_tracking: std::sync::RwLock::new(dt),
}
}
}
impl Default for PermissionContext {
fn default() -> Self {
Self {
mode: PermissionMode::default(),
allow_rules: Vec::new(),
deny_rules: Vec::new(),
ask_rules: Vec::new(),
denial_tracking: std::sync::RwLock::new(
crate::utils::permissions::denial_tracking::DenialTrackingState::default(),
),
}
}
}
fn tool_name_matches_rule(tool_name: &str, rule: &PermissionRule) -> bool {
let rule_tool = &rule.tool_name;
if rule_tool == tool_name {
return true;
}
if rule_tool.ends_with("__") && tool_name.starts_with(rule_tool.as_str()) {
return true;
}
if rule_tool.ends_with('_') && tool_name.starts_with(rule_tool.as_str()) {
return true;
}
if rule_tool == "*" {
return true;
}
false
}
impl PermissionContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_mode(mut self, mode: PermissionMode) -> Self {
self.mode = mode;
self
}
pub fn with_allow_rule(mut self, rule: PermissionRule) -> Self {
self.allow_rules.push(rule);
self
}
pub fn with_deny_rule(mut self, rule: PermissionRule) -> Self {
self.deny_rules.push(rule);
self
}
pub fn with_ask_rule(mut self, rule: PermissionRule) -> Self {
self.ask_rules.push(rule);
self
}
pub fn with_denial_tracking(
mut self,
state: crate::utils::permissions::denial_tracking::DenialTrackingState,
) -> Self {
let guard = self.denial_tracking.get_mut().unwrap();
*guard = state;
self
}
fn deny_rule_matches(&self, tool_name: &str, rule: &PermissionRule) -> bool {
if rule.rule_content.is_some() {
return false;
}
tool_name_matches_rule(tool_name, rule)
}
fn allow_rule_matches(
&self,
tool_name: &str,
input: Option<&serde_json::Value>,
rule: &PermissionRule,
) -> bool {
if !tool_name_matches_rule(tool_name, rule) {
return false;
}
if let Some(content) = &rule.rule_content {
if let Some(input) = input {
let input_str = input.to_string();
return input_str.contains(content);
}
return false;
}
true
}
pub fn check_tool(
&self,
tool_name: &str,
input: Option<&serde_json::Value>,
) -> PermissionResult {
for rule in &self.deny_rules {
if self.deny_rule_matches(tool_name, rule) {
return PermissionResult::Deny(PermissionDenyDecision::new(
&format!("Tool '{}' is denied by rule", tool_name),
PermissionDecisionReason::Rule { rule: rule.clone() },
));
}
}
for rule in &self.allow_rules {
if self.allow_rule_matches(tool_name, input, rule) {
return PermissionResult::Allow(
PermissionAllowDecision::new()
.with_reason(PermissionDecisionReason::Rule { rule: rule.clone() }),
);
}
}
for rule in &self.ask_rules {
if self.deny_rule_matches(tool_name, rule) {
return PermissionResult::Ask(
PermissionAskDecision::new(&format!(
"Tool '{}' requires permission",
tool_name
))
.with_reason(PermissionDecisionReason::Rule { rule: rule.clone() }),
);
}
}
match self.mode {
PermissionMode::Bypass => {
return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
PermissionDecisionReason::Mode {
mode: PermissionMode::Bypass,
},
));
}
PermissionMode::DontAsk => {
return PermissionResult::Deny(PermissionDenyDecision::new(
"Permission mode is 'dontAsk'",
PermissionDecisionReason::Mode {
mode: PermissionMode::DontAsk,
},
));
}
PermissionMode::AcceptEdits => {
if tool_name == "Write" || tool_name == "Edit" || tool_name == "Bash" {
return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
PermissionDecisionReason::Mode {
mode: PermissionMode::AcceptEdits,
},
));
}
}
PermissionMode::Bubble => {
let safe_tools = ["Read", "Glob", "Grep", "Search", "WebFetch", "WebSearch"];
if safe_tools.iter().any(|&t| t == tool_name) {
return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
PermissionDecisionReason::Mode {
mode: PermissionMode::Bubble,
},
));
}
if let Some(input_val) = input {
let input_str = input_val.to_string();
let dangerous_patterns = [
"rm -rf",
"rm /",
"del /",
"format",
"dd if=",
"> /dev/sd",
"chmod 777",
"chown -R",
];
for pattern in dangerous_patterns {
if input_str.contains(pattern) {
return PermissionResult::Ask(
PermissionAskDecision::new(&format!(
"Tool '{}' contains potentially dangerous pattern: {}",
tool_name, pattern
))
.with_reason(
PermissionDecisionReason::Mode {
mode: PermissionMode::Bubble,
},
),
);
}
}
}
if tool_name == "Write"
|| tool_name == "Edit"
|| tool_name == "Bash"
|| tool_name == "FileEdit"
|| tool_name == "Write"
{
return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
PermissionDecisionReason::Mode {
mode: PermissionMode::Bubble,
},
));
}
}
PermissionMode::Auto => {
if crate::utils::permissions::classifier_decision::is_auto_mode_allowlisted_tool(
tool_name,
) {
if let Ok(mut dt) = self.denial_tracking.write() {
*dt = crate::utils::permissions::denial_tracking::record_success(*dt);
}
return PermissionResult::Allow(
PermissionAllowDecision::new()
.with_reason(PermissionDecisionReason::Mode {
mode: PermissionMode::Auto,
}),
);
}
if let Ok(mut dt) = self.denial_tracking.write() {
*dt = crate::utils::permissions::denial_tracking::record_denial(*dt);
}
let should_fallback = if let Ok(dt) = self.denial_tracking.read() {
crate::utils::permissions::denial_tracking::should_fallback_to_prompting(*dt)
} else {
false
};
let mut msg = format!("Tool '{}' requires auto-classification", tool_name);
if should_fallback {
msg = format!(
"{}. Auto mode has failed repeatedly — consider switching to a different permission mode.",
msg
);
}
return PermissionResult::Ask(
PermissionAskDecision::new(&msg).with_reason(PermissionDecisionReason::Mode {
mode: PermissionMode::Auto,
}),
);
}
_ => {}
}
PermissionResult::Ask(
PermissionAskDecision::new(&format!("Permission required to use {}", tool_name))
.with_reason(PermissionDecisionReason::Mode { mode: self.mode }),
)
}
}
pub type PermissionCallback =
Box<dyn Fn(PermissionMetadata, PermissionResult) -> PermissionResult + Send + Sync>;
pub struct PermissionHandler {
context: PermissionContext,
callback: Option<PermissionCallback>,
}
impl PermissionHandler {
pub fn new(context: PermissionContext) -> Self {
Self {
context,
callback: None,
}
}
pub fn with_callback(context: PermissionContext, callback: PermissionCallback) -> Self {
Self {
context,
callback: Some(callback),
}
}
pub fn check(&self, metadata: PermissionMetadata) -> PermissionResult {
let result = self
.context
.check_tool(&metadata.tool_name, metadata.input.as_ref());
if let Some(callback) = &self.callback {
return callback(metadata, result);
}
result
}
pub fn is_allowed(&self, metadata: &PermissionMetadata) -> bool {
self.check(metadata.clone()).is_allowed()
}
}
impl PermissionHandler {
pub fn default() -> Self {
Self::new(PermissionContext::default())
}
}