nucleus-mcp 1.0.7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/package.json +2 -4
  2. package/.github/workflows/ci.yml +0 -72
  3. package/.smithery/shttp/module.js +0 -3
  4. package/.smithery/shttp/module.js.map +0 -7
  5. package/.smithery/stdio/index.cjs +0 -74
  6. package/.smithery/stdio/index.cjs.map +0 -7
  7. package/CHANGELOG.md +0 -65
  8. package/CODE_OF_CONDUCT.md +0 -34
  9. package/COMPARISON.md +0 -20
  10. package/CONTRIBUTING.md +0 -137
  11. package/DEVELOPMENT.md +0 -109
  12. package/LICENSE +0 -21
  13. package/README.md +0 -319
  14. package/ROADMAP.md +0 -24
  15. package/SECURITY.md +0 -88
  16. package/docs/COMMUNITY_FAQ.md +0 -48
  17. package/docs/COMPETITIVE_ANALYSIS.md +0 -90
  18. package/docs/DEMO_SCRIPT.md +0 -86
  19. package/docs/ENTERPRISE.md +0 -169
  20. package/docs/FAQ.md +0 -228
  21. package/docs/QUICK_START.md +0 -155
  22. package/docs/README.md +0 -67
  23. package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
  24. package/docs/social-preview-hq.png +0 -0
  25. package/docs/social-preview.png +0 -0
  26. package/examples/basic_usage.py +0 -84
  27. package/package.json.real +0 -24
  28. package/pyproject.toml +0 -50
  29. package/registry-server.json +0 -20
  30. package/scripts/demo_governance.py +0 -109
  31. package/scripts/sse_bridge.py +0 -87
  32. package/scripts/verify_windows_paths.py +0 -48
  33. package/server.json +0 -20
  34. package/src/mcp_server_nucleus/__init__.py +0 -8583
  35. package/src/mcp_server_nucleus/__main__.py +0 -9
  36. package/src/mcp_server_nucleus/cli.py +0 -1225
  37. package/src/mcp_server_nucleus/commitment_core.py +0 -146
  38. package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
  39. package/src/mcp_server_nucleus/core/__init__.py +0 -6
  40. package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
  41. package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
  42. package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
  43. package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
  44. package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
  45. package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
  46. package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
  47. package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
  48. package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
  49. package/src/mcp_server_nucleus/runtime/agent.py +0 -517
  50. package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
  51. package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
  52. package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
  53. package/src/mcp_server_nucleus/runtime/auth.py +0 -98
  54. package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
  55. package/src/mcp_server_nucleus/runtime/broker.py +0 -126
  56. package/src/mcp_server_nucleus/runtime/budget.py +0 -153
  57. package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
  58. package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
  59. package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
  60. package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
  61. package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
  62. package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
  63. package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
  64. package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
  65. package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
  66. package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
  67. package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
  68. package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
  69. package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
  70. package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
  71. package/src/mcp_server_nucleus/runtime/common.py +0 -134
  72. package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
  73. package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
  74. package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
  75. package/src/mcp_server_nucleus/runtime/critic.py +0 -150
  76. package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
  77. package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
  78. package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
  79. package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
  80. package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
  81. package/src/mcp_server_nucleus/runtime/export.py +0 -64
  82. package/src/mcp_server_nucleus/runtime/factory.py +0 -594
  83. package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
  84. package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
  85. package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
  86. package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
  87. package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
  88. package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
  89. package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
  90. package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
  91. package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
  92. package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
  93. package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
  94. package/src/mcp_server_nucleus/runtime/installer.py +0 -81
  95. package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
  96. package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
  97. package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
  98. package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
  99. package/src/mcp_server_nucleus/runtime/locking.py +0 -228
  100. package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
  101. package/src/mcp_server_nucleus/runtime/memory.py +0 -158
  102. package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
  103. package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
  104. package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
  105. package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
  106. package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
  107. package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
  108. package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
  109. package/src/mcp_server_nucleus/runtime/policy.py +0 -117
  110. package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
  111. package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
  112. package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
  113. package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
  114. package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
  115. package/src/mcp_server_nucleus/runtime/registry.py +0 -82
  116. package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
  117. package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
  118. package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
  119. package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
  120. package/src/mcp_server_nucleus/runtime/storage.py +0 -83
  121. package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
  122. package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
  123. package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
  124. package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
  125. package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
  126. package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
  127. package/src/mcp_server_nucleus/runtime/team.py +0 -59
  128. package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
  129. package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
  130. package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
  131. package/src/mcp_server_nucleus/tool_tiers.py +0 -243
  132. package/tests/__init__.py +0 -1
  133. package/tests/conftest.py +0 -42
  134. package/tests/test_core.py +0 -235
  135. package/tests/test_search.py +0 -69
@@ -1,1040 +0,0 @@
1
- """
2
- NOP V3.1 Federation Engine - Distributed Multi-Brain Orchestration
3
-
4
- This module implements the federation layer for NOP, enabling multiple brains
5
- to coordinate across geographic regions with:
6
- - SWIM-based peer discovery and membership
7
- - Raft consensus for critical operations
8
- - CRDT-based state synchronization with Merkle tree verification
9
- - Composite task routing with pluggable strategies
10
- - Partition tolerance and automatic recovery
11
-
12
- v0.6.0 DSoR Integration:
13
- - DecisionMade events for all federation operations
14
- - Context hashing for state verification
15
- - IPC token security for cross-brain communication
16
-
17
- Author: NOP V3.1 Team
18
- Version: 1.1.0-DSoR
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import asyncio
24
- import hashlib
25
- import json
26
- import logging
27
- import random
28
- import time
29
- import uuid
30
- from abc import ABC, abstractmethod
31
- from dataclasses import dataclass, field
32
- from datetime import datetime
33
- from enum import Enum, auto
34
- from pathlib import Path
35
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple
36
-
37
- # v0.6.0 DSoR imports
38
- try:
39
- from .event_stream import emit_event, EventTypes
40
- DSOR_AVAILABLE = True
41
- except ImportError:
42
- DSOR_AVAILABLE = False
43
-
44
- logger = logging.getLogger(__name__)
45
-
46
- # =============================================================================
47
- # ENUMS AND CONSTANTS
48
- # =============================================================================
49
-
50
- class TrustLevel(Enum):
51
- OWNER = 4
52
- ADMIN = 3
53
- MEMBER = 2
54
- GUEST = 1
55
-
56
- class PeerStatus(Enum):
57
- UNKNOWN = auto()
58
- ONLINE = auto()
59
- SUSPECT = auto()
60
- OFFLINE = auto()
61
- QUARANTINED = auto()
62
-
63
- class PartitionStatus(Enum):
64
- NORMAL = auto()
65
- MAJORITY = auto()
66
- MINORITY = auto()
67
- ISOLATED = auto()
68
-
69
- class ConsistencyClass(Enum):
70
- CLASS_A = "critical"
71
- CLASS_B = "important"
72
- CLASS_C = "normal"
73
-
74
- class RaftState(Enum):
75
- FOLLOWER = auto()
76
- CANDIDATE = auto()
77
- LEADER = auto()
78
-
79
- DEFAULT_HEARTBEAT_INTERVAL = 1.0
80
- DEFAULT_HEARTBEAT_TIMEOUT = 5.0
81
- DEFAULT_SYNC_INTERVAL = 5.0
82
- DEFAULT_ELECTION_TIMEOUT_MIN = 150
83
- DEFAULT_ELECTION_TIMEOUT_MAX = 300
84
- DEFAULT_GOSSIP_FANOUT = 3
85
- DEFAULT_SUSPECT_TIMEOUT = 5.0
86
-
87
- # =============================================================================
88
- # CORE DATA STRUCTURES
89
- # =============================================================================
90
-
91
- @dataclass
92
- class VectorClock:
93
- """Vector clock for causal ordering across distributed brains."""
94
- clocks: Dict[str, int] = field(default_factory=dict)
95
-
96
- def increment(self, brain_id: str) -> 'VectorClock':
97
- new_clocks = self.clocks.copy()
98
- new_clocks[brain_id] = new_clocks.get(brain_id, 0) + 1
99
- return VectorClock(new_clocks)
100
-
101
- def merge(self, other: 'VectorClock') -> 'VectorClock':
102
- all_keys = set(self.clocks.keys()) | set(other.clocks.keys())
103
- merged = {k: max(self.clocks.get(k, 0), other.clocks.get(k, 0)) for k in all_keys}
104
- return VectorClock(merged)
105
-
106
- def happens_before(self, other: 'VectorClock') -> bool:
107
- dominated = all(self.clocks.get(k, 0) <= other.clocks.get(k, 0) for k in self.clocks)
108
- strictly_less = any(self.clocks.get(k, 0) < other.clocks.get(k, 0) for k in other.clocks)
109
- return dominated and strictly_less
110
-
111
- def to_dict(self) -> Dict[str, int]:
112
- return self.clocks.copy()
113
-
114
- @classmethod
115
- def from_dict(cls, data: Dict[str, int]) -> 'VectorClock':
116
- return cls(clocks=data.copy())
117
-
118
-
119
- class MerkleTree:
120
- """Merkle tree for efficient state comparison and synchronization."""
121
-
122
- def __init__(self):
123
- self.leaves: Dict[str, str] = {}
124
- self._root: str = ""
125
- self._dirty = True
126
-
127
- def update(self, key: str, value: bytes) -> None:
128
- value_hash = hashlib.sha256(value).hexdigest()
129
- self.leaves[key] = hashlib.sha256(f"{key}:{value_hash}".encode()).hexdigest()
130
- self._dirty = True
131
-
132
- def remove(self, key: str) -> None:
133
- if key in self.leaves:
134
- del self.leaves[key]
135
- self._dirty = True
136
-
137
- def get_root(self) -> str:
138
- if self._dirty:
139
- if not self.leaves:
140
- self._root = ""
141
- else:
142
- sorted_hashes = sorted(self.leaves.values())
143
- combined = ":".join(sorted_hashes)
144
- self._root = hashlib.sha256(combined.encode()).hexdigest()
145
- self._dirty = False
146
- return self._root
147
-
148
-
149
- @dataclass
150
- class FederationPeer:
151
- """Represents a remote brain in the federation."""
152
- peer_id: str
153
- address: str
154
- region: str
155
- trust_level: TrustLevel = TrustLevel.MEMBER
156
- capabilities: Set[str] = field(default_factory=set)
157
- status: PeerStatus = PeerStatus.UNKNOWN
158
- last_heartbeat: Optional[datetime] = None
159
- last_sync: Optional[datetime] = None
160
- vector_clock: VectorClock = field(default_factory=VectorClock)
161
- merkle_root: str = ""
162
- latency_ms: float = 0.0
163
- load: float = 0.0
164
- version: str = "1.0.0"
165
- incarnation: int = 0
166
- suspect_time: Optional[datetime] = None
167
-
168
- def is_online(self) -> bool:
169
- return self.status == PeerStatus.ONLINE
170
-
171
- def is_healthy(self) -> bool:
172
- return self.status in (PeerStatus.ONLINE, PeerStatus.SUSPECT)
173
-
174
- def to_dict(self) -> Dict[str, Any]:
175
- return {
176
- "peer_id": self.peer_id, "address": self.address, "region": self.region,
177
- "trust_level": self.trust_level.name, "capabilities": list(self.capabilities),
178
- "status": self.status.name, "latency_ms": self.latency_ms, "load": self.load,
179
- "last_heartbeat": self.last_heartbeat.isoformat() if self.last_heartbeat else None,
180
- }
181
-
182
- @classmethod
183
- def from_dict(cls, data: Dict[str, Any]) -> 'FederationPeer':
184
- return cls(
185
- peer_id=data["peer_id"], address=data["address"],
186
- region=data.get("region", "unknown"),
187
- trust_level=TrustLevel[data.get("trust_level", "MEMBER")],
188
- capabilities=set(data.get("capabilities", [])),
189
- status=PeerStatus[data.get("status", "UNKNOWN")],
190
- latency_ms=data.get("latency_ms", 0.0), load=data.get("load", 0.0),
191
- )
192
-
193
-
194
- @dataclass
195
- class RoutingDecision:
196
- """Result of task routing computation."""
197
- target_brain: str
198
- score: float
199
- factors: Dict[str, float]
200
- alternatives: List[Tuple[str, float]]
201
- routing_time_ms: float
202
-
203
-
204
- @dataclass
205
- class RoutingProfile:
206
- """Weights for routing decision factors."""
207
- skill_weight: float = 0.3
208
- latency_weight: float = 0.2
209
- load_weight: float = 0.2
210
- cost_weight: float = 0.2
211
- affinity_weight: float = 0.1
212
-
213
- ROUTING_PROFILES: Dict[str, RoutingProfile] = {
214
- "default": RoutingProfile(),
215
- "realtime": RoutingProfile(0.2, 0.5, 0.2, 0.0, 0.1),
216
- "batch": RoutingProfile(0.3, 0.0, 0.3, 0.3, 0.1),
217
- "premium": RoutingProfile(0.5, 0.1, 0.2, 0.0, 0.2),
218
- "budget": RoutingProfile(0.2, 0.0, 0.2, 0.5, 0.1),
219
- }
220
-
221
-
222
- @dataclass
223
- class RaftLogEntry:
224
- """Entry in the Raft consensus log."""
225
- term: int
226
- index: int
227
- command: Dict[str, Any]
228
- timestamp: datetime = field(default_factory=datetime.utcnow)
229
-
230
-
231
- @dataclass
232
- class SyncResult:
233
- """Result of a state synchronization operation."""
234
- success: bool
235
- peer_id: str
236
- items_synced: int
237
- conflicts_resolved: int
238
- sync_time_ms: float
239
- new_merkle_root: str
240
- error: Optional[str] = None
241
-
242
-
243
- @dataclass
244
- class FederationConfig:
245
- """Configuration for federation engine."""
246
- brain_id: str
247
- region: str = "default"
248
- address: str = "localhost:9000"
249
- seed_peers: List[str] = field(default_factory=list)
250
- heartbeat_interval: float = DEFAULT_HEARTBEAT_INTERVAL
251
- heartbeat_timeout: float = DEFAULT_HEARTBEAT_TIMEOUT
252
- sync_interval: float = DEFAULT_SYNC_INTERVAL
253
- election_timeout_min: int = DEFAULT_ELECTION_TIMEOUT_MIN
254
- election_timeout_max: int = DEFAULT_ELECTION_TIMEOUT_MAX
255
- gossip_fanout: int = DEFAULT_GOSSIP_FANOUT
256
- suspect_timeout: float = DEFAULT_SUSPECT_TIMEOUT
257
- brain_path: Path = field(default_factory=lambda: Path(".brain"))
258
- enable_consensus: bool = True
259
- enable_auto_sync: bool = True
260
-
261
-
262
- @dataclass
263
- class FederationState:
264
- """Complete federation state for persistence."""
265
- brain_id: str
266
- region: str
267
- peers: Dict[str, FederationPeer] = field(default_factory=dict)
268
- leader_id: Optional[str] = None
269
- term: int = 0
270
- voted_for: Optional[str] = None
271
- log: List[RaftLogEntry] = field(default_factory=list)
272
- commit_index: int = 0
273
- last_applied: int = 0
274
- vector_clock: VectorClock = field(default_factory=VectorClock)
275
- merkle_root: str = ""
276
- partition_status: PartitionStatus = PartitionStatus.NORMAL
277
- class_a_enabled: bool = True
278
-
279
- def to_dict(self) -> Dict[str, Any]:
280
- return {
281
- "brain_id": self.brain_id, "region": self.region,
282
- "peers": {k: v.to_dict() for k, v in self.peers.items()},
283
- "leader_id": self.leader_id, "term": self.term,
284
- "vector_clock": self.vector_clock.to_dict(),
285
- "partition_status": self.partition_status.name,
286
- }
287
-
288
-
289
- @dataclass
290
- class FederationMetrics:
291
- """Metrics for federation monitoring."""
292
- peers_total: int = 0
293
- peers_online: int = 0
294
- peers_suspect: int = 0
295
- sync_operations: int = 0
296
- sync_conflicts: int = 0
297
- avg_sync_lag_ms: float = 0.0
298
- raft_term: int = 0
299
- raft_leader_changes: int = 0
300
- tasks_routed: int = 0
301
- tasks_forwarded: int = 0
302
- avg_routing_time_ms: float = 0.0
303
- partition_events: int = 0
304
-
305
- def to_dict(self) -> Dict[str, Any]:
306
- return {
307
- "peers": {"total": self.peers_total, "online": self.peers_online},
308
- "sync": {"operations": self.sync_operations, "avg_lag_ms": self.avg_sync_lag_ms},
309
- "consensus": {"term": self.raft_term, "leader_changes": self.raft_leader_changes},
310
- "routing": {"tasks_routed": self.tasks_routed, "avg_time_ms": self.avg_routing_time_ms},
311
- }
312
-
313
-
314
- # =============================================================================
315
- # ROUTING STRATEGIES
316
- # =============================================================================
317
-
318
- class RoutingStrategy(ABC):
319
- @abstractmethod
320
- def score(self, brain: FederationPeer, task: Dict[str, Any], local_brain_id: str) -> float:
321
- pass
322
-
323
-
324
- class CompositeRoutingStrategy(RoutingStrategy):
325
- def __init__(self, profile: RoutingProfile = None):
326
- self.profile = profile or ROUTING_PROFILES["default"]
327
-
328
- def score(self, brain: FederationPeer, task: Dict[str, Any], local_brain_id: str) -> float:
329
- required_skills = set(task.get("required_skills", []))
330
- if required_skills and not required_skills.issubset(brain.capabilities):
331
- return -1.0
332
-
333
- skill_score = len(required_skills & brain.capabilities) / max(len(required_skills), 1)
334
- latency_score = 1.0 - min(brain.latency_ms / 200.0, 1.0)
335
- load_score = 1.0 - brain.load
336
- cost_score = 1.0
337
-
338
- affinity = task.get("affinity", {})
339
- affinity_score = 0.0
340
- if affinity.get("preferred_brain") == brain.peer_id:
341
- affinity_score = 1.0
342
- elif affinity.get("preferred_region") == brain.region:
343
- affinity_score = 0.5
344
- elif brain.peer_id == local_brain_id:
345
- affinity_score = 0.3
346
-
347
- return (self.profile.skill_weight * skill_score +
348
- self.profile.latency_weight * latency_score +
349
- self.profile.load_weight * load_score +
350
- self.profile.cost_weight * cost_score +
351
- self.profile.affinity_weight * affinity_score)
352
-
353
-
354
- # =============================================================================
355
- # DISCOVERY MANAGER
356
- # =============================================================================
357
-
358
- class DiscoveryManager:
359
- """SWIM-based peer discovery and membership management."""
360
-
361
- def __init__(self, config: FederationConfig, state: FederationState):
362
- self.config = config
363
- self.state = state
364
- self.running = False
365
- self._gossip_task: Optional[asyncio.Task] = None
366
- self._probe_task: Optional[asyncio.Task] = None
367
- self.on_peer_joined: Optional[Callable[[FederationPeer], None]] = None
368
- self.on_peer_left: Optional[Callable[[str], None]] = None
369
- self.on_peer_suspect: Optional[Callable[[str], None]] = None
370
-
371
- async def start(self) -> None:
372
- self.running = True
373
- await self._bootstrap()
374
- self._gossip_task = asyncio.create_task(self._gossip_loop())
375
- self._probe_task = asyncio.create_task(self._probe_loop())
376
- logger.info(f"Discovery started for {self.config.brain_id}")
377
-
378
- async def stop(self) -> None:
379
- self.running = False
380
- for task in [self._gossip_task, self._probe_task]:
381
- if task:
382
- task.cancel()
383
- try:
384
- await task
385
- except asyncio.CancelledError:
386
- pass
387
-
388
- async def _bootstrap(self) -> None:
389
- for seed in self.config.seed_peers:
390
- try:
391
- host, port = seed.rsplit(":", 1) if ":" in seed else (seed, "9000")
392
- peer_id = f"seed_{host}_{port}"
393
- peer = FederationPeer(peer_id=peer_id, address=f"{host}:{port}", region="unknown")
394
- self.state.peers[peer_id] = peer
395
- peer.status = PeerStatus.ONLINE
396
- peer.last_heartbeat = datetime.utcnow()
397
- if self.on_peer_joined:
398
- self.on_peer_joined(peer)
399
- except Exception as e:
400
- logger.warning(f"Failed to contact seed peer {seed}: {e}")
401
-
402
- async def _gossip_loop(self) -> None:
403
- while self.running:
404
- try:
405
- await self._gossip_round()
406
- await asyncio.sleep(self.config.heartbeat_interval)
407
- except asyncio.CancelledError:
408
- break
409
-
410
- async def _gossip_round(self) -> None:
411
- online_peers = [p for p in self.state.peers.values() if p.is_healthy()]
412
- if not online_peers:
413
- return
414
- random.sample(online_peers, min(self.config.gossip_fanout, len(online_peers)))
415
- # In production: exchange membership info with targets
416
-
417
- async def _probe_loop(self) -> None:
418
- while self.running:
419
- try:
420
- await self._probe_round()
421
- await asyncio.sleep(self.config.heartbeat_interval)
422
- except asyncio.CancelledError:
423
- break
424
-
425
- async def _probe_round(self) -> None:
426
- now = datetime.utcnow()
427
- for peer in list(self.state.peers.values()):
428
- if peer.peer_id == self.config.brain_id:
429
- continue
430
- if peer.last_heartbeat:
431
- elapsed = (now - peer.last_heartbeat).total_seconds()
432
- if elapsed > self.config.heartbeat_timeout:
433
- if peer.status == PeerStatus.ONLINE:
434
- peer.status = PeerStatus.SUSPECT
435
- peer.suspect_time = now
436
- if self.on_peer_suspect:
437
- self.on_peer_suspect(peer.peer_id)
438
- elif peer.status == PeerStatus.SUSPECT and peer.suspect_time:
439
- if (now - peer.suspect_time).total_seconds() > self.config.suspect_timeout:
440
- peer.status = PeerStatus.OFFLINE
441
- if self.on_peer_left:
442
- self.on_peer_left(peer.peer_id)
443
-
444
- def get_online_peers(self) -> List[FederationPeer]:
445
- return [p for p in self.state.peers.values() if p.is_online()]
446
-
447
- def get_healthy_peers(self) -> List[FederationPeer]:
448
- return [p for p in self.state.peers.values() if p.is_healthy()]
449
-
450
-
451
- # =============================================================================
452
- # CONSENSUS MANAGER (Simplified Raft)
453
- # =============================================================================
454
-
455
- class ConsensusManager:
456
- """Simplified Raft consensus for critical operations."""
457
-
458
- def __init__(self, config: FederationConfig, state: FederationState):
459
- self.config = config
460
- self.state = state
461
- self.raft_state = RaftState.FOLLOWER
462
- self.running = False
463
- self._election_timer: Optional[asyncio.Task] = None
464
- self._heartbeat_timer: Optional[asyncio.Task] = None
465
- self.on_leader_change: Optional[Callable[[Optional[str]], None]] = None
466
- self.on_commit: Optional[Callable[[RaftLogEntry], None]] = None
467
-
468
- async def start(self) -> None:
469
- self.running = True
470
- self._reset_election_timer()
471
- logger.info(f"Consensus started for {self.config.brain_id}")
472
-
473
- async def stop(self) -> None:
474
- self.running = False
475
- for timer in [self._election_timer, self._heartbeat_timer]:
476
- if timer:
477
- timer.cancel()
478
-
479
- def _get_election_timeout(self) -> float:
480
- return random.randint(self.config.election_timeout_min, self.config.election_timeout_max) / 1000.0
481
-
482
- def _reset_election_timer(self) -> None:
483
- if self._election_timer:
484
- self._election_timer.cancel()
485
- if self.running and self.raft_state != RaftState.LEADER:
486
- self._election_timer = asyncio.create_task(
487
- self._election_timeout_handler(self._get_election_timeout())
488
- )
489
-
490
- async def _election_timeout_handler(self, timeout: float) -> None:
491
- try:
492
- await asyncio.sleep(timeout)
493
- if self.running and self.raft_state != RaftState.LEADER:
494
- await self._start_election()
495
- except asyncio.CancelledError:
496
- pass
497
-
498
- async def _start_election(self) -> None:
499
- self.state.term += 1
500
- self.raft_state = RaftState.CANDIDATE
501
- self.state.voted_for = self.config.brain_id
502
-
503
- peers = [p for p in self.state.peers.values() if p.is_healthy()]
504
- total_nodes = len(peers) + 1
505
- votes_received = 1 # Vote for self
506
-
507
- if total_nodes == 1 or votes_received >= (total_nodes // 2) + 1:
508
- await self._become_leader()
509
- else:
510
- self.raft_state = RaftState.FOLLOWER
511
- self._reset_election_timer()
512
-
513
- async def _become_leader(self) -> None:
514
- self.raft_state = RaftState.LEADER
515
- self.state.leader_id = self.config.brain_id
516
- logger.info(f"Became leader for term {self.state.term}")
517
- self._start_heartbeat()
518
- if self.on_leader_change:
519
- self.on_leader_change(self.config.brain_id)
520
-
521
- def _start_heartbeat(self) -> None:
522
- if self._heartbeat_timer:
523
- self._heartbeat_timer.cancel()
524
- if self.running and self.raft_state == RaftState.LEADER:
525
- self._heartbeat_timer = asyncio.create_task(self._heartbeat_loop())
526
-
527
- async def _heartbeat_loop(self) -> None:
528
- while self.running and self.raft_state == RaftState.LEADER:
529
- try:
530
- await asyncio.sleep(self.config.heartbeat_interval / 2)
531
- except asyncio.CancelledError:
532
- break
533
-
534
- async def propose(self, command: Dict[str, Any]) -> bool:
535
- if self.raft_state != RaftState.LEADER:
536
- return False
537
- entry = RaftLogEntry(term=self.state.term, index=len(self.state.log) + 1, command=command)
538
- self.state.log.append(entry)
539
- if len(self.state.peers) == 0:
540
- self.state.commit_index = entry.index
541
- if self.on_commit:
542
- self.on_commit(entry)
543
- return True
544
-
545
- def is_leader(self) -> bool:
546
- return self.raft_state == RaftState.LEADER
547
-
548
- def get_leader(self) -> Optional[str]:
549
- return self.state.leader_id
550
-
551
-
552
- # =============================================================================
553
- # SYNC MANAGER
554
- # =============================================================================
555
-
556
- class SyncManager:
557
- """State synchronization using Merkle trees and CRDT merge."""
558
-
559
- def __init__(self, config: FederationConfig, state: FederationState):
560
- self.config = config
561
- self.state = state
562
- self.merkle_tree = MerkleTree()
563
- self.running = False
564
- self._sync_task: Optional[asyncio.Task] = None
565
- self.sync_in_progress: Set[str] = set()
566
- self.on_state_changed: Optional[Callable[[Dict[str, Any]], None]] = None
567
-
568
- async def start(self) -> None:
569
- self.running = True
570
- if self.config.enable_auto_sync:
571
- self._sync_task = asyncio.create_task(self._sync_loop())
572
- logger.info(f"Sync started for {self.config.brain_id}")
573
-
574
- async def stop(self) -> None:
575
- self.running = False
576
- if self._sync_task:
577
- self._sync_task.cancel()
578
-
579
- async def _sync_loop(self) -> None:
580
- while self.running:
581
- try:
582
- for peer in [p for p in self.state.peers.values() if p.is_healthy()]:
583
- if peer.peer_id not in self.sync_in_progress:
584
- await self.sync_with_peer(peer.peer_id)
585
- await asyncio.sleep(self.config.sync_interval)
586
- except asyncio.CancelledError:
587
- break
588
-
589
- async def sync_with_peer(self, peer_id: str, full: bool = False) -> SyncResult:
590
- if peer_id in self.sync_in_progress:
591
- return SyncResult(False, peer_id, 0, 0, 0, self.merkle_tree.get_root(), "Sync in progress")
592
-
593
- self.sync_in_progress.add(peer_id)
594
- start_time = time.perf_counter()
595
-
596
- try:
597
- peer = self.state.peers.get(peer_id)
598
- if not peer:
599
- return SyncResult(False, peer_id, 0, 0, 0, "", "Peer not found")
600
-
601
- local_root = self.merkle_tree.get_root()
602
- if not full and local_root == peer.merkle_root:
603
- return SyncResult(True, peer_id, 0, 0, (time.perf_counter() - start_time) * 1000, local_root)
604
-
605
- self.state.vector_clock = self.state.vector_clock.merge(peer.vector_clock).increment(self.config.brain_id)
606
- peer.last_sync = datetime.utcnow()
607
-
608
- return SyncResult(True, peer_id, 0, 0, (time.perf_counter() - start_time) * 1000, self.merkle_tree.get_root())
609
- except Exception as e: # Fixed bare except
610
- logger.error(f"Error during sync with peer {peer_id}: {e}")
611
- return SyncResult(False, peer_id, 0, 0, (time.perf_counter() - start_time) * 1000, self.merkle_tree.get_root(), str(e))
612
- finally:
613
- self.sync_in_progress.discard(peer_id)
614
-
615
- async def force_sync(self) -> List[SyncResult]:
616
- return [await self.sync_with_peer(p.peer_id, full=True) for p in self.state.peers.values() if p.is_healthy()]
617
-
618
- def update_local_state(self, key: str, value: bytes) -> None:
619
- self.merkle_tree.update(key, value)
620
- self.state.merkle_root = self.merkle_tree.get_root()
621
- self.state.vector_clock = self.state.vector_clock.increment(self.config.brain_id)
622
-
623
-
624
- # =============================================================================
625
- # ROUTING ENGINE
626
- # =============================================================================
627
-
628
- class RoutingEngine:
629
- """Task routing with pluggable strategies and caching."""
630
-
631
- def __init__(self, config: FederationConfig, state: FederationState):
632
- self.config = config
633
- self.state = state
634
- self.strategies: Dict[str, RoutingStrategy] = {"composite": CompositeRoutingStrategy()}
635
- self.routing_cache: Dict[str, Tuple[RoutingDecision, float]] = {}
636
- self.cache_ttl = 1.0
637
- self.total_routes = 0
638
- self.total_routing_time = 0.0
639
-
640
- async def route_task(self, task: Dict[str, Any], profile: str = "default") -> RoutingDecision:
641
- start_time = time.perf_counter()
642
-
643
- cache_key = hashlib.md5(json.dumps(task, sort_keys=True, default=str).encode()).hexdigest()
644
- if cache_key in self.routing_cache:
645
- decision, cached_time = self.routing_cache[cache_key]
646
- if time.time() - cached_time < self.cache_ttl:
647
- return decision
648
-
649
- strategy = CompositeRoutingStrategy(ROUTING_PROFILES.get(profile, ROUTING_PROFILES["default"]))
650
- candidates: List[Tuple[str, float]] = []
651
-
652
- local_peer = FederationPeer(self.config.brain_id, self.config.address, self.config.region, latency_ms=0)
653
- local_score = strategy.score(local_peer, task, self.config.brain_id)
654
- candidates.append((self.config.brain_id, local_score))
655
-
656
- for peer in self.state.peers.values():
657
- if peer.is_healthy():
658
- score = strategy.score(peer, task, self.config.brain_id)
659
- if score >= 0:
660
- candidates.append((peer.peer_id, score))
661
-
662
- candidates.sort(key=lambda x: x[1], reverse=True)
663
- best_brain, best_score = candidates[0] if candidates else (self.config.brain_id, 0.0)
664
-
665
- elapsed = (time.perf_counter() - start_time) * 1000
666
- decision = RoutingDecision(best_brain, best_score, {}, candidates[1:5], elapsed)
667
-
668
- self.routing_cache[cache_key] = (decision, time.time())
669
- self.total_routes += 1
670
- self.total_routing_time += elapsed
671
-
672
- return decision
673
-
674
-
675
- # =============================================================================
676
- # CIRCUIT BREAKER
677
- # =============================================================================
678
-
679
- class CircuitBreaker:
680
- CLOSED = "closed"
681
- OPEN = "open"
682
- HALF_OPEN = "half_open"
683
-
684
- def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 30.0):
685
- self.state = self.CLOSED
686
- self.failures = 0
687
- self.failure_threshold = failure_threshold
688
- self.recovery_timeout = recovery_timeout
689
- self.last_failure_time: Optional[float] = None
690
-
691
- def record_success(self) -> None:
692
- self.failures = 0
693
- self.state = self.CLOSED
694
-
695
- def record_failure(self) -> None:
696
- self.failures += 1
697
- self.last_failure_time = time.time()
698
- if self.failures >= self.failure_threshold:
699
- self.state = self.OPEN
700
-
701
- def allow_request(self) -> bool:
702
- if self.state == self.CLOSED:
703
- return True
704
- if self.state == self.OPEN and self.last_failure_time:
705
- if time.time() - self.last_failure_time >= self.recovery_timeout:
706
- self.state = self.HALF_OPEN
707
- return True
708
- return self.state == self.HALF_OPEN
709
-
710
-
711
- # =============================================================================
712
- # RECOVERY MANAGER
713
- # =============================================================================
714
-
715
- class RecoveryManager:
716
- """Failure detection and recovery handling."""
717
-
718
- def __init__(self, config: FederationConfig, state: FederationState,
719
- discovery: DiscoveryManager, consensus: ConsensusManager, sync: SyncManager):
720
- self.config = config
721
- self.state = state
722
- self.discovery = discovery
723
- self.consensus = consensus
724
- self.sync = sync
725
- self.circuit_breakers: Dict[str, CircuitBreaker] = {}
726
- self.unreachable_peers: Set[str] = set()
727
- self.on_partition_detected: Optional[Callable[[Set[str]], None]] = None
728
- self.on_partition_healed: Optional[Callable[[Set[str]], None]] = None
729
-
730
- def check_partition_status(self) -> PartitionStatus:
731
- total_peers = len(self.state.peers) + 1
732
- reachable = len([p for p in self.state.peers.values() if p.is_healthy()]) + 1
733
-
734
- if reachable == total_peers:
735
- return PartitionStatus.NORMAL
736
- majority = (total_peers // 2) + 1
737
- if reachable >= majority:
738
- return PartitionStatus.MAJORITY
739
- elif reachable == 1:
740
- return PartitionStatus.ISOLATED
741
- return PartitionStatus.MINORITY
742
-
743
- def is_operation_allowed(self, consistency_class: ConsistencyClass) -> bool:
744
- status = self.check_partition_status()
745
- if consistency_class == ConsistencyClass.CLASS_A:
746
- return status in (PartitionStatus.NORMAL, PartitionStatus.MAJORITY)
747
- return True
748
-
749
- async def handle_peer_failure(self, peer_id: str) -> None:
750
- self.unreachable_peers.add(peer_id)
751
- new_status = self.check_partition_status()
752
- if new_status != self.state.partition_status:
753
- self.state.partition_status = new_status
754
- if new_status in (PartitionStatus.MINORITY, PartitionStatus.ISOLATED):
755
- self.state.class_a_enabled = False
756
- if self.on_partition_detected:
757
- self.on_partition_detected(self.unreachable_peers.copy())
758
-
759
- async def handle_peer_recovery(self, peer_id: str) -> None:
760
- self.unreachable_peers.discard(peer_id)
761
- new_status = self.check_partition_status()
762
- if new_status != self.state.partition_status:
763
- self.state.partition_status = new_status
764
- if new_status == PartitionStatus.NORMAL:
765
- self.state.class_a_enabled = True
766
- if self.on_partition_healed:
767
- self.on_partition_healed(self.unreachable_peers.copy())
768
- await self.sync.sync_with_peer(peer_id, full=True)
769
-
770
-
771
- # =============================================================================
772
- # FEDERATION ENGINE (Main Entry Point)
773
- # =============================================================================
774
-
775
- class FederationEngine:
776
- """Main federation engine coordinating all components."""
777
-
778
- def __init__(self, config: FederationConfig):
779
- self.config = config
780
- self.state = FederationState(brain_id=config.brain_id, region=config.region)
781
-
782
- self.discovery = DiscoveryManager(config, self.state)
783
- self.consensus = ConsensusManager(config, self.state)
784
- self.sync = SyncManager(config, self.state)
785
- self.routing = RoutingEngine(config, self.state)
786
- self.recovery = RecoveryManager(config, self.state, self.discovery, self.consensus, self.sync)
787
-
788
- self._setup_callbacks()
789
- self.metrics = FederationMetrics()
790
- self.running = False
791
- self._persistence_path = config.brain_path / "federation"
792
-
793
- def _setup_callbacks(self) -> None:
794
- self.discovery.on_peer_joined = self._on_peer_joined
795
- self.discovery.on_peer_left = self._on_peer_left
796
- self.discovery.on_peer_suspect = self._on_peer_suspect
797
- self.consensus.on_leader_change = self._on_leader_change
798
-
799
- def _on_peer_joined(self, peer: FederationPeer) -> None:
800
- self.metrics.peers_total = len(self.state.peers)
801
- self.metrics.peers_online = len([p for p in self.state.peers.values() if p.is_online()])
802
- logger.info(f"Peer joined: {peer.peer_id}")
803
-
804
- # v0.6.0 DSoR: Emit federation event
805
- if DSOR_AVAILABLE:
806
- try:
807
- emit_event(EventTypes.FEDERATION_PEER_JOINED, {
808
- "peer_id": peer.peer_id,
809
- "region": peer.region,
810
- "trust_level": peer.trust_level.name,
811
- "decision_id": str(uuid.uuid4()),
812
- "context_hash": self.sync.merkle_tree.get_root()
813
- })
814
- except Exception as e:
815
- logger.debug(f"DSoR event emission failed: {e}")
816
-
817
- def _on_peer_left(self, peer_id: str) -> None:
818
- self.metrics.peers_online = len([p for p in self.state.peers.values() if p.is_online()])
819
- asyncio.create_task(self.recovery.handle_peer_failure(peer_id))
820
- logger.warning(f"Peer left: {peer_id}")
821
-
822
- # v0.6.0 DSoR: Emit federation event
823
- if DSOR_AVAILABLE:
824
- try:
825
- emit_event(EventTypes.FEDERATION_PEER_LEFT, {
826
- "peer_id": peer_id,
827
- "decision_id": str(uuid.uuid4()),
828
- "context_hash": self.sync.merkle_tree.get_root()
829
- })
830
- except Exception as e:
831
- logger.debug(f"DSoR event emission failed: {e}")
832
-
833
- def _on_peer_suspect(self, peer_id: str) -> None:
834
- self.metrics.peers_suspect = len([p for p in self.state.peers.values() if p.status == PeerStatus.SUSPECT])
835
- logger.warning(f"Peer suspect: {peer_id}")
836
-
837
- # v0.6.0 DSoR: Emit federation event
838
- if DSOR_AVAILABLE:
839
- try:
840
- emit_event(EventTypes.FEDERATION_PEER_SUSPECT, {
841
- "peer_id": peer_id,
842
- "decision_id": str(uuid.uuid4())
843
- })
844
- except Exception as e:
845
- logger.debug(f"DSoR event emission failed: {e}")
846
-
847
- def _on_leader_change(self, leader_id: Optional[str]) -> None:
848
- self.metrics.raft_leader_changes += 1
849
- self.metrics.raft_term = self.state.term
850
- logger.info(f"Leader changed to: {leader_id}")
851
-
852
- # v0.6.0 DSoR: Emit leader election event (critical decision)
853
- if DSOR_AVAILABLE:
854
- try:
855
- emit_event(EventTypes.FEDERATION_LEADER_ELECTED, {
856
- "leader_id": leader_id,
857
- "term": self.state.term,
858
- "decision_id": str(uuid.uuid4()),
859
- "context_hash": self.sync.merkle_tree.get_root(),
860
- "is_self": leader_id == self.config.brain_id
861
- })
862
- except Exception as e:
863
- logger.debug(f"DSoR event emission failed: {e}")
864
-
865
- async def start(self) -> None:
866
- self.running = True
867
- self._persistence_path.mkdir(parents=True, exist_ok=True)
868
- await self._load_state()
869
-
870
- await self.discovery.start()
871
- if self.config.enable_consensus:
872
- await self.consensus.start()
873
- await self.sync.start()
874
-
875
- logger.info(f"Federation engine started: {self.config.brain_id}")
876
-
877
- async def stop(self) -> None:
878
- self.running = False
879
- await self._save_state()
880
-
881
- await self.sync.stop()
882
- await self.consensus.stop()
883
- await self.discovery.stop()
884
-
885
- logger.info(f"Federation engine stopped: {self.config.brain_id}")
886
-
887
- async def _load_state(self) -> None:
888
- state_file = self._persistence_path / "state.json"
889
- if state_file.exists():
890
- try:
891
- with open(state_file) as f:
892
- data = json.load(f)
893
- self.state.term = data.get("term", 0)
894
- self.state.leader_id = data.get("leader_id")
895
- self.state.vector_clock = VectorClock.from_dict(data.get("vector_clock", {}))
896
- except Exception as e:
897
- logger.warning(f"Failed to load federation state: {e}")
898
-
899
- async def _save_state(self) -> None:
900
- state_file = self._persistence_path / "state.json"
901
- try:
902
- with open(state_file, "w") as f:
903
- json.dump(self.state.to_dict(), f, indent=2, default=str)
904
- except Exception as e:
905
- logger.error(f"Failed to save federation state: {e}")
906
-
907
- # Public API
908
- async def join(self, seed_peer: str) -> Dict[str, Any]:
909
- """Join the federation via a seed peer."""
910
- try:
911
- host, port = seed_peer.rsplit(":", 1) if ":" in seed_peer else (seed_peer, "9000")
912
- peer_id = f"peer_{host}_{port}"
913
- peer = FederationPeer(peer_id=peer_id, address=f"{host}:{port}", region="unknown")
914
- self.state.peers[peer_id] = peer
915
- peer.status = PeerStatus.ONLINE
916
- peer.last_heartbeat = datetime.utcnow()
917
- return {"success": True, "peers": len(self.state.peers)}
918
- except Exception as e:
919
- return {"success": False, "error": str(e)}
920
-
921
- async def leave(self) -> Dict[str, Any]:
922
- """Leave the federation gracefully."""
923
- # Notify peers and stop
924
- await self.stop()
925
- return {"success": True}
926
-
927
- async def route_task(self, task: Dict[str, Any], profile: str = "default") -> RoutingDecision:
928
- """Route a task to the optimal brain."""
929
- decision = await self.routing.route_task(task, profile)
930
- self.metrics.tasks_routed += 1
931
-
932
- # v0.6.0 DSoR: Log routing decision with provenance
933
- if DSOR_AVAILABLE:
934
- try:
935
- emit_event(EventTypes.FEDERATION_TASK_ROUTED, {
936
- "target_brain": decision.target_brain,
937
- "score": decision.score,
938
- "profile": profile,
939
- "decision_id": str(uuid.uuid4()),
940
- "routing_time_ms": decision.routing_time_ms,
941
- "alternatives_count": len(decision.alternatives)
942
- })
943
- except Exception as e:
944
- logger.debug(f"DSoR routing event failed: {e}")
945
-
946
- return decision
947
-
948
- async def sync_now(self) -> List[SyncResult]:
949
- """Force immediate synchronization."""
950
- return await self.sync.force_sync()
951
-
952
- def get_peers(self) -> List[FederationPeer]:
953
- """Get all known peers."""
954
- return list(self.state.peers.values())
955
-
956
- def get_online_peers(self) -> List[FederationPeer]:
957
- """Get online peers."""
958
- return self.discovery.get_online_peers()
959
-
960
- def get_status(self) -> Dict[str, Any]:
961
- """Get federation status."""
962
- return {
963
- "brain_id": self.config.brain_id,
964
- "region": self.config.region,
965
- "running": self.running,
966
- "leader_id": self.state.leader_id,
967
- "is_leader": self.consensus.is_leader(),
968
- "term": self.state.term,
969
- "partition_status": self.state.partition_status.name,
970
- "class_a_enabled": self.state.class_a_enabled,
971
- "peers": {
972
- "total": len(self.state.peers),
973
- "online": len([p for p in self.state.peers.values() if p.is_online()]),
974
- "suspect": len([p for p in self.state.peers.values() if p.status == PeerStatus.SUSPECT]),
975
- },
976
- "sync": {
977
- "merkle_root": self.sync.merkle_tree.get_root(),
978
- "vector_clock": self.state.vector_clock.to_dict(),
979
- },
980
- "metrics": self.metrics.to_dict(),
981
- }
982
-
983
- def get_health(self) -> Dict[str, Any]:
984
- """Get federation health status."""
985
- partition = self.recovery.check_partition_status()
986
- online_count = len([p for p in self.state.peers.values() if p.is_online()])
987
- total_count = len(self.state.peers)
988
-
989
- health_score = 1.0
990
- if partition == PartitionStatus.MINORITY:
991
- health_score = 0.3
992
- elif partition == PartitionStatus.ISOLATED:
993
- health_score = 0.1
994
- elif partition == PartitionStatus.MAJORITY:
995
- health_score = 0.7
996
-
997
- if total_count > 0:
998
- health_score *= (online_count / total_count)
999
-
1000
- return {
1001
- "healthy": health_score > 0.5,
1002
- "score": health_score,
1003
- "partition_status": partition.name,
1004
- "peers_online": online_count,
1005
- "peers_total": total_count,
1006
- "leader": self.state.leader_id,
1007
- "warnings": self._get_warnings(),
1008
- }
1009
-
1010
- def _get_warnings(self) -> List[str]:
1011
- warnings = []
1012
- partition = self.recovery.check_partition_status()
1013
- if partition != PartitionStatus.NORMAL:
1014
- warnings.append(f"Partition detected: {partition.name}")
1015
- if not self.state.class_a_enabled:
1016
- warnings.append("Class A operations disabled")
1017
- suspect = [p for p in self.state.peers.values() if p.status == PeerStatus.SUSPECT]
1018
- if suspect:
1019
- warnings.append(f"{len(suspect)} peer(s) suspect")
1020
- return warnings
1021
-
1022
-
1023
- # =============================================================================
1024
- # FACTORY FUNCTION
1025
- # =============================================================================
1026
-
1027
- def create_federation_engine(
1028
- brain_id: str,
1029
- region: str = "default",
1030
- seed_peers: List[str] = None,
1031
- brain_path: Path = None
1032
- ) -> FederationEngine:
1033
- """Factory function to create a federation engine."""
1034
- config = FederationConfig(
1035
- brain_id=brain_id,
1036
- region=region,
1037
- seed_peers=seed_peers or [],
1038
- brain_path=brain_path or Path(".brain")
1039
- )
1040
- return FederationEngine(config)