arbiter_behavior/
classifier.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum OperationType {
12 Read,
13 Write,
14 Delete,
15 Admin,
16}
17
18pub fn classify_operation(method: &str, tool_name: Option<&str>) -> OperationType {
27 match method {
29 "resources/read" | "resources/subscribe" | "completion/complete" => {
30 return OperationType::Read;
31 }
32 _ => {}
33 }
34
35 let tool = match tool_name {
36 Some(t) => t.to_lowercase(),
37 None => return OperationType::Read, };
39
40 if contains_any_token(&tool, &["admin", "manage", "configure", "grant", "revoke"]) {
42 return OperationType::Admin;
43 }
44
45 if contains_any_token(&tool, &["delete", "remove", "drop", "purge"]) {
47 return OperationType::Delete;
48 }
49
50 if contains_any_token(
52 &tool,
53 &[
54 "read",
55 "get",
56 "list",
57 "search",
58 "view",
59 "describe",
60 "fetch",
61 "query",
62 "analyze",
63 "summarize",
64 "report",
65 "calculate",
66 "compute",
67 "check",
68 "inspect",
69 "review",
70 ],
71 ) {
72 return OperationType::Read;
73 }
74
75 OperationType::Write
77}
78
79fn contains_any_token(haystack: &str, needles: &[&str]) -> bool {
83 let lower = haystack.to_lowercase();
84 let tokens: Vec<&str> = lower.split(['_', '-', '.', '/', ' ']).collect();
86 needles.iter().any(|needle| {
87 let lower_needle = needle.to_lowercase();
88 tokens.iter().any(|token| *token == lower_needle)
89 })
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn classify_resource_read_method() {
98 assert_eq!(
99 classify_operation("resources/read", None),
100 OperationType::Read
101 );
102 }
103
104 #[test]
105 fn classify_tool_by_name() {
106 assert_eq!(
107 classify_operation("tools/call", Some("read_file")),
108 OperationType::Read
109 );
110 assert_eq!(
111 classify_operation("tools/call", Some("write_file")),
112 OperationType::Write
113 );
114 assert_eq!(
115 classify_operation("tools/call", Some("delete_resource")),
116 OperationType::Delete
117 );
118 assert_eq!(
119 classify_operation("tools/call", Some("admin_users")),
120 OperationType::Admin
121 );
122 }
123
124 #[test]
125 fn classify_mixed_patterns() {
126 assert_eq!(
128 classify_operation("tools/call", Some("get_user")),
129 OperationType::Read
130 );
131 assert_eq!(
133 classify_operation("tools/call", Some("create_user")),
134 OperationType::Write
135 );
136 assert_eq!(
138 classify_operation("tools/call", Some("list_files")),
139 OperationType::Read
140 );
141 }
142
143 #[test]
144 fn admin_beats_delete() {
145 assert_eq!(
147 classify_operation("tools/call", Some("admin_delete")),
148 OperationType::Admin
149 );
150 }
151
152 #[test]
154 fn classify_novel_tool_names() {
155 let result = classify_operation("tools/call", Some("extract_intelligence"));
157 assert_eq!(result, OperationType::Write);
158
159 let result = classify_operation("tools/call", Some("xread_file"));
162 assert_eq!(result, OperationType::Write);
163
164 let result = classify_operation("tools/call", Some("readonly_report"));
166 assert_eq!(result, OperationType::Read);
167 }
168
169 #[test]
171 fn empty_tool_name() {
172 let result = classify_operation("tools/call", Some(""));
174 assert_eq!(result, OperationType::Write);
175
176 let result = classify_operation("", None);
178 assert_eq!(result, OperationType::Read);
179
180 let result = classify_operation("", Some(""));
182 assert_eq!(result, OperationType::Write);
183 }
184
185 #[test]
187 fn tool_name_with_only_delimiters() {
188 let result = classify_operation("tools/call", Some("___---..."));
189 assert_eq!(result, OperationType::Write);
192 }
193}