aetherruntime-arch 2.0.0__py3-none-any.whl

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.
@@ -0,0 +1,216 @@
1
+ """
2
+ AetherRuntime v2.0.0 — Simulation: Network (Layer 6, simulation only)
3
+
4
+ This module provides a simple peer-to-peer network simulator for
5
+ testing AetherRuntime. It supports:
6
+ - N nodes (agents)
7
+ - Message passing between nodes
8
+ - Network partitions
9
+ - Gossip propagation (for halt orders)
10
+
11
+ This is a simulation, not a production network.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import random
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum
19
+ from typing import Optional
20
+
21
+
22
+ # ═══════════════════════════════════════════════════════════════════════════
23
+ # Enums
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
+
26
+
27
+ class NetworkState(str, Enum):
28
+ """Network operational state."""
29
+ CONNECTED = "connected"
30
+ PARTITIONED = "partitioned"
31
+ DEGRADED = "degraded"
32
+
33
+
34
+ # ═══════════════════════════════════════════════════════════════════════════
35
+ # Partition schedule
36
+ # ═══════════════════════════════════════════════════════════════════════════
37
+
38
+
39
+ @dataclass
40
+ class PartitionSchedule:
41
+ """
42
+ A schedule for network partitions.
43
+
44
+ Example:
45
+ >>> schedule = PartitionSchedule()
46
+ >>> schedule.add_step(step=10, partition=True)
47
+ >>> schedule.add_step(step=20, partition=False)
48
+ """
49
+ events: list[tuple[int, bool]] = field(default_factory=list)
50
+
51
+ def add_step(self, step: int, partition: bool) -> None:
52
+ """Add a partition event at a given step."""
53
+ self.events.append((step, partition))
54
+ self.events.sort(key=lambda e: e[0])
55
+
56
+ def state_at(self, step: int) -> bool:
57
+ """Return True if partitioned at `step`, False otherwise."""
58
+ current = False
59
+ for event_step, is_partition in self.events:
60
+ if event_step <= step:
61
+ current = is_partition
62
+ else:
63
+ break
64
+ return current
65
+
66
+
67
+ # ═══════════════════════════════════════════════════════════════════════════
68
+ # Message
69
+ # ═══════════════════════════════════════════════════════════════════════════
70
+
71
+
72
+ @dataclass
73
+ class Message:
74
+ """A message between two nodes."""
75
+ sender: str
76
+ receiver: str
77
+ payload: dict
78
+ timestamp: str
79
+ message_type: str = "generic"
80
+
81
+ def to_dict(self) -> dict:
82
+ return {
83
+ "sender": self.sender,
84
+ "receiver": self.receiver,
85
+ "payload": self.payload,
86
+ "timestamp": self.timestamp,
87
+ "type": self.message_type,
88
+ }
89
+
90
+
91
+ # ═══════════════════════════════════════════════════════════════════════════
92
+ # Network
93
+ # ═══════════════════════════════════════════════════════════════════════════
94
+
95
+
96
+ class Network:
97
+ """
98
+ A simple peer-to-peer network simulator.
99
+
100
+ Attributes:
101
+ n_nodes: Number of nodes.
102
+ state: Current network state.
103
+ partition_schedule: Schedule of partitions.
104
+ message_queue: Pending messages.
105
+ delivered_messages: Delivered messages (for analysis).
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ n_nodes: int,
111
+ partition_schedule: Optional[PartitionSchedule] = None,
112
+ seed: Optional[int] = None,
113
+ ) -> None:
114
+ if n_nodes <= 0:
115
+ raise ValueError("n_nodes must be > 0")
116
+ self.n_nodes = n_nodes
117
+ self.node_ids = [f"node-{i:03d}" for i in range(n_nodes)]
118
+ self.state = NetworkState.CONNECTED
119
+ self.partition_schedule = partition_schedule or PartitionSchedule()
120
+ self.message_queue: list[Message] = []
121
+ self.delivered_messages: list[Message] = []
122
+ self.step = 0
123
+ self._rng = random.Random(seed)
124
+
125
+ def step_forward(self) -> None:
126
+ """Advance the simulation by one step."""
127
+ self.step += 1
128
+ if self.partition_schedule.state_at(self.step):
129
+ self.state = NetworkState.PARTITIONED
130
+ else:
131
+ self.state = NetworkState.CONNECTED
132
+
133
+ def is_partitioned(self) -> bool:
134
+ """Check if the network is currently partitioned."""
135
+ return self.state == NetworkState.PARTITIONED
136
+
137
+ def broadcast(self, sender: str, payload: dict, message_type: str = "generic") -> int:
138
+ """
139
+ Broadcast a message from `sender` to all other nodes.
140
+
141
+ Args:
142
+ sender: The sender node ID.
143
+ payload: The message payload.
144
+ message_type: Type of message (for classification).
145
+
146
+ Returns:
147
+ Number of messages queued.
148
+ """
149
+ if sender not in self.node_ids:
150
+ raise ValueError(f"Unknown sender: {sender}")
151
+ count = 0
152
+ for receiver in self.node_ids:
153
+ if receiver == sender:
154
+ continue
155
+ self.send(sender, receiver, payload, message_type)
156
+ count += 1
157
+ return count
158
+
159
+ def send(
160
+ self,
161
+ sender: str,
162
+ receiver: str,
163
+ payload: dict,
164
+ message_type: str = "generic",
165
+ ) -> bool:
166
+ """
167
+ Send a message from `sender` to `receiver`.
168
+
169
+ Returns:
170
+ True if queued successfully, False if dropped (partition).
171
+ """
172
+ if sender not in self.node_ids or receiver not in self.node_ids:
173
+ raise ValueError("Unknown sender or receiver")
174
+ if self.is_partitioned():
175
+ # Drop message during partition
176
+ return False
177
+ from datetime import datetime, timezone
178
+ msg = Message(
179
+ sender=sender,
180
+ receiver=receiver,
181
+ payload=payload,
182
+ timestamp=datetime.now(timezone.utc).isoformat(),
183
+ message_type=message_type,
184
+ )
185
+ self.message_queue.append(msg)
186
+ return True
187
+
188
+ def deliver_all(self) -> int:
189
+ """
190
+ Deliver all queued messages.
191
+
192
+ Returns:
193
+ Number of messages delivered.
194
+ """
195
+ n = len(self.message_queue)
196
+ self.delivered_messages.extend(self.message_queue)
197
+ self.message_queue.clear()
198
+ return n
199
+
200
+ def pending_count(self) -> int:
201
+ """Count pending messages."""
202
+ return len(self.message_queue)
203
+
204
+ def delivered_count(self) -> int:
205
+ """Count delivered messages."""
206
+ return len(self.delivered_messages)
207
+
208
+ def statistics(self) -> dict:
209
+ """Return network statistics."""
210
+ return {
211
+ "n_nodes": self.n_nodes,
212
+ "step": self.step,
213
+ "state": self.state.value,
214
+ "pending": self.pending_count(),
215
+ "delivered": self.delivered_count(),
216
+ }
@@ -0,0 +1,341 @@
1
+ """
2
+ AetherRuntime v2.0.0 — Simulation: Node (Layer 6, simulation only)
3
+
4
+ This module provides a Node class that represents an agent in the
5
+ simulated network. Each node has:
6
+ - A unique agent (from models)
7
+ - A telemetry log (from monitoring)
8
+ - A baseline registry (from monitoring)
9
+ - An alert registry (from detection)
10
+ - A containment manager (from containment)
11
+ - A halt order registry (from halt)
12
+ - A snapshot registry (from recovery)
13
+
14
+ Nodes can:
15
+ - Collect telemetry
16
+ - Update baselines
17
+ - Detect anomalies
18
+ - Contain themselves
19
+ - Halt themselves
20
+ - Recover themselves
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import random
26
+ import secrets
27
+ from typing import Optional
28
+
29
+ from ..models import (
30
+ Agent,
31
+ AgentStatus,
32
+ AnomalyAlert,
33
+ Baseline,
34
+ BaselineModel,
35
+ ContainmentLevel,
36
+ ContainmentPolicy,
37
+ HaltOrder,
38
+ RecoveryRecord,
39
+ Snapshot,
40
+ TelemetryCategory,
41
+ TelemetryRecord,
42
+ create_agent,
43
+ )
44
+ from ..monitoring.telemetry import TelemetryLog, collect
45
+ from ..monitoring.baseline import BaselineRegistry, update_baseline
46
+ from ..detection.alert import AlertRegistry
47
+ from ..detection.classifier import detect
48
+ from ..containment.policy import ContainmentRegistry, contain
49
+ from ..containment.isolation import IsolationManager
50
+ from ..halt.order import HaltOrderRegistry, propose_halt
51
+ from ..halt.quorum import approve_halt, QuorumTracker
52
+ from ..recovery.snapshot import SnapshotRegistry, create_snapshot
53
+ from ..recovery.replay import RecoveryRegistry, recover
54
+
55
+
56
+ # ═══════════════════════════════════════════════════════════════════════════
57
+ # Node
58
+ # ═══════════════════════════════════════════════════════════════════════════
59
+
60
+
61
+ class Node:
62
+ """
63
+ A node in the simulated network.
64
+
65
+ Wraps an Agent with all per-node state:
66
+ - telemetry_log
67
+ - baseline_registry
68
+ - alert_registry
69
+ - containment_registry
70
+ - isolation_manager
71
+ - halt_order_registry
72
+ - snapshot_registry
73
+ - recovery_registry
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ agent: Optional[Agent] = None,
79
+ seed: Optional[int] = None,
80
+ anomaly_threshold: float = 2.0,
81
+ ) -> None:
82
+ # ─── Agent ───
83
+ if agent is None:
84
+ public_key = secrets.token_bytes(32)
85
+ agent = create_agent(public_key=public_key)
86
+ self.agent = agent
87
+
88
+ # ─── Per-node state ───
89
+ self.telemetry_log = TelemetryLog()
90
+ self.baseline_registry = BaselineRegistry()
91
+ self.alert_registry = AlertRegistry()
92
+ self.containment_registry = ContainmentRegistry()
93
+ self.isolation_manager = IsolationManager()
94
+ self.halt_order_registry = HaltOrderRegistry()
95
+ self.snapshot_registry = SnapshotRegistry()
96
+ self.recovery_registry = RecoveryRegistry()
97
+
98
+ # ─── Configuration ───
99
+ self.anomaly_threshold = anomaly_threshold
100
+ self._rng = random.Random(seed)
101
+
102
+ # ─── Identity ───
103
+ @property
104
+ def node_id(self) -> str:
105
+ return self.agent.agent_id
106
+
107
+ @property
108
+ def status(self) -> AgentStatus:
109
+ return self.agent.status
110
+
111
+ @status.setter
112
+ def status(self, value: AgentStatus) -> None:
113
+ self.agent.status = value
114
+
115
+ # ─── Monitoring ───
116
+ def start(self) -> None:
117
+ """Start the node (set status to running)."""
118
+ self.agent.status = AgentStatus.RUNNING
119
+
120
+ def collect_telemetry(
121
+ self,
122
+ category: TelemetryCategory,
123
+ value: float,
124
+ metadata: Optional[dict] = None,
125
+ ) -> TelemetryRecord:
126
+ """Collect a telemetry record."""
127
+ previous = self.telemetry_log.last_timestamp(self.agent.agent_id)
128
+ record = collect(
129
+ agent=self.agent,
130
+ category=category,
131
+ value=value,
132
+ previous_timestamp=previous,
133
+ metadata=metadata,
134
+ )
135
+ self.telemetry_log.append(record)
136
+ return record
137
+
138
+ def update_baseline(
139
+ self,
140
+ category: TelemetryCategory,
141
+ model_type: BaselineModel = BaselineModel.MEAN_STD,
142
+ window_size: int = 10,
143
+ baseline_window: Optional[int] = None,
144
+ ) -> Baseline:
145
+ """
146
+ Update the baseline for a category.
147
+
148
+ Args:
149
+ category: Telemetry category.
150
+ model_type: Model type.
151
+ window_size: Minimum window size (for validation).
152
+ baseline_window: If provided, use only the FIRST `baseline_window`
153
+ telemetry records (assumed normal). Default: all records.
154
+ """
155
+ all_records = self.telemetry_log.get(self.agent.agent_id)
156
+ if baseline_window is not None:
157
+ all_records = all_records[:baseline_window]
158
+
159
+ baseline = update_baseline(
160
+ agent=self.agent,
161
+ category=category,
162
+ model_type=model_type,
163
+ window_size=window_size,
164
+ telemetry_records=all_records,
165
+ existing_baseline=self.baseline_registry.get(
166
+ self.agent.agent_id, category
167
+ ),
168
+ )
169
+ # Register the baseline so detect_anomaly can find it
170
+ self.baseline_registry.put(baseline)
171
+ return baseline
172
+
173
+ # ─── Detection ───
174
+ def detect_anomaly(
175
+ self,
176
+ record: TelemetryRecord,
177
+ ) -> Optional[AnomalyAlert]:
178
+ """Detect an anomaly in a telemetry record."""
179
+ baseline = self.baseline_registry.get(
180
+ self.agent.agent_id, record.category
181
+ )
182
+ if baseline is None:
183
+ return None
184
+ alert = detect(record, baseline, self.anomaly_threshold)
185
+ if alert is not None:
186
+ self.alert_registry.put(alert)
187
+ self.agent.status = AgentStatus.ANOMALOUS
188
+ return alert
189
+
190
+ # ─── Containment ───
191
+ def contain(
192
+ self,
193
+ alert: AnomalyAlert,
194
+ level: ContainmentLevel = ContainmentLevel.L1_THROTTLE,
195
+ expiry_seconds: int = 300,
196
+ ) -> ContainmentPolicy:
197
+ """Contain the node."""
198
+ existing = self.containment_registry.get_by_agent(self.agent.agent_id)
199
+ existing_policy = existing[-1] if existing else None
200
+ policy = contain(
201
+ agent=self.agent,
202
+ alert=alert,
203
+ level=level,
204
+ expiry_seconds=expiry_seconds,
205
+ existing_policy=existing_policy,
206
+ )
207
+ self.containment_registry.put(policy)
208
+ self.isolation_manager.apply(policy)
209
+ return policy
210
+
211
+ def release(self) -> None:
212
+ """Release the node from containment."""
213
+ self.isolation_manager.release(self.agent.agent_id)
214
+
215
+ # ─── Halt ───
216
+ def propose_halt(
217
+ self,
218
+ target: "Node",
219
+ reason: str,
220
+ quorum_threshold: float = 0.67,
221
+ ) -> HaltOrder:
222
+ """Propose a halt order for a target node."""
223
+ signature = secrets.token_bytes(64)
224
+ order = propose_halt(
225
+ agent=target.agent,
226
+ proposer=self.agent,
227
+ proposer_signature=signature,
228
+ reason=reason,
229
+ quorum_threshold=quorum_threshold,
230
+ )
231
+ self.halt_order_registry.put(order)
232
+ return order
233
+
234
+ def approve_halt(
235
+ self,
236
+ order: HaltOrder,
237
+ total_authorized_nodes: int,
238
+ ) -> None:
239
+ """Approve a halt order."""
240
+ signature = secrets.token_bytes(64)
241
+ approve_halt(
242
+ halt_order=order,
243
+ approver=self.agent,
244
+ approver_signature=signature,
245
+ total_authorized_nodes=total_authorized_nodes,
246
+ )
247
+ if order.is_enforced():
248
+ target_id = order.agent_id
249
+ if target_id == self.agent.agent_id:
250
+ self.agent.status = AgentStatus.HALTED
251
+
252
+ # ─── Recovery ───
253
+ def snapshot(self, state: Optional[bytes] = None) -> Snapshot:
254
+ """Take a snapshot of the node's state."""
255
+ if state is None:
256
+ state = f"state-of-{self.agent.agent_id}".encode("utf-8")
257
+ snapshot = create_snapshot(self.agent, state)
258
+ self.snapshot_registry.put(snapshot)
259
+ return snapshot
260
+
261
+ def recover(
262
+ self,
263
+ halt_order: HaltOrder,
264
+ snapshot: Snapshot,
265
+ reauthorizer: "Node",
266
+ ) -> RecoveryRecord:
267
+ """Recover the node from a halt."""
268
+ record = recover(
269
+ agent=self.agent,
270
+ halt_order=halt_order,
271
+ snapshot=snapshot,
272
+ replay_log=[],
273
+ reauthorizer=reauthorizer.agent,
274
+ )
275
+ self.recovery_registry.put(record)
276
+ return record
277
+
278
+ # ─── Statistics ───
279
+ def stats(self) -> dict:
280
+ """Return node statistics."""
281
+ return {
282
+ "agent_id": self.agent.agent_id,
283
+ "status": self.agent.status.value,
284
+ "telemetry_count": self.telemetry_log.count(self.agent.agent_id),
285
+ "baseline_count": len(
286
+ self.baseline_registry.get_all(self.agent.agent_id)
287
+ ),
288
+ "alert_count": self.alert_registry.count_by_agent(
289
+ self.agent.agent_id
290
+ ),
291
+ "containment_count": len(
292
+ self.containment_registry.get_by_agent(self.agent.agent_id)
293
+ ),
294
+ "halt_count": len(
295
+ self.halt_order_registry.get_by_agent(self.agent.agent_id)
296
+ ),
297
+ "snapshot_count": self.snapshot_registry.count_by_agent(
298
+ self.agent.agent_id
299
+ ),
300
+ "recovery_count": len(
301
+ self.recovery_registry.get_by_agent(self.agent.agent_id)
302
+ ),
303
+ }
304
+
305
+ def __repr__(self) -> str:
306
+ return (
307
+ f"Node(id={self.agent.agent_id[:8]}..., "
308
+ f"status={self.agent.status.value})"
309
+ )
310
+
311
+
312
+ # ═══════════════════════════════════════════════════════════════════════════
313
+ # Cluster factory
314
+ # ═══════════════════════════════════════════════════════════════════════════
315
+
316
+
317
+ def create_cluster(
318
+ n_nodes: int,
319
+ seed: Optional[int] = None,
320
+ anomaly_threshold: float = 2.0,
321
+ ) -> list[Node]:
322
+ """
323
+ Create a cluster of N nodes.
324
+
325
+ Args:
326
+ n_nodes: Number of nodes.
327
+ seed: Random seed (for reproducibility).
328
+ anomaly_threshold: Threshold for anomaly detection.
329
+
330
+ Returns:
331
+ List of Node instances.
332
+ """
333
+ if n_nodes <= 0:
334
+ raise ValueError("n_nodes must be > 0")
335
+ nodes = []
336
+ for i in range(n_nodes):
337
+ node_seed = (seed + i) if seed is not None else None
338
+ node = Node(seed=node_seed, anomaly_threshold=anomaly_threshold)
339
+ node.start()
340
+ nodes.append(node)
341
+ return nodes