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![],
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 && p.matches(path)
167 {
168 return true;
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 && p.matches(path)
179 {
180 return true;
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"));
257 assert!(!perms.is_path_allowed("/tmp/evil"));
258 let mut perms = AgentPermissions::for_new_agent("test");
260 perms.allow_path("/workspace/**");
261 assert!(perms.is_path_allowed("/workspace/src/main.rs"));
262 assert!(perms.is_path_allowed("/workspace/README.md"));
263 }
264
265 #[test]
266 fn test_for_new_agent_sets_name() {
267 let perms = AgentPermissions::for_new_agent("test-agent");
268 assert_eq!(perms.agent_name, "test-agent");
269 assert!(perms.allowed_tools.contains("read"));
270 }
271
272 #[test]
273 fn test_allow_and_deny_tool() {
274 let mut perms = AgentPermissions::for_new_agent("a");
275 perms.allow_tool("custom_tool");
276 assert!(perms.allowed_tools.contains("custom_tool"));
277
278 perms.deny_tool("bash");
279 assert!(!perms.allowed_tools.contains("bash"));
280
281 perms.deny_tool("nonexistent");
283 }
284
285 #[test]
286 fn test_allow_and_deny_path_deduplication() {
287 let mut perms = AgentPermissions::for_new_agent("a");
288 perms.allow_path("/data/**");
289 perms.allow_path("/data/**"); assert_eq!(
291 perms
292 .allowed_paths
293 .iter()
294 .filter(|p| **p == "/data/**")
295 .count(),
296 1
297 );
298
299 perms.deny_path("/secret/**");
300 perms.deny_path("/secret/**"); assert_eq!(
302 perms
303 .denied_paths
304 .iter()
305 .filter(|p| **p == "/secret/**")
306 .count(),
307 1
308 );
309 }
310
311 #[test]
312 fn test_enable_network_and_forking() {
313 let mut perms = AgentPermissions::for_new_agent("a");
314 assert!(!perms.network_access);
315 assert!(!perms.can_fork);
316
317 perms.enable_network();
318 assert!(perms.network_access);
319
320 perms.enable_forking();
321 assert!(perms.can_fork);
322 }
323
324 #[test]
325 fn test_denied_overrides_allowed() {
326 let mut perms = AgentPermissions::for_new_agent("a");
327 perms.allowed_paths = vec!["/workspace/**".to_string()];
328 perms.denied_paths = vec!["/workspace/secret/**".to_string()];
329
330 assert!(perms.is_path_allowed("/workspace/secret/key.pem"));
331 assert!(perms.is_path_denied("/workspace/secret/key.pem"));
332 }
334
335 #[test]
336 fn test_invalid_glob_pattern() {
337 let mut perms = AgentPermissions::for_new_agent("a");
338 perms.allowed_paths = vec!["[invalid".to_string()];
339 assert!(!perms.is_path_allowed("/anything"));
341 }
342
343 #[test]
344 fn test_permission_update_partial() {
345 let mut perms = AgentPermissions::for_new_agent("a");
346 let original_tools = perms.allowed_tools.clone();
347
348 let update = PermissionUpdate {
349 network_access: Some(true),
350 max_execution_time_secs: Some(600),
351 ..Default::default()
352 };
353 update.apply(&mut perms);
354
355 assert!(perms.network_access);
356 assert_eq!(perms.max_execution_time_secs, 600);
357 assert_eq!(perms.allowed_tools, original_tools);
359 assert!(!perms.can_fork);
360 }
361
362 #[test]
363 fn test_permission_update_full_replace() {
364 let mut perms = AgentPermissions::for_new_agent("a");
365
366 let update = PermissionUpdate {
367 allowed_tools: Some(HashSet::from(["read".to_string()])),
368 allowed_paths: Some(vec!["/safe/**".to_string()]),
369 denied_paths: Some(vec![]),
370 network_access: Some(true),
371 max_execution_time_secs: Some(0),
372 max_memory_mb: Some(1024),
373 can_fork: Some(true),
374 };
375 update.apply(&mut perms);
376
377 assert_eq!(perms.allowed_tools.len(), 1);
378 assert!(perms.allowed_tools.contains("read"));
379 assert_eq!(perms.allowed_paths, vec!["/safe/**"]);
380 assert!(perms.denied_paths.is_empty());
381 assert!(perms.network_access);
382 assert!(perms.can_fork);
383 assert_eq!(perms.max_memory_mb, 1024);
384 }
385
386 #[test]
387 fn test_audit_entry_new_allowed() {
388 let entry = AuditEntry::new("agent-1", "use_tool", "bash", true, None);
389 assert_eq!(entry.agent_name, "agent-1");
390 assert_eq!(entry.action, "use_tool");
391 assert_eq!(entry.resource, "bash");
392 assert!(entry.allowed);
393 assert!(entry.reason.is_none());
394 }
395
396 #[test]
397 fn test_audit_entry_new_denied_with_reason() {
398 let entry = AuditEntry::new(
399 "rogue-agent",
400 "access_path",
401 "/etc/shadow",
402 false,
403 Some("path not in allowed list".to_string()),
404 );
405 assert!(!entry.allowed);
406 assert_eq!(entry.reason.as_deref(), Some("path not in allowed list"));
407 }
408
409 #[test]
410 fn test_permissions_serialization_roundtrip() {
411 let mut perms = AgentPermissions::for_new_agent("serializer");
412 perms.enable_network();
413 perms.allow_tool("curl");
414
415 let json = serde_json::to_string(&perms).unwrap();
416 let restored: AgentPermissions = serde_json::from_str(&json).unwrap();
417 assert_eq!(restored.agent_name, "serializer");
418 assert!(restored.network_access);
419 assert!(restored.allowed_tools.contains("curl"));
420 }
421
422 #[test]
423 fn test_audit_entry_serialization_roundtrip() {
424 let entry = AuditEntry::new(
425 "test",
426 "network_request",
427 "https://example.com",
428 false,
429 Some("network not allowed".to_string()),
430 );
431 let json = serde_json::to_string(&entry).unwrap();
432 let restored: AuditEntry = serde_json::from_str(&json).unwrap();
433 assert_eq!(restored.agent_name, entry.agent_name);
434 assert_eq!(restored.action, entry.action);
435 assert_eq!(restored.allowed, entry.allowed);
436 assert_eq!(restored.reason, entry.reason);
437 }
438
439 #[test]
440 fn test_permission_update_default_is_noop() {
441 let mut perms = AgentPermissions::for_new_agent("a");
442 let snapshot = perms.clone();
443
444 let update = PermissionUpdate::default();
445 update.apply(&mut perms);
446
447 assert_eq!(perms.agent_name, snapshot.agent_name);
448 assert_eq!(perms.allowed_tools, snapshot.allowed_tools);
449 assert_eq!(perms.allowed_paths, snapshot.allowed_paths);
450 assert_eq!(perms.denied_paths, snapshot.denied_paths);
451 assert_eq!(perms.network_access, snapshot.network_access);
452 assert_eq!(
453 perms.max_execution_time_secs,
454 snapshot.max_execution_time_secs
455 );
456 assert_eq!(perms.max_memory_mb, snapshot.max_memory_mb);
457 assert_eq!(perms.can_fork, snapshot.can_fork);
458 }
459}