1use std::collections::HashSet;
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Permission {
15 Read,
17
18 Write,
20
21 Execute,
23
24 Network,
26
27 Environment,
29
30 Clipboard,
32
33 Ui,
35
36 Notify,
38}
39
40#[derive(Debug, Clone)]
42pub struct SandboxConfig {
43 pub allowed_read_paths: Vec<PathBuf>,
45
46 pub allowed_write_paths: Vec<PathBuf>,
48
49 pub allowed_commands: HashSet<String>,
51
52 pub allow_network: bool,
54
55 pub allow_env: bool,
57
58 pub timeout_ms: u64,
60
61 pub max_memory: usize,
63
64 pub max_read_size: usize,
66
67 pub permissions: HashSet<Permission>,
69}
70
71impl Default for SandboxConfig {
72 fn default() -> Self {
73 let mut permissions = HashSet::new();
74 permissions.insert(Permission::Read);
75 permissions.insert(Permission::Notify);
76
77 Self {
78 allowed_read_paths: vec![],
79 allowed_write_paths: vec![],
80 allowed_commands: HashSet::new(),
81 allow_network: false,
82 allow_env: false,
83 timeout_ms: 5000,
84 max_memory: 256 * 1024 * 1024, max_read_size: 10 * 1024 * 1024, permissions,
87 }
88 }
89}
90
91impl SandboxConfig {
92 pub fn minimal() -> Self {
94 Self {
95 allowed_read_paths: vec![],
96 allowed_write_paths: vec![],
97 allowed_commands: HashSet::new(),
98 allow_network: false,
99 allow_env: false,
100 timeout_ms: 1000,
101 max_memory: 64 * 1024 * 1024,
102 max_read_size: 1024 * 1024,
103 permissions: HashSet::new(),
104 }
105 }
106
107 pub fn permissive() -> Self {
114 let mut permissions = HashSet::new();
115 permissions.insert(Permission::Read);
116 permissions.insert(Permission::Write);
117 permissions.insert(Permission::Environment);
118 permissions.insert(Permission::Ui);
119 permissions.insert(Permission::Notify);
120
121 Self {
122 allowed_read_paths: vec![PathBuf::from("/")],
123 allowed_write_paths: vec![],
124 allowed_commands: HashSet::new(),
125 allow_network: false,
126 allow_env: true,
127 timeout_ms: 30000,
128 max_memory: 1024 * 1024 * 1024,
129 max_read_size: 100 * 1024 * 1024,
130 permissions,
131 }
132 }
133
134 pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
136 self.allowed_read_paths.push(path.into());
137 self.permissions.insert(Permission::Read);
138 self
139 }
140
141 pub fn allow_write(mut self, path: impl Into<PathBuf>) -> Self {
143 self.allowed_write_paths.push(path.into());
144 self.permissions.insert(Permission::Write);
145 self
146 }
147
148 pub fn allow_command(mut self, cmd: impl Into<String>) -> Self {
150 self.allowed_commands.insert(cmd.into());
151 self.permissions.insert(Permission::Execute);
152 self
153 }
154
155 pub fn allow_network(mut self) -> Self {
157 self.allow_network = true;
158 self.permissions.insert(Permission::Network);
159 self
160 }
161
162 pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
164 self.timeout_ms = timeout_ms;
165 self
166 }
167
168 pub fn with_memory_limit(mut self, bytes: usize) -> Self {
170 self.max_memory = bytes;
171 self
172 }
173
174 pub fn grant(mut self, permission: Permission) -> Self {
176 self.permissions.insert(permission);
177 self
178 }
179
180 pub fn has_permission(&self, permission: Permission) -> bool {
182 self.permissions.contains(&permission)
183 }
184
185 pub fn can_read(&self, path: &std::path::Path) -> bool {
193 if !self.has_permission(Permission::Read) {
194 return false;
195 }
196 if self.allowed_read_paths.is_empty() {
198 return false;
199 }
200 let Ok(canonical) = std::fs::canonicalize(path) else {
201 return false;
202 };
203 self.allowed_read_paths
204 .iter()
205 .filter_map(|p| std::fs::canonicalize(p).ok())
206 .any(|allowed| canonical.starts_with(&allowed))
207 }
208
209 pub fn can_write(&self, path: &std::path::Path) -> bool {
217 if !self.has_permission(Permission::Write) {
218 return false;
219 }
220 if self.allowed_write_paths.is_empty() {
222 return false;
223 }
224 let Ok(canonical) = std::fs::canonicalize(path) else {
225 return false;
226 };
227 self.allowed_write_paths
228 .iter()
229 .filter_map(|p| std::fs::canonicalize(p).ok())
230 .any(|allowed| canonical.starts_with(&allowed))
231 }
232
233 pub fn can_execute(&self, command: &str) -> bool {
235 if !self.has_permission(Permission::Execute) {
236 return false;
237 }
238 self.allowed_commands.contains("*") || self.allowed_commands.contains(command)
239 }
240}
241
242#[derive(Debug, Clone)]
244#[allow(dead_code)]
245pub struct SandboxViolation {
246 pub kind: ViolationKind,
248
249 pub description: String,
251
252 pub path: Option<PathBuf>,
254
255 pub command: Option<String>,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261#[allow(dead_code)]
262pub enum ViolationKind {
263 ReadDenied,
265
266 WriteDenied,
268
269 ExecuteDenied,
271
272 NetworkDenied,
274
275 EnvDenied,
277
278 MemoryExceeded,
280
281 TimeoutExceeded,
283
284 FileTooLarge,
286}
287
288impl std::fmt::Display for SandboxViolation {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 match self.kind {
291 ViolationKind::ReadDenied => {
292 write!(f, "Read denied: {}", self.description)
293 }
294 ViolationKind::WriteDenied => {
295 write!(f, "Write denied: {}", self.description)
296 }
297 ViolationKind::ExecuteDenied => {
298 write!(f, "Execute denied: {}", self.description)
299 }
300 ViolationKind::NetworkDenied => {
301 write!(f, "Network access denied")
302 }
303 ViolationKind::EnvDenied => {
304 write!(f, "Environment access denied")
305 }
306 ViolationKind::MemoryExceeded => {
307 write!(f, "Memory limit exceeded")
308 }
309 ViolationKind::TimeoutExceeded => {
310 write!(f, "Execution timeout exceeded")
311 }
312 ViolationKind::FileTooLarge => {
313 write!(f, "File too large: {}", self.description)
314 }
315 }
316 }
317}
318
319impl std::error::Error for SandboxViolation {}