- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use std::path::PathBuf; - 4
use std::process::Command; - 5
use std::time::Duration; - 6
- 7
use vak_core::sandbox_docker::DockerSandbox; - 8
use vak_tools::sandbox::SandboxMode; - 9
- 10
/// Run a command exactly like BashTool does: the sandbox wrapper goes - 11
/// through one more shell layer. - 12
fn run_wrapped(wrapped: &str, _timeout: Duration) -> (bool, String) { - 13
let out = Command::new("sh") - 14
.arg("-c") - 15
.arg(wrapped) - 16
.stdout(std::process::Stdio::piped()) - 17
.stderr(std::process::Stdio::piped()) - 18
.output(); - 19
match out { - 20
Ok(o) => { - 21
let mut text = String::from_utf8_lossy(&o.stdout).into_owned(); - 22
text.push_str(&String::from_utf8_lossy(&o.stderr)); - 23
(o.status.success(), text) - 24
} - 25
Err(e) => (false, format!("spawn failed: {e}")), - 26
} - 27
} - 28
- 29
fn ensure_image(image: &str) { - 30
let have = Command::new("docker") - 31
.args(["image", "inspect", image]) - 32
.stdout(std::process::Stdio::null()) - 33
.stderr(std::process::Stdio::null()) - 34
.status() - 35
.map(|s| s.success()) - 36
.unwrap_or(false); - 37
if !have { - 38
let pulled = Command::new("docker") - 39
.args(["pull", "-q", image]) - 40
.status() - 41
.map(|s| s.success()) - 42
.unwrap_or(false); - 43
assert!(pulled, "docker pull {image} failed"); - 44
} - 45
} - 46
- 47
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 48
async fn docker_backend_executes_mounts_and_denies_network() { - 49
if !DockerSandbox::available() { - 50
println!("skipping: no reachable docker daemon"); - 51
return; - 52
} - 53
let dir = tempfile::tempdir().unwrap(); - 54
let ws: PathBuf = dir.path().to_path_buf(); - 55
let sb = DockerSandbox::new(SandboxMode::WorkspaceWrite, None, &ws); - 56
ensure_image("alpine:3.20"); - 57
- 58
// 1. Execution inside the container. - 59
let (ok, out) = run_wrapped( - 60
&sb.wrap_command("echo container-alive"), - 61
Duration::from_secs(60), - 62
); - 63
assert!(ok, "container exec failed: {out}"); - 64
assert!(out.contains("container-alive"), "{out}"); - 65
- 66
// 2. Writes land in the HOST workspace through the bind mount. - 67
let (ok, out) = run_wrapped( - 68
&sb.wrap_command("echo from-container > mounted.txt"), - 69
Duration::from_secs(60), - 70
); - 71
assert!(ok, "write failed: {out}"); - 72
let written = std::fs::read_to_string(ws.join("mounted.txt")).unwrap(); - 73
assert_eq!(written.trim(), "from-container"); - 74
- 75
// 3. Network is denied (--network none); busybox wget must fail fast. - 76
let (ok, out) = run_wrapped( - 77
&sb.wrap_command( - 78
"wget -q -T 3 -O /dev/null http://example.com >/dev/null 2>&1 && echo NETUP || echo NETBLOCKED", - 79
), - 80
Duration::from_secs(60), - 81
); - 82
assert!(ok); - 83
assert!(out.contains("NETBLOCKED"), "network must be denied: {out}"); - 84
} - 85
- 86
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] - 87
async fn docker_readonly_mode_blocks_workspace_writes() { - 88
if !DockerSandbox::available() { - 89
println!("skipping: no reachable docker daemon"); - 90
return; - 91
} - 92
let dir = tempfile::tempdir().unwrap(); - 93
let ws = dir.path().to_path_buf(); - 94
std::fs::write(ws.join("existing.txt"), "keep").unwrap(); - 95
ensure_image("alpine:3.20"); - 96
- 97
let sb = DockerSandbox::new(SandboxMode::ReadOnly, None, &ws); - 98
let (ok, out) = run_wrapped( - 99
&sb.wrap_command("(echo x > existing.txt && echo WROTE) || echo ROENFORCED"), - 100
Duration::from_secs(60), - 101
); - 102
assert!(ok); - 103
assert!( - 104
out.contains("ROENFORCED"), - 105
"read-only mount must block writes: {out}" - 106
); - 107
assert_eq!( - 108
std::fs::read_to_string(ws.join("existing.txt")).unwrap(), - 109
"keep" - 110
); - 111
} - 112
- 113
#[test] - 114
fn effective_sandbox_name_reports_docker_when_selected() { - 115
// Config-level selection flows through Core without needing a daemon. - 116
let dir = tempfile::tempdir().unwrap(); - 117
let project = dir.path().join(".vak"); - 118
std::fs::create_dir_all(&project).unwrap(); - 119
std::fs::write( - 120
project.join("config.toml"), - 121
"[sandbox]\nbackend = \"docker\"\nimage = \"alpine:3.20\"\n", - 122
) - 123
.unwrap(); - 124
vak_config::paths::isolate_home_for_tests(); - 125
let core = vak_core::Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 126
core.set_permission_mode(vak_config::PermissionMode::WorkspaceWrite); - 127
let name = core.effective_sandbox_name(); - 128
assert!(name.starts_with("docker"), "got {name}"); - 129
} - 130
- 131
#[cfg(target_os = "macos")] - 132
#[test] - 133
fn macos_auto_backend_uses_seatbelt_for_restricted_modes() { - 134
let dir = tempfile::tempdir().unwrap(); - 135
vak_config::paths::isolate_home_for_tests(); - 136
let core = vak_core::Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 137
core.set_permission_mode(vak_config::PermissionMode::WorkspaceWrite); - 138
core.set_sandbox_backend(Some("auto".into())); - 139
assert_eq!(core.effective_sandbox_name(), "seatbelt"); - 140
} - 141
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.