pub mod docker;
pub mod e2b;
#[cfg(feature = "native-sandbox")]
pub mod native;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use docker::{DockerConfig, DockerRunner};
pub use e2b::E2BSandbox;
#[cfg(feature = "native-sandbox")]
pub use native::{NativeConfig, NativeRunner};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SandboxTier {
None,
Docker,
GVisor,
Firecracker,
E2B,
}
impl SandboxTier {
pub fn enforce_production_guard(&self) -> Result<(), String> {
match self {
SandboxTier::GVisor | SandboxTier::Firecracker => {
return Err(format!(
"SECURITY: {:?} is declared but has no runner implementation; \
use Docker or E2B until this tier ships",
self
));
}
_ => {}
}
if !matches!(self, SandboxTier::None) {
return Ok(());
}
let is_prod = crate::env::is_production().map_err(|e| e.to_string())?;
if is_prod {
let allow = std::env::var("SYMBIONT_ALLOW_UNISOLATED")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !allow {
return Err(
"SECURITY: SandboxTier::None is not permitted in production; \
set SYMBIONT_ALLOW_UNISOLATED=1 to override (not recommended)"
.to_string(),
);
}
tracing::error!(
"SandboxTier::None enabled in production via SYMBIONT_ALLOW_UNISOLATED=1 — \
agents run without host isolation"
);
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub execution_time_ms: u64,
pub success: bool,
#[serde(default)]
pub stdout_truncated: bool,
#[serde(default)]
pub stderr_truncated: bool,
}
impl ExecutionResult {
pub fn success(stdout: String, execution_time_ms: u64) -> Self {
Self {
exit_code: 0,
stdout,
stderr: String::new(),
execution_time_ms,
success: true,
stdout_truncated: false,
stderr_truncated: false,
}
}
pub fn failure(exit_code: i32, stderr: String, execution_time_ms: u64) -> Self {
Self {
exit_code,
stdout: String::new(),
stderr,
execution_time_ms,
success: false,
stdout_truncated: false,
stderr_truncated: false,
}
}
pub fn error(error_message: String) -> Self {
Self {
exit_code: -1,
stdout: String::new(),
stderr: error_message,
execution_time_ms: 0,
success: false,
stdout_truncated: false,
stderr_truncated: false,
}
}
}
#[async_trait]
pub trait SandboxRunner: Send + Sync {
async fn execute(
&self,
code: &str,
env: HashMap<String, String>,
) -> Result<ExecutionResult, anyhow::Error>;
}
#[cfg(test)]
mod tier_guard_tests {
use super::*;
fn scoped_env<F: FnOnce()>(vars: &[(&str, Option<&str>)], f: F) {
let saved: Vec<_> = vars
.iter()
.map(|(k, _)| (k.to_string(), std::env::var(k).ok()))
.collect();
for (k, v) in vars {
match v {
Some(val) => std::env::set_var(k, val),
None => std::env::remove_var(k),
}
}
f();
for (k, prior) in saved {
match prior {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
}
#[test]
#[serial_test::serial(sandbox_tier_env)]
fn non_none_tier_passes_guard() {
assert!(SandboxTier::Docker.enforce_production_guard().is_ok());
assert!(SandboxTier::E2B.enforce_production_guard().is_ok());
}
#[test]
#[serial_test::serial(sandbox_tier_env)]
fn unimplemented_tiers_are_refused() {
assert!(SandboxTier::GVisor.enforce_production_guard().is_err());
assert!(SandboxTier::Firecracker.enforce_production_guard().is_err());
}
#[test]
#[serial_test::serial(sandbox_tier_env)]
fn none_tier_outside_production_passes() {
scoped_env(&[("SYMBIONT_ENV", Some("development"))], || {
assert!(SandboxTier::None.enforce_production_guard().is_ok());
});
}
#[test]
#[serial_test::serial(sandbox_tier_env)]
fn none_tier_in_production_fails_without_override() {
scoped_env(
&[
("SYMBIONT_ENV", Some("production")),
("SYMBIONT_ALLOW_UNISOLATED", None),
],
|| {
let res = SandboxTier::None.enforce_production_guard();
assert!(res.is_err(), "production None must refuse, got {:?}", res);
},
);
}
#[test]
#[serial_test::serial(sandbox_tier_env)]
fn none_tier_in_production_with_override_passes() {
scoped_env(
&[
("SYMBIONT_ENV", Some("production")),
("SYMBIONT_ALLOW_UNISOLATED", Some("1")),
],
|| {
assert!(SandboxTier::None.enforce_production_guard().is_ok());
},
);
}
}