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)]
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/// High-level verdict kind (for audit storage without carrying payload).
33#[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/// Reason code for denial.
51#[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    /// Stable label for JSON-RPC error messages and audit details.
63    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}