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.
@@ -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
@@ -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 common patterns
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