use tokio::process::Command;
use tracing::{debug, info};
use synwire_core::agents::sandbox::SandboxConfig;
use crate::SandboxError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContainerRuntime {
AppleContainer,
Docker,
Podman,
Colima,
}
impl ContainerRuntime {
fn display_name(self) -> &'static str {
match self {
Self::AppleContainer => "Apple Container",
Self::Docker => "Docker Desktop",
Self::Podman => "Podman",
Self::Colima => "Colima",
}
}
fn probe_binary(self) -> &'static str {
match self {
Self::AppleContainer => "container",
Self::Docker => "docker",
Self::Podman => "podman",
Self::Colima => "colima",
}
}
fn run_binary(self) -> &'static str {
match self {
Self::AppleContainer => "container",
Self::Docker | Self::Colima => "docker",
Self::Podman => "podman",
}
}
}
pub async fn detect_container_runtime() -> Option<ContainerRuntime> {
if binary_responds("container").await {
info!(
runtime = "Apple Container",
"detected macOS container runtime"
);
return Some(ContainerRuntime::AppleContainer);
}
if docker_daemon_running().await {
info!(
runtime = "Docker Desktop",
"detected macOS container runtime"
);
return Some(ContainerRuntime::Docker);
}
if binary_responds("podman").await {
info!(runtime = "Podman", "detected macOS container runtime");
return Some(ContainerRuntime::Podman);
}
if colima_running().await {
info!(runtime = "Colima", "detected macOS container runtime");
return Some(ContainerRuntime::Colima);
}
debug!("no macOS container runtime found (tried: container, docker, podman, colima)");
None
}
pub async fn spawn_with_runtime(
runtime: ContainerRuntime,
config: &SandboxConfig,
image: &str,
command: &str,
args: &[String],
) -> Result<tokio::process::Child, SandboxError> {
match runtime {
ContainerRuntime::AppleContainer => {
spawn_apple_container(config, image, command, args).await
}
ContainerRuntime::Docker | ContainerRuntime::Colima => {
spawn_docker_compatible(runtime, config, image, command, args).await
}
ContainerRuntime::Podman => {
spawn_docker_compatible(runtime, config, image, command, args).await
}
}
}
async fn spawn_apple_container(
config: &SandboxConfig,
image: &str,
command: &str,
args: &[String],
) -> Result<tokio::process::Child, SandboxError> {
let mut cmd = Command::new("container");
let _c = cmd.arg("run").arg("--rm");
if let Some(fs) = &config.filesystem {
for path in &fs.allow_write {
let _c = cmd
.arg("--mount")
.arg(format!("type=bind,src={path},dst={path}"));
}
for path in &fs.deny_write {
let _c = cmd
.arg("--mount")
.arg(format!("type=bind,src={path},dst={path},readonly"));
}
}
let network_enabled = config.network.as_ref().map(|n| n.enabled).unwrap_or(false);
if !network_enabled {
let _c = cmd.arg("--network").arg("none");
}
apply_resource_flags(&mut cmd, config);
apply_security_flags(&mut cmd, config);
apply_env_flags(&mut cmd, config);
let _c = cmd.arg(image).arg(command);
for arg in args {
let _c = cmd.arg(arg);
}
cmd.kill_on_drop(true)
.spawn()
.map_err(|e| SandboxError::InitFailed {
reason: format!("Apple Container spawn failed: {e}"),
})
}
async fn spawn_docker_compatible(
runtime: ContainerRuntime,
config: &SandboxConfig,
image: &str,
command: &str,
args: &[String],
) -> Result<tokio::process::Child, SandboxError> {
let binary = runtime.run_binary();
let mut cmd = Command::new(binary);
let _c = cmd.arg("run").arg("--rm").arg("--interactive");
if let Some(fs) = &config.filesystem {
for path in &fs.allow_write {
let _c = cmd.arg("--volume").arg(format!("{path}:{path}:rw"));
}
for path in &fs.deny_write {
let _c = cmd.arg("--volume").arg(format!("{path}:{path}:ro"));
}
}
let network_enabled = config.network.as_ref().map(|n| n.enabled).unwrap_or(false);
if !network_enabled {
let _c = cmd.arg("--network").arg("none");
}
apply_resource_flags(&mut cmd, config);
if let (Some(uid), Some(gid)) = (config.security.run_as_user, config.security.run_as_group) {
let _c = cmd.arg("--user").arg(format!("{uid}:{gid}"));
}
apply_security_flags(&mut cmd, config);
apply_env_flags(&mut cmd, config);
let _c = cmd.arg(image).arg(command);
for arg in args {
let _c = cmd.arg(arg);
}
cmd.kill_on_drop(true)
.spawn()
.map_err(|e| SandboxError::InitFailed {
reason: format!("{} spawn failed: {e}", runtime.display_name()),
})
}
fn apply_resource_flags(cmd: &mut Command, config: &SandboxConfig) {
if let Some(limits) = &config.resources {
if let Some(mem) = limits.memory_bytes {
let _c = cmd.arg("--memory").arg(mem.to_string());
}
if let Some(cpu) = limits.cpu_quota {
let _c = cmd.arg("--cpus").arg(format!("{cpu:.2}"));
}
}
}
fn apply_security_flags(cmd: &mut Command, config: &SandboxConfig) {
if config.security.no_new_privileges {
let _c = cmd.arg("--security-opt").arg("no-new-privileges");
}
}
fn apply_env_flags(cmd: &mut Command, config: &SandboxConfig) {
if config.env.inherit_parent {
for (k, v) in std::env::vars() {
if !config.env.unset.contains(&k) {
let _c = cmd.arg("--env").arg(format!("{k}={v}"));
}
}
}
for (k, v) in &config.env.set {
let _c = cmd.arg("--env").arg(format!("{k}={v}"));
}
}
async fn binary_responds(binary: &str) -> bool {
Command::new(binary)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}
async fn docker_daemon_running() -> bool {
Command::new("docker")
.arg("version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}
async fn colima_running() -> bool {
Command::new("colima")
.arg("status")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}