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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- 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
|
+
]
|