#!/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 $*"; } err() { echo -e "\033[1;31m[error]\033[0m $*" >&2; exit 1; } # ─── Preflight checks ──────────────────────────────────────────────── log "Running preflight checks..." [[ $(uname) != "Linux" ]] && err "Linux required." [[ ! -e /dev/kvm ]] && err "KVM not available. Enable virtualization in BIOS." if ! groups | grep -qw kvm; then log "Adding $(whoami) to kvm group (re-login required after install)..." sudo usermod -aG kvm "$(whoami)" fi # ─── System packages ───────────────────────────────────────────────── log "Installing system packages..." if command -v apt-get &>/dev/null; then sudo apt-get update -qq sudo apt-get install -y -qq curl jq git ngircd >/dev/null elif command -v dnf &>/dev/null; then sudo dnf install -y -q curl jq git ngircd elif command -v apk &>/dev/null; then sudo apk add --no-cache curl jq git ngircd else err "Unsupported package manager. Install manually: curl, jq, git, ngircd" fi # ─── 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 -lt 20 ]]; then log "Installing Node.js 20 (found: ${NODE_VER:-none})..." 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 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." log "Node.js $(node -v) OK" # ─── Firecracker ────────────────────────────────────────────────────── if ! command -v firecracker &>/dev/null; then log "Installing Firecracker..." ARCH=$(uname -m) FC_VERSION=$(curl -fsSL https://api.github.com/repos/firecracker-microvm/firecracker/releases/latest | jq -r .tag_name) curl -fSL -o /tmp/firecracker.tgz \ "https://github.com/firecracker-microvm/firecracker/releases/download/${FC_VERSION}/firecracker-${FC_VERSION}-${ARCH}.tgz" 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}" log "Firecracker $(firecracker --version 2>&1 | head -1) installed" else log "Firecracker already installed: $(firecracker --version 2>&1 | head -1)" fi # ─── Ollama ─────────────────────────────────────────────────────────── if ! command -v ollama &>/dev/null; then log "Installing Ollama..." curl -fsSL https://ollama.com/install.sh | sh >/dev/null 2>&1 || true fi # Configure Ollama service log "Configuring Ollama 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 # Pull default model log "Pulling default model (qwen2.5-coder:7b)..." ollama pull qwen2.5-coder:7b 2>/dev/null || true if $WITH_GPU; then log "GPU mode: pulling larger models..." ollama pull qwen2.5:14b 2>/dev/null || true ollama pull qwen2.5-coder:14b 2>/dev/null || true fi # ─── ngircd ─────────────────────────────────────────────────────────── log "Configuring ngircd..." HOSTNAME=$(hostname -s) 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 log "ngircd configured as nyx.fireclaw.local" # ─── Fireclaw ───────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" log "Installing fireclaw from ${SCRIPT_DIR}..." cd "$SCRIPT_DIR" npm install --silent 2>/dev/null npm run build 2>/dev/null sudo npm link 2>/dev/null # Run fireclaw setup (kernel, rootfs, bridge, SSH keys) log "Running fireclaw setup..." fireclaw setup # ─── Agent rootfs ───────────────────────────────────────────────────── FIRECLAW_DIR="$HOME/.fireclaw" if [[ ! -f "$FIRECLAW_DIR/agent-rootfs.ext4" ]]; then log "Building agent rootfs..." cp "$FIRECLAW_DIR/base-rootfs.ext4" /tmp/agent-build.ext4 truncate -s 1G /tmp/agent-build.ext4 sudo /usr/sbin/e2fsck -fy /tmp/agent-build.ext4 >/dev/null 2>&1 sudo /usr/sbin/resize2fs /tmp/agent-build.ext4 >/dev/null 2>&1 mkdir -p /tmp/agent-build-mnt sudo mount /tmp/agent-build.ext4 /tmp/agent-build-mnt # Install packages sudo chroot /tmp/agent-build-mnt sh -c ' apk update >/dev/null 2>&1 apk add --no-cache openssh-server ca-certificates curl jq python3 bash openrc podman iptables >/dev/null 2>&1 rc-update add sshd default 2>/dev/null rc-update add cgroups boot 2>/dev/null ssh-keygen -A 2>/dev/null 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 ' # Install agent script 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 # Default config 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 # Inittab — auto-start agent as non-root 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 # Boot services for podman + workspace 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 # Networking init 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 sudo umount /tmp/agent-build-mnt rmdir /tmp/agent-build-mnt sudo mv /tmp/agent-build.ext4 "$FIRECLAW_DIR/agent-rootfs.ext4" log "Agent rootfs built" else log "Agent rootfs already exists" fi # ─── Snapshot ───────────────────────────────────────────────────────── if [[ ! -f "$FIRECLAW_DIR/snapshot.state" ]]; then log "Creating VM snapshot for fast restores..." fireclaw snapshot create fi # ─── Overseer service ──────────────────────────────────────────────── log "Configuring overseer 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 log "Overseer service started" # ─── Templates ──────────────────────────────────────────────────────── TMPL_DIR="$FIRECLAW_DIR/templates" mkdir -p "$TMPL_DIR" [[ ! -f "$TMPL_DIR/worker.json" ]] && cat > "$TMPL_DIR/worker.json" << 'EOF' {"name":"worker","nick":"worker","model":"qwen2.5-coder:7b","trigger":"mention","persona":"You are a general-purpose assistant on IRC. Keep responses concise."} EOF [[ ! -f "$TMPL_DIR/coder.json" ]] && cat > "$TMPL_DIR/coder.json" << 'EOF' {"name":"coder","nick":"coder","model":"qwen2.5-coder:7b","trigger":"mention","persona":"You are a code-focused assistant on IRC. Be direct and technical."} EOF [[ ! -f "$TMPL_DIR/quick.json" ]] && cat > "$TMPL_DIR/quick.json" << 'EOF' {"name":"quick","nick":"quick","model":"phi4-mini","trigger":"mention","tools":false,"network":"none","persona":"You are a fast assistant on IRC. One sentence answers."} EOF log "Templates installed" # ─── Done ───────────────────────────────────────────────────────────── echo "" log "═══════════════════════════════════════════════" log " Fireclaw installed successfully!" log "═══════════════════════════════════════════════" log "" log " Services running:" log " ngircd nyx.fireclaw.local :6667" log " ollama 0.0.0.0:11434" log " overseer IRC #control" log "" log " Connect to IRC:" log " irssi -c localhost -n human" log " /join #control" log " !invoke worker" log "" log " Commands:" log " fireclaw run \"uname -a\" Run in ephemeral VM" log " fireclaw agent list List running agents" log " fireclaw --help Full help" log "" if $WITH_GPU; then log " GPU mode enabled — larger models available" fi log "═══════════════════════════════════════════════"