4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
218 lines
6.9 KiB
Python
218 lines
6.9 KiB
Python
"""Python sandbox runner with timeout and restricted access."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import textwrap
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from config.constants import OPENSRE_TMP_DIR, ensure_opensre_tmp_dir
|
|
|
|
DEFAULT_TIMEOUT: int = 30
|
|
MAX_TIMEOUT: int = 60
|
|
_SANDBOX_TMP_ROOT = os.path.realpath(os.fspath(OPENSRE_TMP_DIR))
|
|
_BASE_ENV_KEYS = (
|
|
"HOME",
|
|
"LANG",
|
|
"LC_ALL",
|
|
"PATH",
|
|
"PYTHONPATH",
|
|
"REQUESTS_CA_BUNDLE",
|
|
"SSL_CERT_FILE",
|
|
"TMPDIR",
|
|
)
|
|
|
|
# Preamble injected before user code when network access is disabled.
|
|
_NETWORK_BLOCK_PREAMBLE = textwrap.dedent("""\
|
|
import socket as _socket_module
|
|
|
|
class _BlockedSocket:
|
|
def __init__(self, *args, **kwargs):
|
|
raise PermissionError("Network access is not permitted in sandbox mode")
|
|
|
|
_socket_module.socket = _BlockedSocket
|
|
|
|
def _blocked_create_connection(*args, **kwargs):
|
|
raise PermissionError("Network access is not permitted in sandbox mode")
|
|
|
|
def _blocked_getaddrinfo(*args, **kwargs):
|
|
raise PermissionError("Network access is not permitted in sandbox mode")
|
|
|
|
_socket_module.create_connection = _blocked_create_connection
|
|
_socket_module.getaddrinfo = _blocked_getaddrinfo
|
|
""")
|
|
|
|
# Preamble always injected before user code: restricts filesystem writes and subprocesses.
|
|
_SANDBOX_PREAMBLE = textwrap.dedent(f"""\
|
|
import builtins as _builtins_module
|
|
import os as _os_module
|
|
|
|
_ALLOWED_WRITE_ROOTS = ({_SANDBOX_TMP_ROOT!r},)
|
|
|
|
_original_open = _builtins_module.open
|
|
|
|
def _restricted_open(file, mode="r", *args, **kwargs):
|
|
if isinstance(file, (str, bytes)) or hasattr(file, "__fspath__"):
|
|
mode_str = str(mode)
|
|
if any(c in mode_str for c in ("w", "a", "x")):
|
|
abs_path = _os_module.path.realpath(_os_module.fspath(file))
|
|
if not any(
|
|
abs_path == root or abs_path.startswith(root + _os_module.sep)
|
|
for root in _ALLOWED_WRITE_ROOTS
|
|
):
|
|
raise PermissionError(
|
|
f"Write access denied outside the OpenSRE temp directory: {{file}}"
|
|
)
|
|
return _original_open(file, mode, *args, **kwargs)
|
|
|
|
_builtins_module.open = _restricted_open
|
|
|
|
import subprocess as _subprocess_module
|
|
import os as _os_shell_module
|
|
|
|
def _blocked_subprocess(*args, **kwargs):
|
|
raise PermissionError("Subprocess execution is not permitted in sandbox mode")
|
|
|
|
_subprocess_module.Popen = _blocked_subprocess
|
|
_subprocess_module.call = _blocked_subprocess
|
|
_subprocess_module.check_call = _blocked_subprocess
|
|
_subprocess_module.check_output = _blocked_subprocess
|
|
_subprocess_module.run = _blocked_subprocess
|
|
|
|
_os_shell_module.system = _blocked_subprocess
|
|
_os_shell_module.popen = _blocked_subprocess
|
|
|
|
""")
|
|
|
|
|
|
@dataclass
|
|
class SandboxResult:
|
|
"""Result of a sandboxed Python execution."""
|
|
|
|
code: str
|
|
inputs: dict[str, Any]
|
|
stdout: str
|
|
stderr: str
|
|
exit_code: int
|
|
timed_out: bool
|
|
error: str | None = None
|
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
|
|
@property
|
|
def success(self) -> bool:
|
|
return self.exit_code == 0 and not self.timed_out
|
|
|
|
|
|
def run_python_sandbox(
|
|
code: str,
|
|
inputs: dict[str, Any] | None = None,
|
|
timeout: int = DEFAULT_TIMEOUT,
|
|
env: dict[str, str] | None = None,
|
|
allow_network: bool = False,
|
|
) -> SandboxResult:
|
|
"""Run Python code in a sandboxed subprocess with timeout and access restrictions.
|
|
|
|
Network access is blocked by replacing ``socket.socket`` and related helpers
|
|
with a class that raises ``PermissionError``. Filesystem writes are restricted
|
|
to the OpenSRE temp directory, so any attempt to open a file outside that
|
|
directory for writing raises ``PermissionError``. Execution is capped at
|
|
*timeout* seconds.
|
|
|
|
Args:
|
|
code: Python source code to execute.
|
|
inputs: Optional mapping injected into the script's global scope as the
|
|
``inputs`` variable.
|
|
timeout: Maximum wall-clock time in seconds. Capped at
|
|
:data:`MAX_TIMEOUT`.
|
|
env: Optional approved environment variables to expose to the subprocess.
|
|
allow_network: If True, do not inject the network-blocking preamble.
|
|
|
|
Returns:
|
|
:class:`SandboxResult` carrying captured stdout/stderr, exit code, and
|
|
timeout/error metadata.
|
|
"""
|
|
effective_timeout = min(max(1, timeout), MAX_TIMEOUT)
|
|
effective_inputs: dict[str, Any] = inputs or {}
|
|
|
|
inputs_injection = ""
|
|
if effective_inputs:
|
|
inputs_json = json.dumps(effective_inputs)
|
|
inputs_injection = (
|
|
f"import json as _json_module; inputs = _json_module.loads({inputs_json!r})\n"
|
|
)
|
|
|
|
network_preamble = "" if allow_network else _NETWORK_BLOCK_PREAMBLE
|
|
full_code = network_preamble + _SANDBOX_PREAMBLE + inputs_injection + code
|
|
|
|
tmp_path: str | None = None
|
|
try:
|
|
ensure_opensre_tmp_dir()
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
suffix=".py",
|
|
delete=False,
|
|
dir=OPENSRE_TMP_DIR,
|
|
) as tmp:
|
|
tmp.write(full_code)
|
|
tmp_path = tmp.name
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, tmp_path],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=effective_timeout,
|
|
env=_sandbox_env(env),
|
|
)
|
|
return SandboxResult(
|
|
code=code,
|
|
inputs=effective_inputs,
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
timed_out=False,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return SandboxResult(
|
|
code=code,
|
|
inputs=effective_inputs,
|
|
stdout="",
|
|
stderr="",
|
|
exit_code=-1,
|
|
timed_out=True,
|
|
error=f"Execution timed out after {effective_timeout} seconds",
|
|
)
|
|
except Exception as exc:
|
|
return SandboxResult(
|
|
code=code,
|
|
inputs=effective_inputs,
|
|
stdout="",
|
|
stderr="",
|
|
exit_code=-1,
|
|
timed_out=False,
|
|
error=f"{type(exc).__name__}: {exc}",
|
|
)
|
|
finally:
|
|
if tmp_path is not None:
|
|
with contextlib.suppress(OSError):
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
def _sandbox_env(extra_env: dict[str, str] | None) -> dict[str, str]:
|
|
"""Build a narrow subprocess environment plus explicitly approved values."""
|
|
sandbox_env: dict[str, str] = {}
|
|
for key in _BASE_ENV_KEYS:
|
|
value = os.environ.get(key)
|
|
if value:
|
|
sandbox_env[key] = value
|
|
if extra_env:
|
|
sandbox_env.update({key: str(value) for key, value in extra_env.items() if value})
|
|
return sandbox_env
|