Initial commit — fireclaw multi-agent system
Firecracker microVM-based multi-agent system with IRC orchestration and local LLMs. Features: - Ephemeral command runner with VM snapshots (~1.1s) - Multi-agent orchestration via overseer IRC bot - 5 agent templates (worker, coder, researcher, quick, creative) - Tool access (shell + podman containers inside VMs) - Persistent workspace + memory system (MEMORY.md pattern) - Agent hot-reload (model/persona swap via SSH + SIGHUP) - Non-root agents, graceful shutdown, crash recovery - Agent-to-agent communication via IRC - DM support, /invite support - Systemd service, 20 regression tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
128
src/snapshot.ts
Normal file
128
src/snapshot.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { CONFIG } from "./config.js";
|
||||
import * as api from "./firecracker-api.js";
|
||||
import {
|
||||
ensureBridge,
|
||||
ensureNat,
|
||||
createTap,
|
||||
deleteTap,
|
||||
macFromOctet,
|
||||
} from "./network.js";
|
||||
import {
|
||||
ensureBaseImage,
|
||||
ensureSshKeypair,
|
||||
createRunCopy,
|
||||
injectSshKey,
|
||||
} from "./rootfs.js";
|
||||
import { waitForSsh } from "./ssh.js";
|
||||
import { copyFileSync } from "node:fs";
|
||||
|
||||
function log(msg: string) {
|
||||
process.stderr.write(`[snapshot] ${msg}\n`);
|
||||
}
|
||||
|
||||
function waitForSocket(socketPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + 5_000;
|
||||
const check = () => {
|
||||
if (existsSync(socketPath)) {
|
||||
setTimeout(resolve, 200);
|
||||
return;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
reject(new Error("Firecracker socket did not appear"));
|
||||
return;
|
||||
}
|
||||
setTimeout(check, 50);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
export function snapshotExists(): boolean {
|
||||
return (
|
||||
existsSync(CONFIG.snapshot.statePath) &&
|
||||
existsSync(CONFIG.snapshot.memPath) &&
|
||||
existsSync(CONFIG.snapshot.rootfsPath)
|
||||
);
|
||||
}
|
||||
|
||||
export async function createSnapshot() {
|
||||
ensureBaseImage();
|
||||
ensureSshKeypair();
|
||||
|
||||
const snap = CONFIG.snapshot;
|
||||
const socketPath = join(CONFIG.socketDir, "snapshot.sock");
|
||||
|
||||
log("Preparing snapshot rootfs...");
|
||||
mkdirSync(CONFIG.socketDir, { recursive: true });
|
||||
copyFileSync(CONFIG.baseRootfs, snap.rootfsPath);
|
||||
injectSshKey(snap.rootfsPath);
|
||||
|
||||
log("Setting up network...");
|
||||
ensureBridge();
|
||||
ensureNat();
|
||||
createTap(snap.tapDevice);
|
||||
|
||||
let proc: ChildProcess | null = null;
|
||||
|
||||
try {
|
||||
log("Booting VM for snapshot...");
|
||||
proc = spawn(CONFIG.firecrackerBin, ["--api-sock", socketPath], {
|
||||
stdio: "pipe",
|
||||
detached: false,
|
||||
});
|
||||
|
||||
await waitForSocket(socketPath);
|
||||
|
||||
const bootArgs = [
|
||||
"console=ttyS0",
|
||||
"reboot=k",
|
||||
"panic=1",
|
||||
"pci=off",
|
||||
"root=/dev/vda",
|
||||
"rw",
|
||||
`ip=${snap.ip}::${CONFIG.bridge.gateway}:${CONFIG.bridge.netmask}::eth0:off`,
|
||||
].join(" ");
|
||||
|
||||
await api.putBootSource(socketPath, CONFIG.kernelPath, bootArgs);
|
||||
await api.putDrive(socketPath, "rootfs", snap.rootfsPath);
|
||||
await api.putNetworkInterface(
|
||||
socketPath,
|
||||
"eth0",
|
||||
snap.tapDevice,
|
||||
macFromOctet(snap.octet)
|
||||
);
|
||||
await api.putMachineConfig(
|
||||
socketPath,
|
||||
CONFIG.vm.vcpuCount,
|
||||
CONFIG.vm.memSizeMib
|
||||
);
|
||||
await api.startInstance(socketPath);
|
||||
|
||||
log("Waiting for SSH...");
|
||||
await waitForSsh(snap.ip);
|
||||
|
||||
log("Pausing VM...");
|
||||
await api.patchVm(socketPath, "Paused");
|
||||
|
||||
log("Creating snapshot...");
|
||||
await api.putSnapshotCreate(socketPath, snap.statePath, snap.memPath);
|
||||
|
||||
log("Snapshot created successfully.");
|
||||
log(` State: ${snap.statePath}`);
|
||||
log(` Memory: ${snap.memPath}`);
|
||||
log(` Rootfs: ${snap.rootfsPath}`);
|
||||
} finally {
|
||||
if (proc && !proc.killed) {
|
||||
proc.kill("SIGKILL");
|
||||
}
|
||||
try {
|
||||
const { unlinkSync } = await import("node:fs");
|
||||
unlinkSync(socketPath);
|
||||
} catch {}
|
||||
deleteTap(snap.tapDevice);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user