UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

237 lines 10.5 kB
/** * RVFA Runner -- Boot and run self-contained Ruflo appliances. * * Supports three run modes (cli, mcp, verify) and two isolation * strategies (native Node.js, container via Docker). * * @module @claude-flow/cli/appliance/rvfa-runner */ import { writeFile, mkdir, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { spawn } from 'node:child_process'; import { tmpdir } from 'node:os'; import { RvfaReader } from './rvfa-format.js'; // ── Internal helpers ──────────────────────────────────────── /** Spawn a child process and capture stdout/stderr. */ function spawnAsync(cmd, args, opts) { return new Promise((resolve) => { const start = performance.now(); const out = []; const err = []; const child = spawn(cmd, args, { cwd: opts.cwd, env: { ...process.env, ...opts.env }, stdio: ['pipe', 'pipe', 'pipe'], }); child.stdout.on('data', (c) => { out.push(c); if (opts.verbose) process.stdout.write(c); }); child.stderr.on('data', (c) => { err.push(c); if (opts.verbose) process.stderr.write(c); }); child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout: Buffer.concat(out).toString(), stderr: Buffer.concat(err).toString(), duration: performance.now() - start, })); child.on('error', (e) => resolve({ exitCode: 1, stdout: '', stderr: e.message, duration: performance.now() - start, })); }); } const fail = (stderr) => ({ exitCode: 1, stdout: '', stderr, duration: 0 }); const cleanup = (dir) => rm(dir, { recursive: true, force: true }).catch(() => { }); /** Check whether the reader has a section with the given id. */ function hasSection(reader, id) { return reader.getSections().some((s) => s.id === id); } /** Safely extract a section, returning null if absent. */ function tryExtract(reader, id) { try { return reader.extractSection(id); } catch { return null; } } // ── Runner ────────────────────────────────────────────────── export class RvfaRunner { reader; header; constructor(reader) { this.reader = reader; this.header = reader.getHeader(); } /** Read and parse an RVFA file from disk. Throws on invalid input. */ static async fromFile(rvfaPath) { const reader = await RvfaReader.fromFile(rvfaPath); return new RvfaRunner(reader); } /** Create a runner from an already-loaded buffer. */ static fromBuffer(buf) { return new RvfaRunner(RvfaReader.fromBuffer(buf)); } /** * Boot the appliance: verify integrity, then dispatch to the * requested isolation strategy and run mode. */ async boot(options) { const { valid, errors } = this.reader.verify(); if (!valid) { return fail(`Integrity check failed:\n${errors.join('\n')}`); } if (options.mode === 'verify') return this.runVerify(options); if (options.isolation === 'container') return this.runContainer(options); return this.runNative(options); } /** * Run natively via Node.js: extract RUFLO section to a temp dir, * configure env vars, optionally decrypt API-key vault, and spawn. */ async runNative(options) { const workDir = join(tmpdir(), `rvfa-${this.header.name}-${Date.now()}`); try { await mkdir(workDir, { recursive: true }); const ruflo = tryExtract(this.reader, 'ruflo'); if (!ruflo) return fail('RVFA appliance does not contain a "ruflo" section'); const entryFile = join(workDir, 'ruflo-bundle.js'); await writeFile(entryFile, ruflo); const env = { ...this.header.boot.env, RVFA_APPLIANCE_NAME: this.header.name, RVFA_APPLIANCE_VERSION: this.header.appVersion, RVFA_RUN_MODE: options.mode, RVFA_PROFILE: this.header.profile, }; if (options.passphrase && this.header.models.provider !== 'ruvllm') { const vault = tryExtract(this.reader, 'models'); if (vault) { const keys = await this.decryptVault(vault, options.passphrase); if (keys) Object.assign(env, keys); } } const args = [...this.header.boot.args]; if (options.mode === 'mcp') args.push('--mcp', '--transport', 'stdio'); return spawnAsync(this.header.boot.entrypoint || 'node', [entryFile, ...args], { cwd: workDir, env, verbose: options.verbose, }); } finally { await cleanup(workDir); } } /** * Run in a Docker container: generate a Dockerfile from the * extracted sections, build the image, and run it. */ async runContainer(options) { const dockerCheck = await spawnAsync('docker', ['info'], { verbose: false }); if (dockerCheck.exitCode !== 0) { return fail('Docker is not available. Install Docker or use isolation: "native".'); } const workDir = join(tmpdir(), `rvfa-container-${Date.now()}`); try { await mkdir(workDir, { recursive: true }); const ruflo = tryExtract(this.reader, 'ruflo'); if (!ruflo) return fail('RVFA appliance does not contain a "ruflo" section'); await writeFile(join(workDir, 'ruflo-bundle.js'), ruflo); const data = tryExtract(this.reader, 'data'); if (data) await writeFile(join(workDir, 'data.bin'), data); const envFlags = []; for (const [k, v] of Object.entries(this.header.boot.env)) envFlags.push('-e', `${k}=${v}`); envFlags.push('-e', `RVFA_RUN_MODE=${options.mode}`, '-e', `RVFA_PROFILE=${this.header.profile}`); const baseImage = this.header.platform === 'alpine' ? 'node:20-alpine' : 'node:20-slim'; const cmdArgs = this.header.boot.args.map((a) => `, "${a}"`).join(''); const dockerfile = [ `FROM ${baseImage}`, 'WORKDIR /app', 'COPY ruflo-bundle.js .', data ? 'COPY data.bin .' : '', `CMD ["node", "ruflo-bundle.js"${cmdArgs}]`, ].filter(Boolean).join('\n'); await writeFile(join(workDir, 'Dockerfile'), dockerfile); const imageName = `rvfa-${this.header.name}:${this.header.appVersion}`.toLowerCase(); const build = await spawnAsync('docker', ['build', '-t', imageName, '.'], { cwd: workDir, verbose: options.verbose, }); if (build.exitCode !== 0) { return { ...build, stderr: `Docker build failed:\n${build.stderr}` }; } return spawnAsync('docker', ['run', '--rm', ...envFlags, imageName], { verbose: options.verbose }); } finally { await cleanup(workDir); } } /** * Run the verification suite. Extracts the VERIFY section and * executes it; falls back to a basic integrity report. */ async runVerify(options) { const start = performance.now(); const verifyPayload = tryExtract(this.reader, 'verify'); if (!verifyPayload) { const { valid, errors } = this.reader.verify(); const lines = [ `Appliance: ${this.header.name} v${this.header.appVersion}`, `Profile: ${this.header.profile}`, `Sections: ${this.header.sections.length}`, `Integrity: ${valid ? 'PASS' : 'FAIL'}`, ...errors.map((e) => ` FAIL: ${e}`), errors.length === 0 ? ' All checks PASS' : '', ]; return { exitCode: valid ? 0 : 1, stdout: lines.filter(Boolean).join('\n'), stderr: '', duration: performance.now() - start, }; } const workDir = join(tmpdir(), `rvfa-verify-${Date.now()}`); try { await mkdir(workDir, { recursive: true }); await writeFile(join(workDir, 'verify.js'), verifyPayload); return spawnAsync('node', [join(workDir, 'verify.js')], { cwd: workDir, verbose: options.verbose, env: { RVFA_APPLIANCE_NAME: this.header.name, RVFA_APPLIANCE_VERSION: this.header.appVersion }, }); } finally { await cleanup(workDir); } } /** Return appliance metadata without booting. */ getInfo() { const sections = this.reader.getSections(); const totalSize = sections.reduce((sum, s) => sum + s.size, 0); return { header: { ...this.header }, sections: sections.map((s) => ({ id: s.id, size: s.size, originalSize: s.originalSize })), totalSize, }; } // ── Private ─────────────────────────────────────────────── /** * Decrypt an API-key vault (AES-256-GCM). * Layout: [16-byte IV][ciphertext][16-byte auth-tag] * Key derived via PBKDF2 with salt = "rvfa-vault-{name}". */ async decryptVault(payload, passphrase) { try { const { createDecipheriv, pbkdf2Sync } = await import('node:crypto'); if (payload.length < 33) return null; const iv = payload.subarray(0, 16); const tag = payload.subarray(payload.length - 16); const ciphertext = payload.subarray(16, payload.length - 16); const key = pbkdf2Sync(passphrase, Buffer.from(`rvfa-vault-${this.header.name}`), 100_000, 32, 'sha256'); const decipher = createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(tag); const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return JSON.parse(dec.toString('utf-8')); } catch { return null; } } } //# sourceMappingURL=rvfa-runner.js.map