From 73b13df38fa25cad612d6760e1ec7df1a94f42fb Mon Sep 17 00:00:00 2001 From: cv70 <2548604505@qq.com> Date: Tue, 26 May 2026 19:36:28 +0800 Subject: [PATCH] feat: Move sandbox runtime dirs out of workspace --- rust/.sandbox-home/.rustup/settings.toml | 3 -- rust/crates/runtime/src/bash.rs | 47 ++++++++++++++++---- rust/crates/runtime/src/sandbox.rs | 56 ++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 15 deletions(-) delete mode 100644 rust/.sandbox-home/.rustup/settings.toml diff --git a/rust/.sandbox-home/.rustup/settings.toml b/rust/.sandbox-home/.rustup/settings.toml deleted file mode 100644 index e34067a495..0000000000 --- a/rust/.sandbox-home/.rustup/settings.toml +++ /dev/null @@ -1,3 +0,0 @@ -version = "12" - -[overrides] diff --git a/rust/crates/runtime/src/bash.rs b/rust/crates/runtime/src/bash.rs index dddf3ccfbb..a01338c8ba 100644 --- a/rust/crates/runtime/src/bash.rs +++ b/rust/crates/runtime/src/bash.rs @@ -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; @@ -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 } @@ -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() { @@ -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). diff --git a/rust/crates/runtime/src/sandbox.rs b/rust/crates/runtime/src/sandbox.rs index 2df08791e3..132d29e85f 100644 --- a/rust/crates/runtime/src/sandbox.rs +++ b/rust/crates/runtime/src/sandbox.rs @@ -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")] @@ -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( @@ -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(), @@ -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 { let cwd = cwd.to_path_buf(); mounts @@ -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()); + } }