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() |