1mod audit_sink;
13mod context;
14mod gate;
15mod permissions;
16mod rbac;
17
18#[cfg(test)]
19pub use audit_sink::NoOpAuditSink;
20pub use audit_sink::{AuditEvent, AuditSink, TracingAuditSink, TrailAuditSink};
21pub use context::AgentContext;
22pub use gate::{AccessDenied, AccessGate, CheckRequest, DenyLayer, PathMode};
23pub use permissions::{AgentPermissions, AuditEntry, PermissionUpdate};
24pub use rbac::{
25 Action, ApprovalStatus, PendingApproval, RbacAuditEntry, RbacManager, RbacPolicy, Role, Subject,
26};
27
28use std::collections::{HashMap, HashSet};
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31
32use crate::types::AgentId;
33
34#[derive(Debug, Clone)]
62pub struct AccessManager {
63 permissions: HashMap<String, AgentPermissions>,
65 audit_log: Vec<AuditEntry>,
67 #[allow(dead_code)]
69 audit_log_path: Option<std::path::PathBuf>,
70 max_audit_entries: usize,
72 pub(crate) rbac: RbacManager,
74 workspace_paths: HashMap<String, PathBuf>,
76 agent_workspaces: HashMap<String, String>,
78 workspace_agents: HashMap<String, HashSet<String>>,
80 audit_sender: Option<tokio::sync::mpsc::Sender<String>>,
82 #[allow(dead_code)]
84 audit_writer_handle: Option<Arc<tokio::task::JoinHandle<()>>>,
85}
86
87impl AccessManager {
88 pub fn new() -> Self {
90 Self {
91 permissions: HashMap::new(),
92 audit_log: Vec::new(),
93 audit_log_path: None,
94 max_audit_entries: 10_000,
95 rbac: RbacManager::new(),
96 workspace_paths: HashMap::new(),
97 agent_workspaces: HashMap::new(),
98 workspace_agents: HashMap::new(),
99 audit_sender: None,
100 audit_writer_handle: None,
101 }
102 }
103
104 pub fn with_max_audit_entries(max_audit_entries: usize) -> Self {
109 Self {
110 permissions: HashMap::new(),
111 audit_log: Vec::new(),
112 audit_log_path: None,
113 max_audit_entries,
114 rbac: RbacManager::new(),
115 workspace_paths: HashMap::new(),
116 agent_workspaces: HashMap::new(),
117 workspace_agents: HashMap::new(),
118 audit_sender: None,
119 audit_writer_handle: None,
120 }
121 }
122
123 pub fn with_audit_log_path(mut self, path: std::path::PathBuf) -> Self {
129 self.audit_log_path = Some(path.clone());
130
131 let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(1000);
133 self.audit_sender = Some(tx);
134
135 if let Ok(handle) = tokio::runtime::Handle::try_current() {
137 let audit_path = path;
138 let audit_handle = handle.spawn(async move {
139 while let Some(line) = rx.recv().await {
140 if let Ok(mut f) = std::fs::OpenOptions::new()
141 .create(true)
142 .append(true)
143 .open(&audit_path)
144 {
145 use std::io::Write;
146 let _ = writeln!(f, "{line}");
147 }
148 }
149 });
150 self.audit_writer_handle = Some(Arc::new(audit_handle));
151 }
152
153 self
154 }
155
156 pub fn can_use_tool(&mut self, agent_name: &str, tool: &str) -> bool {
160 let allowed = match self.permissions.get(agent_name) {
161 Some(perms) => perms.allowed_tools.contains(tool),
162 None => {
163 tracing::warn!(agent = %agent_name, tool = %tool, "Agent not found in access manager, denying");
164 false
165 }
166 };
167
168 let reason = if allowed {
169 None
170 } else {
171 Some("tool not in allowed set".to_string())
172 };
173
174 self.log_access(agent_name, "use_tool", tool, allowed, reason);
175
176 allowed
177 }
178
179 pub fn can_access_path(&mut self, agent_name: &str, path: &str) -> bool {
187 let allowed = match self.permissions.get(agent_name) {
188 Some(perms) => {
189 if perms.is_path_denied(path) {
191 false
192 } else {
193 perms.is_path_allowed(path)
194 }
195 }
196 None => {
197 tracing::warn!(agent = %agent_name, path = %path, "Agent not found, denying path access");
198 false
199 }
200 };
201
202 let reason = if allowed {
203 None
204 } else {
205 Some("path not in allowed set or is denied".to_string())
206 };
207
208 self.log_access(agent_name, "access_path", path, allowed, reason);
209
210 allowed
211 }
212
213 pub fn can_access_network(&mut self, agent_name: &str) -> bool {
217 let allowed = match self.permissions.get(agent_name) {
218 Some(perms) => perms.network_access,
219 None => false,
220 };
221
222 let reason = if allowed {
223 None
224 } else {
225 Some("network access not enabled".to_string())
226 };
227
228 self.log_access(agent_name, "network_request", "<network>", allowed, reason);
229
230 allowed
231 }
232
233 pub fn can_execute_for(&self, agent_name: &str, duration_secs: u64) -> bool {
238 match self.permissions.get(agent_name) {
239 Some(perms) => {
240 perms.max_execution_time_secs == 0 || duration_secs <= perms.max_execution_time_secs
241 }
242 None => false,
243 }
244 }
245
246 pub fn can_use_memory(&self, agent_name: &str, memory_mb: u64) -> bool {
251 match self.permissions.get(agent_name) {
252 Some(perms) => perms.max_memory_mb == 0 || memory_mb <= perms.max_memory_mb,
253 None => false,
254 }
255 }
256
257 pub fn can_fork(&self, agent_name: &str) -> bool {
259 match self.permissions.get(agent_name) {
260 Some(perms) => perms.can_fork,
261 None => false,
262 }
263 }
264
265 pub fn get_permissions(&self, agent_name: &str) -> Option<&AgentPermissions> {
269 self.permissions.get(agent_name)
270 }
271
272 pub fn get_or_create_permissions(&mut self, agent_name: &str) -> &mut AgentPermissions {
276 self.permissions
277 .entry(agent_name.to_string())
278 .or_insert_with(|| AgentPermissions::for_new_agent(agent_name))
279 }
280
281 pub fn set_permissions(&mut self, permissions: AgentPermissions) {
285 let agent_name = permissions.agent_name.clone();
286 self.permissions.insert(agent_name, permissions);
287 }
288
289 pub fn update_permissions(
294 &mut self,
295 agent_name: &str,
296 update: PermissionUpdate,
297 ) -> anyhow::Result<()> {
298 let perms = self
299 .permissions
300 .entry(agent_name.to_string())
301 .or_insert_with(|| AgentPermissions::for_new_agent(agent_name));
302 update.apply(perms);
303 Ok(())
304 }
305
306 pub fn remove_permissions(&mut self, agent_name: &str) {
310 self.permissions.remove(agent_name);
311 tracing::info!(agent = %agent_name, "Agent permissions removed");
312 }
313
314 pub fn list_agents(&self) -> Vec<String> {
316 self.permissions.keys().cloned().collect()
317 }
318
319 pub fn audit_log(&self) -> &[AuditEntry] {
323 &self.audit_log
324 }
325
326 pub fn audit_log_recent(&self, limit: usize) -> Vec<AuditEntry> {
331 let start = self.audit_log.len().saturating_sub(limit);
332 self.audit_log[start..].to_vec()
333 }
334
335 pub fn audit_log_for_agent(&self, agent_name: &str) -> Vec<AuditEntry> {
337 self.audit_log
338 .iter()
339 .filter(|e| e.agent_name == agent_name)
340 .cloned()
341 .collect()
342 }
343
344 pub fn denied_actions(&self) -> Vec<&AuditEntry> {
346 self.audit_log.iter().filter(|e| !e.allowed).collect()
347 }
348
349 pub fn rbac_manager(&self) -> &RbacManager {
351 &self.rbac
352 }
353
354 pub fn rbac_manager_mut(&mut self) -> &mut RbacManager {
356 &mut self.rbac
357 }
358
359 pub fn register_workspace_path(&mut self, workspace_name: &str, workspace_path: PathBuf) {
369 self.workspace_paths
370 .insert(workspace_name.to_string(), workspace_path);
371 tracing::debug!(workspace = %workspace_name, "Workspace path registered");
372 }
373
374 pub fn assign_workspace(&mut self, agent_name: &str, workspace_name: &str) -> bool {
385 if !self.workspace_paths.contains_key(workspace_name) {
386 tracing::warn!(agent = %agent_name, workspace = %workspace_name, "Cannot assign agent to non-existent workspace");
387 return false;
388 }
389
390 if let Some(prev_workspace) = self.agent_workspaces.get(agent_name) {
392 if let Some(agents) = self.workspace_agents.get_mut(prev_workspace) {
393 agents.remove(agent_name);
394 }
395 }
396
397 self.agent_workspaces
399 .insert(agent_name.to_string(), workspace_name.to_string());
400 self.workspace_agents
401 .entry(workspace_name.to_string())
402 .or_default()
403 .insert(agent_name.to_string());
404
405 tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent assigned to workspace");
406 true
407 }
408
409 pub fn get_workspace_for_agent(&self, agent_name: &str) -> Option<String> {
414 self.agent_workspaces.get(agent_name).cloned()
415 }
416
417 pub fn get_workspace_path(&self, workspace_name: &str) -> Option<&PathBuf> {
422 self.workspace_paths.get(workspace_name)
423 }
424
425 pub fn list_workspaces(&self) -> Vec<String> {
427 self.workspace_paths.keys().cloned().collect()
428 }
429
430 pub fn list_agents_in_workspace(&self, workspace_name: &str) -> Vec<String> {
432 self.workspace_agents
433 .get(workspace_name)
434 .map(|agents| agents.iter().cloned().collect())
435 .unwrap_or_default()
436 }
437
438 pub fn can_access_workspace(&self, agent_name: &str, workspace_name: &str) -> bool {
449 self.agent_workspaces
450 .get(agent_name)
451 .map(|w| w == workspace_name)
452 .unwrap_or(false)
453 }
454
455 pub fn is_path_in_workspace(&self, workspace_name: &str, path: &str) -> bool {
467 let workspace = match self.workspace_paths.get(workspace_name) {
468 Some(w) => w,
469 None => return false,
470 };
471
472 let requested_path = match Path::new(path).canonicalize() {
474 Ok(p) => p,
475 Err(_) => {
476 let candidate = workspace.join(path);
478 match candidate.canonicalize() {
479 Ok(p) => p,
480 Err(_) => return false,
481 }
482 }
483 };
484
485 let workspace_canonical = match workspace.canonicalize() {
487 Ok(w) => w,
488 Err(_) => return false,
489 };
490
491 requested_path.starts_with(&workspace_canonical)
492 }
493
494 pub fn can_access_path_in_workspace(
521 &mut self,
522 agent_id: &AgentId,
523 agent_name: &str,
524 path: &str,
525 workspace: Option<&str>,
526 ) -> bool {
527 let subject = Subject::Agent(*agent_id);
529 let action = Action::AccessPath(path.to_string());
530 let rbac_allowed = self.rbac.check_permission(&subject, &action, path);
531
532 let path_allowed = self.can_access_path(agent_name, path);
534
535 let workspace_allowed = if let Some(workspace_name) = workspace {
537 let is_in_workspace = self.is_path_in_workspace(workspace_name, path);
538
539 if !is_in_workspace {
540 self.log_access(
542 agent_name,
543 "sandbox_violation",
544 path,
545 false,
546 Some(format!(
547 "Path '{path}' is outside workspace '{workspace_name}' boundary"
548 )),
549 );
550 }
551
552 is_in_workspace
553 } else {
554 if let Some(assigned_workspace) = self.agent_workspaces.get(agent_name) {
556 let is_in_workspace = self.is_path_in_workspace(assigned_workspace, path);
557
558 if !is_in_workspace {
559 self.log_access(
560 agent_name,
561 "sandbox_violation",
562 path,
563 false,
564 Some(format!(
565 "Path '{path}' is outside assigned workspace '{assigned_workspace}' boundary"
566 )),
567 );
568 }
569
570 is_in_workspace
571 } else {
572 true
574 }
575 };
576
577 rbac_allowed && path_allowed && workspace_allowed
579 }
580
581 pub fn unassign_workspace(&mut self, agent_name: &str) -> Option<String> {
585 if let Some(workspace_name) = self.agent_workspaces.remove(agent_name) {
586 if let Some(agents) = self.workspace_agents.get_mut(&workspace_name) {
587 agents.remove(agent_name);
588 }
589 tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent unassigned from workspace");
590 Some(workspace_name)
591 } else {
592 None
593 }
594 }
595
596 pub fn remove_workspace(&mut self, workspace_name: &str) {
600 if let Some(agents) = self.workspace_agents.remove(workspace_name) {
602 for agent_name in agents {
603 self.agent_workspaces.remove(&agent_name);
604 }
605 }
606
607 self.workspace_paths.remove(workspace_name);
609
610 tracing::info!(workspace = %workspace_name, "Workspace removed from access manager");
611 }
612
613 pub fn clear_audit_log(&mut self) {
615 let count = self.audit_log.len();
616 self.audit_log.clear();
617 tracing::info!(cleared = count, "Audit log cleared");
618 }
619
620 pub(crate) fn log_access(
625 &mut self,
626 agent_name: &str,
627 action: &str,
628 resource: &str,
629 allowed: bool,
630 reason: Option<String>,
631 ) {
632 let entry = AuditEntry::new(agent_name, action, resource, allowed, reason.clone());
633
634 self.audit_log.push(entry.clone());
635
636 if self.audit_log.len() > self.max_audit_entries {
638 let prune_count = self.audit_log.len() - self.max_audit_entries;
639 self.audit_log.drain(0..prune_count);
640 }
641
642 self.persist_audit_entry(&entry);
644
645 if !allowed {
647 tracing::warn!(
648 agent = %agent_name,
649 action = %action,
650 resource = %resource,
651 reason = ?reason,
652 "Access denied"
653 );
654 }
655 }
656
657 fn persist_audit_entry(&self, entry: &AuditEntry) {
663 if self.audit_log_path.is_none() {
664 return;
665 }
666 let line = match serde_json::to_string(entry) {
667 Ok(s) => s,
668 Err(_) => return,
669 };
670 if let Some(sender) = &self.audit_sender {
671 match sender.try_send(line) {
672 Ok(()) => {}
673 Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
674 tracing::warn!("Audit log channel full — dropping entry");
675 }
676 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
677 tracing::warn!("Audit log channel closed — dropping entry");
678 }
679 }
680 }
681 }
682
683 pub fn validate_permissions(&self, perms: &AgentPermissions) -> Vec<String> {
687 let mut warnings = Vec::new();
688
689 if perms.allowed_tools.is_empty() {
690 warnings.push("Agent has no allowed tools".to_string());
691 }
692
693 if perms.allowed_paths.is_empty() {
694 warnings.push("Agent has no path restrictions (wide open)".to_string());
695 }
696
697 if perms.network_access {
698 warnings.push("Agent has network access enabled".to_string());
699 }
700
701 if perms.can_fork {
702 warnings.push("Agent can fork sub-agents".to_string());
703 }
704
705 if perms.max_execution_time_secs == 0 {
706 warnings.push("Agent has no execution time limit".to_string());
707 }
708
709 if perms.max_memory_mb == 0 {
710 warnings.push("Agent has no memory limit".to_string());
711 }
712
713 warnings
714 }
715}
716
717impl Default for AccessManager {
718 fn default() -> Self {
719 Self::new()
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 #[test]
730 fn test_default_permissions() {
731 let perms = AgentPermissions::default();
732 assert!(perms.allowed_tools.contains("bash"));
733 assert!(!perms.network_access);
734 assert!(!perms.can_fork);
735 assert_eq!(perms.max_execution_time_secs, 300);
736 assert_eq!(perms.max_memory_mb, 512);
737 }
738
739 #[test]
740 fn test_for_new_agent() {
741 let perms = AgentPermissions::for_new_agent("my-agent");
742 assert_eq!(perms.agent_name, "my-agent");
743 assert!(perms.allowed_tools.contains("bash"));
744 }
745
746 #[test]
747 fn test_allow_deny_tool() {
748 let mut perms = AgentPermissions::for_new_agent("test");
749 assert!(perms.allowed_tools.contains("bash"));
750
751 perms.deny_tool("bash");
752 assert!(!perms.allowed_tools.contains("bash"));
753
754 perms.allow_tool("custom");
755 assert!(perms.allowed_tools.contains("custom"));
756 }
757
758 #[test]
759 fn test_allow_deny_path() {
760 let mut perms = AgentPermissions::for_new_agent("test");
761
762 perms.allow_path("/workspace/**");
763 assert!(perms.allowed_paths.contains(&"/workspace/**".to_string()));
764
765 perms.deny_path("/workspace/.secret/**");
766 assert!(perms
767 .denied_paths
768 .contains(&"/workspace/.secret/**".to_string()));
769 }
770
771 #[test]
772 fn test_enable_network() {
773 let mut perms = AgentPermissions::for_new_agent("test");
774 assert!(!perms.network_access);
775
776 perms.enable_network();
777 assert!(perms.network_access);
778 }
779
780 #[test]
781 fn test_enable_forking() {
782 let mut perms = AgentPermissions::for_new_agent("test");
783 assert!(!perms.can_fork);
784
785 perms.enable_forking();
786 assert!(perms.can_fork);
787 }
788
789 #[test]
790 fn test_path_matching_allowed() {
791 let mut perms = AgentPermissions::for_new_agent("test");
792 perms.allowed_paths = vec!["/workspace/**".to_string(), "/home/*/docs/**".to_string()];
793 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
794
795 assert!(perms.is_path_allowed("/workspace/project/file.rs"));
797 assert!(perms.is_path_allowed("/home/user/docs/readme.md"));
798
799 assert!(!perms.is_path_allowed("/etc/passwd"));
801 assert!(!perms.is_path_allowed("/home/user/secret.txt"));
802 }
803
804 #[test]
805 fn test_path_matching_denied() {
806 let mut perms = AgentPermissions::for_new_agent("test");
807 perms.allowed_paths = vec!["/workspace/**".to_string()];
808 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
809
810 assert!(perms.is_path_denied("/workspace/.oxios/config.toml"));
812 assert!(!perms.is_path_denied("/workspace/project/file.rs"));
813
814 let _access = AccessManager::new();
816 let mut perms2 = perms.clone();
817 perms2.agent_name = "test".to_string();
818 }
822
823 #[test]
824 fn test_path_denied_pattern_matching() {
825 let mut perms = AgentPermissions::for_new_agent("test");
826 perms.denied_paths = vec!["/etc/**".to_string(), "**/secrets/*".to_string()];
827
828 assert!(perms.is_path_denied("/etc/passwd"));
829 assert!(perms.is_path_denied("/etc/shadow"));
830 assert!(!perms.is_path_denied("/workspace/file"));
831 }
832
833 #[test]
836 fn test_can_use_tool_allowed() {
837 let mut access = AccessManager::new();
838
839 let mut perms = AgentPermissions::for_new_agent("code-agent");
840 perms.allow_tool("bash");
841 perms.allow_tool("read");
842 access.set_permissions(perms);
843
844 assert!(access.can_use_tool("code-agent", "bash"));
845 assert!(access.can_use_tool("code-agent", "read"));
846 }
847
848 #[test]
849 fn test_can_use_tool_denied() {
850 let mut access = AccessManager::new();
851
852 let mut perms = AgentPermissions::for_new_agent("code-agent");
853 perms.allow_tool("read");
854 perms.deny_tool("bash"); access.set_permissions(perms);
856
857 assert!(!access.can_use_tool("code-agent", "bash")); assert!(!access.can_use_tool("code-agent", "spawn")); assert!(!access.can_use_tool("unknown-agent", "bash")); }
861
862 #[test]
863 fn test_unknown_agent_denied_all_tools() {
864 let mut access = AccessManager::new();
865
866 assert!(!access.can_use_tool("unknown-agent", "read"));
868 assert!(!access.can_access_path("unknown-agent", "/workspace/test.txt"));
869 assert!(!access.can_access_network("unknown-agent"));
870 assert!(!access.can_fork("unknown-agent"));
871 }
872
873 #[test]
876 fn test_can_access_path_allowed() {
877 let mut access = AccessManager::new();
878
879 let perms = AgentPermissions::for_new_agent("file-agent");
880 access.set_permissions(perms);
881
882 assert!(access.can_access_path("file-agent", "/workspace/project/file.rs"));
883 assert!(!access.can_access_path("file-agent", "/etc/passwd"));
884 }
885
886 #[test]
887 fn test_can_access_path_denied_takes_precedence() {
888 let mut access = AccessManager::new();
889
890 let mut perms = AgentPermissions::for_new_agent("test");
891 perms.allowed_paths = vec!["/workspace/**".to_string()];
892 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
893 access.set_permissions(perms);
894
895 assert!(!access.can_access_path("test", "/workspace/.oxios/config.toml"));
897
898 assert!(access.can_access_path("test", "/workspace/project/file.rs"));
900 }
901
902 #[test]
905 fn test_can_access_network() {
906 let mut access = AccessManager::new();
907
908 let mut perms = AgentPermissions::for_new_agent("net-agent");
909 perms.enable_network();
910 access.set_permissions(perms);
911
912 assert!(access.can_access_network("net-agent"));
913 assert!(!access.can_access_network("no-net-agent"));
914 }
915
916 #[test]
919 fn test_can_execute_for() {
920 let mut access = AccessManager::new();
921
922 let mut perms = AgentPermissions::for_new_agent("test");
923 perms.max_execution_time_secs = 300;
924 access.set_permissions(perms);
925
926 assert!(access.can_execute_for("test", 100));
927 assert!(access.can_execute_for("test", 300));
928 assert!(!access.can_execute_for("test", 301));
929 }
930
931 #[test]
932 fn test_unlimited_execution_time() {
933 let mut access = AccessManager::new();
934
935 let mut perms = AgentPermissions::for_new_agent("test");
936 perms.max_execution_time_secs = 0; access.set_permissions(perms);
938
939 assert!(access.can_execute_for("test", 100_000));
940 }
941
942 #[test]
943 fn test_can_use_memory() {
944 let mut access = AccessManager::new();
945
946 let mut perms = AgentPermissions::for_new_agent("test");
947 perms.max_memory_mb = 512;
948 access.set_permissions(perms);
949
950 assert!(access.can_use_memory("test", 256));
951 assert!(access.can_use_memory("test", 512));
952 assert!(!access.can_use_memory("test", 513));
953 }
954
955 #[test]
956 fn test_unlimited_memory() {
957 let mut access = AccessManager::new();
958
959 let mut perms = AgentPermissions::for_new_agent("test");
960 perms.max_memory_mb = 0;
961 access.set_permissions(perms);
962
963 assert!(access.can_use_memory("test", 1_000_000));
964 }
965
966 #[test]
969 fn test_can_fork() {
970 let mut access = AccessManager::new();
971
972 let mut perms = AgentPermissions::for_new_agent("test");
973 perms.enable_forking();
974 access.set_permissions(perms);
975
976 assert!(access.can_fork("test"));
977 assert!(!access.can_fork("no-fork-agent"));
978 }
979
980 #[test]
983 fn test_set_and_get_permissions() {
984 let mut access = AccessManager::new();
985
986 let perms = AgentPermissions::for_new_agent("test-agent");
987 access.set_permissions(perms);
988
989 let retrieved = access.get_permissions("test-agent");
990 assert!(retrieved.is_some());
991 assert_eq!(retrieved.unwrap().agent_name, "test-agent");
992 }
993
994 #[test]
995 fn test_get_nonexistent_permissions() {
996 let access = AccessManager::new();
997 assert!(access.get_permissions("ghost").is_none());
998 }
999
1000 #[test]
1001 fn test_get_or_create_permissions() {
1002 let mut access = AccessManager::new();
1003
1004 let perms = access.get_or_create_permissions("new-agent");
1006 assert_eq!(perms.agent_name, "new-agent");
1007
1008 let perms2 = access.get_or_create_permissions("new-agent");
1010 assert_eq!(perms2.agent_name, "new-agent");
1011 }
1012
1013 #[test]
1014 fn test_remove_permissions() {
1015 let mut access = AccessManager::new();
1016
1017 let perms = AgentPermissions::for_new_agent("to-remove");
1018 access.set_permissions(perms);
1019
1020 assert!(access.get_permissions("to-remove").is_some());
1021
1022 access.remove_permissions("to-remove");
1023
1024 assert!(access.get_permissions("to-remove").is_none());
1025 assert!(!access.can_use_tool("to-remove", "bash"));
1027 }
1028
1029 #[test]
1030 fn test_list_agents() {
1031 let mut access = AccessManager::new();
1032
1033 access.set_permissions(AgentPermissions::for_new_agent("agent-1"));
1034 access.set_permissions(AgentPermissions::for_new_agent("agent-2"));
1035
1036 let agents = access.list_agents();
1037 assert_eq!(agents.len(), 2);
1038 assert!(agents.contains(&"agent-1".to_string()));
1039 assert!(agents.contains(&"agent-2".to_string()));
1040 }
1041
1042 #[test]
1045 fn test_audit_log_records_access() {
1046 let mut access = AccessManager::new();
1047
1048 let perms = AgentPermissions::for_new_agent("test-agent");
1049 access.set_permissions(perms);
1050
1051 access.can_use_tool("test-agent", "bash"); access.can_use_tool("test-agent", "network"); let log = access.audit_log();
1055 assert_eq!(log.len(), 2);
1056 assert!(log[0].allowed);
1057 assert!(!log[1].allowed);
1058 assert_eq!(log[0].agent_name, "test-agent");
1059 assert_eq!(log[0].action, "use_tool");
1060 assert_eq!(log[0].resource, "bash");
1061 }
1062
1063 #[test]
1064 fn test_audit_log_recent() {
1065 let mut access = AccessManager::new();
1066
1067 let perms = AgentPermissions::for_new_agent("test");
1068 access.set_permissions(perms);
1069
1070 for i in 0..10 {
1071 access.can_use_tool("test", &format!("tool-{}", i));
1072 }
1073
1074 let recent = access.audit_log_recent(3);
1075 assert_eq!(recent.len(), 3);
1076 }
1077
1078 #[test]
1079 fn test_audit_log_for_agent() {
1080 let mut access = AccessManager::new();
1081
1082 access.set_permissions(AgentPermissions::for_new_agent("agent-a"));
1083 access.set_permissions(AgentPermissions::for_new_agent("agent-b"));
1084
1085 access.can_use_tool("agent-a", "tool1");
1086 access.can_use_tool("agent-b", "tool2");
1087 access.can_use_tool("agent-a", "tool3");
1088
1089 let log_a = access.audit_log_for_agent("agent-a");
1090 assert_eq!(log_a.len(), 2);
1091 }
1092
1093 #[test]
1094 fn test_denied_actions() {
1095 let mut access = AccessManager::new();
1096
1097 let perms = AgentPermissions::for_new_agent("test");
1098 access.set_permissions(perms);
1099
1100 access.can_use_tool("test", "bash"); access.can_use_tool("test", "dangerous"); access.can_access_path("test", "/etc/shadow"); let denied = access.denied_actions();
1105 assert_eq!(denied.len(), 2);
1106 }
1107
1108 #[test]
1109 fn test_clear_audit_log() {
1110 let mut access = AccessManager::new();
1111
1112 let perms = AgentPermissions::for_new_agent("test");
1113 access.set_permissions(perms);
1114
1115 for _ in 0..5 {
1116 access.can_use_tool("test", "tool");
1117 }
1118
1119 assert_eq!(access.audit_log().len(), 5);
1120
1121 access.clear_audit_log();
1122
1123 assert!(access.audit_log().is_empty());
1124 }
1125
1126 #[test]
1129 fn test_audit_log_prunes_old_entries() {
1130 let mut access = AccessManager::with_max_audit_entries(5);
1131
1132 let perms = AgentPermissions::for_new_agent("test");
1133 access.set_permissions(perms);
1134
1135 for i in 0..10 {
1137 access.can_use_tool("test", &format!("tool-{}", i));
1138 }
1139
1140 assert_eq!(access.audit_log().len(), 5);
1142 }
1143
1144 #[test]
1147 fn test_validate_permissions_no_tools() {
1148 let mut access = AccessManager::new();
1149 let mut perms = AgentPermissions::for_new_agent("test");
1150 perms.allowed_tools.clear();
1151 access.set_permissions(perms.clone());
1152
1153 let warnings = access.validate_permissions(&perms);
1154 assert!(warnings.iter().any(|w| w.contains("no allowed tools")));
1155 }
1156
1157 #[test]
1158 fn test_validate_permissions_no_path_restrictions() {
1159 let mut perms = AgentPermissions::for_new_agent("test");
1160 perms.allowed_paths.clear();
1161
1162 let access = AccessManager::new();
1163 let warnings = access.validate_permissions(&perms);
1164 assert!(warnings.iter().any(|w| w.contains("no path restrictions")));
1165 }
1166
1167 #[test]
1168 fn test_validate_permissions_warnings() {
1169 let mut access = AccessManager::new();
1170 let mut perms = AgentPermissions::for_new_agent("test");
1171 perms.network_access = true;
1172 perms.can_fork = true;
1173 perms.max_execution_time_secs = 0;
1174 perms.max_memory_mb = 0;
1175 access.set_permissions(perms.clone());
1176
1177 let warnings = access.validate_permissions(&perms);
1178 assert!(warnings.iter().any(|w| w.contains("network access")));
1179 assert!(warnings.iter().any(|w| w.contains("fork sub-agents")));
1180 assert!(warnings
1181 .iter()
1182 .any(|w| w.contains("no execution time limit")));
1183 assert!(warnings.iter().any(|w| w.contains("no memory limit")));
1184 }
1185
1186 #[test]
1189 fn test_audit_entry_has_timestamp() {
1190 let entry = AuditEntry::new("agent", "action", "resource", true, None);
1191 assert!(entry.timestamp.timestamp() > 0);
1193 }
1194
1195 #[test]
1198 fn test_register_workspace_path() {
1199 let mut access = AccessManager::new();
1200 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my-workspace"));
1201
1202 assert_eq!(access.list_workspaces(), vec!["my-workspace"]);
1203 assert_eq!(
1204 access.get_workspace_path("my-workspace"),
1205 Some(&PathBuf::from("/workspace/my-workspace"))
1206 );
1207 }
1208
1209 #[test]
1210 fn test_assign_agent_to_workspace() {
1211 let mut access = AccessManager::new();
1212 access.register_workspace_path("project-alpha", PathBuf::from("/workspace/alpha"));
1213
1214 assert!(access.assign_workspace("agent-1", "project-alpha"));
1216
1217 assert_eq!(
1219 access.get_workspace_for_agent("agent-1"),
1220 Some("project-alpha".to_string())
1221 );
1222 assert!(access.can_access_workspace("agent-1", "project-alpha"));
1223 assert!(!access.can_access_workspace("agent-1", "other-workspace"));
1224 }
1225
1226 #[test]
1227 fn test_assign_agent_to_nonexistent_workspace_fails() {
1228 let mut access = AccessManager::new();
1229
1230 assert!(!access.assign_workspace("agent-1", "nonexistent"));
1232 assert_eq!(access.get_workspace_for_agent("agent-1"), None);
1233 }
1234
1235 #[test]
1236 fn test_reassign_agent_to_different_workspace() {
1237 let mut access = AccessManager::new();
1238 access.register_workspace_path("workspace-a", PathBuf::from("/workspace/a"));
1239 access.register_workspace_path("workspace-b", PathBuf::from("/workspace/b"));
1240
1241 access.assign_workspace("agent-1", "workspace-a");
1243 assert_eq!(
1244 access.get_workspace_for_agent("agent-1"),
1245 Some("workspace-a".to_string())
1246 );
1247
1248 access.assign_workspace("agent-1", "workspace-b");
1250 assert_eq!(
1251 access.get_workspace_for_agent("agent-1"),
1252 Some("workspace-b".to_string())
1253 );
1254
1255 assert!(!access.can_access_workspace("agent-1", "workspace-a"));
1257 }
1258
1259 #[test]
1260 fn test_unassign_agent_from_workspace() {
1261 let mut access = AccessManager::new();
1262 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1263
1264 access.assign_workspace("agent-1", "my-workspace");
1265 assert!(access.get_workspace_for_agent("agent-1").is_some());
1266
1267 let removed = access.unassign_workspace("agent-1");
1268 assert_eq!(removed, Some("my-workspace".to_string()));
1269 assert!(access.get_workspace_for_agent("agent-1").is_none());
1270 }
1271
1272 #[test]
1273 fn test_list_agents_in_workspace() {
1274 let mut access = AccessManager::new();
1275 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1276
1277 access.assign_workspace("agent-1", "my-workspace");
1278 access.assign_workspace("agent-2", "my-workspace");
1279 access.assign_workspace("agent-3", "other-workspace");
1280
1281 let agents = access.list_agents_in_workspace("my-workspace");
1282 assert_eq!(agents.len(), 2);
1283 assert!(agents.contains(&"agent-1".to_string()));
1284 assert!(agents.contains(&"agent-2".to_string()));
1285 assert!(!agents.contains(&"agent-3".to_string()));
1286 }
1287
1288 #[test]
1289 fn test_remove_workspace_unassigns_all_agents() {
1290 let mut access = AccessManager::new();
1291 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1292
1293 access.assign_workspace("agent-1", "my-workspace");
1294 access.assign_workspace("agent-2", "my-workspace");
1295
1296 access.remove_workspace("my-workspace");
1297
1298 assert!(access.list_workspaces().is_empty());
1299 assert!(access.get_workspace_for_agent("agent-1").is_none());
1300 assert!(access.get_workspace_for_agent("agent-2").is_none());
1301 }
1302
1303 #[test]
1304 fn test_is_path_in_workspace() {
1305 let mut access = AccessManager::new();
1306
1307 let workspace = PathBuf::from("/tmp/oxios-test-workspace");
1309
1310 std::fs::create_dir_all(&workspace).ok();
1312 std::fs::create_dir_all(workspace.join("subdir")).ok();
1313
1314 access.register_workspace_path("my-workspace", workspace.clone());
1316
1317 let inside_path = workspace.join("file.txt");
1319 std::fs::write(&inside_path, "test").ok(); assert!(
1322 access.is_path_in_workspace("my-workspace", inside_path.to_str().unwrap()),
1323 "Path {:?} should be inside workspace",
1324 inside_path
1325 );
1326
1327 let nested_path = workspace.join("subdir/nested.txt");
1328 std::fs::write(&nested_path, "test").ok();
1329 assert!(
1330 access.is_path_in_workspace("my-workspace", nested_path.to_str().unwrap()),
1331 "Path {:?} should be inside workspace",
1332 nested_path
1333 );
1334
1335 assert!(!access.is_path_in_workspace("my-workspace", "/tmp/other-workspace/file.txt"));
1337
1338 assert!(!access.is_path_in_workspace("nonexistent", "/tmp/test"));
1340
1341 std::fs::remove_dir_all(workspace).ok();
1343 }
1344}