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,784 @@
1
+ """
2
+ Inter-agent trust boundaries for Proxilion.
3
+
4
+ This module provides trust-based authorization for inter-agent communication,
5
+ ensuring that delegation chains don't violate trust boundaries.
6
+
7
+ Quick Start:
8
+ >>> from proxilion.security import (
9
+ ... TrustLevel,
10
+ ... AgentIdentity,
11
+ ... TrustEnforcer,
12
+ ... )
13
+ >>>
14
+ >>> # Create trust enforcer with default boundaries
15
+ >>> enforcer = TrustEnforcer()
16
+ >>>
17
+ >>> # Register agents with their trust levels
18
+ >>> enforcer.register_agent(AgentIdentity(
19
+ ... agent_id="main_agent",
20
+ ... trust_level=TrustLevel.INTERNAL,
21
+ ... allowed_scopes={"read", "write", "admin"},
22
+ ... ))
23
+ >>> enforcer.register_agent(AgentIdentity(
24
+ ... agent_id="external_plugin",
25
+ ... trust_level=TrustLevel.EXTERNAL,
26
+ ... allowed_scopes={"read"},
27
+ ... ))
28
+ >>>
29
+ >>> # Check trust boundary
30
+ >>> allowed, requires_approval = enforcer.check_trust_boundary(
31
+ ... "main_agent", "external_plugin"
32
+ ... )
33
+ >>> if not allowed:
34
+ ... raise TrustBoundaryViolation("Not allowed")
35
+ >>> if requires_approval:
36
+ ... await request_approval()
37
+
38
+ Trust Levels:
39
+ - INTERNAL: Fully trusted internal agents (same organization).
40
+ - PARTNER: Trusted partner agents (shared service agreements).
41
+ - EXTERNAL: External agents with limited trust.
42
+ - UNTRUSTED: No trust, all operations denied.
43
+
44
+ Delegation:
45
+ >>> # Create a delegation token
46
+ >>> token = enforcer.create_delegation(
47
+ ... from_agent="main_agent",
48
+ ... to_agent="partner_agent",
49
+ ... scopes={"read", "write"},
50
+ ... ttl=3600, # 1 hour
51
+ ... )
52
+ >>>
53
+ >>> # Validate the delegation
54
+ >>> valid, reason = enforcer.validate_delegation(token)
55
+ >>> if not valid:
56
+ ... print(f"Invalid delegation: {reason}")
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import hashlib
62
+ import hmac
63
+ import secrets
64
+ import threading
65
+ from dataclasses import dataclass, field
66
+ from datetime import datetime, timedelta, timezone
67
+ from enum import IntEnum
68
+ from typing import Any
69
+
70
+ from proxilion.exceptions import ProxilionError
71
+
72
+
73
+ class TrustBoundaryViolation(ProxilionError):
74
+ """
75
+ Raised when a delegation crosses an invalid trust boundary.
76
+
77
+ Attributes:
78
+ reason: Description of the violation.
79
+ chain_depth: Current depth of the delegation chain.
80
+ from_level: Trust level of the issuer.
81
+ to_level: Trust level of the target.
82
+
83
+ Example:
84
+ >>> raise TrustBoundaryViolation(
85
+ ... reason="External agents cannot delegate to internal agents",
86
+ ... from_level=TrustLevel.EXTERNAL,
87
+ ... to_level=TrustLevel.INTERNAL,
88
+ ... )
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ reason: str,
94
+ chain_depth: int | None = None,
95
+ from_level: TrustLevel | None = None,
96
+ to_level: TrustLevel | None = None,
97
+ ):
98
+ self.reason = reason
99
+ self.chain_depth = chain_depth
100
+ self.from_level = from_level
101
+ self.to_level = to_level
102
+
103
+ details = {"reason": reason}
104
+ if chain_depth is not None:
105
+ details["chain_depth"] = chain_depth
106
+ if from_level is not None:
107
+ details["from_level"] = from_level.name
108
+ if to_level is not None:
109
+ details["to_level"] = to_level.name
110
+
111
+ super().__init__(f"Trust boundary violation: {reason}", details)
112
+
113
+
114
+ class TrustLevel(IntEnum):
115
+ """
116
+ Trust levels for agents.
117
+
118
+ Lower values indicate higher trust. This allows trust level
119
+ comparisons using standard comparison operators.
120
+
121
+ Example:
122
+ >>> TrustLevel.INTERNAL < TrustLevel.EXTERNAL
123
+ True
124
+ >>> # Trust can only decrease (increase in numeric value)
125
+ """
126
+
127
+ INTERNAL = 0
128
+ """Fully trusted internal agents (same organization)."""
129
+
130
+ PARTNER = 1
131
+ """Trusted partner agents with formal agreements."""
132
+
133
+ EXTERNAL = 2
134
+ """External agents with limited, verified trust."""
135
+
136
+ UNTRUSTED = 3
137
+ """No trust - all operations denied."""
138
+
139
+
140
+ @dataclass
141
+ class AgentIdentity:
142
+ """
143
+ Identity and trust attributes for an agent.
144
+
145
+ Attributes:
146
+ agent_id: Unique identifier for the agent.
147
+ trust_level: The agent's trust level.
148
+ allowed_scopes: Set of scopes this agent can access.
149
+ metadata: Additional metadata about the agent.
150
+
151
+ Example:
152
+ >>> agent = AgentIdentity(
153
+ ... agent_id="data_processor",
154
+ ... trust_level=TrustLevel.PARTNER,
155
+ ... allowed_scopes={"read", "transform"},
156
+ ... metadata={"organization": "partner_corp"},
157
+ ... )
158
+ """
159
+
160
+ agent_id: str
161
+ trust_level: TrustLevel
162
+ allowed_scopes: set[str] = field(default_factory=set)
163
+ metadata: dict[str, Any] = field(default_factory=dict)
164
+
165
+ def __hash__(self) -> int:
166
+ return hash(self.agent_id)
167
+
168
+ def to_dict(self) -> dict[str, Any]:
169
+ """Convert to dictionary for serialization."""
170
+ return {
171
+ "agent_id": self.agent_id,
172
+ "trust_level": self.trust_level.name,
173
+ "allowed_scopes": list(self.allowed_scopes),
174
+ "metadata": self.metadata,
175
+ }
176
+
177
+
178
+ @dataclass
179
+ class DelegationToken:
180
+ """
181
+ Token representing a delegation of authority from one agent to another.
182
+
183
+ Attributes:
184
+ token_id: Unique identifier for this token.
185
+ issuer: Agent ID that created this delegation.
186
+ subject: Agent ID that receives the delegation.
187
+ scopes: Set of scopes being delegated.
188
+ issued_at: When the token was issued.
189
+ expires_at: When the token expires.
190
+ chain: List of prior delegations in the chain.
191
+ signature: Cryptographic signature for integrity.
192
+
193
+ Example:
194
+ >>> token = DelegationToken(
195
+ ... token_id="tok_123",
196
+ ... issuer="main_agent",
197
+ ... subject="helper_agent",
198
+ ... scopes={"read"},
199
+ ... issued_at=datetime.now(timezone.utc),
200
+ ... expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
201
+ ... chain=[],
202
+ ... )
203
+ """
204
+
205
+ token_id: str
206
+ issuer: str
207
+ subject: str
208
+ scopes: set[str]
209
+ issued_at: datetime
210
+ expires_at: datetime
211
+ chain: list[DelegationToken] = field(default_factory=list)
212
+ signature: str | None = None
213
+
214
+ @property
215
+ def is_expired(self) -> bool:
216
+ """Check if the token has expired."""
217
+ return datetime.now(timezone.utc) >= self.expires_at
218
+
219
+ @property
220
+ def chain_depth(self) -> int:
221
+ """Get the total depth of the delegation chain."""
222
+ return len(self.chain) + 1
223
+
224
+ def to_dict(self) -> dict[str, Any]:
225
+ """Convert to dictionary for serialization."""
226
+ return {
227
+ "token_id": self.token_id,
228
+ "issuer": self.issuer,
229
+ "subject": self.subject,
230
+ "scopes": list(self.scopes),
231
+ "issued_at": self.issued_at.isoformat(),
232
+ "expires_at": self.expires_at.isoformat(),
233
+ "chain_depth": self.chain_depth,
234
+ "chain": [t.token_id for t in self.chain],
235
+ }
236
+
237
+
238
+ @dataclass
239
+ class TrustBoundary:
240
+ """
241
+ Definition of a trust boundary between trust levels.
242
+
243
+ Attributes:
244
+ from_level: Trust level of the initiator.
245
+ to_level: Trust level of the target (or "*" for any).
246
+ allowed: Whether this boundary crossing is allowed.
247
+ requires_approval: Whether human approval is required.
248
+
249
+ Example:
250
+ >>> # Internal can delegate to partner without approval
251
+ >>> boundary = TrustBoundary(
252
+ ... from_level=TrustLevel.INTERNAL,
253
+ ... to_level=TrustLevel.PARTNER,
254
+ ... allowed=True,
255
+ ... requires_approval=False,
256
+ ... )
257
+ """
258
+
259
+ from_level: TrustLevel
260
+ to_level: TrustLevel | str # str for "*" wildcard
261
+ allowed: bool
262
+ requires_approval: bool = False
263
+
264
+
265
+ # Default trust boundaries
266
+ DEFAULT_BOUNDARIES = [
267
+ # Internal can communicate with anyone (external requires approval)
268
+ TrustBoundary(TrustLevel.INTERNAL, TrustLevel.INTERNAL, allowed=True, requires_approval=False),
269
+ TrustBoundary(TrustLevel.INTERNAL, TrustLevel.PARTNER, allowed=True, requires_approval=False),
270
+ TrustBoundary(TrustLevel.INTERNAL, TrustLevel.EXTERNAL, allowed=True, requires_approval=True),
271
+ TrustBoundary(
272
+ TrustLevel.INTERNAL, TrustLevel.UNTRUSTED, allowed=False, requires_approval=False
273
+ ),
274
+
275
+ # Partner can communicate with internal (with approval) and other partners
276
+ TrustBoundary(TrustLevel.PARTNER, TrustLevel.INTERNAL, allowed=True, requires_approval=True),
277
+ TrustBoundary(TrustLevel.PARTNER, TrustLevel.PARTNER, allowed=True, requires_approval=False),
278
+ TrustBoundary(TrustLevel.PARTNER, TrustLevel.EXTERNAL, allowed=False, requires_approval=False),
279
+ TrustBoundary(TrustLevel.PARTNER, TrustLevel.UNTRUSTED, allowed=False, requires_approval=False),
280
+
281
+ # External can only communicate with each other
282
+ TrustBoundary(TrustLevel.EXTERNAL, TrustLevel.INTERNAL, allowed=False, requires_approval=False),
283
+ TrustBoundary(TrustLevel.EXTERNAL, TrustLevel.PARTNER, allowed=False, requires_approval=False),
284
+ TrustBoundary(TrustLevel.EXTERNAL, TrustLevel.EXTERNAL, allowed=True, requires_approval=False),
285
+ TrustBoundary(
286
+ TrustLevel.EXTERNAL, TrustLevel.UNTRUSTED, allowed=False, requires_approval=False
287
+ ),
288
+
289
+ # Untrusted cannot communicate with anyone
290
+ TrustBoundary(TrustLevel.UNTRUSTED, "*", allowed=False, requires_approval=False),
291
+ ]
292
+
293
+
294
+ class TrustEnforcer:
295
+ """
296
+ Main class for enforcing trust boundaries between agents.
297
+
298
+ Manages agent identities, validates delegations, and enforces
299
+ trust boundaries.
300
+
301
+ Example:
302
+ >>> enforcer = TrustEnforcer()
303
+ >>>
304
+ >>> # Register agents
305
+ >>> enforcer.register_agent(AgentIdentity(
306
+ ... agent_id="orchestrator",
307
+ ... trust_level=TrustLevel.INTERNAL,
308
+ ... allowed_scopes={"*"},
309
+ ... ))
310
+ >>>
311
+ >>> # Create delegation
312
+ >>> token = enforcer.create_delegation(
313
+ ... from_agent="orchestrator",
314
+ ... to_agent="worker",
315
+ ... scopes={"read", "write"},
316
+ ... ttl=3600,
317
+ ... )
318
+ >>>
319
+ >>> # Validate before use
320
+ >>> valid, reason = enforcer.validate_delegation(token)
321
+ """
322
+
323
+ def __init__(
324
+ self,
325
+ boundaries: list[TrustBoundary] | None = None,
326
+ max_chain_depth: int = 5,
327
+ signing_key: bytes | None = None,
328
+ ):
329
+ """
330
+ Initialize the trust enforcer.
331
+
332
+ Args:
333
+ boundaries: List of trust boundaries. Defaults to DEFAULT_BOUNDARIES.
334
+ max_chain_depth: Maximum allowed delegation chain depth.
335
+ signing_key: Secret key for signing tokens. Auto-generated if not provided.
336
+ """
337
+ self.boundaries = boundaries or DEFAULT_BOUNDARIES
338
+ self.max_chain_depth = max_chain_depth
339
+ self._signing_key = signing_key or secrets.token_bytes(32)
340
+
341
+ self._agents: dict[str, AgentIdentity] = {}
342
+ self._lock = threading.RLock()
343
+
344
+ # Build boundary lookup for fast access
345
+ self._boundary_map: dict[tuple[TrustLevel, TrustLevel | str], TrustBoundary] = {}
346
+ for boundary in self.boundaries:
347
+ key = (boundary.from_level, boundary.to_level)
348
+ self._boundary_map[key] = boundary
349
+
350
+ def register_agent(self, identity: AgentIdentity) -> None:
351
+ """
352
+ Register an agent with its identity.
353
+
354
+ Args:
355
+ identity: The agent identity to register.
356
+
357
+ Example:
358
+ >>> enforcer.register_agent(AgentIdentity(
359
+ ... agent_id="data_agent",
360
+ ... trust_level=TrustLevel.PARTNER,
361
+ ... allowed_scopes={"read", "write"},
362
+ ... ))
363
+ """
364
+ with self._lock:
365
+ self._agents[identity.agent_id] = identity
366
+
367
+ def unregister_agent(self, agent_id: str) -> bool:
368
+ """
369
+ Unregister an agent.
370
+
371
+ Args:
372
+ agent_id: The agent ID to unregister.
373
+
374
+ Returns:
375
+ True if the agent was unregistered, False if not found.
376
+ """
377
+ with self._lock:
378
+ if agent_id in self._agents:
379
+ del self._agents[agent_id]
380
+ return True
381
+ return False
382
+
383
+ def get_agent(self, agent_id: str) -> AgentIdentity | None:
384
+ """
385
+ Get an agent's identity.
386
+
387
+ Args:
388
+ agent_id: The agent ID to look up.
389
+
390
+ Returns:
391
+ The agent identity, or None if not found.
392
+ """
393
+ with self._lock:
394
+ return self._agents.get(agent_id)
395
+
396
+ def check_trust_boundary(
397
+ self,
398
+ from_agent: str,
399
+ to_agent: str,
400
+ ) -> tuple[bool, bool]:
401
+ """
402
+ Check if communication between two agents is allowed.
403
+
404
+ Args:
405
+ from_agent: The agent initiating communication.
406
+ to_agent: The target agent.
407
+
408
+ Returns:
409
+ Tuple of (allowed, requires_approval).
410
+
411
+ Raises:
412
+ ValueError: If either agent is not registered.
413
+
414
+ Example:
415
+ >>> allowed, needs_approval = enforcer.check_trust_boundary(
416
+ ... "internal_agent", "external_plugin"
417
+ ... )
418
+ >>> if not allowed:
419
+ ... raise TrustBoundaryViolation("Not allowed")
420
+ """
421
+ with self._lock:
422
+ from_identity = self._agents.get(from_agent)
423
+ to_identity = self._agents.get(to_agent)
424
+
425
+ if from_identity is None:
426
+ raise ValueError(f"Agent not registered: {from_agent}")
427
+ if to_identity is None:
428
+ raise ValueError(f"Agent not registered: {to_agent}")
429
+
430
+ return self._check_boundary(
431
+ from_identity.trust_level,
432
+ to_identity.trust_level,
433
+ )
434
+
435
+ def _check_boundary(
436
+ self,
437
+ from_level: TrustLevel,
438
+ to_level: TrustLevel,
439
+ ) -> tuple[bool, bool]:
440
+ """Check boundary between trust levels."""
441
+ # Check specific boundary first
442
+ key = (from_level, to_level)
443
+ if key in self._boundary_map:
444
+ boundary = self._boundary_map[key]
445
+ return boundary.allowed, boundary.requires_approval
446
+
447
+ # Check wildcard
448
+ wildcard_key = (from_level, "*")
449
+ if wildcard_key in self._boundary_map:
450
+ boundary = self._boundary_map[wildcard_key]
451
+ return boundary.allowed, boundary.requires_approval
452
+
453
+ # Default: deny
454
+ return False, False
455
+
456
+ def create_delegation(
457
+ self,
458
+ from_agent: str,
459
+ to_agent: str,
460
+ scopes: set[str],
461
+ ttl: int,
462
+ parent_token: DelegationToken | None = None,
463
+ ) -> DelegationToken:
464
+ """
465
+ Create a delegation token.
466
+
467
+ Args:
468
+ from_agent: The agent creating the delegation.
469
+ to_agent: The agent receiving the delegation.
470
+ scopes: Scopes to delegate.
471
+ ttl: Time-to-live in seconds.
472
+ parent_token: Optional parent token for chained delegation.
473
+
474
+ Returns:
475
+ The created delegation token.
476
+
477
+ Raises:
478
+ TrustBoundaryViolation: If the delegation violates trust rules.
479
+
480
+ Example:
481
+ >>> token = enforcer.create_delegation(
482
+ ... from_agent="main",
483
+ ... to_agent="helper",
484
+ ... scopes={"read"},
485
+ ... ttl=3600,
486
+ ... )
487
+ """
488
+ with self._lock:
489
+ from_identity = self._agents.get(from_agent)
490
+ to_identity = self._agents.get(to_agent)
491
+
492
+ if from_identity is None:
493
+ raise ValueError(f"Agent not registered: {from_agent}")
494
+ if to_identity is None:
495
+ raise ValueError(f"Agent not registered: {to_agent}")
496
+
497
+ # Check trust boundary
498
+ allowed, requires_approval = self._check_boundary(
499
+ from_identity.trust_level,
500
+ to_identity.trust_level,
501
+ )
502
+ if not allowed:
503
+ raise TrustBoundaryViolation(
504
+ f"Delegation from {from_identity.trust_level.name} to "
505
+ f"{to_identity.trust_level.name} is not allowed",
506
+ from_level=from_identity.trust_level,
507
+ to_level=to_identity.trust_level,
508
+ )
509
+
510
+ # Build chain
511
+ chain: list[DelegationToken] = []
512
+ if parent_token:
513
+ chain = parent_token.chain + [parent_token]
514
+
515
+ # Check chain depth
516
+ if len(chain) + 1 > self.max_chain_depth:
517
+ raise TrustBoundaryViolation(
518
+ f"Delegation chain too long: {len(chain) + 1} > {self.max_chain_depth}",
519
+ chain_depth=len(chain) + 1,
520
+ )
521
+
522
+ # Validate scope narrowing
523
+ if parent_token:
524
+ if not scopes.issubset(parent_token.scopes):
525
+ raise TrustBoundaryViolation(
526
+ f"Scope expansion not allowed: requesting {scopes - parent_token.scopes}",
527
+ )
528
+ else:
529
+ # Initial delegation - validate against from_agent's allowed scopes
530
+ if "*" not in from_identity.allowed_scopes:
531
+ if not scopes.issubset(from_identity.allowed_scopes):
532
+ raise TrustBoundaryViolation(
533
+ f"Agent {from_agent} cannot delegate scopes: "
534
+ f"{scopes - from_identity.allowed_scopes}",
535
+ )
536
+
537
+ # Validate against to_agent's allowed scopes
538
+ if "*" not in to_identity.allowed_scopes:
539
+ effective_scopes = scopes & to_identity.allowed_scopes
540
+ if not effective_scopes:
541
+ raise TrustBoundaryViolation(
542
+ f"No overlapping scopes for agent {to_agent}",
543
+ )
544
+ scopes = effective_scopes
545
+
546
+ # Create token
547
+ now = datetime.now(timezone.utc)
548
+ token = DelegationToken(
549
+ token_id=f"dtk_{secrets.token_hex(16)}",
550
+ issuer=from_agent,
551
+ subject=to_agent,
552
+ scopes=scopes,
553
+ issued_at=now,
554
+ expires_at=now + timedelta(seconds=ttl),
555
+ chain=chain,
556
+ )
557
+
558
+ # Sign the token
559
+ token.signature = self._sign_token(token)
560
+
561
+ return token
562
+
563
+ def validate_delegation(self, token: DelegationToken) -> tuple[bool, str | None]:
564
+ """
565
+ Validate a delegation token.
566
+
567
+ Args:
568
+ token: The token to validate.
569
+
570
+ Returns:
571
+ Tuple of (is_valid, error_reason).
572
+
573
+ Example:
574
+ >>> valid, reason = enforcer.validate_delegation(token)
575
+ >>> if not valid:
576
+ ... print(f"Invalid: {reason}")
577
+ """
578
+ with self._lock:
579
+ # Check expiry
580
+ if token.is_expired:
581
+ return False, "Token has expired"
582
+
583
+ # Check signature
584
+ if not self._verify_signature(token):
585
+ return False, "Invalid token signature"
586
+
587
+ # Check chain depth
588
+ if token.chain_depth > self.max_chain_depth:
589
+ return False, (
590
+ f"Chain depth {token.chain_depth} exceeds maximum {self.max_chain_depth}"
591
+ )
592
+
593
+ # Validate chain
594
+ return self._validate_chain(token)
595
+
596
+ def _validate_chain(self, token: DelegationToken) -> tuple[bool, str | None]:
597
+ """Validate the entire delegation chain."""
598
+ if not token.chain:
599
+ # No chain, just validate this token
600
+ return self._validate_single_hop(token.issuer, token.subject, token.scopes)
601
+
602
+ # Start with the first token in the chain
603
+ prev_level: TrustLevel | None = None
604
+ prev_scopes: set[str] | None = None # None means wildcard (all scopes)
605
+ prev_subject: str | None = None
606
+
607
+ for i, chain_token in enumerate(token.chain):
608
+ # Verify issuer matches previous subject
609
+ if prev_subject is not None and chain_token.issuer != prev_subject:
610
+ return False, f"Chain broken at hop {i}: expected issuer {prev_subject}"
611
+
612
+ # Check expiry
613
+ if chain_token.is_expired:
614
+ return False, f"Token in chain at hop {i} has expired"
615
+
616
+ # Get agent identities
617
+ issuer = self._agents.get(chain_token.issuer)
618
+ subject = self._agents.get(chain_token.subject)
619
+
620
+ if issuer is None:
621
+ return False, f"Unknown issuer in chain: {chain_token.issuer}"
622
+ if subject is None:
623
+ return False, f"Unknown subject in chain: {chain_token.subject}"
624
+
625
+ # Trust can only decrease (level increases)
626
+ if prev_level is not None and subject.trust_level < prev_level:
627
+ return False, (
628
+ f"Trust escalation detected at hop {i}: "
629
+ f"{prev_level.name} -> {subject.trust_level.name}"
630
+ )
631
+
632
+ # Scopes can only narrow (None/wildcard allows any scopes)
633
+ if prev_scopes is not None and "*" not in prev_scopes:
634
+ if not chain_token.scopes.issubset(prev_scopes):
635
+ return False, f"Scope expansion detected at hop {i}"
636
+
637
+ prev_level = subject.trust_level
638
+ prev_scopes = chain_token.scopes
639
+ prev_subject = chain_token.subject
640
+
641
+ # Validate final hop (from last chain token to current token)
642
+ if prev_subject is not None and token.issuer != prev_subject:
643
+ return False, f"Final hop broken: expected issuer {prev_subject}"
644
+
645
+ # Check scopes don't expand in final hop (prev_scopes can't be None here if we have a chain)
646
+ if prev_scopes is not None and "*" not in prev_scopes:
647
+ if not token.scopes.issubset(prev_scopes):
648
+ return False, "Scope expansion in final delegation"
649
+
650
+ # Validate trust boundary for final hop
651
+ issuer = self._agents.get(token.issuer)
652
+ subject = self._agents.get(token.subject)
653
+
654
+ if issuer is None:
655
+ return False, f"Unknown issuer: {token.issuer}"
656
+ if subject is None:
657
+ return False, f"Unknown subject: {token.subject}"
658
+
659
+ # Trust can only decrease
660
+ if prev_level is not None and subject.trust_level < prev_level:
661
+ return False, (
662
+ f"Trust escalation in final hop: "
663
+ f"{prev_level.name} -> {subject.trust_level.name}"
664
+ )
665
+
666
+ return True, None
667
+
668
+ def _validate_single_hop(
669
+ self,
670
+ from_agent: str,
671
+ to_agent: str,
672
+ scopes: set[str],
673
+ ) -> tuple[bool, str | None]:
674
+ """Validate a single delegation hop."""
675
+ from_identity = self._agents.get(from_agent)
676
+ to_identity = self._agents.get(to_agent)
677
+
678
+ if from_identity is None:
679
+ return False, f"Unknown issuer: {from_agent}"
680
+ if to_identity is None:
681
+ return False, f"Unknown subject: {to_agent}"
682
+
683
+ # Check trust boundary
684
+ allowed, _ = self._check_boundary(
685
+ from_identity.trust_level,
686
+ to_identity.trust_level,
687
+ )
688
+ if not allowed:
689
+ return False, (
690
+ f"Trust boundary violation: {from_identity.trust_level.name} -> "
691
+ f"{to_identity.trust_level.name}"
692
+ )
693
+
694
+ return True, None
695
+
696
+ def get_delegation_chain(self, token: DelegationToken) -> list[AgentIdentity]:
697
+ """
698
+ Get the full chain of agent identities from a delegation token.
699
+
700
+ Args:
701
+ token: The delegation token.
702
+
703
+ Returns:
704
+ List of agent identities in the chain, from root to subject.
705
+
706
+ Example:
707
+ >>> chain = enforcer.get_delegation_chain(token)
708
+ >>> for agent in chain:
709
+ ... print(f"{agent.agent_id} ({agent.trust_level.name})")
710
+ """
711
+ with self._lock:
712
+ identities: list[AgentIdentity] = []
713
+
714
+ # Add chain token identities
715
+ for chain_token in token.chain:
716
+ issuer = self._agents.get(chain_token.issuer)
717
+ if issuer:
718
+ identities.append(issuer)
719
+
720
+ # Add current token's issuer and subject
721
+ issuer = self._agents.get(token.issuer)
722
+ if issuer:
723
+ identities.append(issuer)
724
+
725
+ subject = self._agents.get(token.subject)
726
+ if subject:
727
+ identities.append(subject)
728
+
729
+ return identities
730
+
731
+ def _sign_token(self, token: DelegationToken) -> str:
732
+ """Generate signature for a token."""
733
+ scopes_str = str(sorted(token.scopes))
734
+ expires_str = token.expires_at.isoformat()
735
+ data = f"{token.token_id}:{token.issuer}:{token.subject}:{scopes_str}:{expires_str}"
736
+ signature = hmac.new(
737
+ self._signing_key,
738
+ data.encode(),
739
+ hashlib.sha256,
740
+ ).hexdigest()
741
+ return signature
742
+
743
+ def _verify_signature(self, token: DelegationToken) -> bool:
744
+ """Verify token signature."""
745
+ if not token.signature:
746
+ return False
747
+ expected = self._sign_token(token)
748
+ return hmac.compare_digest(token.signature, expected)
749
+
750
+ def get_effective_scopes(self, token: DelegationToken) -> set[str]:
751
+ """
752
+ Get the effective scopes for a delegation token.
753
+
754
+ Takes into account scope narrowing through the chain.
755
+
756
+ Args:
757
+ token: The delegation token.
758
+
759
+ Returns:
760
+ Set of effective scopes.
761
+ """
762
+ with self._lock:
763
+ scopes = token.scopes.copy()
764
+
765
+ # Intersect with subject's allowed scopes
766
+ subject = self._agents.get(token.subject)
767
+ if subject and "*" not in subject.allowed_scopes:
768
+ scopes &= subject.allowed_scopes
769
+
770
+ return scopes
771
+
772
+ def get_all_agents(self) -> list[AgentIdentity]:
773
+ """Get all registered agents."""
774
+ with self._lock:
775
+ return list(self._agents.values())
776
+
777
+ def get_agents_by_trust_level(self, trust_level: TrustLevel) -> list[AgentIdentity]:
778
+ """Get all agents at a specific trust level."""
779
+ with self._lock:
780
+ return [
781
+ agent
782
+ for agent in self._agents.values()
783
+ if agent.trust_level == trust_level
784
+ ]