agent_code_lib/permissions/
tracking.rs1use std::collections::VecDeque;
7
8#[derive(Debug, Clone)]
10pub struct DenialRecord {
11 pub tool_name: String,
13 pub tool_use_id: String,
15 pub reason: String,
17 pub timestamp: String,
19 pub input_summary: String,
21}
22
23pub struct DenialTracker {
25 records: VecDeque<DenialRecord>,
27 max_records: usize,
29 total_denials: usize,
31}
32
33impl DenialTracker {
34 pub fn new(max_records: usize) -> Self {
35 Self {
36 records: VecDeque::new(),
37 max_records,
38 total_denials: 0,
39 }
40 }
41
42 pub fn record(
44 &mut self,
45 tool_name: &str,
46 tool_use_id: &str,
47 reason: &str,
48 input: &serde_json::Value,
49 ) {
50 let summary = summarize_input(tool_name, input);
51
52 self.records.push_back(DenialRecord {
53 tool_name: tool_name.to_string(),
54 tool_use_id: tool_use_id.to_string(),
55 reason: reason.to_string(),
56 timestamp: chrono::Utc::now().to_rfc3339(),
57 input_summary: summary,
58 });
59
60 self.total_denials += 1;
61
62 while self.records.len() > self.max_records {
64 self.records.pop_front();
65 }
66 }
67
68 pub fn denials(&self) -> &VecDeque<DenialRecord> {
70 &self.records
71 }
72
73 pub fn total(&self) -> usize {
75 self.total_denials
76 }
77
78 pub fn clear(&mut self) {
80 self.records.clear();
81 self.total_denials = 0;
82 }
83
84 pub fn denials_for_tool(&self, tool_name: &str) -> Vec<&DenialRecord> {
86 self.records
87 .iter()
88 .filter(|r| r.tool_name == tool_name)
89 .collect()
90 }
91}
92
93fn summarize_input(tool_name: &str, input: &serde_json::Value) -> String {
94 match tool_name {
95 "Bash" => input
96 .get("command")
97 .and_then(|v| v.as_str())
98 .unwrap_or("")
99 .chars()
100 .take(100)
101 .collect(),
102 "FileWrite" | "FileEdit" | "FileRead" => input
103 .get("file_path")
104 .and_then(|v| v.as_str())
105 .unwrap_or("")
106 .to_string(),
107 _ => serde_json::to_string(input)
108 .unwrap_or_default()
109 .chars()
110 .take(100)
111 .collect(),
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_new_tracker() {
121 let t = DenialTracker::new(50);
122 assert_eq!(t.total(), 0);
123 assert!(t.denials().is_empty());
124 }
125
126 #[test]
127 fn test_record_denial() {
128 let mut t = DenialTracker::new(10);
129 t.record(
130 "Bash",
131 "call_1",
132 "too dangerous",
133 &serde_json::json!({"command": "rm -rf /"}),
134 );
135 assert_eq!(t.total(), 1);
136 assert_eq!(t.denials().len(), 1);
137 assert_eq!(t.denials()[0].tool_name, "Bash");
138 }
139
140 #[test]
141 fn test_denials_for_tool() {
142 let mut t = DenialTracker::new(10);
143 t.record("Bash", "c1", "reason", &serde_json::json!({}));
144 t.record("FileWrite", "c2", "reason", &serde_json::json!({}));
145 t.record("Bash", "c3", "reason", &serde_json::json!({}));
146 assert_eq!(t.denials_for_tool("Bash").len(), 2);
147 assert_eq!(t.denials_for_tool("FileWrite").len(), 1);
148 assert_eq!(t.denials_for_tool("Grep").len(), 0);
149 }
150
151 #[test]
152 fn test_bounded_capacity() {
153 let mut t = DenialTracker::new(3);
154 for i in 0..5 {
155 t.record("Bash", &format!("c{i}"), "r", &serde_json::json!({}));
156 }
157 assert_eq!(t.total(), 5); assert_eq!(t.denials().len(), 3); }
160
161 #[test]
162 fn test_clear() {
163 let mut t = DenialTracker::new(10);
164 t.record("Bash", "c1", "r", &serde_json::json!({}));
165 t.clear();
166 assert_eq!(t.total(), 0);
167 assert!(t.denials().is_empty());
168 }
169
170 #[test]
171 fn test_input_summary_bash() {
172 let mut t = DenialTracker::new(10);
173 t.record(
174 "Bash",
175 "c1",
176 "r",
177 &serde_json::json!({"command": "rm -rf /"}),
178 );
179 assert!(t.denials()[0].input_summary.contains("rm -rf"));
180 }
181
182 #[test]
183 fn test_input_summary_file() {
184 let mut t = DenialTracker::new(10);
185 t.record(
186 "FileWrite",
187 "c1",
188 "r",
189 &serde_json::json!({"file_path": "/etc/passwd"}),
190 );
191 assert!(t.denials()[0].input_summary.contains("/etc/passwd"));
192 }
193}