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,437 @@
1
+ """
2
+ Simple policy engine for Proxilion.
3
+
4
+ This module provides the SimplePolicyEngine, a zero-dependency
5
+ policy engine that uses the Pundit-style Policy classes for
6
+ authorization decisions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from proxilion.engines.base import BasePolicyEngine, EngineCapabilities
16
+ from proxilion.policies.base import Policy
17
+ from proxilion.policies.builtin import DenyAllPolicy
18
+ from proxilion.policies.registry import PolicyRegistry
19
+ from proxilion.types import AuthorizationResult
20
+
21
+ if TYPE_CHECKING:
22
+ from proxilion.types import UserContext
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class SimplePolicyEngine(BasePolicyEngine):
28
+ """
29
+ Simple policy engine using Pundit-style Policy classes.
30
+
31
+ This is the default policy engine for Proxilion. It uses the
32
+ PolicyRegistry to look up policy classes and evaluates them
33
+ against user context and resources.
34
+
35
+ Features:
36
+ - Class-based policies (Pundit-style)
37
+ - Dictionary-based rules for simple cases
38
+ - Default deny behavior
39
+ - Integration with PolicyRegistry
40
+ - No external dependencies
41
+
42
+ Example:
43
+ >>> engine = SimplePolicyEngine()
44
+ >>>
45
+ >>> # Register a policy
46
+ >>> @engine.registry.policy("calculator")
47
+ >>> class CalculatorPolicy(Policy):
48
+ ... def can_execute(self, context: dict) -> bool:
49
+ ... return "calculator_user" in self.user.roles
50
+ >>>
51
+ >>> # Evaluate
52
+ >>> result = engine.evaluate(user, "execute", "calculator")
53
+ >>> print(result.allowed) # True or False
54
+
55
+ Configuration:
56
+ - default_policy: Policy class to use when no policy is registered.
57
+ Defaults to DenyAllPolicy.
58
+ - allow_missing_policies: If True, missing policies return deny
59
+ instead of raising an error. Defaults to True.
60
+ """
61
+
62
+ name = "simple"
63
+
64
+ def __init__(
65
+ self,
66
+ config: dict[str, Any] | None = None,
67
+ registry: PolicyRegistry | None = None,
68
+ ) -> None:
69
+ """
70
+ Initialize the SimplePolicyEngine.
71
+
72
+ Args:
73
+ config: Engine configuration options.
74
+ registry: Optional PolicyRegistry to use. If not provided,
75
+ a new registry is created.
76
+ """
77
+ super().__init__(config)
78
+
79
+ # Get default policy from config or use DenyAllPolicy
80
+ default_policy = self.get_config("default_policy", DenyAllPolicy)
81
+ self.allow_missing_policies = self.get_config("allow_missing_policies", True)
82
+
83
+ # Use provided registry or create new one
84
+ self.registry = registry or PolicyRegistry(default_policy=default_policy)
85
+
86
+ # Dictionary-based rules for simple authorization
87
+ self._dict_rules: dict[str, dict[str, list[str]]] = {}
88
+
89
+ self._initialized = True
90
+ logger.debug(f"SimplePolicyEngine initialized with registry: {self.registry}")
91
+
92
+ @property
93
+ def capabilities(self) -> EngineCapabilities:
94
+ """Get engine capabilities."""
95
+ return EngineCapabilities(
96
+ supports_async=True,
97
+ supports_caching=False,
98
+ supports_explain=True,
99
+ supports_partial_eval=False,
100
+ supports_hot_reload=True,
101
+ max_batch_size=100,
102
+ )
103
+
104
+ def evaluate(
105
+ self,
106
+ user: UserContext,
107
+ action: str,
108
+ resource: str,
109
+ context: dict[str, Any] | None = None,
110
+ ) -> AuthorizationResult:
111
+ """
112
+ Evaluate an authorization request.
113
+
114
+ The evaluation process:
115
+ 1. Check dictionary-based rules first (if configured)
116
+ 2. Look up policy class from registry
117
+ 3. Instantiate policy with user and resource
118
+ 4. Call the can_<action> method
119
+ 5. Return AuthorizationResult
120
+
121
+ Args:
122
+ user: The user context.
123
+ action: The action being attempted.
124
+ resource: The resource being accessed.
125
+ context: Additional context for the decision.
126
+
127
+ Returns:
128
+ AuthorizationResult with the decision.
129
+ """
130
+ ctx = context or {}
131
+ policies_evaluated: list[str] = []
132
+
133
+ logger.debug(
134
+ f"Evaluating: user={user.user_id}, action={action}, "
135
+ f"resource={resource}"
136
+ )
137
+
138
+ # Check dictionary rules first
139
+ if resource in self._dict_rules:
140
+ dict_result = self._evaluate_dict_rules(user, action, resource)
141
+ if dict_result is not None:
142
+ policies_evaluated.append(f"dict_rules:{resource}")
143
+ return AuthorizationResult(
144
+ allowed=dict_result,
145
+ reason=f"Dictionary rule for {resource}:{action}",
146
+ policies_evaluated=policies_evaluated,
147
+ )
148
+
149
+ # Get policy class from registry
150
+ try:
151
+ policy_class = self.registry.get_policy(resource)
152
+ policies_evaluated.append(policy_class.__name__)
153
+ except Exception as e:
154
+ if self.allow_missing_policies:
155
+ logger.debug(f"No policy for '{resource}', denying: {e}")
156
+ return AuthorizationResult(
157
+ allowed=False,
158
+ reason=f"No policy registered for resource '{resource}'",
159
+ policies_evaluated=policies_evaluated,
160
+ )
161
+ raise
162
+
163
+ # Get the resource object from context if provided
164
+ resource_obj = ctx.get("resource_object")
165
+
166
+ # Instantiate policy
167
+ policy = policy_class(user, resource_obj)
168
+
169
+ # Evaluate the action
170
+ allowed = policy.authorize(action, ctx)
171
+
172
+ # Mark as authorized in registry for tracking
173
+ self.registry.mark_authorized(resource)
174
+
175
+ # Build result
176
+ if allowed:
177
+ reason = f"Policy {policy_class.__name__} allowed action '{action}'"
178
+ else:
179
+ reason = f"Policy {policy_class.__name__} denied action '{action}'"
180
+
181
+ logger.debug(f"Result: allowed={allowed}, reason={reason}")
182
+
183
+ return AuthorizationResult(
184
+ allowed=allowed,
185
+ reason=reason,
186
+ policies_evaluated=policies_evaluated,
187
+ )
188
+
189
+ def _evaluate_dict_rules(
190
+ self,
191
+ user: UserContext,
192
+ action: str,
193
+ resource: str,
194
+ ) -> bool | None:
195
+ """
196
+ Evaluate dictionary-based rules.
197
+
198
+ Args:
199
+ user: The user context.
200
+ action: The action being attempted.
201
+ resource: The resource being accessed.
202
+
203
+ Returns:
204
+ True if allowed, False if denied, None if no rule matches.
205
+ """
206
+ rules = self._dict_rules.get(resource, {})
207
+ required_roles = rules.get(action)
208
+
209
+ if required_roles is None:
210
+ return None # No rule for this action
211
+
212
+ user_roles = set(user.roles)
213
+ allowed_roles = set(required_roles)
214
+
215
+ return bool(user_roles & allowed_roles)
216
+
217
+ def add_rule(
218
+ self,
219
+ resource: str,
220
+ action: str,
221
+ allowed_roles: list[str],
222
+ ) -> None:
223
+ """
224
+ Add a dictionary-based authorization rule.
225
+
226
+ This is a simpler alternative to defining a full Policy class.
227
+ Dictionary rules are checked before policy classes.
228
+
229
+ Args:
230
+ resource: The resource name.
231
+ action: The action name.
232
+ allowed_roles: List of roles that are allowed.
233
+
234
+ Example:
235
+ >>> engine.add_rule("calculator", "execute", ["user", "admin"])
236
+ >>> engine.add_rule("calculator", "configure", ["admin"])
237
+ """
238
+ if resource not in self._dict_rules:
239
+ self._dict_rules[resource] = {}
240
+
241
+ self._dict_rules[resource][action] = allowed_roles
242
+ logger.debug(
243
+ f"Added rule: {resource}:{action} -> {allowed_roles}"
244
+ )
245
+
246
+ def add_rules(self, rules: dict[str, dict[str, list[str]]]) -> None:
247
+ """
248
+ Add multiple dictionary-based rules at once.
249
+
250
+ Args:
251
+ rules: Nested dict of resource -> action -> roles.
252
+
253
+ Example:
254
+ >>> engine.add_rules({
255
+ ... "calculator": {
256
+ ... "execute": ["user", "admin"],
257
+ ... "configure": ["admin"],
258
+ ... },
259
+ ... "database": {
260
+ ... "query": ["analyst", "admin"],
261
+ ... "write": ["admin"],
262
+ ... },
263
+ ... })
264
+ """
265
+ for resource, actions in rules.items():
266
+ for action, roles in actions.items():
267
+ self.add_rule(resource, action, roles)
268
+
269
+ def remove_rule(self, resource: str, action: str | None = None) -> bool:
270
+ """
271
+ Remove a dictionary-based rule.
272
+
273
+ Args:
274
+ resource: The resource name.
275
+ action: The action to remove, or None to remove all actions.
276
+
277
+ Returns:
278
+ True if a rule was removed, False otherwise.
279
+ """
280
+ if resource not in self._dict_rules:
281
+ return False
282
+
283
+ if action is None:
284
+ del self._dict_rules[resource]
285
+ return True
286
+
287
+ if action in self._dict_rules[resource]:
288
+ del self._dict_rules[resource][action]
289
+ if not self._dict_rules[resource]:
290
+ del self._dict_rules[resource]
291
+ return True
292
+
293
+ return False
294
+
295
+ def clear_rules(self) -> None:
296
+ """Clear all dictionary-based rules."""
297
+ self._dict_rules.clear()
298
+ logger.debug("Cleared all dictionary rules")
299
+
300
+ def load_policies(self, source: str | Path) -> None:
301
+ """
302
+ Load policies from a Python module or directory.
303
+
304
+ This method imports Policy classes from the specified source
305
+ and registers them with the registry.
306
+
307
+ Args:
308
+ source: Path to a Python module or directory containing
309
+ Policy class definitions.
310
+
311
+ Note:
312
+ Policy classes must use the naming convention *Policy
313
+ (e.g., DatabasePolicy) to be auto-discovered.
314
+ """
315
+ path = Path(source)
316
+
317
+ if path.is_file() and path.suffix == ".py":
318
+ self._load_from_file(path)
319
+ elif path.is_dir():
320
+ self._load_from_directory(path)
321
+ else:
322
+ # Assume it's a module path
323
+ self._load_from_module(str(source))
324
+
325
+ def _load_from_file(self, path: Path) -> None:
326
+ """Load policies from a Python file."""
327
+ import importlib.util
328
+
329
+ spec = importlib.util.spec_from_file_location("policies", path)
330
+ if spec is None or spec.loader is None:
331
+ logger.warning(f"Could not load policies from {path}")
332
+ return
333
+
334
+ module = importlib.util.module_from_spec(spec)
335
+ spec.loader.exec_module(module)
336
+
337
+ self._register_policies_from_module(module)
338
+
339
+ def _load_from_directory(self, path: Path) -> None:
340
+ """Load policies from all Python files in a directory."""
341
+ for py_file in path.glob("*.py"):
342
+ if py_file.name.startswith("_"):
343
+ continue
344
+ self._load_from_file(py_file)
345
+
346
+ def _load_from_module(self, module_path: str) -> None:
347
+ """Load policies from a module path."""
348
+ import importlib
349
+
350
+ try:
351
+ module = importlib.import_module(module_path)
352
+ self._register_policies_from_module(module)
353
+ except ImportError as e:
354
+ logger.warning(f"Could not import module {module_path}: {e}")
355
+
356
+ def _register_policies_from_module(self, module: Any) -> None:
357
+ """Register all Policy subclasses from a module."""
358
+ for name in dir(module):
359
+ obj = getattr(module, name)
360
+ if (
361
+ isinstance(obj, type)
362
+ and issubclass(obj, Policy)
363
+ and obj is not Policy
364
+ and name.endswith("Policy")
365
+ ):
366
+ self.registry.register_by_convention(obj)
367
+ logger.debug(f"Registered policy: {name}")
368
+
369
+ def policy(self, resource_name: str) -> Any:
370
+ """
371
+ Decorator for registering a policy class.
372
+
373
+ Convenience method that delegates to the registry.
374
+
375
+ Args:
376
+ resource_name: The resource this policy handles.
377
+
378
+ Returns:
379
+ A decorator function.
380
+
381
+ Example:
382
+ >>> @engine.policy("database")
383
+ >>> class DatabasePolicy(Policy):
384
+ ... def can_query(self, context: dict) -> bool:
385
+ ... return True
386
+ """
387
+ return self.registry.policy(resource_name)
388
+
389
+ def explain(
390
+ self,
391
+ user: UserContext,
392
+ action: str,
393
+ resource: str,
394
+ context: dict[str, Any] | None = None,
395
+ ) -> dict[str, Any]:
396
+ """
397
+ Explain an authorization decision.
398
+
399
+ Provides detailed information about how a decision was made,
400
+ useful for debugging and auditing.
401
+
402
+ Args:
403
+ user: The user context.
404
+ action: The action being attempted.
405
+ resource: The resource being accessed.
406
+ context: Additional context.
407
+
408
+ Returns:
409
+ Dictionary containing explanation details.
410
+ """
411
+ result = self.evaluate(user, action, resource, context)
412
+
413
+ explanation = {
414
+ "decision": "ALLOW" if result.allowed else "DENY",
415
+ "reason": result.reason,
416
+ "policies_evaluated": result.policies_evaluated,
417
+ "user": {
418
+ "user_id": user.user_id,
419
+ "roles": user.roles,
420
+ },
421
+ "request": {
422
+ "action": action,
423
+ "resource": resource,
424
+ },
425
+ }
426
+
427
+ # Check for dict rules
428
+ if resource in self._dict_rules:
429
+ explanation["dict_rules"] = self._dict_rules[resource]
430
+
431
+ # Check for policy class
432
+ if self.registry.has_policy(resource):
433
+ policy_class = self.registry.get_policy(resource)
434
+ explanation["policy_class"] = policy_class.__name__
435
+ explanation["available_actions"] = policy_class.get_available_actions()
436
+
437
+ return explanation