mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-29 19:33:34 +00:00
Merge pull request #189 from ruvnet/fix/ruvbot-startup-and-rvdna-binaries
feat(ruvbot): self-contained RVF with real Linux 6.6 kernel + API server fix
This commit is contained in:
commit
e34a47cbb0
5 changed files with 653 additions and 7 deletions
BIN
npm/packages/ruvbot/kernel/bzImage
Normal file
BIN
npm/packages/ruvbot/kernel/bzImage
Normal file
Binary file not shown.
|
|
@ -40,6 +40,7 @@
|
|||
"build": "npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc -p tsconfig.json",
|
||||
"build:esm": "tsc -p tsconfig.esm.json",
|
||||
"build:rvf": "node scripts/build-rvf.js",
|
||||
"dev": "tsc -w",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
|
|
@ -134,6 +135,8 @@
|
|||
"dist",
|
||||
"bin",
|
||||
"scripts",
|
||||
"kernel/bzImage",
|
||||
"ruvbot.rvf",
|
||||
"src/api/public",
|
||||
".env.example",
|
||||
"README.md"
|
||||
|
|
|
|||
BIN
npm/packages/ruvbot/ruvbot.rvf
Normal file
BIN
npm/packages/ruvbot/ruvbot.rvf
Normal file
Binary file not shown.
506
npm/packages/ruvbot/scripts/build-rvf.js
Normal file
506
npm/packages/ruvbot/scripts/build-rvf.js
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-rvf.js — Assemble RuvBot as a self-contained .rvf file.
|
||||
*
|
||||
* The output contains:
|
||||
* KERNEL_SEG (0x0E) — Real Linux 6.6 microkernel (bzImage, x86_64)
|
||||
* WASM_SEG (0x10) — RuvBot runtime bundle (Node.js application)
|
||||
* META_SEG (0x07) — Package metadata (name, version, config)
|
||||
* PROFILE_SEG (0x0B) — AI assistant domain profile
|
||||
* WITNESS_SEG (0x0A) — Build provenance chain
|
||||
* MANIFEST_SEG(0x05) — Segment directory + epoch
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/build-rvf.js --kernel /path/to/bzImage [--output ruvbot.rvf]
|
||||
*
|
||||
* The --kernel flag provides a real Linux bzImage to embed. If omitted,
|
||||
* the script looks for kernel/bzImage relative to the package root.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { writeFileSync, readFileSync, existsSync, readdirSync, statSync, mkdirSync } = require('fs');
|
||||
const { join, resolve, isAbsolute } = require('path');
|
||||
const { createHash } = require('crypto');
|
||||
const { execSync } = require('child_process');
|
||||
const { gzipSync } = require('zlib');
|
||||
|
||||
// ─── RVF format constants ───────────────────────────────────────────────────
|
||||
const SEGMENT_MAGIC = 0x5256_4653; // "RVFS" big-endian
|
||||
const SEGMENT_VERSION = 1;
|
||||
const KERNEL_MAGIC = 0x5256_4B4E; // "RVKN" big-endian
|
||||
const WASM_MAGIC = 0x5256_574D; // "RVWM" big-endian
|
||||
|
||||
// Segment type discriminators
|
||||
const SEG_MANIFEST = 0x05;
|
||||
const SEG_META = 0x07;
|
||||
const SEG_WITNESS = 0x0A;
|
||||
const SEG_PROFILE = 0x0B;
|
||||
const SEG_KERNEL = 0x0E;
|
||||
const SEG_WASM = 0x10;
|
||||
|
||||
// Kernel constants
|
||||
const KERNEL_ARCH_X86_64 = 0x00;
|
||||
const KERNEL_TYPE_MICROLINUX = 0x01;
|
||||
const KERNEL_FLAG_HAS_NETWORKING = 1 << 3;
|
||||
const KERNEL_FLAG_HAS_QUERY_API = 1 << 4;
|
||||
const KERNEL_FLAG_HAS_ADMIN_API = 1 << 6;
|
||||
const KERNEL_FLAG_RELOCATABLE = 1 << 11;
|
||||
const KERNEL_FLAG_HAS_VIRTIO_NET = 1 << 12;
|
||||
const KERNEL_FLAG_HAS_VIRTIO_BLK = 1 << 13;
|
||||
const KERNEL_FLAG_HAS_VSOCK = 1 << 14;
|
||||
const KERNEL_FLAG_COMPRESSED = 1 << 10;
|
||||
|
||||
// WASM constants
|
||||
const WASM_ROLE_COMBINED = 0x02;
|
||||
const WASM_TARGET_NODE = 0x01;
|
||||
|
||||
// ─── Binary helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function writeU8(buf, offset, val) {
|
||||
buf[offset] = val & 0xFF;
|
||||
return offset + 1;
|
||||
}
|
||||
|
||||
function writeU16LE(buf, offset, val) {
|
||||
buf.writeUInt16LE(val, offset);
|
||||
return offset + 2;
|
||||
}
|
||||
|
||||
function writeU32LE(buf, offset, val) {
|
||||
buf.writeUInt32LE(val >>> 0, offset);
|
||||
return offset + 4;
|
||||
}
|
||||
|
||||
function writeU64LE(buf, offset, val) {
|
||||
const big = BigInt(Math.floor(val));
|
||||
buf.writeBigUInt64LE(big, offset);
|
||||
return offset + 8;
|
||||
}
|
||||
|
||||
function contentHash16(payload) {
|
||||
return createHash('sha256').update(payload).digest().subarray(0, 16);
|
||||
}
|
||||
|
||||
// ─── Segment header writer (64 bytes) ───────────────────────────────────────
|
||||
|
||||
function makeSegmentHeader(segType, segId, payloadLength, payload) {
|
||||
const buf = Buffer.alloc(64);
|
||||
writeU32LE(buf, 0x00, SEGMENT_MAGIC);
|
||||
writeU8(buf, 0x04, SEGMENT_VERSION);
|
||||
writeU8(buf, 0x05, segType);
|
||||
writeU16LE(buf, 0x06, 0); // flags
|
||||
writeU64LE(buf, 0x08, segId);
|
||||
writeU64LE(buf, 0x10, payloadLength);
|
||||
writeU64LE(buf, 0x18, Date.now() * 1e6); // timestamp_ns
|
||||
writeU8(buf, 0x20, 0); // checksum_algo (CRC32C)
|
||||
writeU8(buf, 0x21, 0); // compression
|
||||
writeU16LE(buf, 0x22, 0); // reserved_0
|
||||
writeU32LE(buf, 0x24, 0); // reserved_1
|
||||
contentHash16(payload).copy(buf, 0x28, 0, 16); // content_hash
|
||||
writeU32LE(buf, 0x38, 0); // uncompressed_len
|
||||
writeU32LE(buf, 0x3C, 0); // alignment_pad
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── Kernel header (128 bytes) ──────────────────────────────────────────────
|
||||
|
||||
function makeKernelHeader(imageSize, compressedSize, cmdlineLen, isCompressed) {
|
||||
const buf = Buffer.alloc(128);
|
||||
writeU32LE(buf, 0x00, KERNEL_MAGIC);
|
||||
writeU16LE(buf, 0x04, 1); // header_version
|
||||
writeU8(buf, 0x06, KERNEL_ARCH_X86_64);
|
||||
writeU8(buf, 0x07, KERNEL_TYPE_MICROLINUX);
|
||||
const flags = KERNEL_FLAG_HAS_NETWORKING
|
||||
| KERNEL_FLAG_HAS_QUERY_API
|
||||
| KERNEL_FLAG_HAS_ADMIN_API
|
||||
| KERNEL_FLAG_RELOCATABLE
|
||||
| KERNEL_FLAG_HAS_VIRTIO_NET
|
||||
| KERNEL_FLAG_HAS_VIRTIO_BLK
|
||||
| (isCompressed ? KERNEL_FLAG_COMPRESSED : 0);
|
||||
writeU32LE(buf, 0x08, flags);
|
||||
writeU32LE(buf, 0x0C, 64); // min_memory_mb
|
||||
writeU64LE(buf, 0x10, 0x1000000); // entry_point (16 MB default load)
|
||||
writeU64LE(buf, 0x18, imageSize); // image_size (uncompressed)
|
||||
writeU64LE(buf, 0x20, compressedSize); // compressed_size
|
||||
writeU8(buf, 0x28, isCompressed ? 1 : 0); // compression (0=none, 1=gzip)
|
||||
writeU8(buf, 0x29, 0x00); // api_transport (TcpHttp)
|
||||
writeU16LE(buf, 0x2A, 3000); // api_port
|
||||
writeU16LE(buf, 0x2C, 1); // api_version
|
||||
// 0x2E: 2 bytes padding
|
||||
// 0x30: image_hash (32 bytes) — filled by caller
|
||||
// 0x50: build_id (16 bytes)
|
||||
writeU64LE(buf, 0x60, Math.floor(Date.now() / 1000)); // build_timestamp
|
||||
writeU8(buf, 0x68, 1); // vcpu_count
|
||||
// 0x69: reserved_0
|
||||
// 0x6A: 2 bytes padding
|
||||
writeU32LE(buf, 0x6C, 128); // cmdline_offset
|
||||
writeU32LE(buf, 0x70, cmdlineLen); // cmdline_length
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── WASM header (64 bytes) ─────────────────────────────────────────────────
|
||||
|
||||
function makeWasmHeader(bytecodeSize) {
|
||||
const buf = Buffer.alloc(64);
|
||||
writeU32LE(buf, 0x00, WASM_MAGIC);
|
||||
writeU16LE(buf, 0x04, 1); // header_version
|
||||
writeU8(buf, 0x06, WASM_ROLE_COMBINED); // role
|
||||
writeU8(buf, 0x07, WASM_TARGET_NODE); // target
|
||||
writeU16LE(buf, 0x08, 0); // required_features
|
||||
writeU16LE(buf, 0x0A, 12); // export_count
|
||||
writeU32LE(buf, 0x0C, bytecodeSize); // bytecode_size
|
||||
writeU32LE(buf, 0x10, 0); // compressed_size
|
||||
writeU8(buf, 0x14, 0); // compression
|
||||
writeU8(buf, 0x15, 2); // min_memory_pages
|
||||
writeU16LE(buf, 0x16, 0); // max_memory_pages
|
||||
writeU16LE(buf, 0x18, 0); // table_count
|
||||
// 0x1A: 2 bytes padding
|
||||
// 0x1C: bytecode_hash (32 bytes) — filled by caller
|
||||
writeU8(buf, 0x3C, 0); // bootstrap_priority
|
||||
writeU8(buf, 0x3D, 0); // interpreter_type
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── Load real kernel image ─────────────────────────────────────────────────
|
||||
|
||||
function loadKernelImage(kernelPath) {
|
||||
if (!existsSync(kernelPath)) {
|
||||
console.error(`ERROR: Kernel image not found: ${kernelPath}`);
|
||||
console.error('Build one with: cd /tmp/linux-6.6.80 && make tinyconfig && make -j$(nproc) bzImage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = readFileSync(kernelPath);
|
||||
const stat = statSync(kernelPath);
|
||||
console.log(` Loaded: ${kernelPath} (${(raw.length / 1024).toFixed(0)} KB)`);
|
||||
|
||||
// Verify it looks like a real kernel (ELF or bzImage magic)
|
||||
const magic = raw.readUInt16LE(0);
|
||||
const elfMagic = raw.subarray(0, 4);
|
||||
if (elfMagic[0] === 0x7F && elfMagic[1] === 0x45 && elfMagic[2] === 0x4C && elfMagic[3] === 0x46) {
|
||||
console.log(' Format: ELF vmlinux');
|
||||
} else if (raw.length > 0x202 && raw.readUInt16LE(0x1FE) === 0xAA55) {
|
||||
console.log(' Format: bzImage (bootable)');
|
||||
} else {
|
||||
console.log(' Format: raw kernel image');
|
||||
}
|
||||
|
||||
// Gzip compress for smaller RVF
|
||||
const compressed = gzipSync(raw, { level: 9 });
|
||||
const ratio = ((1 - compressed.length / raw.length) * 100).toFixed(1);
|
||||
console.log(` Compressed: ${(compressed.length / 1024).toFixed(0)} KB (${ratio}% reduction)`);
|
||||
|
||||
return { raw, compressed };
|
||||
}
|
||||
|
||||
// ─── Build the runtime bundle ───────────────────────────────────────────────
|
||||
|
||||
function buildRuntimeBundle(pkgDir) {
|
||||
const distDir = join(pkgDir, 'dist');
|
||||
const binDir = join(pkgDir, 'bin');
|
||||
const files = [];
|
||||
|
||||
if (existsSync(distDir)) collectFiles(distDir, '', files);
|
||||
if (existsSync(binDir)) collectFiles(binDir, 'bin/', files);
|
||||
|
||||
const pkgJsonPath = join(pkgDir, 'package.json');
|
||||
if (existsSync(pkgJsonPath)) {
|
||||
files.push({ path: 'package.json', data: readFileSync(pkgJsonPath) });
|
||||
}
|
||||
|
||||
// Bundle format: [file_count(u32)] [file_table] [file_data]
|
||||
const fileCount = Buffer.alloc(4);
|
||||
fileCount.writeUInt32LE(files.length, 0);
|
||||
|
||||
let tableSize = 0;
|
||||
for (const f of files) {
|
||||
tableSize += 2 + 8 + 8 + Buffer.byteLength(f.path, 'utf8');
|
||||
}
|
||||
|
||||
let dataOffset = 4 + tableSize;
|
||||
const tableEntries = [];
|
||||
for (const f of files) {
|
||||
const pathBuf = Buffer.from(f.path, 'utf8');
|
||||
const entry = Buffer.alloc(2 + 8 + 8 + pathBuf.length);
|
||||
let o = writeU16LE(entry, 0, pathBuf.length);
|
||||
o = writeU64LE(entry, o, dataOffset);
|
||||
o = writeU64LE(entry, o, f.data.length);
|
||||
pathBuf.copy(entry, o);
|
||||
tableEntries.push(entry);
|
||||
dataOffset += f.data.length;
|
||||
}
|
||||
|
||||
return Buffer.concat([fileCount, ...tableEntries, ...files.map(f => f.data)]);
|
||||
}
|
||||
|
||||
function collectFiles(dir, prefix, files) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = join(dir, name);
|
||||
const rel = prefix + name;
|
||||
const stat = statSync(full);
|
||||
if (stat.isDirectory()) collectFiles(full, rel + '/', files);
|
||||
else if (stat.isFile()) files.push({ path: rel, data: readFileSync(full) });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Build META_SEG ─────────────────────────────────────────────────────────
|
||||
|
||||
function buildMetaPayload(pkgDir, kernelInfo) {
|
||||
const pkgJson = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
||||
return Buffer.from(JSON.stringify({
|
||||
name: pkgJson.name,
|
||||
version: pkgJson.version,
|
||||
description: pkgJson.description,
|
||||
format: 'rvf-self-contained',
|
||||
runtime: 'node',
|
||||
runtime_version: '>=18.0.0',
|
||||
arch: 'x86_64',
|
||||
kernel: {
|
||||
type: 'microlinux',
|
||||
version: '6.6.80',
|
||||
config: 'tinyconfig+virtio+net',
|
||||
image_size: kernelInfo.rawSize,
|
||||
compressed_size: kernelInfo.compressedSize,
|
||||
},
|
||||
build_time: new Date().toISOString(),
|
||||
builder: 'ruvbot/build-rvf.js',
|
||||
capabilities: [
|
||||
'self-booting',
|
||||
'api-server',
|
||||
'chat',
|
||||
'vector-search',
|
||||
'self-learning',
|
||||
'multi-llm',
|
||||
'security-scanning',
|
||||
],
|
||||
dependencies: Object.keys(pkgJson.dependencies || {}),
|
||||
entrypoint: 'bin/ruvbot.js',
|
||||
api_port: 3000,
|
||||
firecracker_compatible: true,
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Build PROFILE_SEG ──────────────────────────────────────────────────────
|
||||
|
||||
function buildProfilePayload() {
|
||||
return Buffer.from(JSON.stringify({
|
||||
profile_id: 0x42,
|
||||
domain: 'ai-assistant',
|
||||
name: 'RuvBot',
|
||||
version: '0.2.0',
|
||||
capabilities: {
|
||||
chat: true,
|
||||
vector_search: true,
|
||||
embeddings: true,
|
||||
self_learning: true,
|
||||
multi_model: true,
|
||||
security: true,
|
||||
self_booting: true,
|
||||
},
|
||||
models: [
|
||||
'claude-sonnet-4-20250514',
|
||||
'gemini-2.0-flash',
|
||||
'gpt-4o',
|
||||
'openrouter/*',
|
||||
],
|
||||
boot_config: {
|
||||
vcpus: 1,
|
||||
memory_mb: 64,
|
||||
api_port: 3000,
|
||||
cmdline: 'console=ttyS0 ruvbot.mode=rvf',
|
||||
},
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Build WITNESS_SEG ──────────────────────────────────────────────────────
|
||||
|
||||
function buildWitnessPayload(kernelHash, runtimeHash) {
|
||||
return Buffer.from(JSON.stringify({
|
||||
witness_type: 'build_provenance',
|
||||
timestamp: new Date().toISOString(),
|
||||
builder: {
|
||||
tool: 'build-rvf.js',
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
artifacts: {
|
||||
kernel: { hash_sha256: kernelHash, type: 'linux-6.6-bzimage' },
|
||||
runtime: { hash_sha256: runtimeHash, type: 'nodejs-bundle' },
|
||||
},
|
||||
chain: [],
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Assemble the RVF ───────────────────────────────────────────────────────
|
||||
|
||||
function assembleRvf(pkgDir, outputPath, kernelPath) {
|
||||
console.log('Building self-contained RuvBot RVF...');
|
||||
console.log(` Package: ${pkgDir}`);
|
||||
console.log(` Kernel: ${kernelPath}`);
|
||||
console.log(` Output: ${outputPath}\n`);
|
||||
|
||||
let segId = 1;
|
||||
const segments = [];
|
||||
const segDir = [];
|
||||
|
||||
// 1. KERNEL_SEG — Real Linux microkernel
|
||||
console.log(' [1/6] Embedding Linux 6.6 microkernel...');
|
||||
const { raw: kernelRaw, compressed: kernelCompressed } = loadKernelImage(kernelPath);
|
||||
const cmdline = Buffer.from('console=ttyS0 ruvbot.api_port=3000 ruvbot.mode=rvf quiet', 'utf8');
|
||||
const kernelHdr = makeKernelHeader(
|
||||
kernelRaw.length,
|
||||
kernelCompressed.length,
|
||||
cmdline.length,
|
||||
true // compressed
|
||||
);
|
||||
const imgHash = createHash('sha256').update(kernelRaw).digest();
|
||||
imgHash.copy(kernelHdr, 0x30, 0, 32);
|
||||
// Build ID from first 16 bytes of hash
|
||||
imgHash.copy(kernelHdr, 0x50, 0, 16);
|
||||
const kernelPayload = Buffer.concat([kernelHdr, kernelCompressed, cmdline]);
|
||||
const kSegId = segId++;
|
||||
segments.push({ segType: SEG_KERNEL, segId: kSegId, payload: kernelPayload });
|
||||
|
||||
// 2. WASM_SEG — RuvBot runtime bundle
|
||||
console.log(' [2/6] Bundling RuvBot runtime...');
|
||||
const runtimeBundle = buildRuntimeBundle(pkgDir);
|
||||
const wasmHdr = makeWasmHeader(runtimeBundle.length);
|
||||
const runtimeHash = createHash('sha256').update(runtimeBundle).digest();
|
||||
runtimeHash.copy(wasmHdr, 0x1C, 0, 32);
|
||||
const wasmPayload = Buffer.concat([wasmHdr, runtimeBundle]);
|
||||
const wSegId = segId++;
|
||||
segments.push({ segType: SEG_WASM, segId: wSegId, payload: wasmPayload });
|
||||
console.log(` Runtime: ${runtimeBundle.length} bytes (${(runtimeBundle.length / 1024).toFixed(0)} KB)`);
|
||||
|
||||
// 3. META_SEG — Package metadata
|
||||
console.log(' [3/6] Writing package metadata...');
|
||||
const metaPayload = buildMetaPayload(pkgDir, {
|
||||
rawSize: kernelRaw.length,
|
||||
compressedSize: kernelCompressed.length,
|
||||
});
|
||||
const mSegId = segId++;
|
||||
segments.push({ segType: SEG_META, segId: mSegId, payload: metaPayload });
|
||||
|
||||
// 4. PROFILE_SEG — Domain profile
|
||||
console.log(' [4/6] Writing domain profile...');
|
||||
const profilePayload = buildProfilePayload();
|
||||
const pSegId = segId++;
|
||||
segments.push({ segType: SEG_PROFILE, segId: pSegId, payload: profilePayload });
|
||||
|
||||
// 5. WITNESS_SEG — Build provenance
|
||||
console.log(' [5/6] Writing build provenance...');
|
||||
const witnessPayload = buildWitnessPayload(
|
||||
imgHash.toString('hex'),
|
||||
runtimeHash.toString('hex'),
|
||||
);
|
||||
const witnSegId = segId++;
|
||||
segments.push({ segType: SEG_WITNESS, segId: witnSegId, payload: witnessPayload });
|
||||
|
||||
// 6. MANIFEST_SEG — Segment directory
|
||||
console.log(' [6/6] Writing manifest...');
|
||||
let currentOffset = 0;
|
||||
for (const seg of segments) {
|
||||
segDir.push({
|
||||
segId: seg.segId,
|
||||
offset: currentOffset,
|
||||
payloadLen: seg.payload.length,
|
||||
segType: seg.segType,
|
||||
});
|
||||
currentOffset += 64 + seg.payload.length;
|
||||
}
|
||||
|
||||
const dirEntrySize = 8 + 8 + 8 + 1;
|
||||
const manifestSize = 4 + 2 + 8 + 4 + 1 + 3 + (segDir.length * dirEntrySize) + 4;
|
||||
const manifestPayload = Buffer.alloc(manifestSize);
|
||||
let mo = 0;
|
||||
mo = writeU32LE(manifestPayload, mo, 1); // epoch
|
||||
mo = writeU16LE(manifestPayload, mo, 0); // dimension
|
||||
mo = writeU64LE(manifestPayload, mo, 0); // total_vectors
|
||||
mo = writeU32LE(manifestPayload, mo, segDir.length); // seg_count
|
||||
mo = writeU8(manifestPayload, mo, 0x42); // profile_id
|
||||
mo += 3; // reserved
|
||||
|
||||
for (const entry of segDir) {
|
||||
mo = writeU64LE(manifestPayload, mo, entry.segId);
|
||||
mo = writeU64LE(manifestPayload, mo, entry.offset);
|
||||
mo = writeU64LE(manifestPayload, mo, entry.payloadLen);
|
||||
mo = writeU8(manifestPayload, mo, entry.segType);
|
||||
}
|
||||
mo = writeU32LE(manifestPayload, mo, 0); // del_count
|
||||
|
||||
const manSegId = segId++;
|
||||
segments.push({ segType: SEG_MANIFEST, segId: manSegId, payload: manifestPayload });
|
||||
|
||||
// Write all segments
|
||||
const allBuffers = [];
|
||||
for (const seg of segments) {
|
||||
allBuffers.push(makeSegmentHeader(seg.segType, seg.segId, seg.payload.length, seg.payload));
|
||||
allBuffers.push(seg.payload);
|
||||
}
|
||||
|
||||
const rvfData = Buffer.concat(allBuffers);
|
||||
mkdirSync(join(pkgDir, 'kernel'), { recursive: true });
|
||||
writeFileSync(outputPath, rvfData);
|
||||
|
||||
// Summary
|
||||
const mb = (rvfData.length / (1024 * 1024)).toFixed(2);
|
||||
console.log(`\n RVF assembled: ${outputPath}`);
|
||||
console.log(` Total size: ${mb} MB`);
|
||||
console.log(` Segments: ${segments.length}`);
|
||||
console.log(` KERNEL_SEG : ${(kernelPayload.length / 1024).toFixed(0)} KB (Linux 6.6.80 bzImage, gzip)`);
|
||||
console.log(` WASM_SEG : ${(wasmPayload.length / 1024).toFixed(0)} KB (Node.js runtime bundle)`);
|
||||
console.log(` META_SEG : ${metaPayload.length} bytes`);
|
||||
console.log(` PROFILE_SEG : ${profilePayload.length} bytes`);
|
||||
console.log(` WITNESS_SEG : ${witnessPayload.length} bytes`);
|
||||
console.log(` MANIFEST_SEG: ${manifestPayload.length} bytes`);
|
||||
console.log(`\n Kernel SHA-256: ${imgHash.toString('hex')}`);
|
||||
console.log(` Self-contained: boot with Firecracker, QEMU, or Cloud Hypervisor`);
|
||||
}
|
||||
|
||||
// ─── CLI entry ──────────────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let outputPath = 'ruvbot.rvf';
|
||||
let kernelPath = '';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--output' && args[i + 1]) { outputPath = args[++i]; }
|
||||
else if (args[i] === '--kernel' && args[i + 1]) { kernelPath = args[++i]; }
|
||||
}
|
||||
|
||||
const pkgDir = resolve(__dirname, '..');
|
||||
|
||||
// Find kernel: CLI arg > kernel/bzImage > RUVBOT_KERNEL env
|
||||
if (!kernelPath) {
|
||||
const candidates = [
|
||||
join(pkgDir, 'kernel', 'bzImage'),
|
||||
join(pkgDir, 'kernel', 'vmlinux'),
|
||||
'/tmp/linux-6.6.80/arch/x86/boot/bzImage',
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (existsSync(c)) { kernelPath = c; break; }
|
||||
}
|
||||
}
|
||||
if (!kernelPath && process.env.RUVBOT_KERNEL) {
|
||||
kernelPath = process.env.RUVBOT_KERNEL;
|
||||
}
|
||||
if (!kernelPath) {
|
||||
console.error('ERROR: No kernel image found.');
|
||||
console.error('Provide one with --kernel /path/to/bzImage or place at kernel/bzImage');
|
||||
console.error('\nTo build a minimal kernel:');
|
||||
console.error(' wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.80.tar.xz');
|
||||
console.error(' tar xf linux-6.6.80.tar.xz && cd linux-6.6.80');
|
||||
console.error(' make tinyconfig');
|
||||
console.error(' scripts/config --enable 64BIT --enable VIRTIO --enable VIRTIO_NET \\');
|
||||
console.error(' --enable NET --enable INET --enable SERIAL_8250_CONSOLE --enable TTY');
|
||||
console.error(' make olddefconfig && make -j$(nproc) bzImage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isAbsolute(outputPath)) {
|
||||
outputPath = join(pkgDir, outputPath);
|
||||
}
|
||||
|
||||
assembleRvf(pkgDir, outputPath, kernelPath);
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import pino from 'pino';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
|
@ -71,6 +72,7 @@ export class RuvBot extends EventEmitter<RuvBotEvents> {
|
|||
private isRunning: boolean = false;
|
||||
private startTime?: Date;
|
||||
private llmProvider: LLMProvider | null = null;
|
||||
private httpServer: Server | null = null;
|
||||
|
||||
constructor(options: RuvBotOptions = {}) {
|
||||
super();
|
||||
|
|
@ -539,16 +541,151 @@ export class RuvBot extends EventEmitter<RuvBotEvents> {
|
|||
}
|
||||
|
||||
private async startApiServer(config: BotConfig): Promise<void> {
|
||||
this.logger.info(
|
||||
{ port: config.api.port, host: config.api.host },
|
||||
'Starting API server...'
|
||||
);
|
||||
// TODO: Initialize Fastify server
|
||||
const port = config.api.port || 3000;
|
||||
const host = config.api.host || '0.0.0.0';
|
||||
|
||||
this.httpServer = createServer((req, res) => {
|
||||
this.handleApiRequest(req, res).catch((error) => {
|
||||
this.logger.error({ err: error }, 'Unhandled API request error');
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.on('error', (err) => {
|
||||
this.logger.error({ err, port, host }, 'API server failed to start');
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.httpServer!.listen(port, host, () => {
|
||||
this.logger.info({ port, host }, 'API server listening');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async stopApiServer(): Promise<void> {
|
||||
this.logger.debug('Stopping API server...');
|
||||
// TODO: Stop Fastify server
|
||||
if (!this.httpServer) return;
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
this.httpServer!.close(() => {
|
||||
this.logger.debug('API server stopped');
|
||||
this.httpServer = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleApiRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
const path = url.pathname;
|
||||
const method = req.method || 'GET';
|
||||
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const json = (status: number, data: unknown) => {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
};
|
||||
|
||||
// Health check
|
||||
if (path === '/health' || path === '/healthz') {
|
||||
json(200, {
|
||||
status: 'healthy',
|
||||
uptime: this.startTime ? Math.floor((Date.now() - this.startTime.getTime()) / 1000) : 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Readiness check
|
||||
if (path === '/ready' || path === '/readyz') {
|
||||
if (this.isRunning) {
|
||||
json(200, { status: 'ready' });
|
||||
} else {
|
||||
json(503, { status: 'not ready' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Status
|
||||
if (path === '/api/status') {
|
||||
json(200, this.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat endpoint
|
||||
if (path === '/api/chat' && method === 'POST') {
|
||||
const body = await this.parseRequestBody(req);
|
||||
const message = body?.message as string;
|
||||
const agentId = (body?.agentId as string) || 'default-agent';
|
||||
|
||||
if (!message) {
|
||||
json(400, { error: 'Missing "message" field' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or reuse a session
|
||||
let sessionId = body?.sessionId as string;
|
||||
if (!sessionId || !this.sessions.has(sessionId)) {
|
||||
const session = await this.createSession(agentId);
|
||||
sessionId = session.id;
|
||||
}
|
||||
|
||||
const response = await this.chat(sessionId, message);
|
||||
json(200, { sessionId, agentId, response });
|
||||
return;
|
||||
}
|
||||
|
||||
// List agents
|
||||
if (path === '/api/agents' && method === 'GET') {
|
||||
json(200, { agents: this.listAgents() });
|
||||
return;
|
||||
}
|
||||
|
||||
// List sessions
|
||||
if (path === '/api/sessions' && method === 'GET') {
|
||||
json(200, { sessions: this.listSessions() });
|
||||
return;
|
||||
}
|
||||
|
||||
// Root — simple landing page
|
||||
if (path === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`<!DOCTYPE html><html><head><title>RuvBot</title>
|
||||
<style>body{font-family:system-ui;background:#0a0a0f;color:#f0f0f5;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
||||
.c{text-align:center}h1{font-size:3rem}p{color:#a0a0b0}a{color:#6366f1;text-decoration:none;padding:12px 24px;border:1px solid #6366f1;border-radius:8px;display:inline-block}a:hover{background:#6366f1;color:#fff}</style>
|
||||
</head><body><div class="c"><h1>RuvBot</h1><p>Enterprise-grade AI Assistant</p><a href="/api/status">API Status</a></div></body></html>`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 404
|
||||
json(404, { error: 'Not found' });
|
||||
}
|
||||
|
||||
private parseRequestBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
if (chunks.length === 0) { resolve(null); return; }
|
||||
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); }
|
||||
catch { reject(new Error('Invalid JSON')); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
private async generateResponse(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue