use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
pub struct ApprovalRequest {
pub id: String,
pub tool_name: String,
pub action: ApprovalAction,
pub details: ApprovalDetails,
pub response_tx: oneshot::Sender<ApprovalResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ApprovalAction {
WriteFile {
path: String,
},
EditFile {
path: String,
},
DeleteFile {
path: String,
},
CreateDirectory {
path: String,
},
ExecuteCommand {
command: String,
},
GitModify {
operation: String,
},
NetworkAccess {
domain: String,
},
Other {
description: String,
},
}
impl ApprovalAction {
pub fn description(&self) -> String {
match self {
ApprovalAction::WriteFile { path } => format!("Write file: {}", path),
ApprovalAction::EditFile { path } => format!("Edit file: {}", path),
ApprovalAction::DeleteFile { path } => format!("Delete: {}", path),
ApprovalAction::CreateDirectory { path } => format!("Create directory: {}", path),
ApprovalAction::ExecuteCommand { command } => {
let truncated = if command.len() > 50 {
format!("{}...", &command[..50])
} else {
command.clone()
};
format!("Execute: {}", truncated)
}
ApprovalAction::GitModify { operation } => format!("Git: {}", operation),
ApprovalAction::NetworkAccess { domain } => format!("Network: {}", domain),
ApprovalAction::Other { description } => description.clone(),
}
}
pub fn category(&self) -> &'static str {
match self {
ApprovalAction::WriteFile { .. } => "File Write",
ApprovalAction::EditFile { .. } => "File Edit",
ApprovalAction::DeleteFile { .. } => "Delete",
ApprovalAction::CreateDirectory { .. } => "Create Directory",
ApprovalAction::ExecuteCommand { .. } => "Shell Command",
ApprovalAction::GitModify { .. } => "Git Operation",
ApprovalAction::NetworkAccess { .. } => "Network Access",
ApprovalAction::Other { .. } => "Other",
}
}
pub fn severity(&self) -> ApprovalSeverity {
match self {
ApprovalAction::DeleteFile { .. } => ApprovalSeverity::High,
ApprovalAction::ExecuteCommand { .. } => ApprovalSeverity::High,
ApprovalAction::GitModify { .. } => ApprovalSeverity::Medium,
ApprovalAction::WriteFile { .. } => ApprovalSeverity::Medium,
ApprovalAction::EditFile { .. } => ApprovalSeverity::Medium,
ApprovalAction::CreateDirectory { .. } => ApprovalSeverity::Low,
ApprovalAction::NetworkAccess { .. } => ApprovalSeverity::Low,
ApprovalAction::Other { .. } => ApprovalSeverity::Medium,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApprovalSeverity {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalDetails {
pub tool_description: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApprovalResponse {
Approve,
Deny,
ApproveForSession,
DenyForSession,
}
impl ApprovalResponse {
pub fn is_approved(&self) -> bool {
matches!(
self,
ApprovalResponse::Approve | ApprovalResponse::ApproveForSession
)
}
pub fn is_session_persistent(&self) -> bool {
matches!(
self,
ApprovalResponse::ApproveForSession | ApprovalResponse::DenyForSession
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approval_action_description() {
let action = ApprovalAction::WriteFile {
path: "/tmp/test.txt".to_string(),
};
assert!(action.description().contains("/tmp/test.txt"));
assert_eq!(action.category(), "File Write");
}
#[test]
fn test_approval_response_is_approved() {
assert!(ApprovalResponse::Approve.is_approved());
assert!(ApprovalResponse::ApproveForSession.is_approved());
assert!(!ApprovalResponse::Deny.is_approved());
assert!(!ApprovalResponse::DenyForSession.is_approved());
}
#[test]
fn test_approval_response_is_session_persistent() {
assert!(!ApprovalResponse::Approve.is_session_persistent());
assert!(ApprovalResponse::ApproveForSession.is_session_persistent());
assert!(!ApprovalResponse::Deny.is_session_persistent());
assert!(ApprovalResponse::DenyForSession.is_session_persistent());
}
#[test]
fn test_command_truncation() {
let long_command = "a".repeat(100);
let action = ApprovalAction::ExecuteCommand {
command: long_command,
};
let desc = action.description();
assert!(desc.len() < 70);
assert!(desc.ends_with("..."));
}
#[test]
fn test_severity_levels() {
assert_eq!(
ApprovalAction::DeleteFile { path: "x".into() }.severity(),
ApprovalSeverity::High
);
assert_eq!(
ApprovalAction::WriteFile { path: "x".into() }.severity(),
ApprovalSeverity::Medium
);
assert_eq!(
ApprovalAction::CreateDirectory { path: "x".into() }.severity(),
ApprovalSeverity::Low
);
}
}