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,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
+ ]