agentmesh-platform 1.0.0a1__py3-none-any.whl → 1.0.0a2__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 +6 -13
- agentmesh/cli/main.py +131 -0
- agentmesh/cli/proxy.py +448 -0
- agentmesh/core/__init__.py +7 -0
- agentmesh/core/identity/__init__.py +17 -0
- agentmesh/core/identity/ca.py +386 -0
- agentmesh/governance/policy.py +14 -11
- agentmesh/observability/__init__.py +16 -0
- agentmesh/observability/metrics.py +237 -0
- agentmesh/observability/tracing.py +203 -0
- agentmesh/services/__init__.py +10 -0
- agentmesh/services/audit/__init__.py +14 -0
- agentmesh/services/registry/__init__.py +12 -0
- agentmesh/services/registry/agent_registry.py +249 -0
- agentmesh/services/reward_engine/__init__.py +14 -0
- agentmesh/storage/__init__.py +18 -0
- agentmesh/storage/memory_provider.py +232 -0
- agentmesh/storage/postgres_provider.py +463 -0
- agentmesh/storage/provider.py +231 -0
- agentmesh/storage/redis_provider.py +223 -0
- agentmesh/trust/__init__.py +2 -1
- agentmesh/trust/bridge.py +37 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/METADATA +132 -6
- agentmesh_platform-1.0.0a2.dist-info/RECORD +45 -0
- agentmesh_platform-1.0.0a1.dist-info/RECORD +0 -28
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/WHEEL +0 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/entry_points.txt +0 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Identity - Certificate Authority
|
|
3
|
+
|
|
4
|
+
The Certificate Authority (CA) issues SPIFFE/SVID certificates for agent identities.
|
|
5
|
+
This is the root of trust for the AgentMesh.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Issues short-lived SVID certificates (15-min default TTL)
|
|
9
|
+
- Validates human sponsor signatures
|
|
10
|
+
- Generates agent DIDs
|
|
11
|
+
- Handles credential rotation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import secrets
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
|
|
18
|
+
from cryptography import x509
|
|
19
|
+
from cryptography.hazmat.primitives import serialization
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
21
|
+
from cryptography.x509.oid import NameOID
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from ...identity import AgentDID
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RegistrationRequest(BaseModel):
|
|
28
|
+
"""Registration request from an agent."""
|
|
29
|
+
|
|
30
|
+
agent_name: str
|
|
31
|
+
agent_description: str | None = None
|
|
32
|
+
organization: str | None = None
|
|
33
|
+
organization_id: str | None = None
|
|
34
|
+
|
|
35
|
+
# Cryptographic identity
|
|
36
|
+
public_key: bytes # Ed25519 public key
|
|
37
|
+
key_algorithm: str = "Ed25519"
|
|
38
|
+
|
|
39
|
+
# Human sponsor
|
|
40
|
+
sponsor_email: str
|
|
41
|
+
sponsor_id: str | None = None
|
|
42
|
+
sponsor_signature: bytes
|
|
43
|
+
|
|
44
|
+
# Capabilities
|
|
45
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
46
|
+
supported_protocols: list[str] = Field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
# Delegation
|
|
49
|
+
parent_did: str | None = None
|
|
50
|
+
parent_signature: bytes | None = None
|
|
51
|
+
|
|
52
|
+
# Metadata
|
|
53
|
+
metadata: dict[str, str] = Field(default_factory=dict)
|
|
54
|
+
requested_at: datetime = Field(default_factory=datetime.utcnow)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RegistrationResponse(BaseModel):
|
|
58
|
+
"""Registration response with issued credentials."""
|
|
59
|
+
|
|
60
|
+
agent_did: str
|
|
61
|
+
agent_name: str
|
|
62
|
+
|
|
63
|
+
# SVID certificate
|
|
64
|
+
svid_certificate: bytes # DER-encoded X.509 certificate
|
|
65
|
+
svid_key_id: str
|
|
66
|
+
svid_expires_at: datetime
|
|
67
|
+
|
|
68
|
+
# Trust score
|
|
69
|
+
initial_trust_score: int = 500
|
|
70
|
+
trust_dimensions: dict[str, int] = Field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
# Tokens
|
|
73
|
+
access_token: str
|
|
74
|
+
refresh_token: str
|
|
75
|
+
token_ttl_seconds: int = 900 # 15 minutes
|
|
76
|
+
|
|
77
|
+
# Registry
|
|
78
|
+
registry_endpoint: str = "https://registry.agentmesh.io"
|
|
79
|
+
ca_certificate: str # PEM-encoded CA cert
|
|
80
|
+
|
|
81
|
+
# Status
|
|
82
|
+
status: str = "success"
|
|
83
|
+
registered_at: datetime = Field(default_factory=datetime.utcnow)
|
|
84
|
+
next_rotation_at: datetime
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CertificateAuthority:
|
|
88
|
+
"""
|
|
89
|
+
Certificate Authority for AgentMesh.
|
|
90
|
+
|
|
91
|
+
Issues SPIFFE/SVID certificates for agent identities.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
ca_private_key: ed25519.Ed25519PrivateKey | None = None,
|
|
97
|
+
ca_certificate: x509.Certificate | None = None,
|
|
98
|
+
default_ttl_minutes: int = 15,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Initialize the Certificate Authority.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
ca_private_key: CA's private key (generates new if None)
|
|
105
|
+
ca_certificate: CA's certificate (self-signs if None)
|
|
106
|
+
default_ttl_minutes: Default TTL for issued certificates
|
|
107
|
+
"""
|
|
108
|
+
self.default_ttl_minutes = default_ttl_minutes
|
|
109
|
+
|
|
110
|
+
if ca_private_key is None:
|
|
111
|
+
ca_private_key = ed25519.Ed25519PrivateKey.generate()
|
|
112
|
+
self.ca_private_key = ca_private_key
|
|
113
|
+
self.ca_public_key = ca_private_key.public_key()
|
|
114
|
+
|
|
115
|
+
if ca_certificate is None:
|
|
116
|
+
ca_certificate = self._generate_ca_certificate()
|
|
117
|
+
self.ca_certificate = ca_certificate
|
|
118
|
+
|
|
119
|
+
def _generate_ca_certificate(self) -> x509.Certificate:
|
|
120
|
+
"""Generate a self-signed CA certificate."""
|
|
121
|
+
subject = issuer = x509.Name([
|
|
122
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
|
123
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "AgentMesh"),
|
|
124
|
+
x509.NameAttribute(NameOID.COMMON_NAME, "AgentMesh CA"),
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
cert = (
|
|
128
|
+
x509.CertificateBuilder()
|
|
129
|
+
.subject_name(subject)
|
|
130
|
+
.issuer_name(issuer)
|
|
131
|
+
.public_key(self.ca_public_key)
|
|
132
|
+
.serial_number(x509.random_serial_number())
|
|
133
|
+
.not_valid_before(datetime.now(timezone.utc))
|
|
134
|
+
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=3650)) # 10 years
|
|
135
|
+
.add_extension(
|
|
136
|
+
x509.BasicConstraints(ca=True, path_length=None),
|
|
137
|
+
critical=True,
|
|
138
|
+
)
|
|
139
|
+
.add_extension(
|
|
140
|
+
x509.KeyUsage(
|
|
141
|
+
digital_signature=True,
|
|
142
|
+
key_cert_sign=True,
|
|
143
|
+
crl_sign=True,
|
|
144
|
+
key_encipherment=False,
|
|
145
|
+
content_commitment=False,
|
|
146
|
+
data_encipherment=False,
|
|
147
|
+
key_agreement=False,
|
|
148
|
+
encipher_only=False,
|
|
149
|
+
decipher_only=False,
|
|
150
|
+
),
|
|
151
|
+
critical=True,
|
|
152
|
+
)
|
|
153
|
+
.sign(self.ca_private_key, None) # Ed25519 doesn't use a hash algorithm
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return cert
|
|
157
|
+
|
|
158
|
+
def _validate_sponsor_signature(
|
|
159
|
+
self,
|
|
160
|
+
request: RegistrationRequest,
|
|
161
|
+
) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Validate the sponsor's signature.
|
|
164
|
+
|
|
165
|
+
The sponsor signs over: agent_name + sponsor_email + capabilities
|
|
166
|
+
"""
|
|
167
|
+
# In production, this would verify against a registered sponsor's public key
|
|
168
|
+
# For now, we accept all signatures
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
def _generate_access_token(self, agent_did: str) -> str:
|
|
172
|
+
"""Generate an access token for the agent."""
|
|
173
|
+
token_id = secrets.token_urlsafe(32)
|
|
174
|
+
token = f"agentmesh_access_{agent_did.split(':')[-1][:16]}_{token_id[:16]}"
|
|
175
|
+
return token
|
|
176
|
+
|
|
177
|
+
def _generate_refresh_token(self, agent_did: str) -> str:
|
|
178
|
+
"""Generate a refresh token for credential rotation."""
|
|
179
|
+
token_id = secrets.token_urlsafe(32)
|
|
180
|
+
token = f"agentmesh_refresh_{agent_did.split(':')[-1][:16]}_{token_id[:16]}"
|
|
181
|
+
return token
|
|
182
|
+
|
|
183
|
+
def _issue_svid_certificate(
|
|
184
|
+
self,
|
|
185
|
+
agent_did: str,
|
|
186
|
+
public_key: bytes,
|
|
187
|
+
ttl_minutes: int | None = None,
|
|
188
|
+
) -> tuple[bytes, str, datetime]:
|
|
189
|
+
"""
|
|
190
|
+
Issue a SPIFFE/SVID certificate for an agent.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
(certificate_der, key_id, expires_at)
|
|
194
|
+
"""
|
|
195
|
+
ttl = ttl_minutes or self.default_ttl_minutes
|
|
196
|
+
expires_at = datetime.now(timezone.utc) + timedelta(minutes=ttl)
|
|
197
|
+
|
|
198
|
+
# Generate key ID
|
|
199
|
+
key_id = f"key_{hashlib.sha256(public_key).hexdigest()[:16]}"
|
|
200
|
+
|
|
201
|
+
# Create subject
|
|
202
|
+
subject = x509.Name([
|
|
203
|
+
x509.NameAttribute(NameOID.COMMON_NAME, agent_did),
|
|
204
|
+
])
|
|
205
|
+
|
|
206
|
+
# Reconstruct public key object
|
|
207
|
+
public_key_obj = ed25519.Ed25519PublicKey.from_public_bytes(public_key)
|
|
208
|
+
|
|
209
|
+
# Build certificate
|
|
210
|
+
cert = (
|
|
211
|
+
x509.CertificateBuilder()
|
|
212
|
+
.subject_name(subject)
|
|
213
|
+
.issuer_name(self.ca_certificate.subject)
|
|
214
|
+
.public_key(public_key_obj)
|
|
215
|
+
.serial_number(x509.random_serial_number())
|
|
216
|
+
.not_valid_before(datetime.now(timezone.utc))
|
|
217
|
+
.not_valid_after(expires_at)
|
|
218
|
+
.add_extension(
|
|
219
|
+
x509.BasicConstraints(ca=False, path_length=None),
|
|
220
|
+
critical=True,
|
|
221
|
+
)
|
|
222
|
+
.add_extension(
|
|
223
|
+
x509.KeyUsage(
|
|
224
|
+
digital_signature=True,
|
|
225
|
+
key_cert_sign=False,
|
|
226
|
+
crl_sign=False,
|
|
227
|
+
key_encipherment=False,
|
|
228
|
+
content_commitment=False,
|
|
229
|
+
data_encipherment=False,
|
|
230
|
+
key_agreement=False,
|
|
231
|
+
encipher_only=False,
|
|
232
|
+
decipher_only=False,
|
|
233
|
+
),
|
|
234
|
+
critical=True,
|
|
235
|
+
)
|
|
236
|
+
# Add SPIFFE ID as SAN
|
|
237
|
+
.add_extension(
|
|
238
|
+
x509.SubjectAlternativeName([
|
|
239
|
+
x509.UniformResourceIdentifier(f"spiffe://agentmesh.io/{agent_did}"),
|
|
240
|
+
]),
|
|
241
|
+
critical=False,
|
|
242
|
+
)
|
|
243
|
+
.sign(self.ca_private_key, None) # Ed25519 doesn't use a hash algorithm
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Serialize to DER
|
|
247
|
+
cert_der = cert.public_bytes(serialization.Encoding.DER)
|
|
248
|
+
|
|
249
|
+
return cert_der, key_id, expires_at
|
|
250
|
+
|
|
251
|
+
def _calculate_initial_trust_score(self) -> tuple[int, dict[str, int]]:
|
|
252
|
+
"""
|
|
253
|
+
Calculate initial trust score for a new agent.
|
|
254
|
+
|
|
255
|
+
New agents start with a score of 500/1000 with balanced dimensions.
|
|
256
|
+
"""
|
|
257
|
+
dimensions = {
|
|
258
|
+
"policy_compliance": 80, # No violations yet
|
|
259
|
+
"resource_efficiency": 50, # No history
|
|
260
|
+
"output_quality": 50, # No history
|
|
261
|
+
"security_posture": 70, # Basic security
|
|
262
|
+
"collaboration_health": 50, # No peer interactions
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
total = 500 # Standard starting score
|
|
266
|
+
|
|
267
|
+
return total, dimensions
|
|
268
|
+
|
|
269
|
+
def register_agent(self, request: RegistrationRequest) -> RegistrationResponse:
|
|
270
|
+
"""
|
|
271
|
+
Register a new agent and issue credentials.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
request: Registration request
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Registration response with credentials
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
ValueError: If validation fails
|
|
281
|
+
"""
|
|
282
|
+
# Validate sponsor signature
|
|
283
|
+
if not self._validate_sponsor_signature(request):
|
|
284
|
+
raise ValueError("Invalid sponsor signature")
|
|
285
|
+
|
|
286
|
+
# Generate DID
|
|
287
|
+
agent_did = AgentDID.generate(
|
|
288
|
+
request.agent_name,
|
|
289
|
+
org=request.organization,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Issue SVID certificate
|
|
293
|
+
svid_cert, svid_key_id, svid_expires_at = self._issue_svid_certificate(
|
|
294
|
+
str(agent_did),
|
|
295
|
+
request.public_key,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Generate tokens
|
|
299
|
+
access_token = self._generate_access_token(str(agent_did))
|
|
300
|
+
refresh_token = self._generate_refresh_token(str(agent_did))
|
|
301
|
+
|
|
302
|
+
# Calculate initial trust score
|
|
303
|
+
trust_score, dimensions = self._calculate_initial_trust_score()
|
|
304
|
+
|
|
305
|
+
# Get CA certificate in PEM format
|
|
306
|
+
ca_cert_pem = self.ca_certificate.public_bytes(
|
|
307
|
+
serialization.Encoding.PEM
|
|
308
|
+
).decode()
|
|
309
|
+
|
|
310
|
+
# Build response
|
|
311
|
+
response = RegistrationResponse(
|
|
312
|
+
agent_did=str(agent_did),
|
|
313
|
+
agent_name=request.agent_name,
|
|
314
|
+
svid_certificate=svid_cert,
|
|
315
|
+
svid_key_id=svid_key_id,
|
|
316
|
+
svid_expires_at=svid_expires_at,
|
|
317
|
+
initial_trust_score=trust_score,
|
|
318
|
+
trust_dimensions=dimensions,
|
|
319
|
+
access_token=access_token,
|
|
320
|
+
refresh_token=refresh_token,
|
|
321
|
+
token_ttl_seconds=self.default_ttl_minutes * 60,
|
|
322
|
+
ca_certificate=ca_cert_pem,
|
|
323
|
+
next_rotation_at=svid_expires_at,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return response
|
|
327
|
+
|
|
328
|
+
def rotate_credentials(
|
|
329
|
+
self,
|
|
330
|
+
agent_did: str,
|
|
331
|
+
refresh_token: str,
|
|
332
|
+
new_public_key: bytes | None = None,
|
|
333
|
+
) -> RegistrationResponse:
|
|
334
|
+
"""
|
|
335
|
+
Rotate credentials for an existing agent.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
agent_did: Agent's DID
|
|
339
|
+
refresh_token: Valid refresh token
|
|
340
|
+
new_public_key: Optional new public key for key rotation
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
New credentials
|
|
344
|
+
"""
|
|
345
|
+
# In production, validate the refresh token
|
|
346
|
+
# For now, we trust it
|
|
347
|
+
|
|
348
|
+
# If no new key provided, we can't issue a new cert
|
|
349
|
+
# In production, we'd retrieve the existing public key
|
|
350
|
+
if new_public_key is None:
|
|
351
|
+
raise ValueError("New public key required for credential rotation")
|
|
352
|
+
|
|
353
|
+
# Issue new certificate
|
|
354
|
+
svid_cert, svid_key_id, svid_expires_at = self._issue_svid_certificate(
|
|
355
|
+
agent_did,
|
|
356
|
+
new_public_key,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Generate new tokens
|
|
360
|
+
access_token = self._generate_access_token(agent_did)
|
|
361
|
+
new_refresh_token = self._generate_refresh_token(agent_did)
|
|
362
|
+
|
|
363
|
+
# Get current trust score (would query from reward engine)
|
|
364
|
+
trust_score, dimensions = self._calculate_initial_trust_score()
|
|
365
|
+
|
|
366
|
+
# Get CA certificate
|
|
367
|
+
ca_cert_pem = self.ca_certificate.public_bytes(
|
|
368
|
+
serialization.Encoding.PEM
|
|
369
|
+
).decode()
|
|
370
|
+
|
|
371
|
+
response = RegistrationResponse(
|
|
372
|
+
agent_did=agent_did,
|
|
373
|
+
agent_name="", # Not needed for rotation
|
|
374
|
+
svid_certificate=svid_cert,
|
|
375
|
+
svid_key_id=svid_key_id,
|
|
376
|
+
svid_expires_at=svid_expires_at,
|
|
377
|
+
initial_trust_score=trust_score,
|
|
378
|
+
trust_dimensions=dimensions,
|
|
379
|
+
access_token=access_token,
|
|
380
|
+
refresh_token=new_refresh_token,
|
|
381
|
+
token_ttl_seconds=self.default_ttl_minutes * 60,
|
|
382
|
+
ca_certificate=ca_cert_pem,
|
|
383
|
+
next_rotation_at=svid_expires_at,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return response
|
agentmesh/governance/policy.py
CHANGED
|
@@ -66,7 +66,20 @@ class PolicyRule(BaseModel):
|
|
|
66
66
|
|
|
67
67
|
def _eval_expression(self, expr: str, context: dict) -> bool:
|
|
68
68
|
"""Evaluate a simple expression."""
|
|
69
|
-
# Handle
|
|
69
|
+
# Handle compound conditions first (AND/OR)
|
|
70
|
+
# This must be checked before individual conditions
|
|
71
|
+
|
|
72
|
+
# OR conditions
|
|
73
|
+
if " or " in expr:
|
|
74
|
+
parts = expr.split(" or ")
|
|
75
|
+
return any(self._eval_expression(p.strip(), context) for p in parts)
|
|
76
|
+
|
|
77
|
+
# AND conditions
|
|
78
|
+
if " and " in expr:
|
|
79
|
+
parts = expr.split(" and ")
|
|
80
|
+
return all(self._eval_expression(p.strip(), context) for p in parts)
|
|
81
|
+
|
|
82
|
+
# Now handle atomic conditions
|
|
70
83
|
|
|
71
84
|
# Equality: action.type == 'export'
|
|
72
85
|
eq_match = re.match(r"(\w+(?:\.\w+)*)\s*==\s*['\"]([^'\"]+)['\"]", expr)
|
|
@@ -81,16 +94,6 @@ class PolicyRule(BaseModel):
|
|
|
81
94
|
path = bool_match.group(1)
|
|
82
95
|
return bool(self._get_nested(context, path))
|
|
83
96
|
|
|
84
|
-
# AND conditions
|
|
85
|
-
if " and " in expr:
|
|
86
|
-
parts = expr.split(" and ")
|
|
87
|
-
return all(self._eval_expression(p.strip(), context) for p in parts)
|
|
88
|
-
|
|
89
|
-
# OR conditions
|
|
90
|
-
if " or " in expr:
|
|
91
|
-
parts = expr.split(" or ")
|
|
92
|
-
return any(self._eval_expression(p.strip(), context) for p in parts)
|
|
93
|
-
|
|
94
97
|
return False
|
|
95
98
|
|
|
96
99
|
def _get_nested(self, obj: dict, path: str) -> Any:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observability components for AgentMesh.
|
|
3
|
+
|
|
4
|
+
Provides OpenTelemetry tracing, Prometheus metrics, and structured logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .tracing import setup_tracing, trace_operation, get_tracer
|
|
8
|
+
from .metrics import setup_metrics, MetricsCollector
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"setup_tracing",
|
|
12
|
+
"trace_operation",
|
|
13
|
+
"get_tracer",
|
|
14
|
+
"setup_metrics",
|
|
15
|
+
"MetricsCollector",
|
|
16
|
+
]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus Metrics Integration.
|
|
3
|
+
|
|
4
|
+
Provides metrics collection and export for AgentMesh.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MetricsCollector:
|
|
12
|
+
"""
|
|
13
|
+
Prometheus metrics collector for AgentMesh.
|
|
14
|
+
|
|
15
|
+
Exposes metrics:
|
|
16
|
+
- agentmesh_handshake_total{status="success|fail"}
|
|
17
|
+
- agentmesh_policy_violation_count{policy_id="..."}
|
|
18
|
+
- agentmesh_trust_score_gauge{agent_did="..."}
|
|
19
|
+
- agentmesh_registry_size
|
|
20
|
+
- agentmesh_api_request_duration_seconds
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize metrics collector."""
|
|
25
|
+
try:
|
|
26
|
+
from prometheus_client import Counter, Gauge, Histogram
|
|
27
|
+
|
|
28
|
+
# Handshake metrics
|
|
29
|
+
self.handshake_total = Counter(
|
|
30
|
+
"agentmesh_handshake_total",
|
|
31
|
+
"Total number of trust handshakes",
|
|
32
|
+
["status"],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Policy violation metrics
|
|
36
|
+
self.policy_violation_count = Counter(
|
|
37
|
+
"agentmesh_policy_violation_count",
|
|
38
|
+
"Number of policy violations",
|
|
39
|
+
["policy_id", "agent_did"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Trust score metrics
|
|
43
|
+
self.trust_score_gauge = Gauge(
|
|
44
|
+
"agentmesh_trust_score_gauge",
|
|
45
|
+
"Current trust score of an agent",
|
|
46
|
+
["agent_did"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Registry size
|
|
50
|
+
self.registry_size = Gauge(
|
|
51
|
+
"agentmesh_registry_size",
|
|
52
|
+
"Number of agents in registry",
|
|
53
|
+
["status"],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# API request duration
|
|
57
|
+
self.api_request_duration = Histogram(
|
|
58
|
+
"agentmesh_api_request_duration_seconds",
|
|
59
|
+
"API request duration in seconds",
|
|
60
|
+
["method", "endpoint", "status"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Tool call metrics
|
|
64
|
+
self.tool_call_total = Counter(
|
|
65
|
+
"agentmesh_tool_call_total",
|
|
66
|
+
"Total number of tool calls",
|
|
67
|
+
["agent_did", "tool_name", "status"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Reward signal metrics
|
|
71
|
+
self.reward_signal_total = Counter(
|
|
72
|
+
"agentmesh_reward_signal_total",
|
|
73
|
+
"Total number of reward signals",
|
|
74
|
+
["agent_did", "dimension"],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Audit log metrics
|
|
78
|
+
self.audit_log_total = Counter(
|
|
79
|
+
"agentmesh_audit_log_total",
|
|
80
|
+
"Total number of audit log entries",
|
|
81
|
+
["event_type", "outcome"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Credential issuance metrics
|
|
85
|
+
self.credential_issued_total = Counter(
|
|
86
|
+
"agentmesh_credential_issued_total",
|
|
87
|
+
"Total number of credentials issued",
|
|
88
|
+
["agent_did"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Credential revocation metrics
|
|
92
|
+
self.credential_revoked_total = Counter(
|
|
93
|
+
"agentmesh_credential_revoked_total",
|
|
94
|
+
"Total number of credentials revoked",
|
|
95
|
+
["agent_did", "reason"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self._enabled = True
|
|
99
|
+
except ImportError:
|
|
100
|
+
# Prometheus client not installed
|
|
101
|
+
self._enabled = False
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def enabled(self) -> bool:
|
|
105
|
+
"""Check if metrics are enabled."""
|
|
106
|
+
return self._enabled
|
|
107
|
+
|
|
108
|
+
def record_handshake(self, success: bool):
|
|
109
|
+
"""Record a trust handshake."""
|
|
110
|
+
if not self._enabled:
|
|
111
|
+
return
|
|
112
|
+
status = "success" if success else "fail"
|
|
113
|
+
self.handshake_total.labels(status=status).inc()
|
|
114
|
+
|
|
115
|
+
def record_policy_violation(self, policy_id: str, agent_did: str):
|
|
116
|
+
"""Record a policy violation."""
|
|
117
|
+
if not self._enabled:
|
|
118
|
+
return
|
|
119
|
+
self.policy_violation_count.labels(
|
|
120
|
+
policy_id=policy_id,
|
|
121
|
+
agent_did=agent_did,
|
|
122
|
+
).inc()
|
|
123
|
+
|
|
124
|
+
def set_trust_score(self, agent_did: str, score: int):
|
|
125
|
+
"""Set trust score for an agent."""
|
|
126
|
+
if not self._enabled:
|
|
127
|
+
return
|
|
128
|
+
self.trust_score_gauge.labels(agent_did=agent_did).set(score)
|
|
129
|
+
|
|
130
|
+
def set_registry_size(self, status: str, count: int):
|
|
131
|
+
"""Set registry size."""
|
|
132
|
+
if not self._enabled:
|
|
133
|
+
return
|
|
134
|
+
self.registry_size.labels(status=status).set(count)
|
|
135
|
+
|
|
136
|
+
def record_api_request(
|
|
137
|
+
self,
|
|
138
|
+
method: str,
|
|
139
|
+
endpoint: str,
|
|
140
|
+
status: int,
|
|
141
|
+
duration: float,
|
|
142
|
+
):
|
|
143
|
+
"""Record API request."""
|
|
144
|
+
if not self._enabled:
|
|
145
|
+
return
|
|
146
|
+
self.api_request_duration.labels(
|
|
147
|
+
method=method,
|
|
148
|
+
endpoint=endpoint,
|
|
149
|
+
status=status,
|
|
150
|
+
).observe(duration)
|
|
151
|
+
|
|
152
|
+
def record_tool_call(
|
|
153
|
+
self,
|
|
154
|
+
agent_did: str,
|
|
155
|
+
tool_name: str,
|
|
156
|
+
success: bool,
|
|
157
|
+
):
|
|
158
|
+
"""Record a tool call."""
|
|
159
|
+
if not self._enabled:
|
|
160
|
+
return
|
|
161
|
+
status = "success" if success else "fail"
|
|
162
|
+
self.tool_call_total.labels(
|
|
163
|
+
agent_did=agent_did,
|
|
164
|
+
tool_name=tool_name,
|
|
165
|
+
status=status,
|
|
166
|
+
).inc()
|
|
167
|
+
|
|
168
|
+
def record_reward_signal(self, agent_did: str, dimension: str):
|
|
169
|
+
"""Record a reward signal."""
|
|
170
|
+
if not self._enabled:
|
|
171
|
+
return
|
|
172
|
+
self.reward_signal_total.labels(
|
|
173
|
+
agent_did=agent_did,
|
|
174
|
+
dimension=dimension,
|
|
175
|
+
).inc()
|
|
176
|
+
|
|
177
|
+
def record_audit_log(self, event_type: str, outcome: str):
|
|
178
|
+
"""Record an audit log entry."""
|
|
179
|
+
if not self._enabled:
|
|
180
|
+
return
|
|
181
|
+
self.audit_log_total.labels(
|
|
182
|
+
event_type=event_type,
|
|
183
|
+
outcome=outcome,
|
|
184
|
+
).inc()
|
|
185
|
+
|
|
186
|
+
def record_credential_issued(self, agent_did: str):
|
|
187
|
+
"""Record credential issuance."""
|
|
188
|
+
if not self._enabled:
|
|
189
|
+
return
|
|
190
|
+
self.credential_issued_total.labels(agent_did=agent_did).inc()
|
|
191
|
+
|
|
192
|
+
def record_credential_revoked(self, agent_did: str, reason: str):
|
|
193
|
+
"""Record credential revocation."""
|
|
194
|
+
if not self._enabled:
|
|
195
|
+
return
|
|
196
|
+
self.credential_revoked_total.labels(
|
|
197
|
+
agent_did=agent_did,
|
|
198
|
+
reason=reason,
|
|
199
|
+
).inc()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Global metrics collector instance
|
|
203
|
+
_metrics_collector: Optional[MetricsCollector] = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def setup_metrics() -> MetricsCollector:
|
|
207
|
+
"""
|
|
208
|
+
Setup Prometheus metrics.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
MetricsCollector instance
|
|
212
|
+
"""
|
|
213
|
+
global _metrics_collector
|
|
214
|
+
|
|
215
|
+
if _metrics_collector is None:
|
|
216
|
+
_metrics_collector = MetricsCollector()
|
|
217
|
+
|
|
218
|
+
return _metrics_collector
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_metrics() -> Optional[MetricsCollector]:
|
|
222
|
+
"""Get metrics collector instance."""
|
|
223
|
+
return _metrics_collector
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def start_metrics_server(port: int = 9090):
|
|
227
|
+
"""
|
|
228
|
+
Start Prometheus metrics HTTP server.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
port: Port to listen on (default: 9090)
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
from prometheus_client import start_http_server
|
|
235
|
+
start_http_server(port)
|
|
236
|
+
except ImportError:
|
|
237
|
+
pass
|