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