diff --git a/src/agent-manager.ts b/src/agent-manager.ts index 3a097ef..c938890 100644 --- a/src/agent-manager.ts +++ b/src/agent-manager.ts @@ -1,4 +1,4 @@ -import { spawn, type ChildProcess } from "node:child_process"; +import { spawn } from "node:child_process"; import { existsSync, mkdirSync, diff --git a/src/config.ts b/src/config.ts index ce67f1b..3e6c532 100644 --- a/src/config.ts +++ b/src/config.ts @@ -46,12 +46,8 @@ export const CONFIG = { workspacesDir: join(HOME, ".fireclaw", "workspaces"), workspaceSizeMib: 64, - // S3 URLs for Firecracker CI assets assets: { kernelUrl: "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.11/x86_64/vmlinux-5.10.225", - rootfsListUrl: - "http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/v1.11/x86_64/ubuntu", - rootfsBaseUrl: "https://s3.amazonaws.com/spec.ccfc.min", }, } as const; diff --git a/src/overseer.ts b/src/overseer.ts index 8223a17..e7385a1 100644 --- a/src/overseer.ts +++ b/src/overseer.ts @@ -147,7 +147,7 @@ export async function runOverseer(config: OverseerConfig) { ); bot.say(event.target, `Models: ${lines.join(", ")}`); } - } catch (e) { + } catch { bot.say(event.target, "Error fetching models from Ollama."); } break; @@ -189,7 +189,7 @@ export async function runOverseer(config: OverseerConfig) { } catch {} bot.say(event.target, `Agents: ${agents.length} running | Load: ${load} | RAM: ${freeMem}/${totalMem} GB free | Disk: ${diskFree} | Uptime: ${uptime}h | Ollama: ${ollamaModel}`); - } catch (e) { + } catch { bot.say(event.target, "Error getting status."); } break; diff --git a/src/setup.ts b/src/setup.ts index 8849f73..ca44f3a 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -36,70 +36,104 @@ export async function runSetup() { if (existsSync(CONFIG.baseRootfs)) { log("Base rootfs already exists, skipping download."); } else { - log("Downloading rootfs..."); + log("Downloading Alpine Linux minirootfs..."); - // Find latest rootfs key from S3 listing + const arch = execFileSync("uname", ["-m"], { encoding: "utf-8" }).trim(); + const alpineTar = `${CONFIG.baseDir}/alpine-minirootfs.tar.gz`; + + // Find latest Alpine version const listing = execFileSync( "curl", - ["-fsSL", CONFIG.assets.rootfsListUrl], + ["-fsSL", "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/" + arch + "/"], { encoding: "utf-8", timeout: 30_000 } ); - const keys = [...listing.matchAll(/([^<]+)<\/Key>/g)].map( - (m) => m[1] + const match = listing.match( + new RegExp(`alpine-minirootfs-[\\d.]+-${arch}\\.tar\\.gz`, "g") ); - const rootfsKey = keys.sort().pop(); - if (!rootfsKey) throw new Error("Could not find rootfs in S3 listing"); + if (!match || match.length === 0) + throw new Error("Could not find Alpine minirootfs"); + const filename = match.sort().pop()!; - const squashfsPath = `${CONFIG.baseDir}/rootfs.squashfs`; - download(`${CONFIG.assets.rootfsBaseUrl}/${rootfsKey}`, squashfsPath); - log("Rootfs downloaded. Converting squashfs to ext4..."); + download( + `https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/${arch}/${filename}`, + alpineTar + ); + log("Alpine downloaded. Building ext4 rootfs..."); - // Convert squashfs to ext4 - const squashMount = "/tmp/fireclaw-squash"; - const ext4Mount = "/tmp/fireclaw-ext4"; - mkdirSync(squashMount, { recursive: true }); + // Create ext4 image and unpack Alpine + const ext4Mount = "/tmp/fireclaw-alpine"; mkdirSync(ext4Mount, { recursive: true }); try { - execFileSync( - "sudo", - ["mount", "-t", "squashfs", squashfsPath, squashMount], - { stdio: "pipe" } - ); - execFileSync("truncate", ["-s", "1G", CONFIG.baseRootfs], { + execFileSync("truncate", ["-s", "256M", CONFIG.baseRootfs], { stdio: "pipe", }); - execFileSync("sudo", ["/usr/sbin/mkfs.ext4", CONFIG.baseRootfs], { + execFileSync("sudo", ["/usr/sbin/mkfs.ext4", "-q", CONFIG.baseRootfs], { stdio: "pipe", }); execFileSync("sudo", ["mount", CONFIG.baseRootfs, ext4Mount], { stdio: "pipe", }); - execFileSync("sudo", ["cp", "-a", `${squashMount}/.`, ext4Mount], { + execFileSync("sudo", ["tar", "xzf", alpineTar, "-C", ext4Mount], { stdio: "pipe", }); - // Bake in DNS config + // DNS execSync( `echo "nameserver 8.8.8.8" | sudo tee ${ext4Mount}/etc/resolv.conf > /dev/null` ); - log("Rootfs converted."); + // Inittab for serial console + execSync(`sudo tee ${ext4Mount}/etc/inittab > /dev/null << 'EOF' +::sysinit:/sbin/openrc sysinit +::sysinit:/sbin/openrc boot +::sysinit:/sbin/openrc default +ttyS0::respawn:/sbin/getty -L 115200 ttyS0 vt100 +::ctrlaltdel:/sbin/reboot +::shutdown:/sbin/openrc shutdown +EOF`); + + // Hostname + execSync( + `echo "fireclaw" | sudo tee ${ext4Mount}/etc/hostname > /dev/null` + ); + + // Allow root login (no password, SSH key auth only) + execSync( + `sudo sed -i 's/root:x:/root::/' ${ext4Mount}/etc/passwd` + ); + + // Install base packages + log("Installing Alpine packages (openssh, python3, curl, ca-certificates)..."); + execFileSync( + "sudo", + ["chroot", ext4Mount, "/bin/sh", "-c", + "apk update && apk add --no-cache openssh-server ca-certificates curl jq python3 bash openrc && " + + "rc-update add sshd default && ssh-keygen -A && " + + "echo 'PermitRootLogin prohibit-password' >> /etc/ssh/sshd_config && " + + "mkdir -p /run/openrc && touch /run/openrc/softlevel"], + { stdio: "pipe", timeout: 120_000 } + ); + + // Networking init script + execSync(`sudo tee ${ext4Mount}/etc/init.d/networking > /dev/null << 'EOF' +#!/sbin/openrc-run +depend() { need localmount; } +start() { ip link set lo up; return 0; } +EOF`); + execFileSync("sudo", ["chmod", "+x", `${ext4Mount}/etc/init.d/networking`], { stdio: "pipe" }); + execFileSync("sudo", ["chroot", ext4Mount, "rc-update", "add", "networking", "boot"], { stdio: "pipe" }); + + log("Alpine rootfs built."); } finally { - try { - execFileSync("sudo", ["umount", squashMount], { stdio: "pipe" }); - } catch {} try { execFileSync("sudo", ["umount", ext4Mount], { stdio: "pipe" }); } catch {} - try { - execFileSync("rmdir", [squashMount], { stdio: "pipe" }); - } catch {} try { execFileSync("rmdir", [ext4Mount], { stdio: "pipe" }); } catch {} try { - execFileSync("rm", ["-f", squashfsPath], { stdio: "pipe" }); + execFileSync("rm", ["-f", alpineTar], { stdio: "pipe" }); } catch {} } } diff --git a/src/snapshot.ts b/src/snapshot.ts index 6edbc27..2069a2d 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -13,7 +13,6 @@ import { import { ensureBaseImage, ensureSshKeypair, - createRunCopy, injectSshKey, } from "./rootfs.js"; import { waitForSsh } from "./ssh.js";