tandem-tools 0.6.2

Tooling and integrations for the Tandem engine
use super::*;
#[cfg(target_os = "linux")]
use std::ffi::OsString;
#[cfg(target_os = "linux")]
use std::sync::{Mutex, OnceLock};

#[cfg(target_os = "linux")]
fn shell_env_lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

#[cfg(target_os = "linux")]
struct EnvRestore {
    name: &'static str,
    previous: Option<OsString>,
}

#[cfg(target_os = "linux")]
impl EnvRestore {
    fn clear(name: &'static str) -> Self {
        let previous = std::env::var_os(name);
        std::env::remove_var(name);
        Self { name, previous }
    }
}

#[cfg(target_os = "linux")]
impl Drop for EnvRestore {
    fn drop(&mut self) {
        if let Some(previous) = self.previous.take() {
            std::env::set_var(self.name, previous);
        } else {
            std::env::remove_var(self.name);
        }
    }
}

#[cfg(target_os = "linux")]
fn bwrap_available() -> bool {
    std::process::Command::new("bwrap")
        .arg("--version")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .is_ok()
}

#[cfg(target_os = "linux")]
fn command_permission_blocked(stderr: &str) -> bool {
    stderr.contains("No permissions")
        || stderr.contains("Operation not permitted")
        || stderr.contains("Creating new namespace failed")
}

#[cfg(target_os = "linux")]
#[test]
fn linux_bwrap_argv_matches_sandbox_policy_snapshot() {
    let workspace = tempfile::tempdir().expect("workspace");
    let root = workspace
        .path()
        .canonicalize()
        .expect("canonical workspace");
    let root_text = root.to_string_lossy().to_string();
    let fake_bwrap = root.join("fake-bwrap");
    let args = json!({
        "__workspace_root": root_text,
        "__effective_cwd": root.to_string_lossy().to_string(),
    });

    let plan = build_bwrap_shell_command_with_bwrap("printf ok", &args, fake_bwrap.clone());
    let ShellCommandPlan::Execute(plan) = plan else {
        panic!("expected executable bwrap plan");
    };
    let argv = plan.args_for_test();
    let path = std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".to_string());

    assert_eq!(
        plan.program_for_test(),
        fake_bwrap.to_string_lossy().to_string()
    );
    assert_eq!(plan.sandbox_mode_for_test(), "bubblewrap");
    assert_eq!(
        argv,
        vec![
            "--die-with-parent",
            "--unshare-all",
            "--new-session",
            "--dev",
            "/dev",
            "--tmpfs",
            "/tmp",
            "--ro-bind",
            "/bin",
            "/bin",
            "--ro-bind",
            "/usr",
            "/usr",
            "--ro-bind-try",
            "/lib",
            "/lib",
            "--ro-bind-try",
            "/lib64",
            "/lib64",
            "--ro-bind-try",
            "/etc/alternatives",
            "/etc/alternatives",
            "--bind",
            &root_text,
            &root_text,
            "--chdir",
            &root_text,
            "--setenv",
            "PATH",
            &path,
            "--setenv",
            "TMPDIR",
            "/tmp",
            "--setenv",
            "HOME",
            &root_text,
            "--",
            "/bin/sh",
            "-lc",
            "printf ok",
        ]
    );
    assert!(
        argv.iter().any(|arg| arg == "--unshare-all"),
        "Linux shell sandbox must unshare the network namespace by default"
    );
    assert!(
        !argv.iter().any(|arg| arg == "--share-net"),
        "Linux shell sandbox must not opt back into host network access"
    );
}

#[cfg(target_os = "linux")]
#[tokio::test]
async fn linux_bwrap_sandbox_blocks_outside_writes_and_allows_workspace_writes() {
    let _guard = shell_env_lock().lock().expect("shell env lock");
    let _unsafe_restore = EnvRestore::clear("TANDEM_UNSAFE_UNSANDBOXED_SHELL");
    if !bwrap_available() {
        eprintln!("skipping bwrap sandbox integration: bwrap is not available");
        return;
    }

    let workspace = tempfile::tempdir().expect("workspace");
    let root = workspace
        .path()
        .canonicalize()
        .expect("canonical workspace");
    let command = "\
printf workspace-ok > allowed.txt
if printf denied > /etc/tandem-shell-sandbox-denied 2>/tmp/tandem-denied.err; then
  echo ETC_WRITE_UNEXPECTED
else
  echo ETC_WRITE_DENIED
fi
cat allowed.txt
";

    let result = BashTool
        .execute(json!({
            "command": command,
            "timeout_ms": 10_000,
            "__workspace_root": root.to_string_lossy().to_string(),
            "__effective_cwd": root.to_string_lossy().to_string(),
        }))
        .await
        .expect("bash tool result");
    let stderr = result
        .metadata
        .get("stderr")
        .and_then(Value::as_str)
        .unwrap_or_default();
    if command_permission_blocked(stderr) {
        eprintln!("skipping bwrap sandbox integration: bwrap cannot create namespaces here");
        return;
    }

    assert_eq!(result.metadata["shell_sandbox"], json!("bubblewrap"));
    assert_eq!(result.metadata["exit_code"], json!(0));
    assert!(
        result.output.contains("ETC_WRITE_DENIED"),
        "outside write must fail; output: {}\nstderr: {}",
        result.output,
        stderr
    );
    assert!(
        result.output.contains("workspace-ok"),
        "workspace write must succeed; output: {}",
        result.output
    );
    assert_eq!(
        std::fs::read_to_string(root.join("allowed.txt")).expect("allowed file"),
        "workspace-ok"
    );
}

#[cfg(unix)]
#[test]
fn unavailable_posix_shell_sandbox_fails_closed_without_explicit_opt_out() {
    let blocked = build_unavailable_posix_shell_command("echo ok", false);
    let ShellCommandPlan::Blocked(result) = blocked else {
        panic!("sandbox-unavailable POSIX shells must fail closed by default");
    };
    assert_eq!(
        result.metadata["guardrail_reason"],
        json!("os_shell_sandbox_unavailable")
    );

    let execute = build_unavailable_posix_shell_command("echo ok", true);
    let ShellCommandPlan::Execute(plan) = execute else {
        panic!("explicit unsafe opt-out should build an unsandboxed POSIX shell plan");
    };
    assert_eq!(plan.sandbox_mode_for_test(), "unsafe_unsandboxed");
}

#[test]
fn windows_shell_translation_and_rejection_matrix_is_stable() {
    let cases = [
        ("ls -la", Some("Get-ChildItem -Force"), None),
        (
            "find src -type f -name \"*.rs\"",
            Some("Get-ChildItem -Path 'src' -Recurse -File -Filter '*.rs'"),
            None,
        ),
        (
            "sed -n '1,5p' README.md",
            None,
            Some("unix_command_untranslatable"),
        ),
        (
            "bash -lc 'echo hi'",
            None,
            Some("unix_command_untranslatable"),
        ),
        ("cat README.md", None, None),
    ];

    for (raw, expected_translation, expected_guardrail) in cases {
        assert_eq!(
            translate_windows_shell_command(raw).as_deref(),
            expected_translation,
            "translation for `{raw}`"
        );
        assert_eq!(
            windows_guardrail_reason(raw),
            expected_guardrail,
            "guardrail reason for `{raw}`"
        );
    }
}