1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum AgentRole {
17 Reader,
19 Writer,
21 Observer,
23 Admin,
25}
26
27impl fmt::Display for AgentRole {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::Reader => write!(f, "reader"),
31 Self::Writer => write!(f, "writer"),
32 Self::Observer => write!(f, "observer"),
33 Self::Admin => write!(f, "admin"),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ToolPermission {
45 pub tool_pattern: String,
47 pub allowed_roles: Vec<AgentRole>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct PermissionManifest {
58 pub agent_id: String,
60 pub role: AgentRole,
62 pub allowed_tools: Vec<String>,
64 pub denied_tools: Vec<String>,
66 pub generated_at: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ErgativeGate {
77 permissions: Vec<ToolPermission>,
78}
79
80impl Default for ErgativeGate {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl ErgativeGate {
87 pub fn new() -> Self {
95 let observer_tools = vec!["Read", "Glob", "Grep"];
96 let reader_extras = vec!["WebSearch", "WebFetch"];
97 let writer_extras = vec!["Edit", "Write"];
98
99 let mut permissions = Vec::new();
100
101 for tool in &observer_tools {
103 permissions.push(ToolPermission {
104 tool_pattern: tool.to_string(),
105 allowed_roles: vec![
106 AgentRole::Observer,
107 AgentRole::Reader,
108 AgentRole::Writer,
109 AgentRole::Admin,
110 ],
111 });
112 }
113
114 for tool in &reader_extras {
116 permissions.push(ToolPermission {
117 tool_pattern: tool.to_string(),
118 allowed_roles: vec![AgentRole::Reader, AgentRole::Writer, AgentRole::Admin],
119 });
120 }
121
122 for tool in &writer_extras {
124 permissions.push(ToolPermission {
125 tool_pattern: tool.to_string(),
126 allowed_roles: vec![AgentRole::Writer, AgentRole::Admin],
127 });
128 }
129
130 permissions.push(ToolPermission {
132 tool_pattern: "*".to_string(),
133 allowed_roles: vec![AgentRole::Admin],
134 });
135
136 Self { permissions }
137 }
138
139 fn matches(pattern: &str, tool: &str) -> bool {
144 if pattern == "*" {
145 return true;
146 }
147 if !pattern.contains('*') {
148 return pattern == tool;
149 }
150 let parts: Vec<&str> = pattern.split('*').filter(|p| !p.is_empty()).collect();
151 if parts.is_empty() {
152 return true;
153 }
154 let mut search_from = 0usize;
155 for part in &parts {
156 match tool[search_from..].find(part) {
157 Some(idx) => search_from += idx + part.len(),
158 None => return false,
159 }
160 }
161 if !pattern.starts_with('*') && !tool.starts_with(parts[0]) {
162 return false;
163 }
164 if !pattern.ends_with('*') && !tool.ends_with(parts[parts.len() - 1]) {
165 return false;
166 }
167 true
168 }
169
170 pub fn can_invoke(&self, _agent_id: &str, role: &AgentRole, tool: &str) -> bool {
172 for perm in &self.permissions {
173 if Self::matches(&perm.tool_pattern, tool) && perm.allowed_roles.contains(role) {
174 return true;
175 }
176 }
177 false
178 }
179
180 pub fn filter_tools(&self, role: &AgentRole, tools: &[String]) -> Vec<String> {
182 tools
183 .iter()
184 .filter(|t| self.can_invoke("_filter", role, t))
185 .cloned()
186 .collect()
187 }
188
189 pub fn generate_manifest(&self, agent_id: &str, role: &AgentRole) -> PermissionManifest {
191 let all_known_tools = vec![
192 "Read", "Glob", "Grep", "WebSearch", "WebFetch", "Edit", "Write", "Bash",
193 "NotebookEdit", "EnterWorktree", "ExitWorktree",
194 ];
195
196 let mut allowed = Vec::new();
197 let mut denied = Vec::new();
198
199 for tool in &all_known_tools {
200 if self.can_invoke(agent_id, role, tool) {
201 allowed.push(tool.to_string());
202 } else {
203 denied.push(tool.to_string());
204 }
205 }
206
207 PermissionManifest {
208 agent_id: agent_id.to_string(),
209 role: role.clone(),
210 allowed_tools: allowed,
211 denied_tools: denied,
212 generated_at: chrono::Utc::now().to_rfc3339(),
213 }
214 }
215
216 pub fn audit(&self, agent_id: &str, role: &AgentRole, tool: &str) -> (bool, String) {
218 let allowed = self.can_invoke(agent_id, role, tool);
219 let reason = if allowed {
220 format!(
221 "agent '{}' with role '{}' is permitted to invoke '{}'",
222 agent_id, role, tool
223 )
224 } else {
225 format!(
226 "agent '{}' with role '{}' is denied access to '{}'",
227 agent_id, role, tool
228 )
229 };
230 (allowed, reason)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn default_permissions() {
240 let gate = ErgativeGate::new();
241
242 assert!(gate.can_invoke("obs-1", &AgentRole::Observer, "Read"));
244 assert!(gate.can_invoke("obs-1", &AgentRole::Observer, "Glob"));
245 assert!(!gate.can_invoke("obs-1", &AgentRole::Observer, "Edit"));
246 assert!(!gate.can_invoke("obs-1", &AgentRole::Observer, "WebSearch"));
247
248 assert!(gate.can_invoke("read-1", &AgentRole::Reader, "Read"));
250 assert!(gate.can_invoke("read-1", &AgentRole::Reader, "WebSearch"));
251 assert!(!gate.can_invoke("read-1", &AgentRole::Reader, "Edit"));
252
253 assert!(gate.can_invoke("write-1", &AgentRole::Writer, "Read"));
255 assert!(gate.can_invoke("write-1", &AgentRole::Writer, "Edit"));
256 assert!(gate.can_invoke("write-1", &AgentRole::Writer, "Write"));
257
258 assert!(gate.can_invoke("admin-1", &AgentRole::Admin, "Read"));
260 assert!(gate.can_invoke("admin-1", &AgentRole::Admin, "Edit"));
261 assert!(gate.can_invoke("admin-1", &AgentRole::Admin, "Bash"));
262 assert!(gate.can_invoke("admin-1", &AgentRole::Admin, "SomeCustomTool"));
263 }
264
265 #[test]
266 fn filter_and_manifest() {
267 let gate = ErgativeGate::new();
268 let tools: Vec<String> = vec!["Read", "Edit", "Bash", "Glob"]
269 .into_iter()
270 .map(String::from)
271 .collect();
272
273 let reader_tools = gate.filter_tools(&AgentRole::Reader, &tools);
274 assert!(reader_tools.contains(&"Read".to_string()));
275 assert!(reader_tools.contains(&"Glob".to_string()));
276 assert!(!reader_tools.contains(&"Edit".to_string()));
277 assert!(!reader_tools.contains(&"Bash".to_string()));
278
279 let manifest = gate.generate_manifest("agent-x", &AgentRole::Writer);
280 assert!(manifest.allowed_tools.contains(&"Edit".to_string()));
281 assert!(manifest.denied_tools.contains(&"Bash".to_string()));
282 }
283
284 #[test]
285 fn audit_reporting() {
286 let gate = ErgativeGate::new();
287 let (allowed, reason) = gate.audit("agent-1", &AgentRole::Reader, "Edit");
288 assert!(!allowed);
289 assert!(reason.contains("denied"));
290
291 let (allowed, reason) = gate.audit("agent-1", &AgentRole::Writer, "Edit");
292 assert!(allowed);
293 assert!(reason.contains("permitted"));
294 }
295}