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 #[cfg(unix)]
141 let result = {
142 use std::os::unix::fs::OpenOptionsExt;
143 std::fs::OpenOptions::new()
144 .create(true)
145 .append(true)
146 .mode(0o600)
147 .open(&audit_path)
148 };
149 #[cfg(not(unix))]
150 let result = std::fs::OpenOptions::new()
151 .create(true)
152 .append(true)
153 .open(&audit_path);
154 if let Ok(mut f) = result {
155 use std::io::Write;
156 let _ = writeln!(f, "{line}");
157 }
158 }
159 });
160 self.audit_writer_handle = Some(Arc::new(audit_handle));
161 }
162
163 self
164 }
165
166 pub fn can_use_tool(&mut self, agent_name: &str, tool: &str) -> bool {
170 let allowed = match self.permissions.get(agent_name) {
171 Some(perms) => perms.allowed_tools.contains(tool),
172 None => {
173 tracing::warn!(agent = %agent_name, tool = %tool, "Agent not found in access manager, denying");
174 false
175 }
176 };
177
178 let reason = if allowed {
179 None
180 } else {
181 Some("tool not in allowed set".to_string())
182 };
183
184 self.log_access(agent_name, "use_tool", tool, allowed, reason);
185
186 allowed
187 }
188
189 pub fn can_access_path(&mut self, agent_name: &str, path: &str) -> bool {
197 let allowed = match self.permissions.get(agent_name) {
198 Some(perms) => {
199 if perms.is_path_denied(path) {
201 false
202 } else {
203 perms.is_path_allowed(path)
204 }
205 }
206 None => {
207 tracing::warn!(agent = %agent_name, path = %path, "Agent not found, denying path access");
208 false
209 }
210 };
211
212 let reason = if allowed {
213 None
214 } else {
215 Some("path not in allowed set or is denied".to_string())
216 };
217
218 self.log_access(agent_name, "access_path", path, allowed, reason);
219
220 allowed
221 }
222
223 pub fn can_access_network(&mut self, agent_name: &str) -> bool {
227 let allowed = match self.permissions.get(agent_name) {
228 Some(perms) => perms.network_access,
229 None => false,
230 };
231
232 let reason = if allowed {
233 None
234 } else {
235 Some("network access not enabled".to_string())
236 };
237
238 self.log_access(agent_name, "network_request", "<network>", allowed, reason);
239
240 allowed
241 }
242
243 pub fn can_execute_for(&self, agent_name: &str, duration_secs: u64) -> bool {
248 match self.permissions.get(agent_name) {
249 Some(perms) => {
250 perms.max_execution_time_secs == 0 || duration_secs <= perms.max_execution_time_secs
251 }
252 None => false,
253 }
254 }
255
256 pub fn can_use_memory(&self, agent_name: &str, memory_mb: u64) -> bool {
261 match self.permissions.get(agent_name) {
262 Some(perms) => perms.max_memory_mb == 0 || memory_mb <= perms.max_memory_mb,
263 None => false,
264 }
265 }
266
267 pub fn can_fork(&self, agent_name: &str) -> bool {
269 match self.permissions.get(agent_name) {
270 Some(perms) => perms.can_fork,
271 None => false,
272 }
273 }
274
275 pub fn get_permissions(&self, agent_name: &str) -> Option<&AgentPermissions> {
279 self.permissions.get(agent_name)
280 }
281
282 pub fn get_or_create_permissions(&mut self, agent_name: &str) -> &mut AgentPermissions {
286 self.permissions
287 .entry(agent_name.to_string())
288 .or_insert_with(|| AgentPermissions::for_new_agent(agent_name))
289 }
290
291 pub fn set_permissions(&mut self, permissions: AgentPermissions) {
295 let agent_name = permissions.agent_name.clone();
296 self.permissions.insert(agent_name, permissions);
297 }
298
299 pub fn update_permissions(
304 &mut self,
305 agent_name: &str,
306 update: PermissionUpdate,
307 ) -> anyhow::Result<()> {
308 let perms = self
309 .permissions
310 .entry(agent_name.to_string())
311 .or_insert_with(|| AgentPermissions::for_new_agent(agent_name));
312 update.apply(perms);
313 Ok(())
314 }
315
316 pub fn remove_permissions(&mut self, agent_name: &str) {
320 self.permissions.remove(agent_name);
321 tracing::info!(agent = %agent_name, "Agent permissions removed");
322 }
323
324 pub fn list_agents(&self) -> Vec<String> {
326 self.permissions.keys().cloned().collect()
327 }
328
329 pub fn audit_log(&self) -> &[AuditEntry] {
333 &self.audit_log
334 }
335
336 pub fn audit_log_recent(&self, limit: usize) -> Vec<AuditEntry> {
341 let start = self.audit_log.len().saturating_sub(limit);
342 self.audit_log[start..].to_vec()
343 }
344
345 pub fn audit_log_for_agent(&self, agent_name: &str) -> Vec<AuditEntry> {
347 self.audit_log
348 .iter()
349 .filter(|e| e.agent_name == agent_name)
350 .cloned()
351 .collect()
352 }
353
354 pub fn denied_actions(&self) -> Vec<&AuditEntry> {
356 self.audit_log.iter().filter(|e| !e.allowed).collect()
357 }
358
359 pub fn rbac_manager(&self) -> &RbacManager {
361 &self.rbac
362 }
363
364 pub fn rbac_manager_mut(&mut self) -> &mut RbacManager {
366 &mut self.rbac
367 }
368
369 pub fn register_workspace_path(&mut self, workspace_name: &str, workspace_path: PathBuf) {
379 self.workspace_paths
380 .insert(workspace_name.to_string(), workspace_path);
381 tracing::debug!(workspace = %workspace_name, "Workspace path registered");
382 }
383
384 pub fn assign_workspace(&mut self, agent_name: &str, workspace_name: &str) -> bool {
395 if !self.workspace_paths.contains_key(workspace_name) {
396 tracing::warn!(agent = %agent_name, workspace = %workspace_name, "Cannot assign agent to non-existent workspace");
397 return false;
398 }
399
400 if let Some(prev_workspace) = self.agent_workspaces.get(agent_name)
402 && let Some(agents) = self.workspace_agents.get_mut(prev_workspace)
403 {
404 agents.remove(agent_name);
405 }
406
407 self.agent_workspaces
409 .insert(agent_name.to_string(), workspace_name.to_string());
410 self.workspace_agents
411 .entry(workspace_name.to_string())
412 .or_default()
413 .insert(agent_name.to_string());
414
415 tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent assigned to workspace");
416 true
417 }
418
419 pub fn get_workspace_for_agent(&self, agent_name: &str) -> Option<String> {
424 self.agent_workspaces.get(agent_name).cloned()
425 }
426
427 pub fn get_workspace_path(&self, workspace_name: &str) -> Option<&PathBuf> {
432 self.workspace_paths.get(workspace_name)
433 }
434
435 pub fn list_workspaces(&self) -> Vec<String> {
437 self.workspace_paths.keys().cloned().collect()
438 }
439
440 pub fn list_agents_in_workspace(&self, workspace_name: &str) -> Vec<String> {
442 self.workspace_agents
443 .get(workspace_name)
444 .map(|agents| agents.iter().cloned().collect())
445 .unwrap_or_default()
446 }
447
448 pub fn can_access_workspace(&self, agent_name: &str, workspace_name: &str) -> bool {
459 self.agent_workspaces
460 .get(agent_name)
461 .map(|w| w == workspace_name)
462 .unwrap_or(false)
463 }
464
465 pub fn is_path_in_workspace(&self, workspace_name: &str, path: &str) -> bool {
477 let workspace = match self.workspace_paths.get(workspace_name) {
478 Some(w) => w,
479 None => return false,
480 };
481
482 let requested_path = match Path::new(path).canonicalize() {
484 Ok(p) => p,
485 Err(_) => {
486 let candidate = workspace.join(path);
488 match candidate.canonicalize() {
489 Ok(p) => p,
490 Err(_) => return false,
491 }
492 }
493 };
494
495 let workspace_canonical = match workspace.canonicalize() {
497 Ok(w) => w,
498 Err(_) => return false,
499 };
500
501 requested_path.starts_with(&workspace_canonical)
502 }
503
504 pub fn can_access_path_in_workspace(
531 &mut self,
532 agent_id: &AgentId,
533 agent_name: &str,
534 path: &str,
535 workspace: Option<&str>,
536 ) -> bool {
537 let subject = Subject::Agent(*agent_id);
539 let action = Action::AccessPath(path.to_string());
540 let rbac_allowed = self.rbac.check_permission(&subject, &action, path);
541
542 let path_allowed = self.can_access_path(agent_name, path);
544
545 let workspace_allowed = if let Some(workspace_name) = workspace {
547 let is_in_workspace = self.is_path_in_workspace(workspace_name, path);
548
549 if !is_in_workspace {
550 self.log_access(
552 agent_name,
553 "sandbox_violation",
554 path,
555 false,
556 Some(format!(
557 "Path '{path}' is outside workspace '{workspace_name}' boundary"
558 )),
559 );
560 }
561
562 is_in_workspace
563 } else {
564 if let Some(assigned_workspace) = self.agent_workspaces.get(agent_name) {
566 let is_in_workspace = self.is_path_in_workspace(assigned_workspace, path);
567
568 if !is_in_workspace {
569 self.log_access(
570 agent_name,
571 "sandbox_violation",
572 path,
573 false,
574 Some(format!(
575 "Path '{path}' is outside assigned workspace '{assigned_workspace}' boundary"
576 )),
577 );
578 }
579
580 is_in_workspace
581 } else {
582 true
584 }
585 };
586
587 rbac_allowed && path_allowed && workspace_allowed
589 }
590
591 pub fn unassign_workspace(&mut self, agent_name: &str) -> Option<String> {
595 if let Some(workspace_name) = self.agent_workspaces.remove(agent_name) {
596 if let Some(agents) = self.workspace_agents.get_mut(&workspace_name) {
597 agents.remove(agent_name);
598 }
599 tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent unassigned from workspace");
600 Some(workspace_name)
601 } else {
602 None
603 }
604 }
605
606 pub fn remove_workspace(&mut self, workspace_name: &str) {
610 if let Some(agents) = self.workspace_agents.remove(workspace_name) {
612 for agent_name in agents {
613 self.agent_workspaces.remove(&agent_name);
614 }
615 }
616
617 self.workspace_paths.remove(workspace_name);
619
620 tracing::info!(workspace = %workspace_name, "Workspace removed from access manager");
621 }
622
623 pub fn clear_audit_log(&mut self) {
628 let count = self.audit_log.len();
629 self.log_access(
630 "system",
631 "audit_clear",
632 &format!("{count} entries"),
633 true,
634 Some("audit log cleared by request".to_string()),
635 );
636 self.audit_log.clear();
637 tracing::info!(cleared = count, "Audit log cleared");
638 }
639
640 pub(crate) fn log_access(
645 &mut self,
646 agent_name: &str,
647 action: &str,
648 resource: &str,
649 allowed: bool,
650 reason: Option<String>,
651 ) {
652 let entry = AuditEntry::new(agent_name, action, resource, allowed, reason.clone());
653
654 self.audit_log.push(entry.clone());
655
656 if self.audit_log.len() > self.max_audit_entries {
658 let prune_count = self.audit_log.len() - self.max_audit_entries;
659 self.audit_log.drain(0..prune_count);
660 }
661
662 self.persist_audit_entry(&entry);
664
665 if !allowed {
667 tracing::warn!(
668 agent = %agent_name,
669 action = %action,
670 resource = %resource,
671 reason = ?reason,
672 "Access denied"
673 );
674 }
675 }
676
677 fn persist_audit_entry(&self, entry: &AuditEntry) {
683 if self.audit_log_path.is_none() {
684 return;
685 }
686 let line = match serde_json::to_string(entry) {
687 Ok(s) => s,
688 Err(_) => return,
689 };
690 if let Some(sender) = &self.audit_sender {
691 match sender.try_send(line) {
692 Ok(()) => {}
693 Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
694 tracing::warn!("Audit log channel full — dropping entry");
695 }
696 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
697 tracing::warn!("Audit log channel closed — dropping entry");
698 }
699 }
700 }
701 }
702
703 pub fn validate_permissions(&self, perms: &AgentPermissions) -> Vec<String> {
707 let mut warnings = Vec::new();
708
709 if perms.allowed_tools.is_empty() {
710 warnings.push("Agent has no allowed tools".to_string());
711 }
712
713 if perms.allowed_paths.is_empty() {
714 warnings.push(
715 "Agent has no path restrictions (paths granted by ensure_permissions)".to_string(),
716 );
717 }
718
719 if perms.network_access {
720 warnings.push("Agent has network access enabled".to_string());
721 }
722
723 if perms.can_fork {
724 warnings.push("Agent can fork sub-agents".to_string());
725 }
726
727 if perms.max_execution_time_secs == 0 {
728 warnings.push("Agent has no execution time limit".to_string());
729 }
730
731 if perms.max_memory_mb == 0 {
732 warnings.push("Agent has no memory limit".to_string());
733 }
734
735 warnings
736 }
737}
738
739impl Default for AccessManager {
740 fn default() -> Self {
741 Self::new()
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
752 fn test_default_permissions() {
753 let perms = AgentPermissions::default();
754 assert!(perms.allowed_tools.contains("bash"));
755 assert!(!perms.network_access);
756 assert!(!perms.can_fork);
757 assert_eq!(perms.max_execution_time_secs, 300);
758 assert_eq!(perms.max_memory_mb, 512);
759 }
760
761 #[test]
762 fn test_for_new_agent() {
763 let perms = AgentPermissions::for_new_agent("my-agent");
764 assert_eq!(perms.agent_name, "my-agent");
765 assert!(perms.allowed_tools.contains("bash"));
766 }
767
768 #[test]
769 fn test_allow_deny_tool() {
770 let mut perms = AgentPermissions::for_new_agent("test");
771 assert!(perms.allowed_tools.contains("bash"));
772
773 perms.deny_tool("bash");
774 assert!(!perms.allowed_tools.contains("bash"));
775
776 perms.allow_tool("custom");
777 assert!(perms.allowed_tools.contains("custom"));
778 }
779
780 #[test]
781 fn test_allow_deny_path() {
782 let mut perms = AgentPermissions::for_new_agent("test");
783
784 perms.allow_path("/workspace/**");
785 assert!(perms.allowed_paths.contains(&"/workspace/**".to_string()));
786
787 perms.deny_path("/workspace/.secret/**");
788 assert!(
789 perms
790 .denied_paths
791 .contains(&"/workspace/.secret/**".to_string())
792 );
793 }
794
795 #[test]
796 fn test_enable_network() {
797 let mut perms = AgentPermissions::for_new_agent("test");
798 assert!(!perms.network_access);
799
800 perms.enable_network();
801 assert!(perms.network_access);
802 }
803
804 #[test]
805 fn test_enable_forking() {
806 let mut perms = AgentPermissions::for_new_agent("test");
807 assert!(!perms.can_fork);
808
809 perms.enable_forking();
810 assert!(perms.can_fork);
811 }
812
813 #[test]
814 fn test_path_matching_allowed() {
815 let mut perms = AgentPermissions::for_new_agent("test");
816 perms.allowed_paths = vec!["/workspace/**".to_string(), "/home/*/docs/**".to_string()];
817 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
818
819 assert!(perms.is_path_allowed("/workspace/project/file.rs"));
821 assert!(perms.is_path_allowed("/home/user/docs/readme.md"));
822
823 assert!(!perms.is_path_allowed("/etc/passwd"));
825 assert!(!perms.is_path_allowed("/home/user/secret.txt"));
826 }
827
828 #[test]
829 fn test_path_matching_denied() {
830 let mut perms = AgentPermissions::for_new_agent("test");
831 perms.allowed_paths = vec!["/workspace/**".to_string()];
832 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
833
834 assert!(perms.is_path_denied("/workspace/.oxios/config.toml"));
836 assert!(!perms.is_path_denied("/workspace/project/file.rs"));
837
838 let _access = AccessManager::new();
840 let mut perms2 = perms.clone();
841 perms2.agent_name = "test".to_string();
842 }
846
847 #[test]
848 fn test_path_denied_pattern_matching() {
849 let mut perms = AgentPermissions::for_new_agent("test");
850 perms.denied_paths = vec!["/etc/**".to_string(), "**/secrets/*".to_string()];
851
852 assert!(perms.is_path_denied("/etc/passwd"));
853 assert!(perms.is_path_denied("/etc/shadow"));
854 assert!(!perms.is_path_denied("/workspace/file"));
855 }
856
857 #[test]
860 fn test_can_use_tool_allowed() {
861 let mut access = AccessManager::new();
862
863 let mut perms = AgentPermissions::for_new_agent("code-agent");
864 perms.allow_tool("bash");
865 perms.allow_tool("read");
866 access.set_permissions(perms);
867
868 assert!(access.can_use_tool("code-agent", "bash"));
869 assert!(access.can_use_tool("code-agent", "read"));
870 }
871
872 #[test]
873 fn test_can_use_tool_denied() {
874 let mut access = AccessManager::new();
875
876 let mut perms = AgentPermissions::for_new_agent("code-agent");
877 perms.allow_tool("read");
878 perms.deny_tool("bash"); access.set_permissions(perms);
880
881 assert!(!access.can_use_tool("code-agent", "bash")); assert!(!access.can_use_tool("code-agent", "spawn")); assert!(!access.can_use_tool("unknown-agent", "bash")); }
885
886 #[test]
887 fn test_unknown_agent_denied_all_tools() {
888 let mut access = AccessManager::new();
889
890 assert!(!access.can_use_tool("unknown-agent", "read"));
892 assert!(!access.can_access_path("unknown-agent", "/workspace/test.txt"));
893 assert!(!access.can_access_network("unknown-agent"));
894 assert!(!access.can_fork("unknown-agent"));
895 }
896
897 #[test]
900 fn test_can_access_path_allowed() {
901 let mut access = AccessManager::new();
902
903 let mut perms = AgentPermissions::for_new_agent("file-agent");
904 perms.allow_path("/workspace/**");
905 access.set_permissions(perms);
906
907 assert!(access.can_access_path("file-agent", "/workspace/project/file.rs"));
908 assert!(!access.can_access_path("file-agent", "/etc/passwd"));
909 }
910
911 #[test]
912 fn test_can_access_path_denied_takes_precedence() {
913 let mut access = AccessManager::new();
914
915 let mut perms = AgentPermissions::for_new_agent("test");
916 perms.allowed_paths = vec!["/workspace/**".to_string()];
917 perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
918 access.set_permissions(perms);
919
920 assert!(!access.can_access_path("test", "/workspace/.oxios/config.toml"));
922
923 assert!(access.can_access_path("test", "/workspace/project/file.rs"));
925 }
926
927 #[test]
930 fn test_can_access_network() {
931 let mut access = AccessManager::new();
932
933 let mut perms = AgentPermissions::for_new_agent("net-agent");
934 perms.enable_network();
935 access.set_permissions(perms);
936
937 assert!(access.can_access_network("net-agent"));
938 assert!(!access.can_access_network("no-net-agent"));
939 }
940
941 #[test]
944 fn test_can_execute_for() {
945 let mut access = AccessManager::new();
946
947 let mut perms = AgentPermissions::for_new_agent("test");
948 perms.max_execution_time_secs = 300;
949 access.set_permissions(perms);
950
951 assert!(access.can_execute_for("test", 100));
952 assert!(access.can_execute_for("test", 300));
953 assert!(!access.can_execute_for("test", 301));
954 }
955
956 #[test]
957 fn test_unlimited_execution_time() {
958 let mut access = AccessManager::new();
959
960 let mut perms = AgentPermissions::for_new_agent("test");
961 perms.max_execution_time_secs = 0; access.set_permissions(perms);
963
964 assert!(access.can_execute_for("test", 100_000));
965 }
966
967 #[test]
968 fn test_can_use_memory() {
969 let mut access = AccessManager::new();
970
971 let mut perms = AgentPermissions::for_new_agent("test");
972 perms.max_memory_mb = 512;
973 access.set_permissions(perms);
974
975 assert!(access.can_use_memory("test", 256));
976 assert!(access.can_use_memory("test", 512));
977 assert!(!access.can_use_memory("test", 513));
978 }
979
980 #[test]
981 fn test_unlimited_memory() {
982 let mut access = AccessManager::new();
983
984 let mut perms = AgentPermissions::for_new_agent("test");
985 perms.max_memory_mb = 0;
986 access.set_permissions(perms);
987
988 assert!(access.can_use_memory("test", 1_000_000));
989 }
990
991 #[test]
994 fn test_can_fork() {
995 let mut access = AccessManager::new();
996
997 let mut perms = AgentPermissions::for_new_agent("test");
998 perms.enable_forking();
999 access.set_permissions(perms);
1000
1001 assert!(access.can_fork("test"));
1002 assert!(!access.can_fork("no-fork-agent"));
1003 }
1004
1005 #[test]
1008 fn test_set_and_get_permissions() {
1009 let mut access = AccessManager::new();
1010
1011 let perms = AgentPermissions::for_new_agent("test-agent");
1012 access.set_permissions(perms);
1013
1014 let retrieved = access.get_permissions("test-agent");
1015 assert!(retrieved.is_some());
1016 assert_eq!(retrieved.unwrap().agent_name, "test-agent");
1017 }
1018
1019 #[test]
1020 fn test_get_nonexistent_permissions() {
1021 let access = AccessManager::new();
1022 assert!(access.get_permissions("ghost").is_none());
1023 }
1024
1025 #[test]
1026 fn test_get_or_create_permissions() {
1027 let mut access = AccessManager::new();
1028
1029 let perms = access.get_or_create_permissions("new-agent");
1031 assert_eq!(perms.agent_name, "new-agent");
1032
1033 let perms2 = access.get_or_create_permissions("new-agent");
1035 assert_eq!(perms2.agent_name, "new-agent");
1036 }
1037
1038 #[test]
1039 fn test_remove_permissions() {
1040 let mut access = AccessManager::new();
1041
1042 let perms = AgentPermissions::for_new_agent("to-remove");
1043 access.set_permissions(perms);
1044
1045 assert!(access.get_permissions("to-remove").is_some());
1046
1047 access.remove_permissions("to-remove");
1048
1049 assert!(access.get_permissions("to-remove").is_none());
1050 assert!(!access.can_use_tool("to-remove", "bash"));
1052 }
1053
1054 #[test]
1055 fn test_list_agents() {
1056 let mut access = AccessManager::new();
1057
1058 access.set_permissions(AgentPermissions::for_new_agent("agent-1"));
1059 access.set_permissions(AgentPermissions::for_new_agent("agent-2"));
1060
1061 let agents = access.list_agents();
1062 assert_eq!(agents.len(), 2);
1063 assert!(agents.contains(&"agent-1".to_string()));
1064 assert!(agents.contains(&"agent-2".to_string()));
1065 }
1066
1067 #[test]
1070 fn test_audit_log_records_access() {
1071 let mut access = AccessManager::new();
1072
1073 let perms = AgentPermissions::for_new_agent("test-agent");
1074 access.set_permissions(perms);
1075
1076 access.can_use_tool("test-agent", "bash"); access.can_use_tool("test-agent", "network"); let log = access.audit_log();
1080 assert_eq!(log.len(), 2);
1081 assert!(log[0].allowed);
1082 assert!(!log[1].allowed);
1083 assert_eq!(log[0].agent_name, "test-agent");
1084 assert_eq!(log[0].action, "use_tool");
1085 assert_eq!(log[0].resource, "bash");
1086 }
1087
1088 #[test]
1089 fn test_audit_log_recent() {
1090 let mut access = AccessManager::new();
1091
1092 let perms = AgentPermissions::for_new_agent("test");
1093 access.set_permissions(perms);
1094
1095 for i in 0..10 {
1096 access.can_use_tool("test", &format!("tool-{}", i));
1097 }
1098
1099 let recent = access.audit_log_recent(3);
1100 assert_eq!(recent.len(), 3);
1101 }
1102
1103 #[test]
1104 fn test_audit_log_for_agent() {
1105 let mut access = AccessManager::new();
1106
1107 access.set_permissions(AgentPermissions::for_new_agent("agent-a"));
1108 access.set_permissions(AgentPermissions::for_new_agent("agent-b"));
1109
1110 access.can_use_tool("agent-a", "tool1");
1111 access.can_use_tool("agent-b", "tool2");
1112 access.can_use_tool("agent-a", "tool3");
1113
1114 let log_a = access.audit_log_for_agent("agent-a");
1115 assert_eq!(log_a.len(), 2);
1116 }
1117
1118 #[test]
1119 fn test_denied_actions() {
1120 let mut access = AccessManager::new();
1121
1122 let perms = AgentPermissions::for_new_agent("test");
1123 access.set_permissions(perms);
1124
1125 access.can_use_tool("test", "bash"); access.can_use_tool("test", "dangerous"); access.can_access_path("test", "/etc/shadow"); let denied = access.denied_actions();
1130 assert_eq!(denied.len(), 2);
1131 }
1132
1133 #[test]
1134 fn test_clear_audit_log() {
1135 let mut access = AccessManager::new();
1136
1137 let perms = AgentPermissions::for_new_agent("test");
1138 access.set_permissions(perms);
1139
1140 for _ in 0..5 {
1141 access.can_use_tool("test", "tool");
1142 }
1143
1144 assert_eq!(access.audit_log().len(), 5);
1145
1146 access.clear_audit_log();
1147
1148 assert!(access.audit_log().is_empty());
1149 }
1150
1151 #[test]
1154 fn test_audit_log_prunes_old_entries() {
1155 let mut access = AccessManager::with_max_audit_entries(5);
1156
1157 let perms = AgentPermissions::for_new_agent("test");
1158 access.set_permissions(perms);
1159
1160 for i in 0..10 {
1162 access.can_use_tool("test", &format!("tool-{}", i));
1163 }
1164
1165 assert_eq!(access.audit_log().len(), 5);
1167 }
1168
1169 #[test]
1172 fn test_validate_permissions_no_tools() {
1173 let mut access = AccessManager::new();
1174 let mut perms = AgentPermissions::for_new_agent("test");
1175 perms.allowed_tools.clear();
1176 access.set_permissions(perms.clone());
1177
1178 let warnings = access.validate_permissions(&perms);
1179 assert!(warnings.iter().any(|w| w.contains("no allowed tools")));
1180 }
1181
1182 #[test]
1183 fn test_validate_permissions_no_path_restrictions() {
1184 let mut perms = AgentPermissions::for_new_agent("test");
1185 perms.allowed_paths.clear();
1186
1187 let access = AccessManager::new();
1188 let warnings = access.validate_permissions(&perms);
1189 assert!(warnings.iter().any(|w| w.contains("no path restrictions")));
1190 }
1191
1192 #[test]
1193 fn test_validate_permissions_warnings() {
1194 let mut access = AccessManager::new();
1195 let mut perms = AgentPermissions::for_new_agent("test");
1196 perms.network_access = true;
1197 perms.can_fork = true;
1198 perms.max_execution_time_secs = 0;
1199 perms.max_memory_mb = 0;
1200 access.set_permissions(perms.clone());
1201
1202 let warnings = access.validate_permissions(&perms);
1203 assert!(warnings.iter().any(|w| w.contains("network access")));
1204 assert!(warnings.iter().any(|w| w.contains("fork sub-agents")));
1205 assert!(
1206 warnings
1207 .iter()
1208 .any(|w| w.contains("no execution time limit"))
1209 );
1210 assert!(warnings.iter().any(|w| w.contains("no memory limit")));
1211 }
1212
1213 #[test]
1216 fn test_audit_entry_has_timestamp() {
1217 let entry = AuditEntry::new("agent", "action", "resource", true, None);
1218 assert!(entry.timestamp.timestamp() > 0);
1220 }
1221
1222 #[test]
1225 fn test_register_workspace_path() {
1226 let mut access = AccessManager::new();
1227 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my-workspace"));
1228
1229 assert_eq!(access.list_workspaces(), vec!["my-workspace"]);
1230 assert_eq!(
1231 access.get_workspace_path("my-workspace"),
1232 Some(&PathBuf::from("/workspace/my-workspace"))
1233 );
1234 }
1235
1236 #[test]
1237 fn test_assign_agent_to_workspace() {
1238 let mut access = AccessManager::new();
1239 access.register_workspace_path("project-alpha", PathBuf::from("/workspace/alpha"));
1240
1241 assert!(access.assign_workspace("agent-1", "project-alpha"));
1243
1244 assert_eq!(
1246 access.get_workspace_for_agent("agent-1"),
1247 Some("project-alpha".to_string())
1248 );
1249 assert!(access.can_access_workspace("agent-1", "project-alpha"));
1250 assert!(!access.can_access_workspace("agent-1", "other-workspace"));
1251 }
1252
1253 #[test]
1254 fn test_assign_agent_to_nonexistent_workspace_fails() {
1255 let mut access = AccessManager::new();
1256
1257 assert!(!access.assign_workspace("agent-1", "nonexistent"));
1259 assert_eq!(access.get_workspace_for_agent("agent-1"), None);
1260 }
1261
1262 #[test]
1263 fn test_reassign_agent_to_different_workspace() {
1264 let mut access = AccessManager::new();
1265 access.register_workspace_path("workspace-a", PathBuf::from("/workspace/a"));
1266 access.register_workspace_path("workspace-b", PathBuf::from("/workspace/b"));
1267
1268 access.assign_workspace("agent-1", "workspace-a");
1270 assert_eq!(
1271 access.get_workspace_for_agent("agent-1"),
1272 Some("workspace-a".to_string())
1273 );
1274
1275 access.assign_workspace("agent-1", "workspace-b");
1277 assert_eq!(
1278 access.get_workspace_for_agent("agent-1"),
1279 Some("workspace-b".to_string())
1280 );
1281
1282 assert!(!access.can_access_workspace("agent-1", "workspace-a"));
1284 }
1285
1286 #[test]
1287 fn test_unassign_agent_from_workspace() {
1288 let mut access = AccessManager::new();
1289 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1290
1291 access.assign_workspace("agent-1", "my-workspace");
1292 assert!(access.get_workspace_for_agent("agent-1").is_some());
1293
1294 let removed = access.unassign_workspace("agent-1");
1295 assert_eq!(removed, Some("my-workspace".to_string()));
1296 assert!(access.get_workspace_for_agent("agent-1").is_none());
1297 }
1298
1299 #[test]
1300 fn test_list_agents_in_workspace() {
1301 let mut access = AccessManager::new();
1302 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1303
1304 access.assign_workspace("agent-1", "my-workspace");
1305 access.assign_workspace("agent-2", "my-workspace");
1306 access.assign_workspace("agent-3", "other-workspace");
1307
1308 let agents = access.list_agents_in_workspace("my-workspace");
1309 assert_eq!(agents.len(), 2);
1310 assert!(agents.contains(&"agent-1".to_string()));
1311 assert!(agents.contains(&"agent-2".to_string()));
1312 assert!(!agents.contains(&"agent-3".to_string()));
1313 }
1314
1315 #[test]
1316 fn test_remove_workspace_unassigns_all_agents() {
1317 let mut access = AccessManager::new();
1318 access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
1319
1320 access.assign_workspace("agent-1", "my-workspace");
1321 access.assign_workspace("agent-2", "my-workspace");
1322
1323 access.remove_workspace("my-workspace");
1324
1325 assert!(access.list_workspaces().is_empty());
1326 assert!(access.get_workspace_for_agent("agent-1").is_none());
1327 assert!(access.get_workspace_for_agent("agent-2").is_none());
1328 }
1329
1330 #[test]
1331 fn test_is_path_in_workspace() {
1332 let mut access = AccessManager::new();
1333
1334 let workspace = PathBuf::from("/tmp/oxios-test-workspace");
1336
1337 std::fs::create_dir_all(&workspace).ok();
1339 std::fs::create_dir_all(workspace.join("subdir")).ok();
1340
1341 access.register_workspace_path("my-workspace", workspace.clone());
1343
1344 let inside_path = workspace.join("file.txt");
1346 std::fs::write(&inside_path, "test").ok(); assert!(
1349 access.is_path_in_workspace("my-workspace", inside_path.to_str().unwrap()),
1350 "Path {:?} should be inside workspace",
1351 inside_path
1352 );
1353
1354 let nested_path = workspace.join("subdir/nested.txt");
1355 std::fs::write(&nested_path, "test").ok();
1356 assert!(
1357 access.is_path_in_workspace("my-workspace", nested_path.to_str().unwrap()),
1358 "Path {:?} should be inside workspace",
1359 nested_path
1360 );
1361
1362 assert!(!access.is_path_in_workspace("my-workspace", "/tmp/other-workspace/file.txt"));
1364
1365 assert!(!access.is_path_in_workspace("nonexistent", "/tmp/test"));
1367
1368 std::fs::remove_dir_all(workspace).ok();
1370 }
1371}