Skip to content

Instantly share code, notes, and snippets.

@akhenakh
Last active March 3, 2026 15:15
Show Gist options
  • Select an option

  • Save akhenakh/c9f33ee5fa3b75d7b599aa9047aa0447 to your computer and use it in GitHub Desktop.

Select an option

Save akhenakh/c9f33ee5fa3b75d7b599aa9047aa0447 to your computer and use it in GitHub Desktop.
create small spawned containers for Arch family Linuxes

AI Agent Container Manager

A lightweight Bash script to create and manage secure systemd-nspawn containers.
Designed for isolating AI Agents and LLM scripts while giving them controlled access to specific project directories on your host.

Prerequisites

This script is designed for Arch Linux (or derivatives).

sudo pacman -S arch-install-scripts systemd-container

Setup

  1. Save the script as spawn.sh.
  2. Make it executable:
    chmod +x spawn.sh

Usage

1. Create a Container

Containers are minimal Arch Linux environments.

Option A: Read-Only Access (Default - Secure)
The agent can read your code but cannot modify or delete files on your host.

# Usage: ./spawn.sh create <name> <host-path> <container-path>
./spawn.sh create ai-sandbox ~/my-projects/app /work

Option B: Read-Write Access
Use this if you want the agent to write code or modify files.

# Add the --rw flag at the end
./spawn.sh create ai-dev ~/my-projects/app /work --rw

2. Enter the Container

This starts the container (if stopped) and drops you into a shell as the user agent.

./spawn.sh shell ai-sandbox

Inside the container:

  • User: agent (UID mapped automatically)
  • Sudo: Enabled without password (sudo pacman -S ... works)
  • Files: Your host directory is mounted at /work

3. Other Commands

Command Description
./spawn.sh start <name> Boot the container in the background.
./spawn.sh stop <name> Power off the container.
./spawn.sh list List created and running containers.
./spawn.sh delete <name> Delete a container and its config.

Pre-installed Software

Modify the script to suit your needs

Security Note

This script uses systemd-nspawn with PrivateUsers=pick.

  • Files on Host: Appear owned by your user (UID 1000).
  • Files in Container: Appear owned by root/agent.
  • Isolation: Processes are isolated from the host system.
#!/bin/bash
# spawn-hybrid.sh - Secure Container Manager for AI Agents
set -euo pipefail
# --- Configuration ---
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}"
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}"
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}"
DEFAULT_CPU_QUOTA="50%"
DEFAULT_MEM_LIMIT="2G"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# --- Logging Helpers ---
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; }
# --- Dependency Check ---
# Ensure all required host utilities are available before running.
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
exit 1
fi
}
# --- Initialization ---
# Setup necessary directory structures in the host environment.
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
mkdir -p "$CONTAINER_LOGS"
chmod 700 "$CONTAINER_LOGS"
}
# --- Help / Usage ---
show_help() {
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
-h, --help Show this help message
EOF
}
# --- Core Functions ---
create_container() {
# Fail early if no container name is provided.
if [ -z "${1:-}" ]; then
log_error "Container name required."
echo ""
show_help
exit 1
fi
# Catch help flag passed as the first argument
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_help
exit 0
fi
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-h|--help) show_help; exit 0 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
# Default container mount destination if none is specified
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
# Ensure mounted host directories are not sensitive system paths.
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
# Install Arch Linux base system and required packages.
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
# Create a secure, random password for the container users.
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
# Set the hostname inside the container.
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
# Configure networking if enabled via flags.
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
# Enable systemd-resolved and systemd-networkd for DNS and DHCP.
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
# Setup the 'agent' user and set the root password for su/sudo access.
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
# Add agent to the wheel group if sudo was installed.
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
# Generate the systemd-nspawn profile defining sandboxing rules.
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
# Configure directory binds based on provided flags.
local bind_mount=""
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
bind_mount="Bind=$mount_src:$mount_dst"
else
bind_mount="BindReadOnly=$mount_src:$mount_dst"
fi
fi
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
# Safely map container root to unprivileged host user
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
# Removed NoNewPrivileges=yes to allow su/sudo functionality
[Network]
VirtualEthernet=yes
Private=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
$bind_mount
EOF
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
# --- Auto-Shutdown Timer ---
# Create a systemd service inside the container to power it off after a timeout.
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
# Check if already running via machinectl
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name.
# This is critical for applying settings when we start manually.
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
# We use systemd-run to launch nspawn as a transient service.
# We explicitly point -D to our custom path, bypassing the need for /var/lib/machines.
# Cleanup any failed previous units
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
# Poll machinectl to confirm the container has registered successfully.
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link from the run directory
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
# Check if running; if not, start it and wait briefly for it to boot.
if ! machinectl list 2>/dev/null | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# Enter the container via machinectl shell.
# Use /usr/bin/env to set variables, as machinectl shell
# does not accept --setenv flags directly.
sudo machinectl shell "agent@$name" \
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash
}
delete_container() {
local name=$1
local path="$CONTAINER_BASE/$name"
if [ ! -d "$path" ]; then
log_error "Not found."
exit 1
fi
echo -e "${RED}DANGER: Deleting $name${NC}"
echo -e "This will wipe: $path"
read -p "Type 'DELETE' to confirm: " confirm
if [ "$confirm" == "DELETE" ]; then
# Force terminate if running
if machinectl list | grep -q "^$name "; then
sudo machinectl terminate "$name" 2>/dev/null || true
fi
# Remove filesystem and all associated metadata/configs
sudo rm -rf "$path"
rm -f "$CONTAINER_CONFIG/$name.nspawn"
rm -f "$CONTAINER_CONFIG/.$name.pwd"
rm -f "$CONTAINER_CONFIG/.$name.meta"
log_info "Deleted."
else
log_info "Cancelled."
fi
}
list_containers() {
echo -e "${BLUE}CONTAINERS:${NC}"
# Iterate over generated metadata files to display inventory
for meta in "$CONTAINER_CONFIG"/.*.meta; do
[ -e "$meta" ] || continue
source "$meta"
local status="STOPPED"
if machinectl list 2>/dev/null | grep -q "^$NAME "; then
status="${GREEN}RUNNING${NC}"
fi
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE"
done
}
info_container() {
local name=$1
local meta="$CONTAINER_CONFIG/.$name.meta"
local pwd="$CONTAINER_CONFIG/.$name.pwd"
if [ ! -f "$meta" ]; then
log_error "No metadata found for $name"
exit 1
fi
source "$meta"
echo "--------------------------------"
echo "Container: $NAME"
echo "Created: $CREATED"
echo "Network: $NET_ENABLED"
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)"
echo "--------------------------------"
if [ -f "$pwd" ]; then
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}"
fi
echo "--------------------------------"
}
# --- Main Dispatch ---
check_dependencies
setup_directories
# Extract main command, defaulting to 'help'
CMD="${1:-help}"
shift || true
# Route the execution based on the main command
case "$CMD" in
-h|--help) show_help ;;
create) create_container "$@" ;;
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;;
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;;
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;;
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;;
list) list_containers ;;
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;;
*) show_help ;;
esac
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
exit 1
fi
}
# --- Initialization ---
# Setup necessary directory structures in the host environment.
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
mkdir -p "$CONTAINER_LOGS"
chmod 700 "$CONTAINER_LOGS"
}
# --- Help / Usage ---
show_help() {
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
-h, --help Show this help message
EOF
}
# --- Core Functions ---
create_container() {
# Fail early if no container name is provided.
if [ -z "${1:-}" ]; then
log_error "Container name required."
echo ""
show_help
exit 1
fi
# Catch help flag passed as the first argument
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_help
exit 0
fi
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-h|--help) show_help; exit 0 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
# Default container mount destination if none is specified
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
# Ensure mounted host directories are not sensitive system paths.
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
# Install Arch Linux base system and required packages.
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
# Create a secure, random password for the container users.
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
# Set the hostname inside the container.
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
# Configure networking if enabled via flags.
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
# Enable systemd-resolved and systemd-networkd for DNS and DHCP.
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
# Setup the 'agent' user and set the root password for su/sudo access.
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
# Add agent to the wheel group if sudo was installed.
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
# Generate the systemd-nspawn profile defining sandboxing rules.
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
# Safely map container root to unprivileged host user
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
# Removed NoNewPrivileges=yes to allow su/sudo functionality
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
EOF
# Configure directory binds based on provided flags.
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file"
fi
fi
echo "Bind=/home/agent" >> "$nspawn_file"
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
# --- Auto-Shutdown Timer ---
# Create a systemd service inside the container to power it off after a timeout.
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
# Check if already running via machinectl
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name.
# This is critical for applying settings when we start manually.
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
# We use systemd-run to launch nspawn as a transient service.
# We explicitly point -D to our custom path, bypassing the need for /var/lib/machines.
# Cleanup any failed previous units
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
# Poll machinectl to confirm the container has registered successfully.
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link from the run directory
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
# Check if running; if not, start it and wait briefly for it to boot.
if ! machinectl list 2>/dev/null | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# Enter the container via machinectl shell.
# Use /usr/bin/env to set variables, as machinectl shell
# does not accept --setenv flags directly.
sudo machinectl shell "agent@$name" \
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash
}
delete_container() {
local name=$1
local path="$CONTAINER_BASE/$name"
if [ ! -d "$path" ]; then
log_error "Not found."
exit 1
fi
echo -e "${RED}DANGER: Deleting $name${NC}"
echo -e "This will wipe: $path"
read -p "Type 'DELETE' to confirm: " confirm
if [ "$confirm" == "DELETE" ]; then
# Force terminate if running
if machinectl list | grep -q "^$name "; then
sudo machinectl terminate "$name" 2>/dev/null || true
fi
# Remove filesystem and all associated metadata/configs
sudo rm -rf "$path"
rm -f "$CONTAINER_CONFIG/$name.nspawn"
rm -f "$CONTAINER_CONFIG/.$name.pwd"
rm -f "$CONTAINER_CONFIG/.$name.meta"
log_info "Deleted."
else
log_info "Cancelled."
fi
}
list_containers() {
echo -e "${BLUE}CONTAINERS:${NC}"
# Iterate over generated metadata files to display inventory
for meta in "$CONTAINER_CONFIG"/.*.meta; do
[ -e "$meta" ] || continue
source "$meta"
local status="STOPPED"
if machinectl list 2>/dev/null | grep -q "^$NAME "; then
status="${GREEN}RUNNING${NC}"
fi
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE"
done
}
info_container() {
local name=$1
local meta="$CONTAINER_CONFIG/.$name.meta"
local pwd="$CONTAINER_CONFIG/.$name.pwd"
if [ ! -f "$meta" ]; then
log_error "No metadata found for $name"
exit 1
fi
source "$meta"
echo "--------------------------------"
echo "Container: $NAME"
echo "Created: $CREATED"
echo "Network: $NET_ENABLED"
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)"
echo "--------------------------------"
if [ -f "$pwd" ]; then
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}"
fi
echo "--------------------------------"
}
# --- Main Dispatch ---
check_dependencies
setup_directories
# Extract main command, defaulting to 'help'
CMD="${1:-help}"
shift || true
# Route the execution based on the main command
case "$CMD" in
-h|--help) show_help ;;
create) create_container "$@" ;;
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;;
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;;
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;;
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;;
list) list_containers ;;
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;;
*) show_help ;;
esac
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
exit 1
fi
}
# --- Initialization ---
# Setup necessary directory structures in the host environment.
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
mkdir -p "$CONTAINER_LOGS"
chmod 700 "$CONTAINER_LOGS"
}
# --- Help / Usage ---
show_help() {
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
-h, --help Show this help message
EOF
}
# --- Core Functions ---
create_container() {
# Fail early if no container name is provided.
if [ -z "${1:-}" ]; then
log_error "Container name required."
echo ""
show_help
exit 1
fi
# Catch help flag passed as the first argument
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
show_help
exit 0
fi
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-h|--help) show_help; exit 0 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
# Default container mount destination if none is specified
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
# Ensure mounted host directories are not sensitive system paths.
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
# Install Arch Linux base system and required packages.
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
# Create a secure, random password for the container users.
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
# Set the hostname inside the container.
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
# Configure networking if enabled via flags.
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
# Enable systemd-resolved and systemd-networkd for DNS and DHCP.
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
# Setup the 'agent' user and set the root password for su/sudo access.
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
# Add agent to the wheel group if sudo was installed.
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
# Generate the systemd-nspawn profile defining sandboxing rules.
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
# Safely map container root to unprivileged host user
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
# Removed NoNewPrivileges=yes to allow su/sudo functionality
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
EOF
# Configure directory binds based on provided flags.
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file"
fi
fi
echo "Bind=/home/agent" >> "$nspawn_file"
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
# --- Auto-Shutdown Timer ---
# Create a systemd service inside the container to power it off after a timeout.
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
# Check if already running via machinectl
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name.
# This is critical for applying settings when we start manually.
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
# We use systemd-run to launch nspawn as a transient service.
# We explicitly point -D to our custom path, bypassing the need for /var/lib/machines.
# Cleanup any failed previous units
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
# Poll machinectl to confirm the container has registered successfully.
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link from the run directory
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
# Check if running; if not, start it and wait briefly for it to boot.
if ! machinectl list 2>/dev/null | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# Enter the container via machinectl shell.
# Use /usr/bin/env to set variables, as machinectl shell
# does not accept --setenv flags directly.
sudo machinectl shell "agent@$name" \
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash
}
delete_container() {
local name=$1
local path="$CONTAINER_BASE/$name"
if [ ! -d "$path" ]; then
log_error "Not found."
exit 1
fi
echo -e "${RED}DANGER: Deleting $name${NC}"
echo -e "This will wipe: $path"
read -p "Type 'DELETE' to confirm: " confirm
if [ "$confirm" == "DELETE" ]; then
# Force terminate if running
if machinectl list | grep -q "^$name "; then
sudo machinectl terminate "$name" 2>/dev/null || true
fi
# Remove filesystem and all associated metadata/configs
sudo rm -rf "$path"
rm -f "$CONTAINER_CONFIG/$name.nspawn"
rm -f "$CONTAINER_CONFIG/.$name.pwd"
rm -f "$CONTAINER_CONFIG/.$name.meta"
log_info "Deleted."
else
log_info "Cancelled."
fi
}
list_containers() {
echo -e "${BLUE}CONTAINERS:${NC}"
# Iterate over generated metadata files to display inventory
for meta in "$CONTAINER_CONFIG"/.*.meta; do
[ -e "$meta" ] || continue
source "$meta"
local status="STOPPED"
if machinectl list 2>/dev/null | grep -q "^$NAME "; then
status="${GREEN}RUNNING${NC}"
fi
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE"
done
}
info_container() {
local name=$1
local meta="$CONTAINER_CONFIG/.$name.meta"
local pwd="$CONTAINER_CONFIG/.$name.pwd"
if [ ! -f "$meta" ]; then
log_error "No metadata found for $name"
exit 1
fi
source "$meta"
echo "--------------------------------"
echo "Container: $NAME"
echo "Created: $CREATED"
echo "Network: $NET_ENABLED"
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)"
echo "--------------------------------"
if [ -f "$pwd" ]; then
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}"
fi
echo "--------------------------------"
}
# --- Main Dispatch ---
check_dependencies
setup_directories
# Extract main command, defaulting to 'help'
CMD="${1:-help}"
shift || true
# Route the execution based on the main command
case "$CMD" in
-h|--help) show_help ;;
create) create_container "$@" ;;
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;;
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;;
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;;
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;;
list) list_containers ;;
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;;
*) show_help ;;
esac
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
EOF
}
# --- Core Functions ---
create_container() {
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
EOF
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file"
fi
fi
echo "Bind=/home/agent" >> "$nspawn_file"
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
# We use systemd-run to launch nspawn as a transient service.
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
if ! machinectl list | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# FIX: Use /usr/bin/env to set variables.
# machinectl shell interprets arguments as command + args.
sudo machinectl shell "agent@$name" \
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash
}
delete_container() {
local name=$1
local path="$CONTAINER_BASE/$name"
if [ ! -d "$path" ]; then
log_error "Not found."
exit 1
fi
echo -e "${RED}DANGER: Deleting $name${NC}"
echo -e "This will wipe: $path"
read -p "Type 'DELETE' to confirm: " confirm
if [ "$confirm" == "DELETE" ]; then
if machinectl list | grep -q "^$name "; then
sudo machinectl terminate "$name" 2>/dev/null || true
fi
sudo rm -rf "$path"
rm -f "$CONTAINER_CONFIG/$name.nspawn"
rm -f "$CONTAINER_CONFIG/.$name.pwd"
rm -f "$CONTAINER_CONFIG/.$name.meta"
log_info "Deleted."
else
log_info "Cancelled."
fi
}
list_containers() {
echo -e "${BLUE}CONTAINERS:${NC}"
for meta in "$CONTAINER_CONFIG"/.*.meta; do
[ -e "$meta" ] || continue
source "$meta"
local status="STOPPED"
if machinectl list 2>/dev/null | grep -q "^$NAME "; then
status="${GREEN}RUNNING${NC}"
fi
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE"
done
}
info_container() {
local name=$1
local meta="$CONTAINER_CONFIG/.$name.meta"
local pwd="$CONTAINER_CONFIG/.$name.pwd"
if [ ! -f "$meta" ]; then
log_error "No metadata found for $name"
exit 1
fi
source "$meta"
echo "--------------------------------"
echo "Container: $NAME"
echo "Created: $CREATED"
echo "Network: $NET_ENABLED"
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)"
echo "--------------------------------"
if [ -f "$pwd" ]; then
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}"
fi
echo "--------------------------------"
}
# --- Main Dispatch ---
check_dependencies
setup_directories
CMD="${1:-help}"
shift || true
case "$CMD" in
create) create_container "$@" ;;
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;;
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;;
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;;
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;;
list) list_containers ;;
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;;
*) show_help ;;
esac
```
if ! command -v $cmd &> /dev/null; then
missing+=($cmd)
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
log_info "Install with: sudo pacman -S arch-install-scripts systemd-container"
exit 1
fi
}
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
log_info "Container base: $CONTAINER_BASE"
}
create_container() {
local name=$1
local mount_src="${2:-}"
local mount_dst="${3:-/mnt/shared}"
local permission_arg="${4:-}"
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists"
exit 1
fi
# Determine Mount Mode
local mount_mode="ro"
if [ "$permission_arg" == "--rw" ]; then
mount_mode="rw"
fi
# Validate mount source if provided
if [ -n "$mount_src" ]; then
# Expand tilde to home directory
mount_src="${mount_src/#\~/$HOME}"
if [ ! -d "$mount_src" ]; then
log_error "Mount source directory does not exist: $mount_src"
exit 1
fi
if [ "$mount_mode" == "rw" ]; then
log_warn "Will mount (READ-WRITE): $mount_src -> $mount_dst"
else
log_info "Will mount (READ-ONLY): $mount_src -> $mount_dst"
fi
fi
log_info "Creating container '$name' at $container_path"
# Create container directory
mkdir -p "$container_path"
# Create mount point inside container
if [ -n "$mount_src" ]; then
mkdir -p "$container_path$mount_dst"
fi
# Bootstrap minimal Arch Linux
log_info "Bootstrapping Arch Linux (requires sudo for pacstrap)..."
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
log_info "Attempt $attempt of $max_attempts..."
if sudo pacstrap -c "$container_path" base base-devel python python-pip git helix go zig --noconfirm; then
log_info "Bootstrap completed successfully!"
break
else
if [ $attempt -eq $max_attempts ]; then
log_error "Failed to bootstrap. Try: sudo pacman-mirrors --fasttrack"
sudo rm -rf "$container_path"
exit 1
fi
sleep 3
attempt=$((attempt + 1))
fi
done
# Create configuration with specific mount mode
create_nspawn_config "$name" "$mount_src" "$mount_dst" "$mount_mode"
# Set hostname
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
# Setup networking
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
# Enable networkd
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
# Create user
log_info "Setting up 'agent' user..."
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -G wheel -s /bin/bash agent
echo "agent:agent" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "$container_path/etc/sudoers.d/wheel" > /dev/null
# Install Python packages
log_info "Installing Python packages..."
sudo systemd-nspawn -D "$container_path" --pipe \
bash -c "python -m pip install --break-system-packages mistral-vibe" || \
log_warn "Failed to install packages."
log_info "Container '$name' created!"
}
create_nspawn_config() {
local name=$1
local mount_src="${2:-}"
local mount_dst="${3:-}"
local mode="${4:-ro}"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
cat > "$config_file" <<EOF
[Exec]
Boot=yes
PrivateUsers=pick
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
EOF
# Add bind mount based on mode
if [ -n "$mount_src" ]; then
if [ "$mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$config_file"
log_warn "Configured READ-WRITE bind: $mount_src -> $mount_dst"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$config_file"
log_info "Configured READ-ONLY bind: $mount_src -> $mount_dst"
fi
fi
log_info "Created nspawn config: $config_file"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container '$name' does not exist"
exit 1
fi
log_info "Starting container '$name'..."
local bind_args=""
if [ -f "$config_file" ]; then
# Extract Bind= lines (Read-Write) -> convert to --bind=
local bind_rw=$(grep "^Bind=" "$config_file" | sed 's/^Bind=/--bind=/' | tr '\n' ' ')
# Extract BindReadOnly= lines (Read-Only) -> convert to --bind-ro=
local bind_ro=$(grep "^BindReadOnly=" "$config_file" | sed 's/^BindReadOnly=/--bind-ro=/' | tr '\n' ' ')
bind_args="$bind_rw $bind_ro"
fi
# Start container
sudo systemd-nspawn -D "$container_path" \
--machine="$name" \
--network-veth \
--private-users=pick \
--private-users-ownership=auto \
$bind_args \
--boot &
sleep 2
log_info "Container '$name' started"
}
shell_container() {
local name=$1
if ! machinectl list | grep -q "$name"; then
log_warn "Container '$name' is not running. Starting..."
start_container "$name"
sleep 3
fi
log_info "Opening shell in container '$name' as user 'agent'..."
sudo machinectl shell agent@$name /bin/bash
}
stop_container() {
local name=$1
log_info "Stopping container '$name'..."
sudo machinectl poweroff "$name" 2>/dev/null || true
sleep 2
log_info "Container '$name' stopped"
}
delete_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container '$name' does not exist"
exit 1
fi
# Check if container is running
if machinectl list 2>/dev/null | grep -q "$name"; then
log_warn "Container '$name' is running. Stopping it first..."
stop_container "$name"
fi
log_warn "About to delete container '$name'"
log_warn "Path: $container_path"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
log_info "Deletion cancelled"
exit 0
fi
log_info "Deleting container '$name' (requires sudo for root-owned files)..."
# Remove container directory (use sudo for root-owned files)
sudo rm -rf "$container_path"
# Remove config file (user-owned, no sudo needed)
if [ -f "$config_file" ]; then
rm -f "$config_file"
fi
log_info "Container '$name' deleted successfully"
}
list_containers() {
log_info "Available containers:"
if [ -d "$CONTAINER_BASE" ]; then
ls -1 "$CONTAINER_BASE" 2>/dev/null || echo " (none)"
else
echo " (none)"
fi
echo ""
log_info "Running containers:"
machinectl list --no-pager 2>/dev/null || echo " (none)"
}
case "${1:-}" in
create)
```bash /home/akh/bin/spawn.sh
#!/bin/bash
# spawn-sh - Production-Grade Secure Container Manager for AI Agents
#
set -euo pipefail
# --- Configuration ---
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}"
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}"
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}"
DEFAULT_CPU_QUOTA="50%"
DEFAULT_MEM_LIMIT="2G"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# --- Logging Helpers ---
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; }
# --- Dependency Check ---
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
exit 1
fi
}
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
mkdir -p "$CONTAINER_LOGS"
chmod 700 "$CONTAINER_LOGS"
}
# --- Parsing Logic ---
show_help() {
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
EOF
}
# --- Core Functions ---
create_container() {
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
NoNewPrivileges=yes
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
EOF
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file"
fi
fi
echo "Bind=/home/agent" >> "$nspawn_file"
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
# We use systemd-run to launch nspawn as a transient service.
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
if ! machinectl list | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# FIX: Use /usr/bin/env to set variables.
```bash /home/akh/bin/spawn.sh
#!/bin/bash
# spawn-sh - Production-Grade Secure Container Manager for AI Agents
#
set -euo pipefail
# --- Configuration ---
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}"
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}"
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}"
DEFAULT_CPU_QUOTA="50%"
DEFAULT_MEM_LIMIT="2G"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# --- Logging Helpers ---
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; }
# --- Dependency Check ---
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
exit 1
fi
}
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
mkdir -p "$CONTAINER_LOGS"
chmod 700 "$CONTAINER_LOGS"
}
# --- Parsing Logic ---
show_help() {
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
EOF
}
# --- Core Functions ---
create_container() {
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
EOF
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file"
fi
fi
echo "Bind=/home/agent" >> "$nspawn_file"
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
# We use systemd-run to launch nspawn as a transient service.
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
if ! machinectl list | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# FIX: Use /usr/bin/env to set variables.
# machinectl shell interprets arguments as command + args.
sudo machinectl shell "agent@$name" \
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash
}
delete_container() {
local name=$1
local path="$CONTAINER_BASE/$name"
if [ ! -d "$path" ]; then
log_error "Not found."
exit 1
fi
echo -e "${RED}DANGER: Deleting $name${NC}"
echo -e "This will wipe: $path"
read -p "Type 'DELETE' to confirm: " confirm
if [ "$confirm" == "DELETE" ]; then
if machinectl list | grep -q "^$name "; then
sudo machinectl terminate "$name" 2>/dev/null || true
fi
sudo rm -rf "$path"
rm -f "$CONTAINER_CONFIG/$name.nspawn"
rm -f "$CONTAINER_CONFIG/.$name.pwd"
rm -f "$CONTAINER_CONFIG/.$name.meta"
log_info "Deleted."
else
log_info "Cancelled."
fi
}
list_containers() {
echo -e "${BLUE}CONTAINERS:${NC}"
for meta in "$CONTAINER_CONFIG"/.*.meta; do
[ -e "$meta" ] || continue
source "$meta"
local status="STOPPED"
if machinectl list 2>/dev/null | grep -q "^$NAME "; then
status="${GREEN}RUNNING${NC}"
fi
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE"
done
}
info_container() {
local name=$1
local meta="$CONTAINER_CONFIG/.$name.meta"
local pwd="$CONTAINER_CONFIG/.$name.pwd"
```bash /home/akh/bin/spawn.sh
#!/bin/bash
# spawn-sh - Production-Grade Secure Container Manager for AI Agents
#
set -euo pipefail
# --- Configuration ---
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}"
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}"
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}"
DEFAULT_CPU_QUOTA="50%"
DEFAULT_MEM_LIMIT="2G"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# --- Logging Helpers ---
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; }
# --- Dependency Check ---
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
exit 1
fi
}
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
mkdir -p "$CONTAINER_LOGS"
chmod 700 "$CONTAINER_LOGS"
}
# --- Parsing Logic ---
show_help() {
cat << EOF
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options]
Commands:
create <name> [source] [dest] Create a secure container
--source, -s Host directory to mount (e.g., ~/projects)
--dest, -d Mount point inside container (default: /mnt/work)
--rw Mount as Read-Write (Default is Read-Only)
--cpu CPU Quota (default: 50%)
--memory Memory Limit (default: 2G)
--network Enable networking (Default: disabled/isolated)
--timeout Auto-shutdown in seconds (default: 3600)
--packages Extra packages to install (comma separated)
start <name> Start container
stop <name> Stop container
shell <name> Open secure shell
delete <name> Destroy container
list Show inventory
info <name> Show details & password
EOF
}
# --- Core Functions ---
create_container() {
local name="$1"
shift
# Defaults
local mount_src=""
local mount_dst=""
local mount_mode="ro"
local cpu_quota="$DEFAULT_CPU_QUOTA"
local mem_limit="$DEFAULT_MEM_LIMIT"
local enable_net="false"
local timeout="3600"
local extra_pkgs=""
# Argument Parsing (Hybrid: Flags or Positional)
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--source) mount_src="$2"; shift 2 ;;
-d|--dest) mount_dst="$2"; shift 2 ;;
--rw) mount_mode="rw"; shift ;;
--cpu) cpu_quota="$2"; shift 2 ;;
--memory) mem_limit="$2"; shift 2 ;;
--network) enable_net="true"; shift ;;
--timeout) timeout="$2"; shift 2 ;;
--packages) extra_pkgs="$2"; shift 2 ;;
-*) log_error "Unknown option: $1"; exit 1 ;;
*)
# Handle positional arguments: [source] [dest]
if [ -z "$mount_src" ]; then
mount_src="$1"
elif [ -z "$mount_dst" ]; then
mount_dst="$1"
else
log_error "Too many arguments. Unexpected: $1"
exit 1
fi
shift
;;
esac
done
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists."
exit 1
fi
# 1. Security Validation
if [ -n "$mount_src" ]; then
mount_src=$(realpath "$mount_src")
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root")
for blocked in "${blocked_paths[@]}"; do
if [[ "$mount_src" == "$blocked"* ]]; then
log_error "Security Block: Mounting '$mount_src' is forbidden."
exit 1
fi
done
if [ ! -d "$mount_src" ]; then
log_error "Source directory does not exist: $mount_src"
exit 1
fi
fi
log_info "Initializing container '$name'..."
mkdir -p "$container_path"
# 2. Bootstrap
log_info "Bootstrapping Arch Linux..."
local pkg_list="base python python-pip git ${extra_pkgs//,/ }"
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log"
sudo rm -rf "$container_path"
exit 1
fi
# 3. Generate Credentials
local agent_pass
agent_pass=$(openssl rand -base64 24)
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd"
chmod 600 "$CONTAINER_CONFIG/.$name.pwd"
# 4. Configure User & Network
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
if [ "$enable_net" == "true" ]; then
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
else
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network"
fi
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
if [[ "$pkg_list" == *"sudo"* ]]; then
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent
fi
# 5. Create Secure Config (.nspawn)
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn"
local mem_bytes
mem_bytes=$(numfmt --from=iec "$mem_limit")
cat > "$nspawn_file" <<EOF
[Exec]
Boot=yes
PrivateUsers=pick
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD
ProcessTwo=yes
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
ProtectSystem=strict
ProtectHome=read-only
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
LockPersonality=yes
[CPU]
CPUQuota=$cpu_quota
[Memory]
MemoryMax=$mem_bytes
EOF
if [ -n "$mount_src" ]; then
if [ "$mount_mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file"
fi
fi
echo "Bind=/home/agent" >> "$nspawn_file"
# 6. Auto-Shutdown Timer
setup_shutdown_timer "$container_path" "$timeout"
# 7. Metadata
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF
NAME=$name
CREATED=$(date -Iseconds)
MOUNT_SRC=$mount_src
MOUNT_MODE=$mount_mode
NET_ENABLED=$enable_net
EOF
log_sec "Container '$name' created."
log_info "Password saved to secure vault."
}
setup_shutdown_timer() {
local path="$1"
local timeout="$2"
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null
#!/bin/bash
sleep $timeout
echo "Time limit reached. Shutting down."
poweroff
EOF
sudo chmod +x "$path/usr/local/bin/auto-shutdown"
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null
[Unit]
Description=Security Timeout
After=multi-user.target
[Service]
ExecStart=/usr/local/bin/auto-shutdown
[Install]
WantedBy=multi-user.target
EOF
sudo ln -sf /etc/systemd/system/auto-shutdown.service \
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container path not found: $container_path"
exit 1
fi
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_warn "Container '$name' is already running."
return
fi
log_info "Starting $name..."
# 1. Config Bridge
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name
sudo mkdir -p /run/systemd/nspawn
if [ -f "$config_file" ]; then
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn"
fi
# 2. Start Service
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true
# We use systemd-run to launch nspawn as a transient service.
sudo systemd-run \
--unit="spawn-$name" \
--description="Spawn-Hybrid Container $name" \
systemd-nspawn --machine="$name" -D "$container_path" --boot
# 3. Verify
local attempts=0
while [ $attempts -lt 5 ]; do
sleep 1
if machinectl list 2>/dev/null | grep -q "^$name "; then
log_info "Container booted successfully."
return
fi
attempts=$((attempts+1))
done
log_warn "Container startup queued. Check status with: systemctl status spawn-$name"
}
stop_container() {
local name=$1
log_info "Stopping $name..."
sudo machinectl poweroff "$name"
# Cleanup config link
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then
sudo rm "/run/systemd/nspawn/$name.nspawn"
fi
}
shell_container() {
local name=$1
if ! machinectl list | grep -q "^$name "; then
start_container "$name"
sleep 2
fi
log_sec "Entering sandbox as user 'agent'..."
# FIX: Use /usr/bin/env to set variables.
# machinectl shell interprets arguments as command + args.
sudo machinectl shell "agent@$name" \
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash
}
delete_container() {
local name=$1
local path="$CONTAINER_BASE/$name"
if [ ! -d "$path" ]; then
log_error "Not found."
exit 1
fi
echo -e "${RED}DANGER: Deleting $name${NC}"
echo -e "This will wipe: $path"
read -p "Type 'DELETE' to confirm: " confirm
if [ "$confirm" == "DELETE" ]; then
if machinectl list | grep -q "^$name "; then
sudo machinectl terminate "$name" 2>/dev/null || true
fi
sudo rm -rf "$path"
rm -f "$CONTAINER_CONFIG/$name.nspawn"
rm -f "$CONTAINER_CONFIG/.$name.pwd"
rm -f "$CONTAINER_CONFIG/.$name.meta"
log_info "Deleted."
else
log_info "Cancelled."
fi
}
list_containers() {
echo -e "${BLUE}CONTAINERS:${NC}"
for meta in "$CONTAINER_CONFIG"/.*.meta; do
[ -e "$meta" ] || continue
source "$meta"
local status="STOPPED"
if machinectl list 2>/dev/null | grep -q "^$NAME "; then
status="${GREEN}RUNNING${NC}"
fi
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE"
done
}
info_container() {
local name=$1
local meta="$CONTAINER_CONFIG/.$name.meta"
local pwd="$CONTAINER_CONFIG/.$name.pwd"
if [ ! -f "$meta" ]; then
log_error "No metadata found for $name"
exit 1
fi
source "$meta"
echo "--------------------------------"
echo "Container: $NAME"
echo "Created: $CREATED"
echo "Network: $NET_ENABLED"
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)"
echo "--------------------------------"
if [ -f "$pwd" ]; then
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}"
fi
echo "--------------------------------"
}
# --- Main Dispatch ---
check_dependencies
setup_directories
CMD="${1:-help}"
shift || true
case "$CMD" in
create) create_container "$@" ;;
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;;
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;;
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;;
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;;
list) list_containers ;;
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;;
*) show_help ;;
esac
echo " create <name> - Create a new container"
echo " start <name> - Start a container"
echo " shell <name> - Open shell in container"
echo " stop <name> - Stop a container"
echo " list - List all containers"
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment