use std::fmt;
use std::future::Future;
use std::io::{BufRead, Write};
use std::pin::Pin;
use std::sync::{Arc, Mutex, PoisonError};
use serde_json::Value;
use crate::id::{EffectId, EffectKey};
use crate::kind::EffectKind;
use crate::policy::RiskLevel;
#[derive(Clone, Debug, PartialEq)]
pub struct ApprovalRequest {
pub effect_id: EffectId,
pub key: EffectKey,
pub kind: EffectKind,
pub risk: RiskLevel,
pub input: Option<Value>,
pub requested_by: Option<String>,
}
impl fmt::Display for ApprovalRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
" effect: {} ({:?}, {} risk)",
self.key, self.kind, self.risk
)?;
writeln!(f, " id: {}", self.effect_id)?;
if let Some(by) = &self.requested_by {
writeln!(f, " by: {by}")?;
}
if let Some(input) = &self.input {
writeln!(f, " input: {input}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ApprovalDecision {
Approved {
by: String,
},
Denied {
by: String,
reason: String,
},
Deferred,
}
pub trait ApprovalProvider: Send + Sync + 'static {
fn request(&self, request: ApprovalRequest) -> impl Future<Output = ApprovalDecision> + Send;
}
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub(crate) trait ErasedApproval: Send + Sync + 'static {
fn request_boxed(&self, request: ApprovalRequest) -> BoxFuture<'_, ApprovalDecision>;
}
impl<P: ApprovalProvider> ErasedApproval for P {
fn request_boxed(&self, request: ApprovalRequest) -> BoxFuture<'_, ApprovalDecision> {
Box::pin(self.request(request))
}
}
pub struct CliApproval {
io: Arc<Mutex<CliIo>>,
approver: String,
}
struct CliIo {
input: Box<dyn BufRead + Send>,
output: Box<dyn Write + Send>,
}
impl CliApproval {
pub fn new() -> Self {
let user = std::env::var("USER").unwrap_or_else(|_| "unknown".into());
Self::with_io(std::io::BufReader::new(std::io::stdin()), std::io::stderr())
.approver(format!("cli:{user}"))
}
pub fn with_io(
input: impl BufRead + Send + 'static,
output: impl Write + Send + 'static,
) -> Self {
Self {
io: Arc::new(Mutex::new(CliIo {
input: Box::new(input),
output: Box::new(output),
})),
approver: "cli".into(),
}
}
#[must_use]
pub fn approver(mut self, name: impl Into<String>) -> Self {
self.approver = name.into();
self
}
}
impl Default for CliApproval {
fn default() -> Self {
Self::new()
}
}
impl ApprovalProvider for CliApproval {
async fn request(&self, request: ApprovalRequest) -> ApprovalDecision {
let (io, by) = (Arc::clone(&self.io), self.approver.clone());
tokio::task::spawn_blocking(move || {
let mut io = io.lock().unwrap_or_else(PoisonError::into_inner);
let asked = write!(
io.output,
"agent-effects: approval needed\n{request}Approve? [y/N]: "
)
.and_then(|()| io.output.flush());
if asked.is_err() {
return ApprovalDecision::Deferred;
}
let mut answer = String::new();
match io.input.read_line(&mut answer) {
Ok(0) | Err(_) => ApprovalDecision::Deferred,
Ok(_) => match answer.trim().to_ascii_lowercase().as_str() {
"y" | "yes" => ApprovalDecision::Approved { by },
_ => ApprovalDecision::Denied {
by,
reason: "denied at the command line".into(),
},
},
}
})
.await
.unwrap_or(ApprovalDecision::Deferred)
}
}