proxilion 0.0.1__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.
Files changed (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,968 @@
1
+ """
2
+ Agent-to-Agent Trust Enforcement for Proxilion.
3
+
4
+ Addresses OWASP ASI07: Insecure Inter-Agent Communication.
5
+
6
+ This module provides cryptographic trust verification for multi-agent
7
+ systems, ensuring that:
8
+ - Agents can only communicate with authorized peers
9
+ - Messages are signed and verified
10
+ - Delegation chains are tracked and validated
11
+ - Trust levels control what actions agents can request
12
+
13
+ Example:
14
+ >>> from proxilion.security.agent_trust import (
15
+ ... AgentTrustManager,
16
+ ... AgentCredential,
17
+ ... DelegationChain,
18
+ ... TrustLevel,
19
+ ... )
20
+ >>>
21
+ >>> # Create trust manager
22
+ >>> manager = AgentTrustManager(secret_key="your-secret")
23
+ >>>
24
+ >>> # Register agents
25
+ >>> orchestrator = manager.register_agent(
26
+ ... agent_id="orchestrator",
27
+ ... trust_level=TrustLevel.FULL,
28
+ ... capabilities=["delegate", "execute_all"],
29
+ ... )
30
+ >>>
31
+ >>> worker = manager.register_agent(
32
+ ... agent_id="worker_1",
33
+ ... trust_level=TrustLevel.LIMITED,
34
+ ... capabilities=["read", "write"],
35
+ ... parent_agent="orchestrator",
36
+ ... )
37
+ >>>
38
+ >>> # Create signed message from orchestrator to worker
39
+ >>> message = manager.create_signed_message(
40
+ ... from_agent="orchestrator",
41
+ ... to_agent="worker_1",
42
+ ... action="execute",
43
+ ... payload={"task": "process_data"},
44
+ ... )
45
+ >>>
46
+ >>> # Verify and process on receiving end
47
+ >>> verified = manager.verify_message(message)
48
+ >>> if verified.valid:
49
+ ... process_task(message.payload)
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import hashlib
55
+ import hmac
56
+ import json
57
+ import logging
58
+ import secrets
59
+ import threading
60
+ import time
61
+ import uuid
62
+ from dataclasses import dataclass, field
63
+ from datetime import datetime, timedelta, timezone
64
+ from enum import IntEnum
65
+ from typing import Any
66
+
67
+ from proxilion.exceptions import AgentTrustError
68
+
69
+ logger = logging.getLogger(__name__)
70
+
71
+
72
+ class TrustLevel(IntEnum):
73
+ """
74
+ Trust levels for agents.
75
+
76
+ Higher levels can delegate to lower levels but not vice versa.
77
+ """
78
+
79
+ UNTRUSTED = 0
80
+ """No trust - cannot communicate."""
81
+
82
+ MINIMAL = 1
83
+ """Can receive read-only requests."""
84
+
85
+ LIMITED = 2
86
+ """Can receive read/write requests within scope."""
87
+
88
+ STANDARD = 3
89
+ """Normal agent trust level."""
90
+
91
+ ELEVATED = 4
92
+ """Can delegate to standard agents."""
93
+
94
+ FULL = 5
95
+ """Full trust - can delegate to all levels."""
96
+
97
+ SYSTEM = 6
98
+ """System-level trust (internal use)."""
99
+
100
+
101
+ @dataclass
102
+ class AgentCredential:
103
+ """
104
+ Cryptographic credential for an agent.
105
+
106
+ Contains the agent's identity, trust level, capabilities,
107
+ and cryptographic material for signing messages.
108
+ """
109
+
110
+ agent_id: str
111
+ trust_level: TrustLevel
112
+ capabilities: set[str]
113
+ public_key: str # Hex-encoded public identifier
114
+ parent_agent: str | None = None
115
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
116
+ expires_at: datetime | None = None
117
+ metadata: dict[str, Any] = field(default_factory=dict)
118
+
119
+ # Internal secret (never serialized)
120
+ _secret: str = field(default="", repr=False)
121
+
122
+ def is_expired(self) -> bool:
123
+ """Check if credential has expired."""
124
+ if self.expires_at is None:
125
+ return False
126
+ return datetime.now(timezone.utc) > self.expires_at
127
+
128
+ def has_capability(self, capability: str) -> bool:
129
+ """Check if agent has a specific capability."""
130
+ # Wildcard capability
131
+ if "*" in self.capabilities:
132
+ return True
133
+ # Exact match
134
+ if capability in self.capabilities:
135
+ return True
136
+ # Prefix match (e.g., "read" matches "read:documents")
137
+ for cap in self.capabilities:
138
+ if capability.startswith(cap + ":"):
139
+ return True
140
+ return False
141
+
142
+ def can_delegate_to(self, other: AgentCredential) -> bool:
143
+ """Check if this agent can delegate to another."""
144
+ # Must have higher trust level
145
+ if self.trust_level <= other.trust_level:
146
+ return False
147
+ # Must have delegate capability
148
+ if not self.has_capability("delegate"):
149
+ return False
150
+ return True
151
+
152
+ def to_dict(self) -> dict[str, Any]:
153
+ """Serialize to dict (excludes secret)."""
154
+ return {
155
+ "agent_id": self.agent_id,
156
+ "trust_level": self.trust_level.value,
157
+ "capabilities": list(self.capabilities),
158
+ "public_key": self.public_key,
159
+ "parent_agent": self.parent_agent,
160
+ "created_at": self.created_at.isoformat(),
161
+ "expires_at": self.expires_at.isoformat() if self.expires_at else None,
162
+ "metadata": self.metadata,
163
+ }
164
+
165
+
166
+ @dataclass
167
+ class DelegationToken:
168
+ """
169
+ A token that grants delegated authority from one agent to another.
170
+
171
+ Delegation tokens are time-limited and scope-limited.
172
+ """
173
+
174
+ token_id: str
175
+ issuer_agent: str
176
+ delegate_agent: str
177
+ granted_capabilities: set[str]
178
+ issued_at: datetime
179
+ expires_at: datetime
180
+ signature: str
181
+ chain_depth: int = 1 # How many delegations deep
182
+ max_chain_depth: int = 3 # Maximum delegation chain length
183
+ constraints: dict[str, Any] = field(default_factory=dict)
184
+
185
+ def is_expired(self) -> bool:
186
+ """Check if token has expired."""
187
+ return datetime.now(timezone.utc) > self.expires_at
188
+
189
+ def is_valid(self) -> bool:
190
+ """Basic validity check (not signature verification)."""
191
+ if self.is_expired():
192
+ return False
193
+ if self.chain_depth > self.max_chain_depth:
194
+ return False
195
+ return True
196
+
197
+ def to_dict(self) -> dict[str, Any]:
198
+ """Serialize to dict."""
199
+ return {
200
+ "token_id": self.token_id,
201
+ "issuer_agent": self.issuer_agent,
202
+ "delegate_agent": self.delegate_agent,
203
+ "granted_capabilities": list(self.granted_capabilities),
204
+ "issued_at": self.issued_at.isoformat(),
205
+ "expires_at": self.expires_at.isoformat(),
206
+ "signature": self.signature,
207
+ "chain_depth": self.chain_depth,
208
+ "max_chain_depth": self.max_chain_depth,
209
+ "constraints": self.constraints,
210
+ }
211
+
212
+
213
+ @dataclass
214
+ class SignedMessage:
215
+ """
216
+ A cryptographically signed message between agents.
217
+ """
218
+
219
+ message_id: str
220
+ from_agent: str
221
+ to_agent: str
222
+ action: str
223
+ payload: dict[str, Any]
224
+ timestamp: float
225
+ signature: str
226
+ delegation_token: DelegationToken | None = None
227
+ reply_to: str | None = None # ID of message being replied to
228
+ metadata: dict[str, Any] = field(default_factory=dict)
229
+
230
+ def to_dict(self) -> dict[str, Any]:
231
+ """Serialize to dict."""
232
+ return {
233
+ "message_id": self.message_id,
234
+ "from_agent": self.from_agent,
235
+ "to_agent": self.to_agent,
236
+ "action": self.action,
237
+ "payload": self.payload,
238
+ "timestamp": self.timestamp,
239
+ "signature": self.signature,
240
+ "delegation_token": self.delegation_token.to_dict() if self.delegation_token else None,
241
+ "reply_to": self.reply_to,
242
+ "metadata": self.metadata,
243
+ }
244
+
245
+
246
+ @dataclass
247
+ class VerificationResult:
248
+ """Result of message or token verification."""
249
+
250
+ valid: bool
251
+ error: str | None = None
252
+ warnings: list[str] = field(default_factory=list)
253
+ verified_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
254
+
255
+ def to_dict(self) -> dict[str, Any]:
256
+ """Serialize to dict."""
257
+ return {
258
+ "valid": self.valid,
259
+ "error": self.error,
260
+ "warnings": self.warnings,
261
+ "verified_at": self.verified_at.isoformat(),
262
+ }
263
+
264
+
265
+ class DelegationChain:
266
+ """
267
+ Tracks the chain of delegations for an action.
268
+
269
+ Ensures that delegation chains don't exceed maximum depth
270
+ and that all delegations in the chain are valid.
271
+ """
272
+
273
+ def __init__(self, max_depth: int = 3) -> None:
274
+ """
275
+ Initialize delegation chain.
276
+
277
+ Args:
278
+ max_depth: Maximum allowed delegation depth.
279
+ """
280
+ self.max_depth = max_depth
281
+ self._chain: list[DelegationToken] = []
282
+
283
+ def add(self, token: DelegationToken) -> bool:
284
+ """
285
+ Add a delegation to the chain.
286
+
287
+ Returns:
288
+ True if added successfully, False if chain would be too deep.
289
+ """
290
+ if len(self._chain) >= self.max_depth:
291
+ return False
292
+ self._chain.append(token)
293
+ return True
294
+
295
+ def validate(self) -> tuple[bool, str | None]:
296
+ """
297
+ Validate the entire chain.
298
+
299
+ Returns:
300
+ Tuple of (valid, error message or None).
301
+ """
302
+ if not self._chain:
303
+ return True, None
304
+
305
+ # Check depth
306
+ if len(self._chain) > self.max_depth:
307
+ return False, f"Chain depth {len(self._chain)} exceeds max {self.max_depth}"
308
+
309
+ # Check each token
310
+ for i, token in enumerate(self._chain):
311
+ if token.is_expired():
312
+ return False, f"Token at position {i} has expired"
313
+ if not token.is_valid():
314
+ return False, f"Token at position {i} is invalid"
315
+
316
+ # Check chain continuity
317
+ if i > 0:
318
+ prev_token = self._chain[i - 1]
319
+ if token.issuer_agent != prev_token.delegate_agent:
320
+ return False, f"Chain break at position {i}: {prev_token.delegate_agent} != {token.issuer_agent}"
321
+
322
+ return True, None
323
+
324
+ @property
325
+ def depth(self) -> int:
326
+ """Current chain depth."""
327
+ return len(self._chain)
328
+
329
+ @property
330
+ def tokens(self) -> list[DelegationToken]:
331
+ """Get all tokens in chain."""
332
+ return list(self._chain)
333
+
334
+ def get_effective_capabilities(self) -> set[str]:
335
+ """Get capabilities available at end of chain (intersection of all)."""
336
+ if not self._chain:
337
+ return set()
338
+
339
+ caps = self._chain[0].granted_capabilities.copy()
340
+ for token in self._chain[1:]:
341
+ caps &= token.granted_capabilities
342
+
343
+ return caps
344
+
345
+
346
+ class AgentTrustManager:
347
+ """
348
+ Manages trust relationships between agents.
349
+
350
+ Provides:
351
+ - Agent registration with cryptographic credentials
352
+ - Message signing and verification
353
+ - Delegation token creation and validation
354
+ - Trust level enforcement
355
+
356
+ Example:
357
+ >>> manager = AgentTrustManager(secret_key="master-secret")
358
+ >>>
359
+ >>> # Register agents
360
+ >>> manager.register_agent("orchestrator", TrustLevel.FULL, {"*"})
361
+ >>> manager.register_agent("worker", TrustLevel.LIMITED, {"read", "write"})
362
+ >>>
363
+ >>> # Create delegation
364
+ >>> token = manager.create_delegation(
365
+ ... from_agent="orchestrator",
366
+ ... to_agent="worker",
367
+ ... capabilities={"read"},
368
+ ... ttl_seconds=3600,
369
+ ... )
370
+ >>>
371
+ >>> # Send signed message
372
+ >>> message = manager.create_signed_message(
373
+ ... from_agent="orchestrator",
374
+ ... to_agent="worker",
375
+ ... action="read_file",
376
+ ... payload={"path": "/data/file.txt"},
377
+ ... )
378
+ >>>
379
+ >>> # Verify on receiving end
380
+ >>> result = manager.verify_message(message)
381
+ """
382
+
383
+ def __init__(
384
+ self,
385
+ secret_key: str | bytes,
386
+ default_token_ttl: int = 3600,
387
+ max_delegation_depth: int = 3,
388
+ require_explicit_trust: bool = True,
389
+ ) -> None:
390
+ """
391
+ Initialize the trust manager.
392
+
393
+ Args:
394
+ secret_key: Master secret for deriving agent keys.
395
+ default_token_ttl: Default TTL for delegation tokens (seconds).
396
+ max_delegation_depth: Maximum delegation chain depth.
397
+ require_explicit_trust: If True, agents must be explicitly registered.
398
+ """
399
+ if isinstance(secret_key, str):
400
+ secret_key = secret_key.encode()
401
+
402
+ self._master_secret = secret_key
403
+ self._default_token_ttl = default_token_ttl
404
+ self._max_delegation_depth = max_delegation_depth
405
+ self._require_explicit_trust = require_explicit_trust
406
+
407
+ self._agents: dict[str, AgentCredential] = {}
408
+ self._delegation_tokens: dict[str, DelegationToken] = {}
409
+ self._revoked_tokens: set[str] = set()
410
+ self._message_nonces: set[str] = set() # Replay protection
411
+ self._lock = threading.RLock()
412
+
413
+ logger.debug("AgentTrustManager initialized")
414
+
415
+ def _derive_agent_secret(self, agent_id: str) -> str:
416
+ """Derive a unique secret for an agent."""
417
+ return hmac.new(
418
+ self._master_secret,
419
+ f"agent:{agent_id}".encode(),
420
+ hashlib.sha256,
421
+ ).hexdigest()
422
+
423
+ def _derive_public_key(self, secret: str) -> str:
424
+ """Derive public key from secret."""
425
+ return hashlib.sha256(secret.encode()).hexdigest()[:32]
426
+
427
+ def register_agent(
428
+ self,
429
+ agent_id: str,
430
+ trust_level: TrustLevel,
431
+ capabilities: set[str] | list[str],
432
+ parent_agent: str | None = None,
433
+ ttl_seconds: int | None = None,
434
+ metadata: dict[str, Any] | None = None,
435
+ ) -> AgentCredential:
436
+ """
437
+ Register an agent with the trust manager.
438
+
439
+ Args:
440
+ agent_id: Unique identifier for the agent.
441
+ trust_level: Trust level for the agent.
442
+ capabilities: Set of capabilities the agent has.
443
+ parent_agent: Optional parent agent (for hierarchical trust).
444
+ ttl_seconds: Optional credential TTL.
445
+ metadata: Optional metadata.
446
+
447
+ Returns:
448
+ AgentCredential for the registered agent.
449
+
450
+ Raises:
451
+ AgentTrustError: If agent already registered or invalid parent.
452
+ """
453
+ with self._lock:
454
+ if agent_id in self._agents:
455
+ raise AgentTrustError(
456
+ source_agent=agent_id,
457
+ target_agent="",
458
+ reason=f"Agent '{agent_id}' is already registered",
459
+ )
460
+
461
+ # Validate parent
462
+ if parent_agent and parent_agent not in self._agents:
463
+ raise AgentTrustError(
464
+ source_agent=agent_id,
465
+ target_agent=parent_agent,
466
+ reason=f"Parent agent '{parent_agent}' not found",
467
+ )
468
+
469
+ # Check parent has higher trust
470
+ if parent_agent:
471
+ parent = self._agents[parent_agent]
472
+ if parent.trust_level <= trust_level:
473
+ raise AgentTrustError(
474
+ source_agent=agent_id,
475
+ target_agent=parent_agent,
476
+ reason="Child agent cannot have equal or higher trust than parent",
477
+ )
478
+
479
+ # Derive credentials
480
+ secret = self._derive_agent_secret(agent_id)
481
+ public_key = self._derive_public_key(secret)
482
+
483
+ expires_at = None
484
+ if ttl_seconds:
485
+ expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)
486
+
487
+ if isinstance(capabilities, list):
488
+ capabilities = set(capabilities)
489
+
490
+ credential = AgentCredential(
491
+ agent_id=agent_id,
492
+ trust_level=trust_level,
493
+ capabilities=capabilities,
494
+ public_key=public_key,
495
+ parent_agent=parent_agent,
496
+ expires_at=expires_at,
497
+ metadata=metadata or {},
498
+ _secret=secret,
499
+ )
500
+
501
+ self._agents[agent_id] = credential
502
+ logger.info(f"Registered agent: {agent_id} (trust={trust_level.name})")
503
+
504
+ return credential
505
+
506
+ def unregister_agent(self, agent_id: str) -> bool:
507
+ """
508
+ Unregister an agent.
509
+
510
+ Args:
511
+ agent_id: Agent to unregister.
512
+
513
+ Returns:
514
+ True if agent was unregistered.
515
+ """
516
+ with self._lock:
517
+ if agent_id in self._agents:
518
+ del self._agents[agent_id]
519
+
520
+ # Revoke all tokens issued by or to this agent
521
+ tokens_to_revoke = [
522
+ tid for tid, token in self._delegation_tokens.items()
523
+ if token.issuer_agent == agent_id or token.delegate_agent == agent_id
524
+ ]
525
+ for tid in tokens_to_revoke:
526
+ self._revoked_tokens.add(tid)
527
+ del self._delegation_tokens[tid]
528
+
529
+ logger.info(f"Unregistered agent: {agent_id}")
530
+ return True
531
+ return False
532
+
533
+ def get_agent(self, agent_id: str) -> AgentCredential | None:
534
+ """Get an agent's credential."""
535
+ with self._lock:
536
+ return self._agents.get(agent_id)
537
+
538
+ def create_delegation(
539
+ self,
540
+ from_agent: str,
541
+ to_agent: str,
542
+ capabilities: set[str] | list[str],
543
+ ttl_seconds: int | None = None,
544
+ constraints: dict[str, Any] | None = None,
545
+ ) -> DelegationToken:
546
+ """
547
+ Create a delegation token from one agent to another.
548
+
549
+ Args:
550
+ from_agent: Agent issuing the delegation.
551
+ to_agent: Agent receiving the delegation.
552
+ capabilities: Capabilities being delegated.
553
+ ttl_seconds: Token lifetime (default: manager default).
554
+ constraints: Optional constraints on the delegation.
555
+
556
+ Returns:
557
+ DelegationToken that can be used for delegated actions.
558
+
559
+ Raises:
560
+ AgentTrustError: If delegation is not allowed.
561
+ """
562
+ with self._lock:
563
+ # Validate agents
564
+ issuer = self._agents.get(from_agent)
565
+ delegate = self._agents.get(to_agent)
566
+
567
+ if not issuer:
568
+ raise AgentTrustError(
569
+ source_agent=from_agent,
570
+ target_agent=to_agent,
571
+ reason=f"Issuer agent '{from_agent}' not found",
572
+ )
573
+
574
+ if not delegate:
575
+ raise AgentTrustError(
576
+ source_agent=from_agent,
577
+ target_agent=to_agent,
578
+ reason=f"Delegate agent '{to_agent}' not found",
579
+ )
580
+
581
+ if issuer.is_expired():
582
+ raise AgentTrustError(
583
+ source_agent=from_agent,
584
+ target_agent=to_agent,
585
+ reason="Issuer credential has expired",
586
+ )
587
+
588
+ if not issuer.can_delegate_to(delegate):
589
+ raise AgentTrustError(
590
+ source_agent=from_agent,
591
+ target_agent=to_agent,
592
+ reason=f"Agent '{from_agent}' cannot delegate to '{to_agent}'",
593
+ )
594
+
595
+ # Normalize capabilities
596
+ if isinstance(capabilities, list):
597
+ capabilities = set(capabilities)
598
+
599
+ # Can only delegate capabilities the issuer has
600
+ invalid_caps = capabilities - issuer.capabilities
601
+ if invalid_caps and "*" not in issuer.capabilities:
602
+ raise AgentTrustError(
603
+ source_agent=from_agent,
604
+ target_agent=to_agent,
605
+ reason=f"Cannot delegate capabilities not owned: {invalid_caps}",
606
+ )
607
+
608
+ # Create token
609
+ ttl = ttl_seconds or self._default_token_ttl
610
+ now = datetime.now(timezone.utc)
611
+ token_id = str(uuid.uuid4())
612
+
613
+ # Sign the token
614
+ token_data = f"{token_id}|{from_agent}|{to_agent}|{sorted(capabilities)}|{now.isoformat()}"
615
+ signature = hmac.new(
616
+ issuer._secret.encode(),
617
+ token_data.encode(),
618
+ hashlib.sha256,
619
+ ).hexdigest()
620
+
621
+ token = DelegationToken(
622
+ token_id=token_id,
623
+ issuer_agent=from_agent,
624
+ delegate_agent=to_agent,
625
+ granted_capabilities=capabilities,
626
+ issued_at=now,
627
+ expires_at=now + timedelta(seconds=ttl),
628
+ signature=signature,
629
+ chain_depth=1,
630
+ max_chain_depth=self._max_delegation_depth,
631
+ constraints=constraints or {},
632
+ )
633
+
634
+ self._delegation_tokens[token_id] = token
635
+ logger.debug(f"Created delegation: {from_agent} -> {to_agent} ({capabilities})")
636
+
637
+ return token
638
+
639
+ def revoke_delegation(self, token_id: str) -> bool:
640
+ """
641
+ Revoke a delegation token.
642
+
643
+ Args:
644
+ token_id: Token to revoke.
645
+
646
+ Returns:
647
+ True if token was revoked.
648
+ """
649
+ with self._lock:
650
+ if token_id in self._delegation_tokens:
651
+ self._revoked_tokens.add(token_id)
652
+ del self._delegation_tokens[token_id]
653
+ logger.info(f"Revoked delegation token: {token_id}")
654
+ return True
655
+ return False
656
+
657
+ def create_signed_message(
658
+ self,
659
+ from_agent: str,
660
+ to_agent: str,
661
+ action: str,
662
+ payload: dict[str, Any],
663
+ delegation_token: DelegationToken | None = None,
664
+ reply_to: str | None = None,
665
+ metadata: dict[str, Any] | None = None,
666
+ ) -> SignedMessage:
667
+ """
668
+ Create a signed message from one agent to another.
669
+
670
+ Args:
671
+ from_agent: Sending agent.
672
+ to_agent: Receiving agent.
673
+ action: Action being requested.
674
+ payload: Message payload.
675
+ delegation_token: Optional delegation token for delegated actions.
676
+ reply_to: Message ID being replied to.
677
+ metadata: Optional metadata.
678
+
679
+ Returns:
680
+ SignedMessage that can be verified by the receiver.
681
+
682
+ Raises:
683
+ AgentTrustError: If agent not found or expired.
684
+ """
685
+ with self._lock:
686
+ sender = self._agents.get(from_agent)
687
+ if not sender:
688
+ raise AgentTrustError(
689
+ source_agent=from_agent,
690
+ target_agent=to_agent,
691
+ reason=f"Sender agent '{from_agent}' not found",
692
+ )
693
+
694
+ if sender.is_expired():
695
+ raise AgentTrustError(
696
+ source_agent=from_agent,
697
+ target_agent=to_agent,
698
+ reason="Sender credential has expired",
699
+ )
700
+
701
+ # Create message
702
+ message_id = str(uuid.uuid4())
703
+ timestamp = time.time()
704
+
705
+ # Build signature data
706
+ sig_data = (
707
+ f"{message_id}|{from_agent}|{to_agent}|{action}|"
708
+ f"{json.dumps(payload, sort_keys=True)}|{timestamp}"
709
+ )
710
+ signature = hmac.new(
711
+ sender._secret.encode(),
712
+ sig_data.encode(),
713
+ hashlib.sha256,
714
+ ).hexdigest()
715
+
716
+ return SignedMessage(
717
+ message_id=message_id,
718
+ from_agent=from_agent,
719
+ to_agent=to_agent,
720
+ action=action,
721
+ payload=payload,
722
+ timestamp=timestamp,
723
+ signature=signature,
724
+ delegation_token=delegation_token,
725
+ reply_to=reply_to,
726
+ metadata=metadata or {},
727
+ )
728
+
729
+ def verify_message(
730
+ self,
731
+ message: SignedMessage,
732
+ check_replay: bool = True,
733
+ max_age_seconds: float = 300.0,
734
+ ) -> VerificationResult:
735
+ """
736
+ Verify a signed message.
737
+
738
+ Args:
739
+ message: Message to verify.
740
+ check_replay: Check for replay attacks.
741
+ max_age_seconds: Maximum message age.
742
+
743
+ Returns:
744
+ VerificationResult indicating if message is valid.
745
+ """
746
+ warnings: list[str] = []
747
+
748
+ with self._lock:
749
+ # Check replay
750
+ if check_replay:
751
+ if message.message_id in self._message_nonces:
752
+ return VerificationResult(
753
+ valid=False,
754
+ error="Replay attack detected: message already processed",
755
+ )
756
+
757
+ # Check message age
758
+ age = time.time() - message.timestamp
759
+ if age > max_age_seconds:
760
+ return VerificationResult(
761
+ valid=False,
762
+ error=f"Message too old: {age:.1f}s > {max_age_seconds}s",
763
+ )
764
+ if age < -60: # Allow 60s clock skew
765
+ return VerificationResult(
766
+ valid=False,
767
+ error="Message timestamp in the future",
768
+ )
769
+
770
+ # Get sender
771
+ sender = self._agents.get(message.from_agent)
772
+ if not sender:
773
+ if self._require_explicit_trust:
774
+ return VerificationResult(
775
+ valid=False,
776
+ error=f"Unknown sender: {message.from_agent}",
777
+ )
778
+ else:
779
+ warnings.append(f"Sender {message.from_agent} not registered")
780
+
781
+ # Check sender credentials
782
+ if sender:
783
+ if sender.is_expired():
784
+ return VerificationResult(
785
+ valid=False,
786
+ error="Sender credential has expired",
787
+ )
788
+
789
+ # Verify signature
790
+ sig_data = (
791
+ f"{message.message_id}|{message.from_agent}|{message.to_agent}|"
792
+ f"{message.action}|{json.dumps(message.payload, sort_keys=True)}|"
793
+ f"{message.timestamp}"
794
+ )
795
+ expected_sig = hmac.new(
796
+ sender._secret.encode(),
797
+ sig_data.encode(),
798
+ hashlib.sha256,
799
+ ).hexdigest()
800
+
801
+ if not hmac.compare_digest(expected_sig, message.signature):
802
+ return VerificationResult(
803
+ valid=False,
804
+ error="Invalid signature",
805
+ )
806
+
807
+ # Check receiver exists
808
+ receiver = self._agents.get(message.to_agent)
809
+ if not receiver and self._require_explicit_trust:
810
+ return VerificationResult(
811
+ valid=False,
812
+ error=f"Unknown receiver: {message.to_agent}",
813
+ )
814
+
815
+ # Check trust levels allow communication
816
+ if sender and receiver:
817
+ if sender.trust_level == TrustLevel.UNTRUSTED:
818
+ return VerificationResult(
819
+ valid=False,
820
+ error="Sender has UNTRUSTED trust level",
821
+ )
822
+
823
+ # Check if action is allowed
824
+ if not sender.has_capability(message.action):
825
+ # Check delegation token
826
+ if message.delegation_token:
827
+ token = message.delegation_token
828
+ if token.token_id in self._revoked_tokens:
829
+ return VerificationResult(
830
+ valid=False,
831
+ error="Delegation token has been revoked",
832
+ )
833
+ if token.is_expired():
834
+ return VerificationResult(
835
+ valid=False,
836
+ error="Delegation token has expired",
837
+ )
838
+ if message.action not in token.granted_capabilities:
839
+ if "*" not in token.granted_capabilities:
840
+ return VerificationResult(
841
+ valid=False,
842
+ error=f"Action '{message.action}' not in delegation",
843
+ )
844
+ else:
845
+ return VerificationResult(
846
+ valid=False,
847
+ error=f"Sender lacks capability for action '{message.action}'",
848
+ )
849
+
850
+ # Record nonce for replay protection
851
+ if check_replay:
852
+ self._message_nonces.add(message.message_id)
853
+ # Cleanup old nonces (keep last 10000)
854
+ if len(self._message_nonces) > 10000:
855
+ # Remove oldest (this is approximate but effective)
856
+ to_remove = list(self._message_nonces)[:5000]
857
+ for nonce in to_remove:
858
+ self._message_nonces.discard(nonce)
859
+
860
+ return VerificationResult(valid=True, warnings=warnings)
861
+
862
+ def verify_delegation_chain(self, chain: DelegationChain) -> VerificationResult:
863
+ """
864
+ Verify a delegation chain.
865
+
866
+ Args:
867
+ chain: The delegation chain to verify.
868
+
869
+ Returns:
870
+ VerificationResult for the chain.
871
+ """
872
+ valid, error = chain.validate()
873
+ if not valid:
874
+ return VerificationResult(valid=False, error=error)
875
+
876
+ # Verify each token's signature
877
+ with self._lock:
878
+ for token in chain.tokens:
879
+ if token.token_id in self._revoked_tokens:
880
+ return VerificationResult(
881
+ valid=False,
882
+ error=f"Token {token.token_id} has been revoked",
883
+ )
884
+
885
+ issuer = self._agents.get(token.issuer_agent)
886
+ if not issuer:
887
+ return VerificationResult(
888
+ valid=False,
889
+ error=f"Issuer {token.issuer_agent} not found",
890
+ )
891
+
892
+ # Verify signature
893
+ token_data = (
894
+ f"{token.token_id}|{token.issuer_agent}|{token.delegate_agent}|"
895
+ f"{sorted(token.granted_capabilities)}|{token.issued_at.isoformat()}"
896
+ )
897
+ expected_sig = hmac.new(
898
+ issuer._secret.encode(),
899
+ token_data.encode(),
900
+ hashlib.sha256,
901
+ ).hexdigest()
902
+
903
+ if not hmac.compare_digest(expected_sig, token.signature):
904
+ return VerificationResult(
905
+ valid=False,
906
+ error=f"Invalid signature on token {token.token_id}",
907
+ )
908
+
909
+ return VerificationResult(valid=True)
910
+
911
+ def get_registered_agents(self) -> list[str]:
912
+ """Get list of registered agent IDs."""
913
+ with self._lock:
914
+ return list(self._agents.keys())
915
+
916
+ def get_trust_level(self, agent_id: str) -> TrustLevel | None:
917
+ """Get an agent's trust level."""
918
+ with self._lock:
919
+ agent = self._agents.get(agent_id)
920
+ return agent.trust_level if agent else None
921
+
922
+ def cleanup_expired(self) -> int:
923
+ """
924
+ Clean up expired credentials and tokens.
925
+
926
+ Returns:
927
+ Number of items cleaned up.
928
+ """
929
+ count = 0
930
+ now = datetime.now(timezone.utc)
931
+
932
+ with self._lock:
933
+ # Clean expired agents
934
+ expired_agents = [
935
+ aid for aid, agent in self._agents.items()
936
+ if agent.expires_at and agent.expires_at < now
937
+ ]
938
+ for aid in expired_agents:
939
+ del self._agents[aid]
940
+ count += 1
941
+
942
+ # Clean expired tokens
943
+ expired_tokens = [
944
+ tid for tid, token in self._delegation_tokens.items()
945
+ if token.expires_at < now
946
+ ]
947
+ for tid in expired_tokens:
948
+ del self._delegation_tokens[tid]
949
+ count += 1
950
+
951
+ if count > 0:
952
+ logger.debug(f"Cleaned up {count} expired items")
953
+
954
+ return count
955
+
956
+
957
+ # Convenience exports
958
+ __all__ = [
959
+ # Core classes
960
+ "AgentTrustManager",
961
+ "AgentCredential",
962
+ "DelegationToken",
963
+ "DelegationChain",
964
+ "SignedMessage",
965
+ "VerificationResult",
966
+ # Enums
967
+ "TrustLevel",
968
+ ]