proxilion 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic scope enforcement for Proxilion.
|
|
3
|
+
|
|
4
|
+
Binds tool calls to execution scopes (read_only, read_write, admin) so that
|
|
5
|
+
even if an agent has access to a tool, it can only use it within the scope
|
|
6
|
+
of the current operation.
|
|
7
|
+
|
|
8
|
+
Addresses:
|
|
9
|
+
- OWASP ASI02 (Tool Misuse)
|
|
10
|
+
- Principle of Least Privilege
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> from proxilion.security.scope_enforcer import (
|
|
14
|
+
... ScopeEnforcer, ExecutionScope, scoped_execution
|
|
15
|
+
... )
|
|
16
|
+
>>> from proxilion import UserContext
|
|
17
|
+
>>>
|
|
18
|
+
>>> enforcer = ScopeEnforcer()
|
|
19
|
+
>>>
|
|
20
|
+
>>> # In a read-only scope, only read operations are allowed
|
|
21
|
+
>>> user = UserContext(user_id="user_123", roles=["viewer"])
|
|
22
|
+
>>> with scoped_execution(enforcer, "read_only", user) as ctx:
|
|
23
|
+
... ctx.validate_tool("get_user") # OK
|
|
24
|
+
... ctx.validate_tool("delete_user") # Raises ScopeViolationError
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import fnmatch
|
|
30
|
+
import logging
|
|
31
|
+
import re
|
|
32
|
+
import threading
|
|
33
|
+
from collections.abc import Generator
|
|
34
|
+
from contextlib import contextmanager
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ExecutionScope(Enum):
|
|
43
|
+
"""
|
|
44
|
+
Predefined execution scopes for tool calls.
|
|
45
|
+
|
|
46
|
+
Scopes define what categories of operations are permitted.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
READ_ONLY = "read_only"
|
|
50
|
+
"""Only read operations (get, list, search, query)."""
|
|
51
|
+
|
|
52
|
+
READ_WRITE = "read_write"
|
|
53
|
+
"""Read and write operations (create, update, modify)."""
|
|
54
|
+
|
|
55
|
+
ADMIN = "admin"
|
|
56
|
+
"""All operations including delete and execute."""
|
|
57
|
+
|
|
58
|
+
CUSTOM = "custom"
|
|
59
|
+
"""Custom scope with explicit allow/deny lists."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ScopeBinding:
|
|
64
|
+
"""
|
|
65
|
+
Binding between a scope and its allowed/denied operations.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
scope: The execution scope this binding represents.
|
|
69
|
+
allowed_tools: Set of tool name patterns that are allowed.
|
|
70
|
+
denied_tools: Set of tool name patterns that are denied.
|
|
71
|
+
allowed_actions: Set of action names that are allowed.
|
|
72
|
+
denied_actions: Set of action names that are denied.
|
|
73
|
+
name: Optional name for the scope binding.
|
|
74
|
+
description: Optional description.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
scope: ExecutionScope
|
|
78
|
+
allowed_tools: set[str] = field(default_factory=set)
|
|
79
|
+
denied_tools: set[str] = field(default_factory=set)
|
|
80
|
+
allowed_actions: set[str] = field(default_factory=set)
|
|
81
|
+
denied_actions: set[str] = field(default_factory=set)
|
|
82
|
+
name: str = ""
|
|
83
|
+
description: str = ""
|
|
84
|
+
|
|
85
|
+
def allows_action(self, action: str) -> bool:
|
|
86
|
+
"""Check if an action is allowed in this scope."""
|
|
87
|
+
# If wildcard allowed, everything is allowed
|
|
88
|
+
if "*" in self.allowed_actions:
|
|
89
|
+
return action.lower() not in {a.lower() for a in self.denied_actions}
|
|
90
|
+
|
|
91
|
+
# Check if explicitly denied
|
|
92
|
+
if action.lower() in {a.lower() for a in self.denied_actions}:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# Check if explicitly allowed
|
|
96
|
+
if action.lower() in {a.lower() for a in self.allowed_actions}:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
# If allowed_actions is non-empty and action not in it, deny
|
|
100
|
+
if self.allowed_actions:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
# Default to allow if no restrictions specified
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def allows_tool(self, tool_name: str) -> bool:
|
|
107
|
+
"""Check if a tool is allowed in this scope."""
|
|
108
|
+
tool_lower = tool_name.lower()
|
|
109
|
+
|
|
110
|
+
# Check if explicitly denied by pattern
|
|
111
|
+
for pattern in self.denied_tools:
|
|
112
|
+
if fnmatch.fnmatch(tool_lower, pattern.lower()):
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Check if explicitly allowed by pattern
|
|
116
|
+
if self.allowed_tools:
|
|
117
|
+
for pattern in self.allowed_tools:
|
|
118
|
+
if pattern == "*" or fnmatch.fnmatch(tool_lower, pattern.lower()):
|
|
119
|
+
return True
|
|
120
|
+
return False # Not in allowed list
|
|
121
|
+
|
|
122
|
+
# Default to allow if no tool restrictions
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ToolClassification:
|
|
128
|
+
"""
|
|
129
|
+
Classification of a tool's default scope and actions.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
tool_name: Name of the tool.
|
|
133
|
+
default_scope: Default scope the tool belongs to.
|
|
134
|
+
actions: Set of actions the tool can perform.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
tool_name: str
|
|
138
|
+
default_scope: ExecutionScope
|
|
139
|
+
actions: set[str] = field(default_factory=set)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Built-in scope bindings
|
|
143
|
+
BUILTIN_SCOPES: dict[str, ScopeBinding] = {
|
|
144
|
+
"read_only": ScopeBinding(
|
|
145
|
+
scope=ExecutionScope.READ_ONLY,
|
|
146
|
+
allowed_actions={"read", "list", "get", "search", "query", "fetch", "find"},
|
|
147
|
+
denied_actions={
|
|
148
|
+
"write", "delete", "execute", "modify", "create", "update", "remove", "drop",
|
|
149
|
+
},
|
|
150
|
+
name="read_only",
|
|
151
|
+
description="Read-only operations only",
|
|
152
|
+
),
|
|
153
|
+
"read_write": ScopeBinding(
|
|
154
|
+
scope=ExecutionScope.READ_WRITE,
|
|
155
|
+
allowed_actions={
|
|
156
|
+
"read", "list", "get", "search", "query", "fetch", "find",
|
|
157
|
+
"write", "create", "modify", "update", "add", "set",
|
|
158
|
+
},
|
|
159
|
+
denied_actions={"delete", "execute", "remove", "drop", "destroy", "run"},
|
|
160
|
+
name="read_write",
|
|
161
|
+
description="Read and write operations, no delete or execute",
|
|
162
|
+
),
|
|
163
|
+
"admin": ScopeBinding(
|
|
164
|
+
scope=ExecutionScope.ADMIN,
|
|
165
|
+
allowed_actions={"*"},
|
|
166
|
+
denied_actions=set(),
|
|
167
|
+
name="admin",
|
|
168
|
+
description="All operations allowed",
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Default tool classification patterns
|
|
174
|
+
DEFAULT_TOOL_CLASSIFICATIONS: dict[str, ExecutionScope] = {
|
|
175
|
+
r"^(get|read|list|search|query|fetch|find|check|view|show)_": ExecutionScope.READ_ONLY,
|
|
176
|
+
r"^(create|write|update|modify|set|add|put|insert|save)_": ExecutionScope.READ_WRITE,
|
|
177
|
+
r"^(delete|remove|drop|destroy|execute|run|kill|terminate|purge)_": ExecutionScope.ADMIN,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ScopeEnforcer:
|
|
182
|
+
"""
|
|
183
|
+
Enforces execution scopes on tool calls.
|
|
184
|
+
|
|
185
|
+
The ScopeEnforcer validates that tool calls are permitted within
|
|
186
|
+
the current execution scope based on tool names, actions, and
|
|
187
|
+
explicit allow/deny rules.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> enforcer = ScopeEnforcer()
|
|
191
|
+
>>>
|
|
192
|
+
>>> # Validate a tool in read_only scope
|
|
193
|
+
>>> scope = enforcer.get_scope("read_only")
|
|
194
|
+
>>> allowed, reason = enforcer.validate_in_scope("get_user", "read", scope)
|
|
195
|
+
>>> print(allowed) # True
|
|
196
|
+
>>>
|
|
197
|
+
>>> # Try write operation in read_only scope
|
|
198
|
+
>>> allowed, reason = enforcer.validate_in_scope("update_user", "write", scope)
|
|
199
|
+
>>> print(allowed) # False
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
default_scope: ExecutionScope = ExecutionScope.READ_ONLY,
|
|
205
|
+
custom_scopes: dict[str, ScopeBinding] | None = None,
|
|
206
|
+
tool_classifications: dict[str, ExecutionScope] | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Initialize the scope enforcer.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
default_scope: Default scope when no scope is specified.
|
|
213
|
+
custom_scopes: Additional custom scope bindings.
|
|
214
|
+
tool_classifications: Custom tool name patterns to scope mappings.
|
|
215
|
+
"""
|
|
216
|
+
self._default_scope = default_scope
|
|
217
|
+
self._lock = threading.RLock()
|
|
218
|
+
|
|
219
|
+
# Initialize scopes with built-ins
|
|
220
|
+
self._scopes: dict[str, ScopeBinding] = dict(BUILTIN_SCOPES)
|
|
221
|
+
if custom_scopes:
|
|
222
|
+
self._scopes.update(custom_scopes)
|
|
223
|
+
|
|
224
|
+
# Initialize tool classifications
|
|
225
|
+
self._tool_classifications: dict[str, ExecutionScope] = dict(DEFAULT_TOOL_CLASSIFICATIONS)
|
|
226
|
+
if tool_classifications:
|
|
227
|
+
self._tool_classifications.update(tool_classifications)
|
|
228
|
+
|
|
229
|
+
# Cache for classified tools
|
|
230
|
+
self._tool_scope_cache: dict[str, ExecutionScope] = {}
|
|
231
|
+
|
|
232
|
+
def get_scope(self, scope: str | ExecutionScope) -> ScopeBinding:
|
|
233
|
+
"""
|
|
234
|
+
Get a scope binding by name or enum.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
scope: Scope name string or ExecutionScope enum.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
The ScopeBinding for the requested scope.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
ValueError: If scope is not found.
|
|
244
|
+
"""
|
|
245
|
+
with self._lock:
|
|
246
|
+
if isinstance(scope, ExecutionScope):
|
|
247
|
+
# Return built-in scope for enum
|
|
248
|
+
scope_name = scope.value
|
|
249
|
+
else:
|
|
250
|
+
scope_name = scope.lower()
|
|
251
|
+
|
|
252
|
+
if scope_name in self._scopes:
|
|
253
|
+
return self._scopes[scope_name]
|
|
254
|
+
|
|
255
|
+
raise ValueError(f"Unknown scope: {scope}")
|
|
256
|
+
|
|
257
|
+
def create_scope(
|
|
258
|
+
self,
|
|
259
|
+
name: str,
|
|
260
|
+
allowed_tools: set[str] | None = None,
|
|
261
|
+
denied_tools: set[str] | None = None,
|
|
262
|
+
allowed_actions: set[str] | None = None,
|
|
263
|
+
denied_actions: set[str] | None = None,
|
|
264
|
+
base_scope: ExecutionScope = ExecutionScope.CUSTOM,
|
|
265
|
+
description: str = "",
|
|
266
|
+
) -> ScopeBinding:
|
|
267
|
+
"""
|
|
268
|
+
Create a custom scope binding.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
name: Unique name for the scope.
|
|
272
|
+
allowed_tools: Set of tool patterns allowed.
|
|
273
|
+
denied_tools: Set of tool patterns denied.
|
|
274
|
+
allowed_actions: Set of actions allowed.
|
|
275
|
+
denied_actions: Set of actions denied.
|
|
276
|
+
base_scope: Base scope type.
|
|
277
|
+
description: Human-readable description.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The created ScopeBinding.
|
|
281
|
+
"""
|
|
282
|
+
binding = ScopeBinding(
|
|
283
|
+
scope=base_scope,
|
|
284
|
+
allowed_tools=allowed_tools or set(),
|
|
285
|
+
denied_tools=denied_tools or set(),
|
|
286
|
+
allowed_actions=allowed_actions or set(),
|
|
287
|
+
denied_actions=denied_actions or set(),
|
|
288
|
+
name=name,
|
|
289
|
+
description=description,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
with self._lock:
|
|
293
|
+
self._scopes[name.lower()] = binding
|
|
294
|
+
|
|
295
|
+
return binding
|
|
296
|
+
|
|
297
|
+
def create_scope_from_enum(self, scope: ExecutionScope) -> ScopeBinding:
|
|
298
|
+
"""
|
|
299
|
+
Create a scope binding from an ExecutionScope enum.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
scope: The ExecutionScope enum value.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
A ScopeBinding matching the enum.
|
|
306
|
+
"""
|
|
307
|
+
return self.get_scope(scope.value)
|
|
308
|
+
|
|
309
|
+
def classify_tool(
|
|
310
|
+
self,
|
|
311
|
+
tool_name: str,
|
|
312
|
+
scope: ExecutionScope | None = None,
|
|
313
|
+
actions: set[str] | None = None,
|
|
314
|
+
) -> ToolClassification:
|
|
315
|
+
"""
|
|
316
|
+
Classify a tool by name pattern or explicit assignment.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
tool_name: Name of the tool.
|
|
320
|
+
scope: Explicit scope assignment (overrides pattern matching).
|
|
321
|
+
actions: Set of actions the tool performs.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
ToolClassification for the tool.
|
|
325
|
+
"""
|
|
326
|
+
with self._lock:
|
|
327
|
+
if scope is not None:
|
|
328
|
+
# Explicit scope assignment
|
|
329
|
+
self._tool_scope_cache[tool_name.lower()] = scope
|
|
330
|
+
return ToolClassification(
|
|
331
|
+
tool_name=tool_name,
|
|
332
|
+
default_scope=scope,
|
|
333
|
+
actions=actions or set(),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Check cache first
|
|
337
|
+
if tool_name.lower() in self._tool_scope_cache:
|
|
338
|
+
return ToolClassification(
|
|
339
|
+
tool_name=tool_name,
|
|
340
|
+
default_scope=self._tool_scope_cache[tool_name.lower()],
|
|
341
|
+
actions=actions or set(),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Pattern matching
|
|
345
|
+
for pattern, pattern_scope in self._tool_classifications.items():
|
|
346
|
+
if re.match(pattern, tool_name, re.IGNORECASE):
|
|
347
|
+
self._tool_scope_cache[tool_name.lower()] = pattern_scope
|
|
348
|
+
return ToolClassification(
|
|
349
|
+
tool_name=tool_name,
|
|
350
|
+
default_scope=pattern_scope,
|
|
351
|
+
actions=actions or self._infer_actions(tool_name),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Default to read_only for unknown tools
|
|
355
|
+
return ToolClassification(
|
|
356
|
+
tool_name=tool_name,
|
|
357
|
+
default_scope=self._default_scope,
|
|
358
|
+
actions=actions or set(),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _infer_actions(self, tool_name: str) -> set[str]:
|
|
362
|
+
"""Infer actions from tool name prefix."""
|
|
363
|
+
tool_lower = tool_name.lower()
|
|
364
|
+
actions = set()
|
|
365
|
+
|
|
366
|
+
read_prefixes = ("get_", "read_", "list_", "search_", "query_", "fetch_", "find_")
|
|
367
|
+
if any(tool_lower.startswith(p) for p in read_prefixes):
|
|
368
|
+
actions.add("read")
|
|
369
|
+
if any(tool_lower.startswith(p) for p in ("create_", "write_", "add_", "insert_", "save_")):
|
|
370
|
+
actions.add("write")
|
|
371
|
+
actions.add("create")
|
|
372
|
+
if any(tool_lower.startswith(p) for p in ("update_", "modify_", "set_", "put_")):
|
|
373
|
+
actions.add("write")
|
|
374
|
+
actions.add("modify")
|
|
375
|
+
delete_prefixes = ("delete_", "remove_", "drop_", "destroy_", "purge_")
|
|
376
|
+
if any(tool_lower.startswith(p) for p in delete_prefixes):
|
|
377
|
+
actions.add("delete")
|
|
378
|
+
if any(tool_lower.startswith(p) for p in ("execute_", "run_", "kill_", "terminate_")):
|
|
379
|
+
actions.add("execute")
|
|
380
|
+
|
|
381
|
+
return actions or {"execute"} # Default to execute if unknown
|
|
382
|
+
|
|
383
|
+
def validate_in_scope(
|
|
384
|
+
self,
|
|
385
|
+
tool_name: str,
|
|
386
|
+
action: str,
|
|
387
|
+
scope: ScopeBinding,
|
|
388
|
+
) -> tuple[bool, str | None]:
|
|
389
|
+
"""
|
|
390
|
+
Validate if a tool call is allowed in the given scope.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
tool_name: Name of the tool to validate.
|
|
394
|
+
action: The action being performed.
|
|
395
|
+
scope: The scope binding to validate against.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of (allowed, reason). If not allowed, reason explains why.
|
|
399
|
+
"""
|
|
400
|
+
# Check if tool is explicitly denied
|
|
401
|
+
if not scope.allows_tool(tool_name):
|
|
402
|
+
scope_id = scope.name or scope.scope.value
|
|
403
|
+
return False, f"Tool '{tool_name}' is not allowed in scope '{scope_id}'"
|
|
404
|
+
|
|
405
|
+
# Check if action is allowed
|
|
406
|
+
if not scope.allows_action(action):
|
|
407
|
+
scope_id = scope.name or scope.scope.value
|
|
408
|
+
return False, f"Action '{action}' is not allowed in scope '{scope_id}'"
|
|
409
|
+
|
|
410
|
+
# Check tool classification against scope
|
|
411
|
+
classification = self.classify_tool(tool_name)
|
|
412
|
+
tool_scope = classification.default_scope
|
|
413
|
+
|
|
414
|
+
# Scope hierarchy: READ_ONLY < READ_WRITE < ADMIN
|
|
415
|
+
scope_levels = {
|
|
416
|
+
ExecutionScope.READ_ONLY: 1,
|
|
417
|
+
ExecutionScope.READ_WRITE: 2,
|
|
418
|
+
ExecutionScope.ADMIN: 3,
|
|
419
|
+
ExecutionScope.CUSTOM: 0, # Custom scopes use explicit rules
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
current_level = scope_levels.get(scope.scope, 0)
|
|
423
|
+
required_level = scope_levels.get(tool_scope, 0)
|
|
424
|
+
|
|
425
|
+
# Custom scopes only use explicit allow/deny rules
|
|
426
|
+
if scope.scope == ExecutionScope.CUSTOM:
|
|
427
|
+
return True, None
|
|
428
|
+
|
|
429
|
+
# Check if current scope level is sufficient
|
|
430
|
+
if required_level > current_level:
|
|
431
|
+
return False, (
|
|
432
|
+
f"Tool '{tool_name}' requires '{tool_scope.value}' scope, "
|
|
433
|
+
f"but current scope is '{scope.scope.value}'"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return True, None
|
|
437
|
+
|
|
438
|
+
def get_allowed_tools(self, scope: ScopeBinding) -> set[str]:
|
|
439
|
+
"""
|
|
440
|
+
Get the set of allowed tool patterns for a scope.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
scope: The scope binding.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Set of allowed tool patterns.
|
|
447
|
+
"""
|
|
448
|
+
if scope.allowed_tools:
|
|
449
|
+
return set(scope.allowed_tools)
|
|
450
|
+
|
|
451
|
+
# If no explicit allowed tools, return patterns based on scope level
|
|
452
|
+
if scope.scope == ExecutionScope.READ_ONLY:
|
|
453
|
+
return {"get_*", "read_*", "list_*", "search_*", "query_*", "fetch_*", "find_*"}
|
|
454
|
+
elif scope.scope == ExecutionScope.READ_WRITE:
|
|
455
|
+
return {
|
|
456
|
+
"get_*", "read_*", "list_*", "search_*", "query_*", "fetch_*", "find_*",
|
|
457
|
+
"create_*", "write_*", "update_*", "modify_*", "set_*", "add_*",
|
|
458
|
+
}
|
|
459
|
+
elif scope.scope == ExecutionScope.ADMIN:
|
|
460
|
+
return {"*"}
|
|
461
|
+
|
|
462
|
+
return set()
|
|
463
|
+
|
|
464
|
+
def add_scope(self, name: str, binding: ScopeBinding) -> None:
|
|
465
|
+
"""
|
|
466
|
+
Add a scope binding.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
name: Name for the scope.
|
|
470
|
+
binding: The ScopeBinding to add.
|
|
471
|
+
"""
|
|
472
|
+
with self._lock:
|
|
473
|
+
self._scopes[name.lower()] = binding
|
|
474
|
+
|
|
475
|
+
def remove_scope(self, name: str) -> bool:
|
|
476
|
+
"""
|
|
477
|
+
Remove a custom scope.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
name: Name of the scope to remove.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
True if removed, False if not found or built-in.
|
|
484
|
+
"""
|
|
485
|
+
name_lower = name.lower()
|
|
486
|
+
if name_lower in BUILTIN_SCOPES:
|
|
487
|
+
return False # Cannot remove built-in scopes
|
|
488
|
+
|
|
489
|
+
with self._lock:
|
|
490
|
+
if name_lower in self._scopes:
|
|
491
|
+
del self._scopes[name_lower]
|
|
492
|
+
return True
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
def get_scopes(self) -> dict[str, ScopeBinding]:
|
|
496
|
+
"""Get all registered scopes."""
|
|
497
|
+
with self._lock:
|
|
498
|
+
return dict(self._scopes)
|
|
499
|
+
|
|
500
|
+
def add_tool_classification(self, pattern: str, scope: ExecutionScope) -> None:
|
|
501
|
+
"""
|
|
502
|
+
Add a tool classification pattern.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
pattern: Regex pattern to match tool names.
|
|
506
|
+
scope: Scope to assign to matching tools.
|
|
507
|
+
"""
|
|
508
|
+
with self._lock:
|
|
509
|
+
self._tool_classifications[pattern] = scope
|
|
510
|
+
# Clear cache as classifications changed
|
|
511
|
+
self._tool_scope_cache.clear()
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class ScopeContext:
|
|
515
|
+
"""
|
|
516
|
+
Context for scoped execution.
|
|
517
|
+
|
|
518
|
+
Tracks tool calls within a scope and validates them against
|
|
519
|
+
the scope's rules.
|
|
520
|
+
|
|
521
|
+
Example:
|
|
522
|
+
>>> ctx = ScopeContext(enforcer, scope, user)
|
|
523
|
+
>>> ctx.validate_tool("get_user") # Returns True
|
|
524
|
+
>>> ctx.validate_tool("delete_user") # Raises ScopeViolationError
|
|
525
|
+
>>> print(ctx.get_calls()) # ["get_user"]
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
def __init__(
|
|
529
|
+
self,
|
|
530
|
+
enforcer: ScopeEnforcer,
|
|
531
|
+
scope: ScopeBinding,
|
|
532
|
+
user: Any,
|
|
533
|
+
) -> None:
|
|
534
|
+
"""
|
|
535
|
+
Initialize scope context.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
enforcer: The ScopeEnforcer instance.
|
|
539
|
+
scope: The scope binding for this context.
|
|
540
|
+
user: The user context.
|
|
541
|
+
"""
|
|
542
|
+
self.enforcer = enforcer
|
|
543
|
+
self.scope = scope
|
|
544
|
+
self.user = user
|
|
545
|
+
self._calls: list[tuple[str, str]] = [] # (tool_name, action)
|
|
546
|
+
self._closed = False
|
|
547
|
+
|
|
548
|
+
def validate_tool(self, tool_name: str, action: str = "execute") -> bool:
|
|
549
|
+
"""
|
|
550
|
+
Validate a tool call within this scope.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
tool_name: Name of the tool.
|
|
554
|
+
action: Action being performed.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
True if allowed.
|
|
558
|
+
|
|
559
|
+
Raises:
|
|
560
|
+
ScopeViolationError: If the tool call is not allowed.
|
|
561
|
+
"""
|
|
562
|
+
if self._closed:
|
|
563
|
+
raise RuntimeError("ScopeContext is closed")
|
|
564
|
+
|
|
565
|
+
# Import here to avoid circular import
|
|
566
|
+
from proxilion.exceptions import ScopeViolationError
|
|
567
|
+
|
|
568
|
+
allowed, reason = self.enforcer.validate_in_scope(tool_name, action, self.scope)
|
|
569
|
+
if not allowed:
|
|
570
|
+
raise ScopeViolationError(
|
|
571
|
+
tool_name=tool_name,
|
|
572
|
+
scope_name=self.scope.name or self.scope.scope.value,
|
|
573
|
+
reason=reason,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
self._calls.append((tool_name, action))
|
|
577
|
+
logger.debug(
|
|
578
|
+
f"Tool '{tool_name}' (action: {action}) validated in scope '{self.scope.name}'"
|
|
579
|
+
)
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
def is_tool_allowed(self, tool_name: str, action: str = "execute") -> bool:
|
|
583
|
+
"""
|
|
584
|
+
Check if a tool is allowed without raising an exception.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
tool_name: Name of the tool.
|
|
588
|
+
action: Action being performed.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
True if allowed, False otherwise.
|
|
592
|
+
"""
|
|
593
|
+
if self._closed:
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
allowed, _ = self.enforcer.validate_in_scope(tool_name, action, self.scope)
|
|
597
|
+
return allowed
|
|
598
|
+
|
|
599
|
+
def get_calls(self) -> list[tuple[str, str]]:
|
|
600
|
+
"""Get all validated tool calls in this context."""
|
|
601
|
+
return list(self._calls)
|
|
602
|
+
|
|
603
|
+
def get_tool_names(self) -> list[str]:
|
|
604
|
+
"""Get just the tool names from validated calls."""
|
|
605
|
+
return [call[0] for call in self._calls]
|
|
606
|
+
|
|
607
|
+
def close(self) -> None:
|
|
608
|
+
"""Close the scope context."""
|
|
609
|
+
self._closed = True
|
|
610
|
+
logger.debug(
|
|
611
|
+
f"ScopeContext closed. Total calls: {len(self._calls)}"
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
@property
|
|
615
|
+
def is_closed(self) -> bool:
|
|
616
|
+
"""Check if context is closed."""
|
|
617
|
+
return self._closed
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@contextmanager
|
|
621
|
+
def scoped_execution(
|
|
622
|
+
enforcer: ScopeEnforcer,
|
|
623
|
+
scope: ExecutionScope | str,
|
|
624
|
+
user: Any,
|
|
625
|
+
) -> Generator[ScopeContext, None, None]:
|
|
626
|
+
"""
|
|
627
|
+
Context manager for scoped tool execution.
|
|
628
|
+
|
|
629
|
+
Creates a scope context that validates all tool calls against
|
|
630
|
+
the specified scope's rules.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
enforcer: The ScopeEnforcer instance.
|
|
634
|
+
scope: Scope name or ExecutionScope enum.
|
|
635
|
+
user: The user context for this execution.
|
|
636
|
+
|
|
637
|
+
Yields:
|
|
638
|
+
ScopeContext for validating tool calls.
|
|
639
|
+
|
|
640
|
+
Example:
|
|
641
|
+
>>> with scoped_execution(enforcer, "read_only", user) as ctx:
|
|
642
|
+
... ctx.validate_tool("get_user") # OK
|
|
643
|
+
... ctx.validate_tool("delete_user") # Raises ScopeViolationError
|
|
644
|
+
"""
|
|
645
|
+
if isinstance(scope, str):
|
|
646
|
+
scope_binding = enforcer.get_scope(scope)
|
|
647
|
+
else:
|
|
648
|
+
scope_binding = enforcer.create_scope_from_enum(scope)
|
|
649
|
+
|
|
650
|
+
ctx = ScopeContext(enforcer, scope_binding, user)
|
|
651
|
+
logger.debug(f"Entering scope '{scope_binding.name or scope_binding.scope.value}'")
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
yield ctx
|
|
655
|
+
finally:
|
|
656
|
+
ctx.close()
|
|
657
|
+
logger.debug(
|
|
658
|
+
f"Exited scope '{scope_binding.name or scope_binding.scope.value}' "
|
|
659
|
+
f"with {len(ctx.get_calls())} tool calls"
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def create_scope_enforcer(
|
|
664
|
+
default_scope: ExecutionScope = ExecutionScope.READ_ONLY,
|
|
665
|
+
custom_scopes: dict[str, ScopeBinding] | None = None,
|
|
666
|
+
) -> ScopeEnforcer:
|
|
667
|
+
"""
|
|
668
|
+
Factory function to create a ScopeEnforcer.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
default_scope: Default scope for unknown tools.
|
|
672
|
+
custom_scopes: Additional custom scope bindings.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
Configured ScopeEnforcer instance.
|
|
676
|
+
"""
|
|
677
|
+
return ScopeEnforcer(
|
|
678
|
+
default_scope=default_scope,
|
|
679
|
+
custom_scopes=custom_scopes,
|
|
680
|
+
)
|