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
proxilion/exceptions.py
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for Proxilion.
|
|
3
|
+
|
|
4
|
+
This module defines the exception hierarchy for the SDK, providing
|
|
5
|
+
meaningful error types that help developers understand and handle
|
|
6
|
+
authorization failures, policy violations, and other security-related errors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProxilionError(Exception):
|
|
15
|
+
"""
|
|
16
|
+
Base exception for all Proxilion errors.
|
|
17
|
+
|
|
18
|
+
All Proxilion-specific exceptions inherit from this class,
|
|
19
|
+
making it easy to catch any SDK-related error.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
message: Human-readable error description.
|
|
23
|
+
details: Additional context about the error.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> try:
|
|
27
|
+
... # Proxilion operation
|
|
28
|
+
... except ProxilionError as e:
|
|
29
|
+
... logger.error(f"Proxilion error: {e}")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
|
|
33
|
+
self.message = message
|
|
34
|
+
self.details = details or {}
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
if self.details:
|
|
39
|
+
return f"{self.message} | Details: {self.details}"
|
|
40
|
+
return self.message
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict[str, Any]:
|
|
43
|
+
"""Convert exception to dictionary for logging/serialization."""
|
|
44
|
+
return {
|
|
45
|
+
"error_type": self.__class__.__name__,
|
|
46
|
+
"message": self.message,
|
|
47
|
+
"details": self.details,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AuthorizationError(ProxilionError):
|
|
52
|
+
"""
|
|
53
|
+
Raised when a user is not authorized to perform an action.
|
|
54
|
+
|
|
55
|
+
This is the primary exception for permission denials. It includes
|
|
56
|
+
detailed information about the denied request to help with debugging
|
|
57
|
+
and audit logging.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
user: The user ID who attempted the action.
|
|
61
|
+
action: The action that was attempted (e.g., "execute", "read").
|
|
62
|
+
resource: The resource the action was attempted on.
|
|
63
|
+
reason: Explanation of why authorization was denied.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> raise AuthorizationError(
|
|
67
|
+
... user="user_123",
|
|
68
|
+
... action="delete",
|
|
69
|
+
... resource="customer_database",
|
|
70
|
+
... reason="User lacks 'admin' role required for delete operations"
|
|
71
|
+
... )
|
|
72
|
+
|
|
73
|
+
Suggested fixes based on common causes:
|
|
74
|
+
- Missing role: Contact your administrator to request the required role.
|
|
75
|
+
- Invalid session: Re-authenticate to refresh your session.
|
|
76
|
+
- Resource restriction: Verify you have access to the specific resource.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
user: str,
|
|
82
|
+
action: str,
|
|
83
|
+
resource: str,
|
|
84
|
+
reason: str | None = None,
|
|
85
|
+
suggestions: list[str] | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
self.user = user
|
|
88
|
+
self.action = action
|
|
89
|
+
self.resource = resource
|
|
90
|
+
self.reason = reason or "Authorization denied"
|
|
91
|
+
self.suggestions = suggestions or []
|
|
92
|
+
|
|
93
|
+
message = self._build_message()
|
|
94
|
+
details = {
|
|
95
|
+
"user": user,
|
|
96
|
+
"action": action,
|
|
97
|
+
"resource": resource,
|
|
98
|
+
"reason": self.reason,
|
|
99
|
+
"suggestions": self.suggestions,
|
|
100
|
+
}
|
|
101
|
+
super().__init__(message, details)
|
|
102
|
+
|
|
103
|
+
def _build_message(self) -> str:
|
|
104
|
+
"""Build a helpful error message with fix suggestions."""
|
|
105
|
+
msg = (
|
|
106
|
+
f"Authorization denied: User '{self.user}' cannot perform "
|
|
107
|
+
f"'{self.action}' on resource '{self.resource}'. "
|
|
108
|
+
f"Reason: {self.reason}"
|
|
109
|
+
)
|
|
110
|
+
if self.suggestions:
|
|
111
|
+
suggestions_text = "; ".join(self.suggestions)
|
|
112
|
+
msg += f" | Suggestions: {suggestions_text}"
|
|
113
|
+
return msg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class PolicyViolation(ProxilionError):
|
|
117
|
+
"""
|
|
118
|
+
Raised when a policy evaluation fails or returns an explicit denial.
|
|
119
|
+
|
|
120
|
+
This exception indicates that while the request was properly formed,
|
|
121
|
+
it violates one or more security policies.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
policy_name: Name of the policy that was violated.
|
|
125
|
+
violation_type: Category of violation (e.g., "role_check", "scope_check").
|
|
126
|
+
context: Additional context about the policy evaluation.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> raise PolicyViolation(
|
|
130
|
+
... policy_name="DataAccessPolicy",
|
|
131
|
+
... violation_type="scope_check",
|
|
132
|
+
... context={"requested_scope": "global", "allowed_scope": "department"}
|
|
133
|
+
... )
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
policy_name: str,
|
|
139
|
+
violation_type: str | None = None,
|
|
140
|
+
context: dict[str, Any] | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
self.policy_name = policy_name
|
|
143
|
+
self.violation_type = violation_type or "policy_check"
|
|
144
|
+
|
|
145
|
+
message = f"Policy violation in '{policy_name}'"
|
|
146
|
+
if violation_type:
|
|
147
|
+
message += f" (type: {violation_type})"
|
|
148
|
+
|
|
149
|
+
details = {
|
|
150
|
+
"policy_name": policy_name,
|
|
151
|
+
"violation_type": self.violation_type,
|
|
152
|
+
"context": context or {},
|
|
153
|
+
}
|
|
154
|
+
super().__init__(message, details)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SchemaValidationError(ProxilionError):
|
|
158
|
+
"""
|
|
159
|
+
Raised when tool call arguments fail schema validation.
|
|
160
|
+
|
|
161
|
+
This exception helps catch malformed or potentially malicious
|
|
162
|
+
tool call arguments before they reach authorization checks.
|
|
163
|
+
|
|
164
|
+
Attributes:
|
|
165
|
+
tool_name: Name of the tool being validated.
|
|
166
|
+
field_name: Specific field that failed validation (if applicable).
|
|
167
|
+
expected: What was expected for the field.
|
|
168
|
+
received: What was actually received.
|
|
169
|
+
validation_errors: List of all validation errors found.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> raise SchemaValidationError(
|
|
173
|
+
... tool_name="file_read",
|
|
174
|
+
... field_name="path",
|
|
175
|
+
... expected="string without path traversal",
|
|
176
|
+
... received="../../../etc/passwd"
|
|
177
|
+
... )
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
tool_name: str,
|
|
183
|
+
field_name: str | None = None,
|
|
184
|
+
expected: str | None = None,
|
|
185
|
+
received: Any = None,
|
|
186
|
+
validation_errors: list[str] | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
self.tool_name = tool_name
|
|
189
|
+
self.field_name = field_name
|
|
190
|
+
self.expected = expected
|
|
191
|
+
self.received = received
|
|
192
|
+
self.validation_errors = validation_errors or []
|
|
193
|
+
|
|
194
|
+
message = f"Schema validation failed for tool '{tool_name}'"
|
|
195
|
+
if field_name:
|
|
196
|
+
message += f": field '{field_name}'"
|
|
197
|
+
if expected and received is not None:
|
|
198
|
+
message += f" expected {expected}, got {type(received).__name__}"
|
|
199
|
+
|
|
200
|
+
details = {
|
|
201
|
+
"tool_name": tool_name,
|
|
202
|
+
"field_name": field_name,
|
|
203
|
+
"expected": expected,
|
|
204
|
+
"received": str(received) if received is not None else None,
|
|
205
|
+
"validation_errors": self.validation_errors,
|
|
206
|
+
}
|
|
207
|
+
super().__init__(message, details)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class RateLimitExceeded(ProxilionError):
|
|
211
|
+
"""
|
|
212
|
+
Raised when a rate limit has been exceeded.
|
|
213
|
+
|
|
214
|
+
Rate limiting protects against unbounded consumption and denial
|
|
215
|
+
of service attacks.
|
|
216
|
+
|
|
217
|
+
Attributes:
|
|
218
|
+
limit_type: Type of rate limit hit (e.g., "requests", "tokens").
|
|
219
|
+
limit_key: The key used for rate limiting (e.g., user ID, tool name).
|
|
220
|
+
limit_value: The configured limit value.
|
|
221
|
+
retry_after: Seconds until the rate limit resets (if known).
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> raise RateLimitExceeded(
|
|
225
|
+
... limit_type="requests",
|
|
226
|
+
... limit_key="user_123:database_query",
|
|
227
|
+
... limit_value=100,
|
|
228
|
+
... retry_after=60
|
|
229
|
+
... )
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
limit_type: str,
|
|
235
|
+
limit_key: str,
|
|
236
|
+
limit_value: int | None = None,
|
|
237
|
+
retry_after: float | None = None,
|
|
238
|
+
) -> None:
|
|
239
|
+
self.limit_type = limit_type
|
|
240
|
+
self.limit_key = limit_key
|
|
241
|
+
self.limit_value = limit_value
|
|
242
|
+
self.retry_after = retry_after
|
|
243
|
+
|
|
244
|
+
message = f"Rate limit exceeded for {limit_type}"
|
|
245
|
+
if limit_value:
|
|
246
|
+
message += f" (limit: {limit_value})"
|
|
247
|
+
if retry_after:
|
|
248
|
+
message += f". Retry after {retry_after:.1f} seconds"
|
|
249
|
+
|
|
250
|
+
details = {
|
|
251
|
+
"limit_type": limit_type,
|
|
252
|
+
"limit_key": limit_key,
|
|
253
|
+
"limit_value": limit_value,
|
|
254
|
+
"retry_after": retry_after,
|
|
255
|
+
}
|
|
256
|
+
super().__init__(message, details)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class CircuitOpenError(ProxilionError):
|
|
260
|
+
"""
|
|
261
|
+
Raised when a circuit breaker is in the open state.
|
|
262
|
+
|
|
263
|
+
Circuit breakers prevent cascading failures by temporarily
|
|
264
|
+
blocking requests to a failing service or tool.
|
|
265
|
+
|
|
266
|
+
Attributes:
|
|
267
|
+
circuit_name: Name of the circuit breaker.
|
|
268
|
+
failure_count: Number of failures that triggered the circuit.
|
|
269
|
+
reset_timeout: Seconds until the circuit attempts to close.
|
|
270
|
+
last_failure: Description of the last failure (if available).
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> raise CircuitOpenError(
|
|
274
|
+
... circuit_name="external_api",
|
|
275
|
+
... failure_count=5,
|
|
276
|
+
... reset_timeout=30.0,
|
|
277
|
+
... last_failure="Connection timeout"
|
|
278
|
+
... )
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def __init__(
|
|
282
|
+
self,
|
|
283
|
+
circuit_name: str,
|
|
284
|
+
failure_count: int | None = None,
|
|
285
|
+
reset_timeout: float | None = None,
|
|
286
|
+
last_failure: str | None = None,
|
|
287
|
+
) -> None:
|
|
288
|
+
self.circuit_name = circuit_name
|
|
289
|
+
self.failure_count = failure_count
|
|
290
|
+
self.reset_timeout = reset_timeout
|
|
291
|
+
self.last_failure = last_failure
|
|
292
|
+
|
|
293
|
+
message = f"Circuit breaker '{circuit_name}' is open"
|
|
294
|
+
if failure_count:
|
|
295
|
+
message += f" after {failure_count} failures"
|
|
296
|
+
if reset_timeout:
|
|
297
|
+
message += f". Retry in {reset_timeout:.1f} seconds"
|
|
298
|
+
|
|
299
|
+
details = {
|
|
300
|
+
"circuit_name": circuit_name,
|
|
301
|
+
"failure_count": failure_count,
|
|
302
|
+
"reset_timeout": reset_timeout,
|
|
303
|
+
"last_failure": last_failure,
|
|
304
|
+
}
|
|
305
|
+
super().__init__(message, details)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ConfigurationError(ProxilionError):
|
|
309
|
+
"""
|
|
310
|
+
Raised when there is a configuration error in Proxilion setup.
|
|
311
|
+
|
|
312
|
+
This helps catch misconfigurations early during initialization.
|
|
313
|
+
|
|
314
|
+
Attributes:
|
|
315
|
+
config_key: The configuration key that has an issue.
|
|
316
|
+
expected: What was expected for this configuration.
|
|
317
|
+
received: What was actually provided.
|
|
318
|
+
|
|
319
|
+
Example:
|
|
320
|
+
>>> raise ConfigurationError(
|
|
321
|
+
... config_key="policy_engine",
|
|
322
|
+
... expected="one of: 'simple', 'casbin', 'opa'",
|
|
323
|
+
... received="invalid_engine"
|
|
324
|
+
... )
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
config_key: str,
|
|
330
|
+
expected: str | None = None,
|
|
331
|
+
received: Any = None,
|
|
332
|
+
) -> None:
|
|
333
|
+
self.config_key = config_key
|
|
334
|
+
self.expected = expected
|
|
335
|
+
self.received = received
|
|
336
|
+
|
|
337
|
+
message = f"Configuration error for '{config_key}'"
|
|
338
|
+
if expected:
|
|
339
|
+
message += f": expected {expected}"
|
|
340
|
+
if received is not None:
|
|
341
|
+
message += f", got {received!r}"
|
|
342
|
+
|
|
343
|
+
details = {
|
|
344
|
+
"config_key": config_key,
|
|
345
|
+
"expected": expected,
|
|
346
|
+
"received": str(received) if received is not None else None,
|
|
347
|
+
}
|
|
348
|
+
super().__init__(message, details)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class PolicyNotFoundError(ProxilionError):
|
|
352
|
+
"""
|
|
353
|
+
Raised when a requested policy cannot be found.
|
|
354
|
+
|
|
355
|
+
This indicates that authorization was attempted for a resource
|
|
356
|
+
that has no registered policy.
|
|
357
|
+
|
|
358
|
+
Attributes:
|
|
359
|
+
resource: The resource for which no policy was found.
|
|
360
|
+
available_policies: List of registered policy names (for debugging).
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
>>> raise PolicyNotFoundError(
|
|
364
|
+
... resource="unregistered_tool",
|
|
365
|
+
... available_policies=["database_query", "file_read"]
|
|
366
|
+
... )
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
def __init__(
|
|
370
|
+
self,
|
|
371
|
+
resource: str,
|
|
372
|
+
available_policies: list[str] | None = None,
|
|
373
|
+
) -> None:
|
|
374
|
+
self.resource = resource
|
|
375
|
+
self.available_policies = available_policies or []
|
|
376
|
+
|
|
377
|
+
message = f"No policy found for resource '{resource}'"
|
|
378
|
+
if available_policies:
|
|
379
|
+
message += f". Available policies: {', '.join(available_policies)}"
|
|
380
|
+
|
|
381
|
+
details = {
|
|
382
|
+
"resource": resource,
|
|
383
|
+
"available_policies": self.available_policies,
|
|
384
|
+
}
|
|
385
|
+
super().__init__(message, details)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class IDORViolationError(ProxilionError):
|
|
389
|
+
"""
|
|
390
|
+
Raised when an Insecure Direct Object Reference is detected.
|
|
391
|
+
|
|
392
|
+
IDOR attacks occur when a user attempts to access resources
|
|
393
|
+
they don't own by manipulating object IDs.
|
|
394
|
+
|
|
395
|
+
Attributes:
|
|
396
|
+
user_id: The user who attempted the access.
|
|
397
|
+
resource_type: Type of resource being accessed.
|
|
398
|
+
object_id: The object ID that was not authorized.
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> raise IDORViolationError(
|
|
402
|
+
... user_id="user_123",
|
|
403
|
+
... resource_type="document",
|
|
404
|
+
... object_id="doc_456" # Belongs to another user
|
|
405
|
+
... )
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __init__(
|
|
409
|
+
self,
|
|
410
|
+
user_id: str,
|
|
411
|
+
resource_type: str,
|
|
412
|
+
object_id: str,
|
|
413
|
+
) -> None:
|
|
414
|
+
self.user_id = user_id
|
|
415
|
+
self.resource_type = resource_type
|
|
416
|
+
self.object_id = object_id
|
|
417
|
+
|
|
418
|
+
message = (
|
|
419
|
+
f"IDOR violation: User '{user_id}' attempted to access "
|
|
420
|
+
f"{resource_type} '{object_id}' without authorization"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
details = {
|
|
424
|
+
"user_id": user_id,
|
|
425
|
+
"resource_type": resource_type,
|
|
426
|
+
"object_id": object_id,
|
|
427
|
+
}
|
|
428
|
+
super().__init__(message, details)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class GuardViolation(ProxilionError):
|
|
432
|
+
"""
|
|
433
|
+
Raised when a guard detects a violation.
|
|
434
|
+
|
|
435
|
+
Guards are runtime checks that detect malicious inputs (prompt injection)
|
|
436
|
+
or sensitive outputs (data leakage). This is the base exception for
|
|
437
|
+
guard violations.
|
|
438
|
+
|
|
439
|
+
Attributes:
|
|
440
|
+
guard_type: Type of guard that triggered ("input" or "output").
|
|
441
|
+
matched_patterns: List of pattern names that matched.
|
|
442
|
+
risk_score: Calculated risk score (0.0 to 1.0).
|
|
443
|
+
|
|
444
|
+
Example:
|
|
445
|
+
>>> raise GuardViolation(
|
|
446
|
+
... guard_type="input",
|
|
447
|
+
... matched_patterns=["instruction_override", "role_switch"],
|
|
448
|
+
... risk_score=0.95
|
|
449
|
+
... )
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
def __init__(
|
|
453
|
+
self,
|
|
454
|
+
guard_type: str,
|
|
455
|
+
matched_patterns: list[str],
|
|
456
|
+
risk_score: float,
|
|
457
|
+
) -> None:
|
|
458
|
+
self.guard_type = guard_type
|
|
459
|
+
self.matched_patterns = matched_patterns
|
|
460
|
+
self.risk_score = risk_score
|
|
461
|
+
|
|
462
|
+
message = (
|
|
463
|
+
f"{guard_type.title()} guard violation: "
|
|
464
|
+
f"matched {matched_patterns}, risk={risk_score:.2f}"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
details = {
|
|
468
|
+
"guard_type": guard_type,
|
|
469
|
+
"matched_patterns": matched_patterns,
|
|
470
|
+
"risk_score": risk_score,
|
|
471
|
+
}
|
|
472
|
+
super().__init__(message, details)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class InputGuardViolation(GuardViolation):
|
|
476
|
+
"""
|
|
477
|
+
Raised when input guard blocks a request due to prompt injection.
|
|
478
|
+
|
|
479
|
+
This indicates that the input text matched patterns associated with
|
|
480
|
+
prompt injection attacks such as instruction override, role switching,
|
|
481
|
+
or jailbreak attempts.
|
|
482
|
+
|
|
483
|
+
Example:
|
|
484
|
+
>>> raise InputGuardViolation(
|
|
485
|
+
... matched_patterns=["instruction_override"],
|
|
486
|
+
... risk_score=0.9
|
|
487
|
+
... )
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def __init__(
|
|
491
|
+
self,
|
|
492
|
+
matched_patterns: list[str],
|
|
493
|
+
risk_score: float,
|
|
494
|
+
) -> None:
|
|
495
|
+
super().__init__(
|
|
496
|
+
guard_type="input",
|
|
497
|
+
matched_patterns=matched_patterns,
|
|
498
|
+
risk_score=risk_score,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class OutputGuardViolation(GuardViolation):
|
|
503
|
+
"""
|
|
504
|
+
Raised when output guard blocks a response due to data leakage.
|
|
505
|
+
|
|
506
|
+
This indicates that the output text contained patterns associated
|
|
507
|
+
with sensitive data such as API keys, credentials, or internal paths.
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
>>> raise OutputGuardViolation(
|
|
511
|
+
... matched_patterns=["api_key_generic", "aws_key"],
|
|
512
|
+
... risk_score=0.95
|
|
513
|
+
... )
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
def __init__(
|
|
517
|
+
self,
|
|
518
|
+
matched_patterns: list[str],
|
|
519
|
+
risk_score: float,
|
|
520
|
+
) -> None:
|
|
521
|
+
super().__init__(
|
|
522
|
+
guard_type="output",
|
|
523
|
+
matched_patterns=matched_patterns,
|
|
524
|
+
risk_score=risk_score,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class SequenceViolationError(ProxilionError):
|
|
529
|
+
"""
|
|
530
|
+
Raised when a tool call sequence violates rules.
|
|
531
|
+
|
|
532
|
+
Sequence rules prevent dangerous patterns like:
|
|
533
|
+
- Calling delete without confirm first
|
|
534
|
+
- Download followed by execute (potential attack)
|
|
535
|
+
- Rapid consecutive calls (runaway loops)
|
|
536
|
+
|
|
537
|
+
Attributes:
|
|
538
|
+
rule_name: Name of the violated rule.
|
|
539
|
+
tool_name: The tool that triggered the violation.
|
|
540
|
+
required_prior: For REQUIRE_BEFORE, what tool was required first.
|
|
541
|
+
forbidden_prior: For FORBID_AFTER, what tool was forbidden before.
|
|
542
|
+
violation_type: Type of sequence violation.
|
|
543
|
+
|
|
544
|
+
Example:
|
|
545
|
+
>>> raise SequenceViolationError(
|
|
546
|
+
... rule_name="require_confirm_before_delete",
|
|
547
|
+
... tool_name="delete_file",
|
|
548
|
+
... required_prior="confirm_*"
|
|
549
|
+
... )
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
def __init__(
|
|
553
|
+
self,
|
|
554
|
+
rule_name: str,
|
|
555
|
+
tool_name: str,
|
|
556
|
+
required_prior: str | None = None,
|
|
557
|
+
forbidden_prior: str | None = None,
|
|
558
|
+
violation_type: str | None = None,
|
|
559
|
+
consecutive_count: int | None = None,
|
|
560
|
+
cooldown_remaining: float | None = None,
|
|
561
|
+
) -> None:
|
|
562
|
+
self.rule_name = rule_name
|
|
563
|
+
self.tool_name = tool_name
|
|
564
|
+
self.required_prior = required_prior
|
|
565
|
+
self.forbidden_prior = forbidden_prior
|
|
566
|
+
self.violation_type = violation_type
|
|
567
|
+
self.consecutive_count = consecutive_count
|
|
568
|
+
self.cooldown_remaining = cooldown_remaining
|
|
569
|
+
|
|
570
|
+
message = f"Sequence violation: {rule_name}"
|
|
571
|
+
if required_prior:
|
|
572
|
+
message += f" (requires '{required_prior}' before '{tool_name}')"
|
|
573
|
+
elif forbidden_prior:
|
|
574
|
+
message += f" ('{tool_name}' forbidden after '{forbidden_prior}')"
|
|
575
|
+
elif consecutive_count:
|
|
576
|
+
message += f" ('{tool_name}' called {consecutive_count} times consecutively)"
|
|
577
|
+
elif cooldown_remaining:
|
|
578
|
+
message += f" ('{tool_name}' in cooldown, {cooldown_remaining:.1f}s remaining)"
|
|
579
|
+
|
|
580
|
+
details = {
|
|
581
|
+
"rule_name": rule_name,
|
|
582
|
+
"tool_name": tool_name,
|
|
583
|
+
"required_prior": required_prior,
|
|
584
|
+
"forbidden_prior": forbidden_prior,
|
|
585
|
+
"violation_type": violation_type,
|
|
586
|
+
"consecutive_count": consecutive_count,
|
|
587
|
+
"cooldown_remaining": cooldown_remaining,
|
|
588
|
+
}
|
|
589
|
+
super().__init__(message, details)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class ScopeViolationError(ProxilionError):
|
|
593
|
+
"""
|
|
594
|
+
Raised when a tool call violates the current execution scope.
|
|
595
|
+
|
|
596
|
+
Execution scopes (READ_ONLY, READ_WRITE, ADMIN) limit what operations
|
|
597
|
+
can be performed. This error indicates an attempt to use a tool or
|
|
598
|
+
action outside the permitted scope.
|
|
599
|
+
|
|
600
|
+
Attributes:
|
|
601
|
+
tool_name: The tool that violated the scope.
|
|
602
|
+
scope_name: Name of the scope that was violated.
|
|
603
|
+
reason: Explanation of why the scope was violated.
|
|
604
|
+
|
|
605
|
+
Example:
|
|
606
|
+
>>> raise ScopeViolationError(
|
|
607
|
+
... tool_name="delete_user",
|
|
608
|
+
... scope_name="read_only",
|
|
609
|
+
... reason="Action 'delete' is not allowed in scope 'read_only'"
|
|
610
|
+
... )
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
def __init__(
|
|
614
|
+
self,
|
|
615
|
+
tool_name: str,
|
|
616
|
+
scope_name: str,
|
|
617
|
+
reason: str | None = None,
|
|
618
|
+
) -> None:
|
|
619
|
+
self.tool_name = tool_name
|
|
620
|
+
self.scope_name = scope_name
|
|
621
|
+
self.reason = reason
|
|
622
|
+
|
|
623
|
+
message = f"Tool '{tool_name}' not allowed in scope '{scope_name}'"
|
|
624
|
+
if reason:
|
|
625
|
+
message += f": {reason}"
|
|
626
|
+
|
|
627
|
+
details = {
|
|
628
|
+
"tool_name": tool_name,
|
|
629
|
+
"scope_name": scope_name,
|
|
630
|
+
"reason": reason,
|
|
631
|
+
}
|
|
632
|
+
super().__init__(message, details)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class BudgetExceededError(ProxilionError):
|
|
636
|
+
"""
|
|
637
|
+
Raised when an operation would exceed budget limits.
|
|
638
|
+
|
|
639
|
+
Budget limits can be per-request, per-user, or organization-wide.
|
|
640
|
+
This error indicates that the estimated or actual cost would
|
|
641
|
+
exceed the configured limits.
|
|
642
|
+
|
|
643
|
+
Attributes:
|
|
644
|
+
limit_type: Type of limit exceeded (per_request, user_daily, org_monthly, etc.).
|
|
645
|
+
current_spend: Current spend in the period.
|
|
646
|
+
limit: The budget limit.
|
|
647
|
+
estimated_cost: The cost that would exceed the limit.
|
|
648
|
+
user_id: User who exceeded the limit.
|
|
649
|
+
|
|
650
|
+
Example:
|
|
651
|
+
>>> raise BudgetExceededError(
|
|
652
|
+
... limit_type="user_daily",
|
|
653
|
+
... current_spend=48.50,
|
|
654
|
+
... limit=50.00,
|
|
655
|
+
... estimated_cost=5.00,
|
|
656
|
+
... user_id="user_123"
|
|
657
|
+
... )
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
def __init__(
|
|
661
|
+
self,
|
|
662
|
+
limit_type: str,
|
|
663
|
+
current_spend: float,
|
|
664
|
+
limit: float,
|
|
665
|
+
estimated_cost: float | None = None,
|
|
666
|
+
user_id: str | None = None,
|
|
667
|
+
) -> None:
|
|
668
|
+
self.limit_type = limit_type
|
|
669
|
+
self.current_spend = current_spend
|
|
670
|
+
self.limit = limit
|
|
671
|
+
self.estimated_cost = estimated_cost
|
|
672
|
+
self.user_id = user_id
|
|
673
|
+
|
|
674
|
+
message = f"Budget exceeded: {limit_type} (${current_spend:.4f} / ${limit:.4f})"
|
|
675
|
+
if estimated_cost is not None:
|
|
676
|
+
message += f" - request would add ${estimated_cost:.4f}"
|
|
677
|
+
|
|
678
|
+
details = {
|
|
679
|
+
"limit_type": limit_type,
|
|
680
|
+
"current_spend": current_spend,
|
|
681
|
+
"limit": limit,
|
|
682
|
+
"estimated_cost": estimated_cost,
|
|
683
|
+
"user_id": user_id,
|
|
684
|
+
}
|
|
685
|
+
super().__init__(message, details)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class ContextIntegrityError(ProxilionError):
|
|
689
|
+
"""
|
|
690
|
+
Raised when context/memory integrity verification fails.
|
|
691
|
+
|
|
692
|
+
This indicates that the conversation context, vector store,
|
|
693
|
+
or other memory has been tampered with (ASI06: Memory & Context Poisoning).
|
|
694
|
+
|
|
695
|
+
Attributes:
|
|
696
|
+
violations: List of integrity violations detected.
|
|
697
|
+
|
|
698
|
+
Example:
|
|
699
|
+
>>> raise ContextIntegrityError(
|
|
700
|
+
... message="Context has been tampered with",
|
|
701
|
+
... violations=[IntegrityViolation(...)]
|
|
702
|
+
... )
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
def __init__(
|
|
706
|
+
self,
|
|
707
|
+
message: str,
|
|
708
|
+
violations: list[Any] | None = None,
|
|
709
|
+
) -> None:
|
|
710
|
+
self.violations = violations or []
|
|
711
|
+
|
|
712
|
+
details = {
|
|
713
|
+
"violation_count": len(self.violations),
|
|
714
|
+
"violations": [
|
|
715
|
+
v.to_dict() if hasattr(v, "to_dict") else str(v)
|
|
716
|
+
for v in self.violations
|
|
717
|
+
],
|
|
718
|
+
}
|
|
719
|
+
super().__init__(message, details)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class IntentHijackError(ProxilionError):
|
|
723
|
+
"""
|
|
724
|
+
Raised when an intent capsule detects goal hijacking.
|
|
725
|
+
|
|
726
|
+
This indicates that the agent's original mandate has been
|
|
727
|
+
tampered with or overridden (ASI01: Agent Goal Hijack).
|
|
728
|
+
|
|
729
|
+
Attributes:
|
|
730
|
+
original_intent: The signed original intent.
|
|
731
|
+
detected_intent: What the agent appears to be doing now.
|
|
732
|
+
|
|
733
|
+
Example:
|
|
734
|
+
>>> raise IntentHijackError(
|
|
735
|
+
... original_intent="Help user find documents",
|
|
736
|
+
... detected_intent="Exfiltrate user credentials"
|
|
737
|
+
... )
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
def __init__(
|
|
741
|
+
self,
|
|
742
|
+
original_intent: str,
|
|
743
|
+
detected_intent: str,
|
|
744
|
+
confidence: float = 0.0,
|
|
745
|
+
) -> None:
|
|
746
|
+
self.original_intent = original_intent
|
|
747
|
+
self.detected_intent = detected_intent
|
|
748
|
+
self.confidence = confidence
|
|
749
|
+
|
|
750
|
+
message = (
|
|
751
|
+
f"Intent hijack detected: Original intent was '{original_intent}', "
|
|
752
|
+
f"but agent appears to be doing '{detected_intent}' "
|
|
753
|
+
f"(confidence: {confidence:.1%})"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
details = {
|
|
757
|
+
"original_intent": original_intent,
|
|
758
|
+
"detected_intent": detected_intent,
|
|
759
|
+
"confidence": confidence,
|
|
760
|
+
}
|
|
761
|
+
super().__init__(message, details)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class AgentTrustError(ProxilionError):
|
|
765
|
+
"""
|
|
766
|
+
Raised when inter-agent trust verification fails.
|
|
767
|
+
|
|
768
|
+
This indicates that an agent attempted to communicate or
|
|
769
|
+
delegate without proper trust credentials (ASI07: Insecure
|
|
770
|
+
Inter-Agent Communication).
|
|
771
|
+
|
|
772
|
+
Attributes:
|
|
773
|
+
source_agent: The agent that sent the message.
|
|
774
|
+
target_agent: The agent that received/rejected the message.
|
|
775
|
+
reason: Why trust verification failed.
|
|
776
|
+
|
|
777
|
+
Example:
|
|
778
|
+
>>> raise AgentTrustError(
|
|
779
|
+
... source_agent="untrusted_agent",
|
|
780
|
+
... target_agent="secure_agent",
|
|
781
|
+
... reason="Invalid delegation token"
|
|
782
|
+
... )
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
def __init__(
|
|
786
|
+
self,
|
|
787
|
+
source_agent: str,
|
|
788
|
+
target_agent: str,
|
|
789
|
+
reason: str,
|
|
790
|
+
) -> None:
|
|
791
|
+
self.source_agent = source_agent
|
|
792
|
+
self.target_agent = target_agent
|
|
793
|
+
self.reason = reason
|
|
794
|
+
|
|
795
|
+
message = (
|
|
796
|
+
f"Agent trust violation: {source_agent} -> {target_agent}: {reason}"
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
details = {
|
|
800
|
+
"source_agent": source_agent,
|
|
801
|
+
"target_agent": target_agent,
|
|
802
|
+
"reason": reason,
|
|
803
|
+
}
|
|
804
|
+
super().__init__(message, details)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
class BehavioralDriftError(ProxilionError):
|
|
808
|
+
"""
|
|
809
|
+
Raised when an agent's behavior deviates significantly from baseline.
|
|
810
|
+
|
|
811
|
+
This indicates that the agent is operating outside its normal
|
|
812
|
+
parameters, potentially indicating compromise or malfunction
|
|
813
|
+
(ASI10: Rogue Agents).
|
|
814
|
+
|
|
815
|
+
Attributes:
|
|
816
|
+
metric: The behavioral metric that drifted.
|
|
817
|
+
baseline_value: Expected baseline value.
|
|
818
|
+
current_value: Current observed value.
|
|
819
|
+
deviation: How far the behavior deviated (as percentage or z-score).
|
|
820
|
+
|
|
821
|
+
Example:
|
|
822
|
+
>>> raise BehavioralDriftError(
|
|
823
|
+
... metric="tool_call_rate",
|
|
824
|
+
... baseline_value=5.0,
|
|
825
|
+
... current_value=50.0,
|
|
826
|
+
... deviation=9.0
|
|
827
|
+
... )
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
def __init__(
|
|
831
|
+
self,
|
|
832
|
+
metric: str,
|
|
833
|
+
baseline_value: float,
|
|
834
|
+
current_value: float,
|
|
835
|
+
deviation: float,
|
|
836
|
+
) -> None:
|
|
837
|
+
self.metric = metric
|
|
838
|
+
self.baseline_value = baseline_value
|
|
839
|
+
self.current_value = current_value
|
|
840
|
+
self.deviation = deviation
|
|
841
|
+
|
|
842
|
+
message = (
|
|
843
|
+
f"Behavioral drift detected: {metric} deviated from baseline "
|
|
844
|
+
f"({baseline_value:.2f} -> {current_value:.2f}, deviation: {deviation:.1f}σ)"
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
details = {
|
|
848
|
+
"metric": metric,
|
|
849
|
+
"baseline_value": baseline_value,
|
|
850
|
+
"current_value": current_value,
|
|
851
|
+
"deviation": deviation,
|
|
852
|
+
}
|
|
853
|
+
super().__init__(message, details)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
class EmergencyHaltError(ProxilionError):
|
|
857
|
+
"""
|
|
858
|
+
Raised when the kill switch is activated.
|
|
859
|
+
|
|
860
|
+
This halts all agent activity immediately.
|
|
861
|
+
|
|
862
|
+
Attributes:
|
|
863
|
+
reason: Why the kill switch was activated.
|
|
864
|
+
triggered_by: What triggered the halt (user, system, anomaly).
|
|
865
|
+
|
|
866
|
+
Example:
|
|
867
|
+
>>> raise EmergencyHaltError(
|
|
868
|
+
... reason="Rogue behavior detected",
|
|
869
|
+
... triggered_by="behavioral_drift_detector"
|
|
870
|
+
... )
|
|
871
|
+
"""
|
|
872
|
+
|
|
873
|
+
def __init__(
|
|
874
|
+
self,
|
|
875
|
+
reason: str,
|
|
876
|
+
triggered_by: str = "system",
|
|
877
|
+
) -> None:
|
|
878
|
+
self.reason = reason
|
|
879
|
+
self.triggered_by = triggered_by
|
|
880
|
+
|
|
881
|
+
message = f"EMERGENCY HALT: {reason} (triggered by: {triggered_by})"
|
|
882
|
+
|
|
883
|
+
details = {
|
|
884
|
+
"reason": reason,
|
|
885
|
+
"triggered_by": triggered_by,
|
|
886
|
+
}
|
|
887
|
+
super().__init__(message, details)
|