- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use std::sync::Arc; - 4
- 5
use serde_json::json; - 6
use tempfile::tempdir; - 7
- 8
use vak_tools::bash::BashTool; - 9
use vak_tools::context::ToolContext; - 10
use vak_tools::sandbox::{Sandbox, SandboxMode, Seatbelt}; - 11
use vak_tools::{Tool, ToolOutput}; - 12
- 13
fn ctx_with(cwd: &std::path::Path, mode: SandboxMode) -> ToolContext { - 14
ToolContext { - 15
cwd: cwd.to_path_buf(), - 16
cancel: tokio_util::sync::CancellationToken::new(), - 17
sandbox: Some(Arc::new(Seatbelt::new(mode, cwd))), - 18
sandbox_sink: None, - 19
agent_id: None, - 20
new_documents: Vec::new(), - 21
} - 22
} - 23
- 24
async fn run(ctx: &ToolContext, cmd: &str) -> ToolOutput { - 25
BashTool.execute(&json!({"command": cmd}), ctx).await - 26
} - 27
- 28
#[test] - 29
fn seatbelt_read_only_profile_denies_all_writes() { - 30
let dir = tempdir().unwrap(); - 31
let sb = Seatbelt::new(SandboxMode::ReadOnly, dir.path()); - 32
let p = sb.profile(); - 33
assert!(p.contains("(deny default)")); - 34
assert!(p.contains("(allow file-read* (subpath")); - 35
assert!(!p.contains("(allow file-read*)\n")); - 36
assert!( - 37
!p.contains("file-write*"), - 38
"read-only must grant no write paths" - 39
); - 40
} - 41
- 42
#[test] - 43
fn seatbelt_workspace_write_profile_scopes_to_cwd_and_tmp() { - 44
let dir = tempdir().unwrap(); - 45
let sb = Seatbelt::new(SandboxMode::WorkspaceWrite, dir.path()); - 46
let p = sb.profile(); - 47
let canonical = dir.path().canonicalize().unwrap(); - 48
assert!(p.contains(&format!("(subpath \"{}\")", canonical.display()))); - 49
assert!(p.contains("(subpath \"/private/tmp\")")); - 50
assert!(p.contains("(subpath \"/dev/null\")")); - 51
} - 52
- 53
#[test] - 54
fn wrap_quotes_command_and_embeds_profile() { - 55
let dir = tempdir().unwrap(); - 56
let sb = Seatbelt::new(SandboxMode::ReadOnly, dir.path()); - 57
let wrapped = sb.wrap("echo 'hello world' && ls"); - 58
assert!(wrapped.starts_with("sandbox-exec -p '")); - 59
assert!( - 60
wrapped.ends_with("-- sh -c 'echo '\\''hello world'\\'' && ls'"), - 61
"inner single quotes must be POSIX-escaped, got: {wrapped}" - 62
); - 63
let profile_start = wrapped.find("(version 1)").expect("profile embedded"); - 64
let profile_end = wrapped - 65
.rfind("' -- sh -c") - 66
.expect("command follows profile"); - 67
assert!( - 68
!wrapped[profile_start..profile_end].contains('\''), - 69
"profile must be quote-stripped before embedding" - 70
); - 71
} - 72
- 73
#[test] - 74
fn off_mode_passes_command_through() { - 75
let dir = tempdir().unwrap(); - 76
let sb = Seatbelt::new(SandboxMode::Off, dir.path()); - 77
assert_eq!(sb.wrap("echo hi"), "echo hi"); - 78
} - 79
- 80
#[cfg(target_os = "macos")] - 81
#[tokio::test] - 82
async fn seatbelt_read_only_blocks_file_writes_but_allows_reads() { - 83
let dir = tempdir().unwrap(); - 84
let ctx = ctx_with(dir.path(), SandboxMode::ReadOnly); - 85
- 86
let out = run(&ctx, "cat /etc/hostname >/dev/null; echo read-ok").await; - 87
assert!( - 88
!out.is_error, - 89
"reads must work under read-only sandbox: {}", - 90
out.content - 91
); - 92
- 93
let out = run(&ctx, "echo blocked > ./should-not-exist.txt").await; - 94
assert!(out.is_error, "writes must be denied"); - 95
assert!( - 96
!dir.path().join("should-not-exist.txt").exists(), - 97
"denied write must not have created the file" - 98
); - 99
} - 100
- 101
#[cfg(target_os = "macos")] - 102
#[tokio::test] - 103
async fn seatbelt_workspace_write_allows_inside_cwd() { - 104
let dir = tempdir().unwrap(); - 105
let ctx = ctx_with(dir.path(), SandboxMode::WorkspaceWrite); - 106
- 107
let out = run(&ctx, "echo inside > ./inside.txt && cat ./inside.txt").await; - 108
assert!( - 109
!out.is_error, - 110
"in-workspace writes must succeed: {}", - 111
out.content - 112
); - 113
assert!(out.content.contains("inside")); - 114
assert!(dir.path().join("inside.txt").exists()); - 115
} - 116
- 117
#[cfg(target_os = "macos")] - 118
#[tokio::test] - 119
async fn seatbelt_workspace_write_blocks_outside_paths() { - 120
let dir = tempdir().unwrap(); - 121
let ctx = ctx_with(dir.path(), SandboxMode::WorkspaceWrite); - 122
- 123
let outside = "$HOME/vak-sb-escape-test.txt"; - 124
let out = run(&ctx, &format!("echo escape > {outside}")).await; - 125
assert!(out.is_error, "writes outside the workspace must be denied"); - 126
assert!( - 127
out.content.contains("Operation not permitted"), - 128
"{}", - 129
out.content - 130
); - 131
let home = std::env::var("HOME").unwrap_or_default(); - 132
assert!( - 133
!std::path::Path::new(&home) - 134
.join("vak-sb-escape-test.txt") - 135
.exists(), - 136
"the escape file must not exist" - 137
); - 138
} - 139
- 140
#[cfg(target_os = "macos")] - 141
#[tokio::test] - 142
async fn seatbelt_blocks_home_reads_outside_workspace() { - 143
let dir = tempdir().unwrap(); - 144
let home = std::path::PathBuf::from(std::env::var_os("HOME").unwrap()); - 145
let protected = tempfile::Builder::new() - 146
.prefix("vak-seatbelt-read-") - 147
.tempdir_in(home) - 148
.unwrap(); - 149
std::fs::write(protected.path().join("secret"), "not-visible").unwrap(); - 150
let ctx = ctx_with(dir.path(), SandboxMode::WorkspaceWrite); - 151
- 152
let out = run( - 153
&ctx, - 154
&format!("cat {}", protected.path().join("secret").display()), - 155
) - 156
.await; - 157
assert!(out.is_error); - 158
assert!(!out.content.contains("not-visible")); - 159
} - 160
- 161
// ── DenySandbox runtime behavior ─────────────────────────────────────── - 162
- 163
/// When the sandbox backend is unavailable or the mode refuses to engage, - 164
/// DenySandbox must make BashTool surface an error (exit 126) rather than - 165
/// silently running the command unsandboxed. - 166
#[tokio::test] - 167
async fn deny_sandbox_returns_error_at_runtime() { - 168
let dir = tempdir().unwrap(); - 169
let ctx = ToolContext { - 170
cwd: dir.path().to_path_buf(), - 171
cancel: tokio_util::sync::CancellationToken::new(), - 172
sandbox: Some(std::sync::Arc::new(vak_tools::sandbox::DenySandbox::new( - 173
"unavailable in this configuration", - 174
))), - 175
sandbox_sink: None, - 176
agent_id: None, - 177
new_documents: Vec::new(), - 178
}; - 179
- 180
let out = BashTool - 181
.execute(&serde_json::json!({"command": "echo leaked"}), &ctx) - 182
.await; - 183
assert!(out.is_error, "denied command must report an error"); - 184
assert!( - 185
out.content.contains("126"), - 186
"exit code 126 expected: {out:?}" - 187
); - 188
assert!( - 189
!out.content.contains("leaked"), - 190
"the denied command must never have executed" - 191
); - 192
} - 193
- 194
/// With no sandbox at all (FullAccess), bash writes freely — this is the - 195
/// explicit-trust escape hatch and must keep working. - 196
#[tokio::test] - 197
async fn off_mode_allows_unrestricted_writes() { - 198
let dir = tempdir().unwrap(); - 199
let ctx = ToolContext { - 200
cwd: dir.path().to_path_buf(), - 201
cancel: tokio_util::sync::CancellationToken::new(), - 202
sandbox: None, - 203
sandbox_sink: None, - 204
agent_id: None, - 205
new_documents: Vec::new(), - 206
}; - 207
- 208
let out = run( - 209
&ctx, - 210
"echo unrestricted > ./freedom.txt && cat ./freedom.txt", - 211
) - 212
.await; - 213
assert!(!out.is_error, "off-mode must allow writes: {}", out.content); - 214
assert!(dir.path().join("freedom.txt").exists()); - 215
} - 216
- 217
/// `read_only_variant()` on a WorkspaceWrite Seatbelt must produce a profile - 218
/// that grants no file-write* entitlements — the trait-level contract that - 219
/// turns that want a tighter sandbox inherit this automatically. - 220
#[test] - 221
fn read_only_variant_strips_all_write_entitlements() { - 222
let dir = tempdir().unwrap(); - 223
let sb = Seatbelt::new(SandboxMode::WorkspaceWrite, dir.path()); - 224
let ro = Sandbox::read_only_variant(&sb).expect("read-only variant"); - 225
let wrapped = ro.wrap("true"); - 226
let profile_start = wrapped.find("(version 1)").expect("profile present"); - 227
let profile_end = wrapped - 228
.rfind("' -- sh -c") - 229
.expect("command follows profile"); - 230
let profile = &wrapped[profile_start..profile_end]; - 231
assert!( - 232
!profile.contains("file-write*"), - 233
"read-only variant must not grant writes: {profile}" - 234
); - 235
} - 236
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.