agentmesh-platform 1.0.0a1__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.
- agentmesh/__init__.py +119 -0
- agentmesh/cli/__init__.py +10 -0
- agentmesh/cli/main.py +405 -0
- agentmesh/governance/__init__.py +26 -0
- agentmesh/governance/audit.py +381 -0
- agentmesh/governance/compliance.py +447 -0
- agentmesh/governance/policy.py +385 -0
- agentmesh/governance/shadow.py +266 -0
- agentmesh/identity/__init__.py +30 -0
- agentmesh/identity/agent_id.py +319 -0
- agentmesh/identity/credentials.py +323 -0
- agentmesh/identity/delegation.py +281 -0
- agentmesh/identity/risk.py +279 -0
- agentmesh/identity/spiffe.py +230 -0
- agentmesh/identity/sponsor.py +178 -0
- agentmesh/reward/__init__.py +19 -0
- agentmesh/reward/engine.py +454 -0
- agentmesh/reward/learning.py +287 -0
- agentmesh/reward/scoring.py +203 -0
- agentmesh/trust/__init__.py +19 -0
- agentmesh/trust/bridge.py +386 -0
- agentmesh/trust/capability.py +293 -0
- agentmesh/trust/handshake.py +334 -0
- agentmesh_platform-1.0.0a1.dist-info/METADATA +332 -0
- agentmesh_platform-1.0.0a1.dist-info/RECORD +28 -0
- agentmesh_platform-1.0.0a1.dist-info/WHEEL +4 -0
- agentmesh_platform-1.0.0a1.dist-info/entry_points.txt +2 -0
- agentmesh_platform-1.0.0a1.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delegation Chains
|
|
3
|
+
|
|
4
|
+
Cryptographic delegation chains that ensure sub-agents can never
|
|
5
|
+
have more capabilities than their parent. Scope always narrows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DelegationLink(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
A single link in a delegation chain.
|
|
18
|
+
|
|
19
|
+
Each link represents a parent granting capabilities to a child.
|
|
20
|
+
The child's capabilities MUST be a subset of the parent's.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
link_id: str = Field(..., description="Unique link identifier")
|
|
24
|
+
|
|
25
|
+
# Chain position
|
|
26
|
+
depth: int = Field(..., ge=0, description="Depth in chain (0 = root)")
|
|
27
|
+
|
|
28
|
+
# Agents
|
|
29
|
+
parent_did: str = Field(..., description="DID of parent agent")
|
|
30
|
+
child_did: str = Field(..., description="DID of child agent")
|
|
31
|
+
|
|
32
|
+
# Capability narrowing
|
|
33
|
+
parent_capabilities: list[str] = Field(..., description="Parent's capabilities at delegation time")
|
|
34
|
+
delegated_capabilities: list[str] = Field(..., description="Capabilities granted to child")
|
|
35
|
+
|
|
36
|
+
# Timestamps
|
|
37
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
38
|
+
expires_at: Optional[datetime] = Field(None)
|
|
39
|
+
|
|
40
|
+
# Cryptographic binding
|
|
41
|
+
parent_signature: str = Field(..., description="Parent's signature on this delegation")
|
|
42
|
+
link_hash: str = Field(..., description="Hash of this link for chain verification")
|
|
43
|
+
previous_link_hash: Optional[str] = Field(None, description="Hash of previous link in chain")
|
|
44
|
+
|
|
45
|
+
def verify_capability_narrowing(self) -> bool:
|
|
46
|
+
"""Verify that delegated capabilities are a subset of parent's."""
|
|
47
|
+
for cap in self.delegated_capabilities:
|
|
48
|
+
if cap not in self.parent_capabilities:
|
|
49
|
+
# Check for wildcard narrowing (e.g., read:* -> read:data)
|
|
50
|
+
if not self._is_narrower_capability(cap, self.parent_capabilities):
|
|
51
|
+
return False
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def _is_narrower_capability(self, cap: str, parent_caps: list[str]) -> bool:
|
|
55
|
+
"""Check if a capability is a narrowed version of a parent capability."""
|
|
56
|
+
for parent_cap in parent_caps:
|
|
57
|
+
if parent_cap == "*":
|
|
58
|
+
return True
|
|
59
|
+
if parent_cap.endswith(":*"):
|
|
60
|
+
prefix = parent_cap[:-2]
|
|
61
|
+
if cap.startswith(prefix + ":"):
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def compute_hash(self) -> str:
|
|
66
|
+
"""Compute hash of this link for chain verification."""
|
|
67
|
+
data = {
|
|
68
|
+
"link_id": self.link_id,
|
|
69
|
+
"depth": self.depth,
|
|
70
|
+
"parent_did": self.parent_did,
|
|
71
|
+
"child_did": self.child_did,
|
|
72
|
+
"delegated_capabilities": sorted(self.delegated_capabilities),
|
|
73
|
+
"created_at": self.created_at.isoformat(),
|
|
74
|
+
"previous_link_hash": self.previous_link_hash,
|
|
75
|
+
}
|
|
76
|
+
canonical = json.dumps(data, sort_keys=True)
|
|
77
|
+
return hashlib.sha256(canonical.encode()).hexdigest()
|
|
78
|
+
|
|
79
|
+
def is_valid(self) -> bool:
|
|
80
|
+
"""Check if this link is valid."""
|
|
81
|
+
# Check expiration
|
|
82
|
+
if self.expires_at and datetime.utcnow() > self.expires_at:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# Verify capability narrowing
|
|
86
|
+
if not self.verify_capability_narrowing():
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# Verify hash
|
|
90
|
+
if self.link_hash != self.compute_hash():
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DelegationChain(BaseModel):
|
|
97
|
+
"""
|
|
98
|
+
Complete delegation chain from root sponsor to current agent.
|
|
99
|
+
|
|
100
|
+
Properties:
|
|
101
|
+
- Immutable once created
|
|
102
|
+
- Each link narrows capabilities
|
|
103
|
+
- Cryptographically verifiable
|
|
104
|
+
- Traceable to human sponsor
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
chain_id: str = Field(..., description="Unique chain identifier")
|
|
108
|
+
|
|
109
|
+
# Root (human sponsor)
|
|
110
|
+
root_sponsor_email: str = Field(..., description="Human sponsor at chain root")
|
|
111
|
+
root_sponsor_verified: bool = Field(default=False)
|
|
112
|
+
root_capabilities: list[str] = Field(..., description="Capabilities granted by sponsor")
|
|
113
|
+
|
|
114
|
+
# Chain links
|
|
115
|
+
links: list[DelegationLink] = Field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
# Final agent
|
|
118
|
+
leaf_did: str = Field(..., description="DID of the agent at end of chain")
|
|
119
|
+
leaf_capabilities: list[str] = Field(..., description="Final effective capabilities")
|
|
120
|
+
|
|
121
|
+
# Chain metadata
|
|
122
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
123
|
+
total_depth: int = Field(default=0)
|
|
124
|
+
|
|
125
|
+
# Verification
|
|
126
|
+
chain_hash: str = Field(default="", description="Hash of entire chain")
|
|
127
|
+
|
|
128
|
+
def add_link(self, link: DelegationLink) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Add a link to the chain.
|
|
131
|
+
|
|
132
|
+
Validates that:
|
|
133
|
+
1. Link connects to current leaf
|
|
134
|
+
2. Capabilities are properly narrowed
|
|
135
|
+
3. Link hash is correct
|
|
136
|
+
"""
|
|
137
|
+
if self.links:
|
|
138
|
+
last_link = self.links[-1]
|
|
139
|
+
if link.parent_did != last_link.child_did:
|
|
140
|
+
raise ValueError("Link does not connect to chain")
|
|
141
|
+
if link.previous_link_hash != last_link.link_hash:
|
|
142
|
+
raise ValueError("Link hash does not match previous link")
|
|
143
|
+
else:
|
|
144
|
+
# First link - parent should be root
|
|
145
|
+
if link.depth != 0:
|
|
146
|
+
raise ValueError("First link must have depth 0")
|
|
147
|
+
|
|
148
|
+
# Verify capability narrowing
|
|
149
|
+
if not link.verify_capability_narrowing():
|
|
150
|
+
raise ValueError("Link does not properly narrow capabilities")
|
|
151
|
+
|
|
152
|
+
self.links.append(link)
|
|
153
|
+
self.total_depth = len(self.links)
|
|
154
|
+
self.leaf_did = link.child_did
|
|
155
|
+
self.leaf_capabilities = link.delegated_capabilities
|
|
156
|
+
self._update_chain_hash()
|
|
157
|
+
|
|
158
|
+
def verify(self) -> tuple[bool, Optional[str]]:
|
|
159
|
+
"""
|
|
160
|
+
Verify the entire chain.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Tuple of (is_valid, error_message)
|
|
164
|
+
"""
|
|
165
|
+
if not self.links:
|
|
166
|
+
return True, None
|
|
167
|
+
|
|
168
|
+
previous_hash = None
|
|
169
|
+
previous_capabilities = self.root_capabilities
|
|
170
|
+
|
|
171
|
+
for i, link in enumerate(self.links):
|
|
172
|
+
# Verify depth
|
|
173
|
+
if link.depth != i:
|
|
174
|
+
return False, f"Invalid depth at link {i}"
|
|
175
|
+
|
|
176
|
+
# Verify hash chain
|
|
177
|
+
if link.previous_link_hash != previous_hash:
|
|
178
|
+
return False, f"Hash chain broken at link {i}"
|
|
179
|
+
|
|
180
|
+
# Verify capability narrowing against actual previous capabilities
|
|
181
|
+
for cap in link.delegated_capabilities:
|
|
182
|
+
if cap not in previous_capabilities:
|
|
183
|
+
if not link._is_narrower_capability(cap, previous_capabilities):
|
|
184
|
+
return False, f"Capability escalation at link {i}: {cap}"
|
|
185
|
+
|
|
186
|
+
# Verify link hash
|
|
187
|
+
if link.link_hash != link.compute_hash():
|
|
188
|
+
return False, f"Invalid link hash at link {i}"
|
|
189
|
+
|
|
190
|
+
previous_hash = link.link_hash
|
|
191
|
+
previous_capabilities = link.delegated_capabilities
|
|
192
|
+
|
|
193
|
+
return True, None
|
|
194
|
+
|
|
195
|
+
def get_effective_capabilities(self) -> list[str]:
|
|
196
|
+
"""Get the effective capabilities at the end of the chain."""
|
|
197
|
+
if self.links:
|
|
198
|
+
return self.links[-1].delegated_capabilities
|
|
199
|
+
return self.root_capabilities
|
|
200
|
+
|
|
201
|
+
def trace_capability(self, capability: str) -> list[dict]:
|
|
202
|
+
"""
|
|
203
|
+
Trace how a capability was granted through the chain.
|
|
204
|
+
|
|
205
|
+
Returns list of grants/narrowings from root to leaf.
|
|
206
|
+
"""
|
|
207
|
+
trace = []
|
|
208
|
+
|
|
209
|
+
# Check root
|
|
210
|
+
if capability in self.root_capabilities or "*" in self.root_capabilities:
|
|
211
|
+
trace.append({
|
|
212
|
+
"level": "root",
|
|
213
|
+
"grantor": self.root_sponsor_email,
|
|
214
|
+
"capability": capability,
|
|
215
|
+
"source_capabilities": self.root_capabilities,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
# Check each link
|
|
219
|
+
for link in self.links:
|
|
220
|
+
if capability in link.delegated_capabilities:
|
|
221
|
+
trace.append({
|
|
222
|
+
"level": f"depth_{link.depth}",
|
|
223
|
+
"grantor": link.parent_did,
|
|
224
|
+
"grantee": link.child_did,
|
|
225
|
+
"capability": capability,
|
|
226
|
+
"parent_capabilities": link.parent_capabilities,
|
|
227
|
+
"delegated_capabilities": link.delegated_capabilities,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
return trace
|
|
231
|
+
|
|
232
|
+
def _update_chain_hash(self) -> None:
|
|
233
|
+
"""Update the overall chain hash."""
|
|
234
|
+
data = {
|
|
235
|
+
"chain_id": self.chain_id,
|
|
236
|
+
"root_sponsor": self.root_sponsor_email,
|
|
237
|
+
"links": [link.link_hash for link in self.links],
|
|
238
|
+
}
|
|
239
|
+
canonical = json.dumps(data, sort_keys=True)
|
|
240
|
+
self.chain_hash = hashlib.sha256(canonical.encode()).hexdigest()
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def create_root(
|
|
244
|
+
cls,
|
|
245
|
+
sponsor_email: str,
|
|
246
|
+
root_agent_did: str,
|
|
247
|
+
capabilities: list[str],
|
|
248
|
+
sponsor_verified: bool = False,
|
|
249
|
+
) -> tuple["DelegationChain", DelegationLink]:
|
|
250
|
+
"""
|
|
251
|
+
Create a new chain with a root sponsor.
|
|
252
|
+
|
|
253
|
+
Returns the chain and the first link to be signed.
|
|
254
|
+
"""
|
|
255
|
+
import uuid
|
|
256
|
+
|
|
257
|
+
chain_id = f"chain_{uuid.uuid4().hex[:16]}"
|
|
258
|
+
|
|
259
|
+
chain = cls(
|
|
260
|
+
chain_id=chain_id,
|
|
261
|
+
root_sponsor_email=sponsor_email,
|
|
262
|
+
root_sponsor_verified=sponsor_verified,
|
|
263
|
+
root_capabilities=capabilities,
|
|
264
|
+
leaf_did=root_agent_did,
|
|
265
|
+
leaf_capabilities=capabilities,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Create first link (sponsor -> root agent)
|
|
269
|
+
link = DelegationLink(
|
|
270
|
+
link_id=f"link_{uuid.uuid4().hex[:12]}",
|
|
271
|
+
depth=0,
|
|
272
|
+
parent_did=f"did:mesh:sponsor:{sponsor_email}",
|
|
273
|
+
child_did=root_agent_did,
|
|
274
|
+
parent_capabilities=capabilities,
|
|
275
|
+
delegated_capabilities=capabilities,
|
|
276
|
+
parent_signature="", # To be signed
|
|
277
|
+
link_hash="", # To be computed after signing
|
|
278
|
+
)
|
|
279
|
+
link.link_hash = link.compute_hash()
|
|
280
|
+
|
|
281
|
+
return chain, link
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Continuous Risk Scoring
|
|
3
|
+
|
|
4
|
+
Updates trust score every ≤30s based on agent behavior.
|
|
5
|
+
Score visible in dashboard; configurable alert thresholds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Optional, Literal
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class RiskSignal:
|
|
16
|
+
"""A single risk signal contributing to the score."""
|
|
17
|
+
|
|
18
|
+
signal_type: str
|
|
19
|
+
severity: Literal["critical", "high", "medium", "low", "info"]
|
|
20
|
+
value: float # 0.0 to 1.0
|
|
21
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
22
|
+
source: Optional[str] = None
|
|
23
|
+
details: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def weight(self) -> float:
|
|
27
|
+
"""Get weight based on severity."""
|
|
28
|
+
weights = {
|
|
29
|
+
"critical": 1.0,
|
|
30
|
+
"high": 0.75,
|
|
31
|
+
"medium": 0.5,
|
|
32
|
+
"low": 0.25,
|
|
33
|
+
"info": 0.1,
|
|
34
|
+
}
|
|
35
|
+
return weights.get(self.severity, 0.1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RiskScore(BaseModel):
|
|
39
|
+
"""
|
|
40
|
+
Comprehensive risk score for an agent.
|
|
41
|
+
|
|
42
|
+
Score ranges from 0 (highest risk) to 1000 (lowest risk).
|
|
43
|
+
Inverted from trust score for clarity: lower = more risky.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
agent_did: str
|
|
47
|
+
|
|
48
|
+
# Overall score (0-1000, higher = safer)
|
|
49
|
+
total_score: int = Field(default=500, ge=0, le=1000)
|
|
50
|
+
risk_level: Literal["critical", "high", "medium", "low", "minimal"] = "medium"
|
|
51
|
+
|
|
52
|
+
# Component scores (0-100 each)
|
|
53
|
+
identity_score: int = Field(default=50, ge=0, le=100)
|
|
54
|
+
behavior_score: int = Field(default=50, ge=0, le=100)
|
|
55
|
+
network_score: int = Field(default=50, ge=0, le=100)
|
|
56
|
+
compliance_score: int = Field(default=50, ge=0, le=100)
|
|
57
|
+
|
|
58
|
+
# Signals
|
|
59
|
+
active_signals: int = Field(default=0)
|
|
60
|
+
critical_signals: int = Field(default=0)
|
|
61
|
+
|
|
62
|
+
# Timestamps
|
|
63
|
+
calculated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
64
|
+
next_update_at: datetime = Field(default_factory=datetime.utcnow)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def get_risk_level(cls, score: int) -> str:
|
|
68
|
+
"""Convert score to risk level."""
|
|
69
|
+
if score >= 800:
|
|
70
|
+
return "minimal"
|
|
71
|
+
elif score >= 600:
|
|
72
|
+
return "low"
|
|
73
|
+
elif score >= 400:
|
|
74
|
+
return "medium"
|
|
75
|
+
elif score >= 200:
|
|
76
|
+
return "high"
|
|
77
|
+
else:
|
|
78
|
+
return "critical"
|
|
79
|
+
|
|
80
|
+
def update(
|
|
81
|
+
self,
|
|
82
|
+
identity: int,
|
|
83
|
+
behavior: int,
|
|
84
|
+
network: int,
|
|
85
|
+
compliance: int,
|
|
86
|
+
active_signals: int = 0,
|
|
87
|
+
critical_signals: int = 0,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Update component scores and recalculate total."""
|
|
90
|
+
self.identity_score = max(0, min(100, identity))
|
|
91
|
+
self.behavior_score = max(0, min(100, behavior))
|
|
92
|
+
self.network_score = max(0, min(100, network))
|
|
93
|
+
self.compliance_score = max(0, min(100, compliance))
|
|
94
|
+
|
|
95
|
+
# Weighted total (behavior and compliance weighted higher)
|
|
96
|
+
self.total_score = int(
|
|
97
|
+
self.identity_score * 2 +
|
|
98
|
+
self.behavior_score * 3 +
|
|
99
|
+
self.network_score * 2 +
|
|
100
|
+
self.compliance_score * 3
|
|
101
|
+
) # Max: 1000
|
|
102
|
+
|
|
103
|
+
self.risk_level = self.get_risk_level(self.total_score)
|
|
104
|
+
self.active_signals = active_signals
|
|
105
|
+
self.critical_signals = critical_signals
|
|
106
|
+
self.calculated_at = datetime.utcnow()
|
|
107
|
+
self.next_update_at = datetime.utcnow() + timedelta(seconds=30)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class RiskScorer:
|
|
111
|
+
"""
|
|
112
|
+
Continuous risk scoring engine.
|
|
113
|
+
|
|
114
|
+
Updates trust score every ≤30s based on:
|
|
115
|
+
- Identity verification status
|
|
116
|
+
- Behavioral patterns
|
|
117
|
+
- Network activity
|
|
118
|
+
- Compliance violations
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
UPDATE_INTERVAL = 30 # seconds
|
|
122
|
+
|
|
123
|
+
# Alert thresholds
|
|
124
|
+
CRITICAL_THRESHOLD = 200
|
|
125
|
+
HIGH_THRESHOLD = 400
|
|
126
|
+
ALERT_THRESHOLD = 600
|
|
127
|
+
|
|
128
|
+
def __init__(self):
|
|
129
|
+
self._scores: dict[str, RiskScore] = {}
|
|
130
|
+
self._signals: dict[str, list[RiskSignal]] = {} # agent_did -> signals
|
|
131
|
+
self._alert_callbacks: list[callable] = []
|
|
132
|
+
|
|
133
|
+
def get_score(self, agent_did: str) -> RiskScore:
|
|
134
|
+
"""Get current risk score for an agent."""
|
|
135
|
+
if agent_did not in self._scores:
|
|
136
|
+
self._scores[agent_did] = RiskScore(agent_did=agent_did)
|
|
137
|
+
return self._scores[agent_did]
|
|
138
|
+
|
|
139
|
+
def add_signal(self, agent_did: str, signal: RiskSignal) -> None:
|
|
140
|
+
"""Add a risk signal for an agent."""
|
|
141
|
+
if agent_did not in self._signals:
|
|
142
|
+
self._signals[agent_did] = []
|
|
143
|
+
|
|
144
|
+
self._signals[agent_did].append(signal)
|
|
145
|
+
|
|
146
|
+
# Trigger immediate recalculation for critical signals
|
|
147
|
+
if signal.severity == "critical":
|
|
148
|
+
self.recalculate(agent_did)
|
|
149
|
+
|
|
150
|
+
def recalculate(self, agent_did: str) -> RiskScore:
|
|
151
|
+
"""
|
|
152
|
+
Recalculate risk score based on current signals.
|
|
153
|
+
|
|
154
|
+
This is called every ≤30s or immediately on critical signals.
|
|
155
|
+
"""
|
|
156
|
+
score = self.get_score(agent_did)
|
|
157
|
+
signals = self._signals.get(agent_did, [])
|
|
158
|
+
|
|
159
|
+
# Filter to recent signals (last 24 hours)
|
|
160
|
+
cutoff = datetime.utcnow() - timedelta(hours=24)
|
|
161
|
+
recent_signals = [s for s in signals if s.timestamp > cutoff]
|
|
162
|
+
|
|
163
|
+
# Calculate component scores
|
|
164
|
+
identity_score = self._calculate_identity_score(recent_signals)
|
|
165
|
+
behavior_score = self._calculate_behavior_score(recent_signals)
|
|
166
|
+
network_score = self._calculate_network_score(recent_signals)
|
|
167
|
+
compliance_score = self._calculate_compliance_score(recent_signals)
|
|
168
|
+
|
|
169
|
+
# Count signals
|
|
170
|
+
active = len(recent_signals)
|
|
171
|
+
critical = len([s for s in recent_signals if s.severity == "critical"])
|
|
172
|
+
|
|
173
|
+
# Update score
|
|
174
|
+
old_level = score.risk_level
|
|
175
|
+
score.update(
|
|
176
|
+
identity=identity_score,
|
|
177
|
+
behavior=behavior_score,
|
|
178
|
+
network=network_score,
|
|
179
|
+
compliance=compliance_score,
|
|
180
|
+
active_signals=active,
|
|
181
|
+
critical_signals=critical,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Check for alerts
|
|
185
|
+
self._check_alerts(agent_did, score, old_level)
|
|
186
|
+
|
|
187
|
+
return score
|
|
188
|
+
|
|
189
|
+
def _calculate_identity_score(self, signals: list[RiskSignal]) -> int:
|
|
190
|
+
"""Calculate identity component score."""
|
|
191
|
+
base = 80 # Start at 80
|
|
192
|
+
|
|
193
|
+
for signal in signals:
|
|
194
|
+
if signal.signal_type.startswith("identity."):
|
|
195
|
+
base -= int(signal.value * signal.weight * 20)
|
|
196
|
+
|
|
197
|
+
return max(0, min(100, base))
|
|
198
|
+
|
|
199
|
+
def _calculate_behavior_score(self, signals: list[RiskSignal]) -> int:
|
|
200
|
+
"""Calculate behavior component score."""
|
|
201
|
+
base = 70
|
|
202
|
+
|
|
203
|
+
for signal in signals:
|
|
204
|
+
if signal.signal_type.startswith("behavior."):
|
|
205
|
+
base -= int(signal.value * signal.weight * 25)
|
|
206
|
+
|
|
207
|
+
return max(0, min(100, base))
|
|
208
|
+
|
|
209
|
+
def _calculate_network_score(self, signals: list[RiskSignal]) -> int:
|
|
210
|
+
"""Calculate network component score."""
|
|
211
|
+
base = 75
|
|
212
|
+
|
|
213
|
+
for signal in signals:
|
|
214
|
+
if signal.signal_type.startswith("network."):
|
|
215
|
+
base -= int(signal.value * signal.weight * 20)
|
|
216
|
+
|
|
217
|
+
return max(0, min(100, base))
|
|
218
|
+
|
|
219
|
+
def _calculate_compliance_score(self, signals: list[RiskSignal]) -> int:
|
|
220
|
+
"""Calculate compliance component score."""
|
|
221
|
+
base = 85 # Start higher - compliance is important
|
|
222
|
+
|
|
223
|
+
for signal in signals:
|
|
224
|
+
if signal.signal_type.startswith("compliance."):
|
|
225
|
+
base -= int(signal.value * signal.weight * 30)
|
|
226
|
+
|
|
227
|
+
return max(0, min(100, base))
|
|
228
|
+
|
|
229
|
+
def _check_alerts(
|
|
230
|
+
self,
|
|
231
|
+
agent_did: str,
|
|
232
|
+
score: RiskScore,
|
|
233
|
+
old_level: str,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Check if alerts should be triggered."""
|
|
236
|
+
# Alert on level change
|
|
237
|
+
if score.risk_level != old_level:
|
|
238
|
+
for callback in self._alert_callbacks:
|
|
239
|
+
try:
|
|
240
|
+
callback({
|
|
241
|
+
"type": "risk_level_change",
|
|
242
|
+
"agent_did": agent_did,
|
|
243
|
+
"old_level": old_level,
|
|
244
|
+
"new_level": score.risk_level,
|
|
245
|
+
"score": score.total_score,
|
|
246
|
+
})
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
# Alert on threshold breach
|
|
251
|
+
if score.total_score < self.CRITICAL_THRESHOLD:
|
|
252
|
+
for callback in self._alert_callbacks:
|
|
253
|
+
try:
|
|
254
|
+
callback({
|
|
255
|
+
"type": "critical_risk",
|
|
256
|
+
"agent_did": agent_did,
|
|
257
|
+
"score": score.total_score,
|
|
258
|
+
"action": "immediate_review_required",
|
|
259
|
+
})
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
def on_alert(self, callback: callable) -> None:
|
|
264
|
+
"""Register an alert callback."""
|
|
265
|
+
self._alert_callbacks.append(callback)
|
|
266
|
+
|
|
267
|
+
def get_high_risk_agents(self, threshold: Optional[int] = None) -> list[RiskScore]:
|
|
268
|
+
"""Get all agents above risk threshold."""
|
|
269
|
+
thresh = threshold or self.HIGH_THRESHOLD
|
|
270
|
+
return [
|
|
271
|
+
score for score in self._scores.values()
|
|
272
|
+
if score.total_score < thresh
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
def clear_signals(self, agent_did: str) -> None:
|
|
276
|
+
"""Clear all signals for an agent (e.g., after remediation)."""
|
|
277
|
+
if agent_did in self._signals:
|
|
278
|
+
self._signals[agent_did] = []
|
|
279
|
+
self.recalculate(agent_did)
|