#!/bin/bash # Fireclaw install script # Installs everything needed to run fireclaw on a fresh Linux machine. # Requires: root or sudo access, KVM support, internet access. # # Usage: ./scripts/install.sh [--with-gpu] set -euo pipefail WITH_GPU=false [[ "${1:-}" == "--with-gpu" ]] && WITH_GPU=true log() { echo -e "\033[1;34m[fireclaw]\033[0m $*"; } step() { echo -e "\n\033[1;32m━━━ $* ━━━\033[0m"; } ok() { echo -e " \033[0;32m✓\033[0m $*"; } skip() { echo -e " \033[0;33m→\033[0m $* (already installed)"; } err() { echo -e "\033[1;31m[error]\033[0m $*" >&2; exit 1; } echo "" echo " ╔═══════════════════════════════════════╗" echo " ║ Fireclaw Installer v0.1.2 ║" echo " ║ Multi-agent Firecracker system ║" echo " ╚═══════════════════════════════════════╝" echo "" # ─── Preflight checks ──────────────────────────────────────────────── step "Preflight checks" [[ $(uname) != "Linux" ]] && err "Linux required." ok "Linux detected: $(uname -r)" [[ ! -e /dev/kvm ]] && err "KVM not available. Enable virtualization in BIOS." ok "KVM available" if groups | grep -qw kvm; then ok "User $(whoami) in kvm group" else log "Adding $(whoami) to kvm group (re-login required after install)..." sudo usermod -aG kvm "$(whoami)" ok "Added to kvm group" fi # ─── System packages ───────────────────────────────────────────────── step "System packages" if command -v apt-get &>/dev/null; then log "Updating apt..." sudo apt-get update -qq for pkg in curl jq git ngircd e2fsprogs; do if dpkg -l "$pkg" &>/dev/null; then skip "$pkg" else log "Installing $pkg..." sudo apt-get install -y -qq "$pkg" >/dev/null ok "$pkg installed" fi done elif command -v dnf &>/dev/null; then for pkg in curl jq git ngircd e2fsprogs; do if rpm -q "$pkg" &>/dev/null; then skip "$pkg" else log "Installing $pkg..." sudo dnf install -y -q "$pkg" ok "$pkg installed" fi done elif command -v apk &>/dev/null; then sudo apk add --no-cache curl jq git ngircd e2fsprogs ok "Packages installed" else err "Unsupported package manager. Install manually: curl, jq, git, ngircd, e2fsprogs" fi # ─── Node.js ────────────────────────────────────────────────────────── step "Node.js" NODE_VER=0 if command -v node &>/dev/null; then NODE_VER=$(node -v | cut -d. -f1 | tr -d v) fi if [[ $NODE_VER -ge 20 ]]; then skip "Node.js $(node -v)" else log "Installing Node.js 20 (current: ${NODE_VER:-not installed})..." if command -v apt-get &>/dev/null; then curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - >/dev/null 2>&1 sudo apt-get install -y -qq nodejs >/dev/null elif command -v dnf &>/dev/null; then curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - >/dev/null 2>&1 sudo dnf install -y -q nodejs elif command -v apk &>/dev/null; then sudo apk add --no-cache nodejs npm else err "Cannot install Node.js 20+. Install it manually." fi NODE_VER=$(node -v | cut -d. -f1 | tr -d v) [[ $NODE_VER -lt 20 ]] && err "Node.js 20+ required, found $(node -v). Install manually." ok "Node.js $(node -v) installed" fi # ─── Firecracker ────────────────────────────────────────────────────── step "Firecracker" if command -v firecracker &>/dev/null; then skip "Firecracker $(firecracker --version 2>&1 | head -1)" else ARCH=$(uname -m) log "Fetching latest Firecracker release..." FC_VERSION=$(curl -fsSL https://api.github.com/repos/firecracker-microvm/firecracker/releases/latest | jq -r .tag_name) log "Downloading Firecracker ${FC_VERSION} (${ARCH})..." curl -fSL -o /tmp/firecracker.tgz \ "https://github.com/firecracker-microvm/firecracker/releases/download/${FC_VERSION}/firecracker-${FC_VERSION}-${ARCH}.tgz" log "Extracting..." tar xzf /tmp/firecracker.tgz -C /tmp sudo cp "/tmp/release-${FC_VERSION}-${ARCH}/firecracker-${FC_VERSION}-${ARCH}" /usr/local/bin/firecracker sudo cp "/tmp/release-${FC_VERSION}-${ARCH}/jailer-${FC_VERSION}-${ARCH}" /usr/local/bin/jailer rm -rf /tmp/firecracker.tgz "/tmp/release-${FC_VERSION}-${ARCH}" ok "Firecracker $(firecracker --version 2>&1 | head -1) installed" fi # ─── Ollama ─────────────────────────────────────────────────────────── step "Ollama" if command -v ollama &>/dev/null; then skip "Ollama $(ollama --version 2>&1 | grep -oP '[\d.]+' | head -1)" else log "Installing Ollama..." curl -fsSL https://ollama.com/install.sh | sh 2>&1 | tail -3 || true ok "Ollama installed" fi log "Configuring Ollama systemd service..." sudo tee /etc/systemd/system/ollama.service > /dev/null << EOF [Unit] Description=Ollama LLM Server After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/ollama serve User=$(whoami) Group=$(id -gn) Restart=always RestartSec=3 Environment="OLLAMA_HOST=0.0.0.0" [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now ollama >/dev/null 2>&1 sleep 2 ok "Ollama service running on 0.0.0.0:11434" log "Pulling default model: qwen2.5-coder:7b (this may take a few minutes)..." ollama pull qwen2.5-coder:7b 2>/dev/null || true ok "qwen2.5-coder:7b ready" if $WITH_GPU; then log "GPU mode: pulling larger models..." log " Pulling qwen2.5:14b..." ollama pull qwen2.5:14b 2>/dev/null || true ok "qwen2.5:14b ready" log " Pulling qwen2.5-coder:14b..." ollama pull qwen2.5-coder:14b 2>/dev/null || true ok "qwen2.5-coder:14b ready" fi # ─── ngircd ─────────────────────────────────────────────────────────── step "IRC Server (ngircd)" log "Backing up existing config..." [[ -f /etc/ngircd/ngircd.conf ]] && sudo cp /etc/ngircd/ngircd.conf /etc/ngircd/ngircd.conf.bak 2>/dev/null || true log "Writing fireclaw IRC config..." sudo tee /etc/ngircd/ngircd.conf > /dev/null << EOF [Global] Name = nyx.fireclaw.local AdminInfo1 = fireclaw AdminEMail = admin@localhost Info = nyx - fireclaw agent network Listen = 127.0.0.1,172.16.0.1 Network = FireclawNet PidFile = /run/ngircd/ngircd.pid ServerGID = irc ServerUID = irc [Limits] ConnectRetry = 60 MaxConnections = 100 MaxConnectionsIP = 20 MaxJoins = 20 PingTimeout = 120 PongTimeout = 20 [Options] DNS = no Ident = no PAM = no OperCanUseMode = yes DefaultUserModes = CiFo SyslogFacility = daemon [Operator] Name = admin Password = fireclaw-oper [Channel] Name = #control Topic = Overseer command channel Modes = +tn [Channel] Name = #agents Topic = Agent common room Modes = +tnQN EOF sudo systemctl enable --now ngircd >/dev/null 2>&1 sudo systemctl restart ngircd ok "ngircd running as nyx.fireclaw.local" # ─── Fireclaw ───────────────────────────────────────────────────────── step "Fireclaw" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" log "Installing npm dependencies..." cd "$SCRIPT_DIR" npm install || err "npm install failed" ok "Dependencies installed" log "Building TypeScript..." npm run build || err "npm run build failed" ok "Build complete" log "Linking global command..." sudo npm link || err "npm link failed" ok "fireclaw command available globally" log "Running fireclaw setup (kernel + base rootfs + bridge + SSH keys)..." fireclaw setup ok "Base setup complete" # ─── Agent rootfs ───────────────────────────────────────────────────── step "Agent rootfs" FIRECLAW_DIR="$HOME/.fireclaw" if [[ -f "$FIRECLAW_DIR/agent-rootfs.ext4" ]]; then skip "Agent rootfs" else log "Creating 1G agent rootfs from Alpine base..." cp "$FIRECLAW_DIR/base-rootfs.ext4" /tmp/agent-build.ext4 truncate -s 1G /tmp/agent-build.ext4 log " Checking filesystem..." sudo e2fsck -fy /tmp/agent-build.ext4 || err "e2fsck failed. Is e2fsprogs installed?" log " Resizing to 1G..." sudo resize2fs /tmp/agent-build.ext4 || err "resize2fs failed." ok "Image resized to 1G" mkdir -p /tmp/agent-build-mnt sudo mount /tmp/agent-build.ext4 /tmp/agent-build-mnt || err "Failed to mount agent rootfs" log "Installing Alpine packages (openssh, python3, podman, curl, jq, bash)..." log " This may take a minute..." sudo chroot /tmp/agent-build-mnt sh -c ' set -e apk update apk add --no-cache openssh-server ca-certificates curl jq python3 bash openrc podman iptables rc-update add sshd default rc-update add cgroups boot ssh-keygen -A echo "PermitRootLogin prohibit-password" >> /etc/ssh/sshd_config adduser -D -h /home/agent -s /bin/bash agent echo "root:100000:65536" > /etc/subuid echo "root:100000:65536" > /etc/subgid echo "agent:100000:65536" >> /etc/subuid echo "agent:100000:65536" >> /etc/subgid mkdir -p /etc/containers echo "[containers]" > /etc/containers/containers.conf echo "netns = \"host\"" >> /etc/containers/containers.conf echo "[storage]" > /etc/containers/storage.conf echo "driver = \"vfs\"" >> /etc/containers/storage.conf ' || err "Failed to install packages in chroot" ok "Alpine packages installed" log "Installing agent script and config..." sudo mkdir -p /tmp/agent-build-mnt/opt/agent /tmp/agent-build-mnt/etc/agent sudo cp "$SCRIPT_DIR/agent/agent.py" /tmp/agent-build-mnt/opt/agent/agent.py sudo chmod +x /tmp/agent-build-mnt/opt/agent/agent.py echo '{"nick":"agent","model":"qwen2.5-coder:7b","trigger":"mention","server":"172.16.0.1","port":6667,"ollama_url":"http://172.16.0.1:11434"}' | \ sudo tee /tmp/agent-build-mnt/etc/agent/config.json > /dev/null echo "You are a helpful assistant on IRC." | \ sudo tee /tmp/agent-build-mnt/etc/agent/persona.md > /dev/null ok "Agent script installed" log "Configuring init system..." sudo tee /tmp/agent-build-mnt/etc/inittab > /dev/null << 'INITTAB' ::sysinit:/sbin/openrc sysinit ::sysinit:/sbin/openrc boot ::sysinit:/sbin/openrc default ttyS0::respawn:/sbin/getty -L 115200 ttyS0 vt100 ::respawn:/bin/su -s /bin/sh agent -c "/usr/bin/python3 /opt/agent/agent.py" ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/openrc shutdown INITTAB sudo tee /tmp/agent-build-mnt/etc/init.d/podman-setup > /dev/null << 'SVC' #!/sbin/openrc-run description="Set up podman prerequisites" depend() { before sshd; after localmount; } start() { mkdir -p /sys/fs/cgroup /dev/shm mount -t cgroup2 cgroup2 /sys/fs/cgroup 2>/dev/null mount -t tmpfs tmpfs /dev/shm 2>/dev/null return 0 } SVC sudo chmod +x /tmp/agent-build-mnt/etc/init.d/podman-setup sudo chroot /tmp/agent-build-mnt rc-update add podman-setup boot 2>/dev/null sudo tee /tmp/agent-build-mnt/etc/init.d/workspace > /dev/null << 'SVC' #!/sbin/openrc-run description="Mount agent workspace" depend() { need localmount; before sshd; } start() { mkdir -p /workspace if [ -b /dev/vdb ]; then mount /dev/vdb /workspace chown -R agent:agent /workspace einfo "Workspace mounted at /workspace" fi return 0 } stop() { umount /workspace 2>/dev/null; return 0; } SVC sudo chmod +x /tmp/agent-build-mnt/etc/init.d/workspace sudo chroot /tmp/agent-build-mnt rc-update add workspace boot 2>/dev/null sudo tee /tmp/agent-build-mnt/etc/init.d/networking > /dev/null << 'SVC' #!/sbin/openrc-run depend() { need localmount; } start() { ip link set lo up; return 0; } SVC sudo chmod +x /tmp/agent-build-mnt/etc/init.d/networking sudo chroot /tmp/agent-build-mnt rc-update add networking boot 2>/dev/null ok "Init services configured" sudo umount /tmp/agent-build-mnt rmdir /tmp/agent-build-mnt sudo mv /tmp/agent-build.ext4 "$FIRECLAW_DIR/agent-rootfs.ext4" ok "Agent rootfs built ($(du -sh "$FIRECLAW_DIR/agent-rootfs.ext4" | cut -f1) on disk)" fi # ─── Snapshot ───────────────────────────────────────────────────────── step "VM Snapshot" if [[ -f "$FIRECLAW_DIR/snapshot.state" ]]; then skip "Snapshot" else log "Creating VM snapshot for fast restores..." fireclaw snapshot create ok "Snapshot created" fi # ─── Overseer service ──────────────────────────────────────────────── step "Overseer service" log "Configuring systemd service..." sudo tee /etc/systemd/system/fireclaw-overseer.service > /dev/null << EOF [Unit] Description=Fireclaw Overseer — IRC agent lifecycle manager After=network-online.target ngircd.service ollama.service Wants=network-online.target [Service] ExecStart=/usr/local/bin/fireclaw overseer User=$(whoami) Group=$(id -gn) Restart=always RestartSec=5 KillMode=process WorkingDirectory=$SCRIPT_DIR [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now fireclaw-overseer >/dev/null 2>&1 ok "Overseer running in #control" # ─── Templates ──────────────────────────────────────────────────────── step "Agent templates" TMPL_DIR="$FIRECLAW_DIR/templates" mkdir -p "$TMPL_DIR" for tmpl in worker coder quick; do if [[ -f "$TMPL_DIR/$tmpl.json" ]]; then skip "$tmpl" else case $tmpl in worker) echo '{"name":"worker","nick":"worker","model":"qwen2.5-coder:7b","trigger":"mention","persona":"You are a general-purpose assistant on IRC. Keep responses concise."}' > "$TMPL_DIR/$tmpl.json" ;; coder) echo '{"name":"coder","nick":"coder","model":"qwen2.5-coder:7b","trigger":"mention","persona":"You are a code-focused assistant on IRC. Be direct and technical."}' > "$TMPL_DIR/$tmpl.json" ;; quick) echo '{"name":"quick","nick":"quick","model":"phi4-mini","trigger":"mention","tools":false,"network":"none","persona":"You are a fast assistant on IRC. One sentence answers."}' > "$TMPL_DIR/$tmpl.json" ;; esac ok "$tmpl template created" fi done # ─── Done ───────────────────────────────────────────────────────────── echo "" echo " ╔═══════════════════════════════════════╗" echo " ║ Installation complete! ║" echo " ╚═══════════════════════════════════════╝" echo "" log "Services:" log " ngircd nyx.fireclaw.local :6667" log " ollama 0.0.0.0:11434" log " overseer IRC #control" echo "" log "Quick start:" log " irssi -c localhost -n human" log " /join #control" log " !invoke worker" echo "" log "CLI:" log " fireclaw run \"uname -a\"" log " fireclaw agent list" log " fireclaw --help" echo "" if $WITH_GPU; then log "GPU mode: larger models pulled (14b)" fi log "Disk usage: $(du -sh "$FIRECLAW_DIR" | cut -f1) in $FIRECLAW_DIR" log "Models: $(ollama list 2>/dev/null | tail -n +2 | wc -l) available" echo ""