返回 DeepSeek-TUI-2026
policy.rs
根目录 / crates / tui / src / sandbox / policy.rs
1 #![allow(dead_code)]
2
3 //! Sandbox policy definitions for command execution restrictions.
4 //!
5 //! This module defines the policies that control what resources a sandboxed
6 //! process can access. Policies range from full unrestricted access to
7 //! tightly controlled workspace-only write access.
8
9 use serde::{Deserialize, Serialize};
10 use std::path::{Path, PathBuf};
11
12 /// Determines execution restrictions for shell commands.
13 ///
14 /// The sandbox policy controls filesystem access, network access, and other
15 /// system resources for executed commands. Choose the most restrictive policy
16 /// that still allows your command to function.
17 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18 #[serde(tag = "type", rename_all = "kebab-case")]
19 pub enum SandboxPolicy {
20 /// No restrictions whatsoever. Use with extreme caution.
21 ///
22 /// This policy disables all sandboxing and allows full system access.
23 /// Only use this when absolutely necessary and the command source is trusted.
24 #[serde(rename = "danger-full-access")]
25 DangerFullAccess,
26
27 /// Read-only access to the entire filesystem.
28 ///
29 /// The process can read any file but cannot write anywhere.
30 /// Useful for analysis tools that need broad read access.
31 #[serde(rename = "read-only")]
32 ReadOnly,
33
34 /// Indicates the process is already running in an external sandbox.
35 ///
36 /// Use this when DeepSeek TUI is itself running inside a container,
37 /// VM, or other sandboxed environment. This avoids double-sandboxing
38 /// which can cause issues.
39 #[serde(rename = "external-sandbox")]
40 ExternalSandbox {
41 /// Whether network access is allowed in the external sandbox.
42 #[serde(default)]
43 network_access: bool,
44 },
45
46 /// Read-only filesystem access plus write access to specified directories.
47 ///
48 /// This is the default and recommended policy. It allows:
49 /// - Read access to the entire filesystem (for tools, libraries, etc.)
50 /// - Write access only to the current working directory and specified roots
51 /// - Optional network access
52 #[serde(rename = "workspace-write")]
53 WorkspaceWrite {
54 /// Additional directories where writes are allowed.
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 writable_roots: Vec<PathBuf>,
57
58 /// Whether outbound network connections are permitted.
59 #[serde(default)]
60 network_access: bool,
61
62 /// Exclude TMPDIR from writable paths.
63 #[serde(default)]
64 exclude_tmpdir: bool,
65
66 /// Exclude /tmp from writable paths.
67 #[serde(default)]
68 exclude_slash_tmp: bool,
69 },
70 }
71
72 impl Default for SandboxPolicy {
73 /// Returns the default policy: workspace-write with no extra roots and no network.
74 fn default() -> Self {
75 SandboxPolicy::WorkspaceWrite {
76 writable_roots: vec![],
77 network_access: false,
78 exclude_tmpdir: false,
79 exclude_slash_tmp: false,
80 }
81 }
82 }
83
84 impl SandboxPolicy {
85 /// Create a workspace-write policy with network access enabled.
86 pub fn workspace_with_network() -> Self {
87 SandboxPolicy::WorkspaceWrite {
88 writable_roots: vec![],
89 network_access: true,
90 exclude_tmpdir: false,
91 exclude_slash_tmp: false,
92 }
93 }
94
95 /// Create a workspace-write policy with additional writable directories.
96 pub fn workspace_with_roots(roots: Vec<PathBuf>, network: bool) -> Self {
97 SandboxPolicy::WorkspaceWrite {
98 writable_roots: roots,
99 network_access: network,
100 exclude_tmpdir: false,
101 exclude_slash_tmp: false,
102 }
103 }
104
105 /// Returns true if the policy allows reading any file on the filesystem.
106 pub fn has_full_disk_read_access() -> bool {
107 // All current policies allow full disk read access
108 true
109 }
110
111 /// Returns true if the policy allows writing to any file on the filesystem.
112 pub fn has_full_disk_write_access(&self) -> bool {
113 matches!(
114 self,
115 SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
116 )
117 }
118
119 /// Returns true if the policy allows outbound network connections.
120 pub fn has_network_access(&self) -> bool {
121 match self {
122 SandboxPolicy::DangerFullAccess => true,
123 SandboxPolicy::ReadOnly => false,
124 SandboxPolicy::ExternalSandbox { network_access }
125 | SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
126 }
127 }
128
129 /// Returns true if the sandbox should be applied (not bypassed).
130 pub fn should_sandbox(&self) -> bool {
131 !matches!(
132 self,
133 SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
134 )
135 }
136
137 /// Get the list of writable roots for this policy.
138 ///
139 /// This includes:
140 /// - The current working directory
141 /// - Any explicitly specified `writable_roots`
142 /// - /tmp (unless excluded)
143 /// - TMPDIR (unless excluded)
144 ///
145 /// For policies with full write access, returns an empty vec since
146 /// there's no need to enumerate specific paths.
147 pub fn get_writable_roots(&self, cwd: &Path) -> Vec<WritableRoot> {
148 match self {
149 // Full write access or read-only - no enumeration needed
150 SandboxPolicy::DangerFullAccess
151 | SandboxPolicy::ExternalSandbox { .. }
152 | SandboxPolicy::ReadOnly => vec![],
153
154 // Workspace write - enumerate all writable paths
155 SandboxPolicy::WorkspaceWrite {
156 writable_roots,
157 exclude_tmpdir,
158 exclude_slash_tmp,
159 ..
160 } => {
161 let mut roots: Vec<PathBuf> = writable_roots.clone();
162
163 // Add the current working directory
164 if let Ok(canonical_cwd) = cwd.canonicalize() {
165 roots.push(canonical_cwd);
166 } else {
167 roots.push(cwd.to_path_buf());
168 }
169
170 // Add /tmp unless excluded
171 if !exclude_slash_tmp && let Ok(tmp) = Path::new("/tmp").canonicalize() {
172 roots.push(tmp);
173 }
174
175 // Add TMPDIR unless excluded
176 if !exclude_tmpdir
177 && let Ok(tmpdir) = std::env::var("TMPDIR")
178 && let Ok(canonical) = Path::new(&tmpdir).canonicalize()
179 {
180 roots.push(canonical);
181 }
182
183 // Convert to WritableRoot with read-only subpaths
184 roots
185 .into_iter()
186 .map(|root| {
187 let mut read_only_subpaths = Vec::new();
188
189 // Protect .deepseek directories from modification
190 let deepseek_dir = root.join(".deepseek");
191 if deepseek_dir.is_dir() {
192 read_only_subpaths.push(deepseek_dir);
193 }
194
195 WritableRoot {
196 root,
197 read_only_subpaths,
198 }
199 })
200 .collect()
201 }
202 }
203 }
204 }
205
206 /// A directory tree where writes are allowed, with optional read-only subpaths.
207 ///
208 /// This allows fine-grained control like "allow writes to /project but not /project/.deepseek".
209 #[derive(Debug, Clone, PartialEq, Eq)]
210 pub struct WritableRoot {
211 /// The root directory where writes are allowed.
212 pub root: PathBuf,
213
214 /// Subdirectories within root that should remain read-only.
215 pub read_only_subpaths: Vec<PathBuf>,
216 }
217
218 impl WritableRoot {
219 /// Create a new writable root with no read-only exceptions.
220 pub fn new(root: PathBuf) -> Self {
221 Self {
222 root,
223 read_only_subpaths: vec![],
224 }
225 }
226
227 /// Create a writable root with specific read-only subpaths.
228 pub fn with_exceptions(root: PathBuf, read_only: Vec<PathBuf>) -> Self {
229 Self {
230 root,
231 read_only_subpaths: read_only,
232 }
233 }
234
235 /// Check if a path is writable under this root.
236 ///
237 /// Returns true if the path is under the root and not under any read-only subpath.
238 pub fn is_path_writable(&self, path: &Path) -> bool {
239 // Must be under the root
240 if !path.starts_with(&self.root) {
241 return false;
242 }
243
244 // Must not be under any read-only subpath
245 for subpath in &self.read_only_subpaths {
246 if path.starts_with(subpath) {
247 return false;
248 }
249 }
250
251 true
252 }
253 }
254
255 #[cfg(test)]
256 mod tests {
257 use super::*;
258
259 #[test]
260 fn test_default_policy() {
261 let policy = SandboxPolicy::default();
262 assert!(matches!(policy, SandboxPolicy::WorkspaceWrite { .. }));
263 assert!(!policy.has_network_access());
264 assert!(policy.should_sandbox());
265 }
266
267 #[test]
268 fn test_full_access_policy() {
269 let policy = SandboxPolicy::DangerFullAccess;
270 assert!(policy.has_full_disk_write_access());
271 assert!(policy.has_network_access());
272 assert!(!policy.should_sandbox());
273 }
274
275 #[test]
276 fn test_read_only_policy() {
277 let policy = SandboxPolicy::ReadOnly;
278 assert!(!policy.has_full_disk_write_access());
279 assert!(!policy.has_network_access());
280 assert!(policy.should_sandbox());
281 }
282
283 #[test]
284 fn test_workspace_with_network() {
285 let policy = SandboxPolicy::workspace_with_network();
286 assert!(policy.has_network_access());
287 assert!(policy.should_sandbox());
288 }
289
290 #[test]
291 fn test_writable_root_basic() {
292 let root = WritableRoot::new(PathBuf::from("/project"));
293 assert!(root.is_path_writable(Path::new("/project/src/main.rs")));
294 assert!(!root.is_path_writable(Path::new("/other/file.txt")));
295 }
296
297 #[test]
298 fn test_writable_root_with_exceptions() {
299 let root = WritableRoot::with_exceptions(
300 PathBuf::from("/project"),
301 vec![PathBuf::from("/project/.deepseek")],
302 );
303 assert!(root.is_path_writable(Path::new("/project/src/main.rs")));
304 assert!(!root.is_path_writable(Path::new("/project/.deepseek/config")));
305 }
306
307 #[test]
308 fn test_policy_serialization() {
309 let policy = SandboxPolicy::WorkspaceWrite {
310 writable_roots: vec![PathBuf::from("/extra")],
311 network_access: true,
312 exclude_tmpdir: false,
313 exclude_slash_tmp: false,
314 };
315
316 let json = serde_json::to_string(&policy).unwrap();
317 assert!(json.contains("workspace-write"));
318
319 let parsed: SandboxPolicy = serde_json::from_str(&json).unwrap();
320 assert_eq!(policy, parsed);
321 }
322 }
323
323 lines RUST