Metadata-Version: 2.4
Name: maf-sandbox-bubblewrap
Version: 0.5.0
Summary: Linux namespace and cgroup sandboxes without a container engine.
Author: SOKOLAI BV
Author-email: SOKOLAI BV <info@sokolai.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Typing :: Typed
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: POSIX :: Linux
Requires-Dist: maf-sandbox>=0.53.0,<0.54
Requires-Python: >=3.12, <3.15
Project-URL: Homepage, https://www.sokol.ai
Project-URL: Source, https://github.com/sokolaidev/maf-extensions
Project-URL: Issues, https://github.com/sokolaidev/maf-extensions/issues
Description-Content-Type: text/markdown

# maf-sandbox-bubblewrap

[![PyPI](https://img.shields.io/pypi/v/maf-sandbox-bubblewrap)](https://pypi.org/project/maf-sandbox-bubblewrap/) [![Python](https://img.shields.io/pypi/pyversions/maf-sandbox-bubblewrap)](https://pypi.org/project/maf-sandbox-bubblewrap/) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/sokolaidev/maf-extensions/blob/main/packages/maf-sandbox-bubblewrap/LICENSE)

> **Experimental.** Not yet released. `maf-sandbox-bubblewrap` may change or be removed without notice and emits `MafSandboxBubblewrapExperimentalWarning` on import.

Experimental Linux sandbox backend using Bubblewrap and cgroup v2, without Docker, an OCI image or a container engine. The runtime is a separately provisioned, trusted Linux root directory. Runtime downloads never happen during a tool call.

## Prerequisites

- Linux with unprivileged user, mount, PID, network, IPC, UTS and cgroup namespaces; Bubblewrap supporting `--disable-userns`, `--assert-userns-disabled`, `--as-pid-1` and bounded tmpfs mounts. Missing support refuses startup.
- A private cgroup v2 delegation with `cpu`, `memory` and `pids` enabled, `memory.swap.max` and `cgroup.kill`. The host process must already belong to that delegation so it can move children into sibling groups. A systemd service with `Delegate=cpu memory pids` can provide it; the host must place itself in a leaf and enable the controllers on its empty parent. This package does not grant privileges or change host-wide policy.
- A trusted runtime directory containing `/usr/bin/python3` and `/bin/sh`, with no host credentials or sockets. Keep it immutable while sandboxes run. Runtime, cgroup and state paths and their ancestors must be controlled by the host administrator. State and cgroup roots must resolve outside the runtime. The state directory must be owned by the service user and mode `0700`.

## Use

```python
from pathlib import Path
from maf_sandbox import Isolation, SandboxRouter
from maf_sandbox_bubblewrap import BubblewrapSandboxBackend, BubblewrapSandboxConfig

async def make_router() -> SandboxRouter:
    backend = await BubblewrapSandboxBackend.create(BubblewrapSandboxConfig(
        runtime_root=Path("/opt/maf-runtime"),
        state_root=Path("/var/lib/my-service/sandbox-state"),
        cgroup_root=Path("/sys/fs/cgroup/my-service"),
        runtime_id="drawio-native",
    ))
    return SandboxRouter([backend], min_isolation=Isolation.CONTAINER)
```

`SandboxSpec.image` is an optional exact match against the configured `runtime_id`, not an OCI reference. `image_id` is unsupported. The storage base is `/maf-sandbox/work`; only that explicit override or `None` is accepted. Relative working directories address the base. Commands are opaque; a string runs through `/bin/sh -c`, while a sequence executes directly.

The protocol's `Isolation.CONTAINER` means Linux namespaces plus cgroups. It describes the boundary even though no container engine is used. The router's default `MICROVM` floor still refuses this backend. Keep a separate router or use per-spec routing for a native renderer alongside a stronger CodeAct backend; never lower a shared host floor implicitly.

## Boundary and lifecycle

Every sandbox gets mandatory private namespaces, no capabilities, disabled nested user namespaces, a new session and a cleared environment. The runtime is read-only. `/tmp`, `/run` and `/maf-sandbox` are separate bounded tmpfs mounts; aggregate memory, swap, process count and CPU are constrained before any guest starts. Default limits are 1 GiB memory, no swap, 256 processes, two CPU equivalents and 128 MiB per writable mount. No host home, display, network interface or cgroup filesystem is mounted. The default declares `Egress.CLOSED`, POSIX `EXEC`, `FILES_IN` and `FILES_OUT`; the explicitly configured native proxy adds the filtered egress surface below. This is a shared-kernel boundary, not a microVM or a claim of protection against kernel exploits. No seccomp syscall filter is currently supplied.

A private PID 1 broker uses no-follow directory descriptors for transfers. It runs at guest filesystem authority; its stat responses are guest-side metadata, not host attestations. Its control descriptors are protected from ptrace, and commands receive separate stdin/stdout/stderr. Individual transfers are capped at 8 MiB; the router applies its collection count and aggregate byte limits. Combined command output is capped at 1 MiB by default; excess output fails explicitly. A command's descendants, including detached processes, are killed before its reply. Timeout or cancellation after dispatch destroys the entire sandbox; while queued, it refuses only the waiting request. There is no reclamation or snapshot capability; the router uses disposal.

Key identity includes scope, thread, agent, call and kind. An exclusive host file lock prevents another backend object or process from acquiring a live instance. Repeated acquire within one backend can reuse a matching spec; another owner is refused. Files live only in tmpfs and disappear on owner death. Records allow a subsequent host to purge dead owners' cgroups; this is cleanup recovery, not restoration of guest state. Stale instance disposal cannot delete a replacement. A scope purge reports busy or failed records and retains them for retry. Hosts must stop new work across replicas before purging a conversation.

Lock files remain after disposal so a concurrent owner cannot lock a replacement inode under the same name. Remove the state directory only after stopping all owners; long-lived deployments should account for those small files in retention policy.

Ownership records are published atomically before cgroup creation, so a host that dies during startup leaves either no cgroup or a complete cleanup record. Failed setup removes its record after cgroup cleanup succeeds. Commands whose requested timeout exceeds `max_timeout` (180 seconds by default) are rejected with `ValueError` before execution. Accepted deadlines include time waiting for the request lock; only the remaining budget reaches the broker.

## Method-filtered HTTPS egress

Configure `egress_proxy` as the absolute path to a trusted native build of the repository's pinned, patched iron-proxy and `egress_proxy_sha256` as its lowercase SHA-256. Without both, admission remains `CLOSED` only. With both, the backend supports `ALLOWLIST`, `EGRESS_METHODS` and `EGRESS_PATHS`; attached credential authority is refused. The binary is checked before each proxy starts. Keep it and its ancestors immutable to untrusted principals.

Each allowlisted sandbox receives its own proxy under the same cgroup and owner-death supervision. The private guest network reaches it through a mounted Unix socket and loopback relay. The kernel selects the proxy's listener ports; the relay requires `/proc` evidence that its child owns the tunnel listener before forwarding guest bytes. Only the CA certificate and socket enter the guest; signing keys remain host-side. Upstream TLS is verified, and HTTP methods and paths are enforced after TLS termination. Private, loopback and link-local upstream addresses are denied. Proxy connections and diagnostic retention are bounded. Disposal removes the proxy, guest and proxy files, including after timeout or cancellation.

`exec_bounded` applies a caller-selected combined stdout/stderr budget, capped by `output_bytes`. The broker enforces it while executing and the host bounds response frames before decoding. Overflow raises `SandboxExecOutputLimitExceeded`; no partial result is returned.

The [experimental pricing sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/experimental/azure_pricing) provides runtime/proxy provisioning, dedicated cgroup setup, retained identities and real pricing/enforcement probes.

## Offline Draw.io runtime

The [runtime provisioning script](https://github.com/sokolaidev/maf-extensions/blob/main/images/drawio-export/build-runtime.sh) uses debootstrap and the pinned Draw.io Desktop package directly, with the same offline assets, fonts and exporter as the image profile. Provisioning needs network access and root; rendering needs neither. Run the host outside that runtime and register this backend with `maf-sandbox-drawio`, passing the configured runtime id as `image`. Linux is the only supported host platform; native Windows and macOS backends are separate work.
