genxai-framework 0.1.0__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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Resource limits and monitoring for tool execution."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from threading import Lock
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ResourceLimits:
|
|
15
|
+
"""Resource limits for tool execution."""
|
|
16
|
+
|
|
17
|
+
max_execution_time: float = 30.0 # seconds
|
|
18
|
+
max_memory_mb: Optional[float] = None # MB (not enforced yet, placeholder)
|
|
19
|
+
max_cpu_percent: Optional[float] = None # % (not enforced yet, placeholder)
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
"""Validate limits."""
|
|
23
|
+
if self.max_execution_time <= 0:
|
|
24
|
+
raise ValueError("max_execution_time must be positive")
|
|
25
|
+
|
|
26
|
+
if self.max_memory_mb is not None and self.max_memory_mb <= 0:
|
|
27
|
+
raise ValueError("max_memory_mb must be positive")
|
|
28
|
+
|
|
29
|
+
if self.max_cpu_percent is not None and (
|
|
30
|
+
self.max_cpu_percent <= 0 or self.max_cpu_percent > 100
|
|
31
|
+
):
|
|
32
|
+
raise ValueError("max_cpu_percent must be between 0 and 100")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExecutionLimiter:
|
|
36
|
+
"""Rate limiter and resource monitor for tool execution."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
max_executions_per_minute: int = 60,
|
|
41
|
+
max_executions_per_hour: int = 1000,
|
|
42
|
+
resource_limits: Optional[ResourceLimits] = None
|
|
43
|
+
):
|
|
44
|
+
"""Initialize execution limiter.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
max_executions_per_minute: Maximum executions per minute per tool
|
|
48
|
+
max_executions_per_hour: Maximum executions per hour per tool
|
|
49
|
+
resource_limits: Resource limits for execution
|
|
50
|
+
"""
|
|
51
|
+
self.max_executions_per_minute = max_executions_per_minute
|
|
52
|
+
self.max_executions_per_hour = max_executions_per_hour
|
|
53
|
+
self.resource_limits = resource_limits or ResourceLimits()
|
|
54
|
+
|
|
55
|
+
# Track execution history
|
|
56
|
+
self._execution_history: Dict[str, list] = defaultdict(list)
|
|
57
|
+
self._lock = Lock()
|
|
58
|
+
|
|
59
|
+
logger.info(
|
|
60
|
+
f"ExecutionLimiter initialized: "
|
|
61
|
+
f"{max_executions_per_minute}/min, {max_executions_per_hour}/hour"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _clean_old_executions(self, tool_name: str, current_time: float) -> None:
|
|
65
|
+
"""Remove executions older than 1 hour.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
tool_name: Name of the tool
|
|
69
|
+
current_time: Current timestamp
|
|
70
|
+
"""
|
|
71
|
+
hour_ago = current_time - 3600
|
|
72
|
+
self._execution_history[tool_name] = [
|
|
73
|
+
ts for ts in self._execution_history[tool_name]
|
|
74
|
+
if ts > hour_ago
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def check_rate_limit(self, tool_name: str) -> tuple[bool, Optional[str]]:
|
|
78
|
+
"""Check if execution is allowed based on rate limits.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
tool_name: Name of the tool to execute
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Tuple of (allowed, error_message)
|
|
85
|
+
"""
|
|
86
|
+
with self._lock:
|
|
87
|
+
current_time = time.time()
|
|
88
|
+
|
|
89
|
+
# Clean old executions
|
|
90
|
+
self._clean_old_executions(tool_name, current_time)
|
|
91
|
+
|
|
92
|
+
executions = self._execution_history[tool_name]
|
|
93
|
+
|
|
94
|
+
# Check per-minute limit
|
|
95
|
+
minute_ago = current_time - 60
|
|
96
|
+
recent_executions = sum(1 for ts in executions if ts > minute_ago)
|
|
97
|
+
|
|
98
|
+
if recent_executions >= self.max_executions_per_minute:
|
|
99
|
+
error_msg = (
|
|
100
|
+
f"Rate limit exceeded: {recent_executions} executions in the last minute. "
|
|
101
|
+
f"Maximum allowed: {self.max_executions_per_minute}/minute"
|
|
102
|
+
)
|
|
103
|
+
logger.warning(f"Rate limit exceeded for tool '{tool_name}': {error_msg}")
|
|
104
|
+
return False, error_msg
|
|
105
|
+
|
|
106
|
+
# Check per-hour limit
|
|
107
|
+
if len(executions) >= self.max_executions_per_hour:
|
|
108
|
+
error_msg = (
|
|
109
|
+
f"Rate limit exceeded: {len(executions)} executions in the last hour. "
|
|
110
|
+
f"Maximum allowed: {self.max_executions_per_hour}/hour"
|
|
111
|
+
)
|
|
112
|
+
logger.warning(f"Rate limit exceeded for tool '{tool_name}': {error_msg}")
|
|
113
|
+
return False, error_msg
|
|
114
|
+
|
|
115
|
+
return True, None
|
|
116
|
+
|
|
117
|
+
def record_execution(self, tool_name: str) -> None:
|
|
118
|
+
"""Record a tool execution.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
tool_name: Name of the tool executed
|
|
122
|
+
"""
|
|
123
|
+
with self._lock:
|
|
124
|
+
current_time = time.time()
|
|
125
|
+
self._execution_history[tool_name].append(current_time)
|
|
126
|
+
logger.debug(f"Recorded execution for tool '{tool_name}'")
|
|
127
|
+
|
|
128
|
+
def get_execution_stats(self, tool_name: str) -> Dict[str, Any]:
|
|
129
|
+
"""Get execution statistics for a tool.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
tool_name: Name of the tool
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dictionary with execution statistics
|
|
136
|
+
"""
|
|
137
|
+
with self._lock:
|
|
138
|
+
current_time = time.time()
|
|
139
|
+
self._clean_old_executions(tool_name, current_time)
|
|
140
|
+
|
|
141
|
+
executions = self._execution_history[tool_name]
|
|
142
|
+
minute_ago = current_time - 60
|
|
143
|
+
|
|
144
|
+
recent_executions = sum(1 for ts in executions if ts > minute_ago)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"tool_name": tool_name,
|
|
148
|
+
"executions_last_minute": recent_executions,
|
|
149
|
+
"executions_last_hour": len(executions),
|
|
150
|
+
"max_per_minute": self.max_executions_per_minute,
|
|
151
|
+
"max_per_hour": self.max_executions_per_hour,
|
|
152
|
+
"remaining_minute": max(0, self.max_executions_per_minute - recent_executions),
|
|
153
|
+
"remaining_hour": max(0, self.max_executions_per_hour - len(executions)),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def reset_limits(self, tool_name: Optional[str] = None) -> None:
|
|
157
|
+
"""Reset execution limits.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
tool_name: Name of tool to reset, or None to reset all
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
if tool_name:
|
|
164
|
+
self._execution_history[tool_name] = []
|
|
165
|
+
logger.info(f"Reset execution limits for tool '{tool_name}'")
|
|
166
|
+
else:
|
|
167
|
+
self._execution_history.clear()
|
|
168
|
+
logger.info("Reset execution limits for all tools")
|
|
169
|
+
|
|
170
|
+
def get_all_stats(self) -> Dict[str, Dict[str, Any]]:
|
|
171
|
+
"""Get execution statistics for all tools.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Dictionary mapping tool names to their statistics
|
|
175
|
+
"""
|
|
176
|
+
with self._lock:
|
|
177
|
+
return {
|
|
178
|
+
tool_name: self.get_execution_stats(tool_name)
|
|
179
|
+
for tool_name in self._execution_history.keys()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Global execution limiter instance
|
|
184
|
+
_global_limiter: Optional[ExecutionLimiter] = None
|
|
185
|
+
_limiter_lock = Lock()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_global_limiter() -> ExecutionLimiter:
|
|
189
|
+
"""Get or create the global execution limiter.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Global ExecutionLimiter instance
|
|
193
|
+
"""
|
|
194
|
+
global _global_limiter
|
|
195
|
+
|
|
196
|
+
with _limiter_lock:
|
|
197
|
+
if _global_limiter is None:
|
|
198
|
+
_global_limiter = ExecutionLimiter()
|
|
199
|
+
logger.info("Created global ExecutionLimiter")
|
|
200
|
+
|
|
201
|
+
return _global_limiter
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def set_global_limiter(limiter: ExecutionLimiter) -> None:
|
|
205
|
+
"""Set the global execution limiter.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
limiter: ExecutionLimiter instance to use globally
|
|
209
|
+
"""
|
|
210
|
+
global _global_limiter
|
|
211
|
+
|
|
212
|
+
with _limiter_lock:
|
|
213
|
+
_global_limiter = limiter
|
|
214
|
+
logger.info("Updated global ExecutionLimiter")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Tool execution policy enforcement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from genxai.config import get_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_tool_allowed(tool_name: str) -> tuple[bool, str | None]:
|
|
9
|
+
"""Determine whether a tool can execute based on allow/deny lists."""
|
|
10
|
+
settings = get_settings()
|
|
11
|
+
allowlist = settings.allowlist_set()
|
|
12
|
+
denylist = settings.denylist_set()
|
|
13
|
+
|
|
14
|
+
if tool_name in denylist:
|
|
15
|
+
return False, f"Tool '{tool_name}' is denied by policy"
|
|
16
|
+
|
|
17
|
+
if allowlist and tool_name not in allowlist:
|
|
18
|
+
return False, f"Tool '{tool_name}' is not in allowlist"
|
|
19
|
+
|
|
20
|
+
return True, None
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Sandboxed execution environment for dynamic tools."""
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from RestrictedPython import compile_restricted, safe_globals
|
|
10
|
+
from RestrictedPython.Guards import guarded_iter_unpack_sequence, safer_getattr
|
|
11
|
+
RESTRICTED_PYTHON_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
RESTRICTED_PYTHON_AVAILABLE = False
|
|
14
|
+
safe_globals = {}
|
|
15
|
+
guarded_iter_unpack_sequence = None
|
|
16
|
+
safer_getattr = None
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExecutionTimeout(Exception):
|
|
22
|
+
"""Raised when code execution exceeds timeout limit."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SafeExecutor:
|
|
27
|
+
"""Secure executor for dynamic Python code using RestrictedPython."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, timeout: int = 30):
|
|
30
|
+
"""Initialize safe executor.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
timeout: Maximum execution time in seconds (default: 30)
|
|
34
|
+
"""
|
|
35
|
+
if not RESTRICTED_PYTHON_AVAILABLE:
|
|
36
|
+
logger.warning(
|
|
37
|
+
"RestrictedPython not available. Falling back to basic execution. "
|
|
38
|
+
"Install with: pip install RestrictedPython"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
self._safe_builtins = self._create_safe_builtins()
|
|
43
|
+
|
|
44
|
+
def _create_safe_builtins(self) -> Dict[str, Any]:
|
|
45
|
+
"""Create a safe set of built-in functions.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary of safe built-in functions
|
|
49
|
+
"""
|
|
50
|
+
if RESTRICTED_PYTHON_AVAILABLE:
|
|
51
|
+
# Use RestrictedPython's safe globals as base
|
|
52
|
+
safe_builtins = safe_globals.copy()
|
|
53
|
+
|
|
54
|
+
# Add additional safe functions
|
|
55
|
+
safe_builtins.update({
|
|
56
|
+
'_iter_unpack_sequence_': guarded_iter_unpack_sequence,
|
|
57
|
+
'_getattr_': safer_getattr,
|
|
58
|
+
# Safe built-ins
|
|
59
|
+
'abs': abs,
|
|
60
|
+
'all': all,
|
|
61
|
+
'any': any,
|
|
62
|
+
'bool': bool,
|
|
63
|
+
'dict': dict,
|
|
64
|
+
'enumerate': enumerate,
|
|
65
|
+
'float': float,
|
|
66
|
+
'int': int,
|
|
67
|
+
'len': len,
|
|
68
|
+
'list': list,
|
|
69
|
+
'max': max,
|
|
70
|
+
'min': min,
|
|
71
|
+
'range': range,
|
|
72
|
+
'round': round,
|
|
73
|
+
'sorted': sorted,
|
|
74
|
+
'str': str,
|
|
75
|
+
'sum': sum,
|
|
76
|
+
'tuple': tuple,
|
|
77
|
+
'zip': zip,
|
|
78
|
+
'isinstance': isinstance,
|
|
79
|
+
'type': type,
|
|
80
|
+
'hasattr': hasattr,
|
|
81
|
+
'getattr': safer_getattr,
|
|
82
|
+
})
|
|
83
|
+
else:
|
|
84
|
+
# Fallback to basic safe builtins
|
|
85
|
+
safe_builtins = {
|
|
86
|
+
'abs': abs,
|
|
87
|
+
'all': all,
|
|
88
|
+
'any': any,
|
|
89
|
+
'bool': bool,
|
|
90
|
+
'dict': dict,
|
|
91
|
+
'enumerate': enumerate,
|
|
92
|
+
'float': float,
|
|
93
|
+
'int': int,
|
|
94
|
+
'len': len,
|
|
95
|
+
'list': list,
|
|
96
|
+
'max': max,
|
|
97
|
+
'min': min,
|
|
98
|
+
'range': range,
|
|
99
|
+
'round': round,
|
|
100
|
+
'sorted': sorted,
|
|
101
|
+
'str': str,
|
|
102
|
+
'sum': sum,
|
|
103
|
+
'tuple': tuple,
|
|
104
|
+
'zip': zip,
|
|
105
|
+
'isinstance': isinstance,
|
|
106
|
+
'type': type,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return safe_builtins
|
|
110
|
+
|
|
111
|
+
@contextmanager
|
|
112
|
+
def _timeout_context(self):
|
|
113
|
+
"""Context manager for execution timeout.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ExecutionTimeout: If execution exceeds timeout
|
|
117
|
+
"""
|
|
118
|
+
def timeout_handler(signum, frame):
|
|
119
|
+
raise ExecutionTimeout(f"Execution exceeded {self.timeout} seconds")
|
|
120
|
+
|
|
121
|
+
# Set the signal handler and alarm
|
|
122
|
+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
|
123
|
+
signal.alarm(self.timeout)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
yield
|
|
127
|
+
finally:
|
|
128
|
+
# Disable the alarm and restore old handler
|
|
129
|
+
signal.alarm(0)
|
|
130
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
131
|
+
|
|
132
|
+
def compile_code(self, code: str, filename: str = '<dynamic>') -> Any:
|
|
133
|
+
"""Compile Python code with security restrictions.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
code: Python source code to compile
|
|
137
|
+
filename: Filename for error messages
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Compiled code object
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
SyntaxError: If code has syntax errors
|
|
144
|
+
ValueError: If code contains restricted operations
|
|
145
|
+
"""
|
|
146
|
+
if RESTRICTED_PYTHON_AVAILABLE:
|
|
147
|
+
# Use RestrictedPython for compilation
|
|
148
|
+
byte_code = compile_restricted(code, filename, 'exec')
|
|
149
|
+
|
|
150
|
+
# Check for compilation errors
|
|
151
|
+
if byte_code.errors:
|
|
152
|
+
error_msg = "; ".join(byte_code.errors)
|
|
153
|
+
raise ValueError(f"Code contains restricted operations: {error_msg}")
|
|
154
|
+
|
|
155
|
+
return byte_code.code
|
|
156
|
+
else:
|
|
157
|
+
# Fallback to standard compilation
|
|
158
|
+
try:
|
|
159
|
+
return compile(code, filename, 'exec')
|
|
160
|
+
except SyntaxError as e:
|
|
161
|
+
raise SyntaxError(f"Invalid Python code: {e}")
|
|
162
|
+
|
|
163
|
+
def execute(
|
|
164
|
+
self,
|
|
165
|
+
compiled_code: Any,
|
|
166
|
+
parameters: Dict[str, Any],
|
|
167
|
+
enable_timeout: bool = True
|
|
168
|
+
) -> Any:
|
|
169
|
+
"""Execute compiled code in a sandboxed environment.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
compiled_code: Compiled code object
|
|
173
|
+
parameters: Parameters to pass to the code
|
|
174
|
+
enable_timeout: Whether to enforce timeout (default: True)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Execution result from 'result' variable
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ExecutionTimeout: If execution exceeds timeout
|
|
181
|
+
ValueError: If code doesn't set 'result' variable
|
|
182
|
+
RuntimeError: If execution fails
|
|
183
|
+
"""
|
|
184
|
+
# Create execution namespace with safe builtins
|
|
185
|
+
namespace = {
|
|
186
|
+
'__builtins__': self._safe_builtins,
|
|
187
|
+
'params': parameters,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Execute with timeout if enabled
|
|
192
|
+
if enable_timeout:
|
|
193
|
+
with self._timeout_context():
|
|
194
|
+
exec(compiled_code, namespace)
|
|
195
|
+
else:
|
|
196
|
+
exec(compiled_code, namespace)
|
|
197
|
+
|
|
198
|
+
# Check for result variable
|
|
199
|
+
if 'result' not in namespace:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"Tool code must set a 'result' variable with the output"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return namespace['result']
|
|
205
|
+
|
|
206
|
+
except ExecutionTimeout:
|
|
207
|
+
logger.error("Code execution timed out")
|
|
208
|
+
raise
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Code execution failed: {e}")
|
|
211
|
+
raise RuntimeError(f"Execution failed: {e}")
|
|
212
|
+
|
|
213
|
+
def execute_code(
|
|
214
|
+
self,
|
|
215
|
+
code: str,
|
|
216
|
+
parameters: Dict[str, Any],
|
|
217
|
+
enable_timeout: bool = True
|
|
218
|
+
) -> Any:
|
|
219
|
+
"""Compile and execute code in one step.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
code: Python source code
|
|
223
|
+
parameters: Parameters to pass to the code
|
|
224
|
+
enable_timeout: Whether to enforce timeout (default: True)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Execution result
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
SyntaxError: If code has syntax errors
|
|
231
|
+
ValueError: If code contains restricted operations or missing result
|
|
232
|
+
ExecutionTimeout: If execution exceeds timeout
|
|
233
|
+
RuntimeError: If execution fails
|
|
234
|
+
"""
|
|
235
|
+
compiled_code = self.compile_code(code)
|
|
236
|
+
return self.execute(compiled_code, parameters, enable_timeout)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def create_safe_executor(timeout: int = 30) -> SafeExecutor:
|
|
240
|
+
"""Factory function to create a SafeExecutor instance.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
timeout: Maximum execution time in seconds
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
SafeExecutor instance
|
|
247
|
+
"""
|
|
248
|
+
return SafeExecutor(timeout=timeout)
|