diff --git a/npm/packages/ruvbot/kernel/bzImage b/npm/packages/ruvbot/kernel/bzImage new file mode 100644 index 00000000..1f52efb9 Binary files /dev/null and b/npm/packages/ruvbot/kernel/bzImage differ diff --git a/npm/packages/ruvbot/package.json b/npm/packages/ruvbot/package.json index f7e7cd5b..2c740bfa 100644 --- a/npm/packages/ruvbot/package.json +++ b/npm/packages/ruvbot/package.json @@ -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" diff --git a/npm/packages/ruvbot/ruvbot.rvf b/npm/packages/ruvbot/ruvbot.rvf new file mode 100644 index 00000000..fdce53a9 Binary files /dev/null and b/npm/packages/ruvbot/ruvbot.rvf differ diff --git a/npm/packages/ruvbot/scripts/build-rvf.js b/npm/packages/ruvbot/scripts/build-rvf.js new file mode 100644 index 00000000..42c98e84 --- /dev/null +++ b/npm/packages/ruvbot/scripts/build-rvf.js @@ -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); diff --git a/npm/packages/ruvbot/src/RuvBot.ts b/npm/packages/ruvbot/src/RuvBot.ts index aff1685a..8fdd0c1f 100644 --- a/npm/packages/ruvbot/src/RuvBot.ts +++ b/npm/packages/ruvbot/src/RuvBot.ts @@ -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 { 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 { } private async startApiServer(config: BotConfig): Promise { - 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((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 { - this.logger.debug('Stopping API server...'); - // TODO: Stop Fastify server + if (!this.httpServer) return; + + return new Promise((resolve) => { + this.httpServer!.close(() => { + this.logger.debug('API server stopped'); + this.httpServer = null; + resolve(); + }); + }); + } + + private async handleApiRequest(req: IncomingMessage, res: ServerResponse): Promise { + 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(`RuvBot + +

RuvBot

Enterprise-grade AI Assistant

API Status
`); + return; + } + + // 404 + json(404, { error: 'Not found' }); + } + + private parseRequestBody(req: IncomingMessage): Promise | 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(