oxios_kernel/access_manager/
permissions.rs1use chrono::{DateTime, Utc};
4use glob::Pattern;
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AgentPermissions {
15 pub agent_name: String,
17 #[serde(default)]
19 pub allowed_tools: HashSet<String>,
20 #[serde(default)]
22 pub allowed_paths: Vec<String>,
23 #[serde(default)]
25 pub denied_paths: Vec<String>,
26 #[serde(default)]
28 pub network_access: bool,
29 #[serde(default)]
31 pub max_execution_time_secs: u64,
32 #[serde(default)]
34 pub max_memory_mb: u64,
35 #[serde(default)]
37 pub can_fork: bool,
38}
39
40impl Default for AgentPermissions {
41 fn default() -> Self {
42 Self {
43 agent_name: String::new(),
44 allowed_tools: ["read", "write", "edit", "bash", "grep", "find", "exec"]
47 .iter()
48 .map(|s| s.to_string())
49 .collect(),
50 allowed_paths: vec!["/workspace/**".to_string()],
51 denied_paths: vec![
52 "/etc/**".to_string(),
53 "/root/**".to_string(),
54 "/sys/**".to_string(),
55 "/proc/**".to_string(),
56 ".oxios/**".to_string(),
57 ],
58 network_access: false,
59 max_execution_time_secs: 300,
60 max_memory_mb: 512,
61 can_fork: false,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct PermissionUpdate {
69 #[serde(default)]
71 pub allowed_tools: Option<HashSet<String>>,
72 #[serde(default)]
74 pub allowed_paths: Option<Vec<String>>,
75 #[serde(default)]
77 pub denied_paths: Option<Vec<String>>,
78 #[serde(default)]
80 pub network_access: Option<bool>,
81 #[serde(default)]
83 pub max_execution_time_secs: Option<u64>,
84 #[serde(default)]
86 pub max_memory_mb: Option<u64>,
87 #[serde(default)]
89 pub can_fork: Option<bool>,
90}
91
92impl PermissionUpdate {
93 pub fn apply(&self, perms: &mut AgentPermissions) {
95 if let Some(tools) = &self.allowed_tools {
96 perms.allowed_tools = tools.clone();
97 }
98 if let Some(paths) = &self.allowed_paths {
99 perms.allowed_paths = paths.clone();
100 }
101 if let Some(paths) = &self.denied_paths {
102 perms.denied_paths = paths.clone();
103 }
104 if let Some(v) = self.network_access {
105 perms.network_access = v;
106 }
107 if let Some(v) = self.max_execution_time_secs {
108 perms.max_execution_time_secs = v;
109 }
110 if let Some(v) = self.max_memory_mb {
111 perms.max_memory_mb = v;
112 }
113 if let Some(v) = self.can_fork {
114 perms.can_fork = v;
115 }
116 }
117}
118
119impl AgentPermissions {
120 pub fn for_new_agent(agent_name: &str) -> Self {
122 Self {
123 agent_name: agent_name.to_string(),
124 ..Default::default()
125 }
126 }
127
128 pub fn allow_tool(&mut self, tool: &str) {
130 self.allowed_tools.insert(tool.to_string());
131 }
132
133 pub fn deny_tool(&mut self, tool: &str) {
135 self.allowed_tools.remove(tool);
136 }
137
138 pub fn allow_path(&mut self, path: &str) {
140 if !self.allowed_paths.contains(&path.to_string()) {
141 self.allowed_paths.push(path.to_string());
142 }
143 }
144
145 pub fn deny_path(&mut self, path: &str) {
147 if !self.denied_paths.contains(&path.to_string()) {
148 self.denied_paths.push(path.to_string());
149 }
150 }
151
152 pub fn enable_network(&mut self) {
154 self.network_access = true;
155 }
156
157 pub fn enable_forking(&mut self) {
159 self.can_fork = true;
160 }
161
162 pub(crate) fn is_path_denied(&self, path: &str) -> bool {
164 for pattern in &self.denied_paths {
165 if let Ok(p) = Pattern::new(pattern) {
166 if p.matches(path) {
167 return true;
168 }
169 }
170 }
171 false
172 }
173
174 pub(crate) fn is_path_allowed(&self, path: &str) -> bool {
176 for pattern in &self.allowed_paths {
177 if let Ok(p) = Pattern::new(pattern) {
178 if p.matches(path) {
179 return true;
180 }
181 }
182 }
183 false
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct AuditEntry {
190 pub timestamp: DateTime<Utc>,
192 pub agent_name: String,
194 pub action: String,
196 pub resource: String,
198 pub allowed: bool,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub reason: Option<String>,
203}
204
205impl AuditEntry {
206 pub fn new(
208 agent_name: &str,
209 action: &str,
210 resource: &str,
211 allowed: bool,
212 reason: Option<String>,
213 ) -> Self {
214 Self {
215 timestamp: Utc::now(),
216 agent_name: agent_name.to_string(),
217 action: action.to_string(),
218 resource: resource.to_string(),
219 allowed,
220 reason,
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_default_permissions_has_basic_tools() {
231 let perms = AgentPermissions::default();
232 assert!(perms.allowed_tools.contains("read"));
233 assert!(perms.allowed_tools.contains("write"));
234 assert!(perms.allowed_tools.contains("bash"));
235 assert!(perms.allowed_tools.contains("exec"));
236 assert!(!perms.network_access);
237 assert!(!perms.can_fork);
238 assert_eq!(perms.max_execution_time_secs, 300);
239 assert_eq!(perms.max_memory_mb, 512);
240 }
241
242 #[test]
243 fn test_default_permissions_denies_sensitive_paths() {
244 let perms = AgentPermissions::default();
245 assert!(perms.is_path_denied("/etc/passwd"));
246 assert!(perms.is_path_denied("/root/.ssh/id_rsa"));
247 assert!(perms.is_path_denied("/proc/self/environ"));
248 assert!(perms.is_path_denied("/sys/kernel/addr"));
249 assert!(perms.is_path_denied(".oxios/config.toml"));
250 }
251
252 #[test]
253 fn test_default_permissions_allows_workspace() {
254 let perms = AgentPermissions::default();
255 assert!(perms.is_path_allowed("/workspace/src/main.rs"));
256 assert!(perms.is_path_allowed("/workspace/README.md"));
257 assert!(!perms.is_path_allowed("/tmp/evil"));
258 }
259
260 #[test]
261 fn test_for_new_agent_sets_name() {
262 let perms = AgentPermissions::for_new_agent("test-agent");
263 assert_eq!(perms.agent_name, "test-agent");
264 assert!(perms.allowed_tools.contains("read"));
265 }
266
267 #[test]
268 fn test_allow_and_deny_tool() {
269 let mut perms = AgentPermissions::for_new_agent("a");
270 perms.allow_tool("custom_tool");
271 assert!(perms.allowed_tools.contains("custom_tool"));
272
273 perms.deny_tool("bash");
274 assert!(!perms.allowed_tools.contains("bash"));
275
276 perms.deny_tool("nonexistent");
278 }
279
280 #[test]
281 fn test_allow_and_deny_path_deduplication() {
282 let mut perms = AgentPermissions::for_new_agent("a");
283 perms.allow_path("/data/**");
284 perms.allow_path("/data/**"); assert_eq!(
286 perms
287 .allowed_paths
288 .iter()
289 .filter(|p| **p == "/data/**")
290 .count(),
291 1
292 );
293
294 perms.deny_path("/secret/**");
295 perms.deny_path("/secret/**"); assert_eq!(
297 perms
298 .denied_paths
299 .iter()
300 .filter(|p| **p == "/secret/**")
301 .count(),
302 1
303 );
304 }
305
306 #[test]
307 fn test_enable_network_and_forking() {
308 let mut perms = AgentPermissions::for_new_agent("a");
309 assert!(!perms.network_access);
310 assert!(!perms.can_fork);
311
312 perms.enable_network();
313 assert!(perms.network_access);
314
315 perms.enable_forking();
316 assert!(perms.can_fork);
317 }
318
319 #[test]
320 fn test_denied_overrides_allowed() {
321 let mut perms = AgentPermissions::for_new_agent("a");
322 perms.allowed_paths = vec!["/workspace/**".to_string()];
323 perms.denied_paths = vec!["/workspace/secret/**".to_string()];
324
325 assert!(perms.is_path_allowed("/workspace/secret/key.pem"));
326 assert!(perms.is_path_denied("/workspace/secret/key.pem"));
327 }
329
330 #[test]
331 fn test_invalid_glob_pattern() {
332 let mut perms = AgentPermissions::for_new_agent("a");
333 perms.allowed_paths = vec!["[invalid".to_string()];
334 assert!(!perms.is_path_allowed("/anything"));
336 }
337
338 #[test]
339 fn test_permission_update_partial() {
340 let mut perms = AgentPermissions::for_new_agent("a");
341 let original_tools = perms.allowed_tools.clone();
342
343 let update = PermissionUpdate {
344 network_access: Some(true),
345 max_execution_time_secs: Some(600),
346 ..Default::default()
347 };
348 update.apply(&mut perms);
349
350 assert!(perms.network_access);
351 assert_eq!(perms.max_execution_time_secs, 600);
352 assert_eq!(perms.allowed_tools, original_tools);
354 assert!(!perms.can_fork);
355 }
356
357 #[test]
358 fn test_permission_update_full_replace() {
359 let mut perms = AgentPermissions::for_new_agent("a");
360
361 let update = PermissionUpdate {
362 allowed_tools: Some(HashSet::from(["read".to_string()])),
363 allowed_paths: Some(vec!["/safe/**".to_string()]),
364 denied_paths: Some(vec![]),
365 network_access: Some(true),
366 max_execution_time_secs: Some(0),
367 max_memory_mb: Some(1024),
368 can_fork: Some(true),
369 };
370 update.apply(&mut perms);
371
372 assert_eq!(perms.allowed_tools.len(), 1);
373 assert!(perms.allowed_tools.contains("read"));
374 assert_eq!(perms.allowed_paths, vec!["/safe/**"]);
375 assert!(perms.denied_paths.is_empty());
376 assert!(perms.network_access);
377 assert!(perms.can_fork);
378 assert_eq!(perms.max_memory_mb, 1024);
379 }
380
381 #[test]
382 fn test_audit_entry_new_allowed() {
383 let entry = AuditEntry::new("agent-1", "use_tool", "bash", true, None);
384 assert_eq!(entry.agent_name, "agent-1");
385 assert_eq!(entry.action, "use_tool");
386 assert_eq!(entry.resource, "bash");
387 assert!(entry.allowed);
388 assert!(entry.reason.is_none());
389 }
390
391 #[test]
392 fn test_audit_entry_new_denied_with_reason() {
393 let entry = AuditEntry::new(
394 "rogue-agent",
395 "access_path",
396 "/etc/shadow",
397 false,
398 Some("path not in allowed list".to_string()),
399 );
400 assert!(!entry.allowed);
401 assert_eq!(entry.reason.as_deref(), Some("path not in allowed list"));
402 }
403
404 #[test]
405 fn test_permissions_serialization_roundtrip() {
406 let mut perms = AgentPermissions::for_new_agent("serializer");
407 perms.enable_network();
408 perms.allow_tool("curl");
409
410 let json = serde_json::to_string(&perms).unwrap();
411 let restored: AgentPermissions = serde_json::from_str(&json).unwrap();
412 assert_eq!(restored.agent_name, "serializer");
413 assert!(restored.network_access);
414 assert!(restored.allowed_tools.contains("curl"));
415 }
416
417 #[test]
418 fn test_audit_entry_serialization_roundtrip() {
419 let entry = AuditEntry::new(
420 "test",
421 "network_request",
422 "https://example.com",
423 false,
424 Some("network not allowed".to_string()),
425 );
426 let json = serde_json::to_string(&entry).unwrap();
427 let restored: AuditEntry = serde_json::from_str(&json).unwrap();
428 assert_eq!(restored.agent_name, entry.agent_name);
429 assert_eq!(restored.action, entry.action);
430 assert_eq!(restored.allowed, entry.allowed);
431 assert_eq!(restored.reason, entry.reason);
432 }
433
434 #[test]
435 fn test_permission_update_default_is_noop() {
436 let mut perms = AgentPermissions::for_new_agent("a");
437 let snapshot = perms.clone();
438
439 let update = PermissionUpdate::default();
440 update.apply(&mut perms);
441
442 assert_eq!(perms.agent_name, snapshot.agent_name);
443 assert_eq!(perms.allowed_tools, snapshot.allowed_tools);
444 assert_eq!(perms.allowed_paths, snapshot.allowed_paths);
445 assert_eq!(perms.denied_paths, snapshot.denied_paths);
446 assert_eq!(perms.network_access, snapshot.network_access);
447 assert_eq!(
448 perms.max_execution_time_secs,
449 snapshot.max_execution_time_secs
450 );
451 assert_eq!(perms.max_memory_mb, snapshot.max_memory_mb);
452 assert_eq!(perms.can_fork, snapshot.can_fork);
453 }
454}