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

205 lines 8.4 kB
/** * ADR-095 G2 — pluggable transport for hive-mind consensus protocols. * * The raft/byzantine/gossip consensus implementations historically used a * local `EventEmitter` for *everything* — both observability events * ("leader.elected", "consensus.achieved") AND inter-node messages * (append-entries, vote requests, pre-prepare/prepare/commit). The latter * never actually crossed a process or node boundary: a node "sent" a * message by `emit`ting it locally and synthesizing the peer's reply * inline. That's the single-process limitation #G2 names. * * This module separates the inter-node-message dimension behind a * `ConsensusTransport` interface. Two implementations: * * - `LocalTransport` — an in-process registry. Multiple consensus * instances in the same Node process share a registry and deliver * messages to each other synchronously. Matches the current * single-process behavior; the default so nothing breaks. * - `FederationTransport` (separate file, ADR-104 wire) — serializes * ConsensusMessages into federation envelopes, signs them with the * node's Ed25519 key, sends over WS via agentic-flow/transport/loader, * and dispatches inbound envelopes with signature verification. * * Observability events stay on the consensus class's own EventEmitter — * this is purely the messaging layer. * * No new dependencies: Ed25519 signing uses Node's built-in `crypto` * (`generateKeyPairSync('ed25519')` + `sign`/`verify` with `null` algorithm, * which is correct for Ed25519). */ import { createHash, generateKeyPairSync, sign as cryptoSign, verify as cryptoVerify, createPrivateKey, createPublicKey } from 'node:crypto'; /** Generate a fresh Ed25519 keypair for a consensus node. */ export function generateNodeKeyPair() { const { privateKey, publicKey } = generateKeyPairSync('ed25519'); return { privateKeyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }).toString(), publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }).toString(), }; } /** * Recursively sort object keys so JSON serialization is deterministic * regardless of insertion order — at every nesting level, not just the top. * Arrays keep their order (order is semantically meaningful, e.g. log entries). */ function deepSortKeys(v) { if (Array.isArray(v)) return v.map(deepSortKeys); if (v && typeof v === 'object') { const out = {}; for (const k of Object.keys(v).sort()) { const val = v[k]; if (val !== undefined) out[k] = deepSortKeys(val); } return out; } return v; } /** * Canonical byte string for signing. Deterministic across hosts: deep-sorted-key * JSON of the message's content fields (everything except `signature`). */ export function canonicalizeForSigning(msg) { return Buffer.from(JSON.stringify(deepSortKeys(msg)), 'utf-8'); } /** Stable digest of a message's content — handy for dedup and logging. */ export function messageDigest(msg) { return createHash('sha256').update(canonicalizeForSigning(msg)).digest('hex'); } /** Sign a message with an Ed25519 private key (PEM). Returns base64 signature. */ export function signMessage(msg, privateKeyPem) { const key = createPrivateKey(privateKeyPem); const sig = cryptoSign(null, canonicalizeForSigning(msg), key); return sig.toString('base64'); } /** * Verify a signed message against a peer's Ed25519 public key (PEM). * Returns true iff the signature is present and valid over the message's * content fields. Fail-closed: a missing signature returns false. */ export function verifyMessage(msg, publicKeyPem) { if (typeof msg.signature !== 'string' || msg.signature.length === 0) return false; try { const { signature, ...content } = msg; const key = createPublicKey(publicKeyPem); return cryptoVerify(null, canonicalizeForSigning(content), key, Buffer.from(signature, 'base64')); } catch { return false; } } // --------------------------------------------------------------------------- // LocalTransport — in-process registry. The default. Matches single-process. // --------------------------------------------------------------------------- /** * Shared registry of LocalTransport instances. Multiple consensus nodes in * the same process register here; send/broadcast deliver to peers' handlers. * Use a fresh registry per test to keep tests isolated. */ export class LocalTransportRegistry { nodes = new Map(); register(t) { this.nodes.set(t.nodeId, t); } unregister(nodeId) { this.nodes.delete(nodeId); } get(nodeId) { return this.nodes.get(nodeId); } peerIds(exclude) { return [...this.nodes.keys()].filter(id => id !== exclude); } } /** Process-wide default registry. Tests should pass their own. */ export const defaultLocalRegistry = new LocalTransportRegistry(); export class LocalTransport { nodeId; registry; defaultTimeoutMs; keyPair; resolvePeerPublicKey; handler = null; closed = false; seqCounter = 0; /** Per-sender last-seen seq for replay defense (only used when signed). */ lastSeenSeq = new Map(); constructor(nodeId, opts = {}) { this.nodeId = nodeId; this.registry = opts.registry ?? defaultLocalRegistry; this.defaultTimeoutMs = opts.defaultTimeoutMs ?? 5_000; this.keyPair = opts.keyPair; this.resolvePeerPublicKey = opts.resolvePeerPublicKey; this.registry.register(this); } onMessage(handler) { this.handler = handler; } peers() { return this.registry.peerIds(this.nodeId); } stamp(msg) { const base = { ...msg, from: this.nodeId, seq: this.keyPair ? ++this.seqCounter : msg.seq, }; if (this.keyPair) { return { ...base, signature: signMessage(base, this.keyPair.privateKeyPem) }; } return base; } /** Deliver an inbound message to a target's handler, with optional sig + replay checks. */ async deliver(target, msg) { if (target.closed) throw new Error(`LocalTransport: peer ${target.nodeId} is closed`); // Verification path — only when the *target* expects signed messages. if (target.keyPair && target.resolvePeerPublicKey) { const pub = target.resolvePeerPublicKey(msg.from); if (!pub || !verifyMessage(msg, pub)) { throw new Error(`LocalTransport: signature verification failed for message from ${msg.from}`); } // Replay defense: seq must be strictly increasing per sender. if (typeof msg.seq === 'number') { const last = target.lastSeenSeq.get(msg.from) ?? 0; if (msg.seq <= last) throw new Error(`LocalTransport: replayed/out-of-order seq from ${msg.from} (${msg.seq} <= ${last})`); target.lastSeenSeq.set(msg.from, msg.seq); } } if (!target.handler) return null; const reply = await target.handler(msg); return (reply ?? null); } async send(to, msg, timeoutMs) { if (this.closed) throw new Error('LocalTransport: closed'); const target = this.registry.get(to); if (!target) throw new Error(`LocalTransport: unreachable peer ${to}`); const stamped = this.stamp(msg); const t = timeoutMs ?? this.defaultTimeoutMs; return Promise.race([ this.deliver(target, stamped), new Promise((_, rej) => setTimeout(() => rej(new Error(`LocalTransport: send to ${to} timed out (${t}ms)`)), t)), ]); } async broadcast(msg) { if (this.closed) throw new Error('LocalTransport: closed'); const stamped = this.stamp(msg); await Promise.allSettled(this.registry.peerIds(this.nodeId).map(id => { const target = this.registry.get(id); return target ? this.deliver(target, stamped).catch(() => { }) : Promise.resolve(); })); } async close() { this.closed = true; this.handler = null; this.registry.unregister(this.nodeId); } } //# sourceMappingURL=transport.js.map