use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub agent_id: String,
pub tool_name: String,
pub params: serde_json::Value,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Verdict {
Allow,
Deny { reason: String, code: DenyCode },
Flag { reason: String },
}
impl Verdict {
#[inline]
#[must_use]
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allow | Self::Flag { .. })
}
#[inline]
#[must_use]
pub fn is_denied(&self) -> bool {
matches!(self, Self::Deny { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum VerdictKind {
Allow,
Deny,
Flag,
}
impl Verdict {
#[inline]
#[must_use]
pub fn kind(&self) -> VerdictKind {
match self {
Self::Allow => VerdictKind::Allow,
Self::Deny { .. } => VerdictKind::Deny,
Self::Flag { .. } => VerdictKind::Flag,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DenyCode {
Unauthorized,
RateLimited,
InjectionDetected,
ToolDisabled,
AnomalyDetected,
ParameterTooLarge,
}
impl DenyCode {
#[inline]
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Unauthorized => "unauthorized",
Self::RateLimited => "rate_limited",
Self::InjectionDetected => "injection_detected",
Self::ToolDisabled => "tool_disabled",
Self::AnomalyDetected => "anomaly_detected",
Self::ParameterTooLarge => "parameter_too_large",
}
}
}
impl std::fmt::Display for DenyCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verdict_allow() {
assert!(Verdict::Allow.is_allowed());
assert!(!Verdict::Allow.is_denied());
}
#[test]
fn verdict_deny() {
let v = Verdict::Deny {
reason: "nope".into(),
code: DenyCode::Unauthorized,
};
assert!(v.is_denied());
assert!(!v.is_allowed());
}
#[test]
fn verdict_flag_is_allowed() {
let v = Verdict::Flag {
reason: "suspicious".into(),
};
assert!(v.is_allowed());
assert!(!v.is_denied());
}
#[test]
fn verdict_kind_mapping() {
assert_eq!(Verdict::Allow.kind(), VerdictKind::Allow);
assert_eq!(
Verdict::Deny {
reason: "x".into(),
code: DenyCode::Unauthorized
}
.kind(),
VerdictKind::Deny
);
assert_eq!(
Verdict::Flag { reason: "x".into() }.kind(),
VerdictKind::Flag
);
}
#[test]
fn verdict_serde_roundtrip() {
let verdicts = vec![
Verdict::Allow,
Verdict::Deny {
reason: "bad".into(),
code: DenyCode::InjectionDetected,
},
Verdict::Flag {
reason: "sus".into(),
},
];
for v in &verdicts {
let json = serde_json::to_string(v).unwrap();
let back: Verdict = serde_json::from_str(&json).unwrap();
assert_eq!(v.is_allowed(), back.is_allowed());
assert_eq!(v.is_denied(), back.is_denied());
}
}
#[test]
fn tool_call_serde_roundtrip() {
let call = ToolCall {
agent_id: "agent-1".into(),
tool_name: "tarang_probe".into(),
params: serde_json::json!({"key": "value"}),
timestamp: chrono::Utc::now(),
};
let json = serde_json::to_string(&call).unwrap();
let back: ToolCall = serde_json::from_str(&json).unwrap();
assert_eq!(call.agent_id, back.agent_id);
assert_eq!(call.tool_name, back.tool_name);
}
#[test]
fn deny_code_all_variants() {
let codes = [
DenyCode::Unauthorized,
DenyCode::RateLimited,
DenyCode::InjectionDetected,
DenyCode::ToolDisabled,
DenyCode::AnomalyDetected,
DenyCode::ParameterTooLarge,
];
for code in &codes {
let json = serde_json::to_string(code).unwrap();
let back: DenyCode = serde_json::from_str(&json).unwrap();
assert_eq!(*code, back);
}
}
}