mod audit_sink;
mod context;
mod gate;
mod permissions;
mod rbac;
#[cfg(test)]
pub use audit_sink::NoOpAuditSink;
pub use audit_sink::{AuditEvent, AuditSink, TracingAuditSink, TrailAuditSink};
pub use context::AgentContext;
pub use gate::{AccessDenied, AccessGate, CheckRequest, DenyLayer, PathMode};
pub use permissions::{AgentPermissions, AuditEntry, PermissionUpdate};
pub use rbac::{
Action, ApprovalStatus, PendingApproval, RbacAuditEntry, RbacManager, RbacPolicy, Role, Subject,
};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::types::AgentId;
#[derive(Debug, Clone)]
pub struct AccessManager {
permissions: HashMap<String, AgentPermissions>,
audit_log: Vec<AuditEntry>,
#[allow(dead_code)]
audit_log_path: Option<std::path::PathBuf>,
max_audit_entries: usize,
pub(crate) rbac: RbacManager,
workspace_paths: HashMap<String, PathBuf>,
agent_workspaces: HashMap<String, String>,
workspace_agents: HashMap<String, HashSet<String>>,
audit_sender: Option<tokio::sync::mpsc::Sender<String>>,
#[allow(dead_code)]
audit_writer_handle: Option<Arc<tokio::task::JoinHandle<()>>>,
}
impl AccessManager {
pub fn new() -> Self {
Self {
permissions: HashMap::new(),
audit_log: Vec::new(),
audit_log_path: None,
max_audit_entries: 10_000,
rbac: RbacManager::new(),
workspace_paths: HashMap::new(),
agent_workspaces: HashMap::new(),
workspace_agents: HashMap::new(),
audit_sender: None,
audit_writer_handle: None,
}
}
pub fn with_max_audit_entries(max_audit_entries: usize) -> Self {
Self {
permissions: HashMap::new(),
audit_log: Vec::new(),
audit_log_path: None,
max_audit_entries,
rbac: RbacManager::new(),
workspace_paths: HashMap::new(),
agent_workspaces: HashMap::new(),
workspace_agents: HashMap::new(),
audit_sender: None,
audit_writer_handle: None,
}
}
pub fn with_audit_log_path(mut self, path: std::path::PathBuf) -> Self {
self.audit_log_path = Some(path.clone());
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(1000);
self.audit_sender = Some(tx);
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let audit_path = path;
let audit_handle = handle.spawn(async move {
while let Some(line) = rx.recv().await {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&audit_path)
{
use std::io::Write;
let _ = writeln!(f, "{line}");
}
}
});
self.audit_writer_handle = Some(Arc::new(audit_handle));
}
self
}
pub fn can_use_tool(&mut self, agent_name: &str, tool: &str) -> bool {
let allowed = match self.permissions.get(agent_name) {
Some(perms) => perms.allowed_tools.contains(tool),
None => {
tracing::warn!(agent = %agent_name, tool = %tool, "Agent not found in access manager, denying");
false
}
};
let reason = if allowed {
None
} else {
Some("tool not in allowed set".to_string())
};
self.log_access(agent_name, "use_tool", tool, allowed, reason);
allowed
}
pub fn can_access_path(&mut self, agent_name: &str, path: &str) -> bool {
let allowed = match self.permissions.get(agent_name) {
Some(perms) => {
if perms.is_path_denied(path) {
false
} else {
perms.is_path_allowed(path)
}
}
None => {
tracing::warn!(agent = %agent_name, path = %path, "Agent not found, denying path access");
false
}
};
let reason = if allowed {
None
} else {
Some("path not in allowed set or is denied".to_string())
};
self.log_access(agent_name, "access_path", path, allowed, reason);
allowed
}
pub fn can_access_network(&mut self, agent_name: &str) -> bool {
let allowed = match self.permissions.get(agent_name) {
Some(perms) => perms.network_access,
None => false,
};
let reason = if allowed {
None
} else {
Some("network access not enabled".to_string())
};
self.log_access(agent_name, "network_request", "<network>", allowed, reason);
allowed
}
pub fn can_execute_for(&self, agent_name: &str, duration_secs: u64) -> bool {
match self.permissions.get(agent_name) {
Some(perms) => {
perms.max_execution_time_secs == 0 || duration_secs <= perms.max_execution_time_secs
}
None => false,
}
}
pub fn can_use_memory(&self, agent_name: &str, memory_mb: u64) -> bool {
match self.permissions.get(agent_name) {
Some(perms) => perms.max_memory_mb == 0 || memory_mb <= perms.max_memory_mb,
None => false,
}
}
pub fn can_fork(&self, agent_name: &str) -> bool {
match self.permissions.get(agent_name) {
Some(perms) => perms.can_fork,
None => false,
}
}
pub fn get_permissions(&self, agent_name: &str) -> Option<&AgentPermissions> {
self.permissions.get(agent_name)
}
pub fn get_or_create_permissions(&mut self, agent_name: &str) -> &mut AgentPermissions {
self.permissions
.entry(agent_name.to_string())
.or_insert_with(|| AgentPermissions::for_new_agent(agent_name))
}
pub fn set_permissions(&mut self, permissions: AgentPermissions) {
let agent_name = permissions.agent_name.clone();
self.permissions.insert(agent_name, permissions);
}
pub fn update_permissions(
&mut self,
agent_name: &str,
update: PermissionUpdate,
) -> anyhow::Result<()> {
let perms = self
.permissions
.entry(agent_name.to_string())
.or_insert_with(|| AgentPermissions::for_new_agent(agent_name));
update.apply(perms);
Ok(())
}
pub fn remove_permissions(&mut self, agent_name: &str) {
self.permissions.remove(agent_name);
tracing::info!(agent = %agent_name, "Agent permissions removed");
}
pub fn list_agents(&self) -> Vec<String> {
self.permissions.keys().cloned().collect()
}
pub fn audit_log(&self) -> &[AuditEntry] {
&self.audit_log
}
pub fn audit_log_recent(&self, limit: usize) -> Vec<AuditEntry> {
let start = self.audit_log.len().saturating_sub(limit);
self.audit_log[start..].to_vec()
}
pub fn audit_log_for_agent(&self, agent_name: &str) -> Vec<AuditEntry> {
self.audit_log
.iter()
.filter(|e| e.agent_name == agent_name)
.cloned()
.collect()
}
pub fn denied_actions(&self) -> Vec<&AuditEntry> {
self.audit_log.iter().filter(|e| !e.allowed).collect()
}
pub fn rbac_manager(&self) -> &RbacManager {
&self.rbac
}
pub fn rbac_manager_mut(&mut self) -> &mut RbacManager {
&mut self.rbac
}
pub fn register_workspace_path(&mut self, workspace_name: &str, workspace_path: PathBuf) {
self.workspace_paths
.insert(workspace_name.to_string(), workspace_path);
tracing::debug!(workspace = %workspace_name, "Workspace path registered");
}
pub fn assign_workspace(&mut self, agent_name: &str, workspace_name: &str) -> bool {
if !self.workspace_paths.contains_key(workspace_name) {
tracing::warn!(agent = %agent_name, workspace = %workspace_name, "Cannot assign agent to non-existent workspace");
return false;
}
if let Some(prev_workspace) = self.agent_workspaces.get(agent_name) {
if let Some(agents) = self.workspace_agents.get_mut(prev_workspace) {
agents.remove(agent_name);
}
}
self.agent_workspaces
.insert(agent_name.to_string(), workspace_name.to_string());
self.workspace_agents
.entry(workspace_name.to_string())
.or_default()
.insert(agent_name.to_string());
tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent assigned to workspace");
true
}
pub fn get_workspace_for_agent(&self, agent_name: &str) -> Option<String> {
self.agent_workspaces.get(agent_name).cloned()
}
pub fn get_workspace_path(&self, workspace_name: &str) -> Option<&PathBuf> {
self.workspace_paths.get(workspace_name)
}
pub fn list_workspaces(&self) -> Vec<String> {
self.workspace_paths.keys().cloned().collect()
}
pub fn list_agents_in_workspace(&self, workspace_name: &str) -> Vec<String> {
self.workspace_agents
.get(workspace_name)
.map(|agents| agents.iter().cloned().collect())
.unwrap_or_default()
}
pub fn can_access_workspace(&self, agent_name: &str, workspace_name: &str) -> bool {
self.agent_workspaces
.get(agent_name)
.map(|w| w == workspace_name)
.unwrap_or(false)
}
pub fn is_path_in_workspace(&self, workspace_name: &str, path: &str) -> bool {
let workspace = match self.workspace_paths.get(workspace_name) {
Some(w) => w,
None => return false,
};
let requested_path = match Path::new(path).canonicalize() {
Ok(p) => p,
Err(_) => {
let candidate = workspace.join(path);
match candidate.canonicalize() {
Ok(p) => p,
Err(_) => return false,
}
}
};
let workspace_canonical = match workspace.canonicalize() {
Ok(w) => w,
Err(_) => return false,
};
requested_path.starts_with(&workspace_canonical)
}
pub fn can_access_path_in_workspace(
&mut self,
agent_id: &AgentId,
agent_name: &str,
path: &str,
workspace: Option<&str>,
) -> bool {
let subject = Subject::Agent(*agent_id);
let action = Action::AccessPath(path.to_string());
let rbac_allowed = self.rbac.check_permission(&subject, &action, path);
let path_allowed = self.can_access_path(agent_name, path);
let workspace_allowed = if let Some(workspace_name) = workspace {
let is_in_workspace = self.is_path_in_workspace(workspace_name, path);
if !is_in_workspace {
self.log_access(
agent_name,
"sandbox_violation",
path,
false,
Some(format!(
"Path '{path}' is outside workspace '{workspace_name}' boundary"
)),
);
}
is_in_workspace
} else {
if let Some(assigned_workspace) = self.agent_workspaces.get(agent_name) {
let is_in_workspace = self.is_path_in_workspace(assigned_workspace, path);
if !is_in_workspace {
self.log_access(
agent_name,
"sandbox_violation",
path,
false,
Some(format!(
"Path '{path}' is outside assigned workspace '{assigned_workspace}' boundary"
)),
);
}
is_in_workspace
} else {
true
}
};
rbac_allowed && path_allowed && workspace_allowed
}
pub fn unassign_workspace(&mut self, agent_name: &str) -> Option<String> {
if let Some(workspace_name) = self.agent_workspaces.remove(agent_name) {
if let Some(agents) = self.workspace_agents.get_mut(&workspace_name) {
agents.remove(agent_name);
}
tracing::info!(agent = %agent_name, workspace = %workspace_name, "Agent unassigned from workspace");
Some(workspace_name)
} else {
None
}
}
pub fn remove_workspace(&mut self, workspace_name: &str) {
if let Some(agents) = self.workspace_agents.remove(workspace_name) {
for agent_name in agents {
self.agent_workspaces.remove(&agent_name);
}
}
self.workspace_paths.remove(workspace_name);
tracing::info!(workspace = %workspace_name, "Workspace removed from access manager");
}
pub fn clear_audit_log(&mut self) {
let count = self.audit_log.len();
self.audit_log.clear();
tracing::info!(cleared = count, "Audit log cleared");
}
pub(crate) fn log_access(
&mut self,
agent_name: &str,
action: &str,
resource: &str,
allowed: bool,
reason: Option<String>,
) {
let entry = AuditEntry::new(agent_name, action, resource, allowed, reason.clone());
self.audit_log.push(entry.clone());
if self.audit_log.len() > self.max_audit_entries {
let prune_count = self.audit_log.len() - self.max_audit_entries;
self.audit_log.drain(0..prune_count);
}
self.persist_audit_entry(&entry);
if !allowed {
tracing::warn!(
agent = %agent_name,
action = %action,
resource = %resource,
reason = ?reason,
"Access denied"
);
}
}
fn persist_audit_entry(&self, entry: &AuditEntry) {
if self.audit_log_path.is_none() {
return;
}
let line = match serde_json::to_string(entry) {
Ok(s) => s,
Err(_) => return,
};
if let Some(sender) = &self.audit_sender {
match sender.try_send(line) {
Ok(()) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
tracing::warn!("Audit log channel full — dropping entry");
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
tracing::warn!("Audit log channel closed — dropping entry");
}
}
}
}
pub fn validate_permissions(&self, perms: &AgentPermissions) -> Vec<String> {
let mut warnings = Vec::new();
if perms.allowed_tools.is_empty() {
warnings.push("Agent has no allowed tools".to_string());
}
if perms.allowed_paths.is_empty() {
warnings.push("Agent has no path restrictions (wide open)".to_string());
}
if perms.network_access {
warnings.push("Agent has network access enabled".to_string());
}
if perms.can_fork {
warnings.push("Agent can fork sub-agents".to_string());
}
if perms.max_execution_time_secs == 0 {
warnings.push("Agent has no execution time limit".to_string());
}
if perms.max_memory_mb == 0 {
warnings.push("Agent has no memory limit".to_string());
}
warnings
}
}
impl Default for AccessManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_permissions() {
let perms = AgentPermissions::default();
assert!(perms.allowed_tools.contains("bash"));
assert!(!perms.network_access);
assert!(!perms.can_fork);
assert_eq!(perms.max_execution_time_secs, 300);
assert_eq!(perms.max_memory_mb, 512);
}
#[test]
fn test_for_new_agent() {
let perms = AgentPermissions::for_new_agent("my-agent");
assert_eq!(perms.agent_name, "my-agent");
assert!(perms.allowed_tools.contains("bash"));
}
#[test]
fn test_allow_deny_tool() {
let mut perms = AgentPermissions::for_new_agent("test");
assert!(perms.allowed_tools.contains("bash"));
perms.deny_tool("bash");
assert!(!perms.allowed_tools.contains("bash"));
perms.allow_tool("custom");
assert!(perms.allowed_tools.contains("custom"));
}
#[test]
fn test_allow_deny_path() {
let mut perms = AgentPermissions::for_new_agent("test");
perms.allow_path("/workspace/**");
assert!(perms.allowed_paths.contains(&"/workspace/**".to_string()));
perms.deny_path("/workspace/.secret/**");
assert!(perms
.denied_paths
.contains(&"/workspace/.secret/**".to_string()));
}
#[test]
fn test_enable_network() {
let mut perms = AgentPermissions::for_new_agent("test");
assert!(!perms.network_access);
perms.enable_network();
assert!(perms.network_access);
}
#[test]
fn test_enable_forking() {
let mut perms = AgentPermissions::for_new_agent("test");
assert!(!perms.can_fork);
perms.enable_forking();
assert!(perms.can_fork);
}
#[test]
fn test_path_matching_allowed() {
let mut perms = AgentPermissions::for_new_agent("test");
perms.allowed_paths = vec!["/workspace/**".to_string(), "/home/*/docs/**".to_string()];
perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
assert!(perms.is_path_allowed("/workspace/project/file.rs"));
assert!(perms.is_path_allowed("/home/user/docs/readme.md"));
assert!(!perms.is_path_allowed("/etc/passwd"));
assert!(!perms.is_path_allowed("/home/user/secret.txt"));
}
#[test]
fn test_path_matching_denied() {
let mut perms = AgentPermissions::for_new_agent("test");
perms.allowed_paths = vec!["/workspace/**".to_string()];
perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
assert!(perms.is_path_denied("/workspace/.oxios/config.toml"));
assert!(!perms.is_path_denied("/workspace/project/file.rs"));
let _access = AccessManager::new();
let mut perms2 = perms.clone();
perms2.agent_name = "test".to_string();
}
#[test]
fn test_path_denied_pattern_matching() {
let mut perms = AgentPermissions::for_new_agent("test");
perms.denied_paths = vec!["/etc/**".to_string(), "**/secrets/*".to_string()];
assert!(perms.is_path_denied("/etc/passwd"));
assert!(perms.is_path_denied("/etc/shadow"));
assert!(!perms.is_path_denied("/workspace/file"));
}
#[test]
fn test_can_use_tool_allowed() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("code-agent");
perms.allow_tool("bash");
perms.allow_tool("read");
access.set_permissions(perms);
assert!(access.can_use_tool("code-agent", "bash"));
assert!(access.can_use_tool("code-agent", "read"));
}
#[test]
fn test_can_use_tool_denied() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("code-agent");
perms.allow_tool("read");
perms.deny_tool("bash"); access.set_permissions(perms);
assert!(!access.can_use_tool("code-agent", "bash")); assert!(!access.can_use_tool("code-agent", "spawn")); assert!(!access.can_use_tool("unknown-agent", "bash")); }
#[test]
fn test_unknown_agent_denied_all_tools() {
let mut access = AccessManager::new();
assert!(!access.can_use_tool("unknown-agent", "read"));
assert!(!access.can_access_path("unknown-agent", "/workspace/test.txt"));
assert!(!access.can_access_network("unknown-agent"));
assert!(!access.can_fork("unknown-agent"));
}
#[test]
fn test_can_access_path_allowed() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("file-agent");
access.set_permissions(perms);
assert!(access.can_access_path("file-agent", "/workspace/project/file.rs"));
assert!(!access.can_access_path("file-agent", "/etc/passwd"));
}
#[test]
fn test_can_access_path_denied_takes_precedence() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.allowed_paths = vec!["/workspace/**".to_string()];
perms.denied_paths = vec!["/workspace/.oxios/**".to_string()];
access.set_permissions(perms);
assert!(!access.can_access_path("test", "/workspace/.oxios/config.toml"));
assert!(access.can_access_path("test", "/workspace/project/file.rs"));
}
#[test]
fn test_can_access_network() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("net-agent");
perms.enable_network();
access.set_permissions(perms);
assert!(access.can_access_network("net-agent"));
assert!(!access.can_access_network("no-net-agent"));
}
#[test]
fn test_can_execute_for() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.max_execution_time_secs = 300;
access.set_permissions(perms);
assert!(access.can_execute_for("test", 100));
assert!(access.can_execute_for("test", 300));
assert!(!access.can_execute_for("test", 301));
}
#[test]
fn test_unlimited_execution_time() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.max_execution_time_secs = 0; access.set_permissions(perms);
assert!(access.can_execute_for("test", 100_000));
}
#[test]
fn test_can_use_memory() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.max_memory_mb = 512;
access.set_permissions(perms);
assert!(access.can_use_memory("test", 256));
assert!(access.can_use_memory("test", 512));
assert!(!access.can_use_memory("test", 513));
}
#[test]
fn test_unlimited_memory() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.max_memory_mb = 0;
access.set_permissions(perms);
assert!(access.can_use_memory("test", 1_000_000));
}
#[test]
fn test_can_fork() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.enable_forking();
access.set_permissions(perms);
assert!(access.can_fork("test"));
assert!(!access.can_fork("no-fork-agent"));
}
#[test]
fn test_set_and_get_permissions() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("test-agent");
access.set_permissions(perms);
let retrieved = access.get_permissions("test-agent");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().agent_name, "test-agent");
}
#[test]
fn test_get_nonexistent_permissions() {
let access = AccessManager::new();
assert!(access.get_permissions("ghost").is_none());
}
#[test]
fn test_get_or_create_permissions() {
let mut access = AccessManager::new();
let perms = access.get_or_create_permissions("new-agent");
assert_eq!(perms.agent_name, "new-agent");
let perms2 = access.get_or_create_permissions("new-agent");
assert_eq!(perms2.agent_name, "new-agent");
}
#[test]
fn test_remove_permissions() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("to-remove");
access.set_permissions(perms);
assert!(access.get_permissions("to-remove").is_some());
access.remove_permissions("to-remove");
assert!(access.get_permissions("to-remove").is_none());
assert!(!access.can_use_tool("to-remove", "bash"));
}
#[test]
fn test_list_agents() {
let mut access = AccessManager::new();
access.set_permissions(AgentPermissions::for_new_agent("agent-1"));
access.set_permissions(AgentPermissions::for_new_agent("agent-2"));
let agents = access.list_agents();
assert_eq!(agents.len(), 2);
assert!(agents.contains(&"agent-1".to_string()));
assert!(agents.contains(&"agent-2".to_string()));
}
#[test]
fn test_audit_log_records_access() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("test-agent");
access.set_permissions(perms);
access.can_use_tool("test-agent", "bash"); access.can_use_tool("test-agent", "network");
let log = access.audit_log();
assert_eq!(log.len(), 2);
assert!(log[0].allowed);
assert!(!log[1].allowed);
assert_eq!(log[0].agent_name, "test-agent");
assert_eq!(log[0].action, "use_tool");
assert_eq!(log[0].resource, "bash");
}
#[test]
fn test_audit_log_recent() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("test");
access.set_permissions(perms);
for i in 0..10 {
access.can_use_tool("test", &format!("tool-{}", i));
}
let recent = access.audit_log_recent(3);
assert_eq!(recent.len(), 3);
}
#[test]
fn test_audit_log_for_agent() {
let mut access = AccessManager::new();
access.set_permissions(AgentPermissions::for_new_agent("agent-a"));
access.set_permissions(AgentPermissions::for_new_agent("agent-b"));
access.can_use_tool("agent-a", "tool1");
access.can_use_tool("agent-b", "tool2");
access.can_use_tool("agent-a", "tool3");
let log_a = access.audit_log_for_agent("agent-a");
assert_eq!(log_a.len(), 2);
}
#[test]
fn test_denied_actions() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("test");
access.set_permissions(perms);
access.can_use_tool("test", "bash"); access.can_use_tool("test", "dangerous"); access.can_access_path("test", "/etc/shadow");
let denied = access.denied_actions();
assert_eq!(denied.len(), 2);
}
#[test]
fn test_clear_audit_log() {
let mut access = AccessManager::new();
let perms = AgentPermissions::for_new_agent("test");
access.set_permissions(perms);
for _ in 0..5 {
access.can_use_tool("test", "tool");
}
assert_eq!(access.audit_log().len(), 5);
access.clear_audit_log();
assert!(access.audit_log().is_empty());
}
#[test]
fn test_audit_log_prunes_old_entries() {
let mut access = AccessManager::with_max_audit_entries(5);
let perms = AgentPermissions::for_new_agent("test");
access.set_permissions(perms);
for i in 0..10 {
access.can_use_tool("test", &format!("tool-{}", i));
}
assert_eq!(access.audit_log().len(), 5);
}
#[test]
fn test_validate_permissions_no_tools() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.allowed_tools.clear();
access.set_permissions(perms.clone());
let warnings = access.validate_permissions(&perms);
assert!(warnings.iter().any(|w| w.contains("no allowed tools")));
}
#[test]
fn test_validate_permissions_no_path_restrictions() {
let mut perms = AgentPermissions::for_new_agent("test");
perms.allowed_paths.clear();
let access = AccessManager::new();
let warnings = access.validate_permissions(&perms);
assert!(warnings.iter().any(|w| w.contains("no path restrictions")));
}
#[test]
fn test_validate_permissions_warnings() {
let mut access = AccessManager::new();
let mut perms = AgentPermissions::for_new_agent("test");
perms.network_access = true;
perms.can_fork = true;
perms.max_execution_time_secs = 0;
perms.max_memory_mb = 0;
access.set_permissions(perms.clone());
let warnings = access.validate_permissions(&perms);
assert!(warnings.iter().any(|w| w.contains("network access")));
assert!(warnings.iter().any(|w| w.contains("fork sub-agents")));
assert!(warnings
.iter()
.any(|w| w.contains("no execution time limit")));
assert!(warnings.iter().any(|w| w.contains("no memory limit")));
}
#[test]
fn test_audit_entry_has_timestamp() {
let entry = AuditEntry::new("agent", "action", "resource", true, None);
assert!(entry.timestamp.timestamp() > 0);
}
#[test]
fn test_register_workspace_path() {
let mut access = AccessManager::new();
access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my-workspace"));
assert_eq!(access.list_workspaces(), vec!["my-workspace"]);
assert_eq!(
access.get_workspace_path("my-workspace"),
Some(&PathBuf::from("/workspace/my-workspace"))
);
}
#[test]
fn test_assign_agent_to_workspace() {
let mut access = AccessManager::new();
access.register_workspace_path("project-alpha", PathBuf::from("/workspace/alpha"));
assert!(access.assign_workspace("agent-1", "project-alpha"));
assert_eq!(
access.get_workspace_for_agent("agent-1"),
Some("project-alpha".to_string())
);
assert!(access.can_access_workspace("agent-1", "project-alpha"));
assert!(!access.can_access_workspace("agent-1", "other-workspace"));
}
#[test]
fn test_assign_agent_to_nonexistent_workspace_fails() {
let mut access = AccessManager::new();
assert!(!access.assign_workspace("agent-1", "nonexistent"));
assert_eq!(access.get_workspace_for_agent("agent-1"), None);
}
#[test]
fn test_reassign_agent_to_different_workspace() {
let mut access = AccessManager::new();
access.register_workspace_path("workspace-a", PathBuf::from("/workspace/a"));
access.register_workspace_path("workspace-b", PathBuf::from("/workspace/b"));
access.assign_workspace("agent-1", "workspace-a");
assert_eq!(
access.get_workspace_for_agent("agent-1"),
Some("workspace-a".to_string())
);
access.assign_workspace("agent-1", "workspace-b");
assert_eq!(
access.get_workspace_for_agent("agent-1"),
Some("workspace-b".to_string())
);
assert!(!access.can_access_workspace("agent-1", "workspace-a"));
}
#[test]
fn test_unassign_agent_from_workspace() {
let mut access = AccessManager::new();
access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
access.assign_workspace("agent-1", "my-workspace");
assert!(access.get_workspace_for_agent("agent-1").is_some());
let removed = access.unassign_workspace("agent-1");
assert_eq!(removed, Some("my-workspace".to_string()));
assert!(access.get_workspace_for_agent("agent-1").is_none());
}
#[test]
fn test_list_agents_in_workspace() {
let mut access = AccessManager::new();
access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
access.assign_workspace("agent-1", "my-workspace");
access.assign_workspace("agent-2", "my-workspace");
access.assign_workspace("agent-3", "other-workspace");
let agents = access.list_agents_in_workspace("my-workspace");
assert_eq!(agents.len(), 2);
assert!(agents.contains(&"agent-1".to_string()));
assert!(agents.contains(&"agent-2".to_string()));
assert!(!agents.contains(&"agent-3".to_string()));
}
#[test]
fn test_remove_workspace_unassigns_all_agents() {
let mut access = AccessManager::new();
access.register_workspace_path("my-workspace", PathBuf::from("/workspace/my"));
access.assign_workspace("agent-1", "my-workspace");
access.assign_workspace("agent-2", "my-workspace");
access.remove_workspace("my-workspace");
assert!(access.list_workspaces().is_empty());
assert!(access.get_workspace_for_agent("agent-1").is_none());
assert!(access.get_workspace_for_agent("agent-2").is_none());
}
#[test]
fn test_is_path_in_workspace() {
let mut access = AccessManager::new();
let workspace = PathBuf::from("/tmp/oxios-test-workspace");
std::fs::create_dir_all(&workspace).ok();
std::fs::create_dir_all(workspace.join("subdir")).ok();
access.register_workspace_path("my-workspace", workspace.clone());
let inside_path = workspace.join("file.txt");
std::fs::write(&inside_path, "test").ok();
assert!(
access.is_path_in_workspace("my-workspace", inside_path.to_str().unwrap()),
"Path {:?} should be inside workspace",
inside_path
);
let nested_path = workspace.join("subdir/nested.txt");
std::fs::write(&nested_path, "test").ok();
assert!(
access.is_path_in_workspace("my-workspace", nested_path.to_str().unwrap()),
"Path {:?} should be inside workspace",
nested_path
);
assert!(!access.is_path_in_workspace("my-workspace", "/tmp/other-workspace/file.txt"));
assert!(!access.is_path_in_workspace("nonexistent", "/tmp/test"));
std::fs::remove_dir_all(workspace).ok();
}
}