1pub mod audit;
19pub mod gate;
20pub mod middleware;
21pub mod pattern;
22pub mod policy;
23pub mod query;
24pub mod rate;
25pub mod scanner;
26pub mod score;
27pub mod tools;
28
29mod error;
30pub use error::TRonError;
31
32use std::sync::Arc;
33
34pub struct TRon {
36 policy: Arc<policy::PolicyEngine>,
37 rate_limiter: Arc<rate::RateLimiter>,
38 pattern: Arc<pattern::PatternAnalyzer>,
39 audit: Arc<audit::AuditLogger>,
40 config: TRonConfig,
41}
42
43#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
45pub struct TRonConfig {
46 pub default_unknown_agent: DefaultAction,
48 pub default_unknown_tool: DefaultAction,
50 pub max_param_size_bytes: usize,
52 pub scan_payloads: bool,
54 pub analyze_patterns: bool,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
60pub enum DefaultAction {
61 Allow,
62 Deny,
63 Flag,
64}
65
66impl Default for TRonConfig {
67 fn default() -> Self {
68 Self {
69 default_unknown_agent: DefaultAction::Deny,
70 default_unknown_tool: DefaultAction::Deny,
71 max_param_size_bytes: 65536,
72 scan_payloads: true,
73 analyze_patterns: true,
74 }
75 }
76}
77
78impl TRon {
79 pub fn new(config: TRonConfig) -> Self {
81 Self {
82 policy: Arc::new(policy::PolicyEngine::new()),
83 rate_limiter: Arc::new(rate::RateLimiter::new()),
84 pattern: Arc::new(pattern::PatternAnalyzer::new()),
85 audit: Arc::new(audit::AuditLogger::new()),
86 config,
87 }
88 }
89
90 pub async fn check(&self, call: &gate::ToolCall) -> gate::Verdict {
92 let param_str = call.params.to_string();
94 if param_str.len() > self.config.max_param_size_bytes {
95 let verdict = gate::Verdict::Deny {
96 reason: format!(
97 "parameter size {} exceeds limit {}",
98 param_str.len(),
99 self.config.max_param_size_bytes
100 ),
101 code: gate::DenyCode::ParameterTooLarge,
102 };
103 self.audit.log(call, &verdict).await;
104 return verdict;
105 }
106
107 match self.policy.check(&call.agent_id, &call.tool_name) {
109 policy::PolicyResult::Allow => {}
110 policy::PolicyResult::Deny(reason) => {
111 let verdict = gate::Verdict::Deny {
112 reason,
113 code: gate::DenyCode::Unauthorized,
114 };
115 self.audit.log(call, &verdict).await;
116 return verdict;
117 }
118 policy::PolicyResult::UnknownAgent => {
119 if let Some(v) = default_action_verdict(
120 self.config.default_unknown_agent,
121 "unknown agent".to_string(),
122 ) {
123 self.audit.log(call, &v).await;
124 return v;
125 }
126 }
127 policy::PolicyResult::UnknownTool => {
128 if let Some(v) = default_action_verdict(
129 self.config.default_unknown_tool,
130 format!(
131 "tool '{}' not in policy for agent '{}'",
132 call.tool_name, call.agent_id
133 ),
134 ) {
135 self.audit.log(call, &v).await;
136 return v;
137 }
138 }
139 }
140
141 if !self.rate_limiter.check(&call.agent_id, &call.tool_name) {
143 let verdict = gate::Verdict::Deny {
144 reason: "rate limit exceeded".to_string(),
145 code: gate::DenyCode::RateLimited,
146 };
147 self.audit.log(call, &verdict).await;
148 return verdict;
149 }
150
151 if self.config.scan_payloads
153 && let Some(threat) = scanner::scan(&call.params)
154 {
155 let verdict = gate::Verdict::Deny {
156 reason: format!("injection detected: {threat}"),
157 code: gate::DenyCode::InjectionDetected,
158 };
159 self.audit.log(call, &verdict).await;
160 return verdict;
161 }
162
163 if self.config.analyze_patterns {
165 self.pattern.record(call);
166 if let Some(anomaly) = self.pattern.check_anomaly(&call.agent_id) {
167 let verdict = gate::Verdict::Flag {
168 reason: format!("anomalous pattern: {anomaly}"),
169 };
170 self.audit.log(call, &verdict).await;
171 return verdict;
172 }
173 }
174
175 let verdict = gate::Verdict::Allow;
177 self.audit.log(call, &verdict).await;
178 verdict
179 }
180
181 pub fn load_policy(&self, toml_str: &str) -> Result<(), TRonError> {
183 self.policy.load_toml(toml_str)
184 }
185
186 pub fn query(&self) -> query::TRonQuery {
188 query::TRonQuery {
189 audit: self.audit.clone(),
190 }
191 }
192
193 pub fn policy_arc(&self) -> Arc<policy::PolicyEngine> {
195 self.policy.clone()
196 }
197}
198
199fn default_action_verdict(action: DefaultAction, reason: String) -> Option<gate::Verdict> {
201 match action {
202 DefaultAction::Deny => Some(gate::Verdict::Deny {
203 reason,
204 code: gate::DenyCode::Unauthorized,
205 }),
206 DefaultAction::Flag => Some(gate::Verdict::Flag { reason }),
207 DefaultAction::Allow => None,
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn default_config() {
217 let config = TRonConfig::default();
218 assert_eq!(config.default_unknown_agent, DefaultAction::Deny);
219 assert_eq!(config.max_param_size_bytes, 65536);
220 assert!(config.scan_payloads);
221 }
222
223 #[tokio::test]
224 async fn deny_unknown_agent() {
225 let tron = TRon::new(TRonConfig::default());
226 let call = gate::ToolCall {
227 agent_id: "unknown-agent".to_string(),
228 tool_name: "some_tool".to_string(),
229 params: serde_json::json!({}),
230 timestamp: chrono::Utc::now(),
231 };
232 let verdict = tron.check(&call).await;
233 assert!(matches!(verdict, gate::Verdict::Deny { .. }));
234 }
235
236 #[tokio::test]
237 async fn deny_oversized_params() {
238 let config = TRonConfig {
239 max_param_size_bytes: 10,
240 default_unknown_agent: DefaultAction::Allow,
241 ..Default::default()
242 };
243 let tron = TRon::new(config);
244 let call = gate::ToolCall {
245 agent_id: "agent".to_string(),
246 tool_name: "tool".to_string(),
247 params: serde_json::json!({"data": "this is way more than 10 bytes of parameter data"}),
248 timestamp: chrono::Utc::now(),
249 };
250 let verdict = tron.check(&call).await;
251 assert!(matches!(
252 verdict,
253 gate::Verdict::Deny {
254 code: gate::DenyCode::ParameterTooLarge,
255 ..
256 }
257 ));
258 }
259
260 #[tokio::test]
261 async fn allow_known_agent_known_tool() {
262 let tron = TRon::new(TRonConfig::default());
263 tron.load_policy(
264 r#"
265[agent."web-agent"]
266allow = ["tarang_*"]
267"#,
268 )
269 .unwrap();
270 let call = gate::ToolCall {
271 agent_id: "web-agent".to_string(),
272 tool_name: "tarang_probe".to_string(),
273 params: serde_json::json!({"path": "/test"}),
274 timestamp: chrono::Utc::now(),
275 };
276 let verdict = tron.check(&call).await;
277 assert!(verdict.is_allowed());
278 assert!(!verdict.is_denied());
279 }
280
281 #[tokio::test]
282 async fn flag_unknown_agent() {
283 let config = TRonConfig {
284 default_unknown_agent: DefaultAction::Flag,
285 ..Default::default()
286 };
287 let tron = TRon::new(config);
288 let call = gate::ToolCall {
289 agent_id: "mystery".to_string(),
290 tool_name: "tool".to_string(),
291 params: serde_json::json!({}),
292 timestamp: chrono::Utc::now(),
293 };
294 let verdict = tron.check(&call).await;
295 assert!(matches!(verdict, gate::Verdict::Flag { .. }));
296 assert!(verdict.is_allowed()); }
298
299 #[tokio::test]
300 async fn deny_unknown_tool_for_known_agent() {
301 let tron = TRon::new(TRonConfig::default());
302 tron.load_policy(
303 r#"
304[agent."limited"]
305allow = ["tarang_*"]
306"#,
307 )
308 .unwrap();
309 let call = gate::ToolCall {
310 agent_id: "limited".to_string(),
311 tool_name: "aegis_scan".to_string(), params: serde_json::json!({}),
313 timestamp: chrono::Utc::now(),
314 };
315 let verdict = tron.check(&call).await;
316 assert!(verdict.is_denied());
317 }
318
319 #[tokio::test]
320 async fn flag_unknown_tool() {
321 let config = TRonConfig {
322 default_unknown_tool: DefaultAction::Flag,
323 ..Default::default()
324 };
325 let tron = TRon::new(config);
326 tron.load_policy(
327 r#"
328[agent."agent-1"]
329allow = ["tarang_*"]
330"#,
331 )
332 .unwrap();
333 let call = gate::ToolCall {
334 agent_id: "agent-1".to_string(),
335 tool_name: "rasa_edit".to_string(),
336 params: serde_json::json!({}),
337 timestamp: chrono::Utc::now(),
338 };
339 let verdict = tron.check(&call).await;
340 assert!(matches!(verdict, gate::Verdict::Flag { .. }));
341 }
342
343 #[tokio::test]
344 async fn allow_unknown_agent_passthrough() {
345 let config = TRonConfig {
346 default_unknown_agent: DefaultAction::Allow,
347 default_unknown_tool: DefaultAction::Allow,
348 ..Default::default()
349 };
350 let tron = TRon::new(config);
351 let call = gate::ToolCall {
352 agent_id: "whoever".to_string(),
353 tool_name: "whatever".to_string(),
354 params: serde_json::json!({"safe": true}),
355 timestamp: chrono::Utc::now(),
356 };
357 let verdict = tron.check(&call).await;
358 assert!(verdict.is_allowed());
359 }
360
361 #[tokio::test]
362 async fn deny_injection_through_pipeline() {
363 let config = TRonConfig {
364 default_unknown_agent: DefaultAction::Allow,
365 default_unknown_tool: DefaultAction::Allow,
366 ..Default::default()
367 };
368 let tron = TRon::new(config);
369 let call = gate::ToolCall {
370 agent_id: "agent".to_string(),
371 tool_name: "tool".to_string(),
372 params: serde_json::json!({"q": "1 UNION SELECT * FROM passwords"}),
373 timestamp: chrono::Utc::now(),
374 };
375 let verdict = tron.check(&call).await;
376 assert!(matches!(
377 verdict,
378 gate::Verdict::Deny {
379 code: gate::DenyCode::InjectionDetected,
380 ..
381 }
382 ));
383 }
384
385 #[tokio::test]
386 async fn scan_payloads_disabled_bypass() {
387 let config = TRonConfig {
388 default_unknown_agent: DefaultAction::Allow,
389 default_unknown_tool: DefaultAction::Allow,
390 scan_payloads: false,
391 ..Default::default()
392 };
393 let tron = TRon::new(config);
394 let call = gate::ToolCall {
395 agent_id: "agent".to_string(),
396 tool_name: "tool".to_string(),
397 params: serde_json::json!({"q": "1 UNION SELECT * FROM passwords"}),
398 timestamp: chrono::Utc::now(),
399 };
400 let verdict = tron.check(&call).await;
402 assert!(verdict.is_allowed());
403 }
404
405 #[tokio::test]
406 async fn analyze_patterns_disabled_bypass() {
407 let config = TRonConfig {
408 default_unknown_agent: DefaultAction::Allow,
409 default_unknown_tool: DefaultAction::Allow,
410 analyze_patterns: false,
411 ..Default::default()
412 };
413 let tron = TRon::new(config);
414 for i in 0..20 {
416 let call = gate::ToolCall {
417 agent_id: "agent".to_string(),
418 tool_name: format!("tool_{i}"),
419 params: serde_json::json!({}),
420 timestamp: chrono::Utc::now(),
421 };
422 let verdict = tron.check(&call).await;
423 assert!(verdict.is_allowed());
424 }
425 }
426
427 #[tokio::test]
428 async fn rate_limit_through_pipeline() {
429 let config = TRonConfig {
430 default_unknown_agent: DefaultAction::Allow,
431 default_unknown_tool: DefaultAction::Allow,
432 scan_payloads: false,
433 analyze_patterns: false,
434 ..Default::default()
435 };
436 let tron = TRon::new(config);
437 let call = gate::ToolCall {
438 agent_id: "agent".to_string(),
439 tool_name: "tool".to_string(),
440 params: serde_json::json!({}),
441 timestamp: chrono::Utc::now(),
442 };
443 for _ in 0..60 {
444 let v = tron.check(&call).await;
445 assert!(v.is_allowed());
446 }
447 let v = tron.check(&call).await;
449 assert!(matches!(
450 v,
451 gate::Verdict::Deny {
452 code: gate::DenyCode::RateLimited,
453 ..
454 }
455 ));
456 }
457
458 #[tokio::test]
459 async fn policy_deny_through_pipeline() {
460 let tron = TRon::new(TRonConfig::default());
461 tron.load_policy(
462 r#"
463[agent."restricted"]
464allow = ["tarang_*"]
465deny = ["tarang_delete"]
466"#,
467 )
468 .unwrap();
469 let call = gate::ToolCall {
470 agent_id: "restricted".to_string(),
471 tool_name: "tarang_delete".to_string(),
472 params: serde_json::json!({}),
473 timestamp: chrono::Utc::now(),
474 };
475 let verdict = tron.check(&call).await;
476 assert!(verdict.is_denied());
477 }
478
479 #[tokio::test]
480 async fn load_policy_error() {
481 let tron = TRon::new(TRonConfig::default());
482 assert!(tron.load_policy("not valid toml {{{").is_err());
483 }
484
485 #[tokio::test]
486 async fn audit_logged_for_every_verdict() {
487 let config = TRonConfig {
488 default_unknown_agent: DefaultAction::Allow,
489 default_unknown_tool: DefaultAction::Allow,
490 scan_payloads: false,
491 analyze_patterns: false,
492 ..Default::default()
493 };
494 let tron = TRon::new(config);
495 let call = gate::ToolCall {
496 agent_id: "agent".to_string(),
497 tool_name: "tool".to_string(),
498 params: serde_json::json!({}),
499 timestamp: chrono::Utc::now(),
500 };
501 tron.check(&call).await;
502 tron.check(&call).await;
503
504 let query = tron.query();
505 assert_eq!(query.total_events().await, 2);
506 }
507}