#![allow(dead_code)]
pub mod backend;
pub mod opensandbox;
pub mod policy;
#[cfg(target_os = "macos")]
pub mod seatbelt;
#[cfg(target_os = "linux")]
pub mod bwrap;
#[cfg(target_os = "linux")]
pub mod landlock;
#[cfg(target_os = "windows")]
pub mod windows;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use zagens_config::WindowsSandboxModeToml;
pub use policy::SandboxPolicy;
use self::backend::SandboxBackend;
#[derive(Clone, Default)]
pub struct TuiSandboxHost(pub Option<Arc<dyn SandboxBackend>>);
impl TuiSandboxHost {
#[must_use]
pub fn new(backend: Option<Arc<dyn SandboxBackend>>) -> Self {
Self(backend)
}
}
impl zagens_core::engine::hosts::SandboxHost for TuiSandboxHost {
fn backend(&self) -> Option<&Arc<dyn SandboxBackend>> {
self.0.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct CommandSpec {
pub program: String,
pub args: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub timeout: Duration,
pub sandbox_policy: SandboxPolicy,
pub justification: Option<String>,
}
#[cfg(windows)]
pub(crate) fn windows_shell() -> (&'static str, &'static str) {
use std::sync::OnceLock;
static DETECTED: OnceLock<(&'static str, &'static str)> = OnceLock::new();
*DETECTED.get_or_init(|| {
for ps in &["pwsh", "powershell"] {
if std::process::Command::new(ps)
.args(["-NoProfile", "-NonInteractive", "-Command", "exit 0"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return (ps, "-Command");
}
}
("cmd", "/C")
})
}
impl CommandSpec {
pub fn shell(command: &str, cwd: PathBuf, timeout: Duration) -> Self {
#[cfg(windows)]
let (program, arg_prefix) = windows_shell();
#[cfg(windows)]
let (program, args) = (
program.to_string(),
vec![arg_prefix.to_string(), command.to_string()],
);
#[cfg(not(windows))]
let (program, args) = (
"sh".to_string(),
vec!["-c".to_string(), command.to_string()],
);
Self {
program,
args,
cwd,
env: HashMap::new(),
timeout,
sandbox_policy: SandboxPolicy::default(),
justification: None,
}
}
pub fn program(program: &str, args: Vec<String>, cwd: PathBuf, timeout: Duration) -> Self {
Self {
program: program.to_string(),
args,
cwd,
env: HashMap::new(),
timeout,
sandbox_policy: SandboxPolicy::default(),
justification: None,
}
}
pub fn with_policy(mut self, policy: SandboxPolicy) -> Self {
self.sandbox_policy = policy;
self
}
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
self.env = env;
self
}
pub fn with_env_var(mut self, key: &str, value: &str) -> Self {
self.env.insert(key.to_string(), value.to_string());
self
}
pub fn with_justification(mut self, justification: &str) -> Self {
self.justification = Some(justification.to_string());
self
}
pub fn display_command(&self) -> String {
if self.program == "sh" && self.args.len() == 2 && self.args[0] == "-c" {
self.args[1].clone()
} else if (self.program.eq_ignore_ascii_case("cmd")
&& self.args.len() == 2
&& self.args[0].eq_ignore_ascii_case("/C"))
|| ((self.program.eq_ignore_ascii_case("powershell")
|| self.program.eq_ignore_ascii_case("pwsh"))
&& self.args.len() == 2
&& self.args[0].eq_ignore_ascii_case("-Command"))
{
self.args[1].clone()
} else {
let mut parts = vec![self.program.clone()];
parts.extend(self.args.clone());
parts.join(" ")
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SandboxType {
#[default]
None,
#[cfg(target_os = "macos")]
MacosSeatbelt,
#[cfg(target_os = "linux")]
LinuxLandlock,
#[cfg(target_os = "linux")]
LinuxBwrap,
#[cfg(target_os = "windows")]
Windows,
}
impl std::fmt::Display for SandboxType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SandboxType::None => write!(f, "none"),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => write!(f, "macos-seatbelt"),
#[cfg(target_os = "linux")]
SandboxType::LinuxLandlock => write!(f, "linux-landlock"),
#[cfg(target_os = "linux")]
SandboxType::LinuxBwrap => write!(f, "linux-bwrap"),
#[cfg(target_os = "windows")]
SandboxType::Windows => write!(f, "windows-sandbox"),
}
}
}
#[derive(Debug)]
pub struct ExecEnv {
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub timeout: Duration,
pub sandbox_type: SandboxType,
pub policy: SandboxPolicy,
pub enforced: bool,
#[cfg(target_os = "windows")]
pub windows_plan: Option<zagens_windows_sandbox::WindowsExecPlan>,
}
impl ExecEnv {
pub fn program(&self) -> &str {
self.command
.first()
.map_or("sh", std::string::String::as_str)
}
pub fn args(&self) -> &[String] {
if self.command.len() > 1 {
&self.command[1..]
} else {
&[]
}
}
pub fn is_sandboxed(&self) -> bool {
!matches!(self.sandbox_type, SandboxType::None)
}
pub fn is_enforced(&self) -> bool {
self.enforced
}
}
pub fn get_platform_sandbox() -> Option<SandboxType> {
#[cfg(target_os = "macos")]
{
if seatbelt::is_available() {
return Some(SandboxType::MacosSeatbelt);
}
}
#[cfg(target_os = "linux")]
{
if landlock::is_available() {
return Some(SandboxType::LinuxLandlock);
}
}
#[cfg(target_os = "windows")]
{
if windows::is_available() {
return Some(SandboxType::Windows);
}
}
None
}
pub fn is_sandbox_available() -> bool {
get_platform_sandbox().is_some()
}
#[must_use]
pub fn policy_degraded_mode_notice() -> Option<&'static str> {
#[cfg(target_os = "macos")]
{
if seatbelt::is_available() {
return None;
}
Some(
"Degraded mode: sandbox-exec (Seatbelt) is unavailable; sandbox_mode declares policy only.",
)
}
#[cfg(target_os = "linux")]
{
let _ = landlock::is_available();
Some(
"Degraded mode: Landlock rules are not enforced yet; sandbox_mode declares policy only. Install bubblewrap and set `prefer_bwrap = true` for enforced isolation.",
)
}
#[cfg(target_os = "windows")]
{
let home = zagens_windows_sandbox::zagens_home();
if zagens_windows_sandbox::sandbox_setup_is_complete(&home) {
return None;
}
if zagens_windows_sandbox::is_enforcement_available() {
return Some(
"Degraded mode: elevated sandbox setup is not complete; using unelevated write isolation only (no profile read isolation). Run `zagens sandbox setup`.",
);
}
Some(
"Degraded mode: Windows sandbox is not enforced yet; sandbox_mode declares policy only.",
)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
Some("Degraded mode: OS sandbox is not supported on this platform.")
}
#[derive(Debug, Default)]
pub struct SandboxManager {
sandbox_available: Option<bool>,
#[allow(dead_code)]
forced_sandbox: Option<SandboxType>,
windows_sandbox_mode: WindowsSandboxModeToml,
windows_private_desktop: bool,
prefer_bwrap: bool,
}
impl SandboxManager {
pub fn new() -> Self {
Self {
sandbox_available: None,
forced_sandbox: None,
windows_sandbox_mode: WindowsSandboxModeToml::Unelevated,
windows_private_desktop: false,
prefer_bwrap: false,
}
}
pub fn set_prefer_bwrap(&mut self, prefer: bool) {
self.prefer_bwrap = prefer;
}
#[must_use]
pub fn prefer_bwrap(&self) -> bool {
self.prefer_bwrap
}
pub fn set_windows_sandbox_mode(&mut self, mode: WindowsSandboxModeToml) {
self.windows_sandbox_mode = mode;
}
pub fn set_windows_private_desktop(&mut self, enabled: bool) {
self.windows_private_desktop = enabled;
}
#[must_use]
pub fn windows_private_desktop(&self) -> bool {
self.windows_private_desktop
}
#[must_use]
pub fn windows_sandbox_mode(&self) -> WindowsSandboxModeToml {
self.windows_sandbox_mode
}
pub fn is_available(&mut self) -> bool {
if let Some(available) = self.sandbox_available {
return available;
}
let available = is_sandbox_available();
self.sandbox_available = Some(available);
available
}
pub fn select_sandbox(&self, policy: &SandboxPolicy) -> SandboxType {
if !policy.should_sandbox() {
return SandboxType::None;
}
if let Some(forced) = self.forced_sandbox {
return forced;
}
#[cfg(target_os = "linux")]
if self.prefer_bwrap && bwrap::is_available() {
return SandboxType::LinuxBwrap;
}
get_platform_sandbox().unwrap_or(SandboxType::None)
}
pub fn prepare(&self, spec: &CommandSpec) -> ExecEnv {
let sandbox_type = self.select_sandbox(&spec.sandbox_policy);
match sandbox_type {
SandboxType::None => Self::prepare_unsandboxed(spec),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => Self::prepare_seatbelt(spec),
#[cfg(target_os = "linux")]
SandboxType::LinuxLandlock => Self::prepare_landlock(spec),
#[cfg(target_os = "linux")]
SandboxType::LinuxBwrap => Self::prepare_bwrap(spec),
#[cfg(target_os = "windows")]
SandboxType::Windows => self.prepare_windows(spec),
}
}
fn prepare_unsandboxed(spec: &CommandSpec) -> ExecEnv {
let mut command = vec![spec.program.clone()];
command.extend(spec.args.clone());
ExecEnv {
command,
cwd: spec.cwd.clone(),
env: spec.env.clone(),
timeout: spec.timeout,
sandbox_type: SandboxType::None,
policy: spec.sandbox_policy.clone(),
enforced: false,
#[cfg(target_os = "windows")]
windows_plan: None,
}
}
#[cfg(target_os = "macos")]
fn prepare_seatbelt(spec: &CommandSpec) -> ExecEnv {
let mut original_command = vec![spec.program.clone()];
original_command.extend(spec.args.clone());
let seatbelt_args =
seatbelt::create_seatbelt_args(original_command, &spec.sandbox_policy, &spec.cwd);
let mut command = vec![seatbelt::SANDBOX_EXEC_PATH.to_string()];
command.extend(seatbelt_args);
let mut env = spec.env.clone();
env.insert("DEEPSEEK_SANDBOX".to_string(), "seatbelt".to_string());
ExecEnv {
command,
cwd: spec.cwd.clone(),
env,
timeout: spec.timeout,
sandbox_type: SandboxType::MacosSeatbelt,
policy: spec.sandbox_policy.clone(),
enforced: true,
}
}
#[cfg(target_os = "linux")]
fn prepare_landlock(spec: &CommandSpec) -> ExecEnv {
let mut command = vec![spec.program.clone()];
command.extend(spec.args.clone());
let mut env = spec.env.clone();
env.insert("DEEPSEEK_SANDBOX".to_string(), "landlock".to_string());
let mut exec = ExecEnv {
command,
cwd: spec.cwd.clone(),
env,
timeout: spec.timeout,
sandbox_type: SandboxType::LinuxLandlock,
policy: spec.sandbox_policy.clone(),
enforced: false,
};
mark_sandbox_policy_unenforced(&mut exec);
exec
}
#[cfg(target_os = "linux")]
fn prepare_bwrap(spec: &CommandSpec) -> ExecEnv {
let command =
bwrap::build_bwrap_command(&spec.sandbox_policy, &spec.cwd, &spec.program, &spec.args);
let mut env = spec.env.clone();
env.insert("DEEPSEEK_SANDBOX".to_string(), "bwrap".to_string());
ExecEnv {
command,
cwd: spec.cwd.clone(),
env,
timeout: spec.timeout,
sandbox_type: SandboxType::LinuxBwrap,
policy: spec.sandbox_policy.clone(),
enforced: true,
}
}
#[cfg(target_os = "windows")]
fn prepare_windows(&self, spec: &CommandSpec) -> ExecEnv {
let mut command = vec![spec.program.clone()];
command.extend(spec.args.clone());
let writable = spec.sandbox_policy.get_writable_roots(&spec.cwd);
let mut roots: Vec<PathBuf> = writable.iter().map(|entry| entry.root.clone()).collect();
roots = zagens_windows_sandbox::filter_ssh_config_dependency_roots(&roots);
let mut protected = Vec::new();
for entry in &writable {
protected.extend(entry.read_only_subpaths.clone());
}
for root in &roots {
protected.extend(zagens_windows_sandbox::protected_subdirs_for_root(root));
}
let plan_mode = Self::resolve_windows_plan_mode(self.windows_sandbox_mode);
match zagens_windows_sandbox::plan_exec(zagens_windows_sandbox::PlanInput {
program: spec.program.clone(),
args: spec.args.clone(),
cwd: spec.cwd.clone(),
env: spec.env.clone(),
writable_roots: roots,
protected_write_paths: protected,
network_allowed: spec.sandbox_policy.has_network_access(),
mode: plan_mode,
private_desktop: self.windows_private_desktop,
tty: false,
}) {
Ok(plan) => ExecEnv {
command: plan.argv.clone(),
cwd: plan.cwd.clone(),
env: plan.env.clone(),
timeout: spec.timeout,
sandbox_type: SandboxType::Windows,
policy: spec.sandbox_policy.clone(),
enforced: true,
windows_plan: Some(plan),
},
Err(err) => {
tracing::warn!(
target: "sandbox",
"Windows sandbox plan failed; falling back to degraded mode: {err:#}"
);
let mut env = spec.env.clone();
env.insert(
"DEEPSEEK_SANDBOX".to_string(),
"windows:unelevated".to_string(),
);
let mut exec = ExecEnv {
command,
cwd: spec.cwd.clone(),
env,
timeout: spec.timeout,
sandbox_type: SandboxType::Windows,
policy: spec.sandbox_policy.clone(),
enforced: false,
windows_plan: None,
};
mark_sandbox_policy_unenforced(&mut exec);
exec
}
}
}
#[cfg(target_os = "windows")]
fn resolve_windows_plan_mode(
configured: WindowsSandboxModeToml,
) -> zagens_windows_sandbox::WindowsSandboxMode {
use zagens_windows_sandbox::WindowsSandboxMode;
match crate::config::effective_windows_sandbox_execution_mode(configured) {
WindowsSandboxModeToml::Unelevated => WindowsSandboxMode::Unelevated,
WindowsSandboxModeToml::Elevated => WindowsSandboxMode::Elevated,
}
}
pub(crate) fn noop_sandbox_warning(sandbox_type: SandboxType) -> Option<&'static str> {
match sandbox_type {
#[cfg(target_os = "linux")]
SandboxType::LinuxLandlock => Some(
"Linux Landlock sandbox is not enforced yet; command runs with full user privileges.",
),
#[cfg(target_os = "windows")]
SandboxType::Windows => {
Some("Windows sandbox is not enforced yet; command runs with full user privileges.")
}
_ => None,
}
}
pub fn was_denied(sandbox_type: SandboxType, exit_code: i32, stderr: &str) -> bool {
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let _ = (exit_code, stderr);
match sandbox_type {
SandboxType::None => false,
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => seatbelt::detect_denial(exit_code, stderr),
#[cfg(target_os = "linux")]
SandboxType::LinuxLandlock => landlock::detect_denial(exit_code, stderr),
#[cfg(target_os = "linux")]
SandboxType::LinuxBwrap => bwrap::detect_denial(exit_code, stderr),
#[cfg(target_os = "windows")]
SandboxType::Windows => windows::detect_denial(exit_code, stderr),
}
}
pub fn denial_message(sandbox_type: SandboxType, stderr: &str) -> String {
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let _ = stderr;
match sandbox_type {
SandboxType::None => "Command failed (no sandbox)".to_string(),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => {
if stderr.contains("file-write") {
"Sandbox blocked write access. The command tried to write to a protected location.".to_string()
} else if stderr.contains("network") {
"Sandbox blocked network access. Enable network_access in sandbox policy if needed.".to_string()
} else {
format!(
"Sandbox blocked operation: {}",
stderr.lines().next().unwrap_or("unknown")
)
}
}
#[cfg(target_os = "linux")]
SandboxType::LinuxLandlock => {
if stderr.contains("Permission denied") {
"Landlock blocked access. The command tried to access a restricted path."
.to_string()
} else {
format!(
"Landlock blocked operation: {}",
stderr.lines().next().unwrap_or("unknown")
)
}
}
#[cfg(target_os = "linux")]
SandboxType::LinuxBwrap => {
if stderr.contains("Read-only file system") {
"Sandbox blocked write access (bwrap read-only root). The command tried to write outside the policy's writable roots.".to_string()
} else if stderr.contains("Network is unreachable")
|| stderr.contains("Temporary failure in name resolution")
{
"Sandbox blocked network access (bwrap). Enable network_access in sandbox policy if needed.".to_string()
} else {
format!(
"Sandbox (bwrap) blocked operation: {}",
stderr.lines().next().unwrap_or("unknown")
)
}
}
#[cfg(target_os = "windows")]
SandboxType::Windows => {
if stderr.contains("Access is denied") {
"Windows sandbox blocked access. The command lacked required privileges."
.to_string()
} else if stderr.contains("network") {
"Windows sandbox blocked network access. Enable network_access in policy if needed."
.to_string()
} else {
format!(
"Windows sandbox blocked operation: {}",
stderr.lines().next().unwrap_or("unknown")
)
}
}
}
}
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn mark_sandbox_policy_unenforced(exec: &mut ExecEnv) {
exec.env
.insert("DEEPSEEK_SANDBOX_UNENFORCED".to_string(), "1".to_string());
tracing::warn!(
target: "sandbox",
"OS sandbox isolation is NOT enforced on this platform; command runs with full user privileges. {}",
policy_degraded_mode_notice().unwrap_or("")
);
}
impl ExecEnv {
#[must_use]
pub fn sandbox_enforcement_warning(&self) -> Option<&'static str> {
if self.enforced {
return None;
}
SandboxManager::noop_sandbox_warning(self.sandbox_type)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn expected_shell_command(command: &str) -> Vec<String> {
#[cfg(windows)]
{
let (shell, arg) = windows_shell();
vec![shell.to_string(), arg.to_string(), command.to_string()]
}
#[cfg(not(windows))]
{
vec!["sh".to_string(), "-c".to_string(), command.to_string()]
}
}
#[test]
fn test_command_spec_shell() {
let spec = CommandSpec::shell("echo hello", PathBuf::from("/tmp"), Duration::from_secs(30));
#[cfg(windows)]
{
let (shell, arg) = windows_shell();
assert_eq!(spec.program, shell);
assert_eq!(spec.args, vec![arg, "echo hello"]);
}
#[cfg(not(windows))]
{
assert_eq!(spec.program, "sh");
assert_eq!(spec.args, vec!["-c", "echo hello"]);
}
assert_eq!(spec.display_command(), "echo hello");
}
#[test]
fn test_command_spec_program() {
let spec = CommandSpec::program(
"cargo",
vec!["build".to_string(), "--release".to_string()],
PathBuf::from("/project"),
Duration::from_secs(300),
);
assert_eq!(spec.program, "cargo");
assert_eq!(spec.display_command(), "cargo build --release");
}
#[test]
fn test_command_spec_builder() {
let spec = CommandSpec::shell("test", PathBuf::from("."), Duration::from_secs(10))
.with_policy(SandboxPolicy::ReadOnly)
.with_env_var("FOO", "bar")
.with_justification("Testing");
assert!(matches!(spec.sandbox_policy, SandboxPolicy::ReadOnly));
assert_eq!(spec.env.get("FOO"), Some(&"bar".to_string()));
assert_eq!(spec.justification, Some("Testing".to_string()));
}
#[test]
fn test_sandbox_manager_new() {
let manager = SandboxManager::new();
assert!(manager.sandbox_available.is_none());
}
#[test]
fn test_sandbox_manager_select_sandbox() {
let manager = SandboxManager::new();
let no_sandbox = manager.select_sandbox(&SandboxPolicy::DangerFullAccess);
assert_eq!(no_sandbox, SandboxType::None);
let external = manager.select_sandbox(&SandboxPolicy::ExternalSandbox {
network_access: true,
});
assert_eq!(external, SandboxType::None);
}
#[test]
fn test_prepare_unsandboxed() {
let manager = SandboxManager::new();
let spec = CommandSpec::shell("echo test", PathBuf::from("/tmp"), Duration::from_secs(30))
.with_policy(SandboxPolicy::DangerFullAccess);
let env = manager.prepare(&spec);
assert_eq!(env.sandbox_type, SandboxType::None);
assert_eq!(env.command, expected_shell_command("echo test"));
assert!(!env.is_sandboxed());
}
#[test]
fn test_exec_env_helpers() {
let env = ExecEnv {
command: vec![
"sandbox-exec".to_string(),
"-p".to_string(),
"policy".to_string(),
"--".to_string(),
"echo".to_string(),
"hello".to_string(),
],
cwd: PathBuf::from("/tmp"),
env: HashMap::new(),
timeout: Duration::from_secs(30),
sandbox_type: SandboxType::None,
policy: SandboxPolicy::default(),
enforced: false,
#[cfg(target_os = "windows")]
windows_plan: None,
};
assert_eq!(env.program(), "sandbox-exec");
assert_eq!(env.args().len(), 5);
}
#[test]
fn test_sandbox_type_display() {
assert_eq!(format!("{}", SandboxType::None), "none");
#[cfg(target_os = "macos")]
assert_eq!(format!("{}", SandboxType::MacosSeatbelt), "macos-seatbelt");
}
}