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.
Files changed (60) hide show
  1. agent_hypervisor-3.1.0.dist-info/METADATA +824 -0
  2. agent_hypervisor-3.1.0.dist-info/RECORD +60 -0
  3. agent_hypervisor-3.1.0.dist-info/WHEEL +4 -0
  4. agent_hypervisor-3.1.0.dist-info/entry_points.txt +2 -0
  5. agent_hypervisor-3.1.0.dist-info/licenses/LICENSE +21 -0
  6. hypervisor/__init__.py +160 -0
  7. hypervisor/api/__init__.py +7 -0
  8. hypervisor/api/models.py +285 -0
  9. hypervisor/api/server.py +742 -0
  10. hypervisor/audit/__init__.py +4 -0
  11. hypervisor/audit/commitment.py +76 -0
  12. hypervisor/audit/delta.py +135 -0
  13. hypervisor/audit/gc.py +99 -0
  14. hypervisor/cli/__init__.py +3 -0
  15. hypervisor/cli/formatters.py +99 -0
  16. hypervisor/cli/session_commands.py +200 -0
  17. hypervisor/constants.py +106 -0
  18. hypervisor/core.py +352 -0
  19. hypervisor/integrations/__init__.py +10 -0
  20. hypervisor/integrations/iatp_adapter.py +142 -0
  21. hypervisor/integrations/nexus_adapter.py +108 -0
  22. hypervisor/integrations/verification_adapter.py +122 -0
  23. hypervisor/liability/__init__.py +142 -0
  24. hypervisor/liability/attribution.py +86 -0
  25. hypervisor/liability/ledger.py +121 -0
  26. hypervisor/liability/quarantine.py +119 -0
  27. hypervisor/liability/slashing.py +80 -0
  28. hypervisor/liability/vouching.py +134 -0
  29. hypervisor/models.py +277 -0
  30. hypervisor/observability/__init__.py +27 -0
  31. hypervisor/observability/causal_trace.py +70 -0
  32. hypervisor/observability/event_bus.py +222 -0
  33. hypervisor/observability/prometheus_collector.py +248 -0
  34. hypervisor/observability/saga_span_exporter.py +341 -0
  35. hypervisor/providers.py +121 -0
  36. hypervisor/py.typed +0 -0
  37. hypervisor/reversibility/__init__.py +3 -0
  38. hypervisor/reversibility/registry.py +108 -0
  39. hypervisor/rings/__init__.py +21 -0
  40. hypervisor/rings/breach_detector.py +200 -0
  41. hypervisor/rings/classifier.py +78 -0
  42. hypervisor/rings/elevation.py +219 -0
  43. hypervisor/rings/enforcer.py +97 -0
  44. hypervisor/saga/__init__.py +22 -0
  45. hypervisor/saga/checkpoint.py +110 -0
  46. hypervisor/saga/dsl.py +190 -0
  47. hypervisor/saga/fan_out.py +126 -0
  48. hypervisor/saga/orchestrator.py +229 -0
  49. hypervisor/saga/schema.py +244 -0
  50. hypervisor/saga/state_machine.py +157 -0
  51. hypervisor/security/__init__.py +13 -0
  52. hypervisor/security/kill_switch.py +200 -0
  53. hypervisor/security/rate_limiter.py +190 -0
  54. hypervisor/session/__init__.py +194 -0
  55. hypervisor/session/intent_locks.py +118 -0
  56. hypervisor/session/isolation.py +37 -0
  57. hypervisor/session/sso.py +169 -0
  58. hypervisor/session/vector_clock.py +118 -0
  59. hypervisor/verification/__init__.py +3 -0
  60. hypervisor/verification/history.py +173 -0
@@ -0,0 +1,108 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """
4
+ Nexus Integration Stub — Trust Scoring for Ring Assignment.
5
+
6
+ Provides the interface for integrating an external trust/reputation
7
+ engine with the Hypervisor. Supply your own scorer implementation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from datetime import UTC, datetime
14
+ from typing import Any, Protocol
15
+
16
+
17
+ class NexusTrustScorer(Protocol):
18
+ """Protocol for a trust scoring backend."""
19
+
20
+ def calculate_trust_score(
21
+ self,
22
+ verification_level: str,
23
+ history: Any,
24
+ capabilities: dict | None = None,
25
+ privacy: dict | None = None,
26
+ ) -> Any: ...
27
+
28
+ def slash_reputation(
29
+ self,
30
+ agent_did: str,
31
+ reason: str,
32
+ severity: str,
33
+ evidence_hash: str | None = None,
34
+ trace_id: str | None = None,
35
+ broadcast: bool = True,
36
+ ) -> Any: ...
37
+
38
+ def record_task_outcome(
39
+ self,
40
+ agent_did: str,
41
+ outcome: str,
42
+ ) -> Any: ...
43
+
44
+
45
+ @dataclass
46
+ class NexusScoreResult:
47
+ """Result of a trust score lookup."""
48
+
49
+ agent_did: str
50
+ raw_nexus_score: int
51
+ normalized_sigma: float
52
+ tier: str
53
+ resolved_at: datetime = field(default_factory=lambda: datetime.now(UTC))
54
+
55
+
56
+ class NexusAdapter:
57
+ """Stub adapter for trust scoring integration.
58
+
59
+ Provides a default sigma of 0.50 when no scorer is configured.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ scorer: NexusTrustScorer | None = None,
65
+ cache_ttl_seconds: int = 300,
66
+ ) -> None:
67
+ self._scorer = scorer
68
+ self._cache: dict[str, NexusScoreResult] = {}
69
+ self._cache_ttl = cache_ttl_seconds
70
+
71
+ def resolve_sigma(
72
+ self,
73
+ agent_did: str,
74
+ verification_level: str = "standard",
75
+ history: Any | None = None,
76
+ capabilities: dict | None = None,
77
+ ) -> float:
78
+ """Resolve an agent's sigma. Returns 0.50 default when no scorer is configured."""
79
+ if self._scorer is None:
80
+ return 0.50
81
+ score = self._scorer.calculate_trust_score(
82
+ verification_level=verification_level,
83
+ history=history,
84
+ capabilities=capabilities,
85
+ )
86
+ raw_score = getattr(score, "total_score", 500)
87
+ return raw_score / 1000.0
88
+
89
+ def report_slash(
90
+ self,
91
+ agent_did: str,
92
+ reason: str,
93
+ severity: str = "medium",
94
+ evidence_hash: str | None = None,
95
+ ) -> None:
96
+ """Report a penalty event to the trust backend."""
97
+ if self._scorer:
98
+ self._scorer.slash_reputation(
99
+ agent_did=agent_did,
100
+ reason=reason,
101
+ severity=severity,
102
+ evidence_hash=evidence_hash,
103
+ )
104
+
105
+ def report_task_outcome(self, agent_did: str, outcome: str) -> None:
106
+ """Report a task outcome for reputation tracking."""
107
+ if self._scorer:
108
+ self._scorer.record_task_outcome(agent_did, outcome)
@@ -0,0 +1,122 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """
4
+ Verification Integration Stub — Behavioral Verification adapter.
5
+
6
+ Provides the interface for integrating a behavioral verification
7
+ system with the Hypervisor. Supply your own verifier implementation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable
13
+ from dataclasses import dataclass, field
14
+ from datetime import UTC, datetime
15
+ from enum import Enum
16
+ from typing import Any, Protocol
17
+
18
+
19
+ class VerificationBackend(Protocol):
20
+ """Protocol for a behavioral verification backend."""
21
+
22
+ def verify_embeddings(
23
+ self,
24
+ embedding_a: Any,
25
+ embedding_b: Any,
26
+ metric: str = "cosine",
27
+ weights: Any = None,
28
+ threshold_profile: str | None = None,
29
+ explain: bool = False,
30
+ ) -> Any: ...
31
+
32
+
33
+ class DriftSeverity(str, Enum):
34
+ NONE = "none"
35
+ LOW = "low"
36
+ MEDIUM = "medium"
37
+ HIGH = "high"
38
+ CRITICAL = "critical"
39
+
40
+
41
+ @dataclass
42
+ class DriftCheckResult:
43
+ """Result of a behavioral verification check."""
44
+
45
+ agent_did: str
46
+ session_id: str
47
+ drift_score: float
48
+ severity: DriftSeverity
49
+ passed: bool
50
+ explanation: str | None = None
51
+ action_id: str | None = None
52
+ checked_at: datetime = field(default_factory=lambda: datetime.now(UTC))
53
+
54
+ @property
55
+ def should_slash(self) -> bool:
56
+ return self.severity in (DriftSeverity.HIGH, DriftSeverity.CRITICAL)
57
+
58
+ @property
59
+ def should_demote(self) -> bool:
60
+ return self.severity == DriftSeverity.MEDIUM
61
+
62
+
63
+ @dataclass
64
+ class DriftThresholds:
65
+ low: float = 0.15
66
+ medium: float = 0.30
67
+ high: float = 0.50
68
+ critical: float = 0.75
69
+
70
+
71
+ class VerificationAdapter:
72
+ """Stub adapter for behavioral verification integration."""
73
+
74
+ def __init__(
75
+ self,
76
+ verifier: VerificationBackend | None = None,
77
+ thresholds: DriftThresholds | None = None,
78
+ on_drift_detected: Callable[[DriftCheckResult], None] | None = None,
79
+ ) -> None:
80
+ self._verifier = verifier
81
+ self.thresholds = thresholds or DriftThresholds()
82
+ self._on_drift_detected = on_drift_detected
83
+ self._check_history: list[DriftCheckResult] = []
84
+
85
+ def check_behavioral_drift(
86
+ self,
87
+ agent_did: str,
88
+ session_id: str,
89
+ claimed_embedding: Any,
90
+ observed_embedding: Any,
91
+ action_id: str | None = None,
92
+ metric: str = "cosine",
93
+ threshold_profile: str | None = None,
94
+ ) -> DriftCheckResult:
95
+ """Check for behavioral drift. Returns a pass-through result when no verifier is configured."""
96
+ result = DriftCheckResult(
97
+ agent_did=agent_did,
98
+ session_id=session_id,
99
+ drift_score=0.0,
100
+ severity=DriftSeverity.NONE,
101
+ passed=True,
102
+ action_id=action_id,
103
+ )
104
+ self._check_history.append(result)
105
+ return result
106
+
107
+ def get_agent_drift_history(self, agent_did: str, session_id: str | None = None) -> list[DriftCheckResult]:
108
+ return [r for r in self._check_history if r.agent_did == agent_did and (session_id is None or r.session_id == session_id)]
109
+
110
+ def get_drift_rate(self, agent_did: str, session_id: str | None = None) -> float:
111
+ history = self.get_agent_drift_history(agent_did, session_id)
112
+ if not history:
113
+ return 0.0
114
+ return sum(1 for r in history if not r.passed) / len(history)
115
+
116
+ @property
117
+ def total_checks(self) -> int:
118
+ return len(self._check_history)
119
+
120
+ @property
121
+ def total_violations(self) -> int:
122
+ return sum(1 for r in self._check_history if not r.passed)
@@ -0,0 +1,142 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ # Public Preview — basic implementation
4
+ """
5
+ Liability Matrix — simple event log for sponsor→sponsored agent relationships.
6
+
7
+ Public Preview: graph operations are retained for API compatibility
8
+ but sponsorship/penalty/quarantine are stubs.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+
17
+ @dataclass
18
+ class LiabilityEdge:
19
+ """An edge in the liability graph."""
20
+
21
+ voucher_did: str
22
+ vouchee_did: str
23
+ bonded_amount: float
24
+ vouch_id: str
25
+
26
+
27
+ class LiabilityMatrix:
28
+ """
29
+ Directed graph tracking sponsor→sponsored agent bonds within a session.
30
+
31
+ Provides query APIs for exposure analysis and cascade detection.
32
+ """
33
+
34
+ def __init__(self, session_id: str) -> None:
35
+ self.session_id = session_id
36
+ self._edges: list[LiabilityEdge] = []
37
+
38
+ def add_edge(
39
+ self,
40
+ voucher_did: str,
41
+ vouchee_did: str,
42
+ bonded_amount: float,
43
+ vouch_id: str,
44
+ ) -> LiabilityEdge:
45
+ """Record a sponsorship relationship."""
46
+ edge = LiabilityEdge(
47
+ voucher_did=voucher_did,
48
+ vouchee_did=vouchee_did,
49
+ bonded_amount=bonded_amount,
50
+ vouch_id=vouch_id,
51
+ )
52
+ self._edges.append(edge)
53
+ return edge
54
+
55
+ def remove_edge(self, vouch_id: str) -> None:
56
+ """Remove a sponsorship relationship by sponsor ID."""
57
+ self._edges = [e for e in self._edges if e.vouch_id != vouch_id]
58
+
59
+ def who_vouches_for(self, agent_did: str) -> list[LiabilityEdge]:
60
+ """Get all sponsors for a given agent."""
61
+ return [e for e in self._edges if e.vouchee_did == agent_did]
62
+
63
+ def who_is_vouched_by(self, agent_did: str) -> list[LiabilityEdge]:
64
+ """Get all sponsored agents of a given sponsor."""
65
+ return [e for e in self._edges if e.voucher_did == agent_did]
66
+
67
+ def total_exposure(self, voucher_did: str) -> float:
68
+ """Total σ bonded by a sponsor across all sponsored agents."""
69
+ return sum(e.bonded_amount for e in self._edges if e.voucher_did == voucher_did)
70
+
71
+ def cascade_path(self, agent_did: str, max_depth: int = 2) -> list[list[str]]:
72
+ """
73
+ Find cascade paths from an agent through the liability graph.
74
+
75
+ Returns all paths where penalty agent_did would cascade to others.
76
+ """
77
+ paths: list[list[str]] = []
78
+ self._dfs_cascade(agent_did, [agent_did], paths, max_depth)
79
+ return paths
80
+
81
+ def has_cycle(self) -> bool:
82
+ """Check if the liability graph contains any cycles."""
83
+ all_nodes = set()
84
+ for e in self._edges:
85
+ all_nodes.add(e.voucher_did)
86
+ all_nodes.add(e.vouchee_did)
87
+
88
+ visited: set[str] = set()
89
+ in_stack: set[str] = set()
90
+
91
+ for node in all_nodes:
92
+ if node not in visited:
93
+ if self._dfs_cycle(node, visited, in_stack):
94
+ return True
95
+ return False
96
+
97
+ def clear(self) -> None:
98
+ """Release all bonds (session termination)."""
99
+ self._edges.clear()
100
+
101
+ @property
102
+ def edges(self) -> list[LiabilityEdge]:
103
+ return list(self._edges)
104
+
105
+ def _dfs_cascade(
106
+ self,
107
+ current: str,
108
+ path: list[str],
109
+ paths: list[list[str]],
110
+ max_depth: int,
111
+ ) -> None:
112
+ if len(path) > max_depth + 1:
113
+ return
114
+ vouchees = self.who_is_vouched_by(current)
115
+ if not vouchees and len(path) > 1:
116
+ paths.append(list(path))
117
+ return
118
+ for edge in vouchees:
119
+ if edge.vouchee_did not in path:
120
+ path.append(edge.vouchee_did)
121
+ self._dfs_cascade(edge.vouchee_did, path, paths, max_depth)
122
+ path.pop()
123
+ if not vouchees:
124
+ return
125
+ if len(path) > 1:
126
+ paths.append(list(path))
127
+
128
+ def _dfs_cycle(
129
+ self, node: str, visited: set[str], in_stack: set[str]
130
+ ) -> bool:
131
+ visited.add(node)
132
+ in_stack.add(node)
133
+ for edge in self._edges:
134
+ if edge.voucher_did == node:
135
+ neighbor = edge.vouchee_did
136
+ if neighbor in in_stack:
137
+ return True
138
+ if neighbor not in visited:
139
+ if self._dfs_cycle(neighbor, visited, in_stack):
140
+ return True
141
+ in_stack.discard(node)
142
+ return False
@@ -0,0 +1,86 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ # Public Preview — basic implementation
4
+ """
5
+ Fault Logging — stub implementation.
6
+
7
+ Public Preview: assigns full liability to the direct-cause agent.
8
+ No causal chain analysis.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import UTC, datetime
16
+
17
+
18
+ @dataclass
19
+ class FaultAttribution:
20
+ """Fault attribution for an agent."""
21
+
22
+ agent_did: str
23
+ liability_score: float
24
+ causal_contribution: float
25
+ is_direct_cause: bool = False
26
+ reason: str = ""
27
+
28
+
29
+ @dataclass
30
+ class AttributionResult:
31
+ """Attribution result for a saga failure."""
32
+
33
+ attribution_id: str = field(default_factory=lambda: f"attr:{uuid.uuid4().hex[:8]}")
34
+ saga_id: str = ""
35
+ session_id: str = ""
36
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
37
+ attributions: list[FaultAttribution] = field(default_factory=list)
38
+ causal_chain_length: int = 0
39
+ root_cause_agent: str | None = None
40
+
41
+ @property
42
+ def agents_involved(self) -> list[str]:
43
+ return [a.agent_did for a in self.attributions]
44
+
45
+ def get_liability(self, agent_did: str) -> float:
46
+ for a in self.attributions:
47
+ if a.agent_did == agent_did:
48
+ return a.liability_score
49
+ return 0.0
50
+
51
+
52
+ class CausalAttributor:
53
+ """Simple fault attribution — assigns liability to the direct cause agent."""
54
+
55
+ def __init__(self) -> None:
56
+ self._history: list[AttributionResult] = []
57
+
58
+ def attribute(
59
+ self,
60
+ saga_id: str,
61
+ session_id: str,
62
+ agent_actions: dict[str, list[dict]],
63
+ failure_step_id: str,
64
+ failure_agent_did: str,
65
+ risk_weights: dict[str, float] | None = None,
66
+ ) -> AttributionResult:
67
+ """Assign full liability to the direct-cause agent."""
68
+ attributions = []
69
+ for agent_did in agent_actions:
70
+ attributions.append(FaultAttribution(
71
+ agent_did=agent_did,
72
+ liability_score=1.0 if agent_did == failure_agent_did else 0.0,
73
+ causal_contribution=1.0 if agent_did == failure_agent_did else 0.0,
74
+ is_direct_cause=(agent_did == failure_agent_did),
75
+ reason="Direct cause" if agent_did == failure_agent_did else "",
76
+ ))
77
+ result = AttributionResult(
78
+ saga_id=saga_id, session_id=session_id,
79
+ attributions=attributions, root_cause_agent=failure_agent_did,
80
+ )
81
+ self._history.append(result)
82
+ return result
83
+
84
+ @property
85
+ def attribution_history(self) -> list[AttributionResult]:
86
+ return list(self._history)
@@ -0,0 +1,121 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ # Public Preview — basic implementation
4
+ """
5
+ Liability Ledger — simple append-only fault log.
6
+
7
+ Public Preview: records fault events as (agent, type, timestamp, details).
8
+ No risk scoring, no admission decisions.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import UTC, datetime
16
+ from enum import Enum
17
+
18
+
19
+ class LedgerEntryType(str, Enum):
20
+ """Types of liability ledger entries."""
21
+
22
+ VOUCH_GIVEN = "vouch_given"
23
+ VOUCH_RECEIVED = "vouch_received"
24
+ VOUCH_RELEASED = "vouch_released"
25
+ SLASH_RECEIVED = "slash_received"
26
+ SLASH_CASCADED = "slash_cascaded"
27
+ QUARANTINE_ENTERED = "quarantine_entered"
28
+ QUARANTINE_RELEASED = "quarantine_released"
29
+ FAULT_ATTRIBUTED = "fault_attributed"
30
+ CLEAN_SESSION = "clean_session"
31
+
32
+
33
+ @dataclass
34
+ class LedgerEntry:
35
+ """A single entry in the liability ledger."""
36
+
37
+ entry_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
38
+ agent_did: str = ""
39
+ entry_type: LedgerEntryType = LedgerEntryType.CLEAN_SESSION
40
+ session_id: str = ""
41
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
42
+ severity: float = 0.0
43
+ details: str = ""
44
+ related_agent: str | None = None
45
+
46
+
47
+ @dataclass
48
+ class AgentRiskProfile:
49
+ """Risk profile for an agent (Public Preview: always admits)."""
50
+
51
+ agent_did: str
52
+ total_entries: int = 0
53
+ slash_count: int = 0
54
+ quarantine_count: int = 0
55
+ clean_session_count: int = 0
56
+ fault_score_avg: float = 0.0
57
+ risk_score: float = 0.0
58
+ recommendation: str = "admit"
59
+
60
+
61
+ class LiabilityLedger:
62
+ """
63
+ Simple append-only fault log.
64
+
65
+ Public Preview: records events for audit trail only.
66
+ No risk scoring or admission logic.
67
+ """
68
+
69
+ PROBATION_THRESHOLD = 0.3
70
+ DENY_THRESHOLD = 0.6
71
+
72
+ def __init__(self) -> None:
73
+ self._entries: list[LedgerEntry] = []
74
+ self._by_agent: dict[str, list[LedgerEntry]] = {}
75
+
76
+ def record(
77
+ self,
78
+ agent_did: str,
79
+ entry_type: LedgerEntryType,
80
+ session_id: str = "",
81
+ severity: float = 0.0,
82
+ details: str = "",
83
+ related_agent: str | None = None,
84
+ ) -> LedgerEntry:
85
+ """Record a liability event."""
86
+ entry = LedgerEntry(
87
+ agent_did=agent_did,
88
+ entry_type=entry_type,
89
+ session_id=session_id,
90
+ severity=severity,
91
+ details=details,
92
+ related_agent=related_agent,
93
+ )
94
+ self._entries.append(entry)
95
+ self._by_agent.setdefault(agent_did, []).append(entry)
96
+ return entry
97
+
98
+ def get_agent_history(self, agent_did: str) -> list[LedgerEntry]:
99
+ """Get all ledger entries for an agent."""
100
+ return list(self._by_agent.get(agent_did, []))
101
+
102
+ def compute_risk_profile(self, agent_did: str) -> AgentRiskProfile:
103
+ """Return a basic risk profile (Public Preview: always admits)."""
104
+ entries = self.get_agent_history(agent_did)
105
+ return AgentRiskProfile(
106
+ agent_did=agent_did,
107
+ total_entries=len(entries),
108
+ recommendation="admit",
109
+ )
110
+
111
+ def should_admit(self, agent_did: str) -> tuple[bool, str]:
112
+ """Always admits in Public Preview."""
113
+ return True, "admit"
114
+
115
+ @property
116
+ def total_entries(self) -> int:
117
+ return len(self._entries)
118
+
119
+ @property
120
+ def tracked_agents(self) -> list[str]:
121
+ return list(self._by_agent.keys())
@@ -0,0 +1,119 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ # Public Preview — basic implementation
4
+ """
5
+ Quarantine Manager — stub implementation.
6
+
7
+ Public Preview: quarantine is not enforced. Calls return safe defaults.
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 QuarantineReason(str, Enum):
19
+ """Why an agent was quarantined."""
20
+
21
+ BEHAVIORAL_DRIFT = "behavioral_drift"
22
+ LIABILITY_VIOLATION = "liability_violation"
23
+ RING_BREACH = "ring_breach"
24
+ RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
25
+ MANUAL = "manual"
26
+ CASCADE_SLASH = "cascade_slash"
27
+
28
+
29
+ @dataclass
30
+ class QuarantineRecord:
31
+ """Record of an agent in quarantine."""
32
+
33
+ quarantine_id: str = field(default_factory=lambda: f"quar:{uuid.uuid4().hex[:8]}")
34
+ agent_did: str = ""
35
+ session_id: str = ""
36
+ reason: QuarantineReason = QuarantineReason.MANUAL
37
+ details: str = ""
38
+ entered_at: datetime = field(default_factory=lambda: datetime.now(UTC))
39
+ expires_at: datetime | None = None
40
+ released_at: datetime | None = None
41
+ is_active: bool = True
42
+ forensic_data: dict = field(default_factory=dict)
43
+
44
+ @property
45
+ def is_expired(self) -> bool:
46
+ if self.expires_at is None:
47
+ return False
48
+ return datetime.now(UTC) > self.expires_at
49
+
50
+ @property
51
+ def duration_seconds(self) -> float:
52
+ end = self.released_at or datetime.now(UTC)
53
+ return (end - self.entered_at).total_seconds()
54
+
55
+
56
+ class QuarantineManager:
57
+ """
58
+ Quarantine stub (Public Preview: no quarantine enforcement).
59
+ """
60
+
61
+ DEFAULT_QUARANTINE_SECONDS = 300
62
+
63
+ def __init__(self) -> None:
64
+ self._quarantines: dict[str, QuarantineRecord] = {}
65
+
66
+ def quarantine(
67
+ self,
68
+ agent_did: str,
69
+ session_id: str,
70
+ reason: QuarantineReason,
71
+ details: str = "",
72
+ duration_seconds: int | None = None,
73
+ forensic_data: dict | None = None,
74
+ ) -> QuarantineRecord:
75
+ """Log a quarantine request (Public Preview: no enforcement)."""
76
+ record = QuarantineRecord(
77
+ agent_did=agent_did,
78
+ session_id=session_id,
79
+ reason=reason,
80
+ details=details,
81
+ is_active=False,
82
+ )
83
+ self._quarantines[record.quarantine_id] = record
84
+ return record
85
+
86
+ def release(self, agent_did: str, session_id: str) -> QuarantineRecord | None:
87
+ """No-op in Public Preview."""
88
+ return None
89
+
90
+ def is_quarantined(self, agent_did: str, session_id: str) -> bool:
91
+ """Always False in Public Preview."""
92
+ return False
93
+
94
+ def get_active_quarantine(
95
+ self, agent_did: str, session_id: str
96
+ ) -> QuarantineRecord | None:
97
+ return None
98
+
99
+ def tick(self) -> list[QuarantineRecord]:
100
+ return []
101
+
102
+ def get_history(
103
+ self, agent_did: str | None = None, session_id: str | None = None
104
+ ) -> list[QuarantineRecord]:
105
+ """Get quarantine history, optionally filtered."""
106
+ records = list(self._quarantines.values())
107
+ if agent_did:
108
+ records = [r for r in records if r.agent_did == agent_did]
109
+ if session_id:
110
+ records = [r for r in records if r.session_id == session_id]
111
+ return records
112
+
113
+ @property
114
+ def active_quarantines(self) -> list[QuarantineRecord]:
115
+ return []
116
+
117
+ @property
118
+ def quarantine_count(self) -> int:
119
+ return 0