oxicode_sdk/security/
permissions.rs1use chrono::{DateTime, Utc};
4use glob::Pattern;
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AgentPermissions {
11 pub agent_name: String,
13 #[serde(default)]
15 pub allowed_tools: HashSet<String>,
16 #[serde(default)]
18 pub allowed_paths: Vec<String>,
19 #[serde(default)]
21 pub denied_paths: Vec<String>,
22 #[serde(default)]
24 pub network_access: bool,
25 #[serde(default)]
27 pub max_execution_time_secs: u64,
28 #[serde(default)]
30 pub max_memory_mb: u64,
31 #[serde(default)]
33 pub can_fork: bool,
34}
35
36impl Default for AgentPermissions {
37 fn default() -> Self {
38 Self {
39 agent_name: String::new(),
40 allowed_tools: ["read", "write", "edit", "bash", "grep", "find", "exec"]
41 .iter()
42 .map(|s| s.to_string())
43 .collect(),
44 allowed_paths: vec!["/workspace/**".to_string()],
45 denied_paths: vec![
46 "/etc/**".to_string(),
47 "/root/**".to_string(),
48 "/sys/**".to_string(),
49 "/proc/**".to_string(),
50 ".oxios/**".to_string(),
51 ],
52 network_access: false,
53 max_execution_time_secs: 300,
54 max_memory_mb: 512,
55 can_fork: false,
56 }
57 }
58}
59
60impl AgentPermissions {
61 pub fn for_new_agent(agent_name: &str) -> Self {
63 Self {
64 agent_name: agent_name.to_string(),
65 ..Default::default()
66 }
67 }
68
69 pub fn allow_tool(&mut self, tool: &str) {
71 self.allowed_tools.insert(tool.to_string());
72 }
73
74 pub fn deny_tool(&mut self, tool: &str) {
76 self.allowed_tools.remove(tool);
77 }
78
79 pub fn allow_path(&mut self, path: &str) {
81 if !self.allowed_paths.contains(&path.to_string()) {
82 self.allowed_paths.push(path.to_string());
83 }
84 }
85
86 pub fn deny_path(&mut self, path: &str) {
88 if !self.denied_paths.contains(&path.to_string()) {
89 self.denied_paths.push(path.to_string());
90 }
91 }
92
93 pub fn enable_network(&mut self) {
95 self.network_access = true;
96 }
97
98 pub fn enable_forking(&mut self) {
100 self.can_fork = true;
101 }
102
103 pub fn is_path_denied(&self, path: &str) -> bool {
105 for pattern in &self.denied_paths {
106 if let Ok(p) = Pattern::new(pattern)
107 && p.matches(path)
108 {
109 return true;
110 }
111 }
112 false
113 }
114
115 pub fn is_path_allowed(&self, path: &str) -> bool {
117 for pattern in &self.allowed_paths {
118 if let Ok(p) = Pattern::new(pattern)
119 && p.matches(path)
120 {
121 return true;
122 }
123 }
124 false
125 }
126}
127
128#[derive(Debug, Clone, Default, Serialize, Deserialize)]
130pub struct PermissionUpdate {
131 pub allowed_tools: Option<HashSet<String>>,
133 pub allowed_paths: Option<Vec<String>>,
135 pub denied_paths: Option<Vec<String>>,
137 pub network_access: Option<bool>,
139 pub max_execution_time_secs: Option<u64>,
141 pub max_memory_mb: Option<u64>,
143 pub can_fork: Option<bool>,
145}
146
147impl PermissionUpdate {
148 pub fn apply(&self, perms: &mut AgentPermissions) {
150 if let Some(tools) = &self.allowed_tools {
151 perms.allowed_tools = tools.clone();
152 }
153 if let Some(paths) = &self.allowed_paths {
154 perms.allowed_paths = paths.clone();
155 }
156 if let Some(paths) = &self.denied_paths {
157 perms.denied_paths = paths.clone();
158 }
159 if let Some(v) = self.network_access {
160 perms.network_access = v;
161 }
162 if let Some(v) = self.max_execution_time_secs {
163 perms.max_execution_time_secs = v;
164 }
165 if let Some(v) = self.max_memory_mb {
166 perms.max_memory_mb = v;
167 }
168 if let Some(v) = self.can_fork {
169 perms.can_fork = v;
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct PermAuditEntry {
177 pub timestamp: DateTime<Utc>,
179 pub agent_name: String,
181 pub action: String,
183 pub resource: String,
185 pub allowed: bool,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub reason: Option<String>,
190}
191
192impl PermAuditEntry {
193 pub fn new(
195 agent_name: &str,
196 action: &str,
197 resource: &str,
198 allowed: bool,
199 reason: Option<String>,
200 ) -> Self {
201 Self {
202 timestamp: Utc::now(),
203 agent_name: agent_name.to_string(),
204 action: action.to_string(),
205 resource: resource.to_string(),
206 allowed,
207 reason,
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn default_has_basic_tools() {
218 let p = AgentPermissions::default();
219 assert!(p.allowed_tools.contains("read"));
220 assert!(p.allowed_tools.contains("bash"));
221 assert!(!p.network_access);
222 }
223
224 #[test]
225 fn denies_sensitive_paths() {
226 let p = AgentPermissions::default();
227 assert!(p.is_path_denied("/etc/passwd"));
228 assert!(p.is_path_denied("/root/.ssh/id_rsa"));
229 }
230
231 #[test]
232 fn allows_workspace() {
233 let p = AgentPermissions::default();
234 assert!(p.is_path_allowed("/workspace/src/main.rs"));
235 assert!(!p.is_path_allowed("/tmp/evil"));
236 }
237
238 #[test]
239 fn partial_update() {
240 let mut p = AgentPermissions::for_new_agent("a");
241 let update = PermissionUpdate {
242 network_access: Some(true),
243 ..Default::default()
244 };
245 update.apply(&mut p);
246 assert!(p.network_access);
247 assert!(!p.can_fork);
248 }
249}