Skip to main content

oxios_kernel/access_manager/
permissions.rs

1//! Agent permissions types — per-agent permission sets and audit entries.
2
3use chrono::{DateTime, Utc};
4use glob::Pattern;
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8/// Permissions for a single agent.
9///
10/// Agents start with minimal permissions (least privilege).
11/// Additional permissions must be explicitly granted via configuration
12/// or an authorized request.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AgentPermissions {
15    /// Name of the agent this permission set applies to.
16    pub agent_name: String,
17    /// Set of allowed tool names. Empty means no tools allowed.
18    #[serde(default)]
19    pub allowed_tools: HashSet<String>,
20    /// Allowed path patterns (glob). Used for file operations.
21    #[serde(default)]
22    pub allowed_paths: Vec<String>,
23    /// Denied path patterns (glob). Always blocked, even if allowed_paths matches.
24    #[serde(default)]
25    pub denied_paths: Vec<String>,
26    /// Whether this agent can make network requests.
27    #[serde(default)]
28    pub network_access: bool,
29    /// Maximum execution time in seconds (0 = unlimited).
30    #[serde(default)]
31    pub max_execution_time_secs: u64,
32    /// Maximum memory in MB (0 = unlimited).
33    #[serde(default)]
34    pub max_memory_mb: u64,
35    /// Whether this agent can spawn sub-agents.
36    #[serde(default)]
37    pub can_fork: bool,
38}
39
40impl Default for AgentPermissions {
41    fn default() -> Self {
42        Self {
43            agent_name: String::new(),
44            // By default, agents get basic file tools.
45            // Network access is denied by default.
46            allowed_tools: ["read", "write", "edit", "bash", "grep", "find"]
47                .iter()
48                .map(|s| s.to_string())
49                .collect(),
50            allowed_paths: vec!["/workspace/**".to_string()],
51            denied_paths: vec![
52                "/etc/**".to_string(),
53                "/root/**".to_string(),
54                "/sys/**".to_string(),
55                "/proc/**".to_string(),
56                ".oxios/**".to_string(),
57            ],
58            network_access: false,
59            max_execution_time_secs: 300,
60            max_memory_mb: 512,
61            can_fork: false,
62        }
63    }
64}
65
66/// Update struct for permission changes (partial updates).
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct PermissionUpdate {
69    /// Set of allowed tool names.
70    #[serde(default)]
71    pub allowed_tools: Option<HashSet<String>>,
72    /// Allowed path patterns (glob).
73    #[serde(default)]
74    pub allowed_paths: Option<Vec<String>>,
75    /// Denied path patterns (glob).
76    #[serde(default)]
77    pub denied_paths: Option<Vec<String>>,
78    /// Whether this agent can make network requests.
79    #[serde(default)]
80    pub network_access: Option<bool>,
81    /// Maximum execution time in seconds (0 = unlimited).
82    #[serde(default)]
83    pub max_execution_time_secs: Option<u64>,
84    /// Maximum memory in MB (0 = unlimited).
85    #[serde(default)]
86    pub max_memory_mb: Option<u64>,
87    /// Whether this agent can spawn sub-agents.
88    #[serde(default)]
89    pub can_fork: Option<bool>,
90}
91
92impl PermissionUpdate {
93    /// Apply this update to a permission set.
94    pub fn apply(&self, perms: &mut AgentPermissions) {
95        if let Some(tools) = &self.allowed_tools {
96            perms.allowed_tools = tools.clone();
97        }
98        if let Some(paths) = &self.allowed_paths {
99            perms.allowed_paths = paths.clone();
100        }
101        if let Some(paths) = &self.denied_paths {
102            perms.denied_paths = paths.clone();
103        }
104        if let Some(v) = self.network_access {
105            perms.network_access = v;
106        }
107        if let Some(v) = self.max_execution_time_secs {
108            perms.max_execution_time_secs = v;
109        }
110        if let Some(v) = self.max_memory_mb {
111            perms.max_memory_mb = v;
112        }
113        if let Some(v) = self.can_fork {
114            perms.can_fork = v;
115        }
116    }
117}
118
119impl AgentPermissions {
120    /// Creates permissions for a new agent with the default restrictive set.
121    pub fn for_new_agent(agent_name: &str) -> Self {
122        Self {
123            agent_name: agent_name.to_string(),
124            ..Default::default()
125        }
126    }
127
128    /// Adds a tool to the allowed set.
129    pub fn allow_tool(&mut self, tool: &str) {
130        self.allowed_tools.insert(tool.to_string());
131    }
132
133    /// Removes a tool from the allowed set.
134    pub fn deny_tool(&mut self, tool: &str) {
135        self.allowed_tools.remove(tool);
136    }
137
138    /// Adds a path pattern to the allowed set.
139    pub fn allow_path(&mut self, path: &str) {
140        if !self.allowed_paths.contains(&path.to_string()) {
141            self.allowed_paths.push(path.to_string());
142        }
143    }
144
145    /// Adds a path pattern to the denied set.
146    pub fn deny_path(&mut self, path: &str) {
147        if !self.denied_paths.contains(&path.to_string()) {
148            self.denied_paths.push(path.to_string());
149        }
150    }
151
152    /// Enables network access for this agent.
153    pub fn enable_network(&mut self) {
154        self.network_access = true;
155    }
156
157    /// Enables agent forking (spawning sub-agents).
158    pub fn enable_forking(&mut self) {
159        self.can_fork = true;
160    }
161
162    /// Checks if a path matches any denied pattern.
163    pub(crate) fn is_path_denied(&self, path: &str) -> bool {
164        for pattern in &self.denied_paths {
165            if let Ok(p) = Pattern::new(pattern) {
166                if p.matches(path) {
167                    return true;
168                }
169            }
170        }
171        false
172    }
173
174    /// Checks if a path matches any allowed pattern.
175    pub(crate) fn is_path_allowed(&self, path: &str) -> bool {
176        for pattern in &self.allowed_paths {
177            if let Ok(p) = Pattern::new(pattern) {
178                if p.matches(path) {
179                    return true;
180                }
181            }
182        }
183        false
184    }
185}
186
187/// An entry in the security audit log.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct AuditEntry {
190    /// When the action occurred.
191    pub timestamp: DateTime<Utc>,
192    /// Agent that performed the action.
193    pub agent_name: String,
194    /// The action attempted (e.g., "use_tool", "access_path", "network_request").
195    pub action: String,
196    /// The resource involved (e.g., "bash", "/workspace/file.txt").
197    pub resource: String,
198    /// Whether the action was allowed.
199    pub allowed: bool,
200    /// Reason for the decision (e.g., "path not in allowed list").
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub reason: Option<String>,
203}
204
205impl AuditEntry {
206    /// Creates a new audit entry.
207    pub(crate) fn new(
208        agent_name: &str,
209        action: &str,
210        resource: &str,
211        allowed: bool,
212        reason: Option<String>,
213    ) -> Self {
214        Self {
215            timestamp: Utc::now(),
216            agent_name: agent_name.to_string(),
217            action: action.to_string(),
218            resource: resource.to_string(),
219            allowed,
220            reason,
221        }
222    }
223}