#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolPermissionMode {
BypassPermissions,
DontAsk,
AcceptEdits,
Default,
}
#[derive(Debug, Clone)]
pub enum PermissionBehavior {
Allow {
updated_input: serde_json::Value,
decision_reason: DecisionReason,
},
Ask,
Passthrough {
message: String,
},
}
#[derive(Debug, Clone)]
pub enum DecisionReason {
Mode {
mode: ToolPermissionMode,
},
}
#[derive(Debug, Clone)]
pub struct ToolPermissionContext {
pub mode: ToolPermissionMode,
}
pub struct BashToolInput {
pub command: String,
}
const ACCEPT_EDITS_ALLOWED_COMMANDS: &[&str] = &[
"mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed",
];
fn is_filesystem_command(command: &str) -> bool {
ACCEPT_EDITS_ALLOWED_COMMANDS.contains(&command)
}
fn validate_command_for_mode(
cmd: &str,
context: &ToolPermissionContext,
) -> PermissionBehavior {
let trimmed = cmd.trim();
let base_cmd = trimmed.split_whitespace().next();
let Some(base_cmd) = base_cmd else {
return PermissionBehavior::Passthrough {
message: "Base command not found".to_string(),
};
};
if context.mode == ToolPermissionMode::AcceptEdits && is_filesystem_command(base_cmd) {
return PermissionBehavior::Allow {
updated_input: serde_json::json!({ "command": cmd }),
decision_reason: DecisionReason::Mode {
mode: ToolPermissionMode::AcceptEdits,
},
};
}
PermissionBehavior::Passthrough {
message: format!("No mode-specific handling for '{base_cmd}' in {:?} mode", context.mode),
}
}
fn split_commands(command: &str) -> Vec<&str> {
command
.split(|c: char| c == ';' || c == '\n')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect()
}
pub fn check_permission_mode(
input: &BashToolInput,
context: &ToolPermissionContext,
) -> PermissionBehavior {
if context.mode == ToolPermissionMode::BypassPermissions {
return PermissionBehavior::Passthrough {
message: "Bypass mode is handled in main permission flow".to_string(),
};
}
if context.mode == ToolPermissionMode::DontAsk {
return PermissionBehavior::Passthrough {
message: "DontAsk mode is handled in main permission flow".to_string(),
};
}
let commands = split_commands(&input.command);
for cmd in &commands {
let result = validate_command_for_mode(cmd, context);
if !matches!(&result, PermissionBehavior::Passthrough { .. }) {
return result;
}
}
PermissionBehavior::Passthrough {
message: "No mode-specific validation required".to_string(),
}
}
pub fn get_auto_allowed_commands(mode: &ToolPermissionMode) -> Vec<&'static str> {
match mode {
ToolPermissionMode::AcceptEdits => ACCEPT_EDITS_ALLOWED_COMMANDS.to_vec(),
_ => vec![],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bypass_mode_returns_passthrough() {
let ctx = ToolPermissionContext {
mode: ToolPermissionMode::BypassPermissions,
};
let input = BashToolInput {
command: "mkdir test".to_string(),
};
assert!(matches!(
check_permission_mode(&input, &ctx),
PermissionBehavior::Passthrough { .. }
));
}
#[test]
fn test_accept_edits_allows_mkdir() {
let ctx = ToolPermissionContext {
mode: ToolPermissionMode::AcceptEdits,
};
let input = BashToolInput {
command: "mkdir test".to_string(),
};
assert!(matches!(
check_permission_mode(&input, &ctx),
PermissionBehavior::Allow { .. }
));
}
#[test]
fn test_default_mode_passthrough() {
let ctx = ToolPermissionContext {
mode: ToolPermissionMode::Default,
};
let input = BashToolInput {
command: "cargo test".to_string(),
};
assert!(matches!(
check_permission_mode(&input, &ctx),
PermissionBehavior::Passthrough { .. }
));
}
#[test]
fn test_get_auto_allowed_commands() {
assert_eq!(
get_auto_allowed_commands(&ToolPermissionMode::AcceptEdits),
vec!["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"]
);
assert!(get_auto_allowed_commands(&ToolPermissionMode::Default).is_empty());
}
}