jarviscore-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.
- examples/calculator_agent_example.py +77 -0
- examples/multi_agent_workflow.py +132 -0
- examples/research_agent_example.py +76 -0
- jarviscore/__init__.py +54 -0
- jarviscore/cli/__init__.py +7 -0
- jarviscore/cli/__main__.py +33 -0
- jarviscore/cli/check.py +404 -0
- jarviscore/cli/smoketest.py +371 -0
- jarviscore/config/__init__.py +7 -0
- jarviscore/config/settings.py +128 -0
- jarviscore/core/__init__.py +7 -0
- jarviscore/core/agent.py +163 -0
- jarviscore/core/mesh.py +463 -0
- jarviscore/core/profile.py +64 -0
- jarviscore/docs/API_REFERENCE.md +932 -0
- jarviscore/docs/CONFIGURATION.md +753 -0
- jarviscore/docs/GETTING_STARTED.md +600 -0
- jarviscore/docs/TROUBLESHOOTING.md +424 -0
- jarviscore/docs/USER_GUIDE.md +983 -0
- jarviscore/execution/__init__.py +94 -0
- jarviscore/execution/code_registry.py +298 -0
- jarviscore/execution/generator.py +268 -0
- jarviscore/execution/llm.py +430 -0
- jarviscore/execution/repair.py +283 -0
- jarviscore/execution/result_handler.py +332 -0
- jarviscore/execution/sandbox.py +555 -0
- jarviscore/execution/search.py +281 -0
- jarviscore/orchestration/__init__.py +18 -0
- jarviscore/orchestration/claimer.py +101 -0
- jarviscore/orchestration/dependency.py +143 -0
- jarviscore/orchestration/engine.py +292 -0
- jarviscore/orchestration/status.py +96 -0
- jarviscore/p2p/__init__.py +23 -0
- jarviscore/p2p/broadcaster.py +353 -0
- jarviscore/p2p/coordinator.py +364 -0
- jarviscore/p2p/keepalive.py +361 -0
- jarviscore/p2p/swim_manager.py +290 -0
- jarviscore/profiles/__init__.py +6 -0
- jarviscore/profiles/autoagent.py +264 -0
- jarviscore/profiles/customagent.py +137 -0
- jarviscore_framework-0.1.0.dist-info/METADATA +136 -0
- jarviscore_framework-0.1.0.dist-info/RECORD +55 -0
- jarviscore_framework-0.1.0.dist-info/WHEEL +5 -0
- jarviscore_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- jarviscore_framework-0.1.0.dist-info/top_level.txt +3 -0
- tests/conftest.py +44 -0
- tests/test_agent.py +165 -0
- tests/test_autoagent.py +140 -0
- tests/test_autoagent_day4.py +186 -0
- tests/test_customagent.py +248 -0
- tests/test_integration.py +293 -0
- tests/test_llm_fallback.py +185 -0
- tests/test_mesh.py +356 -0
- tests/test_p2p_integration.py +375 -0
- tests/test_remote_sandbox.py +116 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sandbox Executor - Safe execution of generated code with resource limits
|
|
3
|
+
Supports async code and provides internet search access
|
|
4
|
+
|
|
5
|
+
Modes:
|
|
6
|
+
- local: In-process execution (development/testing)
|
|
7
|
+
- remote: HTTP POST to sandbox service (production)
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import aiohttp
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import signal
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from typing import Dict, Any, Optional
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExecutionTimeout(Exception):
|
|
24
|
+
"""Raised when code execution times out."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@contextmanager
|
|
29
|
+
def time_limit(seconds: int):
|
|
30
|
+
"""Context manager for enforcing time limits (Unix only)."""
|
|
31
|
+
def signal_handler(signum, frame):
|
|
32
|
+
raise ExecutionTimeout(f"Execution exceeded {seconds} seconds")
|
|
33
|
+
|
|
34
|
+
# Only works on Unix systems
|
|
35
|
+
if hasattr(signal, 'SIGALRM'):
|
|
36
|
+
signal.signal(signal.SIGALRM, signal_handler)
|
|
37
|
+
signal.alarm(seconds)
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
signal.alarm(0)
|
|
42
|
+
else:
|
|
43
|
+
# Windows fallback - no timeout enforcement
|
|
44
|
+
logger.warning("Timeout enforcement not available on Windows")
|
|
45
|
+
yield
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SandboxExecutor:
|
|
49
|
+
"""
|
|
50
|
+
Safe code executor with resource limits and internet access.
|
|
51
|
+
|
|
52
|
+
Modes:
|
|
53
|
+
- local: In-process exec() (fast, for development)
|
|
54
|
+
- remote: HTTP POST to sandbox service (isolated, for production)
|
|
55
|
+
|
|
56
|
+
Philosophy:
|
|
57
|
+
- Execute generated code in isolated namespace
|
|
58
|
+
- Enforce timeout limits
|
|
59
|
+
- Provide search tools if available
|
|
60
|
+
- Capture all output and errors
|
|
61
|
+
- Extract 'result' variable
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
# Local mode (development)
|
|
65
|
+
executor = SandboxExecutor(mode="local")
|
|
66
|
+
|
|
67
|
+
# Remote mode (production)
|
|
68
|
+
executor = SandboxExecutor(
|
|
69
|
+
mode="remote",
|
|
70
|
+
sandbox_url="https://sandbox.mycompany.com/execute"
|
|
71
|
+
)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
timeout: int = 300,
|
|
77
|
+
search_client=None,
|
|
78
|
+
config: Optional[Dict] = None
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Initialize sandbox executor.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
timeout: Max execution time in seconds (default 300 = 5 min)
|
|
85
|
+
search_client: Optional InternetSearch for web access
|
|
86
|
+
config: Optional config dict with:
|
|
87
|
+
- sandbox_mode: "local" or "remote"
|
|
88
|
+
- sandbox_service_url: URL for remote sandbox
|
|
89
|
+
"""
|
|
90
|
+
self.timeout = timeout
|
|
91
|
+
self.search = search_client
|
|
92
|
+
self.config = config or {}
|
|
93
|
+
|
|
94
|
+
# Determine execution mode
|
|
95
|
+
self.mode = self.config.get('sandbox_mode', 'local').lower()
|
|
96
|
+
self.sandbox_url = self.config.get('sandbox_service_url')
|
|
97
|
+
|
|
98
|
+
if self.mode == 'remote' and not self.sandbox_url:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Remote sandbox mode requires sandbox_service_url. "
|
|
101
|
+
"Falling back to local mode."
|
|
102
|
+
)
|
|
103
|
+
self.mode = 'local'
|
|
104
|
+
|
|
105
|
+
logger.info(f"Sandbox initialized: mode={self.mode}, timeout={timeout}s")
|
|
106
|
+
|
|
107
|
+
async def execute(
|
|
108
|
+
self,
|
|
109
|
+
code: str,
|
|
110
|
+
timeout: Optional[int] = None,
|
|
111
|
+
context: Optional[Dict] = None
|
|
112
|
+
) -> Dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Execute Python code in sandbox (local or remote).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
code: Python code string to execute
|
|
118
|
+
timeout: Optional timeout override (seconds)
|
|
119
|
+
context: Optional context variables to inject
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
{
|
|
123
|
+
"status": "success" | "failure",
|
|
124
|
+
"output": Any, # Value of 'result' variable
|
|
125
|
+
"error": str, # Error message if failed
|
|
126
|
+
"error_type": str, # Exception type
|
|
127
|
+
"execution_time": float, # Seconds taken
|
|
128
|
+
"mode": "local" | "remote" # Execution mode used
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
result = await executor.execute("result = 2 + 2")
|
|
133
|
+
print(result['output']) # 4
|
|
134
|
+
"""
|
|
135
|
+
timeout = timeout or self.timeout
|
|
136
|
+
start_time = time.time()
|
|
137
|
+
|
|
138
|
+
logger.info(f"Executing code ({self.mode} mode, {timeout}s timeout)")
|
|
139
|
+
logger.debug(f"Code length: {len(code)} chars")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Route to appropriate execution method
|
|
143
|
+
if self.mode == 'remote':
|
|
144
|
+
result = await self._execute_remote(code, timeout, context)
|
|
145
|
+
else:
|
|
146
|
+
result = await self._execute_local(code, timeout, context)
|
|
147
|
+
|
|
148
|
+
# Add execution metadata
|
|
149
|
+
execution_time = time.time() - start_time
|
|
150
|
+
result['execution_time'] = execution_time
|
|
151
|
+
result['mode'] = self.mode
|
|
152
|
+
|
|
153
|
+
logger.info(f"Code execution successful ({execution_time:.3f}s)")
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
execution_time = time.time() - start_time
|
|
158
|
+
logger.error(f"Execution failed: {type(e).__name__}: {e}")
|
|
159
|
+
return {
|
|
160
|
+
"status": "failure",
|
|
161
|
+
"error": str(e),
|
|
162
|
+
"error_type": type(e).__name__,
|
|
163
|
+
"execution_time": execution_time,
|
|
164
|
+
"mode": self.mode
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async def _execute_local(
|
|
168
|
+
self,
|
|
169
|
+
code: str,
|
|
170
|
+
timeout: int,
|
|
171
|
+
context: Optional[Dict] = None
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Execute code locally in-process."""
|
|
174
|
+
# Create isolated namespace
|
|
175
|
+
namespace = self._create_namespace(context)
|
|
176
|
+
|
|
177
|
+
# Check if code is async
|
|
178
|
+
is_async = 'async def' in code or 'await ' in code or 'asyncio' in code
|
|
179
|
+
|
|
180
|
+
if is_async:
|
|
181
|
+
return await self._execute_async(code, namespace, timeout)
|
|
182
|
+
else:
|
|
183
|
+
return await self._execute_sync(code, namespace, timeout)
|
|
184
|
+
|
|
185
|
+
async def _execute_remote(
|
|
186
|
+
self,
|
|
187
|
+
code: str,
|
|
188
|
+
timeout: int,
|
|
189
|
+
context: Optional[Dict] = None
|
|
190
|
+
) -> Dict[str, Any]:
|
|
191
|
+
"""
|
|
192
|
+
Execute code via remote sandbox service (Azure Container Apps).
|
|
193
|
+
|
|
194
|
+
Matches integration-agent format:
|
|
195
|
+
{
|
|
196
|
+
"STEP_DATA": {
|
|
197
|
+
"id": "job_id",
|
|
198
|
+
"function_name": "generated_code",
|
|
199
|
+
"parameters": {},
|
|
200
|
+
"options": {}
|
|
201
|
+
},
|
|
202
|
+
"TASK_CODE_B64": "base64_encoded_code"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Expects response:
|
|
206
|
+
{
|
|
207
|
+
"success": true/false,
|
|
208
|
+
"result": ...,
|
|
209
|
+
"error": "...",
|
|
210
|
+
...
|
|
211
|
+
}
|
|
212
|
+
"""
|
|
213
|
+
# Wrap code to capture result (matching integration agent behavior)
|
|
214
|
+
wrapped_code = self._wrap_code_for_sandbox(code, context)
|
|
215
|
+
|
|
216
|
+
# Encode code to base64
|
|
217
|
+
code_b64 = base64.b64encode(wrapped_code.encode('utf-8')).decode('utf-8')
|
|
218
|
+
|
|
219
|
+
# Prepare payload in Azure Container Apps format
|
|
220
|
+
payload = {
|
|
221
|
+
"STEP_DATA": {
|
|
222
|
+
"id": f"jarviscore_{int(time.time())}",
|
|
223
|
+
"function_name": "generated_code",
|
|
224
|
+
"parameters": context or {},
|
|
225
|
+
"options": {"timeout": timeout}
|
|
226
|
+
},
|
|
227
|
+
"TASK_CODE_B64": code_b64
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Make HTTP request to sandbox service
|
|
232
|
+
# Use /normal endpoint for API tasks
|
|
233
|
+
endpoint_url = f"{self.sandbox_url}/normal"
|
|
234
|
+
|
|
235
|
+
async with aiohttp.ClientSession() as session:
|
|
236
|
+
async with session.post(
|
|
237
|
+
endpoint_url,
|
|
238
|
+
json=payload,
|
|
239
|
+
headers={"Content-Type": "application/json"},
|
|
240
|
+
timeout=aiohttp.ClientTimeout(total=timeout + 10) # Buffer
|
|
241
|
+
) as response:
|
|
242
|
+
if response.status != 200:
|
|
243
|
+
error_text = await response.text()
|
|
244
|
+
raise RuntimeError(
|
|
245
|
+
f"Sandbox service error ({response.status}): {error_text}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
sandbox_response = await response.json()
|
|
249
|
+
|
|
250
|
+
logger.debug(f"Remote sandbox response: {sandbox_response}")
|
|
251
|
+
|
|
252
|
+
# Extract result using robust method (matching integration agent)
|
|
253
|
+
actual_result = self._extract_sandbox_result(sandbox_response)
|
|
254
|
+
|
|
255
|
+
# Convert to our format
|
|
256
|
+
if actual_result.get('success') is False:
|
|
257
|
+
# Error case
|
|
258
|
+
return {
|
|
259
|
+
'status': 'failure',
|
|
260
|
+
'error': actual_result.get('error', 'Unknown error'),
|
|
261
|
+
'error_type': 'RemoteSandboxError'
|
|
262
|
+
}
|
|
263
|
+
else:
|
|
264
|
+
# Success case
|
|
265
|
+
return {
|
|
266
|
+
'status': 'success',
|
|
267
|
+
'output': actual_result.get('result', actual_result.get('data', actual_result.get('output')))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
except asyncio.TimeoutError:
|
|
271
|
+
logger.error(f"Remote sandbox timeout after {timeout}s")
|
|
272
|
+
raise ExecutionTimeout(f"Remote execution exceeded {timeout} seconds")
|
|
273
|
+
|
|
274
|
+
except aiohttp.ClientError as e:
|
|
275
|
+
# Network/HTTP errors
|
|
276
|
+
logger.warning(f"Remote sandbox connection error: {e}. Falling back to local execution.")
|
|
277
|
+
return await self._execute_local(code, timeout, context)
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
# Only fallback for actual execution errors, not during cleanup
|
|
281
|
+
if "object has no attribute" not in str(e):
|
|
282
|
+
logger.warning(f"Remote sandbox failed: {e}. Falling back to local execution.")
|
|
283
|
+
return await self._execute_local(code, timeout, context)
|
|
284
|
+
else:
|
|
285
|
+
# This is likely a cleanup issue, just log and don't fallback
|
|
286
|
+
logger.debug(f"Ignoring cleanup error: {e}")
|
|
287
|
+
raise
|
|
288
|
+
|
|
289
|
+
def _wrap_code_for_sandbox(self, code: str, context: Optional[Dict] = None) -> str:
|
|
290
|
+
"""
|
|
291
|
+
Wrap code to capture and print result as JSON (matches integration agent).
|
|
292
|
+
|
|
293
|
+
The sandbox executes code and captures stdout. We need to:
|
|
294
|
+
1. Execute the code
|
|
295
|
+
2. Extract the 'result' variable
|
|
296
|
+
3. Print it as JSON to stdout
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
code: Python code to wrap
|
|
300
|
+
context: Optional context variables
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Wrapped code that prints result as JSON
|
|
304
|
+
"""
|
|
305
|
+
# Add imports if needed
|
|
306
|
+
imports = []
|
|
307
|
+
if 'import json' not in code:
|
|
308
|
+
imports.append('import json')
|
|
309
|
+
if 'import sys' not in code:
|
|
310
|
+
imports.append('import sys')
|
|
311
|
+
|
|
312
|
+
imports_str = '\n'.join(imports) + '\n' if imports else ''
|
|
313
|
+
|
|
314
|
+
# Wrap code to capture and print result
|
|
315
|
+
wrapper = f'''{imports_str}{code}
|
|
316
|
+
|
|
317
|
+
# JarvisCore: Capture and print result
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
try:
|
|
320
|
+
# Check if result variable exists
|
|
321
|
+
if 'result' in locals() or 'result' in globals():
|
|
322
|
+
output = {{"success": True, "result": result}}
|
|
323
|
+
else:
|
|
324
|
+
output = {{"success": False, "error": "No 'result' variable found"}}
|
|
325
|
+
|
|
326
|
+
# Print as JSON to stdout (sandbox captures this)
|
|
327
|
+
print(json.dumps(output))
|
|
328
|
+
sys.exit(0)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
error_output = {{
|
|
331
|
+
"success": False,
|
|
332
|
+
"error": str(e),
|
|
333
|
+
"error_type": type(e).__name__
|
|
334
|
+
}}
|
|
335
|
+
print(json.dumps(error_output))
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
'''
|
|
338
|
+
return wrapper
|
|
339
|
+
|
|
340
|
+
def _extract_sandbox_result(self, sandbox_response: Any) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Extract the actual function result from sandbox response.
|
|
343
|
+
Matches integration agent's robust extraction logic.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
sandbox_response: Raw response from sandbox service
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Extracted result dict
|
|
350
|
+
"""
|
|
351
|
+
# Handle None response
|
|
352
|
+
if sandbox_response is None:
|
|
353
|
+
logger.warning("Sandbox returned None response")
|
|
354
|
+
return {
|
|
355
|
+
"success": False,
|
|
356
|
+
"error": "Sandbox returned null response",
|
|
357
|
+
"error_type": "null_response"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Handle non-dict response
|
|
361
|
+
if not isinstance(sandbox_response, dict):
|
|
362
|
+
logger.warning(f"Sandbox returned non-dict response: {type(sandbox_response)}")
|
|
363
|
+
return {
|
|
364
|
+
"success": False,
|
|
365
|
+
"error": f"Sandbox returned unexpected response type: {type(sandbox_response)}",
|
|
366
|
+
"error_type": "invalid_response_type"
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# Try to parse 'output' field if it's a JSON string
|
|
370
|
+
if 'output' in sandbox_response and isinstance(sandbox_response.get('output'), str):
|
|
371
|
+
output_str = sandbox_response['output'].strip()
|
|
372
|
+
if output_str:
|
|
373
|
+
try:
|
|
374
|
+
parsed_output = json.loads(output_str)
|
|
375
|
+
if isinstance(parsed_output, dict):
|
|
376
|
+
logger.debug("Successfully parsed result from output field")
|
|
377
|
+
return parsed_output
|
|
378
|
+
except json.JSONDecodeError as e:
|
|
379
|
+
logger.debug(f"JSON parse failed: {e}, trying line-by-line")
|
|
380
|
+
lines = output_str.strip().split('\n')
|
|
381
|
+
for line in reversed(lines):
|
|
382
|
+
line = line.strip()
|
|
383
|
+
if line.startswith('{') and line.endswith('}'):
|
|
384
|
+
try:
|
|
385
|
+
parsed_output = json.loads(line)
|
|
386
|
+
if isinstance(parsed_output, dict) and 'success' in parsed_output:
|
|
387
|
+
logger.debug("Successfully parsed result from last JSON line")
|
|
388
|
+
return parsed_output
|
|
389
|
+
except json.JSONDecodeError:
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
logger.warning("Could not parse any JSON from output")
|
|
393
|
+
return {
|
|
394
|
+
"success": sandbox_response.get('success', False),
|
|
395
|
+
"output": output_str,
|
|
396
|
+
"error": sandbox_response.get('error') or "Failed to parse output as JSON"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# If response has 'success' field but no nested result fields, return as-is
|
|
400
|
+
if 'success' in sandbox_response:
|
|
401
|
+
wrapper_fields = {'result', 'function_result', 'execution_result'}
|
|
402
|
+
if not any(field in sandbox_response for field in wrapper_fields):
|
|
403
|
+
return sandbox_response
|
|
404
|
+
|
|
405
|
+
# Try common result field names
|
|
406
|
+
result_candidates = ['result', 'function_result', 'execution_result', 'data', 'response']
|
|
407
|
+
for field in result_candidates:
|
|
408
|
+
if field in sandbox_response and sandbox_response[field] is not None:
|
|
409
|
+
candidate = sandbox_response[field]
|
|
410
|
+
if isinstance(candidate, dict):
|
|
411
|
+
return candidate
|
|
412
|
+
|
|
413
|
+
logger.debug(f"No specific result field found, returning whole response")
|
|
414
|
+
return sandbox_response
|
|
415
|
+
|
|
416
|
+
def _create_namespace(self, context: Optional[Dict] = None) -> Dict:
|
|
417
|
+
"""
|
|
418
|
+
Create isolated namespace with safe built-ins and tools.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
context: Optional context variables to inject
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Namespace dict for code execution
|
|
425
|
+
"""
|
|
426
|
+
# Get all built-ins except dangerous ones
|
|
427
|
+
safe_builtins = {}
|
|
428
|
+
for name in dir(__builtins__):
|
|
429
|
+
if name.startswith('_'):
|
|
430
|
+
continue
|
|
431
|
+
# Exclude dangerous functions
|
|
432
|
+
if name in ['eval', 'exec', 'compile', 'open', 'input', 'file']:
|
|
433
|
+
continue
|
|
434
|
+
try:
|
|
435
|
+
safe_builtins[name] = getattr(__builtins__, name)
|
|
436
|
+
except AttributeError:
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
# Ensure critical built-ins are present
|
|
440
|
+
critical_builtins = [
|
|
441
|
+
'print', '__import__', 'len', 'range', 'str', 'int', 'float',
|
|
442
|
+
'list', 'dict', 'set', 'tuple', 'bool', 'type', 'isinstance',
|
|
443
|
+
'min', 'max', 'sum', 'sorted', 'enumerate', 'zip', 'map', 'filter',
|
|
444
|
+
'Exception', 'ValueError', 'TypeError', 'KeyError', 'IndexError',
|
|
445
|
+
'NameError', 'AttributeError', 'RuntimeError', 'ZeroDivisionError'
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
for builtin in critical_builtins:
|
|
449
|
+
if builtin not in safe_builtins:
|
|
450
|
+
try:
|
|
451
|
+
safe_builtins[builtin] = eval(builtin)
|
|
452
|
+
except:
|
|
453
|
+
logger.warning(f"Could not add built-in: {builtin}")
|
|
454
|
+
|
|
455
|
+
namespace = {
|
|
456
|
+
'__builtins__': safe_builtins,
|
|
457
|
+
'result': None, # Where code should store output
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Inject search client if available
|
|
461
|
+
if self.search:
|
|
462
|
+
namespace['search'] = self.search
|
|
463
|
+
logger.debug("Injected search client into namespace")
|
|
464
|
+
|
|
465
|
+
# Inject context variables
|
|
466
|
+
if context:
|
|
467
|
+
namespace.update(context)
|
|
468
|
+
logger.debug(f"Injected {len(context)} context variables")
|
|
469
|
+
|
|
470
|
+
return namespace
|
|
471
|
+
|
|
472
|
+
async def _execute_sync(
|
|
473
|
+
self,
|
|
474
|
+
code: str,
|
|
475
|
+
namespace: Dict,
|
|
476
|
+
timeout: int
|
|
477
|
+
) -> Dict[str, Any]:
|
|
478
|
+
"""Execute synchronous code."""
|
|
479
|
+
try:
|
|
480
|
+
# Run in thread pool to enforce timeout
|
|
481
|
+
loop = asyncio.get_event_loop()
|
|
482
|
+
await asyncio.wait_for(
|
|
483
|
+
loop.run_in_executor(None, exec, code, namespace),
|
|
484
|
+
timeout=timeout
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Extract result
|
|
488
|
+
result = namespace.get('result')
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
"status": "success",
|
|
492
|
+
"output": result
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
except asyncio.TimeoutError:
|
|
496
|
+
raise ExecutionTimeout(f"Execution exceeded {timeout} seconds")
|
|
497
|
+
|
|
498
|
+
async def _execute_async(
|
|
499
|
+
self,
|
|
500
|
+
code: str,
|
|
501
|
+
namespace: Dict,
|
|
502
|
+
timeout: int
|
|
503
|
+
) -> Dict[str, Any]:
|
|
504
|
+
"""Execute asynchronous code."""
|
|
505
|
+
# Inject asyncio and search for async code
|
|
506
|
+
namespace['asyncio'] = asyncio
|
|
507
|
+
if self.search:
|
|
508
|
+
namespace['search'] = self.search
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
# Execute code to define functions
|
|
512
|
+
exec(code, namespace)
|
|
513
|
+
|
|
514
|
+
# Look for main() or run() function
|
|
515
|
+
if 'main' in namespace and callable(namespace['main']):
|
|
516
|
+
# Run main() with timeout
|
|
517
|
+
result_value = await asyncio.wait_for(
|
|
518
|
+
namespace['main'](),
|
|
519
|
+
timeout=timeout
|
|
520
|
+
)
|
|
521
|
+
elif 'run' in namespace and callable(namespace['run']):
|
|
522
|
+
result_value = await asyncio.wait_for(
|
|
523
|
+
namespace['run'](),
|
|
524
|
+
timeout=timeout
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
# Check if result was set directly
|
|
528
|
+
result_value = namespace.get('result')
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
"status": "success",
|
|
532
|
+
"output": result_value
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
except asyncio.TimeoutError:
|
|
536
|
+
raise ExecutionTimeout(f"Async execution exceeded {timeout} seconds")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def create_sandbox_executor(
|
|
540
|
+
timeout: int = 300,
|
|
541
|
+
search_client=None,
|
|
542
|
+
config: Optional[Dict] = None
|
|
543
|
+
) -> SandboxExecutor:
|
|
544
|
+
"""
|
|
545
|
+
Factory function to create sandbox executor.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
timeout: Max execution time (default 300s)
|
|
549
|
+
search_client: Optional search client for web access
|
|
550
|
+
config: Optional configuration
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
SandboxExecutor instance
|
|
554
|
+
"""
|
|
555
|
+
return SandboxExecutor(timeout, search_client, config)
|