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.
- package/package.json +2 -4
- package/.github/workflows/ci.yml +0 -72
- package/.smithery/shttp/module.js +0 -3
- package/.smithery/shttp/module.js.map +0 -7
- package/.smithery/stdio/index.cjs +0 -74
- package/.smithery/stdio/index.cjs.map +0 -7
- package/CHANGELOG.md +0 -65
- package/CODE_OF_CONDUCT.md +0 -34
- package/COMPARISON.md +0 -20
- package/CONTRIBUTING.md +0 -137
- package/DEVELOPMENT.md +0 -109
- package/LICENSE +0 -21
- package/README.md +0 -319
- package/ROADMAP.md +0 -24
- package/SECURITY.md +0 -88
- package/docs/COMMUNITY_FAQ.md +0 -48
- package/docs/COMPETITIVE_ANALYSIS.md +0 -90
- package/docs/DEMO_SCRIPT.md +0 -86
- package/docs/ENTERPRISE.md +0 -169
- package/docs/FAQ.md +0 -228
- package/docs/QUICK_START.md +0 -155
- package/docs/README.md +0 -67
- package/docs/RECURSIVE_SNAP_DEMO.gif +0 -0
- package/docs/social-preview-hq.png +0 -0
- package/docs/social-preview.png +0 -0
- package/examples/basic_usage.py +0 -84
- package/package.json.real +0 -24
- package/pyproject.toml +0 -50
- package/registry-server.json +0 -20
- package/scripts/demo_governance.py +0 -109
- package/scripts/sse_bridge.py +0 -87
- package/scripts/verify_windows_paths.py +0 -48
- package/server.json +0 -20
- package/src/mcp_server_nucleus/__init__.py +0 -8583
- package/src/mcp_server_nucleus/__main__.py +0 -9
- package/src/mcp_server_nucleus/cli.py +0 -1225
- package/src/mcp_server_nucleus/commitment_core.py +0 -146
- package/src/mcp_server_nucleus/commitment_ledger.py +0 -841
- package/src/mcp_server_nucleus/core/__init__.py +0 -6
- package/src/mcp_server_nucleus/core/orchestrator.py +0 -9
- package/src/mcp_server_nucleus/core/tool_registration_impl.py +0 -99
- package/src/mcp_server_nucleus/daemon/safety/lock.py +0 -121
- package/src/mcp_server_nucleus/deployment/render_integration.py +0 -4
- package/src/mcp_server_nucleus/hypervisor/__init__.py +0 -0
- package/src/mcp_server_nucleus/hypervisor/injector.py +0 -83
- package/src/mcp_server_nucleus/hypervisor/locker.py +0 -130
- package/src/mcp_server_nucleus/hypervisor/watchdog.py +0 -71
- package/src/mcp_server_nucleus/runtime/__init__.py +0 -58
- package/src/mcp_server_nucleus/runtime/agent.py +0 -517
- package/src/mcp_server_nucleus/runtime/agent_pool.py +0 -797
- package/src/mcp_server_nucleus/runtime/agents/base.py +0 -34
- package/src/mcp_server_nucleus/runtime/agents/oracle/manifest.json +0 -28
- package/src/mcp_server_nucleus/runtime/auth.py +0 -98
- package/src/mcp_server_nucleus/runtime/autopilot.py +0 -1057
- package/src/mcp_server_nucleus/runtime/broker.py +0 -126
- package/src/mcp_server_nucleus/runtime/budget.py +0 -153
- package/src/mcp_server_nucleus/runtime/capabilities/base.py +0 -20
- package/src/mcp_server_nucleus/runtime/capabilities/brain_ops.py +0 -253
- package/src/mcp_server_nucleus/runtime/capabilities/code_ops.py +0 -192
- package/src/mcp_server_nucleus/runtime/capabilities/depth_tracker.py +0 -81
- package/src/mcp_server_nucleus/runtime/capabilities/feature_map.py +0 -274
- package/src/mcp_server_nucleus/runtime/capabilities/marketing_engine.py +0 -205
- package/src/mcp_server_nucleus/runtime/capabilities/memory_ops.py +0 -79
- package/src/mcp_server_nucleus/runtime/capabilities/proof_system.py +0 -127
- package/src/mcp_server_nucleus/runtime/capabilities/render_ops.py +0 -80
- package/src/mcp_server_nucleus/runtime/capabilities/render_poller_cap.py +0 -266
- package/src/mcp_server_nucleus/runtime/capabilities/self_healing.py +0 -110
- package/src/mcp_server_nucleus/runtime/capabilities/strategy.py +0 -138
- package/src/mcp_server_nucleus/runtime/capabilities/synthesizer.py +0 -134
- package/src/mcp_server_nucleus/runtime/capabilities/web_ops.py +0 -94
- package/src/mcp_server_nucleus/runtime/common.py +0 -134
- package/src/mcp_server_nucleus/runtime/context_manager.py +0 -342
- package/src/mcp_server_nucleus/runtime/context_rules.json +0 -86
- package/src/mcp_server_nucleus/runtime/crdt_task_store.py +0 -349
- package/src/mcp_server_nucleus/runtime/critic.py +0 -150
- package/src/mcp_server_nucleus/runtime/daemon.py +0 -142
- package/src/mcp_server_nucleus/runtime/dashboard.py +0 -1087
- package/src/mcp_server_nucleus/runtime/depth_ops.py +0 -339
- package/src/mcp_server_nucleus/runtime/event_ops.py +0 -103
- package/src/mcp_server_nucleus/runtime/event_stream.py +0 -213
- package/src/mcp_server_nucleus/runtime/export.py +0 -64
- package/src/mcp_server_nucleus/runtime/factory.py +0 -594
- package/src/mcp_server_nucleus/runtime/federation.py +0 -1040
- package/src/mcp_server_nucleus/runtime/fetcher.py +0 -92
- package/src/mcp_server_nucleus/runtime/file_monitor.py +0 -208
- package/src/mcp_server_nucleus/runtime/firestore_bridge.py +0 -91
- package/src/mcp_server_nucleus/runtime/gcloud_ops.py +0 -186
- package/src/mcp_server_nucleus/runtime/hooks.py +0 -246
- package/src/mcp_server_nucleus/runtime/identity/gatekeeper.py +0 -106
- package/src/mcp_server_nucleus/runtime/identity/keygen.py +0 -147
- package/src/mcp_server_nucleus/runtime/identity/manifest.py +0 -62
- package/src/mcp_server_nucleus/runtime/identity/trust.py +0 -27
- package/src/mcp_server_nucleus/runtime/inspector.py +0 -74
- package/src/mcp_server_nucleus/runtime/installer.py +0 -81
- package/src/mcp_server_nucleus/runtime/ipc_auth.py +0 -377
- package/src/mcp_server_nucleus/runtime/lifecycle.py +0 -129
- package/src/mcp_server_nucleus/runtime/llm_client.py +0 -408
- package/src/mcp_server_nucleus/runtime/llm_tiers.json +0 -88
- package/src/mcp_server_nucleus/runtime/locking.py +0 -228
- package/src/mcp_server_nucleus/runtime/loops/fixer.py +0 -110
- package/src/mcp_server_nucleus/runtime/memory.py +0 -158
- package/src/mcp_server_nucleus/runtime/mounter.py +0 -210
- package/src/mcp_server_nucleus/runtime/mounter_ops.py +0 -250
- package/src/mcp_server_nucleus/runtime/nuke_protocol.py +0 -147
- package/src/mcp_server_nucleus/runtime/orchestrator.py +0 -370
- package/src/mcp_server_nucleus/runtime/orchestrator_unified.py +0 -263
- package/src/mcp_server_nucleus/runtime/orchestrator_v3.py +0 -617
- package/src/mcp_server_nucleus/runtime/plugin_loader.py +0 -84
- package/src/mcp_server_nucleus/runtime/policy.py +0 -117
- package/src/mcp_server_nucleus/runtime/profiling.py +0 -219
- package/src/mcp_server_nucleus/runtime/prometheus.py +0 -286
- package/src/mcp_server_nucleus/runtime/proposals.py +0 -101
- package/src/mcp_server_nucleus/runtime/publisher.py +0 -109
- package/src/mcp_server_nucleus/runtime/pulse.py +0 -516
- package/src/mcp_server_nucleus/runtime/registry.py +0 -82
- package/src/mcp_server_nucleus/runtime/render_ops.py +0 -83
- package/src/mcp_server_nucleus/runtime/schema_gen.py +0 -140
- package/src/mcp_server_nucleus/runtime/session_ops.py +0 -224
- package/src/mcp_server_nucleus/runtime/stdio_server.py +0 -633
- package/src/mcp_server_nucleus/runtime/storage.py +0 -83
- package/src/mcp_server_nucleus/runtime/strategy.py +0 -91
- package/src/mcp_server_nucleus/runtime/swarm.py +0 -170
- package/src/mcp_server_nucleus/runtime/sync_ops.py +0 -878
- package/src/mcp_server_nucleus/runtime/task_ingestion.py +0 -1020
- package/src/mcp_server_nucleus/runtime/task_ops.py +0 -449
- package/src/mcp_server_nucleus/runtime/task_scheduler.py +0 -488
- package/src/mcp_server_nucleus/runtime/team.py +0 -59
- package/src/mcp_server_nucleus/runtime/triggers.py +0 -237
- package/src/mcp_server_nucleus/runtime/vector_store.py +0 -125
- package/src/mcp_server_nucleus/runtime/watcher.py +0 -112
- package/src/mcp_server_nucleus/tool_tiers.py +0 -243
- package/tests/__init__.py +0 -1
- package/tests/conftest.py +0 -42
- package/tests/test_core.py +0 -235
- 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)
|