426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
362 lines
11 KiB
Python
362 lines
11 KiB
Python
"""
|
|
Python Code Executor for MINT Benchmark
|
|
|
|
Provides safe, sandboxed execution of Python code for tool use in MINT tasks.
|
|
Supports both Docker-based sandboxing and local execution (with restrictions).
|
|
"""
|
|
|
|
import asyncio
|
|
import builtins
|
|
import logging
|
|
import re
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ExecutionResult:
|
|
"""Result of code execution."""
|
|
|
|
def __init__(
|
|
self,
|
|
output: str,
|
|
success: bool,
|
|
error: Optional[str] = None,
|
|
execution_time_ms: float = 0.0,
|
|
) -> None:
|
|
self.output = output
|
|
self.success = success
|
|
self.error = error
|
|
self.execution_time_ms = execution_time_ms
|
|
|
|
|
|
class PythonExecutor:
|
|
"""Execute Python code in a sandboxed environment."""
|
|
|
|
# Dangerous patterns that should be blocked in local execution
|
|
DANGEROUS_PATTERNS = [
|
|
r"\bimport\s+os\b",
|
|
r"\bimport\s+subprocess\b",
|
|
r"\bimport\s+shutil\b",
|
|
r"\bimport\s+sys\b.*\bexit\b",
|
|
r"\b__import__\b",
|
|
r"\beval\s*\(",
|
|
r"\bexec\s*\(",
|
|
r"\bopen\s*\([^)]*['\"]w['\"]", # writing files
|
|
r"\bos\.system\b",
|
|
r"\bos\.popen\b",
|
|
r"\bsubprocess\.",
|
|
r"\bshutil\.rmtree\b",
|
|
]
|
|
|
|
# Safe imports allowed in sandboxed mode
|
|
SAFE_IMPORTS = [
|
|
"math",
|
|
"statistics",
|
|
"itertools",
|
|
"functools",
|
|
"collections",
|
|
"heapq",
|
|
"bisect",
|
|
"array",
|
|
"decimal",
|
|
"fractions",
|
|
"random",
|
|
"string",
|
|
"re",
|
|
"json",
|
|
"datetime",
|
|
"time",
|
|
"copy",
|
|
"operator",
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
timeout: int = 30,
|
|
use_docker: bool = True,
|
|
docker_image: str = "python:3.11-slim",
|
|
memory_limit: str = "512m",
|
|
cpu_limit: str = "1",
|
|
) -> None:
|
|
"""
|
|
Initialize the Python executor.
|
|
|
|
Args:
|
|
timeout: Maximum execution time in seconds
|
|
use_docker: Whether to use Docker for sandboxing
|
|
docker_image: Docker image to use
|
|
memory_limit: Memory limit for Docker container
|
|
cpu_limit: CPU limit for Docker container
|
|
"""
|
|
self.timeout = timeout
|
|
self.use_docker = use_docker
|
|
self.docker_image = docker_image
|
|
self.memory_limit = memory_limit
|
|
self.cpu_limit = cpu_limit
|
|
self._docker_available: Optional[bool] = None
|
|
|
|
async def execute(self, code: str) -> ExecutionResult:
|
|
"""
|
|
Execute Python code and return output.
|
|
|
|
Args:
|
|
code: Python code to execute
|
|
|
|
Returns:
|
|
ExecutionResult with output, success status, and timing
|
|
"""
|
|
import time
|
|
|
|
start_time = time.time()
|
|
|
|
# Clean up the code
|
|
code = self._clean_code(code)
|
|
|
|
if not code.strip():
|
|
return ExecutionResult(
|
|
output="",
|
|
success=False,
|
|
error="Empty code provided",
|
|
execution_time_ms=0.0,
|
|
)
|
|
|
|
try:
|
|
if self.use_docker and await self._check_docker():
|
|
result = await self._execute_docker(code)
|
|
else:
|
|
result = await self._execute_local(code)
|
|
|
|
result.execution_time_ms = (time.time() - start_time) * 1000
|
|
return result
|
|
|
|
except asyncio.TimeoutError:
|
|
return ExecutionResult(
|
|
output="",
|
|
success=False,
|
|
error=f"Execution timed out after {self.timeout} seconds",
|
|
execution_time_ms=(time.time() - start_time) * 1000,
|
|
)
|
|
except Exception as e:
|
|
return ExecutionResult(
|
|
output="",
|
|
success=False,
|
|
error=str(e),
|
|
execution_time_ms=(time.time() - start_time) * 1000,
|
|
)
|
|
|
|
def _clean_code(self, code: str) -> str:
|
|
"""Clean and prepare code for execution."""
|
|
# Remove markdown code blocks if present
|
|
code = re.sub(r"```python\s*", "", code)
|
|
code = re.sub(r"```\s*", "", code)
|
|
|
|
# Remove leading/trailing whitespace
|
|
code = code.strip()
|
|
|
|
return code
|
|
|
|
async def _check_docker(self) -> bool:
|
|
"""Check if Docker is available."""
|
|
if self._docker_available is not None:
|
|
return self._docker_available
|
|
|
|
try:
|
|
proc = await asyncio.create_subprocess_exec(
|
|
"docker", "version",
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
await asyncio.wait_for(proc.communicate(), timeout=5)
|
|
self._docker_available = proc.returncode == 0
|
|
except Exception:
|
|
self._docker_available = False
|
|
|
|
if not self._docker_available:
|
|
logger.warning(
|
|
"[PythonExecutor] Docker not available, falling back to local execution"
|
|
)
|
|
|
|
return self._docker_available
|
|
|
|
async def _execute_docker(self, code: str) -> ExecutionResult:
|
|
"""Execute code in Docker container."""
|
|
# Create a temporary file with the code
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".py", delete=False
|
|
) as f:
|
|
# Add output capture wrapper
|
|
wrapped_code = f'''
|
|
import sys
|
|
from io import StringIO
|
|
|
|
# Capture stdout
|
|
old_stdout = sys.stdout
|
|
sys.stdout = StringIO()
|
|
|
|
try:
|
|
{self._indent_code(code, 4)}
|
|
except Exception as e:
|
|
print(f"Error: {{type(e).__name__}}: {{e}}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
output = sys.stdout.getvalue()
|
|
sys.stdout = old_stdout
|
|
print(output, end="")
|
|
'''
|
|
f.write(wrapped_code)
|
|
temp_path = f.name
|
|
|
|
try:
|
|
cmd = [
|
|
"docker", "run", "--rm",
|
|
f"--memory={self.memory_limit}",
|
|
f"--cpus={self.cpu_limit}",
|
|
"--network=none", # No network access
|
|
"-v", f"{temp_path}:/code.py:ro",
|
|
self.docker_image,
|
|
"python", "/code.py",
|
|
]
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
|
|
stdout, stderr = await asyncio.wait_for(
|
|
proc.communicate(),
|
|
timeout=self.timeout,
|
|
)
|
|
|
|
output = stdout.decode().strip()
|
|
error_output = stderr.decode().strip()
|
|
|
|
if proc.returncode == 0:
|
|
return ExecutionResult(output=output, success=True)
|
|
else:
|
|
return ExecutionResult(
|
|
output=output,
|
|
success=False,
|
|
error=error_output or f"Exit code: {proc.returncode}",
|
|
)
|
|
|
|
finally:
|
|
# Clean up temp file
|
|
Path(temp_path).unlink(missing_ok=True)
|
|
|
|
async def _execute_local(self, code: str) -> ExecutionResult:
|
|
"""Execute code locally with safety restrictions."""
|
|
# Check for dangerous patterns
|
|
if not self._is_safe_code(code):
|
|
return ExecutionResult(
|
|
output="",
|
|
success=False,
|
|
error="Code contains potentially dangerous operations",
|
|
)
|
|
|
|
# Create a restricted execution environment
|
|
safe_globals = {"__builtins__": {}}
|
|
|
|
# Add safe builtins
|
|
safe_builtin_names = [
|
|
"abs", "all", "any", "bin", "bool", "chr", "dict", "divmod",
|
|
"enumerate", "filter", "float", "format", "frozenset", "hash",
|
|
"hex", "int", "isinstance", "issubclass", "iter", "len", "list",
|
|
"map", "max", "min", "next", "oct", "ord", "pow", "print",
|
|
"range", "repr", "reversed", "round", "set", "slice", "sorted",
|
|
"str", "sum", "tuple", "type", "zip",
|
|
]
|
|
for name in safe_builtin_names:
|
|
builtin_func = getattr(builtins, name, None)
|
|
if builtin_func is not None:
|
|
safe_globals["__builtins__"][name] = builtin_func
|
|
|
|
# Add safe modules (pre-loaded, available as globals)
|
|
for module_name in self.SAFE_IMPORTS:
|
|
try:
|
|
safe_globals[module_name] = __import__(module_name)
|
|
except ImportError:
|
|
pass
|
|
|
|
# Capture output
|
|
from io import StringIO
|
|
import sys
|
|
|
|
old_stdout = sys.stdout
|
|
old_stderr = sys.stderr
|
|
sys.stdout = StringIO()
|
|
sys.stderr = StringIO()
|
|
|
|
try:
|
|
# Execute with timeout using subprocess for isolation
|
|
exec(compile(code, "<string>", "exec"), safe_globals)
|
|
output = sys.stdout.getvalue().strip()
|
|
error = sys.stderr.getvalue().strip()
|
|
|
|
return ExecutionResult(
|
|
output=output,
|
|
success=True,
|
|
error=error if error else None,
|
|
)
|
|
|
|
except Exception as e:
|
|
return ExecutionResult(
|
|
output=sys.stdout.getvalue().strip(),
|
|
success=False,
|
|
error=f"{type(e).__name__}: {e}",
|
|
)
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
sys.stderr = old_stderr
|
|
|
|
def _is_safe_code(self, code: str) -> bool:
|
|
"""Check if code is safe to execute locally."""
|
|
for pattern in self.DANGEROUS_PATTERNS:
|
|
if re.search(pattern, code, re.IGNORECASE):
|
|
logger.warning(f"[PythonExecutor] Blocked dangerous pattern: {pattern}")
|
|
return False
|
|
return True
|
|
|
|
def _indent_code(self, code: str, spaces: int) -> str:
|
|
"""Indent code by specified number of spaces."""
|
|
indent = " " * spaces
|
|
return "\n".join(indent + line for line in code.split("\n"))
|
|
|
|
|
|
class MockExecutor:
|
|
"""Mock executor for smoke tests only.
|
|
|
|
Must be opted into via ``MINTConfig.use_mock_executor=True`` (or the
|
|
``--mock`` CLI flag). The default-"42" fallback that used to live here
|
|
has been removed: when no caller-supplied response matches and no print
|
|
statement is detectable, the executor returns an empty failure result so
|
|
metrics surface that nothing was actually executed.
|
|
"""
|
|
|
|
def __init__(self, responses: Optional[dict[str, str]] = None) -> None:
|
|
self.responses = responses or {}
|
|
self.executions: list[str] = []
|
|
|
|
async def execute(self, code: str) -> ExecutionResult:
|
|
self.executions.append(code)
|
|
|
|
for pattern, response in self.responses.items():
|
|
if pattern in code:
|
|
return ExecutionResult(output=response, success=True)
|
|
|
|
prints = re.findall(r'print\s*\(\s*["\']?([^"\')\n]+)["\']?\s*\)', code)
|
|
if prints:
|
|
return ExecutionResult(output=prints[-1], success=True)
|
|
|
|
return ExecutionResult(
|
|
output="",
|
|
success=False,
|
|
error=(
|
|
"MockExecutor has no matching response. Provide one via the "
|
|
"constructor or run with a real PythonExecutor."
|
|
),
|
|
)
|