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 && let Some(agents) = self.workspace_agents.get_mut(prev_workspace)
393 {
394 agents.remove(agent_name);
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(
695 "Agent has no path restrictions (paths granted by ensure_permissions)".to_string(),
696 );
697 }
698
699 if perms.network_access {
700 warnings.push("Agent has network access enabled".to_string());
701 }
702
703 if perms.can_fork {
704 warnings.push("Agent can fork sub-agents".to_string());
705 }
706
707 if perms.max_execution_time_secs == 0 {
708 warnings.push("Agent has no execution time limit".to_string());
709 }
710
711 if perms.max_memory_mb == 0 {
712 warnings.push("Agent has no memory limit".to_string());
713 }
714
715 warnings
716 }
717}
718
719impl Default for AccessManager {
720 fn default() -> Self {
721 Self::new()
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
732 fn test_default_permissions() {
733 let perms = AgentPermissions::default();
734 assert!(perms.allowed_tools.contains("bash"));
735 assert!(!perms.network_access);
736 assert!(!perms.can_fork);
737 assert_eq!(perms.max_execution_time_secs, 300);
738 assert_eq!(perms.max_memory_mb, 512);
739 }
740
741 #[test]
742 fn test_for_new_agent() {
743 let perms = AgentPermissions::for_new_agent("my-agent");
744 assert_eq!(perms.agent_name, "my-agent");
745 assert!(perms.allowed_tools.contains("bash"));
746 }
747
748 #[test]
749 fn test_allow_deny_tool() {
750 let mut perms = AgentPermissions::for_new_agent("test");
751 assert!(perms.allowed_tools.contains("bash"));
752
753 perms.deny_tool("bash");
754 assert!(!perms.allowed_tools.contains("bash"));
755
756 perms.allow_tool("custom");
757 assert!(perms.allowed_tools.contains("custom"));
758 }
759
760 #[test]
761 fn test_allow_deny_path() {
762 let mut perms = AgentPermissions::for_new_agent("test");
763
764 perms.allow_path("/workspace/**");
765 assert!(perms.allowed_paths.contains(&"/workspace/**".to_string()));
766
767 perms.deny_path("/workspace/.secret/**");
768 assert!(
769 perms
770 .denied_paths
771 .contains(&"/workspace/.secret/**".to_string())
772 );
773 }
774
775 #[test]
776 fn test_enable_network() {
777 let mut perms = AgentPermissions::for_new_agent("test");
778 assert!(!perms.network_access);
779
780 perms.enable_network();
781 assert!(perms.network_access);
782 }
783
784 #[test]
785 fn test_enable_forking() {
786 let mut perms = AgentPermissions::for_new_agent("test");
787 assert!(!perms.can_fork);
788
789 perms.enable_forking();
790 assert!(perms.can_fork);
791 }
792
793 #[test]
794 fn test_path_matching_allowed() {
795 let mut perms = AgentPermissions::for_new_agent("test");
796 perms.allowed_paths = vec!["/workspace/**".to_string(), "/home/*/docs/**".to_string()];
797 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
798
799 assert!(perms.is_path_allowed("/workspace/project/file.rs"));
801 assert!(perms.is_path_allowed("/home/user/docs/readme.md"));
802
803 assert!(!perms.is_path_allowed("/etc/passwd"));
805 assert!(!perms.is_path_allowed("/home/user/secret.txt"));
806 }
807
808 #[test]
809 fn test_path_matching_denied() {
810 let mut perms = AgentPermissions::for_new_agent("test");
811 perms.allowed_paths = vec!["/workspace/**".to_string()];
812 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
813
814 assert!(perms.is_path_denied("/workspace/.oxios/config.toml"));
816 assert!(!perms.is_path_denied("/workspace/project/file.rs"));
817
818 let _access = AccessManager::new();
820 let mut perms2 = perms.clone();
821 perms2.agent_name = "test".to_string();
822 }
826
827 #[test]
828 fn test_path_denied_pattern_matching() {
829 let mut perms = AgentPermissions::for_new_agent("test");
830 perms.denied_paths = vec!["/etc/**".to_string(), "**/secrets/*".to_string()];
831
832 assert!(perms.is_path_denied("/etc/passwd"));
833 assert!(perms.is_path_denied("/etc/shadow"));
834 assert!(!perms.is_path_denied("/workspace/file"));
835 }
836
837 #[test]
840 fn test_can_use_tool_allowed() {
841 let mut access = AccessManager::new();
842
843 let mut perms = AgentPermissions::for_new_agent("code-agent");
844 perms.allow_tool("bash");
845 perms.allow_tool("read");
846 access.set_permissions(perms);
847
848 assert!(access.can_use_tool("code-agent", "bash"));
849 assert!(access.can_use_tool("code-agent", "read"));
850 }
851
852 #[test]
853 fn test_can_use_tool_denied() {
854 let mut access = AccessManager::new();
855
856 let mut perms = AgentPermissions::for_new_agent("code-agent");
857 perms.allow_tool("read");
858 perms.deny_tool("bash"); access.set_permissions(perms);
860
861 assert!(!access.can_use_tool("code-agent", "bash")); assert!(!access.can_use_tool("code-agent", "spawn")); assert!(!access.can_use_tool("unknown-agent", "bash")); }
865
866 #[test]
867 fn test_unknown_agent_denied_all_tools() {
868 let mut access = AccessManager::new();
869
870 assert!(!access.can_use_tool("unknown-agent", "read"));
872 assert!(!access.can_access_path("unknown-agent", "/workspace/test.txt"));
873 assert!(!access.can_access_network("unknown-agent"));
874 assert!(!access.can_fork("unknown-agent"));
875 }
876
877 #[test]
880 fn test_can_access_path_allowed() {
881 let mut access = AccessManager::new();
882
883 let mut perms = AgentPermissions::for_new_agent("file-agent");
884 perms.allow_path("/workspace/**");
885 access.set_permissions(perms);
886
887 assert!(access.can_access_path("file-agent", "/workspace/project/file.rs"));
888 assert!(!access.can_access_path("file-agent", "/etc/passwd"));
889 }
890
891 #[test]
892 fn test_can_access_path_denied_takes_precedence() {
893 let mut access = AccessManager::new();
894
895 let mut perms = AgentPermissions::for_new_agent("test");
896 perms.allowed_paths = vec!["/workspace/**".to_string()];
897 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
898 access.set_permissions(perms);
899
900 assert!(!access.can_access_path("test", "/workspace/.oxios/config.toml"));
902
903 assert!(access.can_access_path("test", "/workspace/project/file.rs"));
905 }
906
907 #[test]
910 fn test_can_access_network() {
911 let mut access = AccessManager::new();
912
913 let mut perms = AgentPermissions::for_new_agent("net-agent");
914 perms.enable_network();
915 access.set_permissions(perms);
916
917 assert!(access.can_access_network("net-agent"));
918 assert!(!access.can_access_network("no-net-agent"));
919 }
920
921 #[test]
924 fn test_can_execute_for() {
925 let mut access = AccessManager::new();
926
927 let mut perms = AgentPermissions::for_new_agent("test");
928 perms.max_execution_time_secs = 300;
929 access.set_permissions(perms);
930
931 assert!(access.can_execute_for("test", 100));
932 assert!(access.can_execute_for("test", 300));
933 assert!(!access.can_execute_for("test", 301));
934 }
935
936 #[test]
937 fn test_unlimited_execution_time() {
938 let mut access = AccessManager::new();
939
940 let mut perms = AgentPermissions::for_new_agent("test");
941 perms.max_execution_time_secs = 0; access.set_permissions(perms);
943
944 assert!(access.can_execute_for("test", 100_000));
945 }
946
947 #[test]
948 fn test_can_use_memory() {
949 let mut access = AccessManager::new();
950
951 let mut perms = AgentPermissions::for_new_agent("test");
952 perms.max_memory_mb = 512;
953 access.set_permissions(perms);
954
955 assert!(access.can_use_memory("test", 256));
956 assert!(access.can_use_memory("test", 512));
957 assert!(!access.can_use_memory("test", 513));
958 }
959
960 #[test]
961 fn test_unlimited_memory() {
962 let mut access = AccessManager::new();
963
964 let mut perms = AgentPermissions::for_new_agent("test");
965 perms.max_memory_mb = 0;
966 access.set_permissions(perms);
967
968 assert!(access.can_use_memory("test", 1_000_000));
969 }
970
971 #[test]
974 fn test_can_fork() {
975 let mut access = AccessManager::new();
976
977 let mut perms = AgentPermissions::for_new_agent("test");
978 perms.enable_forking();
979 access.set_permissions(perms);
980
981 assert!(access.can_fork("test"));
982 assert!(!access.can_fork("no-fork-agent"));
983 }
984
985 #[test]
988 fn test_set_and_get_permissions() {
989 let mut access = AccessManager::new();
990
991 let perms = AgentPermissions::for_new_agent("test-agent");
992 access.set_permissions(perms);
993
994 let retrieved = access.get_permissions("test-agent");
995 assert!(retrieved.is_some());
996 assert_eq!(retrieved.unwrap().agent_name, "test-agent");
997 }
998
999 #[test]
1000 fn test_get_nonexistent_permissions() {
1001 let access = AccessManager::new();
1002 assert!(access.get_permissions("ghost").is_none());
1003 }
1004
1005 #[test]
1006 fn test_get_or_create_permissions() {
1007 let mut access = AccessManager::new();
1008
1009 let perms = access.get_or_create_permissions("new-agent");
1011 assert_eq!(perms.agent_name, "new-agent");
1012
1013 let perms2 = access.get_or_create_permissions("new-agent");
1015 assert_eq!(perms2.agent_name, "new-agent");
1016 }
1017
1018 #[test]
1019 fn test_remove_permissions() {
1020 let mut access = AccessManager::new();
1021
1022 let perms = AgentPermissions::for_new_agent("to-remove");
1023 access.set_permissions(perms);
1024
1025 assert!(access.get_permissions("to-remove").is_some());
1026
1027 access.remove_permissions("to-remove");
1028
1029 assert!(access.get_permissions("to-remove").is_none());
1030 assert!(!access.can_use_tool("to-remove", "bash"));
1032 }
1033
1034 #[test]
1035 fn test_list_agents() {
1036 let mut access = AccessManager::new();
1037
1038 access.set_permissions(AgentPermissions::for_new_agent("agent-1"));
1039 access.set_permissions(AgentPermissions::for_new_agent("agent-2"));
1040
1041 let agents = access.list_agents();
1042 assert_eq!(agents.len(), 2);
1043 assert!(agents.contains(&"agent-1".to_string()));
1044 assert!(agents.contains(&"agent-2".to_string()));
1045 }
1046
1047 #[test]
1050 fn test_audit_log_records_access() {
1051 let mut access = AccessManager::new();
1052
1053 let perms = AgentPermissions::for_new_agent("test-agent");
1054 access.set_permissions(perms);
1055
1056 access.can_use_tool("test-agent", "bash"); access.can_use_tool("test-agent", "network"); let log = access.audit_log();
1060 assert_eq!(log.len(), 2);
1061 assert!(log[0].allowed);
1062 assert!(!log[1].allowed);
1063 assert_eq!(log[0].agent_name, "test-agent");
1064 assert_eq!(log[0].action, "use_tool");
1065 assert_eq!(log[0].resource, "bash");
1066 }
1067
1068 #[test]
1069 fn test_audit_log_recent() {
1070 let mut access = AccessManager::new();
1071
1072 let perms = AgentPermissions::for_new_agent("test");
1073 access.set_permissions(perms);
1074
1075 for i in 0..10 {
1076 access.can_use_tool("test", &format!("tool-{}", i));
1077 }
1078
1079 let recent = access.audit_log_recent(3);
1080 assert_eq!(recent.len(), 3);
1081 }
1082
1083 #[test]
1084 fn test_audit_log_for_agent() {
1085 let mut access = AccessManager::new();
1086
1087 access.set_permissions(AgentPermissions::for_new_agent("agent-a"));
1088 access.set_permissions(AgentPermissions::for_new_agent("agent-b"));
1089
1090 access.can_use_tool("agent-a", "tool1");
1091 access.can_use_tool("agent-b", "tool2");
1092 access.can_use_tool("agent-a", "tool3");
1093
1094 let log_a = access.audit_log_for_agent("agent-a");
1095 assert_eq!(log_a.len(), 2);
1096 }
1097
1098 #[test]
1099 fn test_denied_actions() {
1100 let mut access = AccessManager::new();
1101
1102 let perms = AgentPermissions::for_new_agent("test");
1103 access.set_permissions(perms);
1104
1105 access.can_use_tool("test", "bash"); access.can_use_tool("test", "dangerous"); access.can_access_path("test", "/etc/shadow"); let denied = access.denied_actions();
1110 assert_eq!(denied.len(), 2);
1111 }
1112
1113 #[test]
1114 fn test_clear_audit_log() {
1115 let mut access = AccessManager::new();
1116
1117 let perms = AgentPermissions::for_new_agent("test");
1118 access.set_permissions(perms);
1119
1120 for _ in 0..5 {
1121 access.can_use_tool("test", "tool");
1122 }
1123
1124 assert_eq!(access.audit_log().len(), 5);
1125
1126 access.clear_audit_log();
1127
1128 assert!(access.audit_log().is_empty());
1129 }
1130
1131 #[test]
1134 fn test_audit_log_prunes_old_entries() {
1135 let mut access = AccessManager::with_max_audit_entries(5);
1136
1137 let perms = AgentPermissions::for_new_agent("test");
1138 access.set_permissions(perms);
1139
1140 for i in 0..10 {
1142 access.can_use_tool("test", &format!("tool-{}", i));
1143 }
1144
1145 assert_eq!(access.audit_log().len(), 5);
1147 }
1148
1149 #[test]
1152 fn test_validate_permissions_no_tools() {
1153 let mut access = AccessManager::new();
1154 let mut perms = AgentPermissions::for_new_agent("test");
1155 perms.allowed_tools.clear();
1156 access.set_permissions(perms.clone());
1157
1158 let warnings = access.validate_permissions(&perms);
1159 assert!(warnings.iter().any(|w| w.contains("no allowed tools")));
1160 }
1161
1162 #[test]
1163 fn test_validate_permissions_no_path_restrictions() {
1164 let mut perms = AgentPermissions::for_new_agent("test");
1165 perms.allowed_paths.clear();
1166
1167 let access = AccessManager::new();
1168 let warnings = access.validate_permissions(&perms);
1169 assert!(warnings.iter().any(|w| w.contains("no path restrictions")));
1170 }
1171
1172 #[test]
1173 fn test_validate_permissions_warnings() {
1174 let mut access = AccessManager::new();
1175 let mut perms = AgentPermissions::for_new_agent("test");
1176 perms.network_access = true;
1177 perms.can_fork = true;
1178 perms.max_execution_time_secs = 0;
1179 perms.max_memory_mb = 0;
1180 access.set_permissions(perms.clone());
1181
1182 let warnings = access.validate_permissions(&perms);
1183 assert!(warnings.iter().any(|w| w.contains("network access")));
1184 assert!(warnings.iter().any(|w| w.contains("fork sub-agents")));
1185 assert!(
1186 warnings
1187 .iter()
1188 .any(|w| w.contains("no execution time limit"))
1189 );
1190 assert!(warnings.iter().any(|w| w.contains("no memory limit")));
1191 }
1192
1193 #[test]
1196 fn test_audit_entry_has_timestamp() {
1197 let entry = AuditEntry::new("agent", "action", "resource", true, None);
1198 assert!(entry.timestamp.timestamp() > 0);
1200 }
1201
1202 #[test]
1205 fn test_register_workspace_path() {
1206 let mut access = AccessManager::new();
1207 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my-workspace"));
1208
1209 assert_eq!(access.list_workspaces(), vec!["my-workspace"]);
1210 assert_eq!(
1211 access.get_workspace_path("my-workspace"),
1212 Some(&PathBuf::from("/workspace/my-workspace"))
1213 );
1214 }
1215
1216 #[test]
1217 fn test_assign_agent_to_workspace() {
1218 let mut access = AccessManager::new();
1219 access.register_workspace_path("project-alpha", PathBuf::from("/workspace/alpha"));
1220
1221 assert!(access.assign_workspace("agent-1", "project-alpha"));
1223
1224 assert_eq!(
1226 access.get_workspace_for_agent("agent-1"),
1227 Some("project-alpha".to_string())
1228 );
1229 assert!(access.can_access_workspace("agent-1", "project-alpha"));
1230 assert!(!access.can_access_workspace("agent-1", "other-workspace"));
1231 }
1232
1233 #[test]
1234 fn test_assign_agent_to_nonexistent_workspace_fails() {
1235 let mut access = AccessManager::new();
1236
1237 assert!(!access.assign_workspace("agent-1", "nonexistent"));
1239 assert_eq!(access.get_workspace_for_agent("agent-1"), None);
1240 }
1241
1242 #[test]
1243 fn test_reassign_agent_to_different_workspace() {
1244 let mut access = AccessManager::new();
1245 access.register_workspace_path("workspace-a", PathBuf::from("/workspace/a"));
1246 access.register_workspace_path("workspace-b", PathBuf::from("/workspace/b"));
1247
1248 access.assign_workspace("agent-1", "workspace-a");
1250 assert_eq!(
1251 access.get_workspace_for_agent("agent-1"),
1252 Some("workspace-a".to_string())
1253 );
1254
1255 access.assign_workspace("agent-1", "workspace-b");
1257 assert_eq!(
1258 access.get_workspace_for_agent("agent-1"),
1259 Some("workspace-b".to_string())
1260 );
1261
1262 assert!(!access.can_access_workspace("agent-1", "workspace-a"));
1264 }
1265
1266 #[test]
1267 fn test_unassign_agent_from_workspace() {
1268 let mut access = AccessManager::new();
1269 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1270
1271 access.assign_workspace("agent-1", "my-workspace");
1272 assert!(access.get_workspace_for_agent("agent-1").is_some());
1273
1274 let removed = access.unassign_workspace("agent-1");
1275 assert_eq!(removed, Some("my-workspace".to_string()));
1276 assert!(access.get_workspace_for_agent("agent-1").is_none());
1277 }
1278
1279 #[test]
1280 fn test_list_agents_in_workspace() {
1281 let mut access = AccessManager::new();
1282 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1283
1284 access.assign_workspace("agent-1", "my-workspace");
1285 access.assign_workspace("agent-2", "my-workspace");
1286 access.assign_workspace("agent-3", "other-workspace");
1287
1288 let agents = access.list_agents_in_workspace("my-workspace");
1289 assert_eq!(agents.len(), 2);
1290 assert!(agents.contains(&"agent-1".to_string()));
1291 assert!(agents.contains(&"agent-2".to_string()));
1292 assert!(!agents.contains(&"agent-3".to_string()));
1293 }
1294
1295 #[test]
1296 fn test_remove_workspace_unassigns_all_agents() {
1297 let mut access = AccessManager::new();
1298 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1299
1300 access.assign_workspace("agent-1", "my-workspace");
1301 access.assign_workspace("agent-2", "my-workspace");
1302
1303 access.remove_workspace("my-workspace");
1304
1305 assert!(access.list_workspaces().is_empty());
1306 assert!(access.get_workspace_for_agent("agent-1").is_none());
1307 assert!(access.get_workspace_for_agent("agent-2").is_none());
1308 }
1309
1310 #[test]
1311 fn test_is_path_in_workspace() {
1312 let mut access = AccessManager::new();
1313
1314 let workspace = PathBuf::from("/tmp/oxios-test-workspace");
1316
1317 std::fs::create_dir_all(&workspace).ok();
1319 std::fs::create_dir_all(workspace.join("subdir")).ok();
1320
1321 access.register_workspace_path("my-workspace", workspace.clone());
1323
1324 let inside_path = workspace.join("file.txt");
1326 std::fs::write(&inside_path, "test").ok(); assert!(
1329 access.is_path_in_workspace("my-workspace", inside_path.to_str().unwrap()),
1330 "Path {:?} should be inside workspace",
1331 inside_path
1332 );
1333
1334 let nested_path = workspace.join("subdir/nested.txt");
1335 std::fs::write(&nested_path, "test").ok();
1336 assert!(
1337 access.is_path_in_workspace("my-workspace", nested_path.to_str().unwrap()),
1338 "Path {:?} should be inside workspace",
1339 nested_path
1340 );
1341
1342 assert!(!access.is_path_in_workspace("my-workspace", "/tmp/other-workspace/file.txt"));
1344
1345 assert!(!access.is_path_in_workspace("nonexistent", "/tmp/test"));
1347
1348 std::fs::remove_dir_all(workspace).ok();
1350 }
1351}