#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum SandboxPolicy {
#[serde(rename = "danger-full-access")]
DangerFullAccess,
#[serde(rename = "read-only")]
ReadOnly,
#[serde(rename = "external-sandbox")]
ExternalSandbox {
#[serde(default)]
network_access: bool,
},
#[serde(rename = "workspace-write")]
WorkspaceWrite {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
writable_roots: Vec<PathBuf>,
#[serde(default)]
network_access: bool,
#[serde(default)]
exclude_tmpdir: bool,
#[serde(default)]
exclude_slash_tmp: bool,
},
}
impl Default for SandboxPolicy {
fn default() -> Self {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir: false,
exclude_slash_tmp: false,
}
}
}
impl SandboxPolicy {
pub fn workspace_with_network() -> Self {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir: false,
exclude_slash_tmp: false,
}
}
pub fn workspace_with_roots(roots: Vec<PathBuf>, network: bool) -> Self {
SandboxPolicy::WorkspaceWrite {
writable_roots: roots,
network_access: network,
exclude_tmpdir: false,
exclude_slash_tmp: false,
}
}
pub fn has_full_disk_read_access() -> bool {
true
}
pub fn has_full_disk_write_access(&self) -> bool {
matches!(
self,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
)
}
pub fn has_network_access(&self) -> bool {
match self {
SandboxPolicy::DangerFullAccess => true,
SandboxPolicy::ReadOnly => false,
SandboxPolicy::ExternalSandbox { network_access }
| SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
}
}
pub fn should_sandbox(&self) -> bool {
!matches!(
self,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
)
}
pub fn get_writable_roots(&self, cwd: &Path) -> Vec<WritableRoot> {
match self {
SandboxPolicy::DangerFullAccess
| SandboxPolicy::ExternalSandbox { .. }
| SandboxPolicy::ReadOnly => vec![],
SandboxPolicy::WorkspaceWrite {
writable_roots,
exclude_tmpdir,
exclude_slash_tmp,
..
} => {
let mut roots: Vec<PathBuf> = writable_roots.clone();
if let Ok(canonical_cwd) = cwd.canonicalize() {
roots.push(canonical_cwd);
} else {
roots.push(cwd.to_path_buf());
}
if !exclude_slash_tmp && let Ok(tmp) = Path::new("/tmp").canonicalize() {
roots.push(tmp);
}
if !exclude_tmpdir
&& let Ok(tmpdir) = std::env::var("TMPDIR")
&& let Ok(canonical) = Path::new(&tmpdir).canonicalize()
{
roots.push(canonical);
}
roots
.into_iter()
.map(|root| {
let mut read_only_subpaths = Vec::new();
let deepseek_dir = root.join(".deepseek");
if deepseek_dir.is_dir() {
read_only_subpaths.push(deepseek_dir);
}
WritableRoot {
root,
read_only_subpaths,
}
})
.collect()
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritableRoot {
pub root: PathBuf,
pub read_only_subpaths: Vec<PathBuf>,
}
impl WritableRoot {
pub fn new(root: PathBuf) -> Self {
Self {
root,
read_only_subpaths: vec![],
}
}
pub fn with_exceptions(root: PathBuf, read_only: Vec<PathBuf>) -> Self {
Self {
root,
read_only_subpaths: read_only,
}
}
pub fn is_path_writable(&self, path: &Path) -> bool {
if !path.starts_with(&self.root) {
return false;
}
for subpath in &self.read_only_subpaths {
if path.starts_with(subpath) {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_policy() {
let policy = SandboxPolicy::default();
assert!(matches!(policy, SandboxPolicy::WorkspaceWrite { .. }));
assert!(!policy.has_network_access());
assert!(policy.should_sandbox());
}
#[test]
fn test_full_access_policy() {
let policy = SandboxPolicy::DangerFullAccess;
assert!(policy.has_full_disk_write_access());
assert!(policy.has_network_access());
assert!(!policy.should_sandbox());
}
#[test]
fn test_read_only_policy() {
let policy = SandboxPolicy::ReadOnly;
assert!(!policy.has_full_disk_write_access());
assert!(!policy.has_network_access());
assert!(policy.should_sandbox());
}
#[test]
fn test_workspace_with_network() {
let policy = SandboxPolicy::workspace_with_network();
assert!(policy.has_network_access());
assert!(policy.should_sandbox());
}
#[test]
fn test_writable_root_basic() {
let root = WritableRoot::new(PathBuf::from("/project"));
assert!(root.is_path_writable(Path::new("/project/src/main.rs")));
assert!(!root.is_path_writable(Path::new("/other/file.txt")));
}
#[test]
fn test_writable_root_with_exceptions() {
let root = WritableRoot::with_exceptions(
PathBuf::from("/project"),
vec![PathBuf::from("/project/.deepseek")],
);
assert!(root.is_path_writable(Path::new("/project/src/main.rs")));
assert!(!root.is_path_writable(Path::new("/project/.deepseek/config")));
}
#[test]
fn test_policy_serialization() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![PathBuf::from("/extra")],
network_access: true,
exclude_tmpdir: false,
exclude_slash_tmp: false,
};
let json = serde_json::to_string(&policy).unwrap();
assert!(json.contains("workspace-write"));
let parsed: SandboxPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy, parsed);
}
}