use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WritableRoot {
pub root: PathBuf,
}
impl WritableRoot {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { root: path.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NetworkAllowlistEntry {
pub domain: String,
#[serde(default = "default_https_port")]
pub port: u16,
#[serde(default = "default_protocol")]
pub protocol: String,
}
fn default_https_port() -> u16 {
443
}
fn default_protocol() -> String {
"tcp".to_string()
}
impl NetworkAllowlistEntry {
pub fn https(domain: impl Into<String>) -> Self {
Self {
domain: domain.into(),
port: 443,
protocol: "tcp".to_string(),
}
}
pub fn with_port(domain: impl Into<String>, port: u16) -> Self {
Self {
domain: domain.into(),
port,
protocol: "tcp".to_string(),
}
}
pub fn matches(&self, domain: &str, port: u16) -> bool {
if self.port != port {
return false;
}
if self.domain.starts_with("*.") {
let suffix = &self.domain[1..]; domain.ends_with(suffix) || domain == &self.domain[2..]
} else {
domain == self.domain
}
}
}
pub const DEFAULT_SENSITIVE_PATHS: &[&str] = &[
"~/.ssh",
"~/.aws",
"~/.config/gcloud",
"~/.azure",
"~/.kube",
"~/.docker",
"~/.npmrc",
"~/.pypirc",
"~/.config/gh",
"~/.secrets",
"~/.gnupg",
"~/.config/op",
"~/.vault-token",
"~/.terraform.d/credentials.tfrc.json",
"~/.cargo/credentials.toml",
"~/.git-credentials",
"~/.netrc",
];
#[cfg(windows)]
const USERPROFILE_READ_ROOT_EXCLUSIONS: &[&str] = &[
".ssh",
".gnupg",
".aws",
".azure",
".kube",
".docker",
".config",
".npm",
".pki",
".terraform.d",
];
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SensitivePath {
pub path: String,
#[serde(default = "default_true")]
pub block_read: bool,
#[serde(default = "default_true")]
pub block_write: bool,
}
fn default_true() -> bool {
true
}
impl SensitivePath {
pub fn new(path: impl Into<String>) -> Self {
Self {
path: path.into(),
block_read: true,
block_write: true,
}
}
pub fn write_only(path: impl Into<String>) -> Self {
Self {
path: path.into(),
block_read: false,
block_write: true,
}
}
pub fn expand_path(&self) -> PathBuf {
if self.path.starts_with("~/")
&& let Some(home) = dirs::home_dir()
{
return home.join(&self.path[2..]);
} else if self.path == "~"
&& let Some(home) = dirs::home_dir()
{
return home;
}
PathBuf::from(&self.path)
}
pub fn matches(&self, path: &Path) -> bool {
let expanded = self.expand_path();
#[cfg(windows)]
{
let path_norm = normalize_windows_path(path);
let expanded_norm = normalize_windows_path(&expanded);
let mut expanded_prefix = expanded_norm.clone();
if !expanded_prefix.ends_with('/') {
expanded_prefix.push('/');
}
return path_norm == expanded_norm || path_norm.starts_with(&expanded_prefix);
}
path.starts_with(&expanded)
}
}
#[cfg(windows)]
fn normalize_windows_path(path: &Path) -> String {
path.to_string_lossy()
.replace('\\', "/")
.to_ascii_lowercase()
}
pub fn default_sensitive_paths() -> Vec<SensitivePath> {
let paths: Vec<SensitivePath> = DEFAULT_SENSITIVE_PATHS
.iter()
.map(|p| SensitivePath::new(*p))
.collect();
#[cfg(windows)]
{
let mut paths = paths;
for entry in USERPROFILE_READ_ROOT_EXCLUSIONS {
let path = format!("~/{}", entry);
if !paths.iter().any(|existing| existing.path == path) {
paths.push(SensitivePath::new(path));
}
}
paths
}
#[cfg(not(windows))]
paths
}
const PROTECTED_WRITABLE_ROOT_DIR_NAMES: &[&str] = &[".git", ".vtcode", ".codex", ".agents"];
fn protected_writable_root_sensitive_paths(writable_roots: &[WritableRoot]) -> Vec<SensitivePath> {
let mut paths = Vec::new();
for root in writable_roots {
for dir_name in PROTECTED_WRITABLE_ROOT_DIR_NAMES {
let protected_path = root.root.join(dir_name).display().to_string();
if !paths.iter().any(|existing: &SensitivePath| {
existing.path == protected_path && !existing.block_read && existing.block_write
}) {
paths.push(SensitivePath::write_only(protected_path));
}
}
}
paths
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceLimits {
#[serde(default)]
pub max_memory_mb: u64,
#[serde(default)]
pub max_pids: u32,
#[serde(default)]
pub max_disk_mb: u64,
#[serde(default)]
pub cpu_time_secs: u64,
#[serde(default)]
pub timeout_secs: u64,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_memory_mb: 0, max_pids: 0, max_disk_mb: 0, cpu_time_secs: 0, timeout_secs: 300, }
}
}
impl ResourceLimits {
pub fn unlimited() -> Self {
Self {
max_memory_mb: 0,
max_pids: 0,
max_disk_mb: 0,
cpu_time_secs: 0,
timeout_secs: 0,
}
}
pub fn conservative() -> Self {
Self {
max_memory_mb: 512, max_pids: 64, max_disk_mb: 1024, cpu_time_secs: 60, timeout_secs: 120, }
}
pub fn moderate() -> Self {
Self {
max_memory_mb: 2048, max_pids: 256, max_disk_mb: 4096, cpu_time_secs: 300, timeout_secs: 600, }
}
pub fn generous() -> Self {
Self {
max_memory_mb: 8192, max_pids: 1024, max_disk_mb: 16384, cpu_time_secs: 0, timeout_secs: 3600, }
}
pub fn with_memory_mb(mut self, mb: u64) -> Self {
self.max_memory_mb = mb;
self
}
pub fn with_max_pids(mut self, pids: u32) -> Self {
self.max_pids = pids;
self
}
pub fn with_disk_mb(mut self, mb: u64) -> Self {
self.max_disk_mb = mb;
self
}
pub fn with_cpu_time_secs(mut self, secs: u64) -> Self {
self.cpu_time_secs = secs;
self
}
pub fn with_timeout_secs(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
pub fn has_limits(&self) -> bool {
self.max_memory_mb > 0
|| self.max_pids > 0
|| self.max_disk_mb > 0
|| self.cpu_time_secs > 0
}
pub fn effective_timeout_secs(&self) -> u64 {
if self.timeout_secs > 0 {
self.timeout_secs
} else {
300 }
}
}
pub const BLOCKED_SYSCALLS: &[&str] = &[
"ptrace",
"mount",
"umount",
"umount2",
"init_module",
"finit_module",
"delete_module",
"kexec_load",
"kexec_file_load",
"bpf",
"perf_event_open",
"userfaultfd",
"process_vm_readv",
"process_vm_writev",
"reboot",
"swapon",
"swapoff",
"settimeofday",
"clock_settime",
"adjtimex",
"add_key",
"request_key",
"keyctl",
"ioperm",
"iopl",
"iopl",
"acct",
"quotactl",
"unshare",
"setns",
"personality",
];
pub const FILTERED_SYSCALLS: &[&str] = &[
"clone", "clone3", "ioctl", "prctl", "socket",
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeccompProfile {
#[serde(default = "default_blocked_syscalls")]
pub blocked_syscalls: Vec<String>,
#[serde(default)]
pub allow_namespaces: bool,
#[serde(default)]
pub allow_network_sockets: bool,
#[serde(default)]
pub log_only: bool,
}
fn default_blocked_syscalls() -> Vec<String> {
BLOCKED_SYSCALLS.iter().map(|s| s.to_string()).collect()
}
impl Default for SeccompProfile {
fn default() -> Self {
Self {
blocked_syscalls: default_blocked_syscalls(),
allow_namespaces: false,
allow_network_sockets: false,
log_only: false,
}
}
}
impl SeccompProfile {
pub fn strict() -> Self {
Self {
blocked_syscalls: default_blocked_syscalls(),
allow_namespaces: false,
allow_network_sockets: false,
log_only: false,
}
}
pub fn permissive() -> Self {
Self {
blocked_syscalls: vec![
"ptrace".to_string(),
"kexec_load".to_string(),
"kexec_file_load".to_string(),
"reboot".to_string(),
],
allow_namespaces: false,
allow_network_sockets: true,
log_only: false,
}
}
pub fn logging() -> Self {
Self {
blocked_syscalls: default_blocked_syscalls(),
allow_namespaces: false,
allow_network_sockets: false,
log_only: true,
}
}
pub fn block_syscall(mut self, syscall: impl Into<String>) -> Self {
let syscall = syscall.into();
if !self.blocked_syscalls.contains(&syscall) {
self.blocked_syscalls.push(syscall);
}
self
}
pub fn with_network(mut self) -> Self {
self.allow_network_sockets = true;
self
}
pub fn with_logging(mut self) -> Self {
self.log_only = true;
self
}
pub fn is_blocked(&self, syscall: &str) -> bool {
self.blocked_syscalls.iter().any(|s| s == syscall)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SandboxPolicy {
ReadOnly {
#[serde(default)]
network_access: bool,
#[serde(default)]
network_allowlist: Vec<NetworkAllowlistEntry>,
},
WorkspaceWrite {
writable_roots: Vec<WritableRoot>,
#[serde(default)]
network_access: bool,
#[serde(default)]
network_allowlist: Vec<NetworkAllowlistEntry>,
#[serde(default)]
sensitive_paths: Option<Vec<SensitivePath>>,
#[serde(default)]
resource_limits: ResourceLimits,
#[serde(default)]
seccomp_profile: SeccompProfile,
#[serde(default)]
exclude_tmpdir_env_var: bool,
#[serde(default)]
exclude_slash_tmp: bool,
},
DangerFullAccess,
ExternalSandbox {
description: String,
},
}
impl SandboxPolicy {
pub fn read_only() -> Self {
Self::ReadOnly {
network_access: false,
network_allowlist: Vec::new(),
}
}
pub fn new_read_only_policy() -> Self {
Self::read_only()
}
pub fn read_only_with_network(network_allowlist: Vec<NetworkAllowlistEntry>) -> Self {
Self::ReadOnly {
network_access: !network_allowlist.is_empty(),
network_allowlist,
}
}
pub fn read_only_with_full_network() -> Self {
Self::ReadOnly {
network_access: true,
network_allowlist: Vec::new(),
}
}
pub fn workspace_write(writable_roots: Vec<PathBuf>) -> Self {
Self::WorkspaceWrite {
writable_roots: writable_roots.into_iter().map(WritableRoot::new).collect(),
network_access: false,
network_allowlist: Vec::new(),
sensitive_paths: None, resource_limits: ResourceLimits::default(),
seccomp_profile: SeccompProfile::strict(),
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn workspace_write_with_network(
writable_roots: Vec<PathBuf>,
network_allowlist: Vec<NetworkAllowlistEntry>,
) -> Self {
Self::WorkspaceWrite {
writable_roots: writable_roots.into_iter().map(WritableRoot::new).collect(),
network_access: !network_allowlist.is_empty(),
network_allowlist,
sensitive_paths: None, resource_limits: ResourceLimits::default(),
seccomp_profile: SeccompProfile::strict().with_network(),
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn workspace_write_with_sensitive_paths(
writable_roots: Vec<PathBuf>,
sensitive_paths: Vec<SensitivePath>,
) -> Self {
Self::WorkspaceWrite {
writable_roots: writable_roots.into_iter().map(WritableRoot::new).collect(),
network_access: false,
network_allowlist: Vec::new(),
sensitive_paths: Some(sensitive_paths),
resource_limits: ResourceLimits::default(),
seccomp_profile: SeccompProfile::strict(),
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn workspace_write_no_sensitive_blocking(writable_roots: Vec<PathBuf>) -> Self {
Self::WorkspaceWrite {
writable_roots: writable_roots.into_iter().map(WritableRoot::new).collect(),
network_access: false,
network_allowlist: Vec::new(),
sensitive_paths: Some(Vec::new()), resource_limits: ResourceLimits::default(),
seccomp_profile: SeccompProfile::strict(),
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn workspace_write_with_limits(
writable_roots: Vec<PathBuf>,
resource_limits: ResourceLimits,
) -> Self {
Self::WorkspaceWrite {
writable_roots: writable_roots.into_iter().map(WritableRoot::new).collect(),
network_access: false,
network_allowlist: Vec::new(),
sensitive_paths: None,
resource_limits,
seccomp_profile: SeccompProfile::strict(),
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn workspace_write_full(
writable_roots: Vec<PathBuf>,
network_allowlist: Vec<NetworkAllowlistEntry>,
sensitive_paths: Option<Vec<SensitivePath>>,
resource_limits: ResourceLimits,
seccomp_profile: SeccompProfile,
) -> Self {
Self::WorkspaceWrite {
writable_roots: writable_roots.into_iter().map(WritableRoot::new).collect(),
network_access: !network_allowlist.is_empty(),
network_allowlist,
sensitive_paths,
resource_limits,
seccomp_profile,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn full_access() -> Self {
Self::DangerFullAccess
}
pub fn has_full_network_access(&self) -> bool {
match self {
Self::ReadOnly {
network_access,
network_allowlist,
}
| Self::WorkspaceWrite {
network_access,
network_allowlist,
..
} => *network_access && network_allowlist.is_empty(),
Self::DangerFullAccess | Self::ExternalSandbox { .. } => true,
}
}
pub fn has_network_allowlist(&self) -> bool {
match self {
Self::ReadOnly {
network_allowlist, ..
}
| Self::WorkspaceWrite {
network_allowlist, ..
} => !network_allowlist.is_empty(),
_ => false,
}
}
pub fn network_allowlist(&self) -> &[NetworkAllowlistEntry] {
match self {
Self::ReadOnly {
network_allowlist, ..
}
| Self::WorkspaceWrite {
network_allowlist, ..
} => network_allowlist,
_ => &[],
}
}
pub fn is_network_allowed(&self, domain: &str, port: u16) -> bool {
match self {
Self::ReadOnly {
network_access,
network_allowlist,
}
| Self::WorkspaceWrite {
network_access,
network_allowlist,
..
} => {
if network_allowlist.is_empty() {
*network_access
} else {
network_allowlist
.iter()
.any(|entry| entry.matches(domain, port))
}
}
Self::DangerFullAccess | Self::ExternalSandbox { .. } => true,
}
}
pub fn sensitive_paths(&self) -> Vec<SensitivePath> {
match self {
Self::ReadOnly { .. } => default_sensitive_paths(),
Self::WorkspaceWrite {
sensitive_paths, ..
} => sensitive_paths
.clone()
.unwrap_or_else(default_sensitive_paths),
Self::DangerFullAccess | Self::ExternalSandbox { .. } => Vec::new(),
}
}
pub fn sensitive_paths_for_execution(&self, cwd: &Path) -> Vec<SensitivePath> {
match self {
Self::WorkspaceWrite { .. } => {
let mut sensitive_paths = self.sensitive_paths();
sensitive_paths.extend(protected_writable_root_sensitive_paths(
&self.get_writable_roots_with_cwd(cwd),
));
sensitive_paths
}
_ => self.sensitive_paths(),
}
}
pub fn is_sensitive_path(&self, path: &Path) -> bool {
self.sensitive_paths()
.iter()
.any(|sp| sp.matches(path) && sp.block_read)
}
pub fn is_path_write_blocked(&self, path: &Path, cwd: &Path) -> bool {
match self {
Self::DangerFullAccess | Self::ExternalSandbox { .. } => false,
_ => self
.sensitive_paths_for_execution(cwd)
.iter()
.any(|sp| sp.matches(path) && sp.block_write),
}
}
pub fn is_path_readable(&self, path: &Path) -> bool {
match self {
Self::DangerFullAccess | Self::ExternalSandbox { .. } => true,
_ => !self.is_sensitive_path(path),
}
}
pub fn resource_limits(&self) -> ResourceLimits {
match self {
Self::ReadOnly { .. } => ResourceLimits::conservative(),
Self::WorkspaceWrite {
resource_limits, ..
} => resource_limits.clone(),
Self::DangerFullAccess | Self::ExternalSandbox { .. } => ResourceLimits::unlimited(),
}
}
pub fn seccomp_profile(&self) -> SeccompProfile {
match self {
Self::ReadOnly {
network_access,
network_allowlist,
} => {
let mut profile = SeccompProfile::strict();
if *network_access || !network_allowlist.is_empty() {
profile = profile.with_network();
}
profile
}
Self::WorkspaceWrite {
seccomp_profile, ..
} => seccomp_profile.clone(),
Self::DangerFullAccess | Self::ExternalSandbox { .. } => SeccompProfile::permissive(),
}
}
pub fn has_full_disk_write_access(&self) -> bool {
matches!(self, Self::DangerFullAccess | Self::ExternalSandbox { .. })
}
pub fn has_full_disk_read_access(&self) -> bool {
true
}
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
match self {
Self::ReadOnly { .. } => vec![],
Self::WorkspaceWrite { writable_roots, .. } => {
let mut roots = writable_roots.clone();
let cwd_root = WritableRoot::new(cwd);
if !roots.contains(&cwd_root) {
roots.push(cwd_root);
}
roots
}
Self::DangerFullAccess | Self::ExternalSandbox { .. } => {
vec![WritableRoot::new(cwd)]
}
}
}
pub fn is_path_writable(&self, path: &Path, cwd: &Path) -> bool {
match self {
Self::ReadOnly { .. } => false,
Self::WorkspaceWrite { .. } => {
let writable = self.get_writable_roots_with_cwd(cwd);
writable.iter().any(|root| path.starts_with(&root.root))
&& !self.is_path_write_blocked(path, cwd)
}
Self::DangerFullAccess | Self::ExternalSandbox { .. } => true,
}
}
pub fn can_set(&self, new_policy: &SandboxPolicy) -> anyhow::Result<()> {
use SandboxPolicy::*;
match (self, new_policy) {
(DangerFullAccess, _) => Ok(()),
(ReadOnly { .. }, WorkspaceWrite { .. } | DangerFullAccess) => Err(anyhow::anyhow!(
"cannot escalate from read-only to write-capable policy"
)),
_ => Ok(()),
}
}
pub fn description(&self) -> &'static str {
match self {
Self::ReadOnly { .. } => "read-only access",
Self::WorkspaceWrite { .. } => "workspace write access",
Self::DangerFullAccess => "full access (dangerous)",
Self::ExternalSandbox { .. } => "external sandbox",
}
}
}
impl Default for SandboxPolicy {
fn default() -> Self {
Self::read_only()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_only_policy() {
let policy = SandboxPolicy::read_only();
assert!(!policy.has_full_network_access());
assert!(!policy.has_network_allowlist());
assert!(!policy.has_full_disk_write_access());
assert!(policy.has_full_disk_read_access());
}
#[test]
fn test_read_only_with_network_allowlist() {
let policy = SandboxPolicy::read_only_with_network(vec![
NetworkAllowlistEntry::https("api.github.com"),
NetworkAllowlistEntry::with_port("registry.npmjs.org", 443),
]);
assert!(!policy.has_full_network_access());
assert!(policy.has_network_allowlist());
assert!(policy.is_network_allowed("api.github.com", 443));
assert!(policy.is_network_allowed("registry.npmjs.org", 443));
assert!(!policy.is_network_allowed("example.com", 443));
}
#[test]
fn test_read_only_with_full_network_access() {
let policy = SandboxPolicy::read_only_with_full_network();
assert!(policy.has_full_network_access());
assert!(policy.is_network_allowed("example.com", 443));
assert!(policy.seccomp_profile().allow_network_sockets);
}
#[test]
fn test_read_only_deserializes_legacy_shape() {
let policy: SandboxPolicy =
serde_json::from_str(r#"{"type":"read_only"}"#).expect("legacy read-only policy");
assert_eq!(policy, SandboxPolicy::read_only());
}
#[test]
fn test_workspace_write_policy() {
let policy = SandboxPolicy::workspace_write(vec![PathBuf::from("/tmp/workspace")]);
assert!(!policy.has_full_network_access());
assert!(!policy.has_full_disk_write_access());
let cwd = PathBuf::from("/tmp/workspace");
assert!(policy.is_path_writable(&cwd, &cwd));
assert!(!policy.is_path_writable(&PathBuf::from("/etc"), &cwd));
}
#[test]
fn test_workspace_write_protects_internal_metadata_dirs() {
let cwd = PathBuf::from("/tmp/workspace");
let policy = SandboxPolicy::workspace_write(vec![cwd.clone()]);
assert!(!policy.is_path_writable(&cwd.join(".git/config"), &cwd));
assert!(!policy.is_path_writable(&cwd.join(".vtcode/cache"), &cwd));
assert!(!policy.is_path_writable(&cwd.join(".codex/state"), &cwd));
assert!(!policy.is_path_writable(&cwd.join(".agents/skills"), &cwd));
assert!(policy.is_path_writable(&cwd.join("src/main.rs"), &cwd));
}
#[test]
fn test_full_access_policy() {
let policy = SandboxPolicy::full_access();
assert!(policy.has_full_network_access());
assert!(policy.has_full_disk_write_access());
}
#[test]
fn test_policy_escalation() {
let read_only = SandboxPolicy::read_only();
let full = SandboxPolicy::full_access();
assert!(read_only.can_set(&full).is_err());
assert!(full.can_set(&read_only).is_ok());
}
#[test]
fn test_network_allowlist_entry_matching() {
let entry = NetworkAllowlistEntry::https("api.github.com");
assert!(entry.matches("api.github.com", 443));
assert!(!entry.matches("api.github.com", 80));
assert!(!entry.matches("github.com", 443));
}
#[test]
fn test_network_allowlist_wildcard() {
let entry = NetworkAllowlistEntry::https("*.npmjs.org");
assert!(entry.matches("registry.npmjs.org", 443));
assert!(entry.matches("npmjs.org", 443));
assert!(!entry.matches("npmjs.org.evil.com", 443));
}
#[test]
fn test_workspace_with_network_allowlist() {
let allowlist = vec![
NetworkAllowlistEntry::https("api.github.com"),
NetworkAllowlistEntry::https("*.npmjs.org"),
];
let policy = SandboxPolicy::workspace_write_with_network(
vec![PathBuf::from("/tmp/workspace")],
allowlist,
);
assert!(!policy.has_full_network_access());
assert!(policy.has_network_allowlist());
assert!(policy.is_network_allowed("api.github.com", 443));
assert!(policy.is_network_allowed("registry.npmjs.org", 443));
assert!(!policy.is_network_allowed("evil.com", 443));
assert!(!policy.is_network_allowed("api.github.com", 80));
}
#[test]
fn test_workspace_no_network() {
let policy = SandboxPolicy::workspace_write(vec![PathBuf::from("/tmp/workspace")]);
assert!(!policy.has_full_network_access());
assert!(!policy.has_network_allowlist());
assert!(!policy.is_network_allowed("api.github.com", 443));
}
#[test]
fn test_sensitive_path_expansion() {
let sp = SensitivePath::new("~/.ssh");
let expanded = sp.expand_path();
assert!(expanded.to_string_lossy().contains(".ssh"));
assert!(!expanded.to_string_lossy().starts_with('~'));
}
#[test]
fn test_sensitive_path_matching() {
let sp = SensitivePath::new("~/.ssh");
let expanded = sp.expand_path();
let ssh_key = expanded.join("id_rsa");
assert!(sp.matches(&ssh_key));
assert!(sp.matches(&expanded));
}
#[test]
fn test_default_sensitive_paths() {
let paths = default_sensitive_paths();
assert!(!paths.is_empty());
let path_strings: Vec<&str> = paths.iter().map(|p| p.path.as_str()).collect();
assert!(path_strings.contains(&"~/.ssh"));
assert!(path_strings.contains(&"~/.aws"));
assert!(path_strings.contains(&"~/.kube"));
}
#[cfg(windows)]
#[test]
fn test_windows_userprofile_root_exclusions_are_in_defaults() {
let paths = default_sensitive_paths();
let path_strings: Vec<&str> = paths.iter().map(|p| p.path.as_str()).collect();
for entry in USERPROFILE_READ_ROOT_EXCLUSIONS {
let expected = format!("~/{}", entry);
assert!(
path_strings.contains(&expected.as_str()),
"missing expected default sensitive path: {expected}"
);
}
}
#[cfg(windows)]
#[test]
fn test_sensitive_path_matching_is_case_insensitive_on_windows() {
let sp = SensitivePath::new("~/.aws");
let home = dirs::home_dir().expect("home dir");
let mixed_case_candidate = home.join(".AWS").join("credentials");
assert!(sp.matches(&mixed_case_candidate));
}
#[test]
fn test_workspace_blocks_sensitive_by_default() {
let policy = SandboxPolicy::workspace_write(vec![PathBuf::from("/tmp/workspace")]);
let sensitive = policy.sensitive_paths();
assert!(!sensitive.is_empty());
if let Some(home) = dirs::home_dir() {
let ssh_path = home.join(".ssh").join("id_rsa");
assert!(policy.is_sensitive_path(&ssh_path));
assert!(!policy.is_path_readable(&ssh_path));
}
}
#[test]
fn test_workspace_no_sensitive_blocking() {
let policy =
SandboxPolicy::workspace_write_no_sensitive_blocking(vec![PathBuf::from("/tmp")]);
let sensitive = policy.sensitive_paths();
assert!(sensitive.is_empty());
if let Some(home) = dirs::home_dir() {
let ssh_path = home.join(".ssh").join("id_rsa");
assert!(!policy.is_sensitive_path(&ssh_path));
assert!(policy.is_path_readable(&ssh_path));
}
}
#[test]
fn test_full_access_no_sensitive_blocking() {
let policy = SandboxPolicy::full_access();
let sensitive = policy.sensitive_paths();
assert!(sensitive.is_empty());
if let Some(home) = dirs::home_dir() {
let ssh_path = home.join(".ssh").join("id_rsa");
assert!(policy.is_path_readable(&ssh_path));
}
}
#[test]
fn test_resource_limits_default() {
let limits = ResourceLimits::default();
assert_eq!(limits.max_memory_mb, 0);
assert_eq!(limits.max_pids, 0);
assert_eq!(limits.timeout_secs, 300);
assert!(!limits.has_limits());
}
#[test]
fn test_resource_limits_conservative() {
let limits = ResourceLimits::conservative();
assert_eq!(limits.max_memory_mb, 512);
assert_eq!(limits.max_pids, 64);
assert_eq!(limits.cpu_time_secs, 60);
assert!(limits.has_limits());
}
#[test]
fn test_resource_limits_builder() {
let limits = ResourceLimits::default()
.with_memory_mb(1024)
.with_max_pids(128)
.with_timeout_secs(60);
assert_eq!(limits.max_memory_mb, 1024);
assert_eq!(limits.max_pids, 128);
assert_eq!(limits.effective_timeout_secs(), 60);
}
#[test]
fn test_workspace_with_limits() {
let limits = ResourceLimits::conservative();
let policy = SandboxPolicy::workspace_write_with_limits(
vec![PathBuf::from("/tmp/workspace")],
limits.clone(),
);
let policy_limits = policy.resource_limits();
assert_eq!(policy_limits.max_memory_mb, limits.max_memory_mb);
assert_eq!(policy_limits.max_pids, limits.max_pids);
}
#[test]
fn test_read_only_conservative_limits() {
let policy = SandboxPolicy::read_only();
let limits = policy.resource_limits();
assert!(limits.has_limits());
assert_eq!(limits.max_memory_mb, 512);
}
#[test]
fn test_full_access_unlimited() {
let policy = SandboxPolicy::full_access();
let limits = policy.resource_limits();
assert!(!limits.has_limits());
}
#[test]
fn test_seccomp_profile_strict() {
let profile = SeccompProfile::strict();
assert!(profile.is_blocked("ptrace"));
assert!(profile.is_blocked("mount"));
assert!(profile.is_blocked("kexec_load"));
assert!(profile.is_blocked("bpf"));
assert!(!profile.allow_network_sockets);
assert!(!profile.allow_namespaces);
}
#[test]
fn test_seccomp_profile_permissive() {
let profile = SeccompProfile::permissive();
assert!(profile.is_blocked("ptrace"));
assert!(profile.is_blocked("kexec_load"));
assert!(profile.allow_network_sockets);
}
#[test]
fn test_seccomp_profile_builder() {
let profile = SeccompProfile::strict()
.with_network()
.block_syscall("custom_syscall");
assert!(profile.allow_network_sockets);
assert!(profile.is_blocked("custom_syscall"));
}
#[test]
fn test_workspace_seccomp_profile() {
let policy = SandboxPolicy::workspace_write(vec![PathBuf::from("/tmp")]);
let profile = policy.seccomp_profile();
assert!(profile.is_blocked("ptrace"));
assert!(profile.is_blocked("mount"));
}
#[test]
fn test_workspace_with_network_seccomp() {
let policy = SandboxPolicy::workspace_write_with_network(
vec![PathBuf::from("/tmp")],
vec![NetworkAllowlistEntry::https("api.github.com")],
);
let profile = policy.seccomp_profile();
assert!(profile.allow_network_sockets);
}
#[test]
fn test_seccomp_profile_json() {
let profile = SeccompProfile::strict();
let json = profile.to_json().unwrap();
assert!(json.contains("ptrace"));
assert!(json.contains("blocked_syscalls"));
}
#[test]
fn test_blocked_syscalls_constant() {
assert!(BLOCKED_SYSCALLS.contains(&"ptrace"));
assert!(BLOCKED_SYSCALLS.contains(&"mount"));
assert!(BLOCKED_SYSCALLS.contains(&"kexec_load"));
assert!(BLOCKED_SYSCALLS.contains(&"bpf"));
assert!(BLOCKED_SYSCALLS.contains(&"perf_event_open"));
}
}