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.
@@ -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)