Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions rust/.sandbox-home/.rustup/settings.toml

This file was deleted.

47 changes: 39 additions & 8 deletions rust/crates/runtime/src/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use tokio::time::timeout;

use crate::lane_events::{LaneEvent, ShipMergeMethod, ShipProvenance};
use crate::sandbox::{
build_linux_sandbox_command, resolve_sandbox_status_for_request, FilesystemIsolationMode,
SandboxConfig, SandboxStatus,
build_linux_sandbox_command, get_sandbox_home, get_sandbox_tmp,
resolve_sandbox_status_for_request, FilesystemIsolationMode, SandboxConfig, SandboxStatus,
};
use crate::ConfigLoader;

Expand Down Expand Up @@ -314,8 +314,8 @@ fn prepare_command(
let mut prepared = Command::new("sh");
prepared.arg("-lc").arg(command).current_dir(cwd);
if sandbox_status.filesystem_active {
prepared.env("HOME", cwd.join(".sandbox-home"));
prepared.env("TMPDIR", cwd.join(".sandbox-tmp"));
prepared.env("HOME", get_sandbox_home(cwd));
prepared.env("TMPDIR", get_sandbox_tmp(cwd));
}
prepared
}
Expand All @@ -341,21 +341,22 @@ fn prepare_tokio_command(
let mut prepared = TokioCommand::new("sh");
prepared.arg("-lc").arg(command).current_dir(cwd);
if sandbox_status.filesystem_active {
prepared.env("HOME", cwd.join(".sandbox-home"));
prepared.env("TMPDIR", cwd.join(".sandbox-tmp"));
prepared.env("HOME", get_sandbox_home(cwd));
prepared.env("TMPDIR", get_sandbox_tmp(cwd));
}
prepared
}

fn prepare_sandbox_dirs(cwd: &std::path::Path) {
let _ = std::fs::create_dir_all(cwd.join(".sandbox-home"));
let _ = std::fs::create_dir_all(cwd.join(".sandbox-tmp"));
let _ = std::fs::create_dir_all(get_sandbox_home(cwd));
let _ = std::fs::create_dir_all(get_sandbox_tmp(cwd));
}

#[cfg(test)]
mod tests {
use super::{execute_bash, BashCommandInput};
use crate::sandbox::FilesystemIsolationMode;
use std::env;

#[test]
fn executes_simple_command() {
Expand Down Expand Up @@ -419,6 +420,36 @@ mod tests {
assert_eq!(structured[0]["event"], "test.hung");
assert_eq!(structured[0]["data"]["provenance"], "bash.timeout");
}

#[test]
fn sandbox_dirs_are_created_outside_current_working_directory() {
let _guard = crate::test_env_lock();
let original_cwd = env::current_dir().expect("current dir");
let workspace = tempfile::tempdir().expect("temp workspace");
env::set_current_dir(workspace.path()).expect("set current dir");

let output = execute_bash(BashCommandInput {
command: String::from("printf '%s\\n%s' \"$HOME\" \"$TMPDIR\""),
timeout: Some(1_000),
description: None,
run_in_background: Some(false),
dangerously_disable_sandbox: Some(false),
namespace_restrictions: Some(false),
isolate_network: Some(false),
filesystem_mode: Some(FilesystemIsolationMode::WorkspaceOnly),
allowed_mounts: None,
})
.expect("bash command should execute");

env::set_current_dir(original_cwd).expect("restore current dir");

assert!(!workspace.path().join(".sandbox-home").exists());
assert!(!workspace.path().join(".sandbox-tmp").exists());
let paths: Vec<_> = output.stdout.lines().collect();
assert_eq!(paths.len(), 2);
assert!(!std::path::Path::new(paths[0]).starts_with(workspace.path()));
assert!(!std::path::Path::new(paths[1]).starts_with(workspace.path()));
}
}

/// Maximum output bytes before truncation (16 KiB, matching upstream).
Expand Down
56 changes: 52 additions & 4 deletions rust/crates/runtime/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
Expand Down Expand Up @@ -82,6 +83,12 @@ pub struct LinuxSandboxCommand {
pub env: Vec<(String, String)>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxRuntimePaths {
pub home: PathBuf,
pub tmp: PathBuf,
}

impl SandboxConfig {
#[must_use]
pub fn resolve_request(
Expand Down Expand Up @@ -236,11 +243,13 @@ pub fn build_linux_sandbox_command(
args.push("-lc".to_string());
args.push(command.to_string());

let sandbox_home = cwd.join(".sandbox-home");
let sandbox_tmp = cwd.join(".sandbox-tmp");
let runtime_paths = sandbox_runtime_paths(cwd);
let mut env = vec![
("HOME".to_string(), sandbox_home.display().to_string()),
("TMPDIR".to_string(), sandbox_tmp.display().to_string()),
("HOME".to_string(), runtime_paths.home.display().to_string()),
(
"TMPDIR".to_string(),
runtime_paths.tmp.display().to_string(),
),
(
"CLAWD_SANDBOX_FILESYSTEM_MODE".to_string(),
status.filesystem_mode.as_str().to_string(),
Expand All @@ -261,6 +270,33 @@ pub fn build_linux_sandbox_command(
})
}

#[must_use]
pub fn sandbox_runtime_paths(cwd: &Path) -> SandboxRuntimePaths {
let workspace_key = sandbox_workspace_key(cwd);
let root = env::temp_dir().join("claw-sandbox").join(workspace_key);
SandboxRuntimePaths {
home: root.join("home"),
tmp: root.join("tmp"),
}
}

#[must_use]
pub fn get_sandbox_home(cwd: &Path) -> PathBuf {
sandbox_runtime_paths(cwd).home
}

#[must_use]
pub fn get_sandbox_tmp(cwd: &Path) -> PathBuf {
sandbox_runtime_paths(cwd).tmp
}

fn sandbox_workspace_key(cwd: &Path) -> String {
let mut hasher = Sha256::new();
hasher.update(cwd.to_string_lossy().as_bytes());
let digest = hasher.finalize();
format!("{:x}", digest)[..16].to_string()
}

fn normalize_mounts(mounts: &[String], cwd: &Path) -> Vec<String> {
let cwd = cwd.to_path_buf();
mounts
Expand Down Expand Up @@ -381,4 +417,16 @@ mod tests {
assert!(launcher.args.iter().any(|arg| arg == "--net") == status.network_active);
}
}

#[test]
fn sandbox_runtime_paths_live_outside_workspace() {
let cwd = Path::new("/workspace/project");
let paths = super::sandbox_runtime_paths(cwd);

assert!(!paths.home.starts_with(cwd));
assert!(!paths.tmp.starts_with(cwd));
assert!(paths.home.ends_with("home"));
assert!(paths.tmp.ends_with("tmp"));
assert_eq!(paths.home.parent(), paths.tmp.parent());
}
}