1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ToolCall {
8 pub agent_id: String,
9 pub tool_name: String,
10 pub params: serde_json::Value,
11 pub timestamp: chrono::DateTime<chrono::Utc>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum Verdict {
17 Allow,
18 Deny { reason: String, code: DenyCode },
19 Flag { reason: String },
20}
21
22impl Verdict {
23 pub fn is_allowed(&self) -> bool {
24 matches!(self, Self::Allow | Self::Flag { .. })
25 }
26
27 pub fn is_denied(&self) -> bool {
28 matches!(self, Self::Deny { .. })
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34pub enum VerdictKind {
35 Allow,
36 Deny,
37 Flag,
38}
39
40impl Verdict {
41 pub fn kind(&self) -> VerdictKind {
42 match self {
43 Self::Allow => VerdictKind::Allow,
44 Self::Deny { .. } => VerdictKind::Deny,
45 Self::Flag { .. } => VerdictKind::Flag,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52pub enum DenyCode {
53 Unauthorized,
54 RateLimited,
55 InjectionDetected,
56 ToolDisabled,
57 AnomalyDetected,
58 ParameterTooLarge,
59}
60
61impl DenyCode {
62 pub fn as_str(self) -> &'static str {
64 match self {
65 Self::Unauthorized => "unauthorized",
66 Self::RateLimited => "rate_limited",
67 Self::InjectionDetected => "injection_detected",
68 Self::ToolDisabled => "tool_disabled",
69 Self::AnomalyDetected => "anomaly_detected",
70 Self::ParameterTooLarge => "parameter_too_large",
71 }
72 }
73}
74
75impl std::fmt::Display for DenyCode {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.write_str(self.as_str())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn verdict_allow() {
87 assert!(Verdict::Allow.is_allowed());
88 assert!(!Verdict::Allow.is_denied());
89 }
90
91 #[test]
92 fn verdict_deny() {
93 let v = Verdict::Deny {
94 reason: "nope".into(),
95 code: DenyCode::Unauthorized,
96 };
97 assert!(v.is_denied());
98 assert!(!v.is_allowed());
99 }
100
101 #[test]
102 fn verdict_flag_is_allowed() {
103 let v = Verdict::Flag {
104 reason: "suspicious".into(),
105 };
106 assert!(v.is_allowed());
107 assert!(!v.is_denied());
108 }
109
110 #[test]
111 fn verdict_kind_mapping() {
112 assert_eq!(Verdict::Allow.kind(), VerdictKind::Allow);
113 assert_eq!(
114 Verdict::Deny {
115 reason: "x".into(),
116 code: DenyCode::Unauthorized
117 }
118 .kind(),
119 VerdictKind::Deny
120 );
121 assert_eq!(
122 Verdict::Flag { reason: "x".into() }.kind(),
123 VerdictKind::Flag
124 );
125 }
126
127 #[test]
128 fn verdict_serde_roundtrip() {
129 let verdicts = vec![
130 Verdict::Allow,
131 Verdict::Deny {
132 reason: "bad".into(),
133 code: DenyCode::InjectionDetected,
134 },
135 Verdict::Flag {
136 reason: "sus".into(),
137 },
138 ];
139 for v in &verdicts {
140 let json = serde_json::to_string(v).unwrap();
141 let back: Verdict = serde_json::from_str(&json).unwrap();
142 assert_eq!(v.is_allowed(), back.is_allowed());
143 assert_eq!(v.is_denied(), back.is_denied());
144 }
145 }
146
147 #[test]
148 fn tool_call_serde_roundtrip() {
149 let call = ToolCall {
150 agent_id: "agent-1".into(),
151 tool_name: "tarang_probe".into(),
152 params: serde_json::json!({"key": "value"}),
153 timestamp: chrono::Utc::now(),
154 };
155 let json = serde_json::to_string(&call).unwrap();
156 let back: ToolCall = serde_json::from_str(&json).unwrap();
157 assert_eq!(call.agent_id, back.agent_id);
158 assert_eq!(call.tool_name, back.tool_name);
159 }
160
161 #[test]
162 fn deny_code_all_variants() {
163 let codes = [
164 DenyCode::Unauthorized,
165 DenyCode::RateLimited,
166 DenyCode::InjectionDetected,
167 DenyCode::ToolDisabled,
168 DenyCode::AnomalyDetected,
169 DenyCode::ParameterTooLarge,
170 ];
171 for code in &codes {
172 let json = serde_json::to_string(code).unwrap();
173 let back: DenyCode = serde_json::from_str(&json).unwrap();
174 assert_eq!(*code, back);
175 }
176 }
177}