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
hypervisor/constants.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Centralized constants for the Agent Hypervisor package.
|
|
4
|
+
|
|
5
|
+
All thresholds, limits, and magic numbers used across modules are defined
|
|
6
|
+
here so they can be maintained in a single place. Modules should import
|
|
7
|
+
from ``hypervisor.constants`` rather than hard-coding values locally.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Ring trust-score thresholds
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
RING_1_TRUST_THRESHOLD: float = 0.95
|
|
16
|
+
"""Minimum effective score (with consensus) for Ring 1 (Privileged)."""
|
|
17
|
+
|
|
18
|
+
RING_2_TRUST_THRESHOLD: float = 0.60
|
|
19
|
+
"""Minimum effective score for Ring 2 (Standard)."""
|
|
20
|
+
|
|
21
|
+
RING_1_ENFORCER_THRESHOLD: float = 0.70
|
|
22
|
+
"""Trust threshold used by the RingEnforcer for Ring 1 access."""
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Rate-limiter defaults (requests/sec, burst capacity)
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
RATE_LIMIT_RING_0: tuple[float, float] = (100.0, 200.0)
|
|
28
|
+
"""Ring 0 (Root/SRE): generous rate limit."""
|
|
29
|
+
|
|
30
|
+
RATE_LIMIT_RING_1: tuple[float, float] = (50.0, 100.0)
|
|
31
|
+
"""Ring 1 (Privileged): moderate rate limit."""
|
|
32
|
+
|
|
33
|
+
RATE_LIMIT_RING_2: tuple[float, float] = (20.0, 40.0)
|
|
34
|
+
"""Ring 2 (Standard): conservative rate limit."""
|
|
35
|
+
|
|
36
|
+
RATE_LIMIT_RING_3: tuple[float, float] = (5.0, 10.0)
|
|
37
|
+
"""Ring 3 (Sandbox): strict rate limit."""
|
|
38
|
+
|
|
39
|
+
RATE_LIMIT_FALLBACK: tuple[float, float] = RATE_LIMIT_RING_2
|
|
40
|
+
"""Fallback rate limit when a ring is not found in the limits map."""
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Vouching / sponsorship thresholds
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
VOUCHING_SCORE_SCALE: float = 1000.0
|
|
46
|
+
"""Maximum trust-score scale used by the vouching engine."""
|
|
47
|
+
|
|
48
|
+
VOUCHING_MIN_VOUCHER_SCORE: float = 0.50
|
|
49
|
+
"""Minimum score required to sponsor another agent."""
|
|
50
|
+
|
|
51
|
+
VOUCHING_DEFAULT_BOND_PCT: float = 0.20
|
|
52
|
+
"""Default percentage of sigma bonded when sponsoring."""
|
|
53
|
+
|
|
54
|
+
VOUCHING_DEFAULT_MAX_EXPOSURE: float = 0.80
|
|
55
|
+
"""Maximum exposure percentage for bonding."""
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Saga orchestrator defaults
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
SAGA_DEFAULT_MAX_RETRIES: int = 2
|
|
61
|
+
"""Default maximum retries per saga step."""
|
|
62
|
+
|
|
63
|
+
SAGA_DEFAULT_RETRY_DELAY_SECONDS: float = 1.0
|
|
64
|
+
"""Default delay between saga step retries (multiplied by attempt number)."""
|
|
65
|
+
|
|
66
|
+
SAGA_DEFAULT_STEP_TIMEOUT_SECONDS: int = 300
|
|
67
|
+
"""Default timeout for a single saga step (5 minutes)."""
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Validation limits (models.py)
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
MAX_AGENT_ID_LENGTH: int = 256
|
|
73
|
+
"""Maximum length of an agent identifier string."""
|
|
74
|
+
|
|
75
|
+
MAX_NAME_LENGTH: int = 256
|
|
76
|
+
"""Maximum length of resource names."""
|
|
77
|
+
|
|
78
|
+
MAX_API_PATH_LENGTH: int = 2048
|
|
79
|
+
"""Maximum length of an API path."""
|
|
80
|
+
|
|
81
|
+
MAX_PARTICIPANTS_LIMIT: int = 1000
|
|
82
|
+
"""Maximum number of participants in a session."""
|
|
83
|
+
|
|
84
|
+
MAX_DURATION_LIMIT: int = 604_800
|
|
85
|
+
"""Maximum session duration in seconds (7 days)."""
|
|
86
|
+
|
|
87
|
+
MAX_UNDO_WINDOW: int = 86_400
|
|
88
|
+
"""Maximum undo window in seconds (24 hours)."""
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# SessionConfig defaults
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
SESSION_DEFAULT_MIN_EFF_SCORE: float = 0.60
|
|
94
|
+
"""Default minimum effective score for session participation."""
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Risk-weight ranges by ReversibilityLevel
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
RISK_WEIGHT_FULL: tuple[float, float] = (0.1, 0.3)
|
|
100
|
+
"""Risk weight range for fully reversible actions."""
|
|
101
|
+
|
|
102
|
+
RISK_WEIGHT_PARTIAL: tuple[float, float] = (0.5, 0.8)
|
|
103
|
+
"""Risk weight range for partially reversible actions."""
|
|
104
|
+
|
|
105
|
+
RISK_WEIGHT_NONE: tuple[float, float] = (0.9, 1.0)
|
|
106
|
+
"""Risk weight range for non-reversible actions."""
|
hypervisor/core.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Hypervisor — Top-level orchestrator for multi-agent Shared Sessions.
|
|
5
|
+
|
|
6
|
+
Composes all submodules (Session, Liability, Rings, Reversibility,
|
|
7
|
+
Saga, Audit, Verification) into a unified governance runtime.
|
|
8
|
+
|
|
9
|
+
Optionally integrates with external trust scoring and behavioral
|
|
10
|
+
verification backends when adapters are provided.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from hypervisor.audit.commitment import CommitmentEngine
|
|
19
|
+
from hypervisor.audit.delta import DeltaEngine
|
|
20
|
+
from hypervisor.audit.gc import EphemeralGC, RetentionPolicy
|
|
21
|
+
from hypervisor.liability.slashing import SlashingEngine
|
|
22
|
+
from hypervisor.liability.vouching import VouchingEngine
|
|
23
|
+
from hypervisor.models import (
|
|
24
|
+
ActionDescriptor,
|
|
25
|
+
ConsistencyMode,
|
|
26
|
+
ExecutionRing,
|
|
27
|
+
SessionConfig,
|
|
28
|
+
SessionState,
|
|
29
|
+
)
|
|
30
|
+
from hypervisor.reversibility.registry import ReversibilityRegistry
|
|
31
|
+
from hypervisor.rings.classifier import ActionClassifier
|
|
32
|
+
from hypervisor.rings.enforcer import RingEnforcer
|
|
33
|
+
from hypervisor.saga.orchestrator import SagaOrchestrator
|
|
34
|
+
from hypervisor.session import SharedSessionObject
|
|
35
|
+
from hypervisor.verification.history import TransactionHistoryVerifier
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# States considered inactive (no longer need monitoring)
|
|
40
|
+
_INACTIVE_STATES = frozenset({SessionState.ARCHIVED, SessionState.TERMINATING})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ManagedSession:
|
|
44
|
+
"""A session with all its associated engines wired together."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("sso", "reversibility", "delta_engine", "saga")
|
|
47
|
+
|
|
48
|
+
def __init__(self, sso: SharedSessionObject) -> None:
|
|
49
|
+
self.sso = sso
|
|
50
|
+
self.reversibility = ReversibilityRegistry(sso.session_id)
|
|
51
|
+
self.delta_engine = DeltaEngine(sso.session_id)
|
|
52
|
+
self.saga = SagaOrchestrator()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Hypervisor:
|
|
56
|
+
"""
|
|
57
|
+
Top-level orchestrator for the Agent Hypervisor.
|
|
58
|
+
|
|
59
|
+
Usage (basic — sigma_raw passed directly):
|
|
60
|
+
hv = Hypervisor()
|
|
61
|
+
session = await hv.create_session(config, creator_did="did:mesh:admin")
|
|
62
|
+
await hv.join_session(session.sso.session_id, "did:mesh:agent-1", sigma_raw=0.85)
|
|
63
|
+
|
|
64
|
+
Usage (enriched — adapters resolve sigma and parse manifests):
|
|
65
|
+
hv = Hypervisor(
|
|
66
|
+
nexus=trust_adapter,
|
|
67
|
+
policy_check=verification_adapter,
|
|
68
|
+
iatp=manifest_adapter,
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
retention_policy: RetentionPolicy | None = None,
|
|
75
|
+
max_exposure: float | None = None,
|
|
76
|
+
nexus: Any | None = None,
|
|
77
|
+
policy_check: Any | None = None,
|
|
78
|
+
iatp: Any | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
# Shared engines
|
|
81
|
+
self.vouching = VouchingEngine(max_exposure=max_exposure)
|
|
82
|
+
self.slashing = SlashingEngine(self.vouching)
|
|
83
|
+
self.ring_enforcer = RingEnforcer()
|
|
84
|
+
self.classifier = ActionClassifier()
|
|
85
|
+
self.verifier = TransactionHistoryVerifier()
|
|
86
|
+
self.commitment = CommitmentEngine()
|
|
87
|
+
self.gc = EphemeralGC(retention_policy)
|
|
88
|
+
|
|
89
|
+
# Aliases expected by API layer
|
|
90
|
+
self.commitment_engine = self.commitment
|
|
91
|
+
self.history_verifier = self.verifier
|
|
92
|
+
|
|
93
|
+
# Integration adapters (optional)
|
|
94
|
+
self.nexus = nexus
|
|
95
|
+
self.policy_check = policy_check
|
|
96
|
+
self.iatp = iatp
|
|
97
|
+
|
|
98
|
+
# Active sessions
|
|
99
|
+
self._sessions: dict[str, ManagedSession] = {}
|
|
100
|
+
# Index of session IDs still requiring monitoring (non-archived/terminating)
|
|
101
|
+
self._active_ids: set[str] = set()
|
|
102
|
+
|
|
103
|
+
async def create_session(
|
|
104
|
+
self,
|
|
105
|
+
config: SessionConfig,
|
|
106
|
+
creator_did: str,
|
|
107
|
+
) -> ManagedSession:
|
|
108
|
+
"""Create a new Shared Session."""
|
|
109
|
+
sso = SharedSessionObject(config=config, creator_did=creator_did)
|
|
110
|
+
sso.begin_handshake()
|
|
111
|
+
managed = ManagedSession(sso)
|
|
112
|
+
self._sessions[sso.session_id] = managed
|
|
113
|
+
self._active_ids.add(sso.session_id)
|
|
114
|
+
return managed
|
|
115
|
+
|
|
116
|
+
async def join_session(
|
|
117
|
+
self,
|
|
118
|
+
session_id: str,
|
|
119
|
+
agent_did: str,
|
|
120
|
+
actions: list[ActionDescriptor] | None = None,
|
|
121
|
+
sigma_raw: float = 0.0,
|
|
122
|
+
manifest: Any | None = None,
|
|
123
|
+
agent_history: Any | None = None,
|
|
124
|
+
) -> ExecutionRing:
|
|
125
|
+
"""
|
|
126
|
+
Join an agent to a session via extended IATP handshake.
|
|
127
|
+
|
|
128
|
+
Steps:
|
|
129
|
+
1. Parse IATP manifest (if adapter + manifest provided)
|
|
130
|
+
2. Register actions in Reversibility Registry
|
|
131
|
+
3. Force Strong mode if non-reversible actions exist
|
|
132
|
+
4. Verify DID transaction history
|
|
133
|
+
5. Resolve eff_score (Nexus adapter or raw fallback) and assign ring
|
|
134
|
+
"""
|
|
135
|
+
managed = self._get_session(session_id)
|
|
136
|
+
|
|
137
|
+
# Step 1: IATP manifest enrichment
|
|
138
|
+
if self.iatp and manifest:
|
|
139
|
+
if isinstance(manifest, dict):
|
|
140
|
+
analysis = self.iatp.analyze_manifest_dict(manifest)
|
|
141
|
+
else:
|
|
142
|
+
analysis = self.iatp.analyze_manifest(manifest)
|
|
143
|
+
# Use manifest actions if none explicitly provided
|
|
144
|
+
if not actions:
|
|
145
|
+
actions = analysis.actions
|
|
146
|
+
# Use IATP sigma hint as fallback
|
|
147
|
+
if sigma_raw == 0.0:
|
|
148
|
+
sigma_raw = analysis.sigma_hint
|
|
149
|
+
logger.debug("IATP manifest parsed for %s: ring_hint=%s", agent_did, analysis.ring_hint)
|
|
150
|
+
|
|
151
|
+
# Step 2: Register actions
|
|
152
|
+
if actions:
|
|
153
|
+
managed.reversibility.register_from_manifest(actions)
|
|
154
|
+
|
|
155
|
+
# Step 3: Mode negotiation
|
|
156
|
+
if managed.reversibility.has_non_reversible_actions():
|
|
157
|
+
managed.sso.force_consistency_mode(ConsistencyMode.STRONG)
|
|
158
|
+
|
|
159
|
+
# Step 4: Verify history
|
|
160
|
+
verification = self.verifier.verify(agent_did)
|
|
161
|
+
|
|
162
|
+
# Step 5: Resolve effective score
|
|
163
|
+
eff_score = sigma_raw
|
|
164
|
+
|
|
165
|
+
# Nexus enrichment: if adapter is available and no explicit sigma given
|
|
166
|
+
if self.nexus and sigma_raw == 0.0:
|
|
167
|
+
eff_score = self.nexus.resolve_sigma(
|
|
168
|
+
agent_did,
|
|
169
|
+
history=agent_history,
|
|
170
|
+
)
|
|
171
|
+
logger.debug("Nexus resolved sigma=%.3f for %s", eff_score, agent_did)
|
|
172
|
+
elif self.nexus and agent_history:
|
|
173
|
+
# Even with explicit sigma, Nexus can verify/enrich
|
|
174
|
+
nexus_sigma = self.nexus.resolve_sigma(
|
|
175
|
+
agent_did,
|
|
176
|
+
history=agent_history,
|
|
177
|
+
)
|
|
178
|
+
# Use the lower of provided vs Nexus (conservative)
|
|
179
|
+
eff_score = min(sigma_raw, nexus_sigma)
|
|
180
|
+
|
|
181
|
+
ring = self.ring_enforcer.compute_ring(eff_score)
|
|
182
|
+
|
|
183
|
+
# Probationary agents get sandbox
|
|
184
|
+
if not verification.is_trustworthy:
|
|
185
|
+
ring = ExecutionRing.RING_3_SANDBOX
|
|
186
|
+
|
|
187
|
+
# Join the session
|
|
188
|
+
managed.sso.join(
|
|
189
|
+
agent_did=agent_did,
|
|
190
|
+
sigma_raw=sigma_raw,
|
|
191
|
+
eff_score=eff_score,
|
|
192
|
+
ring=ring,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return ring
|
|
196
|
+
|
|
197
|
+
async def activate_session(self, session_id: str) -> None:
|
|
198
|
+
"""Activate a session after handshaking is complete."""
|
|
199
|
+
managed = self._get_session(session_id)
|
|
200
|
+
managed.sso.activate()
|
|
201
|
+
|
|
202
|
+
async def terminate_session(self, session_id: str) -> str | None:
|
|
203
|
+
"""
|
|
204
|
+
Terminate a session and commit audit trail.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
audit log root summary hash, or None if audit disabled
|
|
208
|
+
"""
|
|
209
|
+
managed = self._get_session(session_id)
|
|
210
|
+
managed.sso.terminate()
|
|
211
|
+
|
|
212
|
+
hash_chain_root = self._commit_audit(session_id, managed)
|
|
213
|
+
self._cleanup_session(session_id, managed)
|
|
214
|
+
|
|
215
|
+
return hash_chain_root
|
|
216
|
+
|
|
217
|
+
def _commit_audit(self, session_id: str, managed: ManagedSession) -> str | None:
|
|
218
|
+
"""Commit audit trail and return hash chain root (None if audit disabled)."""
|
|
219
|
+
if not managed.sso.config.enable_audit:
|
|
220
|
+
return None
|
|
221
|
+
hash_chain_root = managed.delta_engine.compute_hash_chain_root()
|
|
222
|
+
if hash_chain_root:
|
|
223
|
+
self.commitment.commit(
|
|
224
|
+
session_id=session_id,
|
|
225
|
+
hash_chain_root=hash_chain_root,
|
|
226
|
+
participant_dids=[p.agent_did for p in managed.sso.participants],
|
|
227
|
+
delta_count=managed.delta_engine.turn_count,
|
|
228
|
+
)
|
|
229
|
+
return hash_chain_root
|
|
230
|
+
|
|
231
|
+
def _cleanup_session(self, session_id: str, managed: ManagedSession) -> None:
|
|
232
|
+
"""Release bonds, purge VFS data, and archive session."""
|
|
233
|
+
self.vouching.release_session_bonds(session_id)
|
|
234
|
+
self.gc.collect(
|
|
235
|
+
session_id=session_id,
|
|
236
|
+
vfs=managed.sso.vfs if hasattr(managed.sso, "vfs") else None,
|
|
237
|
+
delta_engine=managed.delta_engine,
|
|
238
|
+
delta_count=managed.delta_engine.turn_count,
|
|
239
|
+
)
|
|
240
|
+
managed.sso.archive()
|
|
241
|
+
# Remove from active index after archiving
|
|
242
|
+
self._active_ids.discard(session_id)
|
|
243
|
+
|
|
244
|
+
def get_session(self, session_id: str) -> ManagedSession | None:
|
|
245
|
+
return self._sessions.get(session_id)
|
|
246
|
+
|
|
247
|
+
async def verify_behavior(
|
|
248
|
+
self,
|
|
249
|
+
session_id: str,
|
|
250
|
+
agent_did: str,
|
|
251
|
+
claimed_embedding: Any,
|
|
252
|
+
observed_embedding: Any,
|
|
253
|
+
action_id: str | None = None,
|
|
254
|
+
) -> Any | None:
|
|
255
|
+
"""
|
|
256
|
+
Verify agent behavior via Verification adapter.
|
|
257
|
+
|
|
258
|
+
If drift exceeds threshold, automatically slashes the agent and
|
|
259
|
+
reports to Nexus (if adapter is available).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
DriftCheckResult if Verification adapter is configured, else None.
|
|
263
|
+
"""
|
|
264
|
+
if not self.policy_check:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
result = self.policy_check.check_behavioral_drift(
|
|
268
|
+
agent_did=agent_did,
|
|
269
|
+
session_id=session_id,
|
|
270
|
+
claimed_embedding=claimed_embedding,
|
|
271
|
+
observed_embedding=observed_embedding,
|
|
272
|
+
action_id=action_id,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if result.should_slash:
|
|
276
|
+
managed = self._get_session(session_id)
|
|
277
|
+
participant = managed.sso.get_participant(agent_did)
|
|
278
|
+
# Build scores dict only for the slash path (avoid on healthy agents)
|
|
279
|
+
agent_scores = {
|
|
280
|
+
p.agent_did: p.eff_score
|
|
281
|
+
for p in managed.sso.participants
|
|
282
|
+
}
|
|
283
|
+
self.slashing.slash(
|
|
284
|
+
vouchee_did=agent_did,
|
|
285
|
+
session_id=session_id,
|
|
286
|
+
vouchee_sigma=participant.eff_score,
|
|
287
|
+
risk_weight=0.95,
|
|
288
|
+
reason=f"Verification drift: {result.drift_score:.3f} ({result.severity.value})",
|
|
289
|
+
agent_scores=agent_scores,
|
|
290
|
+
)
|
|
291
|
+
# Propagate to Nexus
|
|
292
|
+
if self.nexus:
|
|
293
|
+
severity = "critical" if result.drift_score >= 0.75 else "high"
|
|
294
|
+
self.nexus.report_slash(
|
|
295
|
+
agent_did=agent_did,
|
|
296
|
+
reason=f"Behavioral drift: {result.drift_score:.3f}",
|
|
297
|
+
severity=severity,
|
|
298
|
+
)
|
|
299
|
+
logger.warning("Agent %s penalized: drift=%.3f", agent_did, result.drift_score)
|
|
300
|
+
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def active_sessions(self) -> list[ManagedSession]:
|
|
305
|
+
# Use the active index to skip archived/terminated sessions
|
|
306
|
+
return [self._sessions[sid] for sid in self._active_ids
|
|
307
|
+
if sid in self._sessions]
|
|
308
|
+
|
|
309
|
+
def _get_session(self, session_id: str) -> ManagedSession:
|
|
310
|
+
managed = self._sessions.get(session_id)
|
|
311
|
+
if not managed:
|
|
312
|
+
raise ValueError(f"Session {session_id} not found")
|
|
313
|
+
return managed
|
|
314
|
+
|
|
315
|
+
async def monitor_sessions(
|
|
316
|
+
self,
|
|
317
|
+
drift_threshold: float = 0.5,
|
|
318
|
+
) -> list[dict[str, Any]]:
|
|
319
|
+
"""
|
|
320
|
+
Batch-monitor all active sessions with early exits.
|
|
321
|
+
|
|
322
|
+
Skips archived/terminated sessions via the active index and skips
|
|
323
|
+
healthy agents (those with eff_score above the drift threshold) to
|
|
324
|
+
reduce per-iteration overhead.
|
|
325
|
+
|
|
326
|
+
Returns a list of issues found (empty if all healthy).
|
|
327
|
+
"""
|
|
328
|
+
issues: list[dict[str, Any]] = []
|
|
329
|
+
# Iterate only over active session IDs (O(active) not O(total))
|
|
330
|
+
for sid in list(self._active_ids):
|
|
331
|
+
managed = self._sessions.get(sid)
|
|
332
|
+
if managed is None:
|
|
333
|
+
self._active_ids.discard(sid)
|
|
334
|
+
continue
|
|
335
|
+
state = managed.sso.state
|
|
336
|
+
# Early exit: skip sessions that have transitioned to inactive
|
|
337
|
+
if state in _INACTIVE_STATES:
|
|
338
|
+
self._active_ids.discard(sid)
|
|
339
|
+
continue
|
|
340
|
+
# Batch-check participants; skip healthy agents
|
|
341
|
+
for p in managed.sso.participants:
|
|
342
|
+
if p.eff_score >= drift_threshold:
|
|
343
|
+
continue
|
|
344
|
+
# Only flag agents below threshold
|
|
345
|
+
issues.append({
|
|
346
|
+
"session_id": sid,
|
|
347
|
+
"agent_did": p.agent_did,
|
|
348
|
+
"eff_score": p.eff_score,
|
|
349
|
+
"ring": p.ring,
|
|
350
|
+
"state": state.value,
|
|
351
|
+
})
|
|
352
|
+
return issues
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Hypervisor Integration Adapters
|
|
5
|
+
|
|
6
|
+
Bridges between the Hypervisor and other Agent-OS modules:
|
|
7
|
+
- Nexus (trust scoring) → ring assignment
|
|
8
|
+
- Verification (behavioral verification) → drift-triggered penalty
|
|
9
|
+
- IATP (capability manifests) → reversibility + trust enrichment
|
|
10
|
+
"""
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
IATP Integration Stub — Capability Manifest parsing.
|
|
5
|
+
|
|
6
|
+
Provides the interface for parsing agent capability manifests
|
|
7
|
+
into Hypervisor-compatible action descriptors and ring hints.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Protocol
|
|
16
|
+
|
|
17
|
+
from hypervisor.models import (
|
|
18
|
+
ActionDescriptor,
|
|
19
|
+
ExecutionRing,
|
|
20
|
+
ReversibilityLevel,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IATPManifest(Protocol):
|
|
25
|
+
"""Protocol for a capability manifest."""
|
|
26
|
+
|
|
27
|
+
agent_id: str
|
|
28
|
+
trust_level: Any
|
|
29
|
+
capabilities: Any
|
|
30
|
+
scopes: list[str]
|
|
31
|
+
|
|
32
|
+
def calculate_trust_score(self) -> int: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IATPTrustLevel(str, Enum):
|
|
36
|
+
VERIFIED_PARTNER = "verified_partner"
|
|
37
|
+
TRUSTED = "trusted"
|
|
38
|
+
STANDARD = "standard"
|
|
39
|
+
UNKNOWN = "unknown"
|
|
40
|
+
UNTRUSTED = "untrusted"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
TRUST_LEVEL_RING_HINTS = {
|
|
44
|
+
IATPTrustLevel.VERIFIED_PARTNER: ExecutionRing.RING_1_PRIVILEGED,
|
|
45
|
+
IATPTrustLevel.TRUSTED: ExecutionRing.RING_2_STANDARD,
|
|
46
|
+
IATPTrustLevel.STANDARD: ExecutionRing.RING_2_STANDARD,
|
|
47
|
+
IATPTrustLevel.UNKNOWN: ExecutionRing.RING_3_SANDBOX,
|
|
48
|
+
IATPTrustLevel.UNTRUSTED: ExecutionRing.RING_3_SANDBOX,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
REVERSIBILITY_MAP = {
|
|
52
|
+
"full": ReversibilityLevel.FULL,
|
|
53
|
+
"partial": ReversibilityLevel.PARTIAL,
|
|
54
|
+
"none": ReversibilityLevel.NONE,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ManifestAnalysis:
|
|
60
|
+
"""Result of analyzing a capability manifest."""
|
|
61
|
+
|
|
62
|
+
agent_did: str
|
|
63
|
+
trust_level: IATPTrustLevel
|
|
64
|
+
ring_hint: ExecutionRing
|
|
65
|
+
iatp_trust_score: int
|
|
66
|
+
sigma_hint: float
|
|
67
|
+
actions: list[ActionDescriptor]
|
|
68
|
+
scopes: list[str]
|
|
69
|
+
has_reversible_actions: bool
|
|
70
|
+
has_non_reversible_actions: bool
|
|
71
|
+
analyzed_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class IATPAdapter:
|
|
75
|
+
"""Stub adapter for capability manifest parsing."""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self._manifest_cache: dict[str, ManifestAnalysis] = {}
|
|
79
|
+
|
|
80
|
+
def analyze_manifest(self, manifest: IATPManifest) -> ManifestAnalysis:
|
|
81
|
+
"""Analyze a manifest and extract ring hints and actions."""
|
|
82
|
+
agent_did = manifest.agent_id
|
|
83
|
+
trust_str = str(getattr(manifest.trust_level, "value", manifest.trust_level))
|
|
84
|
+
try:
|
|
85
|
+
trust_level = IATPTrustLevel(trust_str)
|
|
86
|
+
except ValueError:
|
|
87
|
+
trust_level = IATPTrustLevel.UNKNOWN
|
|
88
|
+
ring_hint = TRUST_LEVEL_RING_HINTS.get(trust_level, ExecutionRing.RING_3_SANDBOX)
|
|
89
|
+
iatp_score = manifest.calculate_trust_score()
|
|
90
|
+
import math
|
|
91
|
+
if not isinstance(iatp_score, (int, float)) or not math.isfinite(iatp_score):
|
|
92
|
+
iatp_score = 0.0
|
|
93
|
+
iatp_score = min(max(iatp_score, 0.0), 100.0)
|
|
94
|
+
sigma_hint = min(max(iatp_score / 10.0, 0.0), 1.0)
|
|
95
|
+
analysis = ManifestAnalysis(
|
|
96
|
+
agent_did=agent_did, trust_level=trust_level, ring_hint=ring_hint,
|
|
97
|
+
iatp_trust_score=iatp_score, sigma_hint=sigma_hint, actions=[],
|
|
98
|
+
scopes=list(manifest.scopes) if manifest.scopes else [],
|
|
99
|
+
has_reversible_actions=False, has_non_reversible_actions=False,
|
|
100
|
+
)
|
|
101
|
+
self._manifest_cache[agent_did] = analysis
|
|
102
|
+
return analysis
|
|
103
|
+
|
|
104
|
+
def analyze_manifest_dict(self, manifest_dict: dict) -> ManifestAnalysis:
|
|
105
|
+
"""Analyze a manifest provided as a dictionary."""
|
|
106
|
+
agent_did = manifest_dict.get("agent_id", "unknown")
|
|
107
|
+
trust_str = manifest_dict.get("trust_level", "unknown")
|
|
108
|
+
try:
|
|
109
|
+
trust_level = IATPTrustLevel(trust_str)
|
|
110
|
+
except ValueError:
|
|
111
|
+
trust_level = IATPTrustLevel.UNKNOWN
|
|
112
|
+
ring_hint = TRUST_LEVEL_RING_HINTS.get(trust_level, ExecutionRing.RING_3_SANDBOX)
|
|
113
|
+
iatp_score = manifest_dict.get("trust_score", 5)
|
|
114
|
+
import math
|
|
115
|
+
if not isinstance(iatp_score, (int, float)) or not math.isfinite(iatp_score):
|
|
116
|
+
iatp_score = 0.0
|
|
117
|
+
iatp_score = min(max(iatp_score, 0.0), 100.0)
|
|
118
|
+
sigma_hint = min(max(iatp_score / 10.0, 0.0), 1.0)
|
|
119
|
+
actions = []
|
|
120
|
+
for cap in manifest_dict.get("actions", []):
|
|
121
|
+
rev_str = cap.get("reversibility", "none")
|
|
122
|
+
actions.append(ActionDescriptor(
|
|
123
|
+
action_id=cap.get("action_id", "unknown"),
|
|
124
|
+
name=cap.get("name", ""),
|
|
125
|
+
execute_api=cap.get("execute_api", ""),
|
|
126
|
+
undo_api=cap.get("undo_api"),
|
|
127
|
+
reversibility=REVERSIBILITY_MAP.get(rev_str, ReversibilityLevel.NONE),
|
|
128
|
+
is_read_only=cap.get("is_read_only", False),
|
|
129
|
+
is_admin=cap.get("is_admin", False),
|
|
130
|
+
))
|
|
131
|
+
analysis = ManifestAnalysis(
|
|
132
|
+
agent_did=agent_did, trust_level=trust_level, ring_hint=ring_hint,
|
|
133
|
+
iatp_trust_score=iatp_score, sigma_hint=sigma_hint, actions=actions,
|
|
134
|
+
scopes=manifest_dict.get("scopes", []),
|
|
135
|
+
has_reversible_actions=any(a.reversibility != ReversibilityLevel.NONE for a in actions),
|
|
136
|
+
has_non_reversible_actions=any(a.reversibility == ReversibilityLevel.NONE and not a.is_read_only for a in actions),
|
|
137
|
+
)
|
|
138
|
+
self._manifest_cache[agent_did] = analysis
|
|
139
|
+
return analysis
|
|
140
|
+
|
|
141
|
+
def get_cached_analysis(self, agent_did: str) -> ManifestAnalysis | None:
|
|
142
|
+
return self._manifest_cache.get(agent_did)
|