1mod permissions;
13mod rbac;
14
15pub use permissions::{AgentPermissions, AuditEntry, PermissionUpdate};
16pub use rbac::{
17 Action, ApprovalStatus, PendingApproval, RbacAuditEntry, RbacManager, RbacPolicy, Role, Subject,
18};
19
20use std::collections::{HashMap, HashSet};
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use crate::types::AgentId;
25
26#[derive(Debug, Clone)]
55pub struct AccessManager {
56 permissions: HashMap<String, AgentPermissions>,
58 audit_log: Vec<AuditEntry>,
60 #[allow(dead_code)]
62 audit_log_path: Option<std::path::PathBuf>,
63 max_audit_entries: usize,
65 pub(crate) rbac: RbacManager,
67 workspace_paths: HashMap<String, PathBuf>,
69 agent_workspaces: HashMap<String, String>,
71 workspace_agents: HashMap<String, HashSet<String>>,
73 audit_sender: Option<tokio::sync::mpsc::Sender<String>>,
75 #[allow(dead_code)]
77 audit_writer_handle: Option<Arc<tokio::task::JoinHandle<()>>>,
78}
79
80impl AccessManager {
81 pub fn new() -> Self {
83 Self {
84 permissions: HashMap::new(),
85 audit_log: Vec::new(),
86 audit_log_path: None,
87 max_audit_entries: 10_000,
88 rbac: RbacManager::new(),
89 workspace_paths: HashMap::new(),
90 agent_workspaces: HashMap::new(),
91 workspace_agents: HashMap::new(),
92 audit_sender: None,
93 audit_writer_handle: None,
94 }
95 }
96
97 pub fn with_max_audit_entries(max_audit_entries: usize) -> Self {
102 Self {
103 permissions: HashMap::new(),
104 audit_log: Vec::new(),
105 audit_log_path: None,
106 max_audit_entries,
107 rbac: RbacManager::new(),
108 workspace_paths: HashMap::new(),
109 agent_workspaces: HashMap::new(),
110 workspace_agents: HashMap::new(),
111 audit_sender: None,
112 audit_writer_handle: None,
113 }
114 }
115
116 pub fn with_audit_log_path(mut self, path: std::path::PathBuf) -> Self {
122 self.audit_log_path = Some(path.clone());
123
124 let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(1000);
126 self.audit_sender = Some(tx);
127
128 if let Ok(handle) = tokio::runtime::Handle::try_current() {
130 let audit_path = path;
131 let audit_handle = handle.spawn(async move {
132 while let Some(line) = rx.recv().await {
133 if let Ok(mut f) = std::fs::OpenOptions::new()
134 .create(true)
135 .append(true)
136 .open(&audit_path)
137 {
138 use std::io::Write;
139 let _ = writeln!(f, "{}", line);
140 }
141 }
142 });
143 self.audit_writer_handle = Some(Arc::new(audit_handle));
144 }
145
146 self
147 }
148
149 pub fn can_use_tool(&mut self, agent_name: &str, tool: &str) -> bool {
153 let allowed = match self.permissions.get(agent_name) {
154 Some(perms) => perms.allowed_tools.contains(tool),
155 None => {
156 tracing::warn!(agent = %agent_name, tool = %tool, "Agent not found in access manager, denying");
157 false
158 }
159 };
160
161 let reason = if allowed {
162 None
163 } else {
164 Some("tool not in allowed set".to_string())
165 };
166
167 self.log_access(agent_name, "use_tool", tool, allowed, reason);
168
169 allowed
170 }
171
172 pub fn can_access_path(&mut self, agent_name: &str, path: &str) -> bool {
180 let allowed = match self.permissions.get(agent_name) {
181 Some(perms) => {
182 if perms.is_path_denied(path) {
184 false
185 } else {
186 perms.is_path_allowed(path)
187 }
188 }
189 None => {
190 tracing::warn!(agent = %agent_name, path = %path, "Agent not found, denying path access");
191 false
192 }
193 };
194
195 let reason = if allowed {
196 None
197 } else {
198 Some("path not in allowed set or is denied".to_string())
199 };
200
201 self.log_access(agent_name, "access_path", path, allowed, reason);
202
203 allowed
204 }
205
206 pub fn can_access_network(&mut self, agent_name: &str) -> bool {
210 let allowed = match self.permissions.get(agent_name) {
211 Some(perms) => perms.network_access,
212 None => false,
213 };
214
215 let reason = if allowed {
216 None
217 } else {
218 Some("network access not enabled".to_string())
219 };
220
221 self.log_access(agent_name, "network_request", "<network>", allowed, reason);
222
223 allowed
224 }
225
226 pub fn can_execute_for(&self, agent_name: &str, duration_secs: u64) -> bool {
231 match self.permissions.get(agent_name) {
232 Some(perms) => {
233 perms.max_execution_time_secs == 0 || duration_secs <= perms.max_execution_time_secs
234 }
235 None => false,
236 }
237 }
238
239 pub fn can_use_memory(&self, agent_name: &str, memory_mb: u64) -> bool {
244 match self.permissions.get(agent_name) {
245 Some(perms) => perms.max_memory_mb == 0 || memory_mb <= perms.max_memory_mb,
246 None => false,
247 }
248 }
249
250 pub fn can_fork(&self, agent_name: &str) -> bool {
252 match self.permissions.get(agent_name) {
253 Some(perms) => perms.can_fork,
254 None => false,
255 }
256 }
257
258 pub fn get_permissions(&self, agent_name: &str) -> Option<&AgentPermissions> {
262 self.permissions.get(agent_name)
263 }
264
265 pub fn get_or_create_permissions(&mut self, agent_name: &str) -> &mut AgentPermissions {
269 self.permissions
270 .entry(agent_name.to_string())
271 .or_insert_with(|| AgentPermissions::for_new_agent(agent_name))
272 }
273
274 pub fn set_permissions(&mut self, permissions: AgentPermissions) {
278 let agent_name = permissions.agent_name.clone();
279 self.permissions.insert(agent_name, permissions);
280 }
281
282 pub fn update_permissions(
287 &mut self,
288 agent_name: &str,
289 update: PermissionUpdate,
290 ) -> anyhow::Result<()> {
291 let perms = self
292 .permissions
293 .entry(agent_name.to_string())
294 .or_insert_with(|| AgentPermissions::for_new_agent(agent_name));
295 update.apply(perms);
296 Ok(())
297 }
298
299 pub fn remove_permissions(&mut self, agent_name: &str) {
303 self.permissions.remove(agent_name);
304 tracing::info!(agent = %agent_name, "Agent permissions removed");
305 }
306
307 pub fn list_agents(&self) -> Vec<String> {
309 self.permissions.keys().cloned().collect()
310 }
311
312 pub fn audit_log(&self) -> &[AuditEntry] {
316 &self.audit_log
317 }
318
319 pub fn audit_log_recent(&self, limit: usize) -> Vec<AuditEntry> {
324 let start = self.audit_log.len().saturating_sub(limit);
325 self.audit_log[start..].to_vec()
326 }
327
328 pub fn audit_log_for_agent(&self, agent_name: &str) -> Vec<AuditEntry> {
330 self.audit_log
331 .iter()
332 .filter(|e| e.agent_name == agent_name)
333 .cloned()
334 .collect()
335 }
336
337 pub fn denied_actions(&self) -> Vec<&AuditEntry> {
339 self.audit_log.iter().filter(|e| !e.allowed).collect()
340 }
341
342 pub fn rbac_manager(&self) -> &RbacManager {
344 &self.rbac
345 }
346
347 pub fn rbac_manager_mut(&mut self) -> &mut RbacManager {
349 &mut self.rbac
350 }
351
352 pub fn register_workspace_path(&mut self, workspace_name: &str, workspace_path: PathBuf) {
362 self.workspace_paths
363 .insert(workspace_name.to_string(), workspace_path);
364 tracing::debug!(workspace = %workspace_name, "Workspace path registered");
365 }
366
367 pub fn assign_workspace(&mut self, agent_name: &str, workspace_name: &str) -> bool {
378 if !self.workspace_paths.contains_key(workspace_name) {
379 tracing::warn!(agent = %agent_name, workspace = %workspace_name, "Cannot assign agent to non-existent workspace");
380 return false;
381 }
382
383 if let Some(prev_workspace) = self.agent_workspaces.get(agent_name) {
385 if let Some(agents) = self.workspace_agents.get_mut(prev_workspace) {
386 agents.remove(agent_name);
387 }
388 }
389
390 self.agent_workspaces
392 .insert(agent_name.to_string(), workspace_name.to_string());
393 self.workspace_agents
394 .entry(workspace_name.to_string())
395 .or_default()
396 .insert(agent_name.to_string());
397
398 tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent assigned to workspace");
399 true
400 }
401
402 pub fn get_workspace_for_agent(&self, agent_name: &str) -> Option<String> {
407 self.agent_workspaces.get(agent_name).cloned()
408 }
409
410 pub fn get_workspace_path(&self, workspace_name: &str) -> Option<&PathBuf> {
415 self.workspace_paths.get(workspace_name)
416 }
417
418 pub fn list_workspaces(&self) -> Vec<String> {
420 self.workspace_paths.keys().cloned().collect()
421 }
422
423 pub fn list_agents_in_workspace(&self, workspace_name: &str) -> Vec<String> {
425 self.workspace_agents
426 .get(workspace_name)
427 .map(|agents| agents.iter().cloned().collect())
428 .unwrap_or_default()
429 }
430
431 pub fn can_access_workspace(&self, agent_name: &str, workspace_name: &str) -> bool {
442 self.agent_workspaces
443 .get(agent_name)
444 .map(|w| w == workspace_name)
445 .unwrap_or(false)
446 }
447
448 pub fn is_path_in_workspace(&self, workspace_name: &str, path: &str) -> bool {
460 let workspace = match self.workspace_paths.get(workspace_name) {
461 Some(w) => w,
462 None => return false,
463 };
464
465 let requested_path = match Path::new(path).canonicalize() {
467 Ok(p) => p,
468 Err(_) => {
469 let candidate = workspace.join(path);
471 match candidate.canonicalize() {
472 Ok(p) => p,
473 Err(_) => return false,
474 }
475 }
476 };
477
478 let workspace_canonical = match workspace.canonicalize() {
480 Ok(w) => w,
481 Err(_) => return false,
482 };
483
484 requested_path.starts_with(&workspace_canonical)
485 }
486
487 pub fn can_access_path_in_workspace(
514 &mut self,
515 agent_id: &AgentId,
516 agent_name: &str,
517 path: &str,
518 workspace: Option<&str>,
519 ) -> bool {
520 let subject = Subject::Agent(*agent_id);
522 let action = Action::AccessPath(path.to_string());
523 let rbac_allowed = self.rbac.check_permission(&subject, &action, path);
524
525 let path_allowed = self.can_access_path(agent_name, path);
527
528 let workspace_allowed = if let Some(workspace_name) = workspace {
530 let is_in_workspace = self.is_path_in_workspace(workspace_name, path);
531
532 if !is_in_workspace {
533 self.log_access(
535 agent_name,
536 "sandbox_violation",
537 path,
538 false,
539 Some(format!(
540 "Path '{}' is outside workspace '{}' boundary",
541 path, workspace_name
542 )),
543 );
544 }
545
546 is_in_workspace
547 } else {
548 if let Some(assigned_workspace) = self.agent_workspaces.get(agent_name) {
550 let is_in_workspace = self.is_path_in_workspace(assigned_workspace, path);
551
552 if !is_in_workspace {
553 self.log_access(
554 agent_name,
555 "sandbox_violation",
556 path,
557 false,
558 Some(format!(
559 "Path '{}' is outside assigned workspace '{}' boundary",
560 path, assigned_workspace
561 )),
562 );
563 }
564
565 is_in_workspace
566 } else {
567 true
569 }
570 };
571
572 rbac_allowed && path_allowed && workspace_allowed
574 }
575
576 pub fn unassign_workspace(&mut self, agent_name: &str) -> Option<String> {
580 if let Some(workspace_name) = self.agent_workspaces.remove(agent_name) {
581 if let Some(agents) = self.workspace_agents.get_mut(&workspace_name) {
582 agents.remove(agent_name);
583 }
584 tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent unassigned from workspace");
585 Some(workspace_name)
586 } else {
587 None
588 }
589 }
590
591 pub fn remove_workspace(&mut self, workspace_name: &str) {
595 if let Some(agents) = self.workspace_agents.remove(workspace_name) {
597 for agent_name in agents {
598 self.agent_workspaces.remove(&agent_name);
599 }
600 }
601
602 self.workspace_paths.remove(workspace_name);
604
605 tracing::info!(workspace = %workspace_name, "Workspace removed from access manager");
606 }
607
608 pub fn clear_audit_log(&mut self) {
610 let count = self.audit_log.len();
611 self.audit_log.clear();
612 tracing::info!(cleared = count, "Audit log cleared");
613 }
614
615 pub(crate) fn log_access(
620 &mut self,
621 agent_name: &str,
622 action: &str,
623 resource: &str,
624 allowed: bool,
625 reason: Option<String>,
626 ) {
627 let entry = AuditEntry::new(agent_name, action, resource, allowed, reason.clone());
628
629 self.audit_log.push(entry.clone());
630
631 if self.audit_log.len() > self.max_audit_entries {
633 let prune_count = self.audit_log.len() - self.max_audit_entries;
634 self.audit_log.drain(0..prune_count);
635 }
636
637 self.persist_audit_entry(&entry);
639
640 if !allowed {
642 tracing::warn!(
643 agent = %agent_name,
644 action = %action,
645 resource = %resource,
646 reason = ?reason,
647 "Access denied"
648 );
649 }
650 }
651
652 fn persist_audit_entry(&self, entry: &AuditEntry) {
658 if self.audit_log_path.is_none() {
659 return;
660 }
661 let line = match serde_json::to_string(entry) {
662 Ok(s) => s,
663 Err(_) => return,
664 };
665 if let Some(sender) = &self.audit_sender {
666 match sender.try_send(line) {
667 Ok(()) => {}
668 Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
669 tracing::warn!("Audit log channel full — dropping entry");
670 }
671 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
672 tracing::warn!("Audit log channel closed — dropping entry");
673 }
674 }
675 }
676 }
677
678 pub fn validate_permissions(&self, perms: &AgentPermissions) -> Vec<String> {
682 let mut warnings = Vec::new();
683
684 if perms.allowed_tools.is_empty() {
685 warnings.push("Agent has no allowed tools".to_string());
686 }
687
688 if perms.allowed_paths.is_empty() {
689 warnings.push("Agent has no path restrictions (wide open)".to_string());
690 }
691
692 if perms.network_access {
693 warnings.push("Agent has network access enabled".to_string());
694 }
695
696 if perms.can_fork {
697 warnings.push("Agent can fork sub-agents".to_string());
698 }
699
700 if perms.max_execution_time_secs == 0 {
701 warnings.push("Agent has no execution time limit".to_string());
702 }
703
704 if perms.max_memory_mb == 0 {
705 warnings.push("Agent has no memory limit".to_string());
706 }
707
708 warnings
709 }
710}
711
712impl Default for AccessManager {
713 fn default() -> Self {
714 Self::new()
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 #[test]
725 fn test_default_permissions() {
726 let perms = AgentPermissions::default();
727 assert!(perms.allowed_tools.contains("bash"));
728 assert!(!perms.network_access);
729 assert!(!perms.can_fork);
730 assert_eq!(perms.max_execution_time_secs, 300);
731 assert_eq!(perms.max_memory_mb, 512);
732 }
733
734 #[test]
735 fn test_for_new_agent() {
736 let perms = AgentPermissions::for_new_agent("my-agent");
737 assert_eq!(perms.agent_name, "my-agent");
738 assert!(perms.allowed_tools.contains("bash"));
739 }
740
741 #[test]
742 fn test_allow_deny_tool() {
743 let mut perms = AgentPermissions::for_new_agent("test");
744 assert!(perms.allowed_tools.contains("bash"));
745
746 perms.deny_tool("bash");
747 assert!(!perms.allowed_tools.contains("bash"));
748
749 perms.allow_tool("custom");
750 assert!(perms.allowed_tools.contains("custom"));
751 }
752
753 #[test]
754 fn test_allow_deny_path() {
755 let mut perms = AgentPermissions::for_new_agent("test");
756
757 perms.allow_path("/workspace/**");
758 assert!(perms.allowed_paths.contains(&"/workspace/**".to_string()));
759
760 perms.deny_path("/workspace/.secret/**");
761 assert!(perms
762 .denied_paths
763 .contains(&"/workspace/.secret/**".to_string()));
764 }
765
766 #[test]
767 fn test_enable_network() {
768 let mut perms = AgentPermissions::for_new_agent("test");
769 assert!(!perms.network_access);
770
771 perms.enable_network();
772 assert!(perms.network_access);
773 }
774
775 #[test]
776 fn test_enable_forking() {
777 let mut perms = AgentPermissions::for_new_agent("test");
778 assert!(!perms.can_fork);
779
780 perms.enable_forking();
781 assert!(perms.can_fork);
782 }
783
784 #[test]
785 fn test_path_matching_allowed() {
786 let mut perms = AgentPermissions::for_new_agent("test");
787 perms.allowed_paths = vec!["/workspace/**".to_string(), "/home/*/docs/**".to_string()];
788 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
789
790 assert!(perms.is_path_allowed("/workspace/project/file.rs"));
792 assert!(perms.is_path_allowed("/home/user/docs/readme.md"));
793
794 assert!(!perms.is_path_allowed("/etc/passwd"));
796 assert!(!perms.is_path_allowed("/home/user/secret.txt"));
797 }
798
799 #[test]
800 fn test_path_matching_denied() {
801 let mut perms = AgentPermissions::for_new_agent("test");
802 perms.allowed_paths = vec!["/workspace/**".to_string()];
803 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
804
805 assert!(perms.is_path_denied("/workspace/.oxios/config.toml"));
807 assert!(!perms.is_path_denied("/workspace/project/file.rs"));
808
809 let _access = AccessManager::new();
811 let mut perms2 = perms.clone();
812 perms2.agent_name = "test".to_string();
813 }
817
818 #[test]
819 fn test_path_denied_pattern_matching() {
820 let mut perms = AgentPermissions::for_new_agent("test");
821 perms.denied_paths = vec!["/etc/**".to_string(), "**/secrets/*".to_string()];
822
823 assert!(perms.is_path_denied("/etc/passwd"));
824 assert!(perms.is_path_denied("/etc/shadow"));
825 assert!(!perms.is_path_denied("/workspace/file"));
826 }
827
828 #[test]
831 fn test_can_use_tool_allowed() {
832 let mut access = AccessManager::new();
833
834 let mut perms = AgentPermissions::for_new_agent("code-agent");
835 perms.allow_tool("bash");
836 perms.allow_tool("read");
837 access.set_permissions(perms);
838
839 assert!(access.can_use_tool("code-agent", "bash"));
840 assert!(access.can_use_tool("code-agent", "read"));
841 }
842
843 #[test]
844 fn test_can_use_tool_denied() {
845 let mut access = AccessManager::new();
846
847 let mut perms = AgentPermissions::for_new_agent("code-agent");
848 perms.allow_tool("read");
849 perms.deny_tool("bash"); access.set_permissions(perms);
851
852 assert!(!access.can_use_tool("code-agent", "bash")); assert!(!access.can_use_tool("code-agent", "spawn")); assert!(!access.can_use_tool("unknown-agent", "bash")); }
856
857 #[test]
858 fn test_unknown_agent_denied_all_tools() {
859 let mut access = AccessManager::new();
860
861 assert!(!access.can_use_tool("unknown-agent", "read"));
863 assert!(!access.can_access_path("unknown-agent", "/workspace/test.txt"));
864 assert!(!access.can_access_network("unknown-agent"));
865 assert!(!access.can_fork("unknown-agent"));
866 }
867
868 #[test]
871 fn test_can_access_path_allowed() {
872 let mut access = AccessManager::new();
873
874 let perms = AgentPermissions::for_new_agent("file-agent");
875 access.set_permissions(perms);
876
877 assert!(access.can_access_path("file-agent", "/workspace/project/file.rs"));
878 assert!(!access.can_access_path("file-agent", "/etc/passwd"));
879 }
880
881 #[test]
882 fn test_can_access_path_denied_takes_precedence() {
883 let mut access = AccessManager::new();
884
885 let mut perms = AgentPermissions::for_new_agent("test");
886 perms.allowed_paths = vec!["/workspace/**".to_string()];
887 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
888 access.set_permissions(perms);
889
890 assert!(!access.can_access_path("test", "/workspace/.oxios/config.toml"));
892
893 assert!(access.can_access_path("test", "/workspace/project/file.rs"));
895 }
896
897 #[test]
900 fn test_can_access_network() {
901 let mut access = AccessManager::new();
902
903 let mut perms = AgentPermissions::for_new_agent("net-agent");
904 perms.enable_network();
905 access.set_permissions(perms);
906
907 assert!(access.can_access_network("net-agent"));
908 assert!(!access.can_access_network("no-net-agent"));
909 }
910
911 #[test]
914 fn test_can_execute_for() {
915 let mut access = AccessManager::new();
916
917 let mut perms = AgentPermissions::for_new_agent("test");
918 perms.max_execution_time_secs = 300;
919 access.set_permissions(perms);
920
921 assert!(access.can_execute_for("test", 100));
922 assert!(access.can_execute_for("test", 300));
923 assert!(!access.can_execute_for("test", 301));
924 }
925
926 #[test]
927 fn test_unlimited_execution_time() {
928 let mut access = AccessManager::new();
929
930 let mut perms = AgentPermissions::for_new_agent("test");
931 perms.max_execution_time_secs = 0; access.set_permissions(perms);
933
934 assert!(access.can_execute_for("test", 100_000));
935 }
936
937 #[test]
938 fn test_can_use_memory() {
939 let mut access = AccessManager::new();
940
941 let mut perms = AgentPermissions::for_new_agent("test");
942 perms.max_memory_mb = 512;
943 access.set_permissions(perms);
944
945 assert!(access.can_use_memory("test", 256));
946 assert!(access.can_use_memory("test", 512));
947 assert!(!access.can_use_memory("test", 513));
948 }
949
950 #[test]
951 fn test_unlimited_memory() {
952 let mut access = AccessManager::new();
953
954 let mut perms = AgentPermissions::for_new_agent("test");
955 perms.max_memory_mb = 0;
956 access.set_permissions(perms);
957
958 assert!(access.can_use_memory("test", 1_000_000));
959 }
960
961 #[test]
964 fn test_can_fork() {
965 let mut access = AccessManager::new();
966
967 let mut perms = AgentPermissions::for_new_agent("test");
968 perms.enable_forking();
969 access.set_permissions(perms);
970
971 assert!(access.can_fork("test"));
972 assert!(!access.can_fork("no-fork-agent"));
973 }
974
975 #[test]
978 fn test_set_and_get_permissions() {
979 let mut access = AccessManager::new();
980
981 let perms = AgentPermissions::for_new_agent("test-agent");
982 access.set_permissions(perms);
983
984 let retrieved = access.get_permissions("test-agent");
985 assert!(retrieved.is_some());
986 assert_eq!(retrieved.unwrap().agent_name, "test-agent");
987 }
988
989 #[test]
990 fn test_get_nonexistent_permissions() {
991 let access = AccessManager::new();
992 assert!(access.get_permissions("ghost").is_none());
993 }
994
995 #[test]
996 fn test_get_or_create_permissions() {
997 let mut access = AccessManager::new();
998
999 let perms = access.get_or_create_permissions("new-agent");
1001 assert_eq!(perms.agent_name, "new-agent");
1002
1003 let perms2 = access.get_or_create_permissions("new-agent");
1005 assert_eq!(perms2.agent_name, "new-agent");
1006 }
1007
1008 #[test]
1009 fn test_remove_permissions() {
1010 let mut access = AccessManager::new();
1011
1012 let perms = AgentPermissions::for_new_agent("to-remove");
1013 access.set_permissions(perms);
1014
1015 assert!(access.get_permissions("to-remove").is_some());
1016
1017 access.remove_permissions("to-remove");
1018
1019 assert!(access.get_permissions("to-remove").is_none());
1020 assert!(!access.can_use_tool("to-remove", "bash"));
1022 }
1023
1024 #[test]
1025 fn test_list_agents() {
1026 let mut access = AccessManager::new();
1027
1028 access.set_permissions(AgentPermissions::for_new_agent("agent-1"));
1029 access.set_permissions(AgentPermissions::for_new_agent("agent-2"));
1030
1031 let agents = access.list_agents();
1032 assert_eq!(agents.len(), 2);
1033 assert!(agents.contains(&"agent-1".to_string()));
1034 assert!(agents.contains(&"agent-2".to_string()));
1035 }
1036
1037 #[test]
1040 fn test_audit_log_records_access() {
1041 let mut access = AccessManager::new();
1042
1043 let perms = AgentPermissions::for_new_agent("test-agent");
1044 access.set_permissions(perms);
1045
1046 access.can_use_tool("test-agent", "bash"); access.can_use_tool("test-agent", "network"); let log = access.audit_log();
1050 assert_eq!(log.len(), 2);
1051 assert!(log[0].allowed);
1052 assert!(!log[1].allowed);
1053 assert_eq!(log[0].agent_name, "test-agent");
1054 assert_eq!(log[0].action, "use_tool");
1055 assert_eq!(log[0].resource, "bash");
1056 }
1057
1058 #[test]
1059 fn test_audit_log_recent() {
1060 let mut access = AccessManager::new();
1061
1062 let perms = AgentPermissions::for_new_agent("test");
1063 access.set_permissions(perms);
1064
1065 for i in 0..10 {
1066 access.can_use_tool("test", &format!("tool-{}", i));
1067 }
1068
1069 let recent = access.audit_log_recent(3);
1070 assert_eq!(recent.len(), 3);
1071 }
1072
1073 #[test]
1074 fn test_audit_log_for_agent() {
1075 let mut access = AccessManager::new();
1076
1077 access.set_permissions(AgentPermissions::for_new_agent("agent-a"));
1078 access.set_permissions(AgentPermissions::for_new_agent("agent-b"));
1079
1080 access.can_use_tool("agent-a", "tool1");
1081 access.can_use_tool("agent-b", "tool2");
1082 access.can_use_tool("agent-a", "tool3");
1083
1084 let log_a = access.audit_log_for_agent("agent-a");
1085 assert_eq!(log_a.len(), 2);
1086 }
1087
1088 #[test]
1089 fn test_denied_actions() {
1090 let mut access = AccessManager::new();
1091
1092 let perms = AgentPermissions::for_new_agent("test");
1093 access.set_permissions(perms);
1094
1095 access.can_use_tool("test", "bash"); access.can_use_tool("test", "dangerous"); access.can_access_path("test", "/etc/shadow"); let denied = access.denied_actions();
1100 assert_eq!(denied.len(), 2);
1101 }
1102
1103 #[test]
1104 fn test_clear_audit_log() {
1105 let mut access = AccessManager::new();
1106
1107 let perms = AgentPermissions::for_new_agent("test");
1108 access.set_permissions(perms);
1109
1110 for _ in 0..5 {
1111 access.can_use_tool("test", "tool");
1112 }
1113
1114 assert_eq!(access.audit_log().len(), 5);
1115
1116 access.clear_audit_log();
1117
1118 assert!(access.audit_log().is_empty());
1119 }
1120
1121 #[test]
1124 fn test_audit_log_prunes_old_entries() {
1125 let mut access = AccessManager::with_max_audit_entries(5);
1126
1127 let perms = AgentPermissions::for_new_agent("test");
1128 access.set_permissions(perms);
1129
1130 for i in 0..10 {
1132 access.can_use_tool("test", &format!("tool-{}", i));
1133 }
1134
1135 assert_eq!(access.audit_log().len(), 5);
1137 }
1138
1139 #[test]
1142 fn test_validate_permissions_no_tools() {
1143 let mut access = AccessManager::new();
1144 let mut perms = AgentPermissions::for_new_agent("test");
1145 perms.allowed_tools.clear();
1146 access.set_permissions(perms.clone());
1147
1148 let warnings = access.validate_permissions(&perms);
1149 assert!(warnings.iter().any(|w| w.contains("no allowed tools")));
1150 }
1151
1152 #[test]
1153 fn test_validate_permissions_no_path_restrictions() {
1154 let mut perms = AgentPermissions::for_new_agent("test");
1155 perms.allowed_paths.clear();
1156
1157 let access = AccessManager::new();
1158 let warnings = access.validate_permissions(&perms);
1159 assert!(warnings.iter().any(|w| w.contains("no path restrictions")));
1160 }
1161
1162 #[test]
1163 fn test_validate_permissions_warnings() {
1164 let mut access = AccessManager::new();
1165 let mut perms = AgentPermissions::for_new_agent("test");
1166 perms.network_access = true;
1167 perms.can_fork = true;
1168 perms.max_execution_time_secs = 0;
1169 perms.max_memory_mb = 0;
1170 access.set_permissions(perms.clone());
1171
1172 let warnings = access.validate_permissions(&perms);
1173 assert!(warnings.iter().any(|w| w.contains("network access")));
1174 assert!(warnings.iter().any(|w| w.contains("fork sub-agents")));
1175 assert!(warnings
1176 .iter()
1177 .any(|w| w.contains("no execution time limit")));
1178 assert!(warnings.iter().any(|w| w.contains("no memory limit")));
1179 }
1180
1181 #[test]
1184 fn test_audit_entry_has_timestamp() {
1185 let entry = AuditEntry::new("agent", "action", "resource", true, None);
1186 assert!(entry.timestamp.timestamp() > 0);
1188 }
1189
1190 #[test]
1193 fn test_register_workspace_path() {
1194 let mut access = AccessManager::new();
1195 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my-workspace"));
1196
1197 assert_eq!(access.list_workspaces(), vec!["my-workspace"]);
1198 assert_eq!(
1199 access.get_workspace_path("my-workspace"),
1200 Some(&PathBuf::from("/workspace/my-workspace"))
1201 );
1202 }
1203
1204 #[test]
1205 fn test_assign_agent_to_workspace() {
1206 let mut access = AccessManager::new();
1207 access.register_workspace_path("project-alpha", PathBuf::from("/workspace/alpha"));
1208
1209 assert!(access.assign_workspace("agent-1", "project-alpha"));
1211
1212 assert_eq!(
1214 access.get_workspace_for_agent("agent-1"),
1215 Some("project-alpha".to_string())
1216 );
1217 assert!(access.can_access_workspace("agent-1", "project-alpha"));
1218 assert!(!access.can_access_workspace("agent-1", "other-workspace"));
1219 }
1220
1221 #[test]
1222 fn test_assign_agent_to_nonexistent_workspace_fails() {
1223 let mut access = AccessManager::new();
1224
1225 assert!(!access.assign_workspace("agent-1", "nonexistent"));
1227 assert_eq!(access.get_workspace_for_agent("agent-1"), None);
1228 }
1229
1230 #[test]
1231 fn test_reassign_agent_to_different_workspace() {
1232 let mut access = AccessManager::new();
1233 access.register_workspace_path("workspace-a", PathBuf::from("/workspace/a"));
1234 access.register_workspace_path("workspace-b", PathBuf::from("/workspace/b"));
1235
1236 access.assign_workspace("agent-1", "workspace-a");
1238 assert_eq!(
1239 access.get_workspace_for_agent("agent-1"),
1240 Some("workspace-a".to_string())
1241 );
1242
1243 access.assign_workspace("agent-1", "workspace-b");
1245 assert_eq!(
1246 access.get_workspace_for_agent("agent-1"),
1247 Some("workspace-b".to_string())
1248 );
1249
1250 assert!(!access.can_access_workspace("agent-1", "workspace-a"));
1252 }
1253
1254 #[test]
1255 fn test_unassign_agent_from_workspace() {
1256 let mut access = AccessManager::new();
1257 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1258
1259 access.assign_workspace("agent-1", "my-workspace");
1260 assert!(access.get_workspace_for_agent("agent-1").is_some());
1261
1262 let removed = access.unassign_workspace("agent-1");
1263 assert_eq!(removed, Some("my-workspace".to_string()));
1264 assert!(access.get_workspace_for_agent("agent-1").is_none());
1265 }
1266
1267 #[test]
1268 fn test_list_agents_in_workspace() {
1269 let mut access = AccessManager::new();
1270 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1271
1272 access.assign_workspace("agent-1", "my-workspace");
1273 access.assign_workspace("agent-2", "my-workspace");
1274 access.assign_workspace("agent-3", "other-workspace");
1275
1276 let agents = access.list_agents_in_workspace("my-workspace");
1277 assert_eq!(agents.len(), 2);
1278 assert!(agents.contains(&"agent-1".to_string()));
1279 assert!(agents.contains(&"agent-2".to_string()));
1280 assert!(!agents.contains(&"agent-3".to_string()));
1281 }
1282
1283 #[test]
1284 fn test_remove_workspace_unassigns_all_agents() {
1285 let mut access = AccessManager::new();
1286 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1287
1288 access.assign_workspace("agent-1", "my-workspace");
1289 access.assign_workspace("agent-2", "my-workspace");
1290
1291 access.remove_workspace("my-workspace");
1292
1293 assert!(access.list_workspaces().is_empty());
1294 assert!(access.get_workspace_for_agent("agent-1").is_none());
1295 assert!(access.get_workspace_for_agent("agent-2").is_none());
1296 }
1297
1298 #[test]
1299 fn test_is_path_in_workspace() {
1300 let mut access = AccessManager::new();
1301
1302 let workspace = PathBuf::from("/tmp/oxios-test-workspace");
1304
1305 std::fs::create_dir_all(&workspace).ok();
1307 std::fs::create_dir_all(workspace.join("subdir")).ok();
1308
1309 access.register_workspace_path("my-workspace", workspace.clone());
1311
1312 let inside_path = workspace.join("file.txt");
1314 std::fs::write(&inside_path, "test").ok(); assert!(
1317 access.is_path_in_workspace("my-workspace", inside_path.to_str().unwrap()),
1318 "Path {:?} should be inside workspace",
1319 inside_path
1320 );
1321
1322 let nested_path = workspace.join("subdir/nested.txt");
1323 std::fs::write(&nested_path, "test").ok();
1324 assert!(
1325 access.is_path_in_workspace("my-workspace", nested_path.to_str().unwrap()),
1326 "Path {:?} should be inside workspace",
1327 nested_path
1328 );
1329
1330 assert!(!access.is_path_in_workspace("my-workspace", "/tmp/other-workspace/file.txt"));
1332
1333 assert!(!access.is_path_in_workspace("nonexistent", "/tmp/test"));
1335
1336 std::fs::remove_dir_all(workspace).ok();
1338 }
1339}