Switch setup.ts to Alpine rootfs, fix remote deployment
- setup.ts now downloads Alpine Linux minirootfs instead of Ubuntu squashfs - Installs Alpine packages (openssh, python3, curl, ca-certificates) in chroot - Fixes install script failing on non-Alpine base rootfs (adduser syntax) - Clean up unused imports and lint warnings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { spawn, type ChildProcess } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import {
|
import {
|
||||||
existsSync,
|
existsSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
|
|||||||
@@ -46,12 +46,8 @@ export const CONFIG = {
|
|||||||
workspacesDir: join(HOME, ".fireclaw", "workspaces"),
|
workspacesDir: join(HOME, ".fireclaw", "workspaces"),
|
||||||
workspaceSizeMib: 64,
|
workspaceSizeMib: 64,
|
||||||
|
|
||||||
// S3 URLs for Firecracker CI assets
|
|
||||||
assets: {
|
assets: {
|
||||||
kernelUrl:
|
kernelUrl:
|
||||||
"https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.11/x86_64/vmlinux-5.10.225",
|
"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;
|
} as const;
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export async function runOverseer(config: OverseerConfig) {
|
|||||||
);
|
);
|
||||||
bot.say(event.target, `Models: ${lines.join(", ")}`);
|
bot.say(event.target, `Models: ${lines.join(", ")}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
bot.say(event.target, "Error fetching models from Ollama.");
|
bot.say(event.target, "Error fetching models from Ollama.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -189,7 +189,7 @@ export async function runOverseer(config: OverseerConfig) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
bot.say(event.target, `Agents: ${agents.length} running | Load: ${load} | RAM: ${freeMem}/${totalMem} GB free | Disk: ${diskFree} | Uptime: ${uptime}h | Ollama: ${ollamaModel}`);
|
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.");
|
bot.say(event.target, "Error getting status.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
96
src/setup.ts
96
src/setup.ts
@@ -36,70 +36,104 @@ export async function runSetup() {
|
|||||||
if (existsSync(CONFIG.baseRootfs)) {
|
if (existsSync(CONFIG.baseRootfs)) {
|
||||||
log("Base rootfs already exists, skipping download.");
|
log("Base rootfs already exists, skipping download.");
|
||||||
} else {
|
} 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(
|
const listing = execFileSync(
|
||||||
"curl",
|
"curl",
|
||||||
["-fsSL", CONFIG.assets.rootfsListUrl],
|
["-fsSL", "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/" + arch + "/"],
|
||||||
{ encoding: "utf-8", timeout: 30_000 }
|
{ encoding: "utf-8", timeout: 30_000 }
|
||||||
);
|
);
|
||||||
const keys = [...listing.matchAll(/<Key>([^<]+)<\/Key>/g)].map(
|
const match = listing.match(
|
||||||
(m) => m[1]
|
new RegExp(`alpine-minirootfs-[\\d.]+-${arch}\\.tar\\.gz`, "g")
|
||||||
);
|
);
|
||||||
const rootfsKey = keys.sort().pop();
|
if (!match || match.length === 0)
|
||||||
if (!rootfsKey) throw new Error("Could not find rootfs in S3 listing");
|
throw new Error("Could not find Alpine minirootfs");
|
||||||
|
const filename = match.sort().pop()!;
|
||||||
|
|
||||||
const squashfsPath = `${CONFIG.baseDir}/rootfs.squashfs`;
|
download(
|
||||||
download(`${CONFIG.assets.rootfsBaseUrl}/${rootfsKey}`, squashfsPath);
|
`https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/${arch}/${filename}`,
|
||||||
log("Rootfs downloaded. Converting squashfs to ext4...");
|
alpineTar
|
||||||
|
);
|
||||||
|
log("Alpine downloaded. Building ext4 rootfs...");
|
||||||
|
|
||||||
// Convert squashfs to ext4
|
// Create ext4 image and unpack Alpine
|
||||||
const squashMount = "/tmp/fireclaw-squash";
|
const ext4Mount = "/tmp/fireclaw-alpine";
|
||||||
const ext4Mount = "/tmp/fireclaw-ext4";
|
|
||||||
mkdirSync(squashMount, { recursive: true });
|
|
||||||
mkdirSync(ext4Mount, { recursive: true });
|
mkdirSync(ext4Mount, { recursive: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execFileSync(
|
execFileSync("truncate", ["-s", "256M", CONFIG.baseRootfs], {
|
||||||
"sudo",
|
|
||||||
["mount", "-t", "squashfs", squashfsPath, squashMount],
|
|
||||||
{ stdio: "pipe" }
|
|
||||||
);
|
|
||||||
execFileSync("truncate", ["-s", "1G", CONFIG.baseRootfs], {
|
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
execFileSync("sudo", ["/usr/sbin/mkfs.ext4", CONFIG.baseRootfs], {
|
execFileSync("sudo", ["/usr/sbin/mkfs.ext4", "-q", CONFIG.baseRootfs], {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
execFileSync("sudo", ["mount", CONFIG.baseRootfs, ext4Mount], {
|
execFileSync("sudo", ["mount", CONFIG.baseRootfs, ext4Mount], {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
execFileSync("sudo", ["cp", "-a", `${squashMount}/.`, ext4Mount], {
|
execFileSync("sudo", ["tar", "xzf", alpineTar, "-C", ext4Mount], {
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bake in DNS config
|
// DNS
|
||||||
execSync(
|
execSync(
|
||||||
`echo "nameserver 8.8.8.8" | sudo tee ${ext4Mount}/etc/resolv.conf > /dev/null`
|
`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 {
|
} finally {
|
||||||
try {
|
|
||||||
execFileSync("sudo", ["umount", squashMount], { stdio: "pipe" });
|
|
||||||
} catch {}
|
|
||||||
try {
|
try {
|
||||||
execFileSync("sudo", ["umount", ext4Mount], { stdio: "pipe" });
|
execFileSync("sudo", ["umount", ext4Mount], { stdio: "pipe" });
|
||||||
} catch {}
|
} catch {}
|
||||||
try {
|
|
||||||
execFileSync("rmdir", [squashMount], { stdio: "pipe" });
|
|
||||||
} catch {}
|
|
||||||
try {
|
try {
|
||||||
execFileSync("rmdir", [ext4Mount], { stdio: "pipe" });
|
execFileSync("rmdir", [ext4Mount], { stdio: "pipe" });
|
||||||
} catch {}
|
} catch {}
|
||||||
try {
|
try {
|
||||||
execFileSync("rm", ["-f", squashfsPath], { stdio: "pipe" });
|
execFileSync("rm", ["-f", alpineTar], { stdio: "pipe" });
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
ensureBaseImage,
|
ensureBaseImage,
|
||||||
ensureSshKeypair,
|
ensureSshKeypair,
|
||||||
createRunCopy,
|
|
||||||
injectSshKey,
|
injectSshKey,
|
||||||
} from "./rootfs.js";
|
} from "./rootfs.js";
|
||||||
import { waitForSsh } from "./ssh.js";
|
import { waitForSsh } from "./ssh.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user