feat: py sandbox for workflow
* chore: update Dockerfile and sandbox.py * feat: py sandbox for workflow * feat: py sandbox for workflow See merge request: !885
This commit is contained in:
214
backend/infra/impl/coderunner/script/sandbox.py
Normal file
214
backend/infra/impl/coderunner/script/sandbox.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import os
|
||||
import dataclasses
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Dict, Literal
|
||||
|
||||
Status = Literal["success", "error"]
|
||||
|
||||
PKG_NAME = "jsr:@langchain/pyodide-sandbox@0.0.4"
|
||||
|
||||
@dataclasses.dataclass(kw_only=True)
|
||||
class Output:
|
||||
result: Dict = None
|
||||
stdout: str | None = None
|
||||
stderr: str | None = None
|
||||
status: Status
|
||||
execution_time: float
|
||||
|
||||
def build_permission_flag(
|
||||
flag: str,
|
||||
*,
|
||||
value: bool | list[str],
|
||||
) -> str | None:
|
||||
if value is True:
|
||||
return flag
|
||||
if isinstance(value, list) and value:
|
||||
return f"{flag}={','.join(value)}"
|
||||
return None
|
||||
|
||||
|
||||
class Sandbox:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
allow_env: list[str] | bool = False,
|
||||
allow_read: list[str] | bool = False,
|
||||
allow_write: list[str] | bool = False,
|
||||
allow_net: list[str] | bool = False,
|
||||
allow_run: list[str] | bool = False,
|
||||
allow_ffi: list[str] | bool = False,
|
||||
node_modules_dir: str = "auto",
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.permissions = []
|
||||
|
||||
perm_defs = [
|
||||
("--allow-env", allow_env, None),
|
||||
("--allow-read", allow_read, ["node_modules"]),
|
||||
("--allow-write", allow_write, ["node_modules"]),
|
||||
("--allow-net", allow_net, None),
|
||||
("--allow-run", allow_run, None),
|
||||
("--allow-ffi", allow_ffi, None),
|
||||
]
|
||||
|
||||
self.permissions = []
|
||||
for flag, value, defaults in perm_defs:
|
||||
perm = build_permission_flag(flag, value=value)
|
||||
if perm is None and defaults is not None:
|
||||
default_value = ",".join(defaults)
|
||||
perm = f"{flag}={default_value}"
|
||||
if perm:
|
||||
self.permissions.append(perm)
|
||||
|
||||
self.permissions.append(f"--node-modules-dir={node_modules_dir}")
|
||||
|
||||
def _build_command(
|
||||
self,
|
||||
code: str,
|
||||
*,
|
||||
session_bytes: bytes | None = None,
|
||||
session_metadata: dict | None = None,
|
||||
memory_limit_mb: int | None = 100,
|
||||
**kwargs
|
||||
) -> list[str]:
|
||||
cmd = [
|
||||
"deno",
|
||||
"run",
|
||||
]
|
||||
|
||||
cmd.extend(self.permissions)
|
||||
|
||||
v8_flags = ["--experimental-wasm-stack-switching"]
|
||||
|
||||
if memory_limit_mb is not None and memory_limit_mb > 0:
|
||||
v8_flags.append(f"--max-old-space-size={memory_limit_mb}")
|
||||
|
||||
cmd.append(f"--v8-flags={','.join(v8_flags)}")
|
||||
|
||||
cmd.append(PKG_NAME)
|
||||
|
||||
cmd.extend(["--code", code])
|
||||
|
||||
if session_bytes:
|
||||
bytes_array = list(session_bytes)
|
||||
cmd.extend(["--session-bytes", json.dumps(bytes_array)])
|
||||
|
||||
if session_metadata:
|
||||
cmd.extend(["--session-metadata", json.dumps(session_metadata)])
|
||||
|
||||
return cmd
|
||||
|
||||
def execute(
|
||||
self,
|
||||
code: str,
|
||||
*,
|
||||
session_bytes: bytes | None = None,
|
||||
session_metadata: dict | None = None,
|
||||
timeout_seconds: float | None = None,
|
||||
memory_limit_mb: int | None = None,
|
||||
**kwargs
|
||||
) -> Output:
|
||||
start_time = time.time()
|
||||
stdout = ""
|
||||
result = None
|
||||
stderr: str
|
||||
status: Literal["success", "error"]
|
||||
cmd = self._build_command(
|
||||
code,
|
||||
session_bytes=session_bytes,
|
||||
session_metadata=session_metadata,
|
||||
memory_limit_mb=memory_limit_mb,
|
||||
)
|
||||
|
||||
try:
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=False,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
|
||||
stdout_bytes = process.stdout
|
||||
stderr_bytes = process.stderr
|
||||
|
||||
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
if stdout:
|
||||
full_result = json.loads(stdout)
|
||||
stdout = full_result.get("stdout", None)
|
||||
stderr = full_result.get("stderr", None)
|
||||
result = full_result.get("result", None)
|
||||
status = "success" if full_result.get("success", False) else "error"
|
||||
else:
|
||||
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
||||
status = "error"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
status = "error"
|
||||
stderr = f"Execution timed out after {timeout_seconds} seconds"
|
||||
|
||||
end_time = time.time()
|
||||
|
||||
return Output(
|
||||
status=status,
|
||||
execution_time=end_time - start_time,
|
||||
stdout=stdout or None,
|
||||
stderr=stderr or None,
|
||||
result=result,
|
||||
)
|
||||
|
||||
|
||||
prefix = """\
|
||||
import json
|
||||
import sys
|
||||
import asyncio
|
||||
class Args:
|
||||
def __init__(self, params):
|
||||
self.params = params
|
||||
|
||||
class Output(dict):
|
||||
pass
|
||||
|
||||
args = {}
|
||||
|
||||
"""
|
||||
|
||||
suffix = """\
|
||||
|
||||
result = None
|
||||
try:
|
||||
result = asyncio.run(main(Args(args)))
|
||||
except Exception as e:
|
||||
print(f"{type(e).__name__}: {str(e)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
result
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
w = os.fdopen(3, "wb", )
|
||||
r = os.fdopen(4, "rb", )
|
||||
|
||||
try:
|
||||
req = json.load(r)
|
||||
user_code, params, config = req["code"], req["params"], req["config"] or {}
|
||||
sandbox = Sandbox(**config)
|
||||
|
||||
if params is not None:
|
||||
code = prefix + f'args={json.dumps(params)}\n' + user_code + suffix
|
||||
else:
|
||||
code = prefix + user_code + suffix
|
||||
|
||||
resp = sandbox.execute(code, **config)
|
||||
result = json.dumps(dataclasses.asdict(resp), ensure_ascii=False)
|
||||
w.write(str.encode(result))
|
||||
w.flush()
|
||||
w.close()
|
||||
except Exception as e:
|
||||
print("sandbox exec error", e)
|
||||
w.write(str.encode(json.dumps({"sandbox_error": str(e)})))
|
||||
w.flush()
|
||||
w.close()
|
||||
Reference in New Issue
Block a user