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,200 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Ring Breach Detector — behavioral anomaly detection for rogue agents.
|
|
5
|
+
|
|
6
|
+
Detects two classes of anomaly:
|
|
7
|
+
|
|
8
|
+
1. **Tool-call frequency spikes** — an agent's call rate inside a sliding
|
|
9
|
+
window exceeds a configurable baseline by a severity-dependent multiplier.
|
|
10
|
+
2. **Privilege-escalation attempts** — a low-privilege agent (Ring 3)
|
|
11
|
+
repeatedly calls into higher-privilege rings (Ring 0/1). The *ring
|
|
12
|
+
distance* amplifies the anomaly score so that sandbox→root jumps are
|
|
13
|
+
scored more aggressively than standard→privileged ones.
|
|
14
|
+
|
|
15
|
+
When a HIGH or CRITICAL breach is detected the internal circuit-breaker
|
|
16
|
+
trips and ``is_breaker_tripped()`` returns ``True`` until explicitly reset
|
|
17
|
+
via ``reset_breaker()``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import time
|
|
23
|
+
from collections import deque
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import UTC, datetime
|
|
26
|
+
from enum import Enum
|
|
27
|
+
|
|
28
|
+
from hypervisor.models import ExecutionRing
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BreachSeverity(str, Enum):
|
|
32
|
+
NONE = "none"
|
|
33
|
+
LOW = "low"
|
|
34
|
+
MEDIUM = "medium"
|
|
35
|
+
HIGH = "high"
|
|
36
|
+
CRITICAL = "critical"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Multiplier thresholds: actual_rate / baseline_rate
|
|
40
|
+
_SEVERITY_THRESHOLDS: list[tuple[float, BreachSeverity]] = [
|
|
41
|
+
(20.0, BreachSeverity.CRITICAL),
|
|
42
|
+
(10.0, BreachSeverity.HIGH),
|
|
43
|
+
(5.0, BreachSeverity.MEDIUM),
|
|
44
|
+
(2.0, BreachSeverity.LOW),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BreachEvent:
|
|
50
|
+
"""A detected ring breach anomaly."""
|
|
51
|
+
|
|
52
|
+
agent_did: str
|
|
53
|
+
session_id: str
|
|
54
|
+
severity: BreachSeverity
|
|
55
|
+
anomaly_score: float
|
|
56
|
+
call_count_window: int
|
|
57
|
+
expected_rate: float
|
|
58
|
+
actual_rate: float
|
|
59
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
60
|
+
details: str = ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _agent_key(agent_did: str, session_id: str) -> str:
|
|
64
|
+
"""Internal composite key for per-agent tracking."""
|
|
65
|
+
return f"{agent_did}::{session_id}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RingBreachDetector:
|
|
69
|
+
"""Behavioural anomaly detector for rogue-agent ring abuse.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
window_seconds:
|
|
74
|
+
Sliding window (in seconds) over which call rates are measured.
|
|
75
|
+
baseline_rate:
|
|
76
|
+
Expected calls-per-second within the window. Rates above multiples
|
|
77
|
+
of this value trigger breach events of increasing severity.
|
|
78
|
+
max_events_per_agent:
|
|
79
|
+
Maximum call timestamps retained per agent (bounded ``deque``).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
window_seconds: int = 60,
|
|
85
|
+
baseline_rate: float = 10.0,
|
|
86
|
+
max_events_per_agent: int = 1_000,
|
|
87
|
+
max_breach_history: int = 10_000,
|
|
88
|
+
) -> None:
|
|
89
|
+
self.window_seconds = window_seconds
|
|
90
|
+
self.baseline_rate = baseline_rate
|
|
91
|
+
self.max_events_per_agent = max_events_per_agent
|
|
92
|
+
|
|
93
|
+
# Per-agent sliding-window timestamps
|
|
94
|
+
self._call_windows: dict[str, deque[float]] = {}
|
|
95
|
+
# Per-agent circuit-breaker flag
|
|
96
|
+
self._tripped: dict[str, bool] = {}
|
|
97
|
+
# Global breach history (bounded)
|
|
98
|
+
self._breach_history: deque[BreachEvent] = deque(maxlen=max_breach_history)
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Public API
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def record_call(
|
|
105
|
+
self,
|
|
106
|
+
agent_did: str,
|
|
107
|
+
session_id: str,
|
|
108
|
+
agent_ring: ExecutionRing,
|
|
109
|
+
called_ring: ExecutionRing,
|
|
110
|
+
) -> BreachEvent | None:
|
|
111
|
+
"""Record a ring call and return a ``BreachEvent`` if anomalous.
|
|
112
|
+
|
|
113
|
+
Returns ``None`` when the call is within normal parameters.
|
|
114
|
+
"""
|
|
115
|
+
key = _agent_key(agent_did, session_id)
|
|
116
|
+
now = time.monotonic()
|
|
117
|
+
|
|
118
|
+
# --- 1. Track timestamp in bounded deque ---
|
|
119
|
+
if key not in self._call_windows:
|
|
120
|
+
self._call_windows[key] = deque(maxlen=self.max_events_per_agent)
|
|
121
|
+
window = self._call_windows[key]
|
|
122
|
+
window.append(now)
|
|
123
|
+
|
|
124
|
+
# --- 2. Prune timestamps outside the sliding window ---
|
|
125
|
+
cutoff = now - self.window_seconds
|
|
126
|
+
while window and window[0] < cutoff:
|
|
127
|
+
window.popleft()
|
|
128
|
+
|
|
129
|
+
# --- 3. Compute actual rate (calls / second) ---
|
|
130
|
+
call_count = len(window)
|
|
131
|
+
actual_rate = call_count / self.window_seconds if self.window_seconds > 0 else 0.0
|
|
132
|
+
|
|
133
|
+
# --- 4. Ring-distance amplifier ---
|
|
134
|
+
# Upward calls (low value = higher privilege) are escalations.
|
|
135
|
+
# ExecutionRing values: 0=root, 1=priv, 2=std, 3=sandbox.
|
|
136
|
+
# ring_distance > 0 means privilege escalation.
|
|
137
|
+
ring_distance = int(agent_ring) - int(called_ring)
|
|
138
|
+
amplifier = max(ring_distance, 1) # at least 1× (no reduction)
|
|
139
|
+
|
|
140
|
+
# --- 5. Score = (actual / baseline) × amplifier ---
|
|
141
|
+
if self.baseline_rate <= 0:
|
|
142
|
+
ratio = 0.0
|
|
143
|
+
else:
|
|
144
|
+
ratio = actual_rate / self.baseline_rate
|
|
145
|
+
anomaly_score = ratio * amplifier
|
|
146
|
+
|
|
147
|
+
# --- 6. Map score → severity ---
|
|
148
|
+
severity = BreachSeverity.NONE
|
|
149
|
+
for threshold, sev in _SEVERITY_THRESHOLDS:
|
|
150
|
+
if anomaly_score >= threshold:
|
|
151
|
+
severity = sev
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if severity == BreachSeverity.NONE:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# --- 7. Build event ---
|
|
158
|
+
event = BreachEvent(
|
|
159
|
+
agent_did=agent_did,
|
|
160
|
+
session_id=session_id,
|
|
161
|
+
severity=severity,
|
|
162
|
+
anomaly_score=round(anomaly_score, 4),
|
|
163
|
+
call_count_window=call_count,
|
|
164
|
+
expected_rate=self.baseline_rate,
|
|
165
|
+
actual_rate=round(actual_rate, 4),
|
|
166
|
+
details=(
|
|
167
|
+
f"rate={actual_rate:.2f}/s (baseline={self.baseline_rate:.2f}/s), "
|
|
168
|
+
f"ring_distance={ring_distance}, amplifier={amplifier}×, "
|
|
169
|
+
f"score={anomaly_score:.2f}"
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
self._breach_history.append(event)
|
|
173
|
+
|
|
174
|
+
# --- 8. Trip circuit-breaker on HIGH / CRITICAL ---
|
|
175
|
+
if severity in (BreachSeverity.HIGH, BreachSeverity.CRITICAL):
|
|
176
|
+
self._tripped[key] = True
|
|
177
|
+
|
|
178
|
+
return event
|
|
179
|
+
|
|
180
|
+
def is_breaker_tripped(self, agent_did: str, session_id: str) -> bool:
|
|
181
|
+
"""Return ``True`` if the circuit-breaker is tripped for this agent."""
|
|
182
|
+
return self._tripped.get(_agent_key(agent_did, session_id), False)
|
|
183
|
+
|
|
184
|
+
def reset_breaker(self, agent_did: str, session_id: str) -> None:
|
|
185
|
+
"""Reset the circuit-breaker and clear the call window for this agent."""
|
|
186
|
+
key = _agent_key(agent_did, session_id)
|
|
187
|
+
self._tripped.pop(key, None)
|
|
188
|
+
self._call_windows.pop(key, None)
|
|
189
|
+
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
# Read-only accessors
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def breach_history(self) -> list[BreachEvent]:
|
|
196
|
+
return list(self._breach_history)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def breach_count(self) -> int:
|
|
200
|
+
return len(self._breach_history)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""
|
|
5
|
+
Action Risk Classifier
|
|
6
|
+
|
|
7
|
+
Classifies actions into ring levels and risk weights.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from hypervisor.models import ActionDescriptor, ExecutionRing, ReversibilityLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ClassificationResult:
|
|
19
|
+
"""Result of classifying an action."""
|
|
20
|
+
|
|
21
|
+
action_id: str
|
|
22
|
+
ring: ExecutionRing
|
|
23
|
+
risk_weight: float
|
|
24
|
+
reversibility: ReversibilityLevel
|
|
25
|
+
confidence: float = 1.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ActionClassifier:
|
|
29
|
+
"""
|
|
30
|
+
Classifies actions into ring levels and risk weights.
|
|
31
|
+
|
|
32
|
+
Classification rules:
|
|
33
|
+
- Has Undo_API → reversible → Ring 2 minimum
|
|
34
|
+
- No Undo_API + destructive → non-reversible → Ring 1 minimum
|
|
35
|
+
- Config/admin operations → Ring 0
|
|
36
|
+
- Read-only operations → Ring 3
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._cache: dict[str, ClassificationResult] = {}
|
|
41
|
+
self._overrides: dict[str, ClassificationResult] = {}
|
|
42
|
+
|
|
43
|
+
def classify(self, action: ActionDescriptor) -> ClassificationResult:
|
|
44
|
+
"""Classify an action and cache the result."""
|
|
45
|
+
if action.action_id in self._overrides:
|
|
46
|
+
return self._overrides[action.action_id]
|
|
47
|
+
|
|
48
|
+
if action.action_id in self._cache:
|
|
49
|
+
return self._cache[action.action_id]
|
|
50
|
+
|
|
51
|
+
result = ClassificationResult(
|
|
52
|
+
action_id=action.action_id,
|
|
53
|
+
ring=action.required_ring,
|
|
54
|
+
risk_weight=action.risk_weight,
|
|
55
|
+
reversibility=action.reversibility,
|
|
56
|
+
)
|
|
57
|
+
self._cache[action.action_id] = result
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
def set_override(
|
|
61
|
+
self,
|
|
62
|
+
action_id: str,
|
|
63
|
+
ring: ExecutionRing | None = None,
|
|
64
|
+
risk_weight: float | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Set a session-level override for action classification."""
|
|
67
|
+
existing = self._cache.get(action_id)
|
|
68
|
+
self._overrides[action_id] = ClassificationResult(
|
|
69
|
+
action_id=action_id,
|
|
70
|
+
ring=ring or (existing.ring if existing else ExecutionRing.RING_3_SANDBOX),
|
|
71
|
+
risk_weight=risk_weight or (existing.risk_weight if existing else 0.5),
|
|
72
|
+
reversibility=existing.reversibility if existing else ReversibilityLevel.NONE,
|
|
73
|
+
confidence=0.9, # overrides have slightly lower confidence
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def clear_cache(self) -> None:
|
|
77
|
+
"""Clear classification cache (e.g., on manifest update)."""
|
|
78
|
+
self._cache.clear()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""
|
|
5
|
+
Ring Elevation — privilege escalation stubs.
|
|
6
|
+
|
|
7
|
+
Public Preview: elevation is not supported. All requests are denied.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
|
|
16
|
+
from hypervisor.models import ExecutionRing
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RingElevationError(Exception):
|
|
20
|
+
"""Raised for invalid ring elevation requests."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
message: str,
|
|
25
|
+
*,
|
|
26
|
+
current_ring: ExecutionRing | None = None,
|
|
27
|
+
target_ring: ExecutionRing | None = None,
|
|
28
|
+
reason: str | None = None,
|
|
29
|
+
agent_did: str = "",
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.current_ring = current_ring
|
|
33
|
+
self.target_ring = target_ring
|
|
34
|
+
self.denial_reason = reason
|
|
35
|
+
self.agent_did = agent_did
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ElevationDenialReason:
|
|
39
|
+
"""Standard denial reasons for ring elevation failures."""
|
|
40
|
+
|
|
41
|
+
COMMUNITY_EDITION = "community_edition"
|
|
42
|
+
INVALID_TARGET = "invalid_target"
|
|
43
|
+
RING_0_FORBIDDEN = "ring_0_forbidden"
|
|
44
|
+
INSUFFICIENT_TRUST = "insufficient_trust"
|
|
45
|
+
NO_SPONSORSHIP = "no_sponsorship"
|
|
46
|
+
EXPIRED_TTL = "expired_ttl"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_RING_LABELS: dict[ExecutionRing, str] = {
|
|
50
|
+
ExecutionRing.RING_0_ROOT: "Ring 0 (Root)",
|
|
51
|
+
ExecutionRing.RING_1_PRIVILEGED: "Ring 1 (Privileged)",
|
|
52
|
+
ExecutionRing.RING_2_STANDARD: "Ring 2 (Standard)",
|
|
53
|
+
ExecutionRing.RING_3_SANDBOX: "Ring 3 (Sandbox)",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_DOCS_URL = "https://github.com/microsoft/agent-governance-toolkit/blob/main/docs/rings.md"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class RingElevation:
|
|
61
|
+
"""A ring elevation grant (stub in Public Preview)."""
|
|
62
|
+
|
|
63
|
+
elevation_id: str = field(default_factory=lambda: f"elev:{uuid.uuid4().hex[:8]}")
|
|
64
|
+
agent_did: str = ""
|
|
65
|
+
session_id: str = ""
|
|
66
|
+
original_ring: ExecutionRing = ExecutionRing.RING_3_SANDBOX
|
|
67
|
+
elevated_ring: ExecutionRing = ExecutionRing.RING_2_STANDARD
|
|
68
|
+
granted_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
69
|
+
expires_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
70
|
+
attestation: str | None = None
|
|
71
|
+
reason: str = ""
|
|
72
|
+
is_active: bool = True
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_expired(self) -> bool:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def remaining_seconds(self) -> float:
|
|
80
|
+
return 0.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RingElevationManager:
|
|
84
|
+
"""Manages ring elevations (Public Preview: always denies)."""
|
|
85
|
+
|
|
86
|
+
MAX_ELEVATION_TTL = 3600
|
|
87
|
+
DEFAULT_TTL = 300
|
|
88
|
+
|
|
89
|
+
def __init__(self) -> None:
|
|
90
|
+
self._elevations: dict[str, RingElevation] = {}
|
|
91
|
+
|
|
92
|
+
def request_elevation(
|
|
93
|
+
self,
|
|
94
|
+
agent_did: str,
|
|
95
|
+
session_id: str,
|
|
96
|
+
current_ring: ExecutionRing,
|
|
97
|
+
target_ring: ExecutionRing,
|
|
98
|
+
ttl_seconds: int = 0,
|
|
99
|
+
attestation: str | None = None,
|
|
100
|
+
reason: str = "",
|
|
101
|
+
) -> RingElevation:
|
|
102
|
+
"""Request temporary ring elevation (Public Preview: always denied)."""
|
|
103
|
+
# Validate: target must be a higher privilege (lower numeric value)
|
|
104
|
+
if target_ring.value >= current_ring.value:
|
|
105
|
+
denial = ElevationDenialReason.INVALID_TARGET
|
|
106
|
+
raise RingElevationError(
|
|
107
|
+
_build_elevation_error_message(
|
|
108
|
+
current_ring=current_ring,
|
|
109
|
+
target_ring=target_ring,
|
|
110
|
+
reason=denial,
|
|
111
|
+
agent_did=agent_did,
|
|
112
|
+
),
|
|
113
|
+
current_ring=current_ring,
|
|
114
|
+
target_ring=target_ring,
|
|
115
|
+
reason=denial,
|
|
116
|
+
agent_did=agent_did,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Validate: Ring 0 cannot be requested via standard API
|
|
120
|
+
if target_ring == ExecutionRing.RING_0_ROOT:
|
|
121
|
+
denial = ElevationDenialReason.RING_0_FORBIDDEN
|
|
122
|
+
raise RingElevationError(
|
|
123
|
+
_build_elevation_error_message(
|
|
124
|
+
current_ring=current_ring,
|
|
125
|
+
target_ring=target_ring,
|
|
126
|
+
reason=denial,
|
|
127
|
+
agent_did=agent_did,
|
|
128
|
+
),
|
|
129
|
+
current_ring=current_ring,
|
|
130
|
+
target_ring=target_ring,
|
|
131
|
+
reason=denial,
|
|
132
|
+
agent_did=agent_did,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Public Preview: all valid requests are denied
|
|
136
|
+
denial = ElevationDenialReason.COMMUNITY_EDITION
|
|
137
|
+
raise RingElevationError(
|
|
138
|
+
_build_elevation_error_message(
|
|
139
|
+
current_ring=current_ring,
|
|
140
|
+
target_ring=target_ring,
|
|
141
|
+
reason=denial,
|
|
142
|
+
agent_did=agent_did,
|
|
143
|
+
),
|
|
144
|
+
current_ring=current_ring,
|
|
145
|
+
target_ring=target_ring,
|
|
146
|
+
reason=denial,
|
|
147
|
+
agent_did=agent_did,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_active_elevation(self, agent_did: str, session_id: str) -> RingElevation | None:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def get_effective_ring(self, agent_did: str, session_id: str, base_ring: ExecutionRing) -> ExecutionRing:
|
|
154
|
+
return base_ring
|
|
155
|
+
|
|
156
|
+
def revoke_elevation(self, elevation_id: str) -> None:
|
|
157
|
+
raise RingElevationError(f"Elevation {elevation_id} not found")
|
|
158
|
+
|
|
159
|
+
def tick(self) -> list[RingElevation]:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
def register_child(self, parent_did: str, child_did: str, parent_ring: ExecutionRing) -> ExecutionRing:
|
|
163
|
+
child_ring_value = min(parent_ring.value + 1, ExecutionRing.RING_3_SANDBOX.value)
|
|
164
|
+
return ExecutionRing(child_ring_value)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def active_elevations(self) -> list[RingElevation]:
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_REMEDIATION: dict[str, str] = {
|
|
172
|
+
ElevationDenialReason.COMMUNITY_EDITION: (
|
|
173
|
+
"Upgrade to the Enterprise edition to enable ring elevation, "
|
|
174
|
+
"or request access from your organization admin."
|
|
175
|
+
),
|
|
176
|
+
ElevationDenialReason.INVALID_TARGET: (
|
|
177
|
+
"Request a target ring with a lower numeric value (higher privilege) "
|
|
178
|
+
"than the agent's current ring."
|
|
179
|
+
),
|
|
180
|
+
ElevationDenialReason.RING_0_FORBIDDEN: (
|
|
181
|
+
"Ring 0 requires SRE Witness attestation and cannot be requested "
|
|
182
|
+
"via the standard elevation API. Contact your platform team."
|
|
183
|
+
),
|
|
184
|
+
ElevationDenialReason.INSUFFICIENT_TRUST: (
|
|
185
|
+
"Increase the agent's effective trust score above the required "
|
|
186
|
+
"threshold by completing successful operations in the current ring."
|
|
187
|
+
),
|
|
188
|
+
ElevationDenialReason.NO_SPONSORSHIP: (
|
|
189
|
+
"Obtain a sponsorship from a Ring 1 or Ring 0 agent to vouch "
|
|
190
|
+
"for this elevation request."
|
|
191
|
+
),
|
|
192
|
+
ElevationDenialReason.EXPIRED_TTL: (
|
|
193
|
+
"Submit a new elevation request with a valid TTL "
|
|
194
|
+
f"(max {RingElevationManager.MAX_ELEVATION_TTL}s)."
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _build_elevation_error_message(
|
|
200
|
+
*,
|
|
201
|
+
current_ring: ExecutionRing,
|
|
202
|
+
target_ring: ExecutionRing,
|
|
203
|
+
reason: str,
|
|
204
|
+
agent_did: str = "",
|
|
205
|
+
) -> str:
|
|
206
|
+
"""Build a structured, actionable error message for elevation failures."""
|
|
207
|
+
current_label = _RING_LABELS.get(current_ring, str(current_ring))
|
|
208
|
+
target_label = _RING_LABELS.get(target_ring, str(target_ring))
|
|
209
|
+
remediation = _REMEDIATION.get(reason, "Review the elevation requirements.")
|
|
210
|
+
|
|
211
|
+
parts = [
|
|
212
|
+
f"Ring elevation denied: {current_label} -> {target_label}",
|
|
213
|
+
]
|
|
214
|
+
if agent_did:
|
|
215
|
+
parts.append(f" Agent: {agent_did}")
|
|
216
|
+
parts.append(f" Reason: {reason}")
|
|
217
|
+
parts.append(f" Remediation: {remediation}")
|
|
218
|
+
parts.append(f" Docs: {_DOCS_URL}")
|
|
219
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""
|
|
5
|
+
Ring Enforcer — simple 2-tier access control.
|
|
6
|
+
|
|
7
|
+
Public Preview: agents get RING_1 (trust > 0.7) or RING_2 (default).
|
|
8
|
+
Ring 0 is reserved for kernel-only operations and always denied.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from hypervisor.constants import RING_1_ENFORCER_THRESHOLD
|
|
16
|
+
from hypervisor.models import ActionDescriptor, ExecutionRing
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RingCheckResult:
|
|
21
|
+
"""Result of a ring enforcement check."""
|
|
22
|
+
|
|
23
|
+
allowed: bool
|
|
24
|
+
required_ring: ExecutionRing
|
|
25
|
+
agent_ring: ExecutionRing
|
|
26
|
+
eff_score: float
|
|
27
|
+
reason: str
|
|
28
|
+
requires_consensus: bool = False
|
|
29
|
+
requires_sre_witness: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RingEnforcer:
|
|
33
|
+
"""
|
|
34
|
+
Simple 2-tier ring enforcer.
|
|
35
|
+
|
|
36
|
+
Ring 0 (Root): Always denied (kernel-only).
|
|
37
|
+
Ring 1 (Privileged): Requires trust > 0.7.
|
|
38
|
+
Ring 2 (Standard): Default for all agents.
|
|
39
|
+
Ring 3 (Sandbox): Read-only / research.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
RING_1_THRESHOLD = RING_1_ENFORCER_THRESHOLD
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def check(
|
|
48
|
+
self,
|
|
49
|
+
agent_ring: ExecutionRing,
|
|
50
|
+
action: ActionDescriptor,
|
|
51
|
+
eff_score: float,
|
|
52
|
+
has_consensus: bool = False,
|
|
53
|
+
has_sre_witness: bool = False,
|
|
54
|
+
) -> RingCheckResult:
|
|
55
|
+
"""Check if an agent can perform an action given their ring level."""
|
|
56
|
+
required = action.required_ring
|
|
57
|
+
|
|
58
|
+
# Ring 0: always denied in Public Preview
|
|
59
|
+
if required == ExecutionRing.RING_0_ROOT:
|
|
60
|
+
return RingCheckResult(
|
|
61
|
+
allowed=False,
|
|
62
|
+
required_ring=required,
|
|
63
|
+
agent_ring=agent_ring,
|
|
64
|
+
eff_score=eff_score,
|
|
65
|
+
reason="Ring 0 actions are not available in Public Preview",
|
|
66
|
+
requires_sre_witness=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Agent's ring must be <= required ring (lower number = more privileged)
|
|
70
|
+
if agent_ring.value > required.value:
|
|
71
|
+
return RingCheckResult(
|
|
72
|
+
allowed=False,
|
|
73
|
+
required_ring=required,
|
|
74
|
+
agent_ring=agent_ring,
|
|
75
|
+
eff_score=eff_score,
|
|
76
|
+
reason=(
|
|
77
|
+
f"Agent ring {agent_ring.value} insufficient for "
|
|
78
|
+
f"required ring {required.value}"
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return RingCheckResult(
|
|
83
|
+
allowed=True,
|
|
84
|
+
required_ring=required,
|
|
85
|
+
agent_ring=agent_ring,
|
|
86
|
+
eff_score=eff_score,
|
|
87
|
+
reason="Access granted",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def compute_ring(self, eff_score: float, has_consensus: bool = False) -> ExecutionRing:
|
|
91
|
+
"""Compute ring assignment from trust score."""
|
|
92
|
+
return ExecutionRing.from_eff_score(eff_score, has_consensus)
|
|
93
|
+
|
|
94
|
+
def should_demote(self, current_ring: ExecutionRing, eff_score: float) -> bool:
|
|
95
|
+
"""Check if an agent should be demoted based on trust drop."""
|
|
96
|
+
appropriate = self.compute_ring(eff_score)
|
|
97
|
+
return appropriate.value > current_ring.value
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Saga subpackage — orchestration, fan-out, checkpoints, DSL."""
|
|
4
|
+
|
|
5
|
+
from hypervisor.saga.checkpoint import CheckpointManager, SemanticCheckpoint
|
|
6
|
+
from hypervisor.saga.dsl import SagaDefinition, SagaDSLError, SagaDSLParser
|
|
7
|
+
from hypervisor.saga.fan_out import FanOutGroup, FanOutOrchestrator, FanOutPolicy
|
|
8
|
+
from hypervisor.saga.schema import SAGA_DEFINITION_SCHEMA, SagaSchemaError, SagaSchemaValidator
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"FanOutOrchestrator",
|
|
12
|
+
"FanOutGroup",
|
|
13
|
+
"FanOutPolicy",
|
|
14
|
+
"CheckpointManager",
|
|
15
|
+
"SemanticCheckpoint",
|
|
16
|
+
"SagaDSLParser",
|
|
17
|
+
"SagaDefinition",
|
|
18
|
+
"SagaDSLError",
|
|
19
|
+
"SagaSchemaValidator",
|
|
20
|
+
"SagaSchemaError",
|
|
21
|
+
"SAGA_DEFINITION_SCHEMA",
|
|
22
|
+
]
|