Files
fireclaw/scripts/install.sh
ansible e6a6fb263d Use dynamic path for fireclaw in overseer service file
$(which fireclaw) instead of hardcoded /usr/local/bin/fireclaw.
Fixes 203/EXEC on systems where npm link installs to /usr/bin/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:09:10 +00:00

466 lines
16 KiB
Bash
Executable File

#!/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..."
# Clean up any stale mounts from previous failed runs
sudo umount /tmp/agent-build-mnt 2>/dev/null || true
sudo umount /tmp/fireclaw-alpine 2>/dev/null || true
rm -rf /tmp/agent-build-mnt /tmp/agent-build.ext4 2>/dev/null || true
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"
# Copy host DNS so chroot can resolve packages
sudo cp /etc/resolv.conf /tmp/agent-build-mnt/etc/resolv.conf
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=$(which 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 ""