214 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			214 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Python
		
	
	
	
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() |