Skip to content

sandbox

sandbox

Lightweight process sandbox using bubblewrap (bwrap).

Provides process-level isolation for agent execution with near-zero overhead. Uses the same technology Claude Code uses. Works on WSL2.

The sandbox provides: - Workspace bind-mount (read-write to agent's work directory) - Shared directory bind-mounts (selective read/write) - MCP socket forwarding (Unix socket bind-mount from pool) - Optional network isolation - Optional resource caps (memory, CPU, PID limits)

Resource budget: sandbox overhead is measured in kilobytes, not megabytes. A bwrap subprocess starts in ~4ms.

Classes

SandboxConfig

Bases: BaseModel

Configuration for a bwrap sandbox instance.

Defines the isolation boundaries for an agent execution subprocess. The conductor creates a SandboxConfig per agent based on their technique declarations and workspace assignment.

SandboxWrapper

SandboxWrapper(config)

Builds and manages bwrap sandbox commands.

Given a SandboxConfig, produces the bwrap command line that sets up the isolation boundaries. The conductor uses this to wrap agent subprocess execution.

Usage::

config = SandboxConfig(workspace="/tmp/agent-ws")
wrapper = SandboxWrapper(config)
cmd = wrapper.build_command(["python", "agent_script.py"])
# cmd is ["bwrap", "--bind", "/tmp/agent-ws", "/workspace", ...]
Source code in src/marianne/execution/sandbox.py
def __init__(self, config: SandboxConfig) -> None:
    self.config = config
Functions
build_command
build_command(inner_command)

Build the bwrap command wrapping the given inner command.

Parameters:

Name Type Description Default
inner_command list[str]

The command to execute inside the sandbox.

required

Returns:

Type Description
list[str]

Full bwrap command line as a list of strings.

Source code in src/marianne/execution/sandbox.py
def build_command(self, inner_command: list[str]) -> list[str]:
    """Build the bwrap command wrapping the given inner command.

    Args:
        inner_command: The command to execute inside the sandbox.

    Returns:
        Full bwrap command line as a list of strings.
    """
    cmd = ["bwrap"]

    # Workspace bind-mount (read-write)
    cmd.extend(["--bind", self.config.workspace, "/workspace"])

    # Standard system directories (read-only)
    for sys_dir in ["/usr", "/lib", "/lib64", "/bin", "/sbin", "/etc"]:
        cmd.extend(["--ro-bind-try", sys_dir, sys_dir])

    # Proc and dev for basic functionality
    cmd.extend(["--proc", "/proc"])
    cmd.extend(["--dev", "/dev"])

    # Temporary directory
    cmd.extend(["--tmpfs", "/tmp"])

    # Additional bind mounts (MCP sockets, shared dirs)
    for mount_path in self.config.bind_mounts:
        cmd.extend(["--bind", mount_path, mount_path])

    # Read-only mounts
    for ro_path in self.config.read_only_mounts:
        cmd.extend(["--ro-bind", ro_path, ro_path])

    # Network isolation
    if self.config.network_isolated:
        cmd.append("--unshare-net")

    # PID namespace isolation
    cmd.append("--unshare-pid")

    # Set working directory
    cmd.extend(["--chdir", "/workspace"])

    # Environment variables
    for key, value in self.config.env_vars.items():
        cmd.extend(["--setenv", key, value])

    # Die with parent — sandbox dies if conductor dies
    cmd.append("--die-with-parent")

    # The inner command
    cmd.extend(inner_command)

    _logger.debug(
        "sandbox_command_built",
        workspace=self.config.workspace,
        network_isolated=self.config.network_isolated,
        bind_mount_count=len(self.config.bind_mounts),
        inner_command=inner_command[0] if inner_command else "<empty>",
    )

    return cmd
check_available async staticmethod
check_available()

Check if bwrap is available on the system.

Returns:

Type Description
bool

True if bwrap is installed and runnable.

Source code in src/marianne/execution/sandbox.py
@staticmethod
async def check_available() -> bool:
    """Check if bwrap is available on the system.

    Returns:
        True if bwrap is installed and runnable.
    """
    try:
        proc = await asyncio.create_subprocess_exec(
            "bwrap", "--version",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        await proc.wait()
        return proc.returncode == 0
    except FileNotFoundError:
        return False

Functions