Skip to main content

t_ron/
gate.rs

1//! Security gate — core types for tool call checking.
2
3use serde::{Deserialize, Serialize};
4
5/// A tool call to be security-checked.
6#[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/// Security verdict for a tool call.
15#[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/// High-level verdict kind (for audit storage without carrying payload).
38#[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/// Reason code for denial.
59#[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    /// Stable label for JSON-RPC error messages and audit details.
72    #[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}