tkr-sandbox 0.1.0

Sandboxing primitives (landlock/macOS sandbox) for the tkr CLI proxy
Documentation
use crate::error::SandboxError;
use crate::policy::SandboxPolicy;
use std::process::Command;

#[derive(Debug, Clone)]
pub struct SandboxOutput {
    pub stdout: Vec<u8>,
    pub stderr: Vec<u8>,
    pub exit: i32,
}

pub fn run_sandboxed(
    command: &str,
    args: &[&str],
    policy: &SandboxPolicy,
) -> Result<SandboxOutput, SandboxError> {
    if let Err(e) = policy.validate() {
        return Err(SandboxError::PolicyViolation(e));
    }
    if policy.disabled {
        return run_unsandboxed(command, args);
    }
    #[cfg(target_os = "linux")]
    {
        crate::linux::run(command, args, policy)
    }
    #[cfg(target_os = "macos")]
    {
        crate::macos::run(command, args, policy)
    }
    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    {
        let _ = (command, args);
        Err(SandboxError::Unsupported)
    }
}

fn run_unsandboxed(command: &str, args: &[&str]) -> Result<SandboxOutput, SandboxError> {
    let out = Command::new(command).args(args).output()?;
    Ok(SandboxOutput {
        stdout: out.stdout,
        stderr: out.stderr,
        exit: out.status.code().unwrap_or(-1),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_relative_paths_in_policy() {
        let p = SandboxPolicy::builder().allow_read("rel").build();
        let r = run_sandboxed("/bin/true", &[], &p);
        assert!(matches!(r, Err(SandboxError::PolicyViolation(_))));
    }

    #[test]
    fn disabled_policy_runs_unsandboxed() {
        let mut p = SandboxPolicy::deny_all();
        p.disabled = true;
        let r = run_sandboxed("/bin/echo", &["hi"], &p).unwrap();
        assert_eq!(r.exit, 0);
        assert!(String::from_utf8_lossy(&r.stdout).contains("hi"));
    }
}