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