Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

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