- 1001
&lineage_context(lineage), - 1002
vak_ooxml::Limits::default(), - 1003
target_of(out), - 1004
&draft, - 1005
) - 1006
.map_err(|error| format!("nothing was written: {error}"))?; - 1007
crate::office_apply::write_atomically(out, &applied.bytes)?; - 1008
serde_json::to_string(&serde_json::json!({ - 1009
"results": applied.results, - 1010
"sha256": crate::office_apply::sha256_hex(&applied.bytes), - 1011
})) - 1012
.map_err(|error| error.to_string()) - 1013
} - 1014
- 1015
/// Spawns one worker for `task` under a network-denied sandbox that can - 1016
/// read `roots` and write nothing, or only `roots[0]` when `writes_first`, - 1017
/// and returns its answer's content. Every failure, including the deadline, - 1018
/// is an error; nothing is guessed. - 1019
async fn run_task( - 1020
worker_exe: &Path, - 1021
cwd: &Path, - 1022
roots: &[&Path], - 1023
writes_first: bool, - 1024
task: WorkerTask, - 1025
) -> Result<String, String> { - 1026
if !worker_exe.is_file() { - 1027
return Err(format!( - 1028
"worker executable not found: {}", - 1029
worker_exe.display() - 1030
)); - 1031
} - 1032
let request = WorkerRequest { - 1033
version: PROTOCOL_VERSION, - 1034
task, - 1035
}; - 1036
let payload = - 1037
serde_json::to_vec(&request).map_err(|error| format!("request encode failed: {error}"))?; - 1038
let worker_command = format!( - 1039
"{} {}", - 1040
shell_quote(&worker_exe.display().to_string()), - 1041
WORKER_SUBCOMMAND - 1042
); - 1043
let effective = match verification_sandbox(roots, writes_first, worker_exe) { - 1044
Some(sandbox) => sandbox.wrap(&worker_command), - 1045
None => worker_command, - 1046
}; - 1047
let mut command = tokio::process::Command::new(crate::bash::POSIX_SHELL); - 1048
command - 1049
.arg("-c") - 1050
.arg(effective) - 1051
.current_dir(cwd) - 1052
.stdin(Stdio::piped()) - 1053
.stdout(Stdio::piped()) - 1054
.stderr(Stdio::piped()) - 1055
.kill_on_drop(true); - 1056
crate::bash::scrub_environment(&mut command); - 1057
command.env(WORKER_ENV, "1"); - 1058
crate::bash::isolate_process_group(&mut command); - 1059
let mut child = command - 1060
.spawn() - 1061
.map_err(|error| format!("worker spawn failed: {error}"))?; - 1062
let pid = child.id(); - 1063
let Some(mut stdin) = child.stdin.take() else { - 1064
crate::bash::kill_process_group(&pid); - 1065
return Err("worker has no stdin".into()); - 1066
}; - 1067
if let Err(error) = stdin.write_all(&payload).await { - 1068
crate::bash::kill_process_group(&pid); - 1069
return Err(format!("worker request failed: {error}")); - 1070
} - 1071
drop(stdin); - 1072
let (Some(stdout), Some(stderr)) = (child.stdout.take(), child.stderr.take()) else { - 1073
crate::bash::kill_process_group(&pid); - 1074
return Err("worker has no output pipes".into()); - 1075
}; - 1076
let stdout_reader = tokio::spawn(async move { - 1077
let mut bytes = Vec::new(); - 1078
let _ = stdout - 1079
.take(MAX_PROTOCOL_BYTES + 1) - 1080
.read_to_end(&mut bytes) - 1081
.await; - 1082
bytes - 1083
}); - 1084
let stderr_reader = tokio::spawn(async move { - 1085
let mut bytes = Vec::new(); - 1086
let _ = stderr.take(64 * 1024).read_to_end(&mut bytes).await; - 1087
bytes - 1088
}); - 1089
let status = match tokio::time::timeout(VERIFY_DEADLINE, child.wait()).await { - 1090
Ok(Ok(status)) => status, - 1091
Ok(Err(error)) => return Err(format!("worker wait failed: {error}")), - 1092
Err(_) => { - 1093
crate::bash::kill_process_group(&pid); - 1094
let _ = child.wait().await; - 1095
return Err(format!( - 1096
"worker exceeded the {}s deadline", - 1097
VERIFY_DEADLINE.as_secs() - 1098
)); - 1099
} - 1100
}; - 1101
let stdout = stdout_reader.await.unwrap_or_default(); - 1102
let stderr = stderr_reader.await.unwrap_or_default(); - 1103
if !status.success() { - 1104
return Err(format!( - 1105
"worker exited with {}: {}", - 1106
status.code().unwrap_or(-1), - 1107
String::from_utf8_lossy(&stderr).trim() - 1108
)); - 1109
} - 1110
if stdout.len() as u64 > MAX_PROTOCOL_BYTES { - 1111
return Err("worker output exceeded the protocol limit".into()); - 1112
} - 1113
let response: WorkerResponse = serde_json::from_slice(&stdout) - 1114
.map_err(|error| format!("worker returned invalid protocol: {error}"))?; - 1115
if response.version != PROTOCOL_VERSION || response.is_error { - 1116
return Err(format!("worker refused the request: {}", response.content)); - 1117
} - 1118
Ok(response.content) - 1119
} - 1120
- 1121
/// Network-denied, able to read `roots` and the worker executable, and to - 1122
/// write `roots[0]` only when `writes_first`. `None` where no OS backend - 1123
/// exists; the in-code bounds of each reader then stand alone. - 1124
fn verification_sandbox( - 1125
roots: &[&Path], - 1126
writes_first: bool, - 1127
worker_exe: &Path, - 1128
) -> Option<Arc<dyn crate::sandbox::Sandbox>> { - 1129
let (first, rest) = roots.split_first()?; - 1130
let mut extra: Vec<PathBuf> = rest - 1131
.iter() - 1132
.filter_map(|root| root.canonicalize().ok()) - 1133
.collect(); - 1134
extra.extend(worker_exe.parent().map(Path::to_path_buf)); - 1135
let mode = if writes_first { - 1136
crate::sandbox::SandboxMode::WorkspaceWrite - 1137
} else { - 1138
crate::sandbox::SandboxMode::ReadOnly - 1139
}; - 1140
#[cfg(target_os = "macos")] - 1141
{ - 1142
let mut sandbox = crate::sandbox::Seatbelt::task_copy(mode, first); - 1143
sandbox.read_paths.extend(extra); - 1144
Some(Arc::new(sandbox)) - 1145
} - 1146
#[cfg(target_os = "linux")] - 1147
{ - 1148
let mut sandbox = crate::landlock::Landlock::task_copy(mode, first); - 1149
sandbox.read_paths.extend(extra); - 1150
Some(Arc::new(sandbox)) - 1151
} - 1152
#[cfg(not(any(target_os = "macos", target_os = "linux")))] - 1153
{ - 1154
let _ = (first, extra, mode); - 1155
None - 1156
} - 1157
} - 1158
- 1159
fn shell_quote(value: &str) -> String { - 1160
let mut out = String::with_capacity(value.len() + 2); - 1161
out.push('\''); - 1162
for character in value.chars() { - 1163
if character == '\'' { - 1164
out.push_str("'\\''"); - 1165
} else { - 1166
out.push(character); - 1167
} - 1168
} - 1169
out.push('\''); - 1170
out - 1171
} - 1172
- 1173
/// Decides how a sandbox wrapper applies to a brokered tool invocation. - 1174
/// - 1175
/// Two strategies: - 1176
/// - **`WorkerProcess` target** (Seatbelt/Landlock): the entire worker - 1177
/// process is wrapped, so the sandbox binary itself is sandboxed at exec. - 1178
/// - **`ToolCommand` target** (Docker): the worker runs on the host and only - 1179
/// the tool's own command is wrapped, so the model-controlled shell lands - 1180
/// inside the container. - 1181
/// - 1182
/// Returns the effective shell command string and mutates `request_args` - 1183
/// in place when the command-level wrapping path is taken. - 1184
fn resolve_worker_command( - 1185
sandbox: Option<&dyn crate::sandbox::Sandbox>, - 1186
tool: &str, - 1187
request_args: &mut serde_json::Value, - 1188
worker_command: &str, - 1189
) -> String { - 1190
match sandbox { - 1191
Some(sb) if sb.target() == SandboxTarget::WorkerProcess => sb.wrap(worker_command), - 1192
Some(sb) => { - 1193
if tool == "bash" - 1194
&& let Some(command) = request_args - 1195
.get("command") - 1196
.and_then(serde_json::Value::as_str) - 1197
{ - 1198
request_args["command"] = serde_json::Value::String(sb.wrap(command)); - 1199
} - 1200
worker_command.to_string() - 1201
} - 1202
None => worker_command.to_string(), - 1203
} - 1204
} - 1205
- 1206
#[cfg(test)] - 1207
mod tests { - 1208
#![allow(clippy::unwrap_used, clippy::expect_used)] - 1209
- 1210
use super::*; - 1211
use crate::sandbox::{Sandbox, SandboxMode, Seatbelt}; - 1212
use serde_json::json; - 1213
- 1214
#[test] - 1215
fn no_sandbox_passes_worker_command_through() { - 1216
let worker_cmd = "/path/to/vak __tool_worker"; - 1217
let mut args = json!({"command": "echo hi"}); - 1218
let effective = resolve_worker_command(None, "bash", &mut args, worker_cmd); - 1219
assert_eq!(effective, worker_cmd); - 1220
// args untouched - 1221
assert_eq!(args["command"], json!("echo hi")); - 1222
} - 1223
- 1224
#[test] - 1225
fn worker_process_target_wraps_the_worker_executable() { - 1226
let dir = std::env::temp_dir(); - 1227
let sb: Arc<dyn Sandbox> = Arc::new(Seatbelt::new(SandboxMode::ReadOnly, &dir)); - 1228
assert_eq!(sb.target(), SandboxTarget::WorkerProcess); - 1229
let worker_cmd = "/vak __tool_worker"; - 1230
let mut args = json!({"command": "echo hi"}); - 1231
let effective = resolve_worker_command(Some(&*sb), "bash", &mut args, worker_cmd); - 1232
// The worker executable itself — not the inner bash command — is - 1233
// wrapped, so the whole broker runs under sandbox-exec. - 1234
assert!( - 1235
effective.starts_with("sandbox-exec -p "), - 1236
"expected sandbox-exec wrapper, got: {effective}" - 1237
); - 1238
assert!(effective.contains("__tool_worker")); - 1239
// The inner bash command must NOT be wrapped — it travels through - 1240
// the worker protocol, not the shell wrapper. - 1241
assert!(!effective.contains("echo hi")); - 1242
} - 1243
- 1244
#[test] - 1245
fn tool_command_target_wraps_bash_command_in_args() { - 1246
// A Docker-style sandbox (ToolCommand target) must wrap the bash - 1247
// command inside the worker request args — the host worker stays - 1248
// a protocol adapter and only the model-controlled shell is sandboxed. - 1249
let sb: Arc<dyn Sandbox> = Arc::new(DockerStub { - 1250
target: SandboxTarget::ToolCommand, - 1251
wrap_fn: |cmd| format!("docker-run-wrapped({})", cmd), - 1252
}); - 1253
assert_eq!(sb.target(), SandboxTarget::ToolCommand); - 1254
- 1255
let worker_cmd = "/vak __tool_worker"; - 1256
- 1257
// bash tool: command gets wrapped in the args - 1258
let mut args = json!({"command": "echo from-bash"}); - 1259
let effective = resolve_worker_command(Some(&*sb), "bash", &mut args, worker_cmd); - 1260
assert_eq!( - 1261
effective, worker_cmd, - 1262
"worker command passes through unwrapped" - 1263
); - 1264
assert_eq!( - 1265
args["command"], - 1266
json!("docker-run-wrapped(echo from-bash)"), - 1267
"bash command must be wrapped in the request args" - 1268
); - 1269
- 1270
// non-bash tool: args untouched, worker command passes through - 1271
let mut args2 = json!({"path": "src/main.rs"}); - 1272
let effective2 = resolve_worker_command(Some(&*sb), "read", &mut args2, worker_cmd); - 1273
assert_eq!(effective2, worker_cmd); - 1274
assert_eq!(args2["path"], json!("src/main.rs")); - 1275
} - 1276
- 1277
#[test] - 1278
fn tool_command_target_does_not_wrap_bash_without_command_arg() { - 1279
let sb: Arc<dyn Sandbox> = Arc::new(DockerStub { - 1280
target: SandboxTarget::ToolCommand, - 1281
wrap_fn: |cmd| format!("wrapped({})", cmd), - 1282
}); - 1283
let worker_cmd = "/vak __tool_worker"; - 1284
let mut args = json!({}); - 1285
let effective = resolve_worker_command(Some(&*sb), "bash", &mut args, worker_cmd); - 1286
assert_eq!(effective, worker_cmd); - 1287
assert_eq!( - 1288
args["command"], - 1289
json!(Value::Null), - 1290
"no command key to wrap" - 1291
); - 1292
} - 1293
- 1294
/// Minimal Sandbox stub that lets us test ToolCommand-target wrapping - 1295
/// without a Docker daemon. - 1296
struct DockerStub { - 1297
target: SandboxTarget, - 1298
wrap_fn: fn(&str) -> String, - 1299
} - 1300
- 1301
impl Sandbox for DockerStub { - 1302
fn name(&self) -> &str { - 1303
"docker-stub" - 1304
} - 1305
fn wrap(&self, command: &str) -> String { - 1306
(self.wrap_fn)(command) - 1307
} - 1308
fn target(&self) -> SandboxTarget { - 1309
self.target - 1310
} - 1311
} - 1312
} - 1313
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.