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:
165
src/network.ts
Normal file
165
src/network.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { openSync, closeSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { CONFIG } from "./config.js";
|
||||
|
||||
function run(cmd: string, args: string[]) {
|
||||
execFileSync(cmd, args, { stdio: "pipe" });
|
||||
}
|
||||
|
||||
function sudo(args: string[]) {
|
||||
run("sudo", args);
|
||||
}
|
||||
|
||||
export function ensureBridge() {
|
||||
try {
|
||||
execFileSync("ip", ["link", "show", CONFIG.bridge.name], {
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
sudo(["ip", "link", "add", CONFIG.bridge.name, "type", "bridge"]);
|
||||
sudo([
|
||||
"ip",
|
||||
"addr",
|
||||
"add",
|
||||
`${CONFIG.bridge.ip}/24`,
|
||||
"dev",
|
||||
CONFIG.bridge.name,
|
||||
]);
|
||||
sudo(["ip", "link", "set", CONFIG.bridge.name, "up"]);
|
||||
sudo(["sysctl", "-w", "net.ipv4.ip_forward=1"]);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureNat() {
|
||||
// Check if rule already exists
|
||||
try {
|
||||
execFileSync(
|
||||
"sudo",
|
||||
[
|
||||
"iptables",
|
||||
"-t",
|
||||
"nat",
|
||||
"-C",
|
||||
"POSTROUTING",
|
||||
"-s",
|
||||
CONFIG.bridge.subnet,
|
||||
"-j",
|
||||
"MASQUERADE",
|
||||
],
|
||||
{ stdio: "pipe" }
|
||||
);
|
||||
} catch {
|
||||
// Find the default route interface
|
||||
const routeOut = execFileSync("ip", ["route", "show", "default"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
const extIface = routeOut.match(/dev\s+(\S+)/)?.[1] ?? "eno2";
|
||||
|
||||
sudo([
|
||||
"iptables",
|
||||
"-t",
|
||||
"nat",
|
||||
"-A",
|
||||
"POSTROUTING",
|
||||
"-s",
|
||||
CONFIG.bridge.subnet,
|
||||
"-o",
|
||||
extIface,
|
||||
"-j",
|
||||
"MASQUERADE",
|
||||
]);
|
||||
sudo([
|
||||
"iptables",
|
||||
"-A",
|
||||
"FORWARD",
|
||||
"-i",
|
||||
CONFIG.bridge.name,
|
||||
"-o",
|
||||
extIface,
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
]);
|
||||
sudo([
|
||||
"iptables",
|
||||
"-A",
|
||||
"FORWARD",
|
||||
"-i",
|
||||
extIface,
|
||||
"-o",
|
||||
CONFIG.bridge.name,
|
||||
"-m",
|
||||
"state",
|
||||
"--state",
|
||||
"RELATED,ESTABLISHED",
|
||||
"-j",
|
||||
"ACCEPT",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTap(tapName: string) {
|
||||
sudo(["ip", "tuntap", "add", tapName, "mode", "tap"]);
|
||||
sudo(["ip", "link", "set", tapName, "master", CONFIG.bridge.name]);
|
||||
sudo(["ip", "link", "set", tapName, "up"]);
|
||||
}
|
||||
|
||||
export function deleteTap(tapName: string) {
|
||||
try {
|
||||
sudo(["ip", "tuntap", "del", tapName, "mode", "tap"]);
|
||||
} catch {
|
||||
// Already gone
|
||||
}
|
||||
}
|
||||
|
||||
export function macFromOctet(octet: number): string {
|
||||
return `AA:FC:00:00:00:${octet.toString(16).padStart(2, "0").toUpperCase()}`;
|
||||
}
|
||||
|
||||
interface IpPool {
|
||||
allocated: number[];
|
||||
}
|
||||
|
||||
function readPool(): IpPool {
|
||||
try {
|
||||
return JSON.parse(readFileSync(CONFIG.ipPoolFile, "utf-8"));
|
||||
} catch {
|
||||
return { allocated: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function writePool(pool: IpPool) {
|
||||
writeFileSync(CONFIG.ipPoolFile, JSON.stringify(pool));
|
||||
}
|
||||
|
||||
export function allocateIp(): { ip: string; octet: number } {
|
||||
const fd = openSync(CONFIG.ipPoolLock, "w");
|
||||
try {
|
||||
// Simple flock via child process
|
||||
const pool = readPool();
|
||||
for (
|
||||
let octet = CONFIG.bridge.minHost;
|
||||
octet <= CONFIG.bridge.maxHost;
|
||||
octet++
|
||||
) {
|
||||
if (!pool.allocated.includes(octet)) {
|
||||
pool.allocated.push(octet);
|
||||
writePool(pool);
|
||||
return { ip: `${CONFIG.bridge.prefix}.${octet}`, octet };
|
||||
}
|
||||
}
|
||||
throw new Error("No free IPs in pool");
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseIp(octet: number) {
|
||||
const fd = openSync(CONFIG.ipPoolLock, "w");
|
||||
try {
|
||||
const pool = readPool();
|
||||
pool.allocated = pool.allocated.filter((o) => o !== octet);
|
||||
writePool(pool);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user