Skip to main content

t_ron/
query.rs

1//! Query API — what T.Ron personality in SecureYeoman queries.
2
3use crate::audit::{AuditLogger, SecurityEvent};
4use crate::score::RiskScorer;
5use std::sync::Arc;
6
7/// Query interface for the T.Ron SecureYeoman personality.
8#[derive(Clone)]
9pub struct TRonQuery {
10    pub(crate) audit: Arc<AuditLogger>,
11}
12
13impl TRonQuery {
14    /// Recent security events.
15    pub async fn recent_events(&self, limit: usize) -> Vec<SecurityEvent> {
16        self.audit.recent(limit).await
17    }
18
19    /// Per-agent risk score (0.0 = trusted, 1.0 = hostile).
20    pub async fn agent_risk_score(&self, agent_id: &str) -> f64 {
21        RiskScorer::score(&self.audit, agent_id).await
22    }
23
24    /// Total events logged.
25    pub async fn total_events(&self) -> usize {
26        self.audit.total_count().await
27    }
28
29    /// Total denied calls.
30    pub async fn total_denials(&self) -> usize {
31        self.audit.deny_count().await
32    }
33
34    /// Audit trail for a specific agent.
35    pub async fn agent_audit(&self, agent_id: &str, limit: usize) -> Vec<SecurityEvent> {
36        self.audit.agent_events(agent_id, limit).await
37    }
38
39    /// Verify the libro audit chain integrity (tamper detection).
40    pub fn verify_chain(&self) -> libro::Result<()> {
41        self.audit.verify_chain()
42    }
43
44    /// Structured review/summary of the audit chain.
45    #[must_use]
46    pub fn chain_review(&self) -> libro::ChainReview {
47        self.audit.chain_review()
48    }
49
50    /// Number of entries in the libro chain.
51    #[must_use]
52    pub fn chain_len(&self) -> usize {
53        self.audit.chain_len()
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use crate::{DefaultAction, TRon, TRonConfig};
60
61    fn permissive_config() -> TRonConfig {
62        TRonConfig {
63            default_unknown_agent: DefaultAction::Allow,
64            default_unknown_tool: DefaultAction::Allow,
65            scan_payloads: false,
66            analyze_patterns: false,
67            ..Default::default()
68        }
69    }
70
71    #[tokio::test]
72    async fn query_api_initial_state() {
73        let tron = TRon::new(TRonConfig::default());
74        let query = tron.query();
75        assert_eq!(query.total_events().await, 0);
76        assert_eq!(query.total_denials().await, 0);
77        assert!(query.recent_events(10).await.is_empty());
78    }
79
80    #[tokio::test]
81    async fn query_after_checks() {
82        let tron = TRon::new(permissive_config());
83        let query = tron.query();
84
85        // Run some calls through the pipeline
86        let call = crate::gate::ToolCall {
87            agent_id: "agent-1".to_string(),
88            tool_name: "tarang_probe".to_string(),
89            params: serde_json::json!({}),
90            timestamp: chrono::Utc::now(),
91        };
92        for _ in 0..5 {
93            tron.check(&call).await;
94        }
95
96        assert_eq!(query.total_events().await, 5);
97        assert_eq!(query.total_denials().await, 0);
98
99        let events = query.recent_events(3).await;
100        assert_eq!(events.len(), 3);
101    }
102
103    #[tokio::test]
104    async fn query_risk_score_after_denials() {
105        let tron = TRon::new(TRonConfig::default());
106        let query = tron.query();
107
108        // Unknown agent will be denied
109        let call = crate::gate::ToolCall {
110            agent_id: "bad-agent".to_string(),
111            tool_name: "anything".to_string(),
112            params: serde_json::json!({}),
113            timestamp: chrono::Utc::now(),
114        };
115        for _ in 0..5 {
116            let v = tron.check(&call).await;
117            assert!(v.is_denied());
118        }
119
120        assert_eq!(query.total_denials().await, 5);
121        assert_eq!(query.agent_risk_score("bad-agent").await, 1.0);
122        assert_eq!(query.agent_risk_score("nobody").await, 0.0);
123    }
124
125    #[tokio::test]
126    async fn query_agent_audit_trail() {
127        let tron = TRon::new(TRonConfig::default());
128        let query = tron.query();
129
130        // Generate events for two agents
131        let call_a = crate::gate::ToolCall {
132            agent_id: "agent-a".to_string(),
133            tool_name: "tool".to_string(),
134            params: serde_json::json!({}),
135            timestamp: chrono::Utc::now(),
136        };
137        let call_b = crate::gate::ToolCall {
138            agent_id: "agent-b".to_string(),
139            tool_name: "tool".to_string(),
140            params: serde_json::json!({}),
141            timestamp: chrono::Utc::now(),
142        };
143        for _ in 0..3 {
144            tron.check(&call_a).await;
145        }
146        for _ in 0..7 {
147            tron.check(&call_b).await;
148        }
149
150        let trail_a = query.agent_audit("agent-a", 100).await;
151        let trail_b = query.agent_audit("agent-b", 100).await;
152        assert_eq!(trail_a.len(), 3);
153        assert_eq!(trail_b.len(), 7);
154
155        // Limit works
156        assert_eq!(query.agent_audit("agent-b", 2).await.len(), 2);
157    }
158
159    #[tokio::test]
160    async fn query_chain_verification() {
161        let tron = TRon::new(permissive_config());
162        let query = tron.query();
163
164        let call = crate::gate::ToolCall {
165            agent_id: "agent-1".to_string(),
166            tool_name: "tool".to_string(),
167            params: serde_json::json!({}),
168            timestamp: chrono::Utc::now(),
169        };
170        for _ in 0..10 {
171            tron.check(&call).await;
172        }
173
174        assert!(query.verify_chain().is_ok());
175        assert_eq!(query.chain_len(), 10);
176
177        let review = query.chain_review();
178        assert_eq!(review.entry_count, 10);
179    }
180}