Skip to main content

sunabako · 砂箱

中文版

Linux sandboxes inside an existing container, without Docker-in-Docker. A Rust runtime manages resource admission, isolation and lifecycle; a Python SDK provides familiar Sandbox.create(), commands.run() and files.read/write() interfaces. Add SSH nodes to distribute rollout sandboxes across an allocation.

This is an early implementation. The current four-machine environment provides user namespaces and Landlock, but its cgroup mount is read-only and AppArmor blocks mounts. The PRoot backend supports directory mappings in this environment. Production startup refuses to proceed without a hard aggregate cgroup memory limit. The explicitly named allow_unbounded_memory_for_tests option is for bounded functional verification only; it is not protection against outer-container OOM and must not be used to oversubscribe production workloads.

Build and install

Linux x86-64, Python 3.10+, and kernel 5.15+ are currently required. Building from source needs Rust/Cargo; installing a built wheel needs neither Rust nor maturin. Node operations run as root inside the allocated outer container; sandboxes run under distinct outer UIDs with capabilities removed. User namespaces must be enabled. PRoot mode additionally requires Landlock and Yama ptrace_scope >= 1.

python -m pip install 'maturin>=1.8,<2'
CARGO_TARGET_DIR=/tmp/sunabako-target maturin build --release --locked --out dist
python -m pip install dist/sunabako-*.whl

The wheel contains the statically linked Rust executable and the Python package. Builds use a local target directory to avoid slow shared-filesystem compilation. Release automation is in .github/workflows/wheels.yml. It builds one Linux x86-64 py3-none wheel, tests the installed package on Python 3.10–3.14, and rebuilds the source distribution in a clean build environment. These checks exercise packaging, SDK imports and the Rust CLI without requiring root, GPUs or delegated cgroups. The privileged runtime tests below remain separate from these packaging checks.

For PyPI publishing, configure a Trusted Publisher for project sunabako with your actual GitHub owner and repository, workflow filename wheels.yml, and environment name pypi. Create that environment in the GitHub repository too. For a first release, use a pending publisher. The workflow uses GitHub OIDC, so no PyPI API token needs to be stored in GitHub secrets.

Keep the versions in Cargo.toml, Cargo.lock, pyproject.toml and python/sunabako/__init__.py aligned. After committing and pushing the release files, push the matching version tag:

git tag v0.1.0
git push origin v0.1.0

Only a v* tag push publishes. Branch pushes, pull requests and manual runs build and test without uploading to PyPI. The publishing job waits for all checks, verifies the package versions against the tag, and uploads the same wheel and sdist that passed verification. A mismatched tag or a failed check blocks publication.

For the mount-free compatibility backend, build the pinned PRoot version with the included PID-namespace compatibility patch:

apt-get install -y gcc make git pkg-config libtalloc-dev
bash scripts/build-proot.sh

PRoot remains a separate GPL-2.0-or-later executable. The Rust/Python project is Apache-2.0. patches/proot-pidns.patch modifies PRoot and is GPL-2.0-or-later; the script pins its upstream source and submodule revisions. PRoot is not silently downloaded or executed on package import.

Configure a node and an image

The platform must delegate an empty writable cgroup-v2 subtree with the memory and pids controllers. Point cgroup_parent at that subtree. sunabako creates its own child pool, sets memory.max, disables swap for the pool, and verifies the limit before allowing sandbox startup. Choose its size to leave RAM for training and the outer container's other processes. Limits cover sunabako workloads, not unrelated processes outside that pool or GPU VRAM.

sunabako doctor
sunabako --state /var/lib/sunabako init-node configs/node.example.json
apt-get install -y skopeo umoci
python -m sunabako.images ubuntu:24.04 /var/lib/sunabako-images/ubuntu

Image import verifies registry layers with skopeo and unpacks with umoci. No Docker daemon is needed. Import trusted images. OCI metadata is saved in image.json; pass the image's environment through envs and its working directory through cwd when a task requires them. Entry points are not automatically started.

Relationship to the e2b Python API

sunabako implements a subset of the e2b Python SDK's calling conventions for creating sandboxes, running commands and reading/writing files. It is an independent package and runtime, not an e2b service endpoint or a complete SDK replacement. Migrate the import, sandbox configuration and exception imports; review the supported methods below. The implementation is in python/sunabako/client.py.

e2b-style call sunabako support and migration details
Sandbox.create(template=..., envs=...) Supported. template is an unpacked rootfs path on the chosen node, not an e2b template ID or an OCI image name. Import the image first. Select node= or cluster= and configure memory, mounts, GPU and network explicitly.
AsyncSandbox.create(...) Async counterpart with the same options. Async operations delegate to the local/SSH client using worker threads.
sb.commands.run(cmd, timeout=..., envs=..., cwd=..., user=...) Runs a shell string or argv list and returns CommandResult with exit_code, stdout, stderr, plus timed_out and truncated. Defaults are 120 seconds, /workspace, and guest root.
Handling nonzero command exits Raises sunabako's CommandExitException by default. Import it from sunabako; e2b's exception class does not catch it. check=False returns the result instead.
sb.files.write(path, data) Accepts str, bytes, or a host Path; returns None. No file-like object, batch-write or WriteInfo return support.
sb.files.read(path, format="text") Returns str; format="bytes" returns bytes. No stream format or per-operation user/request_timeout options on files.
Sandbox.connect(sandbox_id, ...) Requires node=. Reconnects to an existing sandbox without starting or resuming it. Save both the ID and node; call resume() or start() explicitly when needed.
sb.sandbox_id, sb.get_info(), sb.kill() Available. get_info() returns a runtime dict, not e2b's SandboxInfo; kill() returns None and removes the local rootfs. External mounts and snapshots remain.
with sb: / async with sb: Cleans up with kill() on exit. Use explicit lifecycle calls if the sandbox should survive the block.

The names pause and snapshot need particular care when migrating. e2b persistence can preserve process memory and resume through connect(). In sunabako, pause() freezes processes while retaining RAM and GPU allocations, and snapshot() copies only the rootfs and metadata. To release memory, use stop() or application-managed hibernate(checkpoint_command); start() starts fresh processes, while restore(restore_command) explicitly restores application state.

sunabako has no sandbox-lifetime create(timeout=...) or set_timeout() API. Its command deadline stops the whole sandbox, including detached children. There is no automatic expiration or e2b-style resume-on-connect. Network access defaults to network="none"; choose network="host" when required. Node authentication uses the local environment or SSH, with no E2B_API_KEY.

Other e2b APIs are currently outside this subset: command streaming callbacks, background=True handles, stdin/PTY, files.list/remove/watch, template build services, public URLs / get_host(), and the code-interpreter run_code() API. Shell commands can still start detached jobs with output redirected to files. sunabako adds its own Node, Cluster, Mount, cgroup-pool and GPU controls. For the upstream interfaces, see e2b's commands and file read/write documentation.

A minimal migrated call site, after node configuration and image import:

from sunabako import Sandbox, Node, CommandExitException

node = Node(rootfs="/var/lib/sunabako-images/ubuntu/rootfs")
with Sandbox.create(node=node, runtime="proot", memory_mb=2048) as sb:
    sb.files.write("/tmp/input.txt", "hello")
    try:
        result = sb.commands.run("cat /tmp/input.txt", timeout=30)
        print(result.stdout)
    except CommandExitException as error:
        print(error.exit_code, error.stderr)

In slime, examples/coding_agent_rl/sunabako_sandbox.py adapts this SDK to the small shared slime.agent.sandbox.Sandbox protocol: async exec() returns (exit_code, stdout, stderr), alongside write_file(), read_file() and async context management. The example's sandbox.py defaults to sunabako and selects the same provider for both agents and graders. This protocol adaptation is separate from full e2b SDK compatibility.

Python

from sunabako import Sandbox, Node, Mount

node = Node(rootfs="/var/lib/sunabako-images/ubuntu/rootfs")
with Sandbox.create(
    node=node, memory_mb=2048, runtime="proot",
    gpus=[0], gpu_mode="shared",
    mounts=[Mount("/data/rollouts/task-001", "/workspace")],
) as sb:
    sb.files.write("/workspace/input.txt", "hello")
    result = sb.commands.run("cat /workspace/input.txt; nvidia-smi -L")
    print(result.stdout)
    sb.pause()       # Freezes the process tree; retains RAM and GPU allocations.
    sb.resume()
    sb.stop()        # Releases processes/RAM/VRAM; preserves the writable rootfs.
    sb.start()       # Fresh processes using that existing filesystem.

A mapped directory must grant the sandbox's dedicated outer UID the intended access (see get_info()['uid']). Use separate per-task directories or a deliberate shared group/ACL. Do not recursively change ownership of existing user data. readonly=True denies file-content writes using Landlock in PRoot mode. Sources should be owned by a different UID to also protect metadata on older Landlock kernels. Native mode uses kernel read-only bind mounts.

commands.run() returns exit_code, stdout, stderr, timed_out, and truncated; nonzero exits raise CommandExitException unless check=False. Output is capped at 2 MiB per stream and file reads at 8 MiB. Uploads stream in 4 MiB chunks, including large Path inputs. An interrupted upload can leave a partial destination. A command timeout terminates the whole sandbox so detached descendants cannot outlive its deadline; the filesystem remains available for start() and inspection. PRoot supports image users through commands.run(..., user="agent"); create that user in the image first. All guest identities map to the sandbox's one unprivileged outer UID, so they do not form security boundaries within a sandbox. Native mode currently supports namespace root only. Shell background jobs persist within the sandbox; redirect their standard streams to files before detaching them.

network="none" uses a private network namespace with loopback only. Explicit network="host" allows network access through the outer container, sharing its network namespace. No virtual bridge or port publishing is installed.

Slime and multiple machines

from sunabako import AsyncSandbox, Cluster

cluster = Cluster.from_file("configs/cluster.example.json")
sb = await AsyncSandbox.create(cluster=cluster, memory_mb=2048, runtime="proot")
async with sb:
    result = await sb.commands.run("echo rollout")
    code, stdout, stderr = await sb.exec("echo compatible with slime's Sandbox protocol")

The async adapter also supplies write_file and read_file for slime.agent.sandbox.Sandbox. Install sunabako into Slime's Python environment; the provider selection, dataset conversion and training launcher belong in Slime's examples/coding_agent_rl, using SWE_SANDBOX_PROVIDER=sunabako. Install the wheel, runtime dependency, image and node configuration on each allocated machine; add nodes to the cluster file to expand capacity. SSH uses existing authentication. A capacity-ranked scheduler selects nodes; each node holds a lock during admission so concurrent clients cannot over-reserve memory or GPU slots. A transport timeout never blindly replays sandbox creation or command execution. Save sandbox_id and node to reconnect from a restarted client.

memory_overcommit=2 permits total requested memory of twice the pool size, while the kernel pool limit stays unchanged. Paused sandboxes retain reservations. Stopped/hibernated sandboxes release them. GPU sharing is ordinary CUDA sharing of specifically exposed devices, with a configurable number of admission slots; it does not partition compute or enforce per-sandbox VRAM quotas. Exclusive reservations exclude other sunabako reservations, not unrelated host processes.

Persistence and hibernation

sb.hibernate("python /workspace/save_checkpoint.py")
# Processes are gone; an application checkpoint and the writable filesystem remain.
sb.restore("python /workspace/load_checkpoint.py")
sb.snapshot("/shared/snapshots/task-001")

The checkpoint command must quiesce its writers and durably save the application's state, and must succeed before processes are terminated. restore runs an explicit application restore command. This is not a transparent CRIU process-memory image. GPU state must likewise be saved by the application before hibernation. A checkpoint command returning nonzero leaves the sandbox running; the general whole-sandbox timeout policy still applies. Writable volume filesystems are flushed before stopping. pause freezes CPU threads and does not release memory or cancel GPU kernels already submitted to the driver.

Filesystem snapshots briefly freeze running processes and copy the writable rootfs plus metadata. They exclude external volume contents and process memory. Create a new sandbox using snapshot/rootfs as its template; volume data lives at the configured source directory. Put snapshots and volumes on shared persistent storage to survive destruction of the outer container. kill() deletes the sandbox's local rootfs; it leaves external volume sources and snapshots intact.

Isolation and current scope

Native mode uses kernel chroot and user/PID/IPC/UTS/mount namespaces. Bind mounts and a private procfs require the outer AppArmor policy to permit mount operations. Without mount permission, use PRoot mode: a persistent trusted init runs inside a private PID namespace; PRoot translates filesystem paths while Landlock limits the actual kernel paths. Commands inherit a syscall filter, no-new-privileges, zero capabilities, PID limits, and a dedicated outer UID. Only selected NVIDIA device nodes and driver libraries are copied into the rootfs.

On Landlock ABI 1, small helpers preserve cross-directory rename and hard-link semantics needed by package managers. Each helper is confined by kernel chroot to exactly one writable mapping, uses the sandbox UID, and drops capabilities and unrelated file descriptors. Read-only mappings have no helper. These processes are charged to the same sandbox cgroup and included in RSS monitoring.

This shares the Linux kernel and (when requested) a GPU driver. It is not a VM, and has not received a security audit for hostile multi-tenant use. Current limitations include no interactive PTY API, no automatic overlay-layer sharing, no transparent CRIU/CUDA process checkpoint, and no GPU memory quotas.

Verification

The current container environment passed the real CUDA kernel probe on all four allocated H100 nodes, writable-volume persistence, two sandboxes sharing one GPU, pause/resume, application-checkpoint hibernation, filesystem snapshots, and cross-sandbox filesystem checks. The integration suite also verifies that normal node startup refuses a missing aggregate cgroup limit. Functional tests here use explicit RSS test mode because the outer cgroup mount is read-only.

Terminal Bench 2.1's cancel-async-tasks image was tested using its official oracle and unmodified verifier: 6/6 tests passed, reward 1. This is one task's runtime compatibility check, not a full benchmark result. Reproduce it after importing alexgshaw/cancel-async-tasks:20251031 and cloning the official task repository:

python tests/tb21.py \
  --repository /path/to/terminal-bench-2-1 \
  --rootfs /path/to/imported-bundle/rootfs \
  --state /tmp/sunabako-tb-new \
  --artifacts /persistent/tb-new \
  --binary /path/to/sunabako \
  --proot /path/to/patched-proot \
  --allow-test-memory

The test requires unused state/artifact paths, saves verifier output, and deletes its sandbox after completion. On a delegated cgroup installation, replace --allow-test-memory with --cgroup-parent /path/to/delegated/cgroup.

Kernel behavior references: cgroup v2, user namespaces, Landlock, PRoot.

Metadata

Release files for sunabako 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sunabako 0.1.0
File Size Uploaded
sunabako-0.1.0.tar.gz 72.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sunabako 0.1.0
File Interpreter ABI Platform
sunabako-0.1.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details

Total release size: 1.0 MB

Release files / sunabako-0.1.0.tar.gz

Download URL sunabako-0.1.0.tar.gz
Size 72.9 kB
Tags Source
SHA-256 checksum
How to use checksums
fdb7c575588e0b4cfa538522d90ab35a9b5e846b279e84b721ababf6c2c68f49
BLAKE2b-256 checksum
How to use checksums
a67fece3f327724bfb42b31f4aec474a4d7a0f4a770ab466a89434965a192e02
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.

Transparency log

Release files / sunabako-0.1.0-py3-none-manylinux_2_28_x86_64.whl

Download URL sunabako-0.1.0-py3-none-manylinux_2_28_x86_64.whl
Size 969.6 kB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
9b0374f2cd796e7b9c0668657d37294baf4ae2b19436bfc2b704cca633a0824a
BLAKE2b-256 checksum
How to use checksums
8fb6b2291acc2ffa78381e8064580ce458c5203e0ec0cf1c9d53026e66925acb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page