use crate::control::handlers::CanUseToolHandler;
use crate::error::ClawError;
use crate::options::PermissionMode;
use crate::permissions::PermissionDecision;
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct DefaultPermissionHandler {
mode: PermissionMode,
allowed_tools: Vec<String>,
disallowed_tools: Vec<String>,
}
impl DefaultPermissionHandler {
pub fn builder() -> DefaultPermissionHandlerBuilder {
DefaultPermissionHandlerBuilder::default()
}
fn is_allowed(&self, tool_name: &str) -> bool {
self.allowed_tools.is_empty() || self.allowed_tools.iter().any(|t| t == tool_name)
}
fn is_denied(&self, tool_name: &str) -> bool {
self.disallowed_tools.iter().any(|t| t == tool_name)
}
fn default_policy(&self) -> bool {
match self.mode {
PermissionMode::Allow => true,
PermissionMode::Deny => false,
PermissionMode::Ask => false, PermissionMode::Custom => false, PermissionMode::Default
| PermissionMode::AcceptEdits
| PermissionMode::BypassPermissions
| PermissionMode::Plan => true,
}
}
}
#[async_trait]
impl CanUseToolHandler for DefaultPermissionHandler {
async fn can_use_tool(
&self,
tool_name: &str,
_tool_input: &Value,
) -> Result<PermissionDecision, ClawError> {
if self.is_denied(tool_name) {
return Ok(PermissionDecision::Deny { interrupt: false });
}
if !self.allowed_tools.is_empty() && self.is_allowed(tool_name) {
return Ok(PermissionDecision::Allow {
updated_input: None,
});
}
if self.default_policy() {
Ok(PermissionDecision::Allow {
updated_input: None,
})
} else {
Ok(PermissionDecision::Deny { interrupt: false })
}
}
}
#[derive(Debug, Default)]
pub struct DefaultPermissionHandlerBuilder {
mode: Option<PermissionMode>,
allowed_tools: Vec<String>,
disallowed_tools: Vec<String>,
}
impl DefaultPermissionHandlerBuilder {
pub fn mode(mut self, mode: PermissionMode) -> Self {
self.mode = Some(mode);
self
}
pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
self.allowed_tools = tools;
self
}
pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
self.disallowed_tools = tools;
self
}
pub fn build(self) -> DefaultPermissionHandler {
DefaultPermissionHandler {
mode: self.mode.unwrap_or(PermissionMode::Default),
allowed_tools: self.allowed_tools,
disallowed_tools: self.disallowed_tools,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn is_allowed(d: PermissionDecision) -> bool {
d.is_allowed()
}
fn is_denied(d: PermissionDecision) -> bool {
d.is_denied()
}
#[tokio::test]
async fn test_allow_mode_allows_all() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("read", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("write", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_deny_mode_denies_all() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Deny)
.build();
assert!(is_denied(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("read", &Value::Null).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("write", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_explicit_allow_overrides_deny_mode() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Deny)
.allowed_tools(vec!["bash".to_string(), "read".to_string()])
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("read", &Value::Null).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("write", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_explicit_deny_overrides_allow_mode() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.disallowed_tools(vec!["bash".to_string(), "write".to_string()])
.build();
assert!(is_denied(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("read", &Value::Null).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("write", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_explicit_deny_beats_explicit_allow() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.allowed_tools(vec!["bash".to_string()])
.disallowed_tools(vec!["bash".to_string()])
.build();
assert!(is_denied(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_ask_mode_defaults_to_deny() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Ask)
.build();
assert!(is_denied(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_custom_mode_defaults_to_deny() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Custom)
.build();
assert!(is_denied(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_legacy_mode_defaults_to_allow() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Default)
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::AcceptEdits)
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_empty_lists_uses_default_policy() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.allowed_tools(vec![])
.disallowed_tools(vec![])
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Deny)
.allowed_tools(vec![])
.disallowed_tools(vec![])
.build();
assert!(is_denied(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_allowlist_restricts_when_not_empty() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.allowed_tools(vec!["bash".to_string()])
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("read", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_builder_defaults() {
let handler = DefaultPermissionHandler::builder().build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_bypass_permissions_mode() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::BypassPermissions)
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("write", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_plan_mode() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Plan)
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_complex_allowlist_denylist() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Ask)
.allowed_tools(vec![
"bash".to_string(),
"read".to_string(),
"write".to_string(),
])
.disallowed_tools(vec!["write".to_string()])
.build();
assert!(is_allowed(
handler.can_use_tool("bash", &Value::Null).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("read", &Value::Null).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("write", &Value::Null).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("grep", &Value::Null).await.unwrap()
));
}
#[tokio::test]
async fn test_tool_input_parameter_ignored() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.build();
let complex_input = serde_json::json!({
"command": "rm -rf /",
"dangerous": true
});
assert!(is_allowed(
handler.can_use_tool("bash", &complex_input).await.unwrap()
));
}
#[tokio::test]
async fn test_realistic_read_only_policy() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Deny)
.allowed_tools(vec![
"read".to_string(),
"glob".to_string(),
"grep".to_string(),
])
.build();
assert!(is_allowed(
handler.can_use_tool("read", &json!({})).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("glob", &json!({})).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("grep", &json!({})).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("write", &json!({})).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("edit", &json!({})).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("bash", &json!({})).await.unwrap()
));
}
#[tokio::test]
async fn test_safe_tools_policy() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.disallowed_tools(vec![
"bash".to_string(),
"write".to_string(),
"delete".to_string(),
])
.build();
assert!(is_allowed(
handler.can_use_tool("read", &json!({})).await.unwrap()
));
assert!(is_allowed(
handler.can_use_tool("grep", &json!({})).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("bash", &json!({})).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("write", &json!({})).await.unwrap()
));
assert!(is_denied(
handler.can_use_tool("delete", &json!({})).await.unwrap()
));
}
#[tokio::test]
async fn test_can_use_tool_trait() {
let handler: Box<dyn CanUseToolHandler> = Box::new(
DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.build(),
);
let result = handler.can_use_tool("bash", &json!({})).await;
assert!(result.is_ok());
assert!(result.unwrap().is_allowed());
}
#[tokio::test]
async fn test_permission_decision_updated_input_is_none_by_default() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Allow)
.build();
let decision = handler.can_use_tool("bash", &json!({})).await.unwrap();
assert!(decision.updated_input().is_none());
}
#[tokio::test]
async fn test_permission_decision_deny_has_no_interrupt_by_default() {
let handler = DefaultPermissionHandler::builder()
.mode(PermissionMode::Deny)
.build();
let decision = handler.can_use_tool("bash", &json!({})).await.unwrap();
match decision {
PermissionDecision::Deny { interrupt } => assert!(!interrupt),
PermissionDecision::Allow { .. } => panic!("Expected Deny"),
}
}
}