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.
- aetherruntime/__init__.py +314 -0
- aetherruntime/__version__.py +1 -0
- aetherruntime/cli.py +306 -0
- aetherruntime/containment/__init__.py +39 -0
- aetherruntime/containment/isolation.py +253 -0
- aetherruntime/containment/policy.py +273 -0
- aetherruntime/detection/__init__.py +35 -0
- aetherruntime/detection/alert.py +190 -0
- aetherruntime/detection/classifier.py +298 -0
- aetherruntime/halt/__init__.py +37 -0
- aetherruntime/halt/order.py +248 -0
- aetherruntime/halt/quorum.py +280 -0
- aetherruntime/models.py +489 -0
- aetherruntime/monitoring/__init__.py +33 -0
- aetherruntime/monitoring/baseline.py +282 -0
- aetherruntime/monitoring/telemetry.py +235 -0
- aetherruntime/py.typed +0 -0
- aetherruntime/recovery/__init__.py +37 -0
- aetherruntime/recovery/replay.py +271 -0
- aetherruntime/recovery/snapshot.py +199 -0
- aetherruntime/simulation/__init__.py +37 -0
- aetherruntime/simulation/network.py +216 -0
- aetherruntime/simulation/node.py +341 -0
- aetherruntime/simulation/testbed.py +262 -0
- aetherruntime_arch-2.0.0.dist-info/METADATA +748 -0
- aetherruntime_arch-2.0.0.dist-info/RECORD +30 -0
- aetherruntime_arch-2.0.0.dist-info/WHEEL +5 -0
- aetherruntime_arch-2.0.0.dist-info/entry_points.txt +2 -0
- aetherruntime_arch-2.0.0.dist-info/licenses/LICENSE +26 -0
- aetherruntime_arch-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Recovery: Replay (Layer 5)
|
|
3
|
+
|
|
4
|
+
This module implements Operation 7 (`recover`) from
|
|
5
|
+
docs/specification/02_operations.md.
|
|
6
|
+
|
|
7
|
+
The `recover` operation restores a halted agent to a known-good state:
|
|
8
|
+
1. Snapshot reconciliation: load the last known-good snapshot.
|
|
9
|
+
2. Safe replay: replay only actions before the anomaly.
|
|
10
|
+
3. Verification: verify the recovered state against specifications.
|
|
11
|
+
4. Reauthorization: the agent must be re-authorized by AetherGovern.
|
|
12
|
+
5. Resume: the agent resumes at the state before the anomaly.
|
|
13
|
+
|
|
14
|
+
Invariants enforced:
|
|
15
|
+
- I21: verification_result = True before running
|
|
16
|
+
- I22: snapshot_id refers to existing Snapshot
|
|
17
|
+
- I23: reauthorized_by is authorized (via AetherGovern)
|
|
18
|
+
- I33: RecoveryRecord follows a HaltOrder
|
|
19
|
+
- O7: recover requires verification_result = True
|
|
20
|
+
- X1: every admitted action has traceable authority
|
|
21
|
+
- X4: every recovery produces RecoveryRecord with verification_result = True
|
|
22
|
+
|
|
23
|
+
Errors:
|
|
24
|
+
- E19: AgentNotHalted
|
|
25
|
+
- E20: SnapshotNotFound
|
|
26
|
+
- E21: UnauthorizedReauthorizer
|
|
27
|
+
- E22: VerificationFailed
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
from ..models import (
|
|
35
|
+
Agent,
|
|
36
|
+
AgentStatus,
|
|
37
|
+
HaltOrder,
|
|
38
|
+
RecoveryRecord,
|
|
39
|
+
Snapshot,
|
|
40
|
+
_now_iso,
|
|
41
|
+
_random_hex,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
# Errors
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AgentNotHalted(Exception):
|
|
51
|
+
"""E19: Agent status != halted."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SnapshotNotFound(Exception):
|
|
56
|
+
"""E20: snapshot_id does not exist."""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UnauthorizedReauthorizer(Exception):
|
|
61
|
+
"""E21: reauthorized_by is not authorized."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class VerificationFailed(Exception):
|
|
66
|
+
"""E22: Recovered state does not match specification."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
# Verification (stub; AetherProof in production)
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _verify_recovered_state(
|
|
76
|
+
agent: Agent,
|
|
77
|
+
snapshot: Snapshot,
|
|
78
|
+
replay_log: list,
|
|
79
|
+
) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Verify that the recovered state matches specifications.
|
|
82
|
+
|
|
83
|
+
In simulation, we check:
|
|
84
|
+
- Snapshot hash is valid.
|
|
85
|
+
- Replay log contains only safe actions (no anomaly triggers).
|
|
86
|
+
|
|
87
|
+
In production, this would call AetherProof.
|
|
88
|
+
"""
|
|
89
|
+
import hashlib
|
|
90
|
+
expected = hashlib.sha256(snapshot.state).hexdigest()
|
|
91
|
+
if expected != snapshot.hash:
|
|
92
|
+
return False
|
|
93
|
+
for action in replay_log:
|
|
94
|
+
if action.get("safe", True) is False:
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
# Operation 7 — recover
|
|
101
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def recover(
|
|
105
|
+
agent: Agent,
|
|
106
|
+
halt_order: HaltOrder,
|
|
107
|
+
snapshot: Snapshot,
|
|
108
|
+
replay_log: list,
|
|
109
|
+
reauthorizer: Agent,
|
|
110
|
+
is_reauthorizer_authorized: bool = True,
|
|
111
|
+
) -> RecoveryRecord:
|
|
112
|
+
"""
|
|
113
|
+
Recover a halted agent to a known-good state (Operation 7).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
agent: The agent to recover. Must be in `halted` state.
|
|
117
|
+
halt_order: The HaltOrder that was enforced.
|
|
118
|
+
snapshot: The Snapshot to restore.
|
|
119
|
+
replay_log: List of safe actions to replay.
|
|
120
|
+
reauthorizer: The agent re-authorizing recovery (via AetherGovern).
|
|
121
|
+
is_reauthorizer_authorized: In production, checked via AetherGovern.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A new RecoveryRecord with verification_result = True.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
AgentNotHalted: If agent.status != halted.
|
|
128
|
+
SnapshotNotFound: If snapshot is None.
|
|
129
|
+
UnauthorizedReauthorizer: If reauthorizer is not authorized.
|
|
130
|
+
VerificationFailed: If recovered state does not match spec.
|
|
131
|
+
|
|
132
|
+
Invariants:
|
|
133
|
+
I21: verification_result = True before running.
|
|
134
|
+
I22: snapshot_id refers to existing Snapshot.
|
|
135
|
+
I23: reauthorized_by is authorized.
|
|
136
|
+
I33: RecoveryRecord follows a HaltOrder.
|
|
137
|
+
O7: recover requires verification_result = True.
|
|
138
|
+
X4: every recovery produces RecoveryRecord with verification_result = True.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> record = recover(agent, halt, snapshot, [], reauth)
|
|
142
|
+
>>> record.verification_result
|
|
143
|
+
True
|
|
144
|
+
>>> agent.status
|
|
145
|
+
<AgentStatus.RUNNING: 'running'>
|
|
146
|
+
"""
|
|
147
|
+
# ─── E19: Agent must be halted ───
|
|
148
|
+
if agent.status != AgentStatus.HALTED:
|
|
149
|
+
raise AgentNotHalted(
|
|
150
|
+
f"Agent {agent.agent_id} is not halted (status: {agent.status.value})"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ─── E20: Snapshot must exist ───
|
|
154
|
+
if snapshot is None:
|
|
155
|
+
raise SnapshotNotFound("snapshot must not be None")
|
|
156
|
+
if snapshot.agent_id != agent.agent_id:
|
|
157
|
+
raise SnapshotNotFound(
|
|
158
|
+
f"snapshot {snapshot.snapshot_id} does not belong to "
|
|
159
|
+
f"agent {agent.agent_id}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# ─── I33: RecoveryRecord must follow a HaltOrder ───
|
|
163
|
+
if halt_order is None or halt_order.agent_id != agent.agent_id:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"halt_order does not match agent {agent.agent_id}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# ─── E21: Reauthorizer must be authorized ───
|
|
169
|
+
if not is_reauthorizer_authorized:
|
|
170
|
+
raise UnauthorizedReauthorizer(
|
|
171
|
+
f"Reauthorizer {reauthorizer.agent_id} is not authorized"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# ─── E22: Verify recovered state ───
|
|
175
|
+
verification_result = _verify_recovered_state(agent, snapshot, replay_log)
|
|
176
|
+
if not verification_result:
|
|
177
|
+
raise VerificationFailed(
|
|
178
|
+
f"Verification failed for agent {agent.agent_id}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# ─── Q4: Update agent status to `recovering` ───
|
|
182
|
+
agent.status = AgentStatus.RECOVERING
|
|
183
|
+
|
|
184
|
+
# ─── Q1: Create RecoveryRecord ───
|
|
185
|
+
record = RecoveryRecord(
|
|
186
|
+
recovery_id=_random_hex(),
|
|
187
|
+
agent_id=agent.agent_id,
|
|
188
|
+
halt_id=halt_order.halt_id,
|
|
189
|
+
snapshot_id=snapshot.snapshot_id,
|
|
190
|
+
replay_log=replay_log,
|
|
191
|
+
verification_result=verification_result,
|
|
192
|
+
timestamp=_now_iso(),
|
|
193
|
+
reauthorized_by=reauthorizer.agent_id,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# ─── Q4: Update agent status to `running` ───
|
|
197
|
+
agent.status = AgentStatus.RUNNING
|
|
198
|
+
|
|
199
|
+
return record
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
203
|
+
# Recovery registry
|
|
204
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class RecoveryRegistry:
|
|
208
|
+
"""An in-memory registry of recovery records."""
|
|
209
|
+
|
|
210
|
+
def __init__(self) -> None:
|
|
211
|
+
self._records: dict[str, RecoveryRecord] = {}
|
|
212
|
+
self._by_agent: dict[str, list[RecoveryRecord]] = {}
|
|
213
|
+
|
|
214
|
+
def put(self, record: RecoveryRecord) -> None:
|
|
215
|
+
"""Register a recovery record."""
|
|
216
|
+
if record.recovery_id in self._records:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"recovery_id {record.recovery_id} already registered"
|
|
219
|
+
)
|
|
220
|
+
self._records[record.recovery_id] = record
|
|
221
|
+
self._by_agent.setdefault(record.agent_id, []).append(record)
|
|
222
|
+
|
|
223
|
+
def get(self, recovery_id: str) -> Optional[RecoveryRecord]:
|
|
224
|
+
"""Get a recovery record by ID."""
|
|
225
|
+
return self._records.get(recovery_id)
|
|
226
|
+
|
|
227
|
+
def get_by_agent(self, agent_id: str) -> list[RecoveryRecord]:
|
|
228
|
+
"""Get all recovery records for an agent."""
|
|
229
|
+
return list(self._by_agent.get(agent_id, []))
|
|
230
|
+
|
|
231
|
+
def count(self) -> int:
|
|
232
|
+
"""Count all recovery records."""
|
|
233
|
+
return len(self._records)
|
|
234
|
+
|
|
235
|
+
def clear(self) -> None:
|
|
236
|
+
"""Clear all records."""
|
|
237
|
+
self._records.clear()
|
|
238
|
+
self._by_agent.clear()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
+
# Recovery statistics
|
|
243
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class RecoveryStats:
|
|
247
|
+
"""Aggregated statistics for recovery records."""
|
|
248
|
+
|
|
249
|
+
def __init__(self, records: list[RecoveryRecord]) -> None:
|
|
250
|
+
self.records = records
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def total(self) -> int:
|
|
254
|
+
return len(self.records)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def n_verified(self) -> int:
|
|
258
|
+
return sum(1 for r in self.records if r.verification_result)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def verification_rate(self) -> float:
|
|
262
|
+
if self.total == 0:
|
|
263
|
+
return 0.0
|
|
264
|
+
return self.n_verified / self.total
|
|
265
|
+
|
|
266
|
+
def __repr__(self) -> str:
|
|
267
|
+
return (
|
|
268
|
+
f"RecoveryStats(total={self.total}, "
|
|
269
|
+
f"verified={self.n_verified}, "
|
|
270
|
+
f"rate={self.verification_rate:.3f})"
|
|
271
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Recovery: Snapshot (Layer 5)
|
|
3
|
+
|
|
4
|
+
This module provides snapshot management for recovery.
|
|
5
|
+
A snapshot is a point-in-time capture of an agent's state.
|
|
6
|
+
|
|
7
|
+
Key properties:
|
|
8
|
+
- Content-addressed (SHA-256 hash).
|
|
9
|
+
- Stored in AetherGit (simulated).
|
|
10
|
+
- Used by `recover` (see `replay.py`).
|
|
11
|
+
|
|
12
|
+
Invariants enforced:
|
|
13
|
+
- I24: hash matches SHA-256 of state
|
|
14
|
+
- I25: stored_at refers to an existing AetherGit record
|
|
15
|
+
- I26: snapshot_id is unique
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from ..models import (
|
|
25
|
+
Agent,
|
|
26
|
+
Snapshot,
|
|
27
|
+
_now_iso,
|
|
28
|
+
_random_hex,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
# Errors
|
|
34
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SnapshotNotFound(Exception):
|
|
38
|
+
"""Snapshot does not exist."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HashMismatch(Exception):
|
|
43
|
+
"""I24: hash does not match SHA-256 of state."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
# Snapshot creation
|
|
49
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_snapshot(
|
|
53
|
+
agent: Agent,
|
|
54
|
+
state: bytes,
|
|
55
|
+
stored_at: Optional[str] = None,
|
|
56
|
+
) -> Snapshot:
|
|
57
|
+
"""
|
|
58
|
+
Create a snapshot of an agent's state.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
agent: The agent to snapshot.
|
|
62
|
+
state: Serialized agent state (bytes).
|
|
63
|
+
stored_at: Storage location (AetherGit ref). Defaults to a generated ref.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A new Snapshot with a valid SHA-256 hash.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If state is empty or agent is None.
|
|
70
|
+
|
|
71
|
+
Invariants:
|
|
72
|
+
I24: hash matches SHA-256 of state.
|
|
73
|
+
I25: stored_at refers to a valid location.
|
|
74
|
+
I26: snapshot_id is unique.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> snapshot = create_snapshot(agent, b"state data")
|
|
78
|
+
>>> len(snapshot.hash)
|
|
79
|
+
64
|
|
80
|
+
"""
|
|
81
|
+
if agent is None:
|
|
82
|
+
raise ValueError("agent must not be None")
|
|
83
|
+
if not state:
|
|
84
|
+
raise ValueError("state must not be empty")
|
|
85
|
+
|
|
86
|
+
# ─── I24: Compute SHA-256 hash ───
|
|
87
|
+
state_hash = hashlib.sha256(state).hexdigest()
|
|
88
|
+
|
|
89
|
+
# ─── I25: Default storage location ───
|
|
90
|
+
if stored_at is None:
|
|
91
|
+
stored_at = f"aethergit://snapshots/{state_hash[:16]}"
|
|
92
|
+
|
|
93
|
+
# ─── I26: Generate unique snapshot_id ───
|
|
94
|
+
snapshot_id = _random_hex()
|
|
95
|
+
|
|
96
|
+
return Snapshot(
|
|
97
|
+
snapshot_id=snapshot_id,
|
|
98
|
+
agent_id=agent.agent_id,
|
|
99
|
+
state=state,
|
|
100
|
+
hash=state_hash,
|
|
101
|
+
timestamp=_now_iso(),
|
|
102
|
+
stored_at=stored_at,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def verify_snapshot(snapshot: Snapshot) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Verify that a snapshot's hash matches its state (I24).
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
snapshot: The Snapshot to verify.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if hash matches, False otherwise.
|
|
115
|
+
"""
|
|
116
|
+
expected = hashlib.sha256(snapshot.state).hexdigest()
|
|
117
|
+
return expected == snapshot.hash
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
121
|
+
# Snapshot registry
|
|
122
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SnapshotRegistry:
|
|
126
|
+
"""
|
|
127
|
+
An in-memory registry of snapshots.
|
|
128
|
+
|
|
129
|
+
Indexes:
|
|
130
|
+
- by_id: dict[snapshot_id → Snapshot]
|
|
131
|
+
- by_agent: dict[agent_id → list[Snapshot]]
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self) -> None:
|
|
135
|
+
self._by_id: dict[str, Snapshot] = {}
|
|
136
|
+
self._by_agent: dict[str, list[Snapshot]] = {}
|
|
137
|
+
|
|
138
|
+
def put(self, snapshot: Snapshot) -> None:
|
|
139
|
+
"""Register a snapshot (I26: snapshot_id unique)."""
|
|
140
|
+
if snapshot.snapshot_id in self._by_id:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"snapshot_id {snapshot.snapshot_id} already registered"
|
|
143
|
+
)
|
|
144
|
+
# I24: verify hash before storing
|
|
145
|
+
if not verify_snapshot(snapshot):
|
|
146
|
+
raise HashMismatch(
|
|
147
|
+
f"snapshot {snapshot.snapshot_id} has invalid hash"
|
|
148
|
+
)
|
|
149
|
+
self._by_id[snapshot.snapshot_id] = snapshot
|
|
150
|
+
self._by_agent.setdefault(snapshot.agent_id, []).append(snapshot)
|
|
151
|
+
|
|
152
|
+
def get(self, snapshot_id: str) -> Optional[Snapshot]:
|
|
153
|
+
"""Get a snapshot by ID."""
|
|
154
|
+
return self._by_id.get(snapshot_id)
|
|
155
|
+
|
|
156
|
+
def get_by_agent(self, agent_id: str) -> list[Snapshot]:
|
|
157
|
+
"""Get all snapshots for an agent."""
|
|
158
|
+
return list(self._by_agent.get(agent_id, []))
|
|
159
|
+
|
|
160
|
+
def latest_for_agent(self, agent_id: str) -> Optional[Snapshot]:
|
|
161
|
+
"""Get the latest snapshot for an agent."""
|
|
162
|
+
snapshots = self.get_by_agent(agent_id)
|
|
163
|
+
if not snapshots:
|
|
164
|
+
return None
|
|
165
|
+
snapshots.sort(
|
|
166
|
+
key=lambda s: datetime.fromisoformat(
|
|
167
|
+
s.timestamp.replace("Z", "+00:00")
|
|
168
|
+
),
|
|
169
|
+
reverse=True,
|
|
170
|
+
)
|
|
171
|
+
return snapshots[0]
|
|
172
|
+
|
|
173
|
+
def count(self) -> int:
|
|
174
|
+
"""Count all snapshots."""
|
|
175
|
+
return len(self._by_id)
|
|
176
|
+
|
|
177
|
+
def count_by_agent(self, agent_id: str) -> int:
|
|
178
|
+
"""Count snapshots for an agent."""
|
|
179
|
+
return len(self._by_agent.get(agent_id, []))
|
|
180
|
+
|
|
181
|
+
def clear(self) -> None:
|
|
182
|
+
"""Clear all snapshots."""
|
|
183
|
+
self._by_id.clear()
|
|
184
|
+
self._by_agent.clear()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
+
# Convenience functions
|
|
189
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_snapshot_hash(state: bytes) -> str:
|
|
193
|
+
"""Compute the SHA-256 hash of a state."""
|
|
194
|
+
return hashlib.sha256(state).hexdigest()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def compare_snapshots(a: Snapshot, b: Snapshot) -> bool:
|
|
198
|
+
"""Check if two snapshots have the same state."""
|
|
199
|
+
return a.hash == b.hash
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""AetherRuntime — Simulation layer (Layer 6, simulation only)."""
|
|
2
|
+
|
|
3
|
+
from .network import (
|
|
4
|
+
Network,
|
|
5
|
+
NetworkState,
|
|
6
|
+
PartitionSchedule,
|
|
7
|
+
Message,
|
|
8
|
+
)
|
|
9
|
+
from .node import (
|
|
10
|
+
Node,
|
|
11
|
+
create_cluster,
|
|
12
|
+
)
|
|
13
|
+
from .testbed import (
|
|
14
|
+
Testbed,
|
|
15
|
+
TestbedConfig,
|
|
16
|
+
Scenario,
|
|
17
|
+
SCENARIOS,
|
|
18
|
+
experiment_1_detection_accuracy,
|
|
19
|
+
experiment_2_halt_window,
|
|
20
|
+
experiment_3_recovery_correctness,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Network",
|
|
25
|
+
"NetworkState",
|
|
26
|
+
"PartitionSchedule",
|
|
27
|
+
"Message",
|
|
28
|
+
"Node",
|
|
29
|
+
"create_cluster",
|
|
30
|
+
"Testbed",
|
|
31
|
+
"TestbedConfig",
|
|
32
|
+
"Scenario",
|
|
33
|
+
"SCENARIOS",
|
|
34
|
+
"experiment_1_detection_accuracy",
|
|
35
|
+
"experiment_2_halt_window",
|
|
36
|
+
"experiment_3_recovery_correctness",
|
|
37
|
+
]
|