use super::config::SandboxConfig;
use super::error::SandboxErrorKind;
use super::guest::{GuestType, GUEST_BINARIES};
use crate::tools::error::ToolError;
use crate::tools::sandbox::traits::{Sandbox, SandboxExecutionFuture};
use hyperlight_host::sandbox::uninitialized::{GuestBinary, UninitializedSandbox};
use hyperlight_host::sandbox::SandboxConfiguration;
use hyperlight_host::MultiUseSandbox;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
#[derive(Debug, Serialize, Deserialize)]
struct ShellRequest {
command: String,
args: Value,
timeout_secs: u64,
}
#[derive(Debug, Deserialize)]
struct ShellResponse {
exit_code: i32,
stdout: String,
stderr: String,
success: bool,
#[serde(default)]
truncated: bool,
}
pub struct HyperlightSandbox {
inner: Mutex<Option<MultiUseSandbox>>,
destroyed: AtomicBool,
config: SandboxConfig,
}
impl std::fmt::Debug for HyperlightSandbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HyperlightSandbox")
.field("destroyed", &self.destroyed.load(Ordering::SeqCst))
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl HyperlightSandbox {
pub fn new(config: SandboxConfig) -> Result<Self, SandboxErrorKind> {
config.validate()?;
if !hyperlight_host::is_hypervisor_present() {
return Err(SandboxErrorKind::HypervisorNotAvailable);
}
let mut hl_config = SandboxConfiguration::default();
hl_config.set_heap_size(config.memory_limit as u64);
let guest_binary = Self::load_guest_binary(&config)?;
let mut uninit = UninitializedSandbox::new(guest_binary, Some(hl_config)).map_err(|e| {
SandboxErrorKind::CreationFailed {
reason: e.to_string(),
}
})?;
uninit
.register("host_run_command", |request_json: String| -> String {
Self::execute_shell_command(&request_json)
})
.map_err(|e| SandboxErrorKind::CreationFailed {
reason: format!("failed to register host_run_command: {e}"),
})?;
let sandbox = uninit
.evolve()
.map_err(|e| SandboxErrorKind::CreationFailed {
reason: format!("failed to initialize VM: {e}"),
})?;
Ok(Self {
inner: Mutex::new(Some(sandbox)),
destroyed: AtomicBool::new(false),
config,
})
}
pub fn new_with_guest_type(
config: SandboxConfig,
guest_type: GuestType,
) -> Result<Self, SandboxErrorKind> {
config.validate()?;
if !hyperlight_host::is_hypervisor_present() {
return Err(SandboxErrorKind::HypervisorNotAvailable);
}
let mut hl_config = SandboxConfiguration::default();
hl_config.set_heap_size(config.memory_limit as u64);
let guest_binary = GuestBinary::Buffer(guest_type.binary());
let mut uninit = UninitializedSandbox::new(guest_binary, Some(hl_config)).map_err(|e| {
SandboxErrorKind::CreationFailed {
reason: e.to_string(),
}
})?;
Self::register_host_functions(&mut uninit, guest_type)?;
let sandbox = uninit
.evolve()
.map_err(|e| SandboxErrorKind::CreationFailed {
reason: format!("failed to initialize VM for {}: {}", guest_type, e),
})?;
Ok(Self {
inner: Mutex::new(Some(sandbox)),
destroyed: AtomicBool::new(false),
config,
})
}
fn register_host_functions(
uninit: &mut UninitializedSandbox,
guest_type: GuestType,
) -> Result<(), SandboxErrorKind> {
match guest_type {
GuestType::Shell => {
uninit
.register("host_run_command", |request_json: String| -> String {
Self::execute_shell_command(&request_json)
})
.map_err(|e| SandboxErrorKind::CreationFailed {
reason: format!("failed to register host_run_command: {e}"),
})?;
}
GuestType::Http => {
uninit
.register("host_http_request", |request_json: String| -> String {
Self::execute_http_request(&request_json)
})
.map_err(|e| SandboxErrorKind::CreationFailed {
reason: format!("failed to register host_http_request: {e}"),
})?;
}
}
Ok(())
}
fn execute_http_request(request_json: &str) -> String {
#[derive(serde::Deserialize)]
struct HttpRequest {
url: String,
#[serde(default = "default_method")]
method: String,
#[serde(default)]
headers: std::collections::HashMap<String, String>,
body: Option<String>,
#[serde(default = "default_timeout")]
timeout_secs: u64,
}
fn default_method() -> String {
"GET".to_string()
}
fn default_timeout() -> u64 {
30
}
let request: HttpRequest = match serde_json::from_str(request_json) {
Ok(req) => req,
Err(e) => {
return serde_json::json!({
"status": 0,
"headers": {},
"body": "",
"success": false,
"error": format!("Failed to parse request: {}", e)
})
.to_string();
}
};
let client = match reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(request.timeout_secs))
.build()
{
Ok(c) => c,
Err(e) => {
return serde_json::json!({
"status": 0,
"headers": {},
"body": "",
"success": false,
"error": format!("Failed to create HTTP client: {}", e)
})
.to_string();
}
};
let mut req_builder = match request.method.to_uppercase().as_str() {
"GET" => client.get(&request.url),
"POST" => client.post(&request.url),
"PUT" => client.put(&request.url),
"DELETE" => client.delete(&request.url),
"PATCH" => client.patch(&request.url),
"HEAD" => client.head(&request.url),
method => {
return serde_json::json!({
"status": 0,
"headers": {},
"body": "",
"success": false,
"error": format!("Unsupported HTTP method: {}", method)
})
.to_string();
}
};
for (key, value) in &request.headers {
req_builder = req_builder.header(key, value);
}
if let Some(body) = request.body {
req_builder = req_builder.body(body);
}
match req_builder.send() {
Ok(response) => {
let status = response.status().as_u16();
let headers: std::collections::HashMap<String, String> = response
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body = response.text().unwrap_or_default();
serde_json::json!({
"status": status,
"headers": headers,
"body": body,
"success": true,
"error": null
})
.to_string()
}
Err(e) => serde_json::json!({
"status": 0,
"headers": {},
"body": "",
"success": false,
"error": format!("HTTP request failed: {}", e)
})
.to_string(),
}
}
fn load_guest_binary(config: &SandboxConfig) -> Result<GuestBinary<'static>, SandboxErrorKind> {
use super::config::GuestBinarySource;
match &config.guest_binary {
GuestBinarySource::Embedded => {
Ok(GuestBinary::Buffer(GUEST_BINARIES.shell))
}
GuestBinarySource::FromPath(path) => {
let path_str = path.to_string_lossy().to_string();
Ok(GuestBinary::FilePath(path_str))
}
GuestBinarySource::FromBytes(bytes) => {
let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
Ok(GuestBinary::Buffer(leaked))
}
}
}
fn execute_shell_command(request_json: &str) -> String {
let request: ShellRequest = match serde_json::from_str(request_json) {
Ok(req) => req,
Err(e) => {
return serde_json::json!({
"exit_code": 1,
"stdout": "",
"stderr": format!("Failed to parse request: {}", e),
"success": false,
"truncated": false
})
.to_string();
}
};
let output = match Command::new("sh").arg("-c").arg(&request.command).output() {
Ok(output) => output,
Err(e) => {
return serde_json::json!({
"exit_code": 1,
"stdout": "",
"stderr": format!("Failed to execute command: {}", e),
"success": false,
"truncated": false
})
.to_string();
}
};
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(1);
serde_json::json!({
"exit_code": exit_code,
"stdout": stdout,
"stderr": stderr,
"success": output.status.success(),
"truncated": false
})
.to_string()
}
fn execute_sync_internal(&self, code: &str, args: Value) -> Result<Value, ToolError> {
if self.destroyed.load(Ordering::SeqCst) {
return Err(SandboxErrorKind::AlreadyDestroyed.into());
}
let mut guard = self
.inner
.lock()
.map_err(|_| ToolError::sandbox_error("sandbox lock poisoned"))?;
let sandbox = guard
.as_mut()
.ok_or_else(|| SandboxErrorKind::AlreadyDestroyed.into_tool_error())?;
let request = ShellRequest {
command: code.to_string(),
args,
timeout_secs: self.config.timeout.as_secs(),
};
let request_json = serde_json::to_string(&request)
.map_err(|e| ToolError::sandbox_error(format!("failed to serialize request: {e}")))?;
let result: String = sandbox.call("execute_shell", request_json).map_err(|e| {
SandboxErrorKind::GuestCallFailed {
function: "execute_shell".to_string(),
reason: e.to_string(),
}
.into_tool_error()
})?;
let response: ShellResponse = serde_json::from_str(&result).map_err(|e| {
ToolError::sandbox_error(format!("failed to parse guest response: {e}"))
})?;
Ok(serde_json::json!({
"exit_code": response.exit_code,
"stdout": response.stdout,
"stderr": response.stderr,
"success": response.success,
"truncated": response.truncated
}))
}
}
unsafe impl Send for HyperlightSandbox {}
unsafe impl Sync for HyperlightSandbox {}
impl Sandbox for HyperlightSandbox {
fn execute(&self, code: &str, _args: Value) -> SandboxExecutionFuture {
let _timeout = self.config.timeout;
if self.destroyed.load(Ordering::SeqCst) {
return Box::pin(async move { Err(SandboxErrorKind::AlreadyDestroyed.into()) });
}
let code = code.to_string();
Box::pin(async move {
Err(ToolError::sandbox_error(format!(
"direct HyperlightSandbox::execute() is not supported; \
use SandboxPool for managed async execution (code: {}...)",
if code.len() > 20 { &code[..20] } else { &code }
)))
})
}
fn destroy(&mut self) {
self.destroyed.store(true, Ordering::SeqCst);
if let Ok(mut guard) = self.inner.lock() {
*guard = None;
}
tracing::debug!("HyperlightSandbox destroyed");
}
fn is_alive(&self) -> bool {
!self.destroyed.load(Ordering::SeqCst)
}
fn execute_sync(&self, code: &str, args: Value) -> Result<Value, ToolError> {
self.execute_sync_internal(code, args)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sandbox_debug_impl() {
let config = SandboxConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("SandboxConfig"));
}
#[test]
#[ignore = "requires hypervisor"]
fn sandbox_creation_requires_hypervisor() {
let config = SandboxConfig::default();
let result = HyperlightSandbox::new(config);
if !hyperlight_host::is_hypervisor_present() {
assert!(matches!(
result.unwrap_err(),
SandboxErrorKind::HypervisorNotAvailable
));
}
}
#[test]
fn sandbox_config_validation_in_new() {
let config = SandboxConfig::new().with_memory_limit(100);
let result = HyperlightSandbox::new(config);
assert!(result.is_err());
}
}