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,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trust Bridge
|
|
3
|
+
|
|
4
|
+
Unified trust layer across all protocols (A2A, MCP, IATP, ACP).
|
|
5
|
+
Ensures consistent trust model regardless of underlying protocol.
|
|
6
|
+
|
|
7
|
+
Integrates with agent-os IATP module for trust verification.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional, Literal, Any
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
import asyncio
|
|
14
|
+
|
|
15
|
+
from .handshake import TrustHandshake, HandshakeResult
|
|
16
|
+
from .capability import CapabilityScope
|
|
17
|
+
|
|
18
|
+
# Import IATP from agent-os (the source of truth for trust protocol)
|
|
19
|
+
try:
|
|
20
|
+
from modules.iatp import IATPClient, IATPMessage, TrustLevel
|
|
21
|
+
from modules.nexus import NexusClient, ReputationEngine
|
|
22
|
+
AGENT_OS_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
# Fallback if agent-os not installed yet (for development)
|
|
25
|
+
AGENT_OS_AVAILABLE = False
|
|
26
|
+
IATPClient = None
|
|
27
|
+
NexusClient = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PeerInfo(BaseModel):
|
|
31
|
+
"""Information about a peer agent."""
|
|
32
|
+
|
|
33
|
+
peer_did: str
|
|
34
|
+
peer_name: Optional[str] = None
|
|
35
|
+
protocol: str # "a2a", "mcp", "iatp", "acp"
|
|
36
|
+
|
|
37
|
+
# Trust info
|
|
38
|
+
trust_score: int = Field(default=0, ge=0, le=1000)
|
|
39
|
+
trust_verified: bool = False
|
|
40
|
+
last_verified: Optional[datetime] = None
|
|
41
|
+
|
|
42
|
+
# Capabilities
|
|
43
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
# Connection info
|
|
46
|
+
endpoint: Optional[str] = None
|
|
47
|
+
connected_at: Optional[datetime] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TrustBridge(BaseModel):
|
|
51
|
+
"""
|
|
52
|
+
Unified trust bridge for multi-protocol agent communication.
|
|
53
|
+
|
|
54
|
+
The TrustBridge ensures that regardless of which protocol is used
|
|
55
|
+
(A2A, MCP, IATP, ACP), trust verification happens consistently.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
agent_did: str = Field(..., description="This agent's DID")
|
|
59
|
+
|
|
60
|
+
# Trust thresholds
|
|
61
|
+
default_trust_threshold: int = Field(default=700, ge=0, le=1000)
|
|
62
|
+
|
|
63
|
+
# Known peers
|
|
64
|
+
peers: dict[str, PeerInfo] = Field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
# Handshake handler
|
|
67
|
+
_handshake: Optional[TrustHandshake] = None
|
|
68
|
+
|
|
69
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
70
|
+
|
|
71
|
+
def __init__(self, **data):
|
|
72
|
+
super().__init__(**data)
|
|
73
|
+
self._handshake = TrustHandshake(agent_did=self.agent_did)
|
|
74
|
+
|
|
75
|
+
async def verify_peer(
|
|
76
|
+
self,
|
|
77
|
+
peer_did: str,
|
|
78
|
+
protocol: str = "iatp",
|
|
79
|
+
required_trust_score: Optional[int] = None,
|
|
80
|
+
required_capabilities: Optional[list[str]] = None,
|
|
81
|
+
) -> HandshakeResult:
|
|
82
|
+
"""
|
|
83
|
+
Verify a peer before communication.
|
|
84
|
+
|
|
85
|
+
This is the core trust gate - all inter-agent communication
|
|
86
|
+
must pass through verification.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
peer_did: The peer's DID
|
|
90
|
+
protocol: Protocol to use for verification
|
|
91
|
+
required_trust_score: Minimum trust score required
|
|
92
|
+
required_capabilities: Capabilities the peer must have
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
HandshakeResult with verification status
|
|
96
|
+
"""
|
|
97
|
+
threshold = required_trust_score or self.default_trust_threshold
|
|
98
|
+
|
|
99
|
+
# Perform handshake
|
|
100
|
+
result = await self._handshake.initiate(
|
|
101
|
+
peer_did=peer_did,
|
|
102
|
+
protocol=protocol,
|
|
103
|
+
required_trust_score=threshold,
|
|
104
|
+
required_capabilities=required_capabilities,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Update peer info
|
|
108
|
+
if result.verified:
|
|
109
|
+
self.peers[peer_did] = PeerInfo(
|
|
110
|
+
peer_did=peer_did,
|
|
111
|
+
peer_name=result.peer_name,
|
|
112
|
+
protocol=protocol,
|
|
113
|
+
trust_score=result.trust_score,
|
|
114
|
+
trust_verified=True,
|
|
115
|
+
last_verified=datetime.utcnow(),
|
|
116
|
+
capabilities=result.capabilities,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
async def is_peer_trusted(
|
|
122
|
+
self,
|
|
123
|
+
peer_did: str,
|
|
124
|
+
required_score: Optional[int] = None,
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""Quick check if a peer is trusted."""
|
|
127
|
+
peer = self.peers.get(peer_did)
|
|
128
|
+
if not peer or not peer.trust_verified:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
threshold = required_score or self.default_trust_threshold
|
|
132
|
+
return peer.trust_score >= threshold
|
|
133
|
+
|
|
134
|
+
def get_peer(self, peer_did: str) -> Optional[PeerInfo]:
|
|
135
|
+
"""Get information about a known peer."""
|
|
136
|
+
return self.peers.get(peer_did)
|
|
137
|
+
|
|
138
|
+
def get_trusted_peers(self, min_score: Optional[int] = None) -> list[PeerInfo]:
|
|
139
|
+
"""Get all peers meeting trust threshold."""
|
|
140
|
+
threshold = min_score or self.default_trust_threshold
|
|
141
|
+
return [
|
|
142
|
+
peer for peer in self.peers.values()
|
|
143
|
+
if peer.trust_verified and peer.trust_score >= threshold
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
async def revoke_peer_trust(self, peer_did: str, reason: str) -> bool:
|
|
147
|
+
"""Revoke trust for a peer."""
|
|
148
|
+
if peer_did in self.peers:
|
|
149
|
+
self.peers[peer_did].trust_verified = False
|
|
150
|
+
self.peers[peer_did].trust_score = 0
|
|
151
|
+
return True
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ProtocolBridge(BaseModel):
|
|
156
|
+
"""
|
|
157
|
+
Protocol translation layer.
|
|
158
|
+
|
|
159
|
+
Translates between A2A, MCP, IATP, and ACP transparently,
|
|
160
|
+
maintaining trust guarantees across protocol boundaries.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
agent_did: str
|
|
164
|
+
trust_bridge: Optional[TrustBridge] = None
|
|
165
|
+
|
|
166
|
+
# Protocol handlers
|
|
167
|
+
supported_protocols: list[str] = Field(
|
|
168
|
+
default=["a2a", "mcp", "iatp", "acp"]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
172
|
+
|
|
173
|
+
def __init__(self, **data):
|
|
174
|
+
super().__init__(**data)
|
|
175
|
+
if not self.trust_bridge:
|
|
176
|
+
self.trust_bridge = TrustBridge(agent_did=self.agent_did)
|
|
177
|
+
|
|
178
|
+
async def send_message(
|
|
179
|
+
self,
|
|
180
|
+
peer_did: str,
|
|
181
|
+
message: Any,
|
|
182
|
+
source_protocol: str,
|
|
183
|
+
target_protocol: Optional[str] = None,
|
|
184
|
+
) -> Any:
|
|
185
|
+
"""
|
|
186
|
+
Send a message to a peer, translating protocols if needed.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
peer_did: Target peer's DID
|
|
190
|
+
message: Message to send
|
|
191
|
+
source_protocol: Protocol the message is in
|
|
192
|
+
target_protocol: Protocol to send as (auto-detect if None)
|
|
193
|
+
"""
|
|
194
|
+
# Verify trust first
|
|
195
|
+
if not await self.trust_bridge.is_peer_trusted(peer_did):
|
|
196
|
+
result = await self.trust_bridge.verify_peer(peer_did, source_protocol)
|
|
197
|
+
if not result.verified:
|
|
198
|
+
raise PermissionError(f"Peer not trusted: {peer_did}")
|
|
199
|
+
|
|
200
|
+
peer = self.trust_bridge.get_peer(peer_did)
|
|
201
|
+
dest_protocol = target_protocol or peer.protocol
|
|
202
|
+
|
|
203
|
+
# Translate if needed
|
|
204
|
+
if source_protocol != dest_protocol:
|
|
205
|
+
message = await self._translate(message, source_protocol, dest_protocol)
|
|
206
|
+
|
|
207
|
+
# Send via appropriate handler
|
|
208
|
+
return await self._send(peer_did, message, dest_protocol)
|
|
209
|
+
|
|
210
|
+
async def _translate(
|
|
211
|
+
self,
|
|
212
|
+
message: Any,
|
|
213
|
+
from_protocol: str,
|
|
214
|
+
to_protocol: str,
|
|
215
|
+
) -> Any:
|
|
216
|
+
"""Translate message between protocols."""
|
|
217
|
+
# Protocol translation mappings
|
|
218
|
+
if from_protocol == "a2a" and to_protocol == "mcp":
|
|
219
|
+
return self._a2a_to_mcp(message)
|
|
220
|
+
elif from_protocol == "mcp" and to_protocol == "a2a":
|
|
221
|
+
return self._mcp_to_a2a(message)
|
|
222
|
+
elif from_protocol == "iatp":
|
|
223
|
+
# IATP can wrap any protocol
|
|
224
|
+
return message
|
|
225
|
+
else:
|
|
226
|
+
# Default: pass through
|
|
227
|
+
return message
|
|
228
|
+
|
|
229
|
+
def _a2a_to_mcp(self, message: dict) -> dict:
|
|
230
|
+
"""Convert A2A message to MCP format."""
|
|
231
|
+
# A2A task -> MCP tool call
|
|
232
|
+
return {
|
|
233
|
+
"method": "tools/call",
|
|
234
|
+
"params": {
|
|
235
|
+
"name": message.get("task_type", "execute"),
|
|
236
|
+
"arguments": message.get("parameters", {}),
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def _mcp_to_a2a(self, message: dict) -> dict:
|
|
241
|
+
"""Convert MCP message to A2A format."""
|
|
242
|
+
# MCP tool call -> A2A task
|
|
243
|
+
params = message.get("params", {})
|
|
244
|
+
return {
|
|
245
|
+
"task_type": params.get("name", "execute"),
|
|
246
|
+
"parameters": params.get("arguments", {}),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async def _send(self, peer_did: str, message: Any, protocol: str) -> Any:
|
|
250
|
+
"""Send message via protocol handler."""
|
|
251
|
+
# In production, would dispatch to actual protocol handlers
|
|
252
|
+
# For now, return a placeholder
|
|
253
|
+
return {
|
|
254
|
+
"status": "sent",
|
|
255
|
+
"peer": peer_did,
|
|
256
|
+
"protocol": protocol,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
def get_protocol_for_peer(self, peer_did: str) -> Optional[str]:
|
|
260
|
+
"""Get the preferred protocol for a peer."""
|
|
261
|
+
peer = self.trust_bridge.get_peer(peer_did)
|
|
262
|
+
return peer.protocol if peer else None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class A2AAdapter:
|
|
266
|
+
"""
|
|
267
|
+
Adapter for Google A2A (Agent-to-Agent) protocol.
|
|
268
|
+
|
|
269
|
+
Supports:
|
|
270
|
+
- Agent Card discovery
|
|
271
|
+
- Task lifecycle management
|
|
272
|
+
- Collaboration messaging
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, agent_did: str, trust_bridge: TrustBridge):
|
|
276
|
+
self.agent_did = agent_did
|
|
277
|
+
self.trust_bridge = trust_bridge
|
|
278
|
+
|
|
279
|
+
async def discover_agent(self, endpoint: str) -> Optional[dict]:
|
|
280
|
+
"""
|
|
281
|
+
Discover an agent via A2A Agent Card.
|
|
282
|
+
|
|
283
|
+
GET /.well-known/agent.json
|
|
284
|
+
"""
|
|
285
|
+
# Would make HTTP request in production
|
|
286
|
+
return {
|
|
287
|
+
"name": "discovered-agent",
|
|
288
|
+
"description": "An A2A-compatible agent",
|
|
289
|
+
"capabilities": ["task/execute"],
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async def create_task(
|
|
293
|
+
self,
|
|
294
|
+
peer_did: str,
|
|
295
|
+
task_type: str,
|
|
296
|
+
parameters: dict,
|
|
297
|
+
) -> dict:
|
|
298
|
+
"""Create a task on a peer agent."""
|
|
299
|
+
# Verify trust
|
|
300
|
+
if not await self.trust_bridge.is_peer_trusted(peer_did):
|
|
301
|
+
raise PermissionError("Peer not trusted")
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"task_id": f"task_{peer_did}_{datetime.utcnow().timestamp()}",
|
|
305
|
+
"status": "created",
|
|
306
|
+
"type": task_type,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async def get_task_status(self, peer_did: str, task_id: str) -> dict:
|
|
310
|
+
"""Get status of a task."""
|
|
311
|
+
return {
|
|
312
|
+
"task_id": task_id,
|
|
313
|
+
"status": "running",
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class MCPAdapter:
|
|
318
|
+
"""
|
|
319
|
+
Adapter for Anthropic MCP (Model Context Protocol).
|
|
320
|
+
|
|
321
|
+
Supports:
|
|
322
|
+
- Tool registration
|
|
323
|
+
- Resource binding
|
|
324
|
+
- Governed tool invocation
|
|
325
|
+
|
|
326
|
+
All MCP tool calls route through AgentMesh policy engine.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def __init__(self, agent_did: str, trust_bridge: TrustBridge):
|
|
330
|
+
self.agent_did = agent_did
|
|
331
|
+
self.trust_bridge = trust_bridge
|
|
332
|
+
self._registered_tools: dict[str, dict] = {}
|
|
333
|
+
|
|
334
|
+
def register_tool(
|
|
335
|
+
self,
|
|
336
|
+
name: str,
|
|
337
|
+
description: str,
|
|
338
|
+
input_schema: dict,
|
|
339
|
+
required_capability: Optional[str] = None,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Register a tool with the MCP adapter."""
|
|
342
|
+
self._registered_tools[name] = {
|
|
343
|
+
"name": name,
|
|
344
|
+
"description": description,
|
|
345
|
+
"inputSchema": input_schema,
|
|
346
|
+
"required_capability": required_capability,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async def call_tool(
|
|
350
|
+
self,
|
|
351
|
+
peer_did: str,
|
|
352
|
+
tool_name: str,
|
|
353
|
+
arguments: dict,
|
|
354
|
+
) -> dict:
|
|
355
|
+
"""
|
|
356
|
+
Call a tool on a peer, with governance.
|
|
357
|
+
|
|
358
|
+
Unlike raw MCP, this:
|
|
359
|
+
1. Verifies peer trust
|
|
360
|
+
2. Checks capability scope
|
|
361
|
+
3. Logs for audit
|
|
362
|
+
"""
|
|
363
|
+
# Verify trust
|
|
364
|
+
if not await self.trust_bridge.is_peer_trusted(peer_did):
|
|
365
|
+
raise PermissionError("Peer not trusted for MCP tool call")
|
|
366
|
+
|
|
367
|
+
peer = self.trust_bridge.get_peer(peer_did)
|
|
368
|
+
|
|
369
|
+
# Check capability if tool requires one
|
|
370
|
+
tool = self._registered_tools.get(tool_name)
|
|
371
|
+
if tool and tool.get("required_capability"):
|
|
372
|
+
if tool["required_capability"] not in peer.capabilities:
|
|
373
|
+
raise PermissionError(
|
|
374
|
+
f"Peer lacks capability: {tool['required_capability']}"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Execute (would actually call MCP in production)
|
|
378
|
+
return {
|
|
379
|
+
"tool": tool_name,
|
|
380
|
+
"result": "success",
|
|
381
|
+
"governed": True,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
def list_tools(self) -> list[dict]:
|
|
385
|
+
"""List all registered tools."""
|
|
386
|
+
return list(self._registered_tools.values())
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Capability Scoping
|
|
3
|
+
|
|
4
|
+
Capability-scoped credential issuance per tool/resource per agent.
|
|
5
|
+
Agents cannot access any resource not explicitly in their credential scope.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional, Literal
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
import hashlib
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CapabilityGrant(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
A specific capability grant to an agent.
|
|
18
|
+
|
|
19
|
+
Capabilities follow the format: action:resource[:qualifier]
|
|
20
|
+
Examples:
|
|
21
|
+
- read:data
|
|
22
|
+
- write:reports
|
|
23
|
+
- execute:tools:calculator
|
|
24
|
+
- admin:*
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
grant_id: str = Field(default_factory=lambda: f"grant_{uuid.uuid4().hex[:12]}")
|
|
28
|
+
|
|
29
|
+
# Capability specification
|
|
30
|
+
capability: str = Field(..., description="Capability string (e.g., 'read:data')")
|
|
31
|
+
action: str = Field(..., description="Action part (e.g., 'read')")
|
|
32
|
+
resource: str = Field(..., description="Resource part (e.g., 'data')")
|
|
33
|
+
qualifier: Optional[str] = Field(None, description="Optional qualifier")
|
|
34
|
+
|
|
35
|
+
# Grant metadata
|
|
36
|
+
granted_to: str = Field(..., description="DID of grantee")
|
|
37
|
+
granted_by: str = Field(..., description="DID of grantor")
|
|
38
|
+
|
|
39
|
+
# Scope restrictions
|
|
40
|
+
resource_ids: list[str] = Field(
|
|
41
|
+
default_factory=list,
|
|
42
|
+
description="Specific resource IDs this grant applies to"
|
|
43
|
+
)
|
|
44
|
+
conditions: dict = Field(
|
|
45
|
+
default_factory=dict,
|
|
46
|
+
description="Additional conditions for this grant"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Timing
|
|
50
|
+
granted_at: datetime = Field(default_factory=datetime.utcnow)
|
|
51
|
+
expires_at: Optional[datetime] = Field(None)
|
|
52
|
+
|
|
53
|
+
# Status
|
|
54
|
+
active: bool = Field(default=True)
|
|
55
|
+
revoked_at: Optional[datetime] = Field(None)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def parse_capability(cls, capability: str) -> tuple[str, str, Optional[str]]:
|
|
59
|
+
"""Parse a capability string into components."""
|
|
60
|
+
parts = capability.split(":")
|
|
61
|
+
if len(parts) < 2:
|
|
62
|
+
raise ValueError(f"Invalid capability format: {capability}")
|
|
63
|
+
|
|
64
|
+
action = parts[0]
|
|
65
|
+
resource = parts[1]
|
|
66
|
+
qualifier = parts[2] if len(parts) > 2 else None
|
|
67
|
+
|
|
68
|
+
return action, resource, qualifier
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def create(
|
|
72
|
+
cls,
|
|
73
|
+
capability: str,
|
|
74
|
+
granted_to: str,
|
|
75
|
+
granted_by: str,
|
|
76
|
+
resource_ids: Optional[list[str]] = None,
|
|
77
|
+
expires_at: Optional[datetime] = None,
|
|
78
|
+
) -> "CapabilityGrant":
|
|
79
|
+
"""Create a new capability grant."""
|
|
80
|
+
action, resource, qualifier = cls.parse_capability(capability)
|
|
81
|
+
|
|
82
|
+
return cls(
|
|
83
|
+
capability=capability,
|
|
84
|
+
action=action,
|
|
85
|
+
resource=resource,
|
|
86
|
+
qualifier=qualifier,
|
|
87
|
+
granted_to=granted_to,
|
|
88
|
+
granted_by=granted_by,
|
|
89
|
+
resource_ids=resource_ids or [],
|
|
90
|
+
expires_at=expires_at,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def is_valid(self) -> bool:
|
|
94
|
+
"""Check if grant is currently valid."""
|
|
95
|
+
if not self.active:
|
|
96
|
+
return False
|
|
97
|
+
if self.expires_at and datetime.utcnow() > self.expires_at:
|
|
98
|
+
return False
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
def matches(self, requested: str, resource_id: Optional[str] = None) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if this grant satisfies a requested capability.
|
|
104
|
+
|
|
105
|
+
Supports:
|
|
106
|
+
- Exact match: read:data matches read:data
|
|
107
|
+
- Wildcard: read:* matches read:data
|
|
108
|
+
- Resource scoping: if resource_ids set, must match
|
|
109
|
+
"""
|
|
110
|
+
if not self.is_valid():
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
req_action, req_resource, req_qualifier = self.parse_capability(requested)
|
|
114
|
+
|
|
115
|
+
# Check action
|
|
116
|
+
if self.action != "*" and self.action != req_action:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Check resource
|
|
120
|
+
if self.resource != "*" and self.resource != req_resource:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Check qualifier if present
|
|
124
|
+
if req_qualifier and self.qualifier:
|
|
125
|
+
if self.qualifier != "*" and self.qualifier != req_qualifier:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
# Check resource ID if scoped
|
|
129
|
+
if self.resource_ids and resource_id:
|
|
130
|
+
if resource_id not in self.resource_ids:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
def revoke(self) -> None:
|
|
136
|
+
"""Revoke this grant."""
|
|
137
|
+
self.active = False
|
|
138
|
+
self.revoked_at = datetime.utcnow()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CapabilityScope(BaseModel):
|
|
142
|
+
"""
|
|
143
|
+
Complete capability scope for an agent.
|
|
144
|
+
|
|
145
|
+
Aggregates all grants and provides capability checking.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
agent_did: str
|
|
149
|
+
grants: list[CapabilityGrant] = Field(default_factory=list)
|
|
150
|
+
|
|
151
|
+
# Denied capabilities (blocklist)
|
|
152
|
+
denied: list[str] = Field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
def add_grant(self, grant: CapabilityGrant) -> None:
|
|
155
|
+
"""Add a capability grant."""
|
|
156
|
+
if grant.granted_to != self.agent_did:
|
|
157
|
+
raise ValueError("Grant is for different agent")
|
|
158
|
+
self.grants.append(grant)
|
|
159
|
+
|
|
160
|
+
def has_capability(
|
|
161
|
+
self,
|
|
162
|
+
capability: str,
|
|
163
|
+
resource_id: Optional[str] = None,
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Check if agent has a capability.
|
|
167
|
+
|
|
168
|
+
Checks:
|
|
169
|
+
1. Not in denied list
|
|
170
|
+
2. Has matching grant
|
|
171
|
+
3. Grant is valid (not expired, not revoked)
|
|
172
|
+
"""
|
|
173
|
+
# Check denied first
|
|
174
|
+
if capability in self.denied:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Check for matching grant
|
|
178
|
+
for grant in self.grants:
|
|
179
|
+
if grant.matches(capability, resource_id):
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def get_capabilities(self) -> list[str]:
|
|
185
|
+
"""Get list of all active capabilities."""
|
|
186
|
+
capabilities = set()
|
|
187
|
+
for grant in self.grants:
|
|
188
|
+
if grant.is_valid():
|
|
189
|
+
capabilities.add(grant.capability)
|
|
190
|
+
return list(capabilities)
|
|
191
|
+
|
|
192
|
+
def filter_capabilities(self, requested: list[str]) -> list[str]:
|
|
193
|
+
"""Filter a list of requested capabilities to only those allowed."""
|
|
194
|
+
return [cap for cap in requested if self.has_capability(cap)]
|
|
195
|
+
|
|
196
|
+
def deny(self, capability: str) -> None:
|
|
197
|
+
"""Add a capability to the deny list."""
|
|
198
|
+
if capability not in self.denied:
|
|
199
|
+
self.denied.append(capability)
|
|
200
|
+
|
|
201
|
+
def revoke_all(self) -> int:
|
|
202
|
+
"""Revoke all grants. Returns count of revoked grants."""
|
|
203
|
+
count = 0
|
|
204
|
+
for grant in self.grants:
|
|
205
|
+
if grant.active:
|
|
206
|
+
grant.revoke()
|
|
207
|
+
count += 1
|
|
208
|
+
return count
|
|
209
|
+
|
|
210
|
+
def revoke_from(self, grantor_did: str) -> int:
|
|
211
|
+
"""Revoke all grants from a specific grantor."""
|
|
212
|
+
count = 0
|
|
213
|
+
for grant in self.grants:
|
|
214
|
+
if grant.active and grant.granted_by == grantor_did:
|
|
215
|
+
grant.revoke()
|
|
216
|
+
count += 1
|
|
217
|
+
return count
|
|
218
|
+
|
|
219
|
+
def cleanup_expired(self) -> int:
|
|
220
|
+
"""Remove expired and revoked grants. Returns count removed."""
|
|
221
|
+
before = len(self.grants)
|
|
222
|
+
self.grants = [g for g in self.grants if g.is_valid()]
|
|
223
|
+
return before - len(self.grants)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class CapabilityRegistry:
|
|
227
|
+
"""
|
|
228
|
+
Central registry for capability grants.
|
|
229
|
+
|
|
230
|
+
Tracks who has what capabilities across the mesh.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def __init__(self):
|
|
234
|
+
self._scopes: dict[str, CapabilityScope] = {}
|
|
235
|
+
self._grants_by_grantor: dict[str, list[str]] = {} # grantor -> [grant_ids]
|
|
236
|
+
|
|
237
|
+
def get_scope(self, agent_did: str) -> CapabilityScope:
|
|
238
|
+
"""Get or create capability scope for an agent."""
|
|
239
|
+
if agent_did not in self._scopes:
|
|
240
|
+
self._scopes[agent_did] = CapabilityScope(agent_did=agent_did)
|
|
241
|
+
return self._scopes[agent_did]
|
|
242
|
+
|
|
243
|
+
def grant(
|
|
244
|
+
self,
|
|
245
|
+
capability: str,
|
|
246
|
+
to_agent: str,
|
|
247
|
+
from_agent: str,
|
|
248
|
+
resource_ids: Optional[list[str]] = None,
|
|
249
|
+
) -> CapabilityGrant:
|
|
250
|
+
"""Grant a capability to an agent."""
|
|
251
|
+
grant = CapabilityGrant.create(
|
|
252
|
+
capability=capability,
|
|
253
|
+
granted_to=to_agent,
|
|
254
|
+
granted_by=from_agent,
|
|
255
|
+
resource_ids=resource_ids,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
scope = self.get_scope(to_agent)
|
|
259
|
+
scope.add_grant(grant)
|
|
260
|
+
|
|
261
|
+
# Track by grantor
|
|
262
|
+
if from_agent not in self._grants_by_grantor:
|
|
263
|
+
self._grants_by_grantor[from_agent] = []
|
|
264
|
+
self._grants_by_grantor[from_agent].append(grant.grant_id)
|
|
265
|
+
|
|
266
|
+
return grant
|
|
267
|
+
|
|
268
|
+
def check(
|
|
269
|
+
self,
|
|
270
|
+
agent_did: str,
|
|
271
|
+
capability: str,
|
|
272
|
+
resource_id: Optional[str] = None,
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""Check if an agent has a capability."""
|
|
275
|
+
scope = self._scopes.get(agent_did)
|
|
276
|
+
if not scope:
|
|
277
|
+
return False
|
|
278
|
+
return scope.has_capability(capability, resource_id)
|
|
279
|
+
|
|
280
|
+
def revoke_all_from(self, grantor_did: str) -> int:
|
|
281
|
+
"""Revoke all grants made by a grantor (e.g., when grantor is compromised)."""
|
|
282
|
+
count = 0
|
|
283
|
+
for scope in self._scopes.values():
|
|
284
|
+
count += scope.revoke_from(grantor_did)
|
|
285
|
+
return count
|
|
286
|
+
|
|
287
|
+
def get_agents_with_capability(self, capability: str) -> list[str]:
|
|
288
|
+
"""Get all agents that have a specific capability."""
|
|
289
|
+
result = []
|
|
290
|
+
for agent_did, scope in self._scopes.items():
|
|
291
|
+
if scope.has_capability(capability):
|
|
292
|
+
result.append(agent_did)
|
|
293
|
+
return result
|