use std::io::{BufRead, Write};
use std::sync::Mutex;
use async_trait::async_trait;
use pi_agent::{PermissionDecision, PermissionPolicy};
use serde_json::Value;
#[allow(dead_code)] pub enum Mode {
Yolo,
Interactive,
DenyAll,
}
pub struct CliPermission {
mode: Mode,
allowed_session: Mutex<std::collections::HashSet<String>>,
}
impl CliPermission {
pub fn new(mode: Mode) -> Self {
Self {
mode,
allowed_session: Mutex::new(Default::default()),
}
}
}
#[async_trait]
impl PermissionPolicy for CliPermission {
async fn check(&self, tool_name: &str, args: &Value) -> PermissionDecision {
if let Ok(set) = self.allowed_session.lock() {
if set.contains(tool_name) {
return PermissionDecision::Allow;
}
}
match self.mode {
Mode::Yolo => PermissionDecision::Allow,
Mode::DenyAll => PermissionDecision::Deny {
reason: "permissions disabled".into(),
},
Mode::Interactive => prompt(tool_name, args, &self.allowed_session).await,
}
}
}
async fn prompt(
tool_name: &str,
args: &Value,
allowed_session: &Mutex<std::collections::HashSet<String>>,
) -> PermissionDecision {
let tool_name = tool_name.to_string();
let args_pretty = serde_json::to_string_pretty(args).unwrap_or_else(|_| args.to_string());
let allowed_session_clone: Mutex<std::collections::HashSet<String>> = Mutex::new(
allowed_session
.lock()
.map(|s| s.clone())
.unwrap_or_default(),
);
let decision = tokio::task::spawn_blocking(move || {
let mut err = std::io::stderr();
let _ = writeln!(err, "\n⚠ tool call requires permission: {tool_name}");
let _ = writeln!(err, "{args_pretty}");
let _ = write!(err, "Allow? [y]es / [a]llow-session / [n]o: ");
let _ = err.flush();
let mut line = String::new();
let _ = std::io::stdin().lock().read_line(&mut line);
let ans = line.trim().to_lowercase();
match ans.as_str() {
"y" | "yes" | "" => PermissionDecision::Allow,
"a" | "all" | "allow" | "session" => PermissionDecision::AllowSession,
_ => PermissionDecision::Deny {
reason: "user denied".into(),
},
}
})
.await
.unwrap_or(PermissionDecision::Deny {
reason: "permission prompt failed".into(),
});
if decision == PermissionDecision::AllowSession {
if let Ok(mut s) = allowed_session.lock() {
if let Ok(snap) = allowed_session_clone.lock() {
s.extend(snap.iter().cloned());
}
}
}
decision
}