use super::*;
use crate::sandbox::{EchoSandbox, FaultSandbox};
use kernel::Address;
mod container;
#[cfg(target_os = "macos")]
#[path = "native_macos/production_tests.rs"]
mod native_macos;
mod shell;
mod yielding;
fn call(arm: Value) -> ToolCall {
let mut args = Map::new();
args.insert("arm".to_owned(), arm);
ToolCall {
id: "c1".to_owned(),
name: ToolName::parse("exec").unwrap(),
args: Payload::new(args).unwrap(),
}
}
fn call_at(arm: Value, placement: &str) -> ToolCall {
let mut args = Map::new();
args.insert("arm".to_owned(), arm);
args.insert("where".to_owned(), Value::String(placement.to_owned()));
ToolCall {
id: "c1".to_owned(),
name: ToolName::parse("exec").unwrap(),
args: Payload::new(args).unwrap(),
}
}
fn setup(workdir: &std::path::Path, python: Option<PathBuf>, shell: Option<PathBuf>) -> ExecSetup {
ExecSetup {
workdir: workdir.to_path_buf(),
mounts: Vec::new(),
python_wasm: python,
shell: shell.map_or(Shell::Absent, |program| Shell::Found {
program,
interpreter: kernel::Interpreter::System,
}),
fuel: Fuel(1_000_000),
env_passthrough: Vec::new(),
domain: Address::parse("work").unwrap(),
run: kernel::RunId::from_bytes([1; 16]),
policy: crate::PolicyCell::new(kernel::RunPolicy::of(kernel::Mode::Work)).reader(),
}
}
fn a_tool(
workdir: &std::path::Path,
python: Option<PathBuf>,
sandbox: Box<dyn Sandbox>,
shell: Option<PathBuf>,
) -> ExecTool {
regression_boundary(ExecTool::new(setup(workdir, python, shell), sandbox, patient()).unwrap())
}
fn regression_boundary(tool: ExecTool) -> ExecTool {
#[cfg(windows)]
if std::env::var("SPRAWLING_DISPOSABLE_NATIVE").as_deref() == Ok("1") {
let tool = tool.confined(Confined::with_arm(
Confinement::WindowsJobObject,
Some(std::env::temp_dir()),
));
assert!(tool.meta().disclosure.contains("windows_job_object"));
eprintln!("NATIVE_REGRESSION_ARM=windows_job_object");
return tool;
}
tool
}
fn patient() -> Backlog {
Backlog::with_window(crate::backlog::PollBudget::new(6_000, 20))
}
fn a_name_this_machine_sets() -> (String, String) {
let mut candidates: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for (name, value) in std::env::vars() {
let admissible = !ENV_ALLOWLIST.contains(&name.as_str())
&& kernel::EnvVarName::parse(&name).is_ok()
&& !value.is_empty()
&& value.is_ascii()
&& !value.contains('%')
&& !value.contains('"');
if admissible {
candidates.insert(name, value);
}
}
candidates
.into_iter()
.next()
.expect("every supported host sets at least one ordinary variable")
}
fn reads_the_variable(name: &str) -> (String, Vec<String>) {
if cfg!(windows) {
(
"cmd".to_owned(),
vec!["/C".to_owned(), format!("echo %{name}%")],
)
} else {
(
"sh".to_owned(),
vec!["-c".to_owned(), format!("printf %s \"${name}\"")],
)
}
}
#[test]
fn a_child_sees_the_names_its_building_declared_and_no_others() {
let (name, value) = a_name_this_machine_sets();
let (path, args) = reads_the_variable(&name);
let chamber = tempfile::tempdir().unwrap();
let declared = ExecSetup {
env_passthrough: vec![kernel::EnvVarName::parse(&name).unwrap()],
..setup(chamber.path(), None, None)
};
let tool = regression_boundary(
ExecTool::new(declared, Box::new(EchoSandbox::new()), patient()).unwrap(),
);
let outcome = tool
.invoke(&call(serde_json::json!({
"program": { "path": path.clone(), "args": args.clone() }
})))
.unwrap();
let result = serde_json::to_value(&outcome.result).unwrap();
assert!(
result["stdout"].as_str().unwrap().contains(&value),
"a declared name reaches the child: {result}"
);
let listed: Vec<String> = serde_json::from_value(result["env"].clone()).unwrap();
assert!(
listed.contains(&name),
"the ledger says what it inherited: {result}"
);
let bare_chamber = tempfile::tempdir().unwrap();
let bare = a_tool(
bare_chamber.path(),
None,
Box::new(EchoSandbox::new()),
None,
);
let outcome = bare
.invoke(&call(serde_json::json!({
"program": { "path": path, "args": args }
})))
.unwrap();
let result = serde_json::to_value(&outcome.result).unwrap();
assert!(
!result["stdout"].as_str().unwrap().contains(&value),
"an undeclared name must not reach the child: {result}"
);
let listed: Vec<String> = serde_json::from_value(result["env"].clone()).unwrap();
assert!(!listed.contains(&name), "{result}");
}
#[test]
fn a_missing_component_refuses_and_names_the_alternative() {
let chamber = tempfile::tempdir().unwrap();
let tool = a_tool(chamber.path(), None, Box::new(EchoSandbox::new()), None);
let err = match tool.invoke(&call(
serde_json::json!({ "python": { "code": "print(1)" } }),
)) {
Err(err) => err,
Ok(_) => panic!("a missing component must refuse"),
};
assert_eq!(*err.code(), AxCode::ToolUnavailable);
assert!(
err.recovery().contains("program arm"),
"the refusal carries the alternative"
);
let err = match tool.invoke(&call(serde_json::json!({ "shell": { "text": "ls" } }))) {
Err(err) => err,
Ok(_) => panic!("a missing shell must refuse"),
};
assert_eq!(*err.code(), AxCode::ToolUnavailable);
}
#[test]
fn the_python_arm_runs_in_the_sandbox_and_reports_its_own_exit() {
let chamber = tempfile::tempdir().unwrap();
let mut echo = EchoSandbox::new();
echo.queue_stdout(b"42\n".to_vec());
let tool = a_tool(
chamber.path(),
Some(PathBuf::from("python.wasm")),
Box::new(echo),
None,
);
let outcome = tool
.invoke(&call(
serde_json::json!({ "python": { "code": "print(42)" } }),
))
.unwrap();
let result = serde_json::to_value(&outcome.result).unwrap();
assert_eq!(result["stdout"], "42\n");
assert_eq!(result["exit_code"], 0);
assert_eq!(result["arm"], "python");
}
#[test]
fn exhaustion_and_traps_reach_the_caller_as_themselves() {
let chamber = tempfile::tempdir().unwrap();
let sandbox = FaultSandbox::new(vec![
SandboxExit::FuelExhausted,
SandboxExit::Trap {
message: "unreachable".to_owned(),
},
]);
let tool = a_tool(
chamber.path(),
Some(PathBuf::from("python.wasm")),
Box::new(sandbox),
None,
);
let first = tool
.invoke(&call(
serde_json::json!({ "python": { "code": "while True: pass" } }),
))
.unwrap();
assert_eq!(
serde_json::to_value(&first.result).unwrap()["outcome"],
"fuel_exhausted"
);
let second = tool
.invoke(&call(serde_json::json!({ "python": { "code": "boom" } })))
.unwrap();
let value = serde_json::to_value(&second.result).unwrap();
assert_eq!(value["outcome"], "trap");
assert_eq!(value["detail"], "unreachable");
}
#[test]
fn an_unrecognised_arm_is_refused_not_guessed() {
let chamber = tempfile::tempdir().unwrap();
let tool = a_tool(chamber.path(), None, Box::new(EchoSandbox::new()), None);
let err = match tool.invoke(&call(serde_json::json!({ "bash": { "text": "ls" } }))) {
Err(err) => err,
Ok(_) => panic!("an unknown arm must refuse"),
};
assert_eq!(*err.code(), AxCode::InvalidArgs);
}
#[test]
fn the_program_arm_runs_a_real_child_with_a_scrubbed_environment() {
let chamber = tempfile::tempdir().unwrap();
let tool = a_tool(chamber.path(), None, Box::new(EchoSandbox::new()), None);
let (path, args) = if cfg!(windows) {
("cmd", vec!["/C".to_owned(), "exit 3".to_owned()])
} else {
("sh", vec!["-c".to_owned(), "exit 3".to_owned()])
};
let outcome = tool
.invoke(&call(serde_json::json!({
"program": { "path": path, "args": args }
})))
.unwrap();
let result = serde_json::to_value(&outcome.result).unwrap();
assert_eq!(result["exit_code"], 3, "{result}");
assert_eq!(result["arm"], "program");
}
fn writes_beside_itself(name: &str) -> (String, Vec<String>) {
if cfg!(windows) {
(
"cmd".to_owned(),
vec![
"/C".to_owned(),
format!("echo written> {name} && type {name}"),
],
)
} else {
(
"sh".to_owned(),
vec![
"-c".to_owned(),
format!("echo written > {name}; cat {name}"),
],
)
}
}
#[test]
fn a_sandboxed_command_writes_in_a_copy_and_leaves_the_source_tree_alone() {
let chamber = tempfile::tempdir().unwrap();
std::fs::write(chamber.path().join("kept.txt"), b"the person's own\n").unwrap();
let tool = a_tool(chamber.path(), None, Box::new(EchoSandbox::new()), None);
let (path, args) = writes_beside_itself("made.txt");
let outcome = tool
.invoke(&call(serde_json::json!({
"program": { "path": path, "args": args }
})))
.unwrap();
let result = serde_json::to_value(&outcome.result).unwrap();
assert_eq!(result["exit_code"], 0, "{result}");
assert!(
result["stdout"].as_str().unwrap().contains("written"),
"the command wrote where it ran: {result}"
);
assert!(
!chamber.path().join("made.txt").exists(),
"a sandboxed write must not land in the tree the person has"
);
assert_eq!(
std::fs::read_to_string(chamber.path().join("kept.txt")).unwrap(),
"the person's own\n",
"the tree it read is the tree the person has"
);
let host = a_tool(chamber.path(), None, Box::new(EchoSandbox::new()), None);
let (path, args) = writes_beside_itself("made.txt");
host.invoke(&call_at(
serde_json::json!({
"program": { "path": path, "args": args }
}),
"host",
))
.unwrap();
assert!(
chamber.path().join("made.txt").exists(),
"the host placement is the one a person decides on, and it is the one that writes"
);
}
#[test]
fn the_python_arm_has_no_sandbox_host_form() {
let chamber = tempfile::tempdir().unwrap();
let tool = a_tool(
chamber.path(),
Some(PathBuf::from("python.wasm")),
Box::new(EchoSandbox::new()),
None,
);
let err = match tool.invoke(&call_at(
serde_json::json!({ "python": { "code": "print(1)" } }),
"host",
)) {
Err(err) => err,
Ok(_) => panic!("the wasip1 guest has no host interpreter to run it"),
};
assert_eq!(*err.code(), AxCode::SandboxDenied);
assert!(
err.recovery().contains("arm: program"),
"{}",
err.recovery()
);
}