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,376 @@
1
+ """
2
+ Policy registry for Proxilion.
3
+
4
+ This module provides the PolicyRegistry class for registering, discovering,
5
+ and looking up policy classes. It supports both explicit registration via
6
+ decorators and automatic discovery based on naming conventions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import threading
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from proxilion.exceptions import PolicyNotFoundError
16
+
17
+ if TYPE_CHECKING:
18
+ from proxilion.policies.base import Policy
19
+ from proxilion.types import UserContext
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class PolicyRegistry:
25
+ """
26
+ Registry for policy classes.
27
+
28
+ The PolicyRegistry manages policy class registration and lookup,
29
+ enabling the authorization system to find the appropriate policy
30
+ for any given resource.
31
+
32
+ Features:
33
+ - Decorator-based registration (@registry.policy("resource_name"))
34
+ - Auto-discovery by naming convention (ResourcePolicy -> "resource")
35
+ - Thread-safe operations
36
+ - Policy caching for performance
37
+ - Verification that all resources have been authorized
38
+
39
+ Example:
40
+ >>> registry = PolicyRegistry()
41
+ >>>
42
+ >>> @registry.policy("calculator")
43
+ >>> class CalculatorPolicy(Policy):
44
+ ... def can_execute(self, context: dict) -> bool:
45
+ ... return "calculator_user" in self.user.roles
46
+ >>>
47
+ >>> # Lookup and use
48
+ >>> policy_class = registry.get_policy("calculator")
49
+ >>> policy = policy_class(user, resource)
50
+ >>> if policy.can("execute"):
51
+ ... # proceed
52
+
53
+ Thread Safety:
54
+ All operations are thread-safe via internal locking.
55
+ """
56
+
57
+ def __init__(self, default_policy: type[Policy] | None = None) -> None:
58
+ """
59
+ Initialize the registry.
60
+
61
+ Args:
62
+ default_policy: Optional default policy class to use when no
63
+ policy is registered for a resource. If None, lookups for
64
+ unregistered resources will raise PolicyNotFoundError.
65
+ """
66
+ self._policies: dict[str, type[Policy]] = {}
67
+ self._default_policy = default_policy
68
+ self._lock = threading.RLock()
69
+ self._authorized_resources: set[str] = set()
70
+
71
+ def policy(self, resource_name: str) -> Any:
72
+ """
73
+ Decorator for registering a policy class.
74
+
75
+ This is the primary way to register policies. The decorator
76
+ associates a policy class with a resource name.
77
+
78
+ Args:
79
+ resource_name: The name of the resource this policy handles.
80
+
81
+ Returns:
82
+ A decorator function that registers the policy class.
83
+
84
+ Example:
85
+ >>> @registry.policy("database")
86
+ >>> class DatabasePolicy(Policy):
87
+ ... def can_query(self, context: dict) -> bool:
88
+ ... return "analyst" in self.user.roles
89
+ """
90
+ def decorator(policy_class: type[Policy]) -> type[Policy]:
91
+ self.register(resource_name, policy_class)
92
+ return policy_class
93
+ return decorator
94
+
95
+ def register(self, resource_name: str, policy_class: type[Policy]) -> None:
96
+ """
97
+ Register a policy class for a resource.
98
+
99
+ Args:
100
+ resource_name: The name of the resource.
101
+ policy_class: The policy class to register.
102
+
103
+ Raises:
104
+ ValueError: If a policy is already registered for this resource.
105
+
106
+ Example:
107
+ >>> registry.register("documents", DocumentPolicy)
108
+ """
109
+ with self._lock:
110
+ if resource_name in self._policies:
111
+ existing = self._policies[resource_name].__name__
112
+ logger.warning(
113
+ f"Overwriting policy for '{resource_name}': "
114
+ f"{existing} -> {policy_class.__name__}"
115
+ )
116
+
117
+ self._policies[resource_name] = policy_class
118
+ # Set the resource name on the policy class
119
+ policy_class._resource_name = resource_name
120
+
121
+ logger.debug(
122
+ f"Registered policy '{policy_class.__name__}' for resource '{resource_name}'"
123
+ )
124
+
125
+ def register_by_convention(self, policy_class: type[Policy]) -> None:
126
+ """
127
+ Register a policy class using naming convention.
128
+
129
+ Derives the resource name from the class name:
130
+ - DatabaseQueryPolicy -> "database_query"
131
+ - UserPolicy -> "user"
132
+
133
+ Args:
134
+ policy_class: The policy class to register.
135
+
136
+ Example:
137
+ >>> class FileSystemPolicy(Policy):
138
+ ... pass
139
+ >>> registry.register_by_convention(FileSystemPolicy)
140
+ >>> # Registered as "file_system"
141
+ """
142
+ resource_name = policy_class.get_resource_name()
143
+ self.register(resource_name, policy_class)
144
+
145
+ def get_policy(self, resource_name: str) -> type[Policy]:
146
+ """
147
+ Get the policy class for a resource.
148
+
149
+ Args:
150
+ resource_name: The name of the resource.
151
+
152
+ Returns:
153
+ The registered policy class.
154
+
155
+ Raises:
156
+ PolicyNotFoundError: If no policy is registered and no default is set.
157
+
158
+ Example:
159
+ >>> policy_class = registry.get_policy("database")
160
+ >>> policy = policy_class(user, db_connection)
161
+ """
162
+ with self._lock:
163
+ if resource_name in self._policies:
164
+ return self._policies[resource_name]
165
+
166
+ if self._default_policy is not None:
167
+ logger.debug(
168
+ f"No policy for '{resource_name}', using default: "
169
+ f"{self._default_policy.__name__}"
170
+ )
171
+ return self._default_policy
172
+
173
+ available = list(self._policies.keys())
174
+ raise PolicyNotFoundError(resource_name, available)
175
+
176
+ def get_policy_instance(
177
+ self,
178
+ resource_name: str,
179
+ user: UserContext,
180
+ resource: Any = None,
181
+ ) -> Policy:
182
+ """
183
+ Get an instantiated policy for a resource.
184
+
185
+ Convenience method that looks up the policy class and
186
+ creates an instance with the provided user and resource.
187
+
188
+ Args:
189
+ resource_name: The name of the resource.
190
+ user: The user context for the policy.
191
+ resource: The resource instance (optional).
192
+
193
+ Returns:
194
+ An instantiated policy object.
195
+
196
+ Example:
197
+ >>> policy = registry.get_policy_instance("document", user, doc)
198
+ >>> if policy.can("read"):
199
+ ... return doc.content
200
+ """
201
+ policy_class = self.get_policy(resource_name)
202
+ return policy_class(user, resource)
203
+
204
+ def has_policy(self, resource_name: str) -> bool:
205
+ """
206
+ Check if a policy is registered for a resource.
207
+
208
+ Args:
209
+ resource_name: The name of the resource.
210
+
211
+ Returns:
212
+ True if a policy is registered, False otherwise.
213
+ """
214
+ with self._lock:
215
+ return resource_name in self._policies
216
+
217
+ def list_policies(self) -> dict[str, str]:
218
+ """
219
+ List all registered policies.
220
+
221
+ Returns:
222
+ Dictionary mapping resource names to policy class names.
223
+
224
+ Example:
225
+ >>> registry.list_policies()
226
+ {'database': 'DatabasePolicy', 'file': 'FilePolicy'}
227
+ """
228
+ with self._lock:
229
+ return {
230
+ resource: policy.__name__
231
+ for resource, policy in self._policies.items()
232
+ }
233
+
234
+ def unregister(self, resource_name: str) -> bool:
235
+ """
236
+ Unregister a policy for a resource.
237
+
238
+ Args:
239
+ resource_name: The name of the resource.
240
+
241
+ Returns:
242
+ True if a policy was unregistered, False if none was registered.
243
+ """
244
+ with self._lock:
245
+ if resource_name in self._policies:
246
+ del self._policies[resource_name]
247
+ logger.debug(f"Unregistered policy for resource '{resource_name}'")
248
+ return True
249
+ return False
250
+
251
+ def clear(self) -> None:
252
+ """
253
+ Clear all registered policies.
254
+
255
+ Useful for testing or reconfiguration.
256
+ """
257
+ with self._lock:
258
+ self._policies.clear()
259
+ self._authorized_resources.clear()
260
+ logger.debug("Cleared all registered policies")
261
+
262
+ def set_default_policy(self, policy_class: type[Policy] | None) -> None:
263
+ """
264
+ Set or clear the default policy.
265
+
266
+ Args:
267
+ policy_class: The default policy class, or None to clear.
268
+ """
269
+ with self._lock:
270
+ self._default_policy = policy_class
271
+ if policy_class:
272
+ logger.debug(f"Set default policy to '{policy_class.__name__}'")
273
+ else:
274
+ logger.debug("Cleared default policy")
275
+
276
+ # Authorization tracking methods
277
+
278
+ def mark_authorized(self, resource_name: str) -> None:
279
+ """
280
+ Mark a resource as having been authorized.
281
+
282
+ This is called automatically by the authorization system
283
+ when a policy check is performed.
284
+
285
+ Args:
286
+ resource_name: The name of the authorized resource.
287
+ """
288
+ with self._lock:
289
+ self._authorized_resources.add(resource_name)
290
+
291
+ def clear_authorized(self) -> None:
292
+ """
293
+ Clear the set of authorized resources.
294
+
295
+ Should be called at the start of each request to track
296
+ which resources were checked during that request.
297
+ """
298
+ with self._lock:
299
+ self._authorized_resources.clear()
300
+
301
+ def verify_all_authorized(self, expected_resources: list[str]) -> tuple[bool, list[str]]:
302
+ """
303
+ Verify that all expected resources were authorized.
304
+
305
+ This method helps catch cases where authorization was
306
+ accidentally skipped for a resource that should have been checked.
307
+
308
+ Args:
309
+ expected_resources: List of resource names that should have been authorized.
310
+
311
+ Returns:
312
+ Tuple of (all_authorized, missing_resources).
313
+ - all_authorized: True if all resources were checked.
314
+ - missing_resources: List of resources that were not authorized.
315
+
316
+ Example:
317
+ >>> # At end of request handling
318
+ >>> ok, missing = registry.verify_all_authorized(["document", "user"])
319
+ >>> if not ok:
320
+ ... logger.error(f"Authorization skipped for: {missing}")
321
+ """
322
+ with self._lock:
323
+ missing = [
324
+ resource for resource in expected_resources
325
+ if resource not in self._authorized_resources
326
+ ]
327
+ return len(missing) == 0, missing
328
+
329
+ def get_authorized_resources(self) -> set[str]:
330
+ """
331
+ Get the set of resources that have been authorized.
332
+
333
+ Returns:
334
+ Set of resource names that were checked.
335
+ """
336
+ with self._lock:
337
+ return self._authorized_resources.copy()
338
+
339
+
340
+ # Global registry instance for convenience
341
+ _global_registry: PolicyRegistry | None = None
342
+
343
+
344
+ def get_global_registry() -> PolicyRegistry:
345
+ """
346
+ Get the global policy registry instance.
347
+
348
+ Creates one if it doesn't exist. This provides a convenient
349
+ singleton for simple use cases.
350
+
351
+ Returns:
352
+ The global PolicyRegistry instance.
353
+
354
+ Example:
355
+ >>> from proxilion.policies.registry import get_global_registry
356
+ >>> registry = get_global_registry()
357
+ >>> @registry.policy("my_resource")
358
+ >>> class MyPolicy(Policy):
359
+ ... pass
360
+ """
361
+ global _global_registry
362
+ if _global_registry is None:
363
+ _global_registry = PolicyRegistry()
364
+ return _global_registry
365
+
366
+
367
+ def reset_global_registry() -> None:
368
+ """
369
+ Reset the global registry.
370
+
371
+ Clears the global registry instance. Primarily useful for testing.
372
+ """
373
+ global _global_registry
374
+ if _global_registry is not None:
375
+ _global_registry.clear()
376
+ _global_registry = None
@@ -0,0 +1,201 @@
1
+ """
2
+ Provider-agnostic helpers for AI integrations.
3
+
4
+ Supports:
5
+ - OpenAI / Azure OpenAI
6
+ - Anthropic Claude
7
+ - Google Vertex AI / Gemini
8
+ - AWS Bedrock (via OpenAI-compatible interface)
9
+ - Local models (Ollama)
10
+
11
+ This module provides a unified interface for working with tool calls
12
+ across different LLM providers, enabling provider-agnostic authorization
13
+ and execution.
14
+
15
+ Example:
16
+ >>> from proxilion.providers import (
17
+ ... get_adapter, detect_provider, UnifiedToolCall,
18
+ ... OpenAIAdapter, AnthropicAdapter, GeminiAdapter,
19
+ ... )
20
+ >>>
21
+ >>> # Auto-detect provider from response
22
+ >>> adapter = get_adapter(response=llm_response)
23
+ >>> tool_calls = adapter.extract_tool_calls(llm_response)
24
+ >>>
25
+ >>> # Or specify provider explicitly
26
+ >>> adapter = get_adapter(provider="openai")
27
+ >>> openai_tools = adapter.format_tools(registry.list_enabled())
28
+ >>>
29
+ >>> # Work with unified tool calls
30
+ >>> for call in tool_calls:
31
+ ... if auth.can(user, "execute", call.name):
32
+ ... result = execute_tool(call.name, **call.arguments)
33
+ ... response_msg = adapter.format_tool_result(call, result)
34
+ """
35
+
36
+ from proxilion.providers.adapter import (
37
+ BaseAdapter,
38
+ Provider,
39
+ ProviderAdapter,
40
+ UnifiedResponse,
41
+ UnifiedToolCall,
42
+ UnifiedToolResult,
43
+ detect_provider,
44
+ detect_provider_safe,
45
+ )
46
+ from proxilion.providers.anthropic_adapter import AnthropicAdapter
47
+ from proxilion.providers.gemini_adapter import GeminiAdapter
48
+ from proxilion.providers.openai_adapter import OpenAIAdapter
49
+
50
+ # Adapter registry
51
+ _ADAPTERS: dict[str, type[BaseAdapter]] = {
52
+ "openai": OpenAIAdapter,
53
+ "anthropic": AnthropicAdapter,
54
+ "gemini": GeminiAdapter,
55
+ "vertexai": GeminiAdapter, # Alias
56
+ "google": GeminiAdapter, # Alias
57
+ }
58
+
59
+ # Singleton instances
60
+ _adapter_instances: dict[str, BaseAdapter] = {}
61
+
62
+
63
+ def get_adapter(
64
+ provider: str | Provider | None = None,
65
+ response: object | None = None,
66
+ ) -> BaseAdapter:
67
+ """
68
+ Get the appropriate adapter for a provider.
69
+
70
+ Can auto-detect the provider from a response object or
71
+ accept an explicit provider name.
72
+
73
+ Args:
74
+ provider: Provider name or enum (e.g., "openai", Provider.ANTHROPIC).
75
+ response: Optional response object for auto-detection.
76
+
77
+ Returns:
78
+ Appropriate adapter instance.
79
+
80
+ Raises:
81
+ ValueError: If provider cannot be determined or is unknown.
82
+
83
+ Example:
84
+ >>> # Auto-detect from response
85
+ >>> adapter = get_adapter(response=openai_response)
86
+ >>>
87
+ >>> # Explicit provider
88
+ >>> adapter = get_adapter(provider="anthropic")
89
+ >>> adapter = get_adapter(provider=Provider.GEMINI)
90
+ """
91
+ # Auto-detect if no provider specified
92
+ if provider is None:
93
+ if response is not None:
94
+ provider = detect_provider(response)
95
+ else:
96
+ raise ValueError("Must specify provider or response for auto-detection")
97
+
98
+ # Convert Provider enum to string
99
+ provider_key = provider.value if isinstance(provider, Provider) else provider.lower()
100
+
101
+ # Get or create adapter instance
102
+ if provider_key not in _adapter_instances:
103
+ if provider_key not in _ADAPTERS:
104
+ raise ValueError(
105
+ f"Unknown provider: {provider_key}. "
106
+ f"Supported: {list(_ADAPTERS.keys())}"
107
+ )
108
+ _adapter_instances[provider_key] = _ADAPTERS[provider_key]()
109
+
110
+ return _adapter_instances[provider_key]
111
+
112
+
113
+ def register_adapter(name: str, adapter_class: type[BaseAdapter]) -> None:
114
+ """
115
+ Register a custom adapter.
116
+
117
+ Args:
118
+ name: Provider name to register under.
119
+ adapter_class: Adapter class (must inherit from BaseAdapter).
120
+
121
+ Example:
122
+ >>> class MyCustomAdapter(BaseAdapter):
123
+ ... ...
124
+ >>> register_adapter("custom", MyCustomAdapter)
125
+ >>> adapter = get_adapter("custom")
126
+ """
127
+ _ADAPTERS[name.lower()] = adapter_class
128
+ # Clear cached instance if exists
129
+ if name.lower() in _adapter_instances:
130
+ del _adapter_instances[name.lower()]
131
+
132
+
133
+ def list_providers() -> list[str]:
134
+ """
135
+ List all registered provider names.
136
+
137
+ Returns:
138
+ List of provider names.
139
+ """
140
+ return list(_ADAPTERS.keys())
141
+
142
+
143
+ def extract_tool_calls(response: object) -> list[UnifiedToolCall]:
144
+ """
145
+ Convenience function to extract tool calls from any supported response.
146
+
147
+ Auto-detects the provider and extracts tool calls.
148
+
149
+ Args:
150
+ response: LLM response object.
151
+
152
+ Returns:
153
+ List of unified tool calls.
154
+
155
+ Example:
156
+ >>> tool_calls = extract_tool_calls(llm_response)
157
+ >>> for call in tool_calls:
158
+ ... print(f"{call.name}: {call.arguments}")
159
+ """
160
+ adapter = get_adapter(response=response)
161
+ return adapter.extract_tool_calls(response)
162
+
163
+
164
+ def extract_response(response: object) -> UnifiedResponse:
165
+ """
166
+ Convenience function to extract full response from any supported response.
167
+
168
+ Auto-detects the provider and extracts the response.
169
+
170
+ Args:
171
+ response: LLM response object.
172
+
173
+ Returns:
174
+ UnifiedResponse instance.
175
+ """
176
+ adapter = get_adapter(response=response)
177
+ return adapter.extract_response(response)
178
+
179
+
180
+ __all__ = [
181
+ # Core types
182
+ "Provider",
183
+ "ProviderAdapter",
184
+ "BaseAdapter",
185
+ "UnifiedToolCall",
186
+ "UnifiedToolResult",
187
+ "UnifiedResponse",
188
+ # Adapters
189
+ "OpenAIAdapter",
190
+ "AnthropicAdapter",
191
+ "GeminiAdapter",
192
+ # Factory functions
193
+ "get_adapter",
194
+ "register_adapter",
195
+ "list_providers",
196
+ "detect_provider",
197
+ "detect_provider_safe",
198
+ # Convenience functions
199
+ "extract_tool_calls",
200
+ "extract_response",
201
+ ]