use std::time::Duration;
use super::repository::{ApprovalRepository, ApprovalRequest, ApprovalStatus};
const POLL_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub enum ApprovalOutcome {
Approved(Box<ApprovalRequest>),
Denied(Box<ApprovalRequest>),
Expired(Box<ApprovalRequest>),
StillPending(Box<ApprovalRequest>),
}
pub async fn wait_for_decision(
repo: &ApprovalRepository,
call_id: &str,
hold: Duration,
) -> ApprovalOutcome {
let deadline = tokio::time::Instant::now() + hold;
let mut last_seen: Option<ApprovalRequest> = None;
loop {
match repo.find(call_id).await {
Ok(Some(request)) => {
match request.status {
ApprovalStatus::Approved => {
return ApprovalOutcome::Approved(Box::new(request));
},
ApprovalStatus::Denied => {
return ApprovalOutcome::Denied(Box::new(request));
},
ApprovalStatus::Expired => {
return ApprovalOutcome::Expired(Box::new(request));
},
ApprovalStatus::Pending => {
if request.expires_at <= chrono::Utc::now() {
return ApprovalOutcome::Expired(Box::new(request));
}
last_seen = Some(request);
},
}
},
Ok(None) => {
tracing::error!(
call_id,
"approval row vanished while a call was waiting on it; \
treating the call as denied"
);
return last_seen.map_or_else(
|| ApprovalOutcome::Expired(Box::new(missing_placeholder(call_id))),
|r| ApprovalOutcome::Denied(Box::new(r)),
);
},
Err(err) => {
tracing::warn!(
call_id,
error = %err,
"could not read the approval row; retrying within the hold budget"
);
},
}
let now = tokio::time::Instant::now();
if now >= deadline {
return last_seen.map_or_else(
|| ApprovalOutcome::Expired(Box::new(missing_placeholder(call_id))),
|r| ApprovalOutcome::StillPending(Box::new(r)),
);
}
tokio::time::sleep(POLL_INTERVAL.min(deadline - now)).await;
}
}
fn missing_placeholder(call_id: &str) -> ApprovalRequest {
let now = chrono::Utc::now();
ApprovalRequest {
call_id: call_id.to_owned(),
tool_name: String::new(),
server_name: String::new(),
arguments: serde_json::Value::Null,
args_digest: String::new(),
requested_by: String::new(),
session_id: None,
trace_id: None,
rule: String::new(),
status: ApprovalStatus::Expired,
approver_id: None,
approver_username: None,
decided_at: Some(now),
decision_note: Some("approval record unavailable".to_owned()),
expires_at: now,
created_at: now,
}
}