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
aetherruntime/models.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Core Entities
|
|
3
|
+
|
|
4
|
+
This module implements the 10 core entities defined in
|
|
5
|
+
docs/specification/01_entities.md:
|
|
6
|
+
|
|
7
|
+
1. Agent
|
|
8
|
+
2. TelemetryRecord
|
|
9
|
+
3. Baseline
|
|
10
|
+
4. AnomalyAlert
|
|
11
|
+
5. ContainmentPolicy
|
|
12
|
+
6. HaltOrder
|
|
13
|
+
7. HaltApproval
|
|
14
|
+
8. RecoveryRecord
|
|
15
|
+
9. Snapshot
|
|
16
|
+
10. RuntimeConfig
|
|
17
|
+
|
|
18
|
+
Each entity is a dataclass with validation in __post_init__.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
# Enums
|
|
31
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentStatus(str, Enum):
|
|
35
|
+
"""Agent status (I3)."""
|
|
36
|
+
IDLE = "idle"
|
|
37
|
+
RUNNING = "running"
|
|
38
|
+
ANOMALOUS = "anomalous"
|
|
39
|
+
CONTAINED = "contained"
|
|
40
|
+
HALTED = "halted"
|
|
41
|
+
RECOVERING = "recovering"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TelemetryCategory(str, Enum):
|
|
45
|
+
"""Telemetry categories."""
|
|
46
|
+
CPU = "cpu"
|
|
47
|
+
MEMORY = "memory"
|
|
48
|
+
NETWORK = "network"
|
|
49
|
+
SYSCALL = "syscall"
|
|
50
|
+
ACTION = "action"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BaselineModel(str, Enum):
|
|
54
|
+
"""Baseline model types (I8)."""
|
|
55
|
+
MEAN_STD = "mean_std"
|
|
56
|
+
ZSCORE = "zscore"
|
|
57
|
+
MOVING_AVERAGE = "moving_average"
|
|
58
|
+
SEASONAL = "seasonal"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ContainmentLevel(str, Enum):
|
|
62
|
+
"""Containment levels (I11)."""
|
|
63
|
+
L1_THROTTLE = "L1_throttle"
|
|
64
|
+
L2_ISOLATE = "L2_isolate"
|
|
65
|
+
L3_QUARANTINE = "L3_quarantine"
|
|
66
|
+
L4_FREEZE = "L4_freeze"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# Helpers
|
|
71
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _now_iso() -> str:
|
|
75
|
+
"""Return current UTC timestamp in ISO 8601 format."""
|
|
76
|
+
return datetime.now(timezone.utc).isoformat()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _validate_hex(value: str, length: int, name: str) -> None:
|
|
80
|
+
"""Validate that value is a hex string of exact length."""
|
|
81
|
+
if not isinstance(value, str) or len(value) != length:
|
|
82
|
+
raise ValueError(f"{name} must be a hex string of length {length}")
|
|
83
|
+
try:
|
|
84
|
+
int(value, 16)
|
|
85
|
+
except ValueError:
|
|
86
|
+
raise ValueError(f"{name} must be a valid hex string")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_iso8601(value: str, name: str) -> None:
|
|
90
|
+
"""Validate that value is a valid ISO 8601 string."""
|
|
91
|
+
try:
|
|
92
|
+
datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
93
|
+
except (ValueError, AttributeError):
|
|
94
|
+
raise ValueError(f"{name} must be a valid ISO 8601 string")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
# Entity 1 — Agent
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class Agent:
|
|
104
|
+
"""
|
|
105
|
+
An autonomous software actor (Entity 1).
|
|
106
|
+
|
|
107
|
+
Fields: 5 (agent_id, public_key, created_at, metadata, status)
|
|
108
|
+
Invariants: I1, I2, I3
|
|
109
|
+
"""
|
|
110
|
+
agent_id: str
|
|
111
|
+
public_key: bytes
|
|
112
|
+
created_at: str
|
|
113
|
+
status: AgentStatus = AgentStatus.IDLE
|
|
114
|
+
metadata: dict = field(default_factory=dict)
|
|
115
|
+
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
# I1: agent_id is a 64-char hex string
|
|
118
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
119
|
+
# I2: public_key is 32 bytes (Ed25519)
|
|
120
|
+
if len(self.public_key) != 32:
|
|
121
|
+
raise ValueError("public_key must be 32 bytes (Ed25519)")
|
|
122
|
+
# I3: status is a valid AgentStatus
|
|
123
|
+
if not isinstance(self.status, AgentStatus):
|
|
124
|
+
raise ValueError(f"status must be AgentStatus, got {type(self.status)}")
|
|
125
|
+
_validate_iso8601(self.created_at, "created_at")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
129
|
+
# Entity 2 — TelemetryRecord
|
|
130
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class TelemetryRecord:
|
|
135
|
+
"""
|
|
136
|
+
A time-stamped observation from a running agent (Entity 2).
|
|
137
|
+
|
|
138
|
+
Fields: 6 (telemetry_id, agent_id, category, value, timestamp, metadata)
|
|
139
|
+
Invariants: I4, I5, I6
|
|
140
|
+
"""
|
|
141
|
+
telemetry_id: str
|
|
142
|
+
agent_id: str
|
|
143
|
+
category: TelemetryCategory
|
|
144
|
+
value: float
|
|
145
|
+
timestamp: str
|
|
146
|
+
metadata: dict = field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
def __post_init__(self) -> None:
|
|
149
|
+
_validate_hex(self.telemetry_id, 64, "telemetry_id")
|
|
150
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
151
|
+
# I5: value is finite
|
|
152
|
+
if not isinstance(self.value, (int, float)):
|
|
153
|
+
raise ValueError("value must be int or float")
|
|
154
|
+
if self.value != self.value or self.value in (float("inf"), float("-inf")):
|
|
155
|
+
raise ValueError("value must be finite (not NaN or Inf)")
|
|
156
|
+
_validate_iso8601(self.timestamp, "timestamp")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
160
|
+
# Entity 3 — Baseline
|
|
161
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class Baseline:
|
|
166
|
+
"""
|
|
167
|
+
A statistical model of expected behaviour (Entity 3).
|
|
168
|
+
|
|
169
|
+
Fields: 7 (baseline_id, agent_id, category, model_type,
|
|
170
|
+
parameters, computed_at, sample_count)
|
|
171
|
+
Invariants: I7, I8
|
|
172
|
+
"""
|
|
173
|
+
baseline_id: str
|
|
174
|
+
agent_id: str
|
|
175
|
+
category: TelemetryCategory
|
|
176
|
+
model_type: BaselineModel
|
|
177
|
+
parameters: dict
|
|
178
|
+
computed_at: str
|
|
179
|
+
sample_count: int
|
|
180
|
+
|
|
181
|
+
def __post_init__(self) -> None:
|
|
182
|
+
_validate_hex(self.baseline_id, 64, "baseline_id")
|
|
183
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
184
|
+
# I7: sample_count >= 10
|
|
185
|
+
if self.sample_count < 10:
|
|
186
|
+
raise ValueError("sample_count must be >= 10")
|
|
187
|
+
# I8: parameters contains required keys
|
|
188
|
+
required_keys = {
|
|
189
|
+
BaselineModel.MEAN_STD: {"mean", "std"},
|
|
190
|
+
BaselineModel.ZSCORE: {"mean", "std"},
|
|
191
|
+
BaselineModel.MOVING_AVERAGE: {"window", "values"},
|
|
192
|
+
BaselineModel.SEASONAL: {"period", "coefficients"},
|
|
193
|
+
}
|
|
194
|
+
required = required_keys.get(self.model_type, set())
|
|
195
|
+
missing = required - set(self.parameters.keys())
|
|
196
|
+
if missing:
|
|
197
|
+
raise ValueError(f"parameters missing keys: {missing}")
|
|
198
|
+
_validate_iso8601(self.computed_at, "computed_at")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
202
|
+
# Entity 4 — AnomalyAlert
|
|
203
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class AnomalyAlert:
|
|
208
|
+
"""
|
|
209
|
+
A record produced by detection (Entity 4).
|
|
210
|
+
|
|
211
|
+
Fields: 7 (alert_id, agent_id, telemetry_id, baseline_id,
|
|
212
|
+
confidence, reason, timestamp)
|
|
213
|
+
Invariants: I9, I10, I30
|
|
214
|
+
"""
|
|
215
|
+
alert_id: str
|
|
216
|
+
agent_id: str
|
|
217
|
+
telemetry_id: str
|
|
218
|
+
baseline_id: str
|
|
219
|
+
confidence: float
|
|
220
|
+
reason: str
|
|
221
|
+
timestamp: str
|
|
222
|
+
|
|
223
|
+
def __post_init__(self) -> None:
|
|
224
|
+
_validate_hex(self.alert_id, 64, "alert_id")
|
|
225
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
226
|
+
_validate_hex(self.telemetry_id, 64, "telemetry_id")
|
|
227
|
+
_validate_hex(self.baseline_id, 64, "baseline_id")
|
|
228
|
+
# I9: confidence in [0.0, 1.0]
|
|
229
|
+
if not (0.0 <= self.confidence <= 1.0):
|
|
230
|
+
raise ValueError(f"confidence must be in [0.0, 1.0], got {self.confidence}")
|
|
231
|
+
_validate_iso8601(self.timestamp, "timestamp")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
235
|
+
# Entity 5 — ContainmentPolicy
|
|
236
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class ContainmentPolicy:
|
|
241
|
+
"""
|
|
242
|
+
A record specifying containment (Entity 5).
|
|
243
|
+
|
|
244
|
+
Fields: 7 (policy_id, agent_id, alert_id, level,
|
|
245
|
+
restrictions, expiry, created_at)
|
|
246
|
+
Invariants: I11, I12, I13
|
|
247
|
+
"""
|
|
248
|
+
policy_id: str
|
|
249
|
+
agent_id: str
|
|
250
|
+
alert_id: str
|
|
251
|
+
level: ContainmentLevel
|
|
252
|
+
restrictions: dict
|
|
253
|
+
expiry: str
|
|
254
|
+
created_at: str
|
|
255
|
+
|
|
256
|
+
def __post_init__(self) -> None:
|
|
257
|
+
_validate_hex(self.policy_id, 64, "policy_id")
|
|
258
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
259
|
+
_validate_hex(self.alert_id, 64, "alert_id")
|
|
260
|
+
# I11: level is a valid ContainmentLevel
|
|
261
|
+
if not isinstance(self.level, ContainmentLevel):
|
|
262
|
+
raise ValueError(f"level must be ContainmentLevel, got {type(self.level)}")
|
|
263
|
+
# I12: expiry > created_at
|
|
264
|
+
_validate_iso8601(self.expiry, "expiry")
|
|
265
|
+
_validate_iso8601(self.created_at, "created_at")
|
|
266
|
+
expiry_dt = datetime.fromisoformat(self.expiry.replace("Z", "+00:00"))
|
|
267
|
+
created_dt = datetime.fromisoformat(self.created_at.replace("Z", "+00:00"))
|
|
268
|
+
if expiry_dt <= created_dt:
|
|
269
|
+
raise ValueError("expiry must be > created_at")
|
|
270
|
+
# I13: restrictions contains required keys
|
|
271
|
+
required = {"cpu_percent", "memory_mb", "network", "filesystem"}
|
|
272
|
+
missing = required - set(self.restrictions.keys())
|
|
273
|
+
if missing:
|
|
274
|
+
raise ValueError(f"restrictions missing keys: {missing}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
278
|
+
# Entity 6 — HaltOrder
|
|
279
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class HaltOrder:
|
|
284
|
+
"""
|
|
285
|
+
A signed halt order (Entity 6).
|
|
286
|
+
|
|
287
|
+
Fields: 9 (halt_id, agent_id, reason, proposer, proposer_signature,
|
|
288
|
+
approvals, quorum_threshold, created_at, enforced_at)
|
|
289
|
+
Invariants: I14, I15, I16, I17
|
|
290
|
+
"""
|
|
291
|
+
halt_id: str
|
|
292
|
+
agent_id: str
|
|
293
|
+
reason: str
|
|
294
|
+
proposer: str
|
|
295
|
+
proposer_signature: bytes
|
|
296
|
+
quorum_threshold: float
|
|
297
|
+
created_at: str
|
|
298
|
+
approvals: list = field(default_factory=list)
|
|
299
|
+
enforced_at: Optional[str] = None
|
|
300
|
+
|
|
301
|
+
def __post_init__(self) -> None:
|
|
302
|
+
_validate_hex(self.halt_id, 64, "halt_id")
|
|
303
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
304
|
+
_validate_hex(self.proposer, 64, "proposer")
|
|
305
|
+
if len(self.proposer_signature) != 64:
|
|
306
|
+
raise ValueError("proposer_signature must be 64 bytes")
|
|
307
|
+
# I15: quorum_threshold in [0.5, 1.0]
|
|
308
|
+
if not (0.5 <= self.quorum_threshold <= 1.0):
|
|
309
|
+
raise ValueError("quorum_threshold must be in [0.5, 1.0]")
|
|
310
|
+
_validate_iso8601(self.created_at, "created_at")
|
|
311
|
+
if self.enforced_at is not None:
|
|
312
|
+
_validate_iso8601(self.enforced_at, "enforced_at")
|
|
313
|
+
|
|
314
|
+
def is_enforced(self) -> bool:
|
|
315
|
+
"""Check if quorum is reached and halt is enforced (I16)."""
|
|
316
|
+
return self.enforced_at is not None
|
|
317
|
+
|
|
318
|
+
def halt_window_seconds(self) -> Optional[float]:
|
|
319
|
+
"""Compute Δt_window (I17, Proposition 2)."""
|
|
320
|
+
if self.enforced_at is None:
|
|
321
|
+
return None
|
|
322
|
+
created_dt = datetime.fromisoformat(self.created_at.replace("Z", "+00:00"))
|
|
323
|
+
enforced_dt = datetime.fromisoformat(self.enforced_at.replace("Z", "+00:00"))
|
|
324
|
+
return (enforced_dt - created_dt).total_seconds()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
328
|
+
# Entity 7 — HaltApproval
|
|
329
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dataclass
|
|
333
|
+
class HaltApproval:
|
|
334
|
+
"""
|
|
335
|
+
A signed approval for a halt order (Entity 7).
|
|
336
|
+
|
|
337
|
+
Fields: 5 (approval_id, halt_id, approver, signature, timestamp)
|
|
338
|
+
Invariants: I18, I19, I20
|
|
339
|
+
"""
|
|
340
|
+
approval_id: str
|
|
341
|
+
halt_id: str
|
|
342
|
+
approver: str
|
|
343
|
+
signature: bytes
|
|
344
|
+
timestamp: str
|
|
345
|
+
|
|
346
|
+
def __post_init__(self) -> None:
|
|
347
|
+
_validate_hex(self.approval_id, 64, "approval_id")
|
|
348
|
+
_validate_hex(self.halt_id, 64, "halt_id")
|
|
349
|
+
_validate_hex(self.approver, 64, "approver")
|
|
350
|
+
if len(self.signature) != 64:
|
|
351
|
+
raise ValueError("signature must be 64 bytes")
|
|
352
|
+
_validate_iso8601(self.timestamp, "timestamp")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
356
|
+
# Entity 8 — Snapshot
|
|
357
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@dataclass
|
|
361
|
+
class Snapshot:
|
|
362
|
+
"""
|
|
363
|
+
A point-in-time capture of an agent's state (Entity 9).
|
|
364
|
+
|
|
365
|
+
Fields: 6 (snapshot_id, agent_id, state, hash, timestamp, stored_at)
|
|
366
|
+
Invariants: I24, I25, I26
|
|
367
|
+
"""
|
|
368
|
+
snapshot_id: str
|
|
369
|
+
agent_id: str
|
|
370
|
+
state: bytes
|
|
371
|
+
hash: str
|
|
372
|
+
timestamp: str
|
|
373
|
+
stored_at: str
|
|
374
|
+
|
|
375
|
+
def __post_init__(self) -> None:
|
|
376
|
+
_validate_hex(self.snapshot_id, 64, "snapshot_id")
|
|
377
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
378
|
+
# I24: hash matches SHA-256 of state
|
|
379
|
+
import hashlib
|
|
380
|
+
actual = hashlib.sha256(self.state).hexdigest()
|
|
381
|
+
if self.hash != actual:
|
|
382
|
+
raise ValueError(f"hash mismatch: expected {actual}, got {self.hash}")
|
|
383
|
+
_validate_iso8601(self.timestamp, "timestamp")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
387
|
+
# Entity 9 — RecoveryRecord
|
|
388
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass
|
|
392
|
+
class RecoveryRecord:
|
|
393
|
+
"""
|
|
394
|
+
A record of a recovery operation (Entity 8).
|
|
395
|
+
|
|
396
|
+
Fields: 8 (recovery_id, agent_id, halt_id, snapshot_id,
|
|
397
|
+
replay_log, verification_result, timestamp, reauthorized_by)
|
|
398
|
+
Invariants: I21, I22, I23, I33
|
|
399
|
+
"""
|
|
400
|
+
recovery_id: str
|
|
401
|
+
agent_id: str
|
|
402
|
+
halt_id: str
|
|
403
|
+
snapshot_id: str
|
|
404
|
+
replay_log: list
|
|
405
|
+
verification_result: bool
|
|
406
|
+
timestamp: str
|
|
407
|
+
reauthorized_by: str
|
|
408
|
+
|
|
409
|
+
def __post_init__(self) -> None:
|
|
410
|
+
_validate_hex(self.recovery_id, 64, "recovery_id")
|
|
411
|
+
_validate_hex(self.agent_id, 64, "agent_id")
|
|
412
|
+
_validate_hex(self.halt_id, 64, "halt_id")
|
|
413
|
+
_validate_hex(self.snapshot_id, 64, "snapshot_id")
|
|
414
|
+
_validate_hex(self.reauthorized_by, 64, "reauthorized_by")
|
|
415
|
+
_validate_iso8601(self.timestamp, "timestamp")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
419
|
+
# Entity 10 — RuntimeConfig
|
|
420
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@dataclass
|
|
424
|
+
class RuntimeConfig:
|
|
425
|
+
"""
|
|
426
|
+
A configuration record (Entity 10).
|
|
427
|
+
|
|
428
|
+
Fields: 6 (config_id, anomaly_threshold, halt_quorum,
|
|
429
|
+
telemetry_rate_hz, containment_expiry_seconds, created_at)
|
|
430
|
+
Invariants: I27, I28, I29
|
|
431
|
+
"""
|
|
432
|
+
config_id: str
|
|
433
|
+
anomaly_threshold: float
|
|
434
|
+
halt_quorum: float
|
|
435
|
+
telemetry_rate_hz: float
|
|
436
|
+
containment_expiry_seconds: int
|
|
437
|
+
created_at: str
|
|
438
|
+
|
|
439
|
+
def __post_init__(self) -> None:
|
|
440
|
+
_validate_hex(self.config_id, 64, "config_id")
|
|
441
|
+
# I27: anomaly_threshold > 0
|
|
442
|
+
if self.anomaly_threshold <= 0:
|
|
443
|
+
raise ValueError("anomaly_threshold must be > 0")
|
|
444
|
+
# I28: halt_quorum in [0.5, 1.0]
|
|
445
|
+
if not (0.5 <= self.halt_quorum <= 1.0):
|
|
446
|
+
raise ValueError("halt_quorum must be in [0.5, 1.0]")
|
|
447
|
+
# I29: telemetry_rate_hz > 0
|
|
448
|
+
if self.telemetry_rate_hz <= 0:
|
|
449
|
+
raise ValueError("telemetry_rate_hz must be > 0")
|
|
450
|
+
_validate_iso8601(self.created_at, "created_at")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
454
|
+
# Factory functions
|
|
455
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _random_hex() -> str:
|
|
459
|
+
"""Generate a random 64-char hex string."""
|
|
460
|
+
import secrets
|
|
461
|
+
return secrets.token_hex(32)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def create_agent(public_key: bytes, metadata: Optional[dict] = None) -> Agent:
|
|
465
|
+
"""Factory: create a new Agent."""
|
|
466
|
+
return Agent(
|
|
467
|
+
agent_id=_random_hex(),
|
|
468
|
+
public_key=public_key,
|
|
469
|
+
created_at=_now_iso(),
|
|
470
|
+
status=AgentStatus.IDLE,
|
|
471
|
+
metadata=metadata or {},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def create_runtime_config(
|
|
476
|
+
anomaly_threshold: float = 1.0,
|
|
477
|
+
halt_quorum: float = 0.67,
|
|
478
|
+
telemetry_rate_hz: float = 1.0,
|
|
479
|
+
containment_expiry_seconds: int = 300,
|
|
480
|
+
) -> RuntimeConfig:
|
|
481
|
+
"""Factory: create a RuntimeConfig with defaults."""
|
|
482
|
+
return RuntimeConfig(
|
|
483
|
+
config_id=_random_hex(),
|
|
484
|
+
anomaly_threshold=anomaly_threshold,
|
|
485
|
+
halt_quorum=halt_quorum,
|
|
486
|
+
telemetry_rate_hz=telemetry_rate_hz,
|
|
487
|
+
containment_expiry_seconds=containment_expiry_seconds,
|
|
488
|
+
created_at=_now_iso(),
|
|
489
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""AetherRuntime — Monitoring layer (Layer 1)."""
|
|
2
|
+
|
|
3
|
+
from .telemetry import (
|
|
4
|
+
collect,
|
|
5
|
+
collect_batch,
|
|
6
|
+
TelemetryLog,
|
|
7
|
+
AgentNotRunning,
|
|
8
|
+
InvalidValue,
|
|
9
|
+
NonMonotonicTimestamp,
|
|
10
|
+
)
|
|
11
|
+
from .baseline import (
|
|
12
|
+
update_baseline,
|
|
13
|
+
update_all_baselines,
|
|
14
|
+
BaselineRegistry,
|
|
15
|
+
AgentNotFound,
|
|
16
|
+
InsufficientTelemetry,
|
|
17
|
+
InvalidWindowSize,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"collect",
|
|
22
|
+
"collect_batch",
|
|
23
|
+
"TelemetryLog",
|
|
24
|
+
"AgentNotRunning",
|
|
25
|
+
"InvalidValue",
|
|
26
|
+
"NonMonotonicTimestamp",
|
|
27
|
+
"update_baseline",
|
|
28
|
+
"update_all_baselines",
|
|
29
|
+
"BaselineRegistry",
|
|
30
|
+
"AgentNotFound",
|
|
31
|
+
"InsufficientTelemetry",
|
|
32
|
+
"InvalidWindowSize",
|
|
33
|
+
]
|