use anyhow::{Context, Result, bail};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::plugin_installer;
pub const RUNTIMES: &[(&str, &str)] = &[
("base", "Minimal Alpine Linux (~64MB)"),
("python", "Python 3.12 with pip (~256MB)"),
("node", "Node.js 20 LTS with npm (~256MB)"),
("go", "Go toolchain (~512MB)"),
("rust", "Rust with Cargo (~512MB)"),
];
#[allow(dead_code)]
pub struct SetupConfig {
pub data_dir: PathBuf,
pub kernel_version: String,
pub runtimes: Vec<String>,
pub install_firecracker: bool,
}
impl Default for SetupConfig {
fn default() -> Self {
Self {
data_dir: default_data_dir(),
kernel_version: "6.1.70".to_string(),
runtimes: vec!["base".to_string()],
install_firecracker: true,
}
}
}
pub fn default_data_dir() -> PathBuf {
if let Some(home) = std::env::var_os("HOME") {
PathBuf::from(home).join(".local/share/agentkernel")
} else {
PathBuf::from("/usr/local/share/agentkernel")
}
}
pub fn check_installation() -> SetupStatus {
let data_dir = default_data_dir();
let kvm_path = std::path::PathBuf::from("/dev/kvm");
let kvm_exists = kvm_path.exists();
let kvm_accessible = check_kvm();
let kvm_permission_denied = kvm_exists && !kvm_accessible;
SetupStatus {
kernel_installed: find_kernel(&data_dir).is_some(),
rootfs_base_installed: data_dir.join("images/rootfs/base.ext4").exists(),
rootfs_python_installed: data_dir.join("images/rootfs/python.ext4").exists(),
rootfs_node_installed: data_dir.join("images/rootfs/node.ext4").exists(),
firecracker_installed: find_firecracker().is_some(),
kvm_available: kvm_accessible,
kvm_permission_denied,
docker_available: check_docker(),
apple_containers_available: check_apple_containers(),
macos_version_supported: check_macos_version(),
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct SetupStatus {
pub kernel_installed: bool,
pub rootfs_base_installed: bool,
pub rootfs_python_installed: bool,
pub rootfs_node_installed: bool,
pub firecracker_installed: bool,
pub kvm_available: bool,
pub kvm_permission_denied: bool,
pub docker_available: bool,
pub apple_containers_available: bool,
pub macos_version_supported: bool,
}
impl SetupStatus {
pub fn is_ready(&self) -> bool {
let firecracker_ready =
self.kvm_available && self.kernel_installed && self.rootfs_base_installed;
let container_ready = self.docker_available || self.apple_containers_available;
if self.kvm_available {
return firecracker_ready;
}
container_ready
}
pub fn print(&self) {
println!("Setup Status:");
println!(
" Kernel: {}",
if self.kernel_installed {
"installed"
} else {
"not installed"
}
);
println!(
" Rootfs base: {}",
if self.rootfs_base_installed {
"installed"
} else {
"not installed"
}
);
println!(
" Firecracker: {}",
if self.firecracker_installed {
"installed"
} else {
"not installed"
}
);
let kvm_status = if self.kvm_available {
"available"
} else if self.kvm_permission_denied {
"permission denied"
} else {
"not available"
};
println!(" KVM: {}", kvm_status);
if self.kvm_permission_denied {
println!();
println!(" ⚠️ /dev/kvm exists but you don't have permission to access it.");
println!(" Fix with: sudo usermod -aG kvm $USER && newgrp kvm");
}
println!(
" Docker: {}",
if self.docker_available {
"available"
} else {
"not available"
}
);
if cfg!(target_os = "macos") {
let apple_status = if self.apple_containers_available {
"available"
} else if self.macos_version_supported {
"not installed (macOS 26+ detected)"
} else {
"not available (requires macOS 26+)"
};
println!(" Apple Containers: {}", apple_status);
if self.macos_version_supported && !self.apple_containers_available {
println!();
println!(" 💡 Apple Containers provides VM-level isolation on macOS.");
println!(" Install from: https://github.com/apple/container/releases");
}
}
}
}
fn find_kernel(data_dir: &Path) -> Option<PathBuf> {
let kernel_dir = data_dir.join("images/kernel");
if kernel_dir.exists()
&& let Ok(entries) = std::fs::read_dir(&kernel_dir)
{
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("vmlinux-") && name_str.ends_with("-agentkernel") {
return Some(entry.path());
}
}
}
None
}
fn find_firecracker() -> Option<PathBuf> {
let data_dir = default_data_dir();
let local_fc = data_dir.join("bin/firecracker");
if local_fc.exists() {
return Some(local_fc);
}
if let Ok(output) = Command::new("which").arg("firecracker").output()
&& output.status.success()
{
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
let locations = ["/usr/local/bin/firecracker", "/usr/bin/firecracker"];
for loc in locations {
let path = PathBuf::from(loc);
if path.exists() {
return Some(path);
}
}
None
}
fn check_kvm() -> bool {
let kvm_path = std::path::PathBuf::from("/dev/kvm");
if !kvm_path.exists() {
return false;
}
#[cfg(unix)]
{
use std::fs::OpenOptions;
OpenOptions::new()
.read(true)
.write(true)
.open(&kvm_path)
.is_ok()
}
#[cfg(not(unix))]
{
false
}
}
fn check_docker() -> bool {
Command::new("docker")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn check_apple_containers() -> bool {
Command::new("container")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn check_macos_version() -> bool {
if !cfg!(target_os = "macos") {
return false;
}
Command::new("sw_vers")
.arg("-productVersion")
.output()
.ok()
.and_then(|output| {
String::from_utf8(output.stdout).ok().and_then(|version| {
version
.trim()
.split('.')
.next()
.and_then(|major| major.parse::<u32>().ok())
.map(|major| major >= 26)
})
})
.unwrap_or(false)
}
fn initialize_apple_containers() -> Result<()> {
let status_output = Command::new("container")
.args(["system", "status"])
.output()?;
if status_output.status.success()
&& String::from_utf8_lossy(&status_output.stdout).contains("is running")
{
println!(" Apple container system already running");
return Ok(());
}
println!(" Starting Apple container system...");
let output = Command::new("sh")
.args(["-c", "echo 'Y' | container system start"])
.output()
.context("Failed to start Apple container system")?;
if output.status.success() {
println!(" Apple container system started");
println!(" Pre-pulling alpine:3.20 image...");
let _ = Command::new("container")
.args(["image", "pull", "alpine:3.20"])
.output();
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("already") {
bail!("Failed to start Apple container system: {}", stderr);
}
}
Ok(())
}
#[allow(dead_code)]
pub fn prompt_select(prompt: &str, options: &[(&str, &str)], default: usize) -> Result<usize> {
println!("\n{}", prompt);
for (i, (name, desc)) in options.iter().enumerate() {
let marker = if i == default { " (recommended)" } else { "" };
println!(" {}. {} - {}{}", i + 1, name, desc, marker);
}
print!("\nEnter choice [{}]: ", default + 1);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
return Ok(default);
}
match input.parse::<usize>() {
Ok(n) if n >= 1 && n <= options.len() => Ok(n - 1),
_ => {
println!("Invalid choice, using default.");
Ok(default)
}
}
}
pub fn prompt_yes_no(prompt: &str, default: bool) -> Result<bool> {
let default_str = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", prompt, default_str);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input.is_empty() {
return Ok(default);
}
Ok(input == "y" || input == "yes")
}
pub fn prompt_multi_select(
prompt: &str,
options: &[(&str, &str)],
defaults: &[usize],
) -> Result<Vec<usize>> {
println!("\n{}", prompt);
for (i, (name, desc)) in options.iter().enumerate() {
let marker = if defaults.contains(&i) { " *" } else { "" };
println!(" {}. {} - {}{}", i + 1, name, desc, marker);
}
println!("\n (* = selected by default)");
print!("Enter choices (comma-separated) or press Enter for defaults: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
return Ok(defaults.to_vec());
}
let mut selected = Vec::new();
for part in input.split(',') {
let part = part.trim();
if let Ok(n) = part.parse::<usize>()
&& n >= 1
&& n <= options.len()
&& !selected.contains(&(n - 1))
{
selected.push(n - 1);
}
}
if selected.is_empty() {
return Ok(defaults.to_vec());
}
Ok(selected)
}
pub async fn run_setup(non_interactive: bool) -> Result<()> {
println!("=== Agentkernel Setup ===\n");
let status = check_installation();
status.print();
if status.is_ready() && non_interactive {
println!("\nAgentkernel is already set up and ready to use!");
offer_plugin_install(non_interactive)?;
return Ok(());
}
if !status.kvm_available && !status.docker_available {
println!("\nWarning: Neither KVM nor Docker is available.");
println!(" - On Linux: Ensure /dev/kvm exists and is accessible");
println!(" - On macOS: Install Docker Desktop");
if !non_interactive && !prompt_yes_no("Continue anyway?", false)? {
return Ok(());
}
}
let data_dir = default_data_dir();
println!("\nInstall location: {}", data_dir.display());
let mut install_kernel = !status.kernel_installed;
let mut install_firecracker = !status.firecracker_installed;
let mut runtimes_to_install: Vec<String> = Vec::new();
if non_interactive {
if !status.rootfs_base_installed {
runtimes_to_install.push("base".to_string());
}
} else {
if !status.kernel_installed {
install_kernel = prompt_yes_no("\nBuild and install kernel?", true)?;
}
if !status.firecracker_installed {
install_firecracker = prompt_yes_no("Download and install Firecracker?", true)?;
}
let runtime_options: Vec<(&str, &str)> = RUNTIMES.to_vec();
let defaults = vec![0];
let selected = prompt_multi_select(
"Which runtimes would you like to install?",
&runtime_options,
&defaults,
)?;
for idx in selected {
let runtime = RUNTIMES[idx].0;
let rootfs_path = data_dir.join(format!("images/rootfs/{}.ext4", runtime));
if !rootfs_path.exists() {
runtimes_to_install.push(runtime.to_string());
}
}
}
std::fs::create_dir_all(data_dir.join("images/kernel"))?;
std::fs::create_dir_all(data_dir.join("images/rootfs"))?;
std::fs::create_dir_all(data_dir.join("bin"))?;
if (install_kernel || !runtimes_to_install.is_empty()) && !status.docker_available {
bail!("Docker is required to build kernel and rootfs images. Please install Docker first.");
}
if install_kernel {
println!("\n==> Building kernel...");
build_kernel(&data_dir).await?;
}
for runtime in &runtimes_to_install {
println!("\n==> Building {} rootfs...", runtime);
build_rootfs(&data_dir, runtime).await?;
}
if install_firecracker {
println!("\n==> Installing Firecracker...");
install_firecracker_binary(&data_dir).await?;
}
if status.docker_available {
println!("\n==> Pre-pulling Docker images...");
if let Err(e) = prepull_docker_images(false) {
eprintln!("Warning: Failed to pre-pull some images: {}", e);
}
}
if status.macos_version_supported && status.apple_containers_available {
println!("\n==> Initializing Apple container system...");
if let Err(e) = initialize_apple_containers() {
eprintln!("Warning: Failed to initialize Apple containers: {}", e);
}
}
println!("\n=== Setup Complete ===");
let final_status = check_installation();
if final_status.kernel_installed
&& final_status.rootfs_base_installed
&& final_status.firecracker_installed
{
if final_status.kvm_available {
if !non_interactive {
println!();
if prompt_yes_no("Run a quick verification test?", true)? {
run_verification_test(&data_dir).await?;
}
}
} else if final_status.kvm_permission_denied {
println!(
"\n⚠️ KVM permission denied - you need to fix this before using Firecracker."
);
println!("\nTo fix KVM permissions:");
println!(" 1. Add yourself to the kvm group:");
println!(" sudo usermod -aG kvm $USER");
println!(" 2. Apply the group change (choose one):");
println!(" - Log out and back in, OR");
println!(" - Run: newgrp kvm");
println!(" - Run commands with: sg kvm -c 'agentkernel start ...'");
println!("\nAfter fixing permissions, run: agentkernel setup --verify");
}
}
offer_plugin_install(non_interactive)?;
println!("\nYou can now create sandboxes with:");
println!(" agentkernel create my-sandbox");
println!(" agentkernel start my-sandbox");
Ok(())
}
async fn build_kernel(data_dir: &Path) -> Result<()> {
let script_content = include_str!("../images/build/build-kernel.sh");
let config_content = include_str!("../images/kernel/microvm.config");
let temp_dir = std::env::temp_dir().join("agentkernel-kernel-build");
std::fs::create_dir_all(&temp_dir)?;
let script_path = temp_dir.join("build-kernel.sh");
let config_path = temp_dir.join("microvm.config");
std::fs::write(&script_path, script_content)?;
std::fs::write(&config_path, config_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755))?;
}
let dockerfile = r#"
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
build-essential bc bison flex libelf-dev libssl-dev curl xz-utils cpio \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY build-kernel.sh /build/
COPY microvm.config /kernel/
RUN chmod +x /build/build-kernel.sh
RUN mkdir -p /kernel
ENV BUILD_DIR=/tmp/kernel-build
ENTRYPOINT ["/build/build-kernel.sh"]
CMD ["6.1.70"]
"#;
let dockerfile_path = temp_dir.join("Dockerfile");
std::fs::write(&dockerfile_path, dockerfile)?;
let status = Command::new("docker")
.args(["build", "-t", "agentkernel-kernel-builder", "."])
.current_dir(&temp_dir)
.status()
.context("Failed to build kernel builder Docker image")?;
if !status.success() {
bail!("Failed to build kernel builder Docker image");
}
let kernel_dir = data_dir.join("images/kernel");
std::fs::create_dir_all(&kernel_dir)?;
std::fs::write(kernel_dir.join("microvm.config"), config_content)?;
let status = Command::new("docker")
.args([
"run",
"--rm",
"-v",
&format!("{}:/kernel", kernel_dir.display()),
"agentkernel-kernel-builder",
"6.1.70",
])
.status()
.context("Failed to run kernel build")?;
if !status.success() {
bail!("Kernel build failed");
}
println!("Kernel installed to: {}", kernel_dir.display());
Ok(())
}
async fn build_guest_agent(data_dir: &Path) -> Result<()> {
let bin_dir = data_dir.join("bin");
std::fs::create_dir_all(&bin_dir)?;
let guest_agent_source = include_str!("embedded/guest_agent_main.rs");
let guest_agent_cargo = include_str!("embedded/guest_agent_cargo.toml");
let temp_dir = std::env::temp_dir().join("agentkernel-guest-build");
std::fs::create_dir_all(&temp_dir)?;
std::fs::create_dir_all(temp_dir.join("src"))?;
std::fs::write(temp_dir.join("src/main.rs"), guest_agent_source)?;
std::fs::write(temp_dir.join("Cargo.toml"), guest_agent_cargo)?;
let dockerfile = r#"
FROM rust:1.85-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /build
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl 2>/dev/null || cargo build --release
RUN cp target/*/release/agent /agent || cp target/release/agent /agent
FROM scratch
COPY --from=builder /agent /agent
CMD ["/agent"]
"#;
std::fs::write(temp_dir.join("Dockerfile"), dockerfile)?;
let status = Command::new("docker")
.args(["build", "-t", "agentkernel-guest-builder", "."])
.current_dir(&temp_dir)
.status()
.context("Failed to build guest agent Docker image")?;
if !status.success() {
bail!("Failed to build guest agent Docker image");
}
let _ = Command::new("docker")
.args(["rm", "-f", "agentkernel-guest-tmp"])
.output();
let status = Command::new("docker")
.args([
"create",
"--name",
"agentkernel-guest-tmp",
"agentkernel-guest-builder",
])
.status()
.context("Failed to create temp container")?;
if !status.success() {
bail!("Failed to create temp container for guest agent");
}
let status = Command::new("docker")
.args([
"cp",
"agentkernel-guest-tmp:/agent",
&bin_dir.join("agent").to_string_lossy(),
])
.status()
.context("Failed to extract guest agent binary")?;
let _ = Command::new("docker")
.args(["rm", "-f", "agentkernel-guest-tmp"])
.output();
if !status.success() {
bail!("Failed to extract guest agent binary");
}
let agent_path = bin_dir.join("agent");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if agent_path.exists() {
std::fs::set_permissions(&agent_path, std::fs::Permissions::from_mode(0o755))?;
}
}
let agent_size = std::fs::metadata(&agent_path).map(|m| m.len()).unwrap_or(0);
if agent_size == 0 {
bail!(
"Guest agent binary is empty (0 bytes). This usually means the Rust build failed.\n\
Check that guest-agent/Cargo.toml uses a supported Rust edition (2021, not 2024)."
);
}
if agent_size < 10000 {
eprintln!(
"Warning: Guest agent binary is unusually small ({} bytes). It may not work correctly.",
agent_size
);
}
println!(
"Guest agent built: {} ({} bytes)",
agent_path.display(),
agent_size
);
Ok(())
}
async fn build_rootfs(data_dir: &Path, runtime: &str) -> Result<()> {
let rootfs_dir = data_dir.join("images/rootfs");
std::fs::create_dir_all(&rootfs_dir)?;
let agent_bin = data_dir.join("bin/agent");
if !agent_bin.exists() {
println!("Building guest agent...");
build_guest_agent(data_dir).await?;
}
let size_mb = match runtime {
"base" => 64,
"python" | "node" => 256,
"go" | "rust" => 512,
_ => 256,
};
let packages = match runtime {
"python" => "python3 py3-pip",
"node" => "nodejs npm",
"go" => "go",
"rust" => "rust cargo",
_ => "",
};
let build_script = format!(
r#"#!/bin/sh
set -eu
# Install required tools
apk add --no-cache e2fsprogs
ROOTFS_IMG="/output/{runtime}.ext4"
MOUNT_DIR="/mnt/rootfs"
SIZE_MB={size_mb}
PACKAGES="{packages}"
echo "Creating ${{SIZE_MB}}MB ext4 image..."
dd if=/dev/zero of="$ROOTFS_IMG" bs=1M count=$SIZE_MB 2>/dev/null
mkfs.ext4 -F "$ROOTFS_IMG"
echo "Mounting and populating rootfs..."
mkdir -p "$MOUNT_DIR"
mount -o loop "$ROOTFS_IMG" "$MOUNT_DIR"
echo "Installing Alpine base system..."
apk -X https://dl-cdn.alpinelinux.org/alpine/v3.20/main \
-X https://dl-cdn.alpinelinux.org/alpine/v3.20/community \
-U --allow-untrusted --root "$MOUNT_DIR" --initdb \
add alpine-base busybox-static $PACKAGES || true
mkdir -p "$MOUNT_DIR"/{{dev,proc,sys,tmp,run,root,app,usr/bin}}
chmod 1777 "$MOUNT_DIR/tmp"
# Copy guest agent if available
if [ -f /agent-bin/agent ]; then
cp /agent-bin/agent "$MOUNT_DIR/usr/bin/agent"
chmod +x "$MOUNT_DIR/usr/bin/agent"
echo "Guest agent installed"
fi
# Create device nodes
mknod -m 622 "$MOUNT_DIR/dev/console" c 5 1 || true
mknod -m 666 "$MOUNT_DIR/dev/null" c 1 3 || true
mknod -m 666 "$MOUNT_DIR/dev/zero" c 1 5 || true
mknod -m 666 "$MOUNT_DIR/dev/tty" c 5 0 || true
mknod -m 666 "$MOUNT_DIR/dev/random" c 1 8 || true
mknod -m 666 "$MOUNT_DIR/dev/urandom" c 1 9 || true
# Create init script that starts the guest agent
cat > "$MOUNT_DIR/init" << 'INIT'
#!/bin/busybox sh
/bin/busybox mount -t proc proc /proc
/bin/busybox mount -t sysfs sysfs /sys
/bin/busybox mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
/bin/busybox hostname agentkernel
# Start guest agent in background if available
if [ -x /usr/bin/agent ]; then
/usr/bin/agent &
AGENT_PID=$!
echo "Guest agent started"
fi
echo "Agentkernel guest ready"
if [ $# -gt 0 ]; then
exec "$@"
elif [ -n "$AGENT_PID" ]; then
wait $AGENT_PID
else
exec /bin/busybox sh
fi
INIT
chmod +x "$MOUNT_DIR/init"
# Set up /etc files
echo "agentkernel" > "$MOUNT_DIR/etc/hostname"
echo "root:x:0:0:root:/root:/bin/sh" > "$MOUNT_DIR/etc/passwd"
echo "root:x:0:" > "$MOUNT_DIR/etc/group"
umount "$MOUNT_DIR"
# Fix ownership so Firecracker can access the file
if [ -n "$HOST_UID" ] && [ -n "$HOST_GID" ]; then
chown "$HOST_UID:$HOST_GID" "$ROOTFS_IMG"
fi
echo "Rootfs created: $ROOTFS_IMG"
ls -lh "$ROOTFS_IMG"
"#,
runtime = runtime,
size_mb = size_mb,
packages = packages
);
let temp_dir = std::env::temp_dir().join("agentkernel-rootfs-build");
std::fs::create_dir_all(&temp_dir)?;
let script_path = temp_dir.join("build.sh");
std::fs::write(&script_path, &build_script)?;
eprintln!(" (Building with privileged Docker - required for loop device access)");
let uid = Command::new("id")
.args(["-u"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "1000".to_string());
let gid = Command::new("id")
.args(["-g"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "1000".to_string());
let status = Command::new("docker")
.args([
"run",
"--rm",
"--privileged",
"-e",
&format!("HOST_UID={}", uid),
"-e",
&format!("HOST_GID={}", gid),
"-v",
&format!("{}:/output", rootfs_dir.display()),
"-v",
&format!("{}:/build.sh:ro", script_path.display()),
"-v",
&format!("{}:/agent-bin:ro", data_dir.join("bin").display()),
"alpine:3.20",
"/bin/sh",
"/build.sh",
])
.status()
.context("Failed to run rootfs build")?;
if !status.success() {
bail!("Rootfs build failed for {}", runtime);
}
let rootfs_path = rootfs_dir.join(format!("{}.ext4", runtime));
#[cfg(unix)]
{
use std::os::unix::fs::chown;
if let (Some(uid), Some(gid)) = (
std::env::var("UID").ok().and_then(|s| s.parse().ok()),
std::env::var("GID")
.ok()
.or_else(|| std::env::var("GROUPS").ok())
.and_then(|s| s.split_whitespace().next().and_then(|g| g.parse().ok())),
) {
let _ = chown(&rootfs_path, Some(uid), Some(gid));
} else {
if let Ok(output) = Command::new("id").args(["-u"]).output()
&& output.status.success()
{
let uid: u32 = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.unwrap_or(1000);
if let Ok(output) = Command::new("id").args(["-g"]).output()
&& output.status.success()
{
let gid: u32 = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.unwrap_or(1000);
let _ = chown(&rootfs_path, Some(uid), Some(gid));
}
}
}
}
println!(
"Rootfs installed to: {}/{}.ext4",
rootfs_dir.display(),
runtime
);
Ok(())
}
async fn run_verification_test(data_dir: &Path) -> Result<()> {
println!("\n==> Running verification test...");
let kernel_path = find_kernel(data_dir).ok_or_else(|| anyhow::anyhow!("Kernel not found"))?;
let rootfs_path = data_dir.join("images/rootfs/base.ext4");
let firecracker_path = data_dir.join("bin/firecracker");
if !rootfs_path.exists() {
bail!("Rootfs not found: {}", rootfs_path.display());
}
if !firecracker_path.exists() {
bail!("Firecracker not found: {}", firecracker_path.display());
}
println!(" Kernel: {}", kernel_path.display());
println!(" Rootfs: {}", rootfs_path.display());
println!(" Firecracker: {}", firecracker_path.display());
let kernel_size = std::fs::metadata(&kernel_path)?.len();
let rootfs_size = std::fs::metadata(&rootfs_path)?.len();
let fc_size = std::fs::metadata(&firecracker_path)?.len();
println!("\n Kernel size: {} bytes", kernel_size);
println!(" Rootfs size: {} bytes", rootfs_size);
println!(" Firecracker size: {} bytes", fc_size);
if kernel_size < 1_000_000 {
eprintln!(" ⚠️ Kernel seems too small, might not be built correctly");
}
if rootfs_size < 10_000_000 {
eprintln!(" ⚠️ Rootfs seems too small, might not be built correctly");
}
let agent_path = data_dir.join("bin/agent");
if agent_path.exists() {
let agent_size = std::fs::metadata(&agent_path)?.len();
println!(" Guest agent: {} bytes", agent_size);
if agent_size > 0 {
println!("\n✓ All components look good!");
println!(
"\nNote: Full boot test requires KVM access. Create and start a sandbox to test:"
);
println!(" agentkernel create test-sandbox");
println!(" agentkernel start test-sandbox");
println!(" agentkernel exec test-sandbox -- echo 'Hello from microVM!'");
}
} else {
eprintln!(" ⚠️ Guest agent not found at {}", agent_path.display());
}
Ok(())
}
fn offer_plugin_install(non_interactive: bool) -> Result<()> {
let uninstalled = plugin_installer::detect_uninstalled_plugins();
if uninstalled.is_empty() {
return Ok(());
}
println!("\n==> Agent plugins");
println!(
"Detected agents without plugins: {}",
uninstalled
.iter()
.map(|t| t.name())
.collect::<Vec<_>>()
.join(", ")
);
let should_install = if non_interactive {
true
} else {
prompt_yes_no("Install agent plugins now?", true)?
};
if should_install {
if let Err(e) = plugin_installer::install_detected_plugins(&uninstalled) {
eprintln!("Warning: Failed to install some plugins: {}", e);
}
} else {
println!(" Skipped. Run later with: agentkernel plugin install <agent>");
}
Ok(())
}
const DOCKER_IMAGES: &[&str] = &[
"alpine:3.20", "python:3.12-alpine", "node:20-alpine", "golang:1.22-alpine", "rust:1.85-alpine", ];
pub fn prepull_docker_images(quiet: bool) -> Result<()> {
if !check_docker() {
if !quiet {
println!("Docker not available, skipping image pre-pull");
}
return Ok(());
}
if !quiet {
println!("Pre-pulling common Docker images for faster startup...");
}
for image in DOCKER_IMAGES {
if !quiet {
print!(" Pulling {}... ", image);
io::stdout().flush()?;
}
let output = Command::new("docker")
.args(["pull", "-q", image])
.output()
.context("Failed to pull Docker image")?;
if output.status.success() {
if !quiet {
println!("done");
}
} else if !quiet {
println!("failed (will be pulled on first use)");
}
}
Ok(())
}
async fn install_firecracker_binary(data_dir: &Path) -> Result<()> {
let bin_dir = data_dir.join("bin");
std::fs::create_dir_all(&bin_dir)?;
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
bail!("Unsupported architecture");
};
let version = "v1.7.0";
let url = format!(
"https://github.com/firecracker-microvm/firecracker/releases/download/{}/firecracker-{}-{}.tgz",
version, version, arch
);
println!("Downloading Firecracker {} for {}...", version, arch);
let status = Command::new("sh")
.args([
"-c",
&format!(
r#"curl -fsSL "{}" | tar -xz -C "{}" && \
mv "{}/release-{}-{}/firecracker-{}-{}" "{}/firecracker" && \
chmod +x "{}/firecracker" && \
rm -rf "{}/release-{}-{}""#,
url,
bin_dir.display(),
bin_dir.display(),
version,
arch,
version,
arch,
bin_dir.display(),
bin_dir.display(),
bin_dir.display(),
version,
arch
),
])
.status()
.context("Failed to download Firecracker")?;
if !status.success() {
bail!("Failed to download Firecracker");
}
let firecracker_path = bin_dir.join("firecracker");
println!("Firecracker installed to: {}", firecracker_path.display());
println!("\nAdd to your PATH:");
println!(" export PATH=\"{}:$PATH\"", bin_dir.display());
Ok(())
}