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,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Halt: Order (Layer 4)
|
|
3
|
+
|
|
4
|
+
This module implements Operation 5 (`propose_halt`) from
|
|
5
|
+
docs/specification/02_operations.md.
|
|
6
|
+
|
|
7
|
+
The `propose_halt` operation creates a signed halt order for an agent.
|
|
8
|
+
The halt becomes enforced when a quorum of authorized nodes approves it
|
|
9
|
+
(see `quorum.py`).
|
|
10
|
+
|
|
11
|
+
Key properties:
|
|
12
|
+
- The proposer must be authorized (via AetherGovern).
|
|
13
|
+
- The proposer's signature must verify (Ed25519).
|
|
14
|
+
- The halt window Δt_window > 0 (Proposition 2).
|
|
15
|
+
|
|
16
|
+
Invariants enforced:
|
|
17
|
+
- I14: proposer_signature verifies
|
|
18
|
+
- I15: quorum_threshold in [0.5, 1.0]
|
|
19
|
+
- I16: enforced_at set iff quorum reached
|
|
20
|
+
- I17: Δt_window > 0 (Proposition 2)
|
|
21
|
+
- O5: propose_halt requires valid signature
|
|
22
|
+
|
|
23
|
+
Errors:
|
|
24
|
+
- E15: AgentNotFound
|
|
25
|
+
- E16: UnauthorizedProposer
|
|
26
|
+
- E17: InvalidSignature
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
from ..models import (
|
|
35
|
+
Agent,
|
|
36
|
+
AgentStatus,
|
|
37
|
+
HaltOrder,
|
|
38
|
+
_now_iso,
|
|
39
|
+
_random_hex,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
# Errors
|
|
45
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AgentNotFound(Exception):
|
|
49
|
+
"""E15: Agent does not exist."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UnauthorizedProposer(Exception):
|
|
54
|
+
"""E16: Proposer is not authorized (via AetherGovern)."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class InvalidSignature(Exception):
|
|
59
|
+
"""E17: proposer_signature does not verify."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
64
|
+
# Signature verification (stub; Ed25519 in production)
|
|
65
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _verify_signature(
|
|
69
|
+
public_key: bytes,
|
|
70
|
+
signature: bytes,
|
|
71
|
+
message: bytes,
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Verify an Ed25519 signature.
|
|
75
|
+
|
|
76
|
+
In simulation, we use a deterministic hash-based stub. In production,
|
|
77
|
+
this would use the `cryptography` library's Ed25519 verifier.
|
|
78
|
+
"""
|
|
79
|
+
if len(signature) != 64:
|
|
80
|
+
return False
|
|
81
|
+
if len(public_key) != 32:
|
|
82
|
+
return False
|
|
83
|
+
# Stub: signature must be 64 bytes and start with a non-zero byte
|
|
84
|
+
# In production: Ed25519PublicKey.from_public_bytes(public_key).verify(signature, message)
|
|
85
|
+
return signature[0] != 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
# Operation 5 — propose_halt
|
|
90
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def propose_halt(
|
|
94
|
+
agent: Agent,
|
|
95
|
+
proposer: Agent,
|
|
96
|
+
proposer_signature: bytes,
|
|
97
|
+
reason: str,
|
|
98
|
+
quorum_threshold: float = 0.67,
|
|
99
|
+
is_proposer_authorized: bool = True,
|
|
100
|
+
) -> HaltOrder:
|
|
101
|
+
"""
|
|
102
|
+
Propose a halt order for a compromised agent (Operation 5).
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent: The agent to halt.
|
|
106
|
+
proposer: The agent proposing the halt. Must be authorized.
|
|
107
|
+
proposer_signature: Ed25519 signature from the proposer.
|
|
108
|
+
reason: Human-readable reason for the halt.
|
|
109
|
+
quorum_threshold: Required fraction of approvals (default 0.67 = 2/3).
|
|
110
|
+
is_proposer_authorized: In production, checked via AetherGovern.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A new HaltOrder with `enforced_at = None`.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
AgentNotFound: If agent or proposer is None.
|
|
117
|
+
UnauthorizedProposer: If proposer is not authorized.
|
|
118
|
+
InvalidSignature: If proposer_signature does not verify.
|
|
119
|
+
|
|
120
|
+
Invariants:
|
|
121
|
+
I14: proposer_signature verifies.
|
|
122
|
+
I15: quorum_threshold in [0.5, 1.0].
|
|
123
|
+
I16: enforced_at is None (until quorum reached).
|
|
124
|
+
O5: propose_halt requires valid signature.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> order = propose_halt(agent, proposer, sig, "anomalous CPU")
|
|
128
|
+
>>> order.enforced_at
|
|
129
|
+
None
|
|
130
|
+
"""
|
|
131
|
+
# ─── E15: Agent and proposer must exist ───
|
|
132
|
+
if agent is None:
|
|
133
|
+
raise AgentNotFound("agent must not be None")
|
|
134
|
+
if proposer is None:
|
|
135
|
+
raise AgentNotFound("proposer must not be None")
|
|
136
|
+
|
|
137
|
+
# ─── E16: Proposer must be authorized (via AetherGovern) ───
|
|
138
|
+
if not is_proposer_authorized:
|
|
139
|
+
raise UnauthorizedProposer(
|
|
140
|
+
f"Proposer {proposer.agent_id} is not authorized"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ─── I15: quorum_threshold in [0.5, 1.0] ───
|
|
144
|
+
if not (0.5 <= quorum_threshold <= 1.0):
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"quorum_threshold must be in [0.5, 1.0], got {quorum_threshold}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# ─── I14: Verify proposer signature ───
|
|
150
|
+
halt_id = _random_hex()
|
|
151
|
+
created_at = _now_iso()
|
|
152
|
+
message = _halt_message(halt_id, agent.agent_id, reason, created_at)
|
|
153
|
+
if not _verify_signature(proposer.public_key, proposer_signature, message):
|
|
154
|
+
raise InvalidSignature("proposer_signature does not verify")
|
|
155
|
+
|
|
156
|
+
# ─── Q1: Create HaltOrder with enforced_at = None ───
|
|
157
|
+
return HaltOrder(
|
|
158
|
+
halt_id=halt_id,
|
|
159
|
+
agent_id=agent.agent_id,
|
|
160
|
+
reason=reason,
|
|
161
|
+
proposer=proposer.agent_id,
|
|
162
|
+
proposer_signature=proposer_signature,
|
|
163
|
+
quorum_threshold=quorum_threshold,
|
|
164
|
+
created_at=created_at,
|
|
165
|
+
approvals=[],
|
|
166
|
+
enforced_at=None, # I16: not enforced until quorum
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _halt_message(
|
|
171
|
+
halt_id: str,
|
|
172
|
+
agent_id: str,
|
|
173
|
+
reason: str,
|
|
174
|
+
created_at: str,
|
|
175
|
+
) -> bytes:
|
|
176
|
+
"""Construct the message that the proposer signs."""
|
|
177
|
+
return hashlib.sha256(
|
|
178
|
+
f"{halt_id}|{agent_id}|{reason}|{created_at}".encode("utf-8")
|
|
179
|
+
).digest()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
+
# Halt order registry
|
|
184
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class HaltOrderRegistry:
|
|
188
|
+
"""An in-memory registry of halt orders."""
|
|
189
|
+
|
|
190
|
+
def __init__(self) -> None:
|
|
191
|
+
self._orders: dict[str, HaltOrder] = {}
|
|
192
|
+
self._by_agent: dict[str, list[HaltOrder]] = {}
|
|
193
|
+
|
|
194
|
+
def put(self, order: HaltOrder) -> None:
|
|
195
|
+
"""Register a halt order."""
|
|
196
|
+
if order.halt_id in self._orders:
|
|
197
|
+
raise ValueError(f"halt_id {order.halt_id} already registered")
|
|
198
|
+
self._orders[order.halt_id] = order
|
|
199
|
+
self._by_agent.setdefault(order.agent_id, []).append(order)
|
|
200
|
+
|
|
201
|
+
def get(self, halt_id: str) -> Optional[HaltOrder]:
|
|
202
|
+
"""Get a halt order by ID."""
|
|
203
|
+
return self._orders.get(halt_id)
|
|
204
|
+
|
|
205
|
+
def get_by_agent(self, agent_id: str) -> list[HaltOrder]:
|
|
206
|
+
"""Get all halt orders for an agent."""
|
|
207
|
+
return list(self._by_agent.get(agent_id, []))
|
|
208
|
+
|
|
209
|
+
def enforced_for_agent(self, agent_id: str) -> list[HaltOrder]:
|
|
210
|
+
"""Get all enforced halt orders for an agent."""
|
|
211
|
+
return [o for o in self.get_by_agent(agent_id) if o.is_enforced()]
|
|
212
|
+
|
|
213
|
+
def count(self) -> int:
|
|
214
|
+
"""Count all halt orders."""
|
|
215
|
+
return len(self._orders)
|
|
216
|
+
|
|
217
|
+
def clear(self) -> None:
|
|
218
|
+
"""Clear all halt orders."""
|
|
219
|
+
self._orders.clear()
|
|
220
|
+
self._by_agent.clear()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
+
# Halt window computation (Proposition 2)
|
|
225
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def compute_halt_window(order: HaltOrder) -> Optional[float]:
|
|
229
|
+
"""
|
|
230
|
+
Compute the halt window Δt_window for an enforced halt order.
|
|
231
|
+
|
|
232
|
+
Δt_window = enforced_at - created_at (Proposition 2).
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The window in seconds, or None if not enforced.
|
|
236
|
+
"""
|
|
237
|
+
return order.halt_window_seconds()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def validate_halt_window(order: HaltOrder) -> bool:
|
|
241
|
+
"""
|
|
242
|
+
Validate that Δt_window > 0 (I17, Proposition 2).
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if enforced and window > 0, False otherwise.
|
|
246
|
+
"""
|
|
247
|
+
window = compute_halt_window(order)
|
|
248
|
+
return window is not None and window > 0
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Halt: Quorum (Layer 4)
|
|
3
|
+
|
|
4
|
+
This module implements Operation 6 (`approve_halt`) from
|
|
5
|
+
docs/specification/02_operations.md.
|
|
6
|
+
|
|
7
|
+
The `approve_halt` operation registers an approval from an authorized node
|
|
8
|
+
and, when the quorum threshold is reached, enforces the halt order.
|
|
9
|
+
|
|
10
|
+
Key properties:
|
|
11
|
+
- Each approver can approve only once (I20).
|
|
12
|
+
- Enforced when approvals count / total ≥ quorum_threshold (I16).
|
|
13
|
+
- Δt_window > 0 (Proposition 2, I17).
|
|
14
|
+
|
|
15
|
+
Invariants enforced:
|
|
16
|
+
- I16: enforced_at set iff quorum reached
|
|
17
|
+
- I17: Δt_window > 0
|
|
18
|
+
- I18: signature verifies
|
|
19
|
+
- I19: approval_id unique
|
|
20
|
+
- I20: approver cannot approve twice
|
|
21
|
+
- O6: approve_halt cannot be done twice by same approver
|
|
22
|
+
|
|
23
|
+
Errors:
|
|
24
|
+
- E18: HaltNotFound
|
|
25
|
+
- E19: InvalidSignature
|
|
26
|
+
- Additional: AlreadyApproved, QuorumAlreadyReached
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from typing import Optional
|
|
34
|
+
|
|
35
|
+
from ..models import (
|
|
36
|
+
Agent,
|
|
37
|
+
HaltApproval,
|
|
38
|
+
HaltOrder,
|
|
39
|
+
_now_iso,
|
|
40
|
+
_random_hex,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
45
|
+
# Errors
|
|
46
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HaltNotFound(Exception):
|
|
50
|
+
"""E18: halt_id does not exist."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class InvalidSignature(Exception):
|
|
55
|
+
"""E19: approver_signature does not verify."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AlreadyApproved(Exception):
|
|
60
|
+
"""The approver has already approved this halt (I20)."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class QuorumAlreadyReached(Exception):
|
|
65
|
+
"""Quorum was already reached; halt is enforced."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# Signature verification (stub)
|
|
71
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _verify_signature(
|
|
75
|
+
public_key: bytes,
|
|
76
|
+
signature: bytes,
|
|
77
|
+
message: bytes,
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""Verify an Ed25519 signature (stub)."""
|
|
80
|
+
if len(signature) != 64:
|
|
81
|
+
return False
|
|
82
|
+
if len(public_key) != 32:
|
|
83
|
+
return False
|
|
84
|
+
return signature[0] != 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _approval_message(halt_id: str, approver_id: str, timestamp: str) -> bytes:
|
|
88
|
+
"""Construct the message that the approver signs."""
|
|
89
|
+
return hashlib.sha256(
|
|
90
|
+
f"{halt_id}|{approver_id}|{timestamp}".encode("utf-8")
|
|
91
|
+
).digest()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
95
|
+
# Operation 6 — approve_halt
|
|
96
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def approve_halt(
|
|
100
|
+
halt_order: HaltOrder,
|
|
101
|
+
approver: Agent,
|
|
102
|
+
approver_signature: bytes,
|
|
103
|
+
total_authorized_nodes: int,
|
|
104
|
+
is_approver_authorized: bool = True,
|
|
105
|
+
) -> HaltApproval:
|
|
106
|
+
"""
|
|
107
|
+
Approve a halt order (Operation 6).
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
halt_order: The HaltOrder to approve.
|
|
111
|
+
approver: The agent approving. Must be authorized.
|
|
112
|
+
approver_signature: Ed25519 signature from the approver.
|
|
113
|
+
total_authorized_nodes: Total number of authorized nodes.
|
|
114
|
+
is_approver_authorized: In production, checked via AetherGovern.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A new HaltApproval. If quorum is reached, halt_order.enforced_at
|
|
118
|
+
is updated and the halt is enforced.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
HaltNotFound: If halt_order is None.
|
|
122
|
+
InvalidSignature: If approver_signature does not verify.
|
|
123
|
+
AlreadyApproved: If approver has already approved.
|
|
124
|
+
QuorumAlreadyReached: If quorum was already reached.
|
|
125
|
+
|
|
126
|
+
Invariants:
|
|
127
|
+
I16: enforced_at set iff quorum reached.
|
|
128
|
+
I17: Δt_window > 0 (Proposition 2).
|
|
129
|
+
I18: signature verifies.
|
|
130
|
+
I19: approval_id unique.
|
|
131
|
+
I20: approver cannot approve twice.
|
|
132
|
+
O6: approve_halt cannot be done twice by same approver.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> approval = approve_halt(order, node1, sig, total=3)
|
|
136
|
+
>>> order.is_enforced()
|
|
137
|
+
False # need 2/3 approvals
|
|
138
|
+
>>> approval2 = approve_halt(order, node2, sig2, total=3)
|
|
139
|
+
>>> order.is_enforced()
|
|
140
|
+
True # 2/3 = quorum
|
|
141
|
+
"""
|
|
142
|
+
# ─── E18: HaltOrder must exist ───
|
|
143
|
+
if halt_order is None:
|
|
144
|
+
raise HaltNotFound("halt_order must not be None")
|
|
145
|
+
|
|
146
|
+
# ─── Quorum already reached? ───
|
|
147
|
+
if halt_order.is_enforced():
|
|
148
|
+
raise QuorumAlreadyReached(
|
|
149
|
+
f"Quorum already reached for halt {halt_order.halt_id}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# ─── E16: Approver must be authorized ───
|
|
153
|
+
if not is_approver_authorized:
|
|
154
|
+
raise InvalidSignature(
|
|
155
|
+
f"Approver {approver.agent_id} is not authorized"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# ─── I20: Approver cannot approve twice ───
|
|
159
|
+
for existing in halt_order.approvals:
|
|
160
|
+
if existing.approver == approver.agent_id:
|
|
161
|
+
raise AlreadyApproved(
|
|
162
|
+
f"Approver {approver.agent_id} already approved halt "
|
|
163
|
+
f"{halt_order.halt_id}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# ─── I19: Generate unique approval_id ───
|
|
167
|
+
approval_id = _random_hex()
|
|
168
|
+
timestamp = _now_iso()
|
|
169
|
+
|
|
170
|
+
# ─── I18: Verify signature ───
|
|
171
|
+
message = _approval_message(halt_order.halt_id, approver.agent_id, timestamp)
|
|
172
|
+
if not _verify_signature(approver.public_key, approver_signature, message):
|
|
173
|
+
raise InvalidSignature("approver_signature does not verify")
|
|
174
|
+
|
|
175
|
+
# ─── Q1: Create HaltApproval ───
|
|
176
|
+
approval = HaltApproval(
|
|
177
|
+
approval_id=approval_id,
|
|
178
|
+
halt_id=halt_order.halt_id,
|
|
179
|
+
approver=approver.agent_id,
|
|
180
|
+
signature=approver_signature,
|
|
181
|
+
timestamp=timestamp,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# ─── Register approval ───
|
|
185
|
+
halt_order.approvals.append(approval)
|
|
186
|
+
|
|
187
|
+
# ─── I16: Check quorum ───
|
|
188
|
+
approvals_count = len(halt_order.approvals)
|
|
189
|
+
quorum_reached = (approvals_count / total_authorized_nodes) >= halt_order.quorum_threshold
|
|
190
|
+
|
|
191
|
+
if quorum_reached:
|
|
192
|
+
# ─── I17: Δt_window > 0 (Proposition 2) ───
|
|
193
|
+
created_dt = datetime.fromisoformat(
|
|
194
|
+
halt_order.created_at.replace("Z", "+00:00")
|
|
195
|
+
)
|
|
196
|
+
enforced_dt = datetime.now(timezone.utc)
|
|
197
|
+
if enforced_dt <= created_dt:
|
|
198
|
+
# Force at least 1 microsecond window
|
|
199
|
+
from datetime import timedelta
|
|
200
|
+
enforced_dt = created_dt + timedelta(microseconds=1)
|
|
201
|
+
halt_order.enforced_at = enforced_dt.isoformat()
|
|
202
|
+
|
|
203
|
+
return approval
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
# Batch approval
|
|
208
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def approve_batch(
|
|
212
|
+
halt_order: HaltOrder,
|
|
213
|
+
approvals: list[tuple[Agent, bytes, bool]],
|
|
214
|
+
total_authorized_nodes: int,
|
|
215
|
+
) -> list[HaltApproval]:
|
|
216
|
+
"""
|
|
217
|
+
Approve a halt order with multiple nodes in sequence.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
halt_order: The HaltOrder to approve.
|
|
221
|
+
approvals: List of (approver, signature, is_authorized) tuples.
|
|
222
|
+
total_authorized_nodes: Total number of authorized nodes.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of successfully registered HaltApprovals.
|
|
226
|
+
"""
|
|
227
|
+
result: list[HaltApproval] = []
|
|
228
|
+
for approver, sig, is_auth in approvals:
|
|
229
|
+
try:
|
|
230
|
+
approval = approve_halt(
|
|
231
|
+
halt_order, approver, sig, total_authorized_nodes, is_auth
|
|
232
|
+
)
|
|
233
|
+
result.append(approval)
|
|
234
|
+
except (AlreadyApproved, QuorumAlreadyReached):
|
|
235
|
+
continue
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
+
# Quorum tracker
|
|
241
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class QuorumTracker:
|
|
245
|
+
"""
|
|
246
|
+
Tracks quorum status for halt orders.
|
|
247
|
+
|
|
248
|
+
Provides query methods for approval counts, quorum percentage,
|
|
249
|
+
and enforcement status.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, total_authorized_nodes: int) -> None:
|
|
253
|
+
if total_authorized_nodes <= 0:
|
|
254
|
+
raise ValueError("total_authorized_nodes must be > 0")
|
|
255
|
+
self.total = total_authorized_nodes
|
|
256
|
+
|
|
257
|
+
def status(self, halt_order: HaltOrder) -> dict:
|
|
258
|
+
"""Return quorum status for a halt order."""
|
|
259
|
+
n_approvals = len(halt_order.approvals)
|
|
260
|
+
percentage = n_approvals / self.total if self.total > 0 else 0.0
|
|
261
|
+
return {
|
|
262
|
+
"halt_id": halt_order.halt_id,
|
|
263
|
+
"n_approvals": n_approvals,
|
|
264
|
+
"total_authorized": self.total,
|
|
265
|
+
"percentage": percentage,
|
|
266
|
+
"threshold": halt_order.quorum_threshold,
|
|
267
|
+
"quorum_reached": percentage >= halt_order.quorum_threshold,
|
|
268
|
+
"enforced": halt_order.is_enforced(),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def is_enforced(self, halt_order: HaltOrder) -> bool:
|
|
272
|
+
"""Check if a halt order is enforced."""
|
|
273
|
+
return halt_order.is_enforced()
|
|
274
|
+
|
|
275
|
+
def remaining_approvals(self, halt_order: HaltOrder) -> int:
|
|
276
|
+
"""Count how many more approvals are needed."""
|
|
277
|
+
if halt_order.is_enforced():
|
|
278
|
+
return 0
|
|
279
|
+
needed = int(self.total * halt_order.quorum_threshold) + 1
|
|
280
|
+
return max(0, needed - len(halt_order.approvals))
|