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,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
|