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,190 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""Runtime-layer per-agent/per-ring rate limiting.
|
|
5
|
+
|
|
6
|
+
This module enforces token-bucket limits per agent, session, and execution ring
|
|
7
|
+
inside the hypervisor runtime layer.
|
|
8
|
+
|
|
9
|
+
See also:
|
|
10
|
+
- agent_os.integrations.rate_limiter: tool-call policy limits in Agent OS.
|
|
11
|
+
- agentmesh.services.rate_limiter: service/proxy-level limits in Agent Mesh.
|
|
12
|
+
- agentmesh.services.rate_limit_middleware: HTTP edge middleware in Agent Mesh.
|
|
13
|
+
- agent_os.policies.rate_limiting: shared token-bucket primitives.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
|
|
21
|
+
from hypervisor.constants import (
|
|
22
|
+
RATE_LIMIT_FALLBACK,
|
|
23
|
+
RATE_LIMIT_RING_0,
|
|
24
|
+
RATE_LIMIT_RING_1,
|
|
25
|
+
RATE_LIMIT_RING_2,
|
|
26
|
+
RATE_LIMIT_RING_3,
|
|
27
|
+
)
|
|
28
|
+
from hypervisor.models import ExecutionRing
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RateLimitExceeded(Exception):
|
|
32
|
+
"""Raised when an agent exceeds their rate limit."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TokenBucket:
|
|
37
|
+
"""A token bucket for rate limiting."""
|
|
38
|
+
|
|
39
|
+
capacity: float
|
|
40
|
+
tokens: float
|
|
41
|
+
refill_rate: float # tokens per second
|
|
42
|
+
last_refill: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
43
|
+
|
|
44
|
+
def consume(self, tokens: float = 1.0) -> bool:
|
|
45
|
+
"""Try to consume tokens. Returns True if successful."""
|
|
46
|
+
self._refill()
|
|
47
|
+
if self.tokens >= tokens:
|
|
48
|
+
self.tokens -= tokens
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def _refill(self) -> None:
|
|
53
|
+
"""Refill tokens based on elapsed time."""
|
|
54
|
+
now = datetime.now(UTC)
|
|
55
|
+
elapsed = (now - self.last_refill).total_seconds()
|
|
56
|
+
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
|
|
57
|
+
self.last_refill = now
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def available(self) -> float:
|
|
61
|
+
self._refill()
|
|
62
|
+
return self.tokens
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Default rate limits per ring (requests per second, burst capacity)
|
|
66
|
+
DEFAULT_RING_LIMITS: dict[ExecutionRing, tuple[float, float]] = {
|
|
67
|
+
ExecutionRing.RING_0_ROOT: RATE_LIMIT_RING_0,
|
|
68
|
+
ExecutionRing.RING_1_PRIVILEGED: RATE_LIMIT_RING_1,
|
|
69
|
+
ExecutionRing.RING_2_STANDARD: RATE_LIMIT_RING_2,
|
|
70
|
+
ExecutionRing.RING_3_SANDBOX: RATE_LIMIT_RING_3,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class RateLimitStats:
|
|
76
|
+
"""Statistics for an agent's rate limiting."""
|
|
77
|
+
|
|
78
|
+
agent_did: str
|
|
79
|
+
ring: ExecutionRing
|
|
80
|
+
total_requests: int = 0
|
|
81
|
+
rejected_requests: int = 0
|
|
82
|
+
tokens_available: float = 0.0
|
|
83
|
+
capacity: float = 0.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AgentRateLimiter:
|
|
87
|
+
"""
|
|
88
|
+
Rate limiting per agent per ring using token buckets.
|
|
89
|
+
|
|
90
|
+
Higher-privilege rings get more generous limits. When an agent
|
|
91
|
+
is promoted/demoted, their bucket is recreated with new limits.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
ring_limits: dict[ExecutionRing, tuple[float, float]] | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
self._limits = ring_limits or dict(DEFAULT_RING_LIMITS)
|
|
99
|
+
# (agent_did, session_id) -> TokenBucket
|
|
100
|
+
self._buckets: dict[str, TokenBucket] = {}
|
|
101
|
+
self._stats: dict[str, RateLimitStats] = {}
|
|
102
|
+
|
|
103
|
+
def check(
|
|
104
|
+
self,
|
|
105
|
+
agent_did: str,
|
|
106
|
+
session_id: str,
|
|
107
|
+
ring: ExecutionRing,
|
|
108
|
+
cost: float = 1.0,
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Check if an agent can make a request.
|
|
112
|
+
|
|
113
|
+
Returns True if allowed, raises RateLimitExceeded if not.
|
|
114
|
+
"""
|
|
115
|
+
key = f"{agent_did}:{session_id}"
|
|
116
|
+
bucket = self._get_or_create_bucket(key, ring)
|
|
117
|
+
|
|
118
|
+
# Track stats
|
|
119
|
+
stats = self._stats.setdefault(
|
|
120
|
+
key,
|
|
121
|
+
RateLimitStats(agent_did=agent_did, ring=ring),
|
|
122
|
+
)
|
|
123
|
+
stats.total_requests += 1
|
|
124
|
+
|
|
125
|
+
if not bucket.consume(cost):
|
|
126
|
+
stats.rejected_requests += 1
|
|
127
|
+
raise RateLimitExceeded(
|
|
128
|
+
f"Agent {agent_did} exceeded rate limit for ring "
|
|
129
|
+
f"{ring.value} ({stats.rejected_requests} rejections)"
|
|
130
|
+
)
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def try_check(
|
|
134
|
+
self,
|
|
135
|
+
agent_did: str,
|
|
136
|
+
session_id: str,
|
|
137
|
+
ring: ExecutionRing,
|
|
138
|
+
cost: float = 1.0,
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""Like check(), but returns False instead of raising."""
|
|
141
|
+
try:
|
|
142
|
+
return self.check(agent_did, session_id, ring, cost)
|
|
143
|
+
except RateLimitExceeded:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def update_ring(
|
|
147
|
+
self,
|
|
148
|
+
agent_did: str,
|
|
149
|
+
session_id: str,
|
|
150
|
+
new_ring: ExecutionRing,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Update an agent's rate limit when their ring changes."""
|
|
153
|
+
key = f"{agent_did}:{session_id}"
|
|
154
|
+
rate, capacity = self._limits.get(
|
|
155
|
+
new_ring, RATE_LIMIT_FALLBACK
|
|
156
|
+
)
|
|
157
|
+
self._buckets[key] = TokenBucket(
|
|
158
|
+
capacity=capacity,
|
|
159
|
+
tokens=capacity, # Start full
|
|
160
|
+
refill_rate=rate,
|
|
161
|
+
)
|
|
162
|
+
if key in self._stats:
|
|
163
|
+
self._stats[key].ring = new_ring
|
|
164
|
+
|
|
165
|
+
def get_stats(self, agent_did: str, session_id: str) -> RateLimitStats | None:
|
|
166
|
+
"""Get rate limit stats for an agent."""
|
|
167
|
+
key = f"{agent_did}:{session_id}"
|
|
168
|
+
stats = self._stats.get(key)
|
|
169
|
+
if stats:
|
|
170
|
+
bucket = self._buckets.get(key)
|
|
171
|
+
if bucket:
|
|
172
|
+
stats.tokens_available = bucket.available
|
|
173
|
+
stats.capacity = bucket.capacity
|
|
174
|
+
return stats
|
|
175
|
+
|
|
176
|
+
def _get_or_create_bucket(
|
|
177
|
+
self, key: str, ring: ExecutionRing
|
|
178
|
+
) -> TokenBucket:
|
|
179
|
+
if key not in self._buckets:
|
|
180
|
+
rate, capacity = self._limits.get(ring, RATE_LIMIT_FALLBACK)
|
|
181
|
+
self._buckets[key] = TokenBucket(
|
|
182
|
+
capacity=capacity,
|
|
183
|
+
tokens=capacity,
|
|
184
|
+
refill_rate=rate,
|
|
185
|
+
)
|
|
186
|
+
return self._buckets[key]
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def tracked_agents(self) -> int:
|
|
190
|
+
return len(self._buckets)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Shared Session Object — lifecycle manager for multi-agent sessions."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import UTC, datetime, timezone
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from hypervisor.models import (
|
|
13
|
+
ConsistencyMode,
|
|
14
|
+
ExecutionRing,
|
|
15
|
+
SessionConfig,
|
|
16
|
+
SessionParticipant,
|
|
17
|
+
SessionState,
|
|
18
|
+
)
|
|
19
|
+
from hypervisor.session.sso import SessionVFS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SharedSessionObject:
|
|
23
|
+
"""
|
|
24
|
+
Encapsulates a multi-agent interaction.
|
|
25
|
+
|
|
26
|
+
Every Shared Session has:
|
|
27
|
+
- SessionID: UUID bound to a DID
|
|
28
|
+
- ConsistencyMode: Strong (consensus) or Eventual (gossip)
|
|
29
|
+
- StateSubstrate: A VFS representing the shared world
|
|
30
|
+
- LiabilityMatrix: Registry of who sponsors for whom
|
|
31
|
+
|
|
32
|
+
Lifecycle: created → handshaking → active → terminating → archived
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
config: SessionConfig,
|
|
38
|
+
creator_did: str,
|
|
39
|
+
session_id: str | None = None,
|
|
40
|
+
):
|
|
41
|
+
self.session_id = session_id or f"session:{uuid.uuid4()}"
|
|
42
|
+
self.creator_did = creator_did
|
|
43
|
+
self.config = config
|
|
44
|
+
self.state = SessionState.CREATED
|
|
45
|
+
self.consistency_mode = config.consistency_mode
|
|
46
|
+
|
|
47
|
+
# Participants
|
|
48
|
+
self._participants: dict[str, SessionParticipant] = {}
|
|
49
|
+
|
|
50
|
+
# VFS state substrate (namespace for this session)
|
|
51
|
+
self.vfs_namespace = f"/sessions/{self.session_id}"
|
|
52
|
+
self.vfs = SessionVFS(self.session_id, namespace=self.vfs_namespace)
|
|
53
|
+
self._vfs_snapshots: dict[str, Any] = {}
|
|
54
|
+
|
|
55
|
+
# Timestamps
|
|
56
|
+
self.created_at = datetime.now(UTC)
|
|
57
|
+
self.terminated_at: datetime | None = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def participants(self) -> list[SessionParticipant]:
|
|
61
|
+
"""Active participants in this session."""
|
|
62
|
+
return [p for p in self._participants.values() if p.is_active]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def participant_count(self) -> int:
|
|
66
|
+
# Avoid building a filtered list just to count
|
|
67
|
+
return sum(1 for p in self._participants.values() if p.is_active)
|
|
68
|
+
|
|
69
|
+
def _assert_state(self, *allowed: SessionState) -> None:
|
|
70
|
+
if self.state not in allowed:
|
|
71
|
+
raise SessionLifecycleError(
|
|
72
|
+
f"Operation not allowed in state {self.state.value}. "
|
|
73
|
+
f"Allowed: {[s.value for s in allowed]}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def begin_handshake(self) -> None:
|
|
77
|
+
"""Transition to handshaking phase."""
|
|
78
|
+
self._assert_state(SessionState.CREATED)
|
|
79
|
+
self.state = SessionState.HANDSHAKING
|
|
80
|
+
|
|
81
|
+
def activate(self) -> None:
|
|
82
|
+
"""Transition to active execution phase."""
|
|
83
|
+
self._assert_state(SessionState.HANDSHAKING)
|
|
84
|
+
if not self._participants:
|
|
85
|
+
raise SessionLifecycleError("Cannot activate session with no participants")
|
|
86
|
+
self.state = SessionState.ACTIVE
|
|
87
|
+
|
|
88
|
+
def join(
|
|
89
|
+
self,
|
|
90
|
+
agent_did: str,
|
|
91
|
+
sigma_raw: float = 0.0,
|
|
92
|
+
eff_score: float = 0.0,
|
|
93
|
+
ring: ExecutionRing = ExecutionRing.RING_3_SANDBOX,
|
|
94
|
+
) -> SessionParticipant:
|
|
95
|
+
"""Add an agent to this session."""
|
|
96
|
+
self._assert_state(SessionState.HANDSHAKING, SessionState.ACTIVE)
|
|
97
|
+
|
|
98
|
+
if agent_did in self._participants:
|
|
99
|
+
raise SessionParticipantError(f"Agent {agent_did} already in session")
|
|
100
|
+
if self.participant_count >= self.config.max_participants:
|
|
101
|
+
raise SessionParticipantError(
|
|
102
|
+
f"Session at capacity ({self.config.max_participants})"
|
|
103
|
+
)
|
|
104
|
+
if eff_score < self.config.min_eff_score and ring != ExecutionRing.RING_3_SANDBOX:
|
|
105
|
+
raise SessionParticipantError(
|
|
106
|
+
f"eff_score {eff_score:.2f} below minimum {self.config.min_eff_score:.2f}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
participant = SessionParticipant(
|
|
110
|
+
agent_did=agent_did,
|
|
111
|
+
ring=ring,
|
|
112
|
+
sigma_raw=sigma_raw,
|
|
113
|
+
eff_score=eff_score,
|
|
114
|
+
)
|
|
115
|
+
self._participants[agent_did] = participant
|
|
116
|
+
return participant
|
|
117
|
+
|
|
118
|
+
def leave(self, agent_did: str) -> None:
|
|
119
|
+
"""Remove an agent from this session."""
|
|
120
|
+
if agent_did not in self._participants:
|
|
121
|
+
raise SessionParticipantError(f"Agent {agent_did} not in session")
|
|
122
|
+
self._participants[agent_did].is_active = False
|
|
123
|
+
|
|
124
|
+
def get_participant(self, agent_did: str) -> SessionParticipant:
|
|
125
|
+
"""Get a participant by DID."""
|
|
126
|
+
if agent_did not in self._participants:
|
|
127
|
+
raise SessionParticipantError(f"Agent {agent_did} not in session")
|
|
128
|
+
return self._participants[agent_did]
|
|
129
|
+
|
|
130
|
+
def update_ring(self, agent_did: str, new_ring: ExecutionRing) -> None:
|
|
131
|
+
"""Update an agent's ring level (escalation or demotion)."""
|
|
132
|
+
participant = self.get_participant(agent_did)
|
|
133
|
+
participant.ring = new_ring
|
|
134
|
+
|
|
135
|
+
def force_consistency_mode(self, mode: ConsistencyMode) -> None:
|
|
136
|
+
"""Force a consistency mode (e.g., when non-reversible actions are detected)."""
|
|
137
|
+
self.consistency_mode = mode
|
|
138
|
+
|
|
139
|
+
def terminate(self) -> None:
|
|
140
|
+
"""Begin session termination."""
|
|
141
|
+
self._assert_state(SessionState.ACTIVE, SessionState.HANDSHAKING)
|
|
142
|
+
self.state = SessionState.TERMINATING
|
|
143
|
+
self.terminated_at = datetime.now(UTC)
|
|
144
|
+
|
|
145
|
+
def archive(self) -> None:
|
|
146
|
+
"""Archive the session after audit commitment."""
|
|
147
|
+
self._assert_state(SessionState.TERMINATING)
|
|
148
|
+
self.state = SessionState.ARCHIVED
|
|
149
|
+
|
|
150
|
+
def create_vfs_snapshot(self, snapshot_id: str | None = None) -> str:
|
|
151
|
+
"""Create a VFS state snapshot for rollback.
|
|
152
|
+
|
|
153
|
+
Captures both VFS file state (snapshot) and participant metadata.
|
|
154
|
+
"""
|
|
155
|
+
self._assert_state(SessionState.ACTIVE)
|
|
156
|
+
# Snapshot the VFS file state
|
|
157
|
+
sid = self.vfs.create_snapshot(snapshot_id)
|
|
158
|
+
# Also snapshot participant metadata for full restore
|
|
159
|
+
self._vfs_snapshots[sid] = {
|
|
160
|
+
"created_at": datetime.now(UTC).isoformat(),
|
|
161
|
+
"participant_states": {
|
|
162
|
+
did: {"ring": p.ring.value, "eff_score": p.eff_score}
|
|
163
|
+
for did, p in self._participants.items()
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
return sid
|
|
167
|
+
|
|
168
|
+
def restore_vfs_snapshot(
|
|
169
|
+
self, snapshot_id: str, agent_did: str
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Restore VFS to a previous snapshot.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
snapshot_id: ID returned by create_vfs_snapshot.
|
|
175
|
+
agent_did: Agent requesting the restore (recorded in audit log).
|
|
176
|
+
"""
|
|
177
|
+
self._assert_state(SessionState.ACTIVE)
|
|
178
|
+
self.vfs.restore_snapshot(snapshot_id, agent_did)
|
|
179
|
+
|
|
180
|
+
def __repr__(self) -> str:
|
|
181
|
+
return (
|
|
182
|
+
f"SharedSessionObject(id={self.session_id!r}, "
|
|
183
|
+
f"state={self.state.value}, "
|
|
184
|
+
f"participants={self.participant_count}, "
|
|
185
|
+
f"mode={self.consistency_mode.value})"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SessionLifecycleError(Exception):
|
|
190
|
+
"""Raised when a session lifecycle transition is invalid."""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class SessionParticipantError(Exception):
|
|
194
|
+
"""Raised for participant-related errors."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""
|
|
5
|
+
Resource Locks — stub implementation.
|
|
6
|
+
|
|
7
|
+
Public Preview: locks are not enforced. All acquire calls succeed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LockIntent(str, Enum):
|
|
19
|
+
"""Types of lock intent."""
|
|
20
|
+
|
|
21
|
+
READ = "read"
|
|
22
|
+
WRITE = "write"
|
|
23
|
+
EXCLUSIVE = "exclusive"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class IntentLock:
|
|
28
|
+
"""A declared resource lock on a resource."""
|
|
29
|
+
|
|
30
|
+
lock_id: str = field(default_factory=lambda: f"lock:{uuid.uuid4().hex[:8]}")
|
|
31
|
+
agent_did: str = ""
|
|
32
|
+
session_id: str = ""
|
|
33
|
+
resource_path: str = ""
|
|
34
|
+
intent: LockIntent = LockIntent.READ
|
|
35
|
+
acquired_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
36
|
+
is_active: bool = True
|
|
37
|
+
saga_step_id: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LockContentionError(Exception):
|
|
41
|
+
"""Raised when lock contention is detected."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DeadlockError(Exception):
|
|
45
|
+
"""Raised when a deadlock is detected."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IntentLockManager:
|
|
49
|
+
"""
|
|
50
|
+
Resource lock stub (Public Preview: all locks succeed, no contention).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._locks: dict[str, IntentLock] = {}
|
|
55
|
+
|
|
56
|
+
def acquire(
|
|
57
|
+
self,
|
|
58
|
+
agent_did: str,
|
|
59
|
+
session_id: str,
|
|
60
|
+
resource_path: str,
|
|
61
|
+
intent: LockIntent,
|
|
62
|
+
saga_step_id: str | None = None,
|
|
63
|
+
) -> IntentLock:
|
|
64
|
+
"""Acquire a lock (Public Preview: always succeeds)."""
|
|
65
|
+
lock = IntentLock(
|
|
66
|
+
agent_did=agent_did,
|
|
67
|
+
session_id=session_id,
|
|
68
|
+
resource_path=resource_path,
|
|
69
|
+
intent=intent,
|
|
70
|
+
saga_step_id=saga_step_id,
|
|
71
|
+
)
|
|
72
|
+
self._locks[lock.lock_id] = lock
|
|
73
|
+
return lock
|
|
74
|
+
|
|
75
|
+
def release(self, lock_id: str) -> None:
|
|
76
|
+
"""Release a lock."""
|
|
77
|
+
lock = self._locks.get(lock_id)
|
|
78
|
+
if lock:
|
|
79
|
+
lock.is_active = False
|
|
80
|
+
|
|
81
|
+
def release_agent_locks(self, agent_did: str, session_id: str) -> int:
|
|
82
|
+
count = 0
|
|
83
|
+
for lock in list(self._locks.values()):
|
|
84
|
+
if lock.agent_did == agent_did and lock.session_id == session_id and lock.is_active:
|
|
85
|
+
lock.is_active = False
|
|
86
|
+
count += 1
|
|
87
|
+
return count
|
|
88
|
+
|
|
89
|
+
def release_session_locks(self, session_id: str) -> int:
|
|
90
|
+
count = 0
|
|
91
|
+
for lock in list(self._locks.values()):
|
|
92
|
+
if lock.session_id == session_id and lock.is_active:
|
|
93
|
+
lock.is_active = False
|
|
94
|
+
count += 1
|
|
95
|
+
return count
|
|
96
|
+
|
|
97
|
+
def get_agent_locks(self, agent_did: str, session_id: str) -> list[IntentLock]:
|
|
98
|
+
return [
|
|
99
|
+
lock for lock in self._locks.values()
|
|
100
|
+
if lock.agent_did == agent_did
|
|
101
|
+
and lock.session_id == session_id
|
|
102
|
+
and lock.is_active
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def get_resource_locks(self, resource_path: str) -> list[IntentLock]:
|
|
106
|
+
return [
|
|
107
|
+
lock for lock in self._locks.values()
|
|
108
|
+
if lock.resource_path == resource_path
|
|
109
|
+
and lock.is_active
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def active_lock_count(self) -> int:
|
|
114
|
+
return sum(1 for lock in self._locks.values() if lock.is_active)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def contention_points(self) -> list[str]:
|
|
118
|
+
return []
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
# Public Preview — basic implementation
|
|
4
|
+
"""
|
|
5
|
+
Session Isolation Levels — stub implementation.
|
|
6
|
+
|
|
7
|
+
Public Preview: all access is serialized via a single lock.
|
|
8
|
+
Isolation levels are retained for API compatibility but not enforced.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IsolationLevel(str, Enum):
|
|
17
|
+
"""Session isolation levels (Public Preview: not enforced)."""
|
|
18
|
+
|
|
19
|
+
SNAPSHOT = "snapshot"
|
|
20
|
+
READ_COMMITTED = "read_committed"
|
|
21
|
+
SERIALIZABLE = "serializable"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def requires_vector_clocks(self) -> bool:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def requires_intent_locks(self) -> bool:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def allows_concurrent_writes(self) -> bool:
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def coordination_cost(self) -> str:
|
|
37
|
+
return "none"
|