use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
use tokio::sync::oneshot;
use async_trait::async_trait;
use serde_json::Value;
use crate::agent::PermissionDecision;
use crate::tools::PermissionHook;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionOutcome {
Allowed,
Denied,
Timeout,
}
pub fn translate(outcome: PermissionOutcome) -> PermissionDecision {
match outcome {
PermissionOutcome::Allowed => PermissionDecision::Allow,
PermissionOutcome::Denied | PermissionOutcome::Timeout => {
PermissionDecision::Deny("permission denied".to_string())
}
}
}
#[derive(Clone, Default)]
pub struct PendingPermissionStore {
inner: Arc<StdMutex<HashMap<String, oneshot::Sender<PermissionOutcome>>>>,
}
impl PendingPermissionStore {
pub fn new() -> Self {
Self {
inner: Arc::new(StdMutex::new(HashMap::new())),
}
}
pub fn insert(&self, id: String, tx: oneshot::Sender<PermissionOutcome>) {
if let Ok(mut guard) = self.inner.lock() {
guard.insert(id, tx);
}
}
pub fn remove(&self, id: &str) -> Option<oneshot::Sender<PermissionOutcome>> {
self.inner
.lock()
.ok()
.and_then(|mut guard| guard.remove(id))
}
#[allow(dead_code)]
pub fn contains(&self, id: &str) -> bool {
self.inner
.lock()
.ok()
.is_some_and(|guard| guard.contains_key(id))
}
}
pub struct AcpPermissionHook {
store: PendingPermissionStore,
session_id: String,
notif_tx: tokio::sync::mpsc::UnboundedSender<Value>,
}
impl AcpPermissionHook {
pub fn new(
store: PendingPermissionStore,
session_id: String,
notif_tx: tokio::sync::mpsc::UnboundedSender<Value>,
) -> Self {
Self {
store,
session_id,
notif_tx,
}
}
}
#[async_trait]
impl PermissionHook for AcpPermissionHook {
async fn check(&self, tool_name: &str, args: &Value) -> PermissionDecision {
let perm_id = uuid::Uuid::new_v4().to_string();
let (tx, rx) = oneshot::channel();
self.store.insert(perm_id.clone(), tx);
let notif = serde_json::json!({
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": self.session_id,
"update": {
"sessionUpdate": "request_permission",
"permissionId": perm_id,
"toolName": tool_name,
"arguments": args,
}
}
});
let _ = self.notif_tx.send(notif);
tokio::time::timeout(Duration::from_secs(30), rx)
.await
.ok()
.and_then(|r| r.ok())
.map(translate)
.unwrap_or(PermissionDecision::Deny("permission timeout".to_string()))
}
}
pub fn handle_request_permission(
id: &Value,
params: Option<&Value>,
store: &PendingPermissionStore,
) -> Value {
let params = match params {
Some(p) => p,
None => {
return serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32602,
"message": "Missing params",
},
});
}
};
let perm_id = match params.get("permissionId").and_then(|v| v.as_str()) {
Some(s) => s,
None => {
return serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32602,
"message": "Missing required field 'permissionId'",
},
});
}
};
let outcome = match params.get("outcome").and_then(|v| v.as_str()) {
Some("allowed") => PermissionOutcome::Allowed,
Some("denied") | Some("rejected") => PermissionOutcome::Denied,
_ => {
return serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32602,
"message": "Invalid or missing 'outcome' field (expected 'allowed' or 'denied')",
},
});
}
};
match store.remove(perm_id) {
Some(tx) => {
let _ = tx.send(outcome);
serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": {},
})
}
None => {
serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32000,
"message": format!("Permission request not found: {perm_id}"),
},
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translate_allowed_to_allow() {
assert_eq!(
translate(PermissionOutcome::Allowed),
PermissionDecision::Allow
);
}
#[test]
fn translate_denied_to_deny() {
assert_eq!(
translate(PermissionOutcome::Denied),
PermissionDecision::Deny("permission denied".to_string())
);
}
#[test]
fn translate_timeout_to_deny() {
assert_eq!(
translate(PermissionOutcome::Timeout),
PermissionDecision::Deny("permission denied".to_string())
);
}
#[test]
fn store_insert_and_remove() {
let store = PendingPermissionStore::new();
let (tx, _rx) = oneshot::channel();
store.insert("perm-1".into(), tx);
assert!(store.contains("perm-1"));
let removed = store.remove("perm-1");
assert!(removed.is_some());
assert!(!store.contains("perm-1"));
}
#[test]
fn store_remove_nonexistent_returns_none() {
let store = PendingPermissionStore::new();
assert!(store.remove("nonexistent").is_none());
}
#[test]
fn store_default_is_empty() {
let store = PendingPermissionStore::default();
assert!(!store.contains("anything"));
}
#[test]
fn handle_permission_missing_params() {
let store = PendingPermissionStore::new();
let result = handle_request_permission(&Value::from(1), None, &store);
assert!(result["error"].is_object());
assert_eq!(result["error"]["code"], -32602);
}
#[test]
fn handle_permission_missing_permission_id() {
let store = PendingPermissionStore::new();
let params = serde_json::json!({"outcome": "allowed"});
let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
assert!(result["error"].is_object());
assert_eq!(result["error"]["code"], -32602);
}
#[test]
fn handle_permission_invalid_outcome() {
let store = PendingPermissionStore::new();
let params = serde_json::json!({
"permissionId": "p1",
"outcome": "maybe"
});
let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
assert!(result["error"].is_object());
assert_eq!(result["error"]["code"], -32602);
}
#[test]
fn handle_permission_nonexistent_id() {
let store = PendingPermissionStore::new();
let params = serde_json::json!({
"permissionId": "nonexistent",
"outcome": "allowed"
});
let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
assert!(result["error"].is_object());
assert_eq!(result["error"]["code"], -32000);
}
#[test]
fn handle_permission_allowed_resolves() {
let store = PendingPermissionStore::new();
let (tx, mut rx) = oneshot::channel();
store.insert("p1".into(), tx);
let params = serde_json::json!({
"permissionId": "p1",
"outcome": "allowed"
});
let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
assert!(result["result"].is_object());
let outcome = rx.try_recv().unwrap();
assert_eq!(outcome, PermissionOutcome::Allowed);
}
#[test]
fn handle_permission_denied_resolves() {
let store = PendingPermissionStore::new();
let (tx, mut rx) = oneshot::channel();
store.insert("p2".into(), tx);
let params = serde_json::json!({
"permissionId": "p2",
"outcome": "denied"
});
let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
assert!(result["result"].is_object());
let outcome = rx.try_recv().unwrap();
assert_eq!(outcome, PermissionOutcome::Denied);
}
}