use std::collections::HashMap;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct SandboxResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub language: String,
pub duration_ms: u64,
}
const TIMEOUT_SECS: u64 = 30;
const MAX_OUTPUT_BYTES: usize = 32_768;
const MAX_CODE_BYTES: usize = 256 * 1024;
pub fn execute(language: &str, code: &str, timeout_secs: Option<u64>) -> SandboxResult {
execute_in(language, code, timeout_secs, None)
}
pub fn execute_in(
language: &str,
code: &str,
timeout_secs: Option<u64>,
cwd: Option<&std::path::Path>,
) -> SandboxResult {
if code.len() > MAX_CODE_BYTES {
return SandboxResult {
stdout: String::new(),
stderr: format!(
"Code exceeds the {MAX_CODE_BYTES}-byte limit ({} bytes). Split it into smaller scripts.",
code.len()
),
exit_code: 1,
language: language.to_string(),
duration_ms: 0,
};
}
let timeout = timeout_secs.unwrap_or(TIMEOUT_SECS);
let start = std::time::Instant::now();
let Some(runtime) = resolve_runtime(language) else {
return SandboxResult {
stdout: String::new(),
stderr: format!(
"Unsupported language: {language}. Supported: javascript, typescript, python, shell, ruby, go, rust, php, perl, r, elixir"
),
exit_code: 1,
language: language.to_string(),
duration_ms: 0,
};
};
let sandbox_level = std::env::var("LEAN_CTX_SANDBOX_LEVEL")
.ok()
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or_else(|| crate::core::config::Config::load().sandbox_level);
if sandbox_level >= 1 && cfg!(target_os = "macos") {
let result = seatbelt_execute(&runtime, code, timeout, cwd);
let duration_ms = start.elapsed().as_millis() as u64;
return match result {
Ok((stdout, stderr, exit_code)) => SandboxResult {
stdout: truncate_output(&stdout),
stderr: truncate_smart(&stderr, 2048),
exit_code,
language: language.to_string(),
duration_ms,
},
Err(e) => SandboxResult {
stdout: String::new(),
stderr: format!("Seatbelt execution error: {e}"),
exit_code: 1,
language: language.to_string(),
duration_ms,
},
};
} else if sandbox_level >= 1 {
#[cfg(target_os = "linux")]
{
let result = landlock_execute(&runtime, code, timeout, cwd);
let duration_ms = start.elapsed().as_millis() as u64;
return match result {
Ok((stdout, stderr, exit_code)) => SandboxResult {
stdout: truncate_output(&stdout),
stderr: truncate_smart(&stderr, 2048),
exit_code,
language: language.to_string(),
duration_ms,
},
Err(e) => SandboxResult {
stdout: String::new(),
stderr: format!("Landlock execution error: {e}"),
exit_code: 1,
language: language.to_string(),
duration_ms,
},
};
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
return sandbox_unavailable(language, start);
}
}
let result = if runtime.needs_temp_file {
execute_with_file(&runtime, code, timeout, cwd)
} else {
execute_with_stdin(&runtime, code, timeout, cwd)
};
let duration_ms = start.elapsed().as_millis() as u64;
match result {
Ok((stdout, stderr, code)) => SandboxResult {
stdout: truncate_output(&stdout),
stderr: truncate_smart(&stderr, 2048),
exit_code: code,
language: language.to_string(),
duration_ms,
},
Err(e) => SandboxResult {
stdout: String::new(),
stderr: format!("Execution error: {e}"),
exit_code: 1,
language: language.to_string(),
duration_ms,
},
}
}
pub fn batch_execute(items: &[(String, String)]) -> Vec<SandboxResult> {
items
.iter()
.map(|(lang, code)| execute(lang, code, None))
.collect()
}
#[must_use]
pub fn is_shell_language(language: &str) -> bool {
matches!(language.to_lowercase().as_str(), "shell" | "bash" | "sh")
}
#[cfg_attr(any(target_os = "macos", target_os = "linux"), allow(dead_code))]
fn sandbox_unavailable(language: &str, start: std::time::Instant) -> SandboxResult {
SandboxResult {
stdout: String::new(),
stderr: "sandbox_level >= 1 is configured but no OS sandbox is available on this \
platform; refusing to run the code unsandboxed. Set sandbox_level = 0 to \
accept unsandboxed execution."
.to_string(),
exit_code: 1,
language: language.to_string(),
duration_ms: start.elapsed().as_millis() as u64,
}
}
struct RuntimeConfig {
command: String,
args: Vec<String>,
needs_temp_file: bool,
file_extension: String,
env: HashMap<String, String>,
}
pub fn canonical_language(language: &str) -> Option<&'static str> {
Some(match language.to_lowercase().as_str() {
"javascript" | "js" | "node" => "javascript",
"typescript" | "ts" => "typescript",
"python" | "py" => "python",
"shell" | "bash" | "sh" => "shell",
"ruby" | "rb" => "ruby",
"go" | "golang" => "go",
"rust" | "rs" => "rust",
"php" => "php",
"perl" | "pl" => "perl",
"r" => "r",
"elixir" | "ex" => "elixir",
_ => return None,
})
}
fn resolve_runtime(language: &str) -> Option<RuntimeConfig> {
match canonical_language(language)? {
"javascript" => Some(RuntimeConfig {
command: find_binary(&["bun", "node"])?,
args: vec!["-e".to_string()],
needs_temp_file: false,
file_extension: "js".to_string(),
env: HashMap::new(),
}),
"typescript" => Some(RuntimeConfig {
command: find_binary(&["bun", "npx"])?,
args: if which_exists("bun") {
vec!["-e".to_string()]
} else {
vec!["tsx".to_string(), "-e".to_string()]
},
needs_temp_file: false,
file_extension: "ts".to_string(),
env: HashMap::new(),
}),
"python" => Some(RuntimeConfig {
command: find_binary(&["python3", "python"])?,
args: vec!["-c".to_string()],
needs_temp_file: false,
file_extension: "py".to_string(),
env: HashMap::from([("PYTHONDONTWRITEBYTECODE".into(), "1".into())]),
}),
lang if is_shell_language(lang) => {
#[cfg(target_os = "windows")]
{
Some(RuntimeConfig {
command: "cmd".to_string(),
args: vec!["/C".to_string()],
needs_temp_file: false,
file_extension: "bat".to_string(),
env: HashMap::new(),
})
}
#[cfg(not(target_os = "windows"))]
{
Some(RuntimeConfig {
command: find_binary(&["bash", "sh"])?,
args: vec!["-c".to_string()],
needs_temp_file: false,
file_extension: "sh".to_string(),
env: HashMap::new(),
})
}
}
"ruby" => Some(RuntimeConfig {
command: find_binary(&["ruby"])?,
args: vec!["-e".to_string()],
needs_temp_file: false,
file_extension: "rb".to_string(),
env: HashMap::new(),
}),
"go" => Some(RuntimeConfig {
command: find_binary(&["go"])?,
args: vec!["run".to_string()],
needs_temp_file: true,
file_extension: "go".to_string(),
env: HashMap::new(),
}),
"rust" => Some(RuntimeConfig {
command: "rustc_script".to_string(),
args: vec![],
needs_temp_file: true,
file_extension: "rs".to_string(),
env: HashMap::new(),
}),
"php" => Some(RuntimeConfig {
command: find_binary(&["php"])?,
args: vec!["-r".to_string()],
needs_temp_file: false,
file_extension: "php".to_string(),
env: HashMap::new(),
}),
"perl" => Some(RuntimeConfig {
command: find_binary(&["perl"])?,
args: vec!["-e".to_string()],
needs_temp_file: false,
file_extension: "pl".to_string(),
env: HashMap::new(),
}),
"r" => Some(RuntimeConfig {
command: find_binary(&["Rscript"])?,
args: vec!["-e".to_string()],
needs_temp_file: false,
file_extension: "R".to_string(),
env: HashMap::new(),
}),
"elixir" => Some(RuntimeConfig {
command: find_binary(&["elixir"])?,
args: vec!["-e".to_string()],
needs_temp_file: false,
file_extension: "exs".to_string(),
env: HashMap::new(),
}),
_ => None,
}
}
fn seatbelt_execute(
runtime: &RuntimeConfig,
code: &str,
timeout: u64,
cwd: Option<&std::path::Path>,
) -> Result<(String, String, i32), String> {
let tmp_dir = std::env::temp_dir().join("lean-ctx-sandbox");
let _ = std::fs::create_dir_all(&tmp_dir);
let env_pairs: Vec<(String, String)> = runtime
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if runtime.needs_temp_file {
let suffix = format!(".{}", runtime.file_extension);
let tmp = tempfile::Builder::new()
.prefix("exec_")
.suffix(&suffix)
.tempfile_in(&tmp_dir)
.map_err(|e| format!("Failed to create temp file: {e}"))?;
let file_path = tmp.into_temp_path();
std::fs::write(&file_path, code).map_err(|e| format!("Failed to write temp file: {e}"))?;
let mut allowed = vec![file_path.to_path_buf()];
if let Some(dir) = cwd {
allowed.push(dir.to_path_buf());
}
let allowed_refs: Vec<&std::path::Path> =
allowed.iter().map(std::path::PathBuf::as_path).collect();
let file_str = file_path.to_string_lossy().to_string();
let mut args: Vec<&str> = runtime
.args
.iter()
.map(std::string::String::as_str)
.collect();
args.push(&file_str);
let result = super::sandbox_seatbelt::execute_sandboxed(
&runtime.command,
&args,
&allowed_refs,
&env_pairs,
timeout,
cwd,
);
let _ = std::fs::remove_file(&file_path);
result
} else {
let mut args: Vec<&str> = runtime
.args
.iter()
.map(std::string::String::as_str)
.collect();
args.push(code);
let allowed: Vec<std::path::PathBuf> =
cwd.map(|d| vec![d.to_path_buf()]).unwrap_or_default();
let allowed_refs: Vec<&std::path::Path> =
allowed.iter().map(std::path::PathBuf::as_path).collect();
super::sandbox_seatbelt::execute_sandboxed(
&runtime.command,
&args,
&allowed_refs,
&env_pairs,
timeout,
cwd,
)
}
}
#[cfg(target_os = "linux")]
fn landlock_execute(
runtime: &RuntimeConfig,
code: &str,
timeout: u64,
cwd: Option<&std::path::Path>,
) -> Result<(String, String, i32), String> {
let tmp_dir = std::env::temp_dir().join("lean-ctx-sandbox");
let _ = std::fs::create_dir_all(&tmp_dir);
let env_pairs: Vec<(String, String)> = runtime
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if runtime.needs_temp_file {
let suffix = format!(".{}", runtime.file_extension);
let tmp = tempfile::Builder::new()
.prefix("exec_")
.suffix(&suffix)
.tempfile_in(&tmp_dir)
.map_err(|e| format!("Failed to create temp file: {e}"))?;
let file_path = tmp.into_temp_path();
std::fs::write(&file_path, code).map_err(|e| format!("Failed to write temp file: {e}"))?;
let mut allowed = vec![file_path.to_path_buf()];
if let Some(dir) = cwd {
allowed.push(dir.to_path_buf());
}
let allowed_refs: Vec<&std::path::Path> =
allowed.iter().map(std::path::PathBuf::as_path).collect();
let file_str = file_path.to_string_lossy().to_string();
let mut args: Vec<&str> = runtime
.args
.iter()
.map(std::string::String::as_str)
.collect();
args.push(&file_str);
let result = super::sandbox_landlock::execute_sandboxed(
&runtime.command,
&args,
&allowed_refs,
&env_pairs,
timeout,
cwd,
);
let _ = std::fs::remove_file(&file_path);
result
} else {
let mut args: Vec<&str> = runtime
.args
.iter()
.map(std::string::String::as_str)
.collect();
args.push(code);
let allowed: Vec<std::path::PathBuf> =
cwd.map(|d| vec![d.to_path_buf()]).unwrap_or_default();
let allowed_refs: Vec<&std::path::Path> =
allowed.iter().map(std::path::PathBuf::as_path).collect();
super::sandbox_landlock::execute_sandboxed(
&runtime.command,
&args,
&allowed_refs,
&env_pairs,
timeout,
cwd,
)
}
}
const SANDBOX_ENV_ALLOWLIST: &[&str] = &[
"PATH",
"HOME",
"USER",
"LANG",
"LC_ALL",
"TERM",
"TMPDIR",
"TMP",
"TEMP",
"SYSTEMROOT",
"WINDIR",
"SystemDrive",
];
fn apply_sandbox_env(cmd: &mut Command, runtime: &RuntimeConfig) {
cmd.env_clear();
for key in SANDBOX_ENV_ALLOWLIST {
if let Ok(val) = std::env::var(key) {
cmd.env(key, val);
}
}
for (k, v) in &runtime.env {
cmd.env(k, v);
}
cmd.env("LEAN_CTX_SANDBOX", "1");
}
fn apply_cwd(cmd: &mut Command, cwd: Option<&std::path::Path>) -> Result<(), String> {
let Some(dir) = cwd else { return Ok(()) };
if !dir.is_dir() {
return Err(format!(
"working directory does not exist: {}",
dir.display()
));
}
cmd.current_dir(dir);
Ok(())
}
fn execute_with_stdin(
runtime: &RuntimeConfig,
code: &str,
timeout: u64,
cwd: Option<&std::path::Path>,
) -> Result<(String, String, i32), String> {
let mut cmd = Command::new(&runtime.command);
apply_cwd(&mut cmd, cwd)?;
for arg in &runtime.args {
cmd.arg(arg);
}
cmd.arg(code);
apply_sandbox_env(&mut cmd, runtime);
run_runtime_command(cmd, timeout)
}
fn run_runtime_command(mut cmd: Command, timeout: u64) -> Result<(String, String, i32), String> {
#[cfg(unix)]
{
use crate::core::protected_execution::{Mode, launch_mode};
match launch_mode()? {
Mode::Brokered(client) => {
let executable = resolve_program_path(&cmd)?;
let executable = executable.to_str().ok_or("runtime path is not UTF-8")?;
let args: Vec<&str> = cmd
.get_args()
.map(|arg| arg.to_str().ok_or("runtime argument is not UTF-8"))
.collect::<Result<_, _>>()?;
let env: Vec<(String, String)> = cmd
.get_envs()
.filter_map(|(key, value)| value.map(|value| (key, value)))
.map(|(key, value)| {
Ok((
key.to_str()
.ok_or("runtime environment key is not UTF-8")?
.to_owned(),
value
.to_str()
.ok_or("runtime environment value is not UTF-8")?
.to_owned(),
))
})
.collect::<Result<_, String>>()?;
return client.session_program(
executable,
&args,
&env,
timeout,
cmd.get_current_dir(),
);
}
Mode::Sandboxed(_) => {
return Err("runtime execution must use the supervised program channel".into());
}
Mode::Direct => {}
}
}
#[cfg(not(unix))]
let _protected = crate::core::protected_store::protected_child_prefix()?;
cmd.stdin(std::process::Stdio::null());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
#[cfg(unix)]
cmd.process_group(0);
let program = cmd.get_program().to_string_lossy().into_owned();
let output = wait_with_timeout(&mut cmd, timeout, |e| {
format!("Failed to spawn {program}: {e}")
})?;
Ok((
crate::shell::decode_output(&output.stdout),
crate::shell::decode_output(&output.stderr),
crate::shell::exit_status::exit_code(output.status),
))
}
#[cfg(unix)]
fn resolve_program_path(cmd: &Command) -> Result<std::path::PathBuf, String> {
use std::os::unix::fs::PermissionsExt;
let program = std::path::Path::new(cmd.get_program());
if program.is_absolute() {
return Ok(program.to_path_buf());
}
let cwd = cmd
.get_current_dir()
.map(std::path::Path::to_path_buf)
.unwrap_or(std::env::current_dir().map_err(|_| "runtime working directory unavailable")?);
if program.components().count() > 1 {
return Ok(cwd.join(program));
}
let path = cmd
.get_envs()
.find(|(key, _)| *key == "PATH")
.and_then(|(_, value)| value.map(std::ffi::OsStr::to_os_string))
.or_else(|| std::env::var_os("PATH"))
.ok_or("runtime PATH unavailable")?;
for directory in std::env::split_paths(&path) {
let directory = if directory.is_absolute() {
directory
} else {
cwd.join(directory)
};
let candidate = directory.join(program);
if candidate
.metadata()
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
{
return Ok(candidate);
}
}
Err("runtime executable unavailable".into())
}
fn execute_with_file(
runtime: &RuntimeConfig,
code: &str,
timeout: u64,
cwd: Option<&std::path::Path>,
) -> Result<(String, String, i32), String> {
let tmp_dir = std::env::temp_dir().join("lean-ctx-sandbox");
let _ = std::fs::create_dir_all(&tmp_dir);
let suffix = format!(".{}", runtime.file_extension);
let tmp = tempfile::Builder::new()
.prefix("exec_")
.suffix(&suffix)
.tempfile_in(&tmp_dir)
.map_err(|e| format!("Failed to create temp file: {e}"))?;
let file_path = tmp.into_temp_path();
std::fs::write(&file_path, code).map_err(|e| format!("Failed to write temp file: {e}"))?;
let result = if runtime.command == "rustc_script" {
execute_rust(&file_path, timeout, cwd)
} else {
let mut cmd = Command::new(&runtime.command);
apply_cwd(&mut cmd, cwd)?;
for arg in &runtime.args {
cmd.arg(arg);
}
cmd.arg(&file_path);
apply_sandbox_env(&mut cmd, runtime);
run_runtime_command(cmd, timeout)
};
let _ = std::fs::remove_file(&file_path);
result
}
fn execute_rust(
source_path: &std::path::Path,
timeout: u64,
cwd: Option<&std::path::Path>,
) -> Result<(String, String, i32), String> {
let binary_path = source_path.with_extension("");
let mut compile_cmd = Command::new("rustc");
compile_cmd.arg(source_path).arg("-o").arg(&binary_path);
compile_cmd.env_clear();
for key in SANDBOX_ENV_ALLOWLIST {
if let Ok(val) = std::env::var(key) {
compile_cmd.env(key, val);
}
}
compile_cmd.env("LEAN_CTX_SANDBOX", "1");
let (_, stderr, code) = run_runtime_command(compile_cmd, timeout)?;
if code != 0 {
let _ = std::fs::remove_file(&binary_path);
return Ok((String::new(), stderr, code));
}
let mut run_cmd = Command::new(&binary_path);
apply_cwd(&mut run_cmd, cwd)?;
run_cmd.env_clear();
for key in SANDBOX_ENV_ALLOWLIST {
if let Ok(val) = std::env::var(key) {
run_cmd.env(key, val);
}
}
run_cmd.env("LEAN_CTX_SANDBOX", "1");
let result = run_runtime_command(run_cmd, timeout);
let _ = std::fs::remove_file(&binary_path);
result
}
#[cfg(unix)]
fn let_go_of_guard(guard: &mut Option<crate::shell::containment::CallGuard>) {
if let Some(guard) = guard.take() {
let ended = guard.finish();
if ended != crate::shell::containment::GuardOutcome::Ended {
tracing::warn!("sandbox run: the end of its process group is {ended:?}");
}
}
}
fn wait_with_timeout(
cmd: &mut Command,
timeout_secs: u64,
spawn_error: impl FnOnce(std::io::Error) -> String,
) -> Result<std::process::Output, String> {
crate::shell::process_tree::ProcessTree::prepare(cmd);
#[cfg(unix)]
let (mut child, mut guard) = {
use crate::shell::containment::Refusal;
match crate::shell::containment::start_guarded_child(cmd) {
Ok(started) => started,
Err(Refusal::NotStarted(error)) => return Err(spawn_error(error)),
Err(Refusal::NotLeader) => {
return Err(
"Execution could not be contained: the process does not lead a group of its own"
.to_string(),
);
}
Err(Refusal::NoGuard(reason)) => {
return Err(format!("Execution could not be contained: {reason}"));
}
}
};
#[cfg(not(unix))]
let mut child = cmd.spawn().map_err(spawn_error)?;
let tree = crate::shell::process_tree::ProcessTree::start(&mut child)?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
loop {
match child.try_wait() {
Ok(Some(_)) => {
tree.release();
#[cfg(unix)]
let_go_of_guard(&mut guard);
return child.wait_with_output().map_err(|e| e.to_string());
}
Ok(None) => {
if std::time::Instant::now() > deadline {
tree.kill(&mut child);
#[cfg(unix)]
let_go_of_guard(&mut guard);
if let Ok(mut output) = child.wait_with_output() {
output.stderr.extend_from_slice(
format!("\n[lean-ctx] Execution timed out after {timeout_secs}s")
.as_bytes(),
);
return Ok(output);
}
return Err(format!("Execution timed out after {timeout_secs}s"));
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => {
tree.kill(&mut child);
#[cfg(unix)]
let_go_of_guard(&mut guard);
let _ = child.wait();
return Err(e.to_string());
}
}
}
}
fn find_binary(candidates: &[&str]) -> Option<String> {
for name in candidates {
if which_exists(name) {
return Some(name.to_string());
}
}
None
}
fn which_exists(name: &str) -> bool {
#[cfg(target_os = "windows")]
let check_cmd = Command::new("where")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
#[cfg(not(target_os = "windows"))]
let check_cmd = Command::new("which")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
check_cmd.is_ok_and(|s| s.success())
}
fn truncate_output(output: &str) -> String {
if output.len() <= MAX_OUTPUT_BYTES {
return output.to_string();
}
crate::core::recovery::with_original_pointer(
"ctx_execute",
"stdout",
output,
truncate_smart(output, MAX_OUTPUT_BYTES),
)
}
fn truncate_smart(output: &str, max_bytes: usize) -> String {
if output.len() <= max_bytes {
return output.to_string();
}
let lines: Vec<&str> = output.lines().collect();
let total_lines = lines.len();
let head_count = (total_lines * 60) / 100;
let tail_count = total_lines - head_count;
let head: Vec<&str> = lines.iter().take(head_count).copied().collect();
let tail: Vec<&str> = lines
.iter()
.skip(total_lines - tail_count)
.copied()
.collect();
let head_text = head.join("\n");
let tail_text = tail.join("\n");
if head_text.len() + tail_text.len() + 100 > max_bytes {
let half = max_bytes / 2;
let h = &output[..output.floor_char_boundary(half.min(output.len()))];
let t_start = output.ceil_char_boundary(output.len().saturating_sub(half));
let t = &output[t_start..];
let skipped = output.len() - h.len() - t.len();
return format!("{h}\n\n... [{skipped} bytes truncated — showing head + tail] ...\n\n{t}");
}
let skipped_lines = total_lines - head_count - tail_count;
let skipped_bytes = output.len() - head_text.len() - tail_text.len();
format!(
"{head_text}\n\n... [{skipped_lines} lines / {skipped_bytes} bytes truncated — showing first {head_count} + last {tail_count} lines] ...\n\n{tail_text}"
)
}
pub fn supported_languages() -> &'static [&'static str] {
&[
"javascript",
"typescript",
"python",
"shell",
"ruby",
"go",
"rust",
"php",
"perl",
"r",
"elixir",
]
}
#[cfg(test)]
pub mod tests {
use super::*;
fn python_available() -> bool {
find_binary(&["python3", "python"]).is_some()
}
#[cfg(target_os = "macos")]
#[test]
fn level_zero_interpreters_and_compilers_require_the_protected_channel() {
let _lock = crate::core::data_dir::test_env_lock();
let isolation = crate::core::data_dir::isolated_data_dir();
let _session =
crate::core::protected_store::pin_synthetic_session(isolation.path()).unwrap();
let runtime = RuntimeConfig {
command: "/bin/sh".into(),
args: vec!["-c".into()],
needs_temp_file: false,
file_extension: "sh".into(),
env: HashMap::new(),
};
let stdin_error = execute_with_stdin(&runtime, "printf bypass", 1, None).unwrap_err();
assert!(
stdin_error.contains("supervised execution channel"),
"{stdin_error}"
);
let file_error = execute_with_file(&runtime, "printf bypass", 1, None).unwrap_err();
assert!(
file_error.contains("supervised execution channel"),
"{file_error}"
);
let source = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
std::fs::write(source.path(), "fn main() {}\n").unwrap();
let rust_error = execute_rust(source.path(), 1, None).unwrap_err();
assert!(
rust_error.contains("supervised execution channel"),
"{rust_error}"
);
assert!(!source.path().with_extension("").exists());
}
#[test]
fn execute_python_hello() {
let _lock = crate::core::data_dir::test_env_lock();
if !python_available() {
return;
}
let result = execute("python", "print('hello sandbox')", None);
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("hello sandbox"));
}
#[test]
#[cfg(not(target_os = "windows"))]
fn execute_in_resolves_relative_paths_against_the_given_directory() {
let _lock = crate::core::data_dir::test_env_lock();
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("only-here.txt"), "x").expect("write marker");
let scoped = execute_in("shell", "cat only-here.txt", None, Some(dir.path()));
assert_eq!(scoped.exit_code, 0, "stderr: {}", scoped.stderr);
assert!(scoped.stdout.contains('x'), "{scoped:?}");
let unscoped = execute_in("shell", "cat only-here.txt", None, None);
assert_ne!(
unscoped.exit_code, 0,
"precondition: the marker is only reachable from the given directory"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn execute_in_reports_a_missing_directory_instead_of_falling_back() {
let _lock = crate::core::data_dir::test_env_lock();
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("does-not-exist");
let result = execute_in("shell", "pwd", None, Some(&missing));
assert_ne!(result.exit_code, 0, "{result:?}");
assert!(
result.stderr.contains("working directory does not exist"),
"{result:?}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn execute_shell_echo() {
let _lock = crate::core::data_dir::test_env_lock();
let result = execute("shell", "echo 'test output'", None);
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("test output"));
}
#[test]
#[cfg(unix)]
fn a_run_ends_what_its_snippet_left_running() {
crate::shell::containment::tests::run_external_fixture(
module_path!(),
"left_running_fixture",
);
}
#[test]
#[ignore = "fixture body of a_run_ends_what_its_snippet_left_running"]
#[cfg(unix)]
fn left_running_fixture() {
let fixture = crate::shell::containment::tests::external_fixture();
let _lock = crate::core::data_dir::test_env_lock();
let result = execute(
"shell",
"sleep 20 >/dev/null 2>&1 &\necho \"LEFT $!\"",
Some(20),
);
assert_eq!(result.exit_code, 0, "{}", result.stderr);
let pid: libc::pid_t = result
.stdout
.lines()
.find_map(|line| line.strip_prefix("LEFT "))
.expect("the snippet names its background child")
.trim()
.parse()
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while unsafe { libc::kill(pid, 0) } == 0 {
assert!(
std::time::Instant::now() < deadline,
"a background child of the snippet outlived the run"
);
std::thread::sleep(std::time::Duration::from_millis(20));
}
fixture.complete();
}
#[test]
#[cfg(unix)]
fn a_run_never_starts_before_its_guard_is_ready() {
crate::shell::containment::tests::run_external_fixture(
module_path!(),
"run_caller_killed_fixture",
);
}
#[test]
#[ignore = "fixture body of a_run_never_starts_before_its_guard_is_ready"]
#[cfg(unix)]
fn run_caller_killed_fixture() {
let fixture = crate::shell::containment::tests::external_fixture();
crate::shell::containment::tests::caller_killed_before_its_guard(
module_path!(),
"run_caller_standin",
);
fixture.complete();
}
#[test]
#[ignore = "stand-in caller of run_caller_killed_fixture"]
#[cfg(unix)]
fn run_caller_standin() {
use std::os::unix::process::CommandExt as _;
let marker = crate::shell::containment::tests::caller_standin_marker();
let mut command = std::process::Command::new("sh");
command
.args([
"-c",
&format!("echo ran > '{}'; sleep 60", marker.display()),
])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.process_group(0);
let _ = super::wait_with_timeout(&mut command, 60, |error| error.to_string());
}
#[test]
fn execute_unsupported_language() {
let result = execute("brainfuck", "++++", None);
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("Unsupported language"));
}
#[test]
fn execute_rejects_oversized_code() {
let huge = "a".repeat(MAX_CODE_BYTES + 1);
let result = execute("python", &huge, None);
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("exceeds the"));
}
#[cfg(windows)]
#[test]
fn sandbox_env_preserves_system_drive() {
let expected =
std::env::var_os("SystemDrive").expect("SystemDrive must be defined on Windows");
let runtime = RuntimeConfig {
command: "cmd".to_string(),
args: Vec::new(),
needs_temp_file: false,
file_extension: "bat".to_string(),
env: HashMap::new(),
};
let mut cmd = Command::new("cmd");
apply_sandbox_env(&mut cmd, &runtime);
let preserved = cmd.get_envs().any(|(key, value)| {
key.to_string_lossy().eq_ignore_ascii_case("SystemDrive")
&& value == Some(expected.as_os_str())
});
assert!(
preserved,
"Windows sandbox child environment must preserve SystemDrive"
);
}
#[test]
fn execute_python_error() {
let _lock = crate::core::data_dir::test_env_lock();
if !python_available() {
return;
}
let result = execute("python", "raise ValueError('test error')", None);
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("ValueError"));
}
#[test]
fn execute_with_timeout() {
let _lock = crate::core::data_dir::test_env_lock();
if !python_available() {
return;
}
let result = execute("python", "import time; time.sleep(60)", Some(1));
assert_ne!(result.exit_code, 0);
assert!(
result.stderr.contains("timed out"),
"stderr should mention timeout: {}",
result.stderr
);
}
#[test]
fn timeout_preserves_partial_output() {
let _lock = crate::core::data_dir::test_env_lock();
if !python_available() {
return;
}
let result = execute(
"python",
"import sys; sys.stdout.write('PARTIAL_1504'); sys.stdout.flush(); import time; time.sleep(60)",
Some(2),
);
assert_ne!(result.exit_code, 0);
assert!(
result.stdout.contains("PARTIAL_1504"),
"partial stdout before timeout must be preserved, got: {:?}",
result.stdout
);
assert!(
result.stderr.contains("timed out"),
"stderr should mention timeout: {}",
result.stderr
);
}
#[test]
fn truncate_preserves_head_and_tail() {
let lines: Vec<String> = (0..100)
.map(|i| format!("line {i}: some content here"))
.collect();
let output = lines.join("\n");
let truncated = truncate_smart(&output, 500);
assert!(truncated.contains("line 0:"));
assert!(truncated.contains("line 99:"));
assert!(truncated.contains("truncated"));
}
#[test]
fn supported_languages_list() {
let langs = supported_languages();
assert!(langs.contains(&"python"));
assert!(langs.contains(&"javascript"));
assert!(langs.contains(&"rust"));
assert_eq!(langs.len(), 11);
}
#[test]
fn sandbox_env_is_set() {
let _lock = crate::core::data_dir::test_env_lock();
if !python_available() {
return;
}
let result = execute(
"python",
"import os; print(os.environ.get('LEAN_CTX_SANDBOX', 'missing'))",
None,
);
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains('1'));
}
#[test]
#[cfg(not(target_os = "windows"))]
fn batch_execute_multiple() {
let _lock = crate::core::data_dir::test_env_lock();
let items = vec![
("python".to_string(), "print(1+1)".to_string()),
("shell".to_string(), "echo hello".to_string()),
];
let results = batch_execute(&items);
assert_eq!(results.len(), 2);
assert!(results[0].stdout.contains('2'));
assert!(results[1].stdout.contains("hello"));
}
}