agent_hypervisor 3.1.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.
- agent_hypervisor-3.1.0.dist-info/METADATA +824 -0
- agent_hypervisor-3.1.0.dist-info/RECORD +60 -0
- agent_hypervisor-3.1.0.dist-info/WHEEL +4 -0
- agent_hypervisor-3.1.0.dist-info/entry_points.txt +2 -0
- agent_hypervisor-3.1.0.dist-info/licenses/LICENSE +21 -0
- hypervisor/__init__.py +160 -0
- hypervisor/api/__init__.py +7 -0
- hypervisor/api/models.py +285 -0
- hypervisor/api/server.py +742 -0
- hypervisor/audit/__init__.py +4 -0
- hypervisor/audit/commitment.py +76 -0
- hypervisor/audit/delta.py +135 -0
- hypervisor/audit/gc.py +99 -0
- hypervisor/cli/__init__.py +3 -0
- hypervisor/cli/formatters.py +99 -0
- hypervisor/cli/session_commands.py +200 -0
- hypervisor/constants.py +106 -0
- hypervisor/core.py +352 -0
- hypervisor/integrations/__init__.py +10 -0
- hypervisor/integrations/iatp_adapter.py +142 -0
- hypervisor/integrations/nexus_adapter.py +108 -0
- hypervisor/integrations/verification_adapter.py +122 -0
- hypervisor/liability/__init__.py +142 -0
- hypervisor/liability/attribution.py +86 -0
- hypervisor/liability/ledger.py +121 -0
- hypervisor/liability/quarantine.py +119 -0
- hypervisor/liability/slashing.py +80 -0
- hypervisor/liability/vouching.py +134 -0
- hypervisor/models.py +277 -0
- hypervisor/observability/__init__.py +27 -0
- hypervisor/observability/causal_trace.py +70 -0
- hypervisor/observability/event_bus.py +222 -0
- hypervisor/observability/prometheus_collector.py +248 -0
- hypervisor/observability/saga_span_exporter.py +341 -0
- hypervisor/providers.py +121 -0
- hypervisor/py.typed +0 -0
- hypervisor/reversibility/__init__.py +3 -0
- hypervisor/reversibility/registry.py +108 -0
- hypervisor/rings/__init__.py +21 -0
- hypervisor/rings/breach_detector.py +200 -0
- hypervisor/rings/classifier.py +78 -0
- hypervisor/rings/elevation.py +219 -0
- hypervisor/rings/enforcer.py +97 -0
- hypervisor/saga/__init__.py +22 -0
- hypervisor/saga/checkpoint.py +110 -0
- hypervisor/saga/dsl.py +190 -0
- hypervisor/saga/fan_out.py +126 -0
- hypervisor/saga/orchestrator.py +229 -0
- hypervisor/saga/schema.py +244 -0
- hypervisor/saga/state_machine.py +157 -0
- hypervisor/security/__init__.py +13 -0
- hypervisor/security/kill_switch.py +200 -0
- hypervisor/security/rate_limiter.py +190 -0
- hypervisor/session/__init__.py +194 -0
- hypervisor/session/intent_locks.py +118 -0
- hypervisor/session/isolation.py +37 -0
- hypervisor/session/sso.py +169 -0
- hypervisor/session/vector_clock.py +118 -0
- hypervisor/verification/__init__.py +3 -0
- hypervisor/verification/history.py +173 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
OpenTelemetry-compatible span exporter for Saga orchestration steps.
|
|
5
|
+
|
|
6
|
+
Subscribes to the HypervisorEventBus for saga lifecycle events and
|
|
7
|
+
produces span records that can be forwarded to any tracing backend
|
|
8
|
+
via the ``SpanSink`` protocol.
|
|
9
|
+
|
|
10
|
+
No external dependencies — the hypervisor stays standalone. A concrete
|
|
11
|
+
``OTelSpanSink`` adapter lives in ``agent-sre`` and bridges into the
|
|
12
|
+
existing ``TraceExporter``.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
from hypervisor.observability import SagaSpanExporter, HypervisorEventBus
|
|
17
|
+
|
|
18
|
+
bus = HypervisorEventBus()
|
|
19
|
+
exporter = SagaSpanExporter(bus)
|
|
20
|
+
|
|
21
|
+
# Optionally attach a sink (e.g. OTelSpanSink from agent-sre)
|
|
22
|
+
exporter.attach_sink(my_sink)
|
|
23
|
+
|
|
24
|
+
# ... saga events flow through the bus ...
|
|
25
|
+
|
|
26
|
+
# Or inspect buffered spans directly
|
|
27
|
+
spans = exporter.completed_spans
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any, Protocol, runtime_checkable
|
|
34
|
+
|
|
35
|
+
from hypervisor.observability.event_bus import EventType, HypervisorEvent, HypervisorEventBus
|
|
36
|
+
|
|
37
|
+
# Saga event types we subscribe to
|
|
38
|
+
_SAGA_LIFECYCLE_EVENTS = frozenset({
|
|
39
|
+
EventType.SAGA_CREATED,
|
|
40
|
+
EventType.SAGA_STEP_STARTED,
|
|
41
|
+
EventType.SAGA_STEP_COMMITTED,
|
|
42
|
+
EventType.SAGA_STEP_FAILED,
|
|
43
|
+
EventType.SAGA_COMPENSATING,
|
|
44
|
+
EventType.SAGA_COMPLETED,
|
|
45
|
+
EventType.SAGA_ESCALATED,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# SpanSink protocol — no hard OTel dependency in hypervisor
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@runtime_checkable
|
|
55
|
+
class SpanSink(Protocol):
|
|
56
|
+
"""Protocol for receiving completed span records.
|
|
57
|
+
|
|
58
|
+
Any object implementing this interface can receive spans from the
|
|
59
|
+
``SagaSpanExporter``. The ``agent-sre`` package ships an
|
|
60
|
+
``OTelSpanSink`` adapter that bridges into the native OTel SDK.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def record_span(
|
|
64
|
+
self,
|
|
65
|
+
name: str,
|
|
66
|
+
start_time: float,
|
|
67
|
+
end_time: float,
|
|
68
|
+
attributes: dict[str, Any],
|
|
69
|
+
status: str,
|
|
70
|
+
) -> None: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Span record dataclass
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class SagaSpanRecord:
|
|
80
|
+
"""An immutable record of a completed saga span."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
saga_id: str
|
|
84
|
+
step_id: str
|
|
85
|
+
step_action: str
|
|
86
|
+
start_time: float
|
|
87
|
+
end_time: float
|
|
88
|
+
status: str # "ok", "error", "compensating"
|
|
89
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def duration_seconds(self) -> float:
|
|
93
|
+
return self.end_time - self.start_time
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# SagaSpanExporter
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SagaSpanExporter:
|
|
102
|
+
"""Exports OpenTelemetry-compatible spans for saga orchestration steps.
|
|
103
|
+
|
|
104
|
+
Subscribes to the ``HypervisorEventBus`` for saga lifecycle events.
|
|
105
|
+
On each step completion (committed, failed, escalated), records a span
|
|
106
|
+
with timing, saga context, and status information.
|
|
107
|
+
|
|
108
|
+
Completed spans are:
|
|
109
|
+
1. Buffered internally in ``completed_spans``
|
|
110
|
+
2. Forwarded to an attached ``SpanSink`` (if any)
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
_bus: The event bus this exporter is subscribed to.
|
|
114
|
+
_sink: Optional SpanSink for forwarding completed spans.
|
|
115
|
+
_step_starts: Tracks step start times: ``(saga_id, step_id) -> timestamp``.
|
|
116
|
+
_saga_starts: Tracks saga creation times: ``saga_id -> timestamp``.
|
|
117
|
+
_completed_spans: Buffer of completed span records.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, bus: HypervisorEventBus, sink: SpanSink | None = None) -> None:
|
|
121
|
+
self._bus = bus
|
|
122
|
+
self._sink = sink
|
|
123
|
+
|
|
124
|
+
# In-flight tracking: (saga_id, step_id) -> start timestamp
|
|
125
|
+
self._step_starts: dict[tuple[str, str], float] = {}
|
|
126
|
+
|
|
127
|
+
# Saga-level tracking: saga_id -> creation timestamp
|
|
128
|
+
self._saga_starts: dict[str, float] = {}
|
|
129
|
+
|
|
130
|
+
# Completed span buffer
|
|
131
|
+
self._completed_spans: list[SagaSpanRecord] = []
|
|
132
|
+
|
|
133
|
+
# Total events processed
|
|
134
|
+
self._events_processed: int = 0
|
|
135
|
+
|
|
136
|
+
# Subscribe to all saga lifecycle events
|
|
137
|
+
for event_type in _SAGA_LIFECYCLE_EVENTS:
|
|
138
|
+
bus.subscribe(event_type=event_type, handler=self._handle_event)
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Public API
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def attach_sink(self, sink: SpanSink) -> None:
|
|
145
|
+
"""Attach a SpanSink to receive completed spans in real time."""
|
|
146
|
+
self._sink = sink
|
|
147
|
+
|
|
148
|
+
def detach_sink(self) -> None:
|
|
149
|
+
"""Detach the current SpanSink."""
|
|
150
|
+
self._sink = None
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def completed_spans(self) -> list[SagaSpanRecord]:
|
|
154
|
+
"""Return a copy of all completed span records."""
|
|
155
|
+
return list(self._completed_spans)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def events_processed(self) -> int:
|
|
159
|
+
"""Total saga events processed."""
|
|
160
|
+
return self._events_processed
|
|
161
|
+
|
|
162
|
+
def reset(self) -> None:
|
|
163
|
+
"""Reset all internal state (for testing)."""
|
|
164
|
+
self._step_starts.clear()
|
|
165
|
+
self._saga_starts.clear()
|
|
166
|
+
self._completed_spans.clear()
|
|
167
|
+
self._events_processed = 0
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Event handling
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def _handle_event(self, event: HypervisorEvent) -> None:
|
|
174
|
+
"""Process a saga lifecycle event from the bus."""
|
|
175
|
+
self._events_processed += 1
|
|
176
|
+
payload = event.payload
|
|
177
|
+
saga_id = payload.get("saga_id", event.causal_trace_id or "unknown")
|
|
178
|
+
step_id = payload.get("step_id", "")
|
|
179
|
+
step_action = payload.get("action", payload.get("step_action", ""))
|
|
180
|
+
|
|
181
|
+
if event.event_type == EventType.SAGA_CREATED:
|
|
182
|
+
self._saga_starts[saga_id] = event.timestamp.timestamp()
|
|
183
|
+
|
|
184
|
+
elif event.event_type == EventType.SAGA_STEP_STARTED:
|
|
185
|
+
key = (saga_id, step_id or step_action)
|
|
186
|
+
self._step_starts[key] = event.timestamp.timestamp()
|
|
187
|
+
|
|
188
|
+
elif event.event_type == EventType.SAGA_STEP_COMMITTED:
|
|
189
|
+
self._complete_step(
|
|
190
|
+
saga_id=saga_id,
|
|
191
|
+
step_id=step_id or step_action,
|
|
192
|
+
step_action=step_action,
|
|
193
|
+
end_time=event.timestamp.timestamp(),
|
|
194
|
+
status="ok",
|
|
195
|
+
extra_attrs=payload,
|
|
196
|
+
agent_did=event.agent_did,
|
|
197
|
+
session_id=event.session_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
elif event.event_type == EventType.SAGA_STEP_FAILED:
|
|
201
|
+
self._complete_step(
|
|
202
|
+
saga_id=saga_id,
|
|
203
|
+
step_id=step_id or step_action,
|
|
204
|
+
step_action=step_action,
|
|
205
|
+
end_time=event.timestamp.timestamp(),
|
|
206
|
+
status="error",
|
|
207
|
+
extra_attrs=payload,
|
|
208
|
+
agent_did=event.agent_did,
|
|
209
|
+
session_id=event.session_id,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
elif event.event_type == EventType.SAGA_COMPENSATING:
|
|
213
|
+
# Compensation is a span in its own right
|
|
214
|
+
self._record_span(
|
|
215
|
+
name=f"saga.compensate.{step_action or step_id}",
|
|
216
|
+
saga_id=saga_id,
|
|
217
|
+
step_id=step_id or "compensation",
|
|
218
|
+
step_action=step_action or "compensate",
|
|
219
|
+
start_time=event.timestamp.timestamp(),
|
|
220
|
+
end_time=event.timestamp.timestamp(), # instantaneous marker
|
|
221
|
+
status="compensating",
|
|
222
|
+
extra_attrs=payload,
|
|
223
|
+
agent_did=event.agent_did,
|
|
224
|
+
session_id=event.session_id,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
elif event.event_type == EventType.SAGA_COMPLETED:
|
|
228
|
+
# Record a saga-level span from creation to completion
|
|
229
|
+
start = self._saga_starts.pop(saga_id, None)
|
|
230
|
+
if start is not None:
|
|
231
|
+
self._record_span(
|
|
232
|
+
name=f"saga.completed.{saga_id}",
|
|
233
|
+
saga_id=saga_id,
|
|
234
|
+
step_id="",
|
|
235
|
+
step_action="complete",
|
|
236
|
+
start_time=start,
|
|
237
|
+
end_time=event.timestamp.timestamp(),
|
|
238
|
+
status="ok",
|
|
239
|
+
extra_attrs=payload,
|
|
240
|
+
agent_did=event.agent_did,
|
|
241
|
+
session_id=event.session_id,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
elif event.event_type == EventType.SAGA_ESCALATED:
|
|
245
|
+
start = self._saga_starts.pop(saga_id, None)
|
|
246
|
+
if start is not None:
|
|
247
|
+
self._record_span(
|
|
248
|
+
name=f"saga.escalated.{saga_id}",
|
|
249
|
+
saga_id=saga_id,
|
|
250
|
+
step_id="",
|
|
251
|
+
step_action="escalate",
|
|
252
|
+
start_time=start,
|
|
253
|
+
end_time=event.timestamp.timestamp(),
|
|
254
|
+
status="error",
|
|
255
|
+
extra_attrs=payload,
|
|
256
|
+
agent_did=event.agent_did,
|
|
257
|
+
session_id=event.session_id,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def _complete_step(
|
|
261
|
+
self,
|
|
262
|
+
saga_id: str,
|
|
263
|
+
step_id: str,
|
|
264
|
+
step_action: str,
|
|
265
|
+
end_time: float,
|
|
266
|
+
status: str,
|
|
267
|
+
extra_attrs: dict[str, Any],
|
|
268
|
+
agent_did: str | None = None,
|
|
269
|
+
session_id: str | None = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Complete an in-flight step span."""
|
|
272
|
+
key = (saga_id, step_id)
|
|
273
|
+
start_time = self._step_starts.pop(key, None)
|
|
274
|
+
if start_time is None:
|
|
275
|
+
# Step started before we subscribed, use end_time as fallback
|
|
276
|
+
start_time = end_time
|
|
277
|
+
|
|
278
|
+
self._record_span(
|
|
279
|
+
name=f"saga.step.{step_action or step_id}",
|
|
280
|
+
saga_id=saga_id,
|
|
281
|
+
step_id=step_id,
|
|
282
|
+
step_action=step_action,
|
|
283
|
+
start_time=start_time,
|
|
284
|
+
end_time=end_time,
|
|
285
|
+
status=status,
|
|
286
|
+
extra_attrs=extra_attrs,
|
|
287
|
+
agent_did=agent_did,
|
|
288
|
+
session_id=session_id,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _record_span(
|
|
292
|
+
self,
|
|
293
|
+
name: str,
|
|
294
|
+
saga_id: str,
|
|
295
|
+
step_id: str,
|
|
296
|
+
step_action: str,
|
|
297
|
+
start_time: float,
|
|
298
|
+
end_time: float,
|
|
299
|
+
status: str,
|
|
300
|
+
extra_attrs: dict[str, Any] | None = None,
|
|
301
|
+
agent_did: str | None = None,
|
|
302
|
+
session_id: str | None = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Create a span record, buffer it, and forward to sink if attached."""
|
|
305
|
+
attrs: dict[str, Any] = {
|
|
306
|
+
"agent.saga.id": saga_id,
|
|
307
|
+
"agent.saga.step_id": step_id,
|
|
308
|
+
"agent.saga.step_action": step_action,
|
|
309
|
+
"agent.saga.state": status,
|
|
310
|
+
}
|
|
311
|
+
if agent_did:
|
|
312
|
+
attrs["agent.did"] = agent_did
|
|
313
|
+
if session_id:
|
|
314
|
+
attrs["session.id"] = session_id
|
|
315
|
+
if extra_attrs:
|
|
316
|
+
# Include select payload fields as span attributes
|
|
317
|
+
for key in ("error", "reason", "result"):
|
|
318
|
+
if key in extra_attrs:
|
|
319
|
+
attrs[f"agent.saga.{key}"] = str(extra_attrs[key])
|
|
320
|
+
|
|
321
|
+
record = SagaSpanRecord(
|
|
322
|
+
name=name,
|
|
323
|
+
saga_id=saga_id,
|
|
324
|
+
step_id=step_id,
|
|
325
|
+
step_action=step_action,
|
|
326
|
+
start_time=start_time,
|
|
327
|
+
end_time=end_time,
|
|
328
|
+
status=status,
|
|
329
|
+
attributes=attrs,
|
|
330
|
+
)
|
|
331
|
+
self._completed_spans.append(record)
|
|
332
|
+
|
|
333
|
+
# Forward to sink if attached
|
|
334
|
+
if self._sink is not None:
|
|
335
|
+
self._sink.record_span(
|
|
336
|
+
name=record.name,
|
|
337
|
+
start_time=record.start_time,
|
|
338
|
+
end_time=record.end_time,
|
|
339
|
+
attributes=record.attributes,
|
|
340
|
+
status=record.status,
|
|
341
|
+
)
|
hypervisor/providers.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Provider Discovery System for Agent Hypervisor
|
|
5
|
+
|
|
6
|
+
Enables plug-and-play upgrades from Public Preview to Advanced implementations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from importlib.metadata import entry_points
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
PROVIDER_GROUPS = {
|
|
18
|
+
"ring_engine": "hypervisor.providers.ring_engine",
|
|
19
|
+
"liability": "hypervisor.providers.liability",
|
|
20
|
+
"saga_engine": "hypervisor.providers.saga_engine",
|
|
21
|
+
"breach_detector": "hypervisor.providers.breach_detector",
|
|
22
|
+
"session_manager": "hypervisor.providers.session_manager",
|
|
23
|
+
"audit_engine": "hypervisor.providers.audit_engine",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_provider_cache: dict[str, Any] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _discover_provider(group: str) -> type | None:
|
|
30
|
+
"""Discover an advanced provider via entry_points."""
|
|
31
|
+
if group in _provider_cache:
|
|
32
|
+
return _provider_cache[group]
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
eps = entry_points(group=group)
|
|
36
|
+
if eps:
|
|
37
|
+
ep = next(iter(eps))
|
|
38
|
+
provider_cls = ep.load()
|
|
39
|
+
if not isinstance(provider_cls, type):
|
|
40
|
+
logger.warning(
|
|
41
|
+
"Provider %s is not a class, skipping", ep.name
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
_provider_cache[group] = provider_cls
|
|
45
|
+
logger.info("Advanced provider loaded: %s from %s", ep.name, ep.value)
|
|
46
|
+
return provider_cls
|
|
47
|
+
except Exception:
|
|
48
|
+
logger.debug("Provider discovery failed for %s", group, exc_info=True)
|
|
49
|
+
|
|
50
|
+
_provider_cache[group] = None
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_ring_engine(**kwargs: Any):
|
|
55
|
+
"""Get the best available execution ring engine.
|
|
56
|
+
|
|
57
|
+
Advanced: 4-ring privilege escalation with breach detection.
|
|
58
|
+
Community: Basic ring assignment with classifier.
|
|
59
|
+
"""
|
|
60
|
+
provider = _discover_provider(PROVIDER_GROUPS["ring_engine"])
|
|
61
|
+
if provider is not None:
|
|
62
|
+
return provider(**kwargs)
|
|
63
|
+
|
|
64
|
+
from hypervisor.rings.enforcer import RingEnforcer
|
|
65
|
+
return RingEnforcer(**kwargs)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_liability_engine(**kwargs: Any):
|
|
69
|
+
"""Get the best available liability engine.
|
|
70
|
+
|
|
71
|
+
Advanced: Shapley-value fault attribution with vouch cascades.
|
|
72
|
+
Community: Basic vouching with linear slashing.
|
|
73
|
+
"""
|
|
74
|
+
provider = _discover_provider(PROVIDER_GROUPS["liability"])
|
|
75
|
+
if provider is not None:
|
|
76
|
+
return provider(**kwargs)
|
|
77
|
+
|
|
78
|
+
from hypervisor.liability.engine import LiabilityEngine
|
|
79
|
+
return LiabilityEngine(**kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_saga_engine(**kwargs: Any):
|
|
83
|
+
"""Get the best available saga orchestration engine.
|
|
84
|
+
|
|
85
|
+
Advanced: Multi-pattern saga with parallel fan-out and escalation.
|
|
86
|
+
Community: Sequential saga with basic compensation.
|
|
87
|
+
"""
|
|
88
|
+
provider = _discover_provider(PROVIDER_GROUPS["saga_engine"])
|
|
89
|
+
if provider is not None:
|
|
90
|
+
return provider(**kwargs)
|
|
91
|
+
|
|
92
|
+
from hypervisor.saga.engine import SagaOrchestrator
|
|
93
|
+
return SagaOrchestrator(**kwargs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_breach_detector(**kwargs: Any):
|
|
97
|
+
"""Get the best available breach detector.
|
|
98
|
+
|
|
99
|
+
Advanced: Multi-signal breach detection with severity scoring.
|
|
100
|
+
Community: Basic threshold-based detection with safe defaults.
|
|
101
|
+
"""
|
|
102
|
+
provider = _discover_provider(PROVIDER_GROUPS["breach_detector"])
|
|
103
|
+
if provider is not None:
|
|
104
|
+
return provider(**kwargs)
|
|
105
|
+
|
|
106
|
+
from hypervisor.rings.breach_detector import RingBreachDetector
|
|
107
|
+
return RingBreachDetector(**kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def list_providers() -> dict[str, str]:
|
|
111
|
+
"""List all provider slots and their current implementations."""
|
|
112
|
+
result = {}
|
|
113
|
+
for name, group in PROVIDER_GROUPS.items():
|
|
114
|
+
provider = _discover_provider(group)
|
|
115
|
+
result[name] = "advanced" if provider is not None else "community"
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def clear_cache() -> None:
|
|
120
|
+
"""Clear the provider cache."""
|
|
121
|
+
_provider_cache.clear()
|
hypervisor/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Reversibility Registry
|
|
5
|
+
|
|
6
|
+
Maps every declared action to its Execute_API and Undo_API,
|
|
7
|
+
populated during the IATP handshake.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from hypervisor.models import ActionDescriptor, ReversibilityLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ReversibilityEntry:
|
|
19
|
+
"""An entry in the reversibility registry."""
|
|
20
|
+
|
|
21
|
+
action_id: str
|
|
22
|
+
execute_api: str
|
|
23
|
+
undo_api: str | None
|
|
24
|
+
reversibility: ReversibilityLevel
|
|
25
|
+
undo_window_seconds: int
|
|
26
|
+
compensation_method: str | None
|
|
27
|
+
risk_weight: float
|
|
28
|
+
undo_api_healthy: bool = True
|
|
29
|
+
last_health_check: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReversibilityRegistry:
|
|
33
|
+
"""
|
|
34
|
+
Session-scoped registry of action reversibility mappings.
|
|
35
|
+
|
|
36
|
+
Auto-populated from IATP Capability Manifests during handshake.
|
|
37
|
+
Provides lookup for the Saga orchestrator during rollback.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, session_id: str) -> None:
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
self._entries: dict[str, ReversibilityEntry] = {}
|
|
43
|
+
|
|
44
|
+
def register(self, action: ActionDescriptor) -> ReversibilityEntry:
|
|
45
|
+
"""Register an action from a capability manifest."""
|
|
46
|
+
entry = ReversibilityEntry(
|
|
47
|
+
action_id=action.action_id,
|
|
48
|
+
execute_api=action.execute_api,
|
|
49
|
+
undo_api=action.undo_api,
|
|
50
|
+
reversibility=action.reversibility,
|
|
51
|
+
undo_window_seconds=action.undo_window_seconds,
|
|
52
|
+
compensation_method=action.compensation_method,
|
|
53
|
+
risk_weight=action.risk_weight,
|
|
54
|
+
)
|
|
55
|
+
self._entries[action.action_id] = entry
|
|
56
|
+
return entry
|
|
57
|
+
|
|
58
|
+
def register_from_manifest(self, actions: list[ActionDescriptor]) -> int:
|
|
59
|
+
"""Register all actions from a manifest. Returns count registered."""
|
|
60
|
+
for action in actions:
|
|
61
|
+
self.register(action)
|
|
62
|
+
return len(actions)
|
|
63
|
+
|
|
64
|
+
def get(self, action_id: str) -> ReversibilityEntry | None:
|
|
65
|
+
"""Look up an action's reversibility entry."""
|
|
66
|
+
return self._entries.get(action_id)
|
|
67
|
+
|
|
68
|
+
def get_undo_api(self, action_id: str) -> str | None:
|
|
69
|
+
"""Get the Undo_API for an action, if any."""
|
|
70
|
+
entry = self._entries.get(action_id)
|
|
71
|
+
return entry.undo_api if entry else None
|
|
72
|
+
|
|
73
|
+
def is_reversible(self, action_id: str) -> bool:
|
|
74
|
+
"""Check if an action has any reversibility."""
|
|
75
|
+
entry = self._entries.get(action_id)
|
|
76
|
+
if not entry:
|
|
77
|
+
return False
|
|
78
|
+
return entry.reversibility != ReversibilityLevel.NONE
|
|
79
|
+
|
|
80
|
+
def get_risk_weight(self, action_id: str) -> float:
|
|
81
|
+
"""Get the risk weight ω for an action."""
|
|
82
|
+
entry = self._entries.get(action_id)
|
|
83
|
+
return entry.risk_weight if entry else ReversibilityLevel.NONE.default_risk_weight
|
|
84
|
+
|
|
85
|
+
def has_non_reversible_actions(self) -> bool:
|
|
86
|
+
"""Check if any registered action is non-reversible."""
|
|
87
|
+
return any(
|
|
88
|
+
e.reversibility == ReversibilityLevel.NONE
|
|
89
|
+
for e in self._entries.values()
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def mark_undo_unhealthy(self, action_id: str) -> None:
|
|
93
|
+
"""Mark an Undo_API as unhealthy (failed health check)."""
|
|
94
|
+
entry = self._entries.get(action_id)
|
|
95
|
+
if entry:
|
|
96
|
+
entry.undo_api_healthy = False
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def entries(self) -> list[ReversibilityEntry]:
|
|
100
|
+
return list(self._entries.values())
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def non_reversible_actions(self) -> list[str]:
|
|
104
|
+
return [
|
|
105
|
+
e.action_id
|
|
106
|
+
for e in self._entries.values()
|
|
107
|
+
if e.reversibility == ReversibilityLevel.NONE
|
|
108
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Execution rings subpackage — enforcement, classification, elevation, breach detection."""
|
|
4
|
+
|
|
5
|
+
from hypervisor.rings.breach_detector import BreachEvent, BreachSeverity, RingBreachDetector
|
|
6
|
+
from hypervisor.rings.elevation import (
|
|
7
|
+
ElevationDenialReason,
|
|
8
|
+
RingElevation,
|
|
9
|
+
RingElevationError,
|
|
10
|
+
RingElevationManager,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"RingElevationManager",
|
|
15
|
+
"RingElevation",
|
|
16
|
+
"RingElevationError",
|
|
17
|
+
"ElevationDenialReason",
|
|
18
|
+
"RingBreachDetector",
|
|
19
|
+
"BreachEvent",
|
|
20
|
+
"BreachSeverity",
|
|
21
|
+
]
|