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
text/typescript
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));
},
},
];
}