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

209 lines (191 loc) 7.85 kB
/** * Inbound message dispatcher (ADR-109). * * The federation plugin's `transport.listen()` accepts inbound bytes * but until this module landed, the bytes had no consumer — they sat * in the transport's per-address message queue with no one polling. * * `dispatchInbound()` is what the plugin registers as the * transport.onMessage handler. For each received message it: * * 1. Resolves the sender via `sourceNodeId` in metadata * 2. Verifies the peer is in the discovery registry (unknown → reject) * 3. Verifies peer is ACTIVE (SUSPENDED/EVICTED → reject defense-in-depth) * 4. Audits success or rejection * 5. Emits a typed event on the eventBus for the integrator to handle * * Anti-coupling: dispatcher does NOT actually execute inbound tasks. * That's the integrator's job. The dispatcher's contract is "deliver * an envelope to the integrator's handler IFF it passes safety gates." */ import type { AgentMessage } from 'agentic-flow/transport/loader'; import type { DiscoveryService } from '../domain/services/discovery-service.js'; import type { AuditService } from '../domain/services/audit-service.js'; import { FederationNodeState } from '../domain/value-objects/federation-node-state.js'; /** What gets emitted on the event bus per messageType. */ export const FEDERATION_INBOUND_EVENT_PREFIX = 'federation:inbound'; /** Reasons we reject an inbound message — constant strings, no oracle leak. */ export type InboundRejectionReason = | 'PEER_UNKNOWN' | 'PEER_SUSPENDED' | 'PEER_EVICTED' | 'MISSING_METADATA' | 'INVALID_PAYLOAD' | 'INVALID_SIGNATURE'; /** * Verifier function for inbound envelopes. Given the canonical bytes of * the message + the claimed signature + the peer's published public * key, returns true iff the signature is valid. Plugin wires this with * `@noble/ed25519`; tests inject a mock. * * Returning `null` means "no signature provided" — handled by the * dispatcher as INVALID_SIGNATURE (defense: unsigned messages from * known peers are still rejected). */ export type EnvelopeVerifier = ( canonicalBytes: string, signatureHex: string | null, peerPublicKeyHex: string, ) => boolean; /** Dispatch dependencies (kept narrow for testability). */ export interface InboundDispatchDeps { readonly discovery: Pick<DiscoveryService, 'getPeer'>; readonly audit: Pick<AuditService, 'log'>; readonly eventBus: { emit: (event: string, data: unknown) => void }; readonly logger: { debug: (m: string) => void; warn: (m: string) => void; }; /** * Optional Ed25519 envelope verifier. When PROVIDED, every accepted * message MUST pass verification — `null` signature or false-returning * verifier rejects the message as INVALID_SIGNATURE. * * When OMITTED, the dispatcher operates in legacy "trust the metadata" * mode (backward compat for tests that inject minimal deps). Production * MUST inject this — see the plugin.ts wiring. */ readonly verifyEnvelope?: EnvelopeVerifier; } /** * Canonical serialization of an envelope for signing. Sorts keys to * make the signed bytes deterministic regardless of object construction * order. Excludes the `signature` field itself (it's what we're * verifying). * * Federation messages are wrapped as `AgentMessage{id, type, payload, * metadata}` on the wire. The `payload` is the actual FederationEnvelope * (per `plugin.ts sendToNode`); we canonicalize the payload + the * metadata so the receiver verifies the same bytes the sender signed. */ export function canonicalizeEnvelopeForVerify(message: AgentMessage): string { const meta = (message.metadata ?? {}) as Record<string, unknown>; // Strip signature from metadata if present (we verify the rest) const { signature: _sig, ...metaForSig } = meta; const canon = { id: message.id, type: message.type, payload: message.payload, metadata: metaForSig, }; // Sort keys for determinism return JSON.stringify(canon, Object.keys(canon).sort()); } /** Outcome reported to the caller (mostly for tests + observability). */ export type InboundDispatchOutcome = | { readonly accepted: true; readonly sourceNodeId: string; readonly messageType: string } | { readonly accepted: false; readonly reason: InboundRejectionReason }; /** * Process one received message. Pure-ish: no side effects beyond audit * + event emission, both injected. * * `address` is the wire-level remote (e.g. `192.168.1.42:54321`). * `message.metadata.sourceNodeId` is the cryptographic identity claim. * The two MAY differ (e.g. behind NAT) — we trust `sourceNodeId` for * peer lookup since it's bound to the Ed25519 keypair. */ export async function dispatchInbound( address: string, message: AgentMessage, deps: InboundDispatchDeps, ): Promise<InboundDispatchOutcome> { const meta = (message.metadata ?? {}) as Record<string, unknown>; const sourceNodeId = typeof meta.sourceNodeId === 'string' ? meta.sourceNodeId : null; if (!sourceNodeId) { await deps.audit.log('message_rejected', { sourceNodeId: undefined, metadata: { address, reason: 'MISSING_METADATA' }, }); deps.logger.warn(`Inbound rejected: no sourceNodeId in metadata (from ${address})`); return { accepted: false, reason: 'MISSING_METADATA' }; } const peer = deps.discovery.getPeer(sourceNodeId); if (!peer) { await deps.audit.log('message_rejected', { sourceNodeId, metadata: { address, reason: 'PEER_UNKNOWN' }, }); deps.logger.warn(`Inbound rejected: ${sourceNodeId} not in discovery (from ${address})`); return { accepted: false, reason: 'PEER_UNKNOWN' }; } if (peer.state === FederationNodeState.SUSPENDED) { await deps.audit.log('message_rejected', { sourceNodeId, metadata: { address, reason: 'PEER_SUSPENDED' }, }); return { accepted: false, reason: 'PEER_SUSPENDED' }; } if (peer.state === FederationNodeState.EVICTED) { await deps.audit.log('message_rejected', { sourceNodeId, metadata: { address, reason: 'PEER_EVICTED' }, }); return { accepted: false, reason: 'PEER_EVICTED' }; } // Cryptographic signature verification (closes the trust gate that // peer-state checks alone don't provide — without this, a malicious // sender could just claim sourceNodeId='known-peer' in metadata and // pass the previous gates). if (deps.verifyEnvelope) { const sig = typeof meta.signature === 'string' ? meta.signature : null; const canon = canonicalizeEnvelopeForVerify(message); const ok = deps.verifyEnvelope(canon, sig, peer.publicKey); if (!ok) { await deps.audit.log('message_rejected', { sourceNodeId, metadata: { address, reason: 'INVALID_SIGNATURE' }, }); deps.logger.warn(`Inbound rejected: bad signature from ${sourceNodeId} (addr=${address})`); return { accepted: false, reason: 'INVALID_SIGNATURE' }; } } // Touch lastSeen on every successful inbound — drives the // discovery service's stale-peer detection. peer.markSeen(); // Audit accepted delivery await deps.audit.log('message_received', { sourceNodeId, metadata: { address, messageType: message.type, messageId: message.id, }, }); // Emit typed event for the integrator // Examples: federation:inbound:task, federation:inbound:memory-query const eventName = `${FEDERATION_INBOUND_EVENT_PREFIX}:${message.type}`; try { deps.eventBus.emit(eventName, { address, sourceNodeId, message, peer, }); } catch (err) { // EventBus throw should not crash the receive loop. deps.logger.warn( `EventBus emit failed for ${eventName}: ${err instanceof Error ? err.message : err}`, ); } return { accepted: true, sourceNodeId, messageType: message.type }; }