use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use thiserror::Error;
pub mod backend;
pub mod chain_runner;
pub mod env;
pub mod payload_runner;
pub mod shim;
pub mod workspace;
pub use backend::birdcage::BirdcageSandbox;
pub use backend::firecracker::{firecracker_host_supported, FirecrackerSandbox, FirecrackerSpec};
pub use backend::libkrun::{libkrun_host_supported, LibkrunSandbox, LibkrunSpec};
pub use backend::process::ProcessSandbox;
pub use chain_runner::{
ChainResult, ChainRun, ChainRunner, ChainRunnerError, ChainStep, ChainStepCapture,
ChainVerdict, InconclusiveReason,
};
pub use payload_runner::{
HarnessSource, HarnessSpecInput, PayloadRun, PayloadRunner, PayloadRunnerError,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BackendKind {
Process,
Birdcage,
Libkrun,
Firecracker,
Docker,
}
impl BackendKind {
pub fn as_str(&self) -> &'static str {
match self {
BackendKind::Process => "process",
BackendKind::Birdcage => "birdcage",
BackendKind::Libkrun => "libkrun",
BackendKind::Firecracker => "firecracker",
BackendKind::Docker => "docker",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Lane {
Fast,
Chain,
}
impl Lane {
pub fn as_str(&self) -> &'static str {
match self {
Lane::Fast => "fast",
Lane::Chain => "chain",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LaneConcurrency {
pub chain: usize,
pub fast: usize,
}
impl LaneConcurrency {
pub const DEFAULT_CHAIN: usize = 2;
pub const DEFAULT_FAST: usize = 8;
pub const fn defaults() -> Self {
Self { chain: Self::DEFAULT_CHAIN, fast: Self::DEFAULT_FAST }
}
pub fn for_lane(&self, lane: Lane) -> usize {
match lane {
Lane::Chain => self.chain,
Lane::Fast => self.fast,
}
}
}
impl Default for LaneConcurrency {
fn default() -> Self {
Self::defaults()
}
}
pub fn permits_loopback(lane: Lane, backend: BackendKind) -> bool {
!matches!((lane, backend), (Lane::Fast, BackendKind::Birdcage))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendSelection {
pub backend: BackendKind,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendChoice {
Auto,
Pinned(BackendKind),
}
pub fn select_backend(choice: BackendChoice, lane: Lane) -> BackendSelection {
let auto = || auto_select(lane);
match choice {
BackendChoice::Auto => auto(),
BackendChoice::Pinned(kind) => match probe(kind) {
Ok(()) => {
BackendSelection { backend: kind, reason: format!("pinned to {}", kind.as_str()) }
}
Err(err) => {
let auto = auto();
BackendSelection {
backend: auto.backend,
reason: format!(
"pinned {} unavailable ({err}); fell back to {} ({})",
kind.as_str(),
auto.backend.as_str(),
auto.reason
),
}
}
},
}
}
fn auto_select(lane: Lane) -> BackendSelection {
let ladder = auto_ladder(lane);
for kind in ladder {
match probe(*kind) {
Ok(()) => {
return BackendSelection {
backend: *kind,
reason: format!("auto-selected for {} lane", lane.as_str()),
};
}
Err(_) => continue,
}
}
BackendSelection {
backend: BackendKind::Process,
reason: format!("auto-selected fallback for {} lane", lane.as_str()),
}
}
fn auto_ladder(lane: Lane) -> &'static [BackendKind] {
match lane {
Lane::Chain => {
#[cfg(target_os = "macos")]
{
&[
BackendKind::Libkrun,
BackendKind::Docker,
BackendKind::Birdcage,
BackendKind::Process,
]
}
#[cfg(target_os = "linux")]
{
&[
BackendKind::Firecracker,
BackendKind::Libkrun,
BackendKind::Docker,
BackendKind::Birdcage,
BackendKind::Process,
]
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
&[BackendKind::Docker, BackendKind::Process]
}
}
Lane::Fast => {
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
&[BackendKind::Birdcage, BackendKind::Process]
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
&[BackendKind::Process]
}
}
}
}
#[derive(Debug, Clone)]
pub struct SandboxOpts {
pub workspace: PathBuf,
pub argv: Vec<String>,
pub cwd: Option<PathBuf>,
pub env: Vec<(String, String)>,
pub timeout: Duration,
pub allow_loopback: bool,
pub lane: Option<Lane>,
pub allow_read: Vec<PathBuf>,
pub allow_write: Vec<PathBuf>,
pub max_output_bytes: usize,
pub snapshot_from: Option<PathBuf>,
pub capture_files: Vec<PathBuf>,
}
impl SandboxOpts {
pub fn new(workspace: PathBuf, argv: Vec<String>) -> Self {
Self {
workspace,
argv,
cwd: None,
env: Vec::new(),
timeout: Duration::from_secs(30),
allow_loopback: false,
lane: None,
allow_read: Vec::new(),
allow_write: Vec::new(),
max_output_bytes: 1 << 20,
snapshot_from: None,
capture_files: Vec::new(),
}
}
pub fn with_snapshot_from(mut self, src: PathBuf) -> Self {
self.snapshot_from = Some(src);
self
}
pub fn capture_file(mut self, rel: PathBuf) -> Self {
self.capture_files.push(rel);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxStatus {
Exited(i32),
Signaled(i32),
TimedOut,
Killed,
}
impl SandboxStatus {
pub fn contained(&self) -> bool {
!matches!(self, SandboxStatus::Exited(0))
}
}
#[derive(Debug, Clone)]
pub struct SandboxOutcome {
pub backend: BackendKind,
pub status: SandboxStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub duration: Duration,
pub refusals: Vec<String>,
pub captured_files: HashMap<PathBuf, Option<Vec<u8>>>,
}
#[derive(Debug, Error)]
pub enum SandboxError {
#[error("backend {backend} unavailable: {reason}")]
BackendUnavailable { backend: &'static str, reason: String },
#[error("spawn failed: {0}")]
Spawn(#[source] std::io::Error),
#[error("workspace setup failed: {0}")]
Workspace(#[source] std::io::Error),
#[error("sandbox config rejected: {0}")]
Config(String),
#[error("invalid sandbox state: {0}")]
State(&'static str),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
#[allow(async_fn_in_trait)]
pub trait Sandbox: Send {
fn backend(&self) -> BackendKind;
async fn run(&mut self, opts: SandboxOpts) -> Result<(), SandboxError>;
async fn kill(&mut self) -> Result<(), SandboxError>;
async fn wait(&mut self) -> Result<SandboxOutcome, SandboxError>;
fn logs(&self) -> (&[u8], &[u8]);
}
pub fn available_backends() -> &'static [BackendKind] {
#[cfg(target_os = "macos")]
{
&[BackendKind::Process, BackendKind::Birdcage, BackendKind::Libkrun, BackendKind::Docker]
}
#[cfg(target_os = "linux")]
{
&[
BackendKind::Process,
BackendKind::Birdcage,
BackendKind::Libkrun,
BackendKind::Firecracker,
BackendKind::Docker,
]
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
&[BackendKind::Process, BackendKind::Docker]
}
}
pub fn probe(backend: BackendKind) -> Result<(), SandboxError> {
match backend {
BackendKind::Process => Ok(()),
BackendKind::Birdcage => {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
backend::birdcage::BirdcageSandbox::new().map(|_| ())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err(SandboxError::BackendUnavailable {
backend: "birdcage",
reason: "requires Linux landlock or macOS Seatbelt".into(),
})
}
}
BackendKind::Libkrun => {
if !libkrun_host_supported() {
return Err(SandboxError::BackendUnavailable {
backend: "libkrun",
reason: "requires macOS with Hypervisor.framework or Linux with KVM".into(),
});
}
backend::libkrun::LibkrunSandbox::new().map(|_| ())
}
BackendKind::Firecracker => {
if !firecracker_host_supported() {
return Err(SandboxError::BackendUnavailable {
backend: "firecracker",
reason: "requires Linux with /dev/kvm".into(),
});
}
backend::firecracker::FirecrackerSandbox::new().map(|_| ())
}
BackendKind::Docker => {
if backend::which_on_path("docker").is_some() {
Ok(())
} else {
Err(SandboxError::BackendUnavailable {
backend: "docker",
reason: "docker not found on PATH".into(),
})
}
}
}
}
#[cfg(test)]
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lane_policy_refuses_fast_birdcage_loopback() {
assert!(!permits_loopback(Lane::Fast, BackendKind::Birdcage));
}
#[test]
fn lane_policy_allows_chain_birdcage_loopback() {
assert!(permits_loopback(Lane::Chain, BackendKind::Birdcage));
}
#[test]
fn lane_policy_allows_fast_process_loopback() {
assert!(permits_loopback(Lane::Fast, BackendKind::Process));
}
#[test]
fn lane_policy_allows_fast_libkrun_and_firecracker_loopback() {
assert!(permits_loopback(Lane::Fast, BackendKind::Libkrun));
assert!(permits_loopback(Lane::Fast, BackendKind::Firecracker));
assert!(permits_loopback(Lane::Fast, BackendKind::Docker));
}
#[test]
fn lane_concurrency_defaults_match_plan() {
let cap = LaneConcurrency::defaults();
assert_eq!(cap.chain, 2);
assert_eq!(cap.fast, 8);
assert_eq!(cap.for_lane(Lane::Chain), 2);
assert_eq!(cap.for_lane(Lane::Fast), 8);
}
#[test]
fn select_auto_chain_picks_strongest_for_host() {
let sel = select_backend(BackendChoice::Auto, Lane::Chain);
assert!(probe(sel.backend).is_ok(), "selected backend must probe Ok");
assert!(sel.reason.contains("chain"));
}
#[test]
fn select_auto_fast_picks_birdcage_or_process() {
let sel = select_backend(BackendChoice::Auto, Lane::Fast);
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
assert!(matches!(sel.backend, BackendKind::Birdcage | BackendKind::Process));
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
assert_eq!(sel.backend, BackendKind::Process);
}
}
#[test]
fn select_pinned_falls_back_when_unavailable() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
std::env::set_var("NYX_LIBKRUN_RUNNER", "/definitely/does/not/exist/libkrun-runner");
let sel = select_backend(BackendChoice::Pinned(BackendKind::Libkrun), Lane::Chain);
std::env::remove_var("NYX_LIBKRUN_RUNNER");
assert_ne!(
sel.backend,
BackendKind::Libkrun,
"pinned libkrun must downgrade when runner is missing"
);
assert!(sel.reason.contains("unavailable"));
assert!(sel.reason.contains("fell back"));
}
#[test]
fn probe_process_is_always_ok() {
assert!(probe(BackendKind::Process).is_ok());
}
#[test]
fn available_backends_includes_process() {
let kinds = available_backends();
assert!(kinds.contains(&BackendKind::Process));
}
#[test]
fn backend_kind_as_str_round_trip() {
for k in [
BackendKind::Process,
BackendKind::Birdcage,
BackendKind::Libkrun,
BackendKind::Firecracker,
BackendKind::Docker,
] {
assert!(!k.as_str().is_empty());
}
}
}