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

468 lines (461 loc) 21.5 kB
import type { MCPToolDefinition } from '@claude-flow/shared/src/plugin-interface.js'; import type { PluginContext } from '@claude-flow/shared/src/plugin-interface.js'; import type { FederationCoordinator } from './application/federation-coordinator.js'; import type { FederationMessageType } from './domain/entities/federation-envelope.js'; import type { WgMeshService } from './domain/services/wg-mesh-service.js'; import { generateWgKeyPair } from './domain/value-objects/wg-config.js'; type CoordinatorGetter = () => FederationCoordinator | null; type ContextGetter = () => PluginContext | null; type WgMeshGetter = () => WgMeshService | null; function textResult(text: string, isError = false) { return { content: [{ type: 'text' as const, text }], isError }; } export function createMcpTools( getCoordinator: CoordinatorGetter, getContext: ContextGetter, getWgMesh: WgMeshGetter = () => null, ): MCPToolDefinition[] { function requireCoordinator(): FederationCoordinator { const c = getCoordinator(); if (!c) throw new Error('Federation not initialized'); return c; } function requireWgMesh(): WgMeshService { const w = getWgMesh(); if (!w) throw new Error('WG mesh layer not initialized (set config.wgMesh = true and inject WgMeshService)'); return w; } return [ { name: 'federation_init', description: 'Initialize federation on this node with a manifest and begin discovery', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Unique node identifier' }, endpoint: { type: 'string', description: 'WebSocket or HTTP endpoint for this node' }, agentTypes: { type: 'array', description: 'Supported agent types', items: { type: 'string' } }, }, required: ['endpoint'], }, handler: async (params) => { const coordinator = requireCoordinator(); const nodeId = (params['nodeId'] as string) ?? `node-${Date.now().toString(36)}`; await coordinator.initialize({ nodeId, publicKey: '', endpoint: params['endpoint'] as string, capabilities: { agentTypes: (params['agentTypes'] as string[]) ?? ['coder', 'reviewer'], maxConcurrentSessions: 10, supportedProtocols: ['websocket', 'http'], complianceModes: [], }, version: '1.0.0-alpha.1', timestamp: new Date().toISOString(), }); return textResult(`Federation initialized for node ${nodeId}`); }, }, { name: 'federation_join', description: 'Join a federation by connecting to a remote peer endpoint', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { endpoint: { type: 'string', description: 'Remote peer endpoint to join' }, }, required: ['endpoint'], }, handler: async (params) => { const coordinator = requireCoordinator(); const session = await coordinator.joinPeer(params['endpoint'] as string); return textResult(`Joined peer. Session: ${session.sessionId}, Trust: ${session.trustLevel}`); }, }, { name: 'federation_peers', description: 'List all known federation peers with their trust levels and status', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: {}, }, handler: async () => { const coordinator = requireCoordinator(); const status = coordinator.getStatus(); return textResult(JSON.stringify(status.trustLevels, null, 2)); }, }, { name: 'federation_send', description: 'Send a message to a federated peer through the PII pipeline and security gates. Optional budget controls (ADR-097): maxHops defaults to 8 to prevent recursive delegation; maxTokens/maxUsd cap cumulative spend across the hop chain.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { targetNodeId: { type: 'string', description: 'Target node ID' }, messageType: { type: 'string', description: 'Message type (task-assignment, memory-query, context-share, etc.)' }, payload: { type: 'object', description: 'Message payload' }, // ADR-097 Phase 1: optional budget envelope. Backward compatible — // omitting these preserves the legacy unbounded-tokens/USD path, // with maxHops still defaulting to 8 to defang recursion loops. budget: { type: 'object', description: 'Optional cumulative spend budget for this delegation chain', properties: { maxTokens: { type: 'number', description: 'Hard cap on Σ tokens across hops' }, maxUsd: { type: 'number', description: 'Hard cap on Σ USD spent' }, }, }, maxHops: { type: 'number', description: 'Maximum hops this message may travel (default 8, 0 disallows remote delegation)', }, hopCount: { type: 'number', description: 'How many hops this message has already taken (0 on the originator)', }, spent: { type: 'object', description: 'Caller-reported usage from previous legs (cumulative)', properties: { tokens: { type: 'number' }, usd: { type: 'number' }, }, }, }, required: ['targetNodeId', 'messageType', 'payload'], }, handler: async (params) => { const coordinator = requireCoordinator(); const result = await coordinator.sendMessage( params['targetNodeId'] as string, params['messageType'] as FederationMessageType, params['payload'], { budget: params['budget'] as { maxTokens?: number; maxUsd?: number } | undefined, maxHops: params['maxHops'] as number | undefined, hopCount: params['hopCount'] as number | undefined, spent: params['spent'] as { tokens?: number; usd?: number } | undefined, }, ); return textResult(JSON.stringify(result, null, 2), !result.success); }, }, { name: 'federation_query', description: 'Query federated memory from a remote peer (PII-gated)', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { targetNodeId: { type: 'string', description: 'Target node ID to query' }, query: { type: 'string', description: 'Memory query string' }, namespace: { type: 'string', description: 'Memory namespace to query' }, }, required: ['targetNodeId', 'query'], }, handler: async (params) => { const coordinator = requireCoordinator(); const result = await coordinator.sendMessage( params['targetNodeId'] as string, 'memory-query', { query: params['query'], namespace: params['namespace'] ?? 'default' }, ); return textResult(JSON.stringify(result, null, 2), !result.success); }, }, { name: 'federation_status', description: 'Get federation health status including active sessions, peers, and trust levels', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: {}, }, handler: async () => { const coordinator = requireCoordinator(); return textResult(JSON.stringify(coordinator.getStatus(), null, 2)); }, }, { name: 'federation_trust', description: 'View or review trust score details for a specific node', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Node ID to inspect' }, }, required: ['nodeId'], }, handler: async (params) => { const coordinator = requireCoordinator(); const status = coordinator.getStatus(); const nodeId = params['nodeId'] as string; const trustLevel = status.trustLevels[nodeId]; if (trustLevel === undefined) { return textResult(`Node ${nodeId} not found`, true); } return textResult(JSON.stringify({ nodeId, trustLevel }, null, 2)); }, }, { name: 'federation_audit', description: 'Query federation audit logs with optional compliance mode filtering', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { eventType: { type: 'string', description: 'Filter by event type' }, severity: { type: 'string', description: 'Filter by severity (info, warn, error, critical)' }, since: { type: 'string', description: 'ISO 8601 date to filter events since' }, limit: { type: 'number', description: 'Maximum number of results' }, }, }, handler: async (params) => { const context = getContext(); if (!context) return textResult('Plugin not initialized', true); const audit = context.services.get<import('./domain/services/audit-service.js').AuditService>('federation:audit'); if (!audit) return textResult('Audit service not found', true); const events = await audit.query({ eventType: params['eventType'] as any, severity: params['severity'] as any, since: params['since'] ? new Date(params['since'] as string) : undefined, limit: (params['limit'] as number) ?? 50, }); return textResult(JSON.stringify(events, null, 2)); }, }, { name: 'federation_breaker_status', description: 'ADR-097 Phase 4: per-peer circuit-breaker state snapshot. Returns each known peer with its lifecycle state (ACTIVE/SUSPENDED/EVICTED), when it changed, and why. Combine with federation_evict / federation_reactivate to operate the breaker manually.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Optional: filter to a single peer' }, }, }, handler: async (params) => { const coordinator = requireCoordinator(); const states = coordinator.getPeerStates(); const filter = params['nodeId'] as string | undefined; const filtered = filter ? states.filter((s) => s.nodeId === filter) : states; const counts = coordinator.getPeerStateCounts(); return textResult(JSON.stringify({ counts, peers: filtered }, null, 2)); }, }, { name: 'federation_evict', description: 'ADR-097 Phase 4: operator-initiated evict for a peer. Marks the peer EVICTED so subsequent federation_send calls short-circuit with PEER_EVICTED. Reversible only via federation_reactivate (operator override).', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Target peer to evict' }, correlationId: { type: 'string', description: 'Optional audit-trail correlation key (operator ticket, etc.)' }, }, required: ['nodeId'], }, handler: async (params) => { const coordinator = requireCoordinator(); const ok = await coordinator.evictPeer( params['nodeId'] as string, 'MANUAL_EVICT', params['correlationId'] as string | undefined, ); const msg = ok ? `Evicted peer ${params['nodeId']}` : `No transition (peer unknown or already EVICTED): ${params['nodeId']}`; return textResult(msg, !ok); }, }, { name: 'federation_reactivate', description: 'ADR-097 Phase 4: operator-initiated reactivate for a SUSPENDED or EVICTED peer. Used after a health probe confirms recovery, or as an operator override on a manual evict. The breaker does NOT auto-reactivate; this MCP tool is the explicit lever.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Target peer to reactivate' }, correlationId: { type: 'string', description: 'Optional audit-trail correlation key (probe ID, ticket, etc.)' }, }, required: ['nodeId'], }, handler: async (params) => { const coordinator = requireCoordinator(); const ok = await coordinator.reactivatePeer( params['nodeId'] as string, params['correlationId'] as string | undefined, ); const msg = ok ? `Reactivated peer ${params['nodeId']}` : `No transition (peer unknown or already ACTIVE): ${params['nodeId']}`; return textResult(msg, !ok); }, }, { name: 'federation_report_spend', description: 'ADR-097 Phase 3 upstream: report the actual cost of a completed federated call. Fans out to the cost-tracker bus (via the integrator-wired SpendReporter) and the breaker service (so its in-memory rolling buffer is fed). Both targets are optional; calling without either configured is a silent no-op.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { peerId: { type: 'string', description: 'Peer this cost was incurred against' }, taskId: { type: 'string', description: 'Optional task correlation key' }, tokensUsed: { type: 'number', description: 'Tokens consumed (input + output)' }, usdSpent: { type: 'number', description: 'USD spent' }, success: { type: 'boolean', description: 'Whether the underlying send succeeded (drives breaker failure-ratio)' }, ts: { type: 'string', description: 'ISO 8601 timestamp; auto-filled if omitted' }, }, required: ['peerId', 'tokensUsed', 'usdSpent', 'success'], }, handler: async (params) => { const coordinator = requireCoordinator(); await coordinator.reportSpend({ peerId: params['peerId'] as string, taskId: params['taskId'] as string | undefined, tokensUsed: params['tokensUsed'] as number, usdSpent: params['usdSpent'] as number, success: params['success'] as boolean, ts: params['ts'] as string | undefined, }); return textResult('Spend reported'); }, }, { name: 'federation_consensus', description: 'Propose a federated consensus operation across all active peers', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { messageType: { type: 'string', description: 'Consensus message type (trust-change, topology-change, agent-spawn)' }, payload: { type: 'object', description: 'Consensus proposal payload' }, quorumFraction: { type: 'number', description: 'Required quorum fraction (default 2/3)' }, }, required: ['messageType', 'payload'], }, handler: async (params) => { const context = getContext(); if (!context) return textResult('Plugin not initialized', true); const routing = context.services.get<import('./domain/services/routing-service.js').RoutingService>('federation:routing'); if (!routing) return textResult('Routing service not found', true); const proposal = await routing.propose( params['messageType'] as FederationMessageType, params['payload'], (params['quorumFraction'] as number) ?? 2 / 3, ); return textResult(JSON.stringify({ proposalId: proposal.proposalId, messageType: proposal.messageType, quorumRequired: proposal.quorumRequired, expiresAt: proposal.expiresAt.toISOString(), }, null, 2)); }, }, { name: 'federation_wg_status', description: 'ADR-111 Phase 6: WG mesh state — per-peer trust level, mesh IP, suspended/evicted flags, and the AllowedIPs slice each peer currently has. Use to inspect what the breaker has propagated to the L3 layer.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Optional: filter to a single peer' }, }, }, handler: async (params) => { const wg = requireWgMesh(); const coordinator = requireCoordinator(); const peers = coordinator.listPeers(); const summary = wg.summarize(peers); const filter = params['nodeId'] as string | undefined; const filtered = filter ? summary.filter(s => s.nodeId === filter) : summary; return textResult(JSON.stringify({ interfaceName: wg.getInterfaceName(), meshSubnet: wg.getMeshSubnet(), peers: filtered, }, null, 2)); }, }, { name: 'federation_wg_attest', description: 'ADR-111 Phase 6 (witness chain): emit an operator-signed attestation entry for a coordination change. Returns the canonical bytes the operator signs + the witness entry; operator appends it to .claude-flow/federation/wg-changes.log. Idempotent — re-attesting the same change just appends another entry.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { eventType: { type: 'string', description: 'peer-added | peer-removed-suspended | peer-restored | peer-evicted | key-rotated | interface-config-applied' }, peerPublicKey: { type: 'string', description: 'Optional: peer pubkey (omit for interface-level events)' }, meshIP: { type: 'string', description: 'Optional: peer mesh IP' }, wgCommand: { type: 'string', description: 'Optional: wg command that was emitted' }, rationale: { type: 'string', description: 'Human-readable rationale for the audit log' }, }, required: ['eventType', 'rationale'], }, handler: async () => { // Phase 6 v1 stub — the WgWitnessService is the canonical signer. This // MCP tool returns instructions until plugin.ts wires the service. // Tracked alongside #1879 follow-up: integrate WgWitnessService into // plugin lifecycle so this handler can call attestWgCommand directly. return textResult( 'federation_wg_attest: wire WgWitnessService in plugin.ts (Phase 6 follow-up). Until then, use the WgWitnessService class directly from the federation package.', true, ); }, }, { name: 'federation_wg_keyrotate', description: 'ADR-111 Phase 6: rotate the local WG keypair. Returns the new public key + recommended next steps (republish manifest, peers regenerate their wg-quick config, grace-period destruction of old key). DESTRUCTIVE — the old private key is overwritten on disk; existing tunnels are dropped until peers update.', pluginName: '@claude-flow/plugin-agent-federation', version: '1.0.0-alpha.1', inputSchema: { type: 'object', properties: { confirm: { type: 'boolean', description: 'Must be true. Destructive: drops existing tunnels until all peers update their config.' }, }, required: ['confirm'], }, handler: async (params) => { if (params['confirm'] !== true) { return textResult('Refusing rotate: confirm must be true. This drops existing tunnels until peers fetch the new manifest.', true); } // Phase 6 v1: emits a fresh keypair + the upgrade checklist. plugin.ts // is responsible for the actual disk write + manifest republish; this // returns the new public key for the operator to coordinate. const newKey = generateWgKeyPair(); return textResult(JSON.stringify({ publicKey: newKey.publicKey, createdAt: newKey.createdAt, nextSteps: [ 'Write the new private key to .claude-flow/federation/wg-key-<nodeId>.json (mode 0600)', 'Republish federation manifest with the new wg.publicKey', 'Peers regenerate their wg-quick config from the updated manifest', 'Operator removes the old [Peer] block from /etc/wireguard/<iface>.conf after the grace period (default 1h)', 'Run federation_wg_attest with eventType=key-rotated', ], warning: 'Private key NOT returned via MCP. The operator wires the actual disk persistence.', }, null, 2)); }, }, ]; }