use chrono::{DateTime, Utc};
use sha2::{Digest, Sha256};
use systemprompt_identifiers::{CallId, SessionId, UserId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, sqlx::Type)]
#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum ApprovalStatus {
Pending,
Approved,
Denied,
Expired,
}
impl ApprovalStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Approved => "approved",
Self::Denied => "denied",
Self::Expired => "expired",
}
}
#[must_use]
pub const fn is_resolved(self) -> bool {
!matches!(self, Self::Pending)
}
}
impl std::fmt::Display for ApprovalStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ApprovalRequest {
pub call_id: String,
pub tool_name: String,
pub server_name: String,
pub arguments: serde_json::Value,
pub args_digest: String,
pub requested_by: String,
pub session_id: Option<String>,
pub trace_id: Option<String>,
pub rule: String,
pub status: ApprovalStatus,
pub approver_id: Option<String>,
pub approver_username: Option<String>,
pub decided_at: Option<DateTime<Utc>>,
pub decision_note: Option<String>,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct NewApprovalRequest<'a> {
pub call_id: &'a CallId,
pub tool_name: &'a str,
pub server_name: &'a str,
pub arguments: &'a serde_json::Value,
pub requested_by: &'a UserId,
pub session_id: Option<&'a SessionId>,
pub trace_id: Option<&'a str>,
pub rule: &'a str,
pub expires_in_seconds: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct ApprovalVerdict<'a> {
pub status: ApprovalStatus,
pub approver_id: &'a UserId,
pub approver_username: &'a str,
pub note: Option<&'a str>,
}
#[must_use]
pub fn args_digest(arguments: &serde_json::Value) -> String {
let canonical = canonicalize(arguments);
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
hex(&hasher.finalize())
}
fn hex(bytes: &[u8]) -> String {
const DIGITS: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(DIGITS[usize::from(byte >> 4)]);
out.push(DIGITS[usize::from(byte & 0x0f)]);
}
out
}
fn canonicalize(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let inner = keys
.into_iter()
.map(|k| {
format!(
"{}:{}",
serde_json::to_string(k).unwrap_or_default(),
canonicalize(&map[k])
)
})
.collect::<Vec<_>>()
.join(",");
format!("{{{inner}}}")
},
serde_json::Value::Array(items) => {
let inner = items.iter().map(canonicalize).collect::<Vec<_>>().join(",");
format!("[{inner}]")
},
other => other.to_string(),
}
}