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,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)