mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-06 07:51:11 +00:00
* Add sandboxed Python execution and home AutoBash plugin * Harden read-only shell commands and filesystem sandboxes * Group file sandbox backends in one package * Fix macOS file sandbox isolation and runtime access * Refine file sandbox limits and executor checks * Harden AutoBash policy and macOS sandbox supervision * fix(home): require ReMe release with file sandbox * refactor(home): simplify sandbox calls and redundant tests * fix(execution): bound output and clean up cancelled sandbox startup * fix(tests): mock Seatbelt platform requirements on Linux * fix(home): block Git help helpers and harden sandbox cleanup * Fix sandbox runtime links and subprocess cleanup * ci: cover Home plugin tests * test: wait for pipeline descendants to exit after cancellation * fix: align Home dependencies and preserve cleanup cancellation Require ReMe 0.4.2.1 and verify isolated Home wheel installation in package CI. Share cancellation-safe cleanup with thread calls, retain AnyIO cancellation semantics, and record Bubblewrap mount targets during argument construction.
75 lines
2.9 KiB
Python
75 lines
2.9 KiB
Python
"""Execute Python code and return printed stdout."""
|
|
|
|
import sys
|
|
from typing import Any
|
|
|
|
from ...utils.local_backend import LocalProcessBackend, ProcessTimeout
|
|
from ..base_step import BaseStep
|
|
from ...components import R
|
|
from .file_sandbox import file_sandbox_for_step
|
|
|
|
DEFAULT_TIMEOUT = 60.0
|
|
|
|
|
|
@R.register("python_execute_step")
|
|
class PythonExecuteStep(BaseStep):
|
|
"""Run Python code and return stdout, limiting each output stream to 1 MiB."""
|
|
|
|
async def execute(self):
|
|
assert self.context is not None
|
|
|
|
code = self.context.get("code", "")
|
|
timeout, timeout_error = self._parse_timeout(
|
|
self.context.get("python_timeout", self.context.get("timeout", DEFAULT_TIMEOUT)),
|
|
)
|
|
if not isinstance(code, str) or not code.strip():
|
|
self.context.response.success = False
|
|
self.context.response.answer = "code is required"
|
|
return self.context.response
|
|
if timeout_error:
|
|
self.context.response.success = False
|
|
self.context.response.answer = timeout_error
|
|
return self.context.response
|
|
|
|
timed_out = False
|
|
try:
|
|
backend = file_sandbox_for_step(self)
|
|
result = await (backend or LocalProcessBackend()).exec_shell(
|
|
[sys.executable, "-c", code],
|
|
cwd=str(self.workspace_path),
|
|
timeout=timeout,
|
|
**({"raise_errors": True} if backend is None else {}),
|
|
)
|
|
except ProcessTimeout as exc:
|
|
result, timed_out = exc.result, True
|
|
except (TypeError, ValueError, OSError, RuntimeError) as exc:
|
|
self.context.response.success = False
|
|
self.context.response.answer = str(exc)
|
|
return self.context.response
|
|
timed_out = timed_out or (result.exit_code == -1 and result.stderr == b"timed out")
|
|
stdout = result.stdout.decode(errors="replace")
|
|
stderr = result.stderr.decode(errors="replace")
|
|
if timed_out:
|
|
self.context.response.success = False
|
|
self.context.response.answer = f"Python execution timed out after {timeout:g}s"
|
|
else:
|
|
self.context.response.success = result.exit_code == 0
|
|
self.context.response.answer = stdout if stdout or result.exit_code == 0 else stderr
|
|
self.context.response.metadata.update(
|
|
{
|
|
"returncode": result.exit_code,
|
|
"stderr": stderr,
|
|
"python_timeout": timeout,
|
|
},
|
|
)
|
|
return self.context.response
|
|
|
|
@staticmethod
|
|
def _parse_timeout(raw: Any) -> tuple[float, str]:
|
|
try:
|
|
timeout = float(raw)
|
|
except (TypeError, ValueError):
|
|
return DEFAULT_TIMEOUT, "python_timeout must be a positive number"
|
|
if timeout <= 0:
|
|
return DEFAULT_TIMEOUT, "python_timeout must be a positive number"
|
|
return timeout, ""
|