Skip to content

subprocess_sandbox

subprocess_sandbox

Subprocess sandbox — secure process execution with environment isolation.

Classes

SandboxResult dataclass

SandboxResult(stdout: str = '', stderr: str = '', returncode: int = -1, timed_out: bool = False, killed: bool = False)

Result of a sandboxed subprocess execution.

Functions

build_safe_env

build_safe_env(passthrough: Optional[List[str]] = None, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]

Build a sanitized environment dict.

Only copies safe vars from current env, plus any in passthrough list. Extra vars are added directly.

Source code in src/openjarvis/security/subprocess_sandbox.py
def build_safe_env(
    passthrough: Optional[List[str]] = None,
    extra: Optional[Dict[str, str]] = None,
) -> Dict[str, str]:
    """Build a sanitized environment dict.

    Only copies safe vars from current env, plus any in passthrough list.
    Extra vars are added directly.
    """
    env: Dict[str, str] = {}
    allowed = _SAFE_ENV_VARS | frozenset(passthrough or [])
    for key in allowed:
        val = os.environ.get(key)
        if val is not None:
            env[key] = val
    if extra:
        env.update(extra)
    return env

kill_process_tree

kill_process_tree(pid: int) -> None

Kill a process and all its children (best effort).

Source code in src/openjarvis/security/subprocess_sandbox.py
def kill_process_tree(pid: int) -> None:
    """Kill a process and all its children (best effort)."""
    try:
        os.killpg(os.getpgid(pid), signal.SIGTERM)
    except (OSError, ProcessLookupError) as exc:
        logger.debug("Failed to terminate process %d: %s", pid, exc)
    try:
        os.kill(pid, signal.SIGKILL)
    except (OSError, ProcessLookupError) as exc:
        logger.debug("Failed to kill process %d: %s", pid, exc)

run_sandboxed

run_sandboxed(command: str, *, timeout: float = 30.0, working_dir: Optional[str] = None, env_passthrough: Optional[List[str]] = None, env_extra: Optional[Dict[str, str]] = None, max_output_bytes: int = 102400) -> SandboxResult

Execute a command in a sandboxed subprocess.

Features: - Clean environment (only safe vars passed through) - Timeout enforcement with process tree kill - Output truncation - New process group for clean cleanup

Source code in src/openjarvis/security/subprocess_sandbox.py
def run_sandboxed(
    command: str,
    *,
    timeout: float = 30.0,
    working_dir: Optional[str] = None,
    env_passthrough: Optional[List[str]] = None,
    env_extra: Optional[Dict[str, str]] = None,
    max_output_bytes: int = 102_400,
) -> SandboxResult:
    """Execute a command in a sandboxed subprocess.

    Features:
    - Clean environment (only safe vars passed through)
    - Timeout enforcement with process tree kill
    - Output truncation
    - New process group for clean cleanup
    """
    env = build_safe_env(passthrough=env_passthrough, extra=env_extra)
    cwd = working_dir if working_dir and os.path.isdir(working_dir) else None

    result = SandboxResult()
    try:
        proc = subprocess.Popen(
            command,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            env=env,
            cwd=cwd,
            preexec_fn=os.setsid,  # New process group
        )
        try:
            stdout, stderr = proc.communicate(timeout=timeout)
            result.stdout = stdout[:max_output_bytes] if stdout else ""
            result.stderr = stderr[:max_output_bytes] if stderr else ""
            result.returncode = proc.returncode
        except subprocess.TimeoutExpired:
            kill_process_tree(proc.pid)
            proc.wait(timeout=5)
            result.timed_out = True
            result.killed = True
            result.returncode = -1
            result.stdout = "(timed out)"
            result.stderr = ""
    except OSError as exc:
        result.stderr = f"Execution error: {exc}"
        result.returncode = -1

    return result