Skip to main content

asta-sandbox

Shared code-execution sandbox abstractions for Asta projects. Provides a uniform async interface over four backends — in-process IPython, Docker, and two Modal modes (stateful kernel, stateless ephemeral) — so application code doesn't depend on a specific execution backend.

Installation

# Base package (in-process backend only, requires ipython)
pip install asta-sandbox

# Modal stateful backend (persistent sandbox + Jupyter Kernel Gateway)
pip install "asta-sandbox[modal-kernel]"

# Modal stateless backend (fresh sandbox per call)
pip install "asta-sandbox[modal-ephemeral]"

# Docker backend
pip install "asta-sandbox[docker]"

# Everything
pip install "asta-sandbox[all]"

For local development against an unpublished branch, swap to a path source (and keep the git source commented out for CI):

[tool.uv.sources]
# Local dev: uncomment below and comment out the git source
# asta-sandbox = { path = "../dv-core-asta-integration/packages/sandbox", editable = true }
asta-sandbox = { git = "https://github.com/allenai/dv-core-asta-integration", subdirectory = "packages/sandbox", rev = "main" }

Then add "asta-sandbox" to the dependencies list of any workspace member that needs it, and run uv sync --all-packages to install.

Quick start — Modal stateful sandbox

ModalKernelExecutor runs a persistent Jupyter Kernel Gateway inside a Modal sandbox. Variable and import state accumulates across run_code() calls, mirroring interactive notebook behaviour.

from asta_sandbox.backends.modal_kernel import ModalKernelExecutor

executor = ModalKernelExecutor(app_name="my-analysis")

result = await executor.run_code("""
%pip install pandas
import pandas as pd
""")
assert not result.error

result = await executor.run_code("""
df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
print(df.describe())
""")
print(result.stdout)
print("success:", result.success)

With cloud-mounted buckets

Pass CloudShare objects to make S3 or GCS bucket prefixes available as local paths inside the sandbox, without downloading to the host machine. Multiple buckets (including a mix of S3 and GCS) can be mounted simultaneously by providing one CloudShare per bucket with distinct dest values.

import modal
from asta_sandbox import CloudShare
from asta_sandbox.backends.modal_kernel import ModalKernelExecutor

# modal_secret carries the bucket credentials (AWS or GCS keys) — separate
# from Modal's own authentication, which is configured via `modal token new`.
share = CloudShare(
    bucket="my-data-bucket",
    key_prefix="datasets/project/",
    dest="/data/project/",
    read_only=True,
    modal_secret=modal.Secret.from_name("aws-credentials"),
)

executor = ModalKernelExecutor(app_name="my-analysis")
await executor.add_shares(share)
async with executor:
    # The bucket prefix is now visible at /data/project/ inside the sandbox
    result = await executor.run_code("import os; print(os.listdir('/data/project/'))")
    print(result.stdout)

For GCS, add bucket_endpoint_url="https://storage.googleapis.com" to the CloudShare.

Installing packages at runtime

Use a %pip install magic inside run_code(). Because the stateful backends use a persistent kernel, the installed package stays available for subsequent calls in the same session:

async with ModalKernelExecutor(app_name="my-analysis") as executor:
    await executor.run_code("%pip install scanpy anndata")
    result = await executor.run_code("import scanpy; print(scanpy.__version__)")

Custom image

Use build_modal_kernel_image to bake packages into the image instead of installing them at runtime (faster cold starts, reproducible environment):

from asta_sandbox.images import build_modal_kernel_image
from asta_sandbox.backends.modal_kernel import ModalKernelExecutor

image = build_modal_kernel_image(extra_packages=["scanpy", "anndata", "matplotlib"])

async with ModalKernelExecutor(app_name="my-analysis", image=image) as executor:
    result = await executor.run_code("import scanpy; print(scanpy.__version__)")

Quick start — Modal stateless (ephemeral) sandbox

ModalEphemeralExecutor creates a fresh Modal sandbox for every run_code() call. No state survives between calls. The executor instance itself is long-lived; construct it once and call it many times.

from asta_sandbox.backends.modal_ephemeral import ModalEphemeralExecutor

async with ModalEphemeralExecutor(app_name="my-runner") as executor:
    r1 = await executor.run_code("x = 42; print(x)")
    r2 = await executor.run_code("print(x)")   # NameError — x not in this sandbox

    print(r1.stdout)        # "42"
    print(r2.success)       # False
    print(r2.error.etype)   # "NameError"

For a known fixed set of packages, bake them into the image (faster, reproducible):

from asta_sandbox.images import build_modal_ephemeral_image

image = build_modal_ephemeral_image(extra_packages=["httpx"])
executor = ModalEphemeralExecutor(app_name="my-runner", image=image)

Cloud bucket mounts (S3/GCS) are also supported for ephemeral sandboxes via add_shares(). See the Cloud mounts subsection under the stateful sandbox for the CloudShare API; it works identically here.

For on-demand installs (e.g. a coding agent that discovers mid-task it needs a package), embed a %pip install magic at the top of the same code string that uses the package. Because each run_code() call runs in a fresh container, the install and the import must be in the same call:

async with ModalEphemeralExecutor(app_name="my-runner") as executor:
    result = await executor.run_code("""\
%pip install httpx
import httpx
print(httpx.__version__)
""")
    print(result.stdout)

Quick start — in-process (local / testing)

from asta_sandbox import InProcessExecutor

async with InProcessExecutor() as executor:
    await executor.run_code("x = 1 + 1")
    result = await executor.run_code("print(x)")
    print(result.stdout)   # "2"

Quick start — Docker (local stateful sandbox)

DockerExecutor runs a Jupyter Kernel Gateway inside a Docker container. Stateful (like ModalKernelExecutor): variable state persists across calls. Useful for local development and CI without a Modal account.

from asta_sandbox.backends.docker import DockerExecutor

async with DockerExecutor() as executor:
    await executor.run_code("x = 42")
    result = await executor.run_code("print(x)")
    print(result.stdout)   # "42"

The default image is built from dockerfiles/kernel_gateway/Dockerfile. If the image is not present locally, DockerExecutor will build it automatically on first use (requires docker running).

Mounting and uploading files

Use add_shares() to make local paths available inside the container:

from pathlib import Path
from asta_sandbox import CopyShare
from asta_sandbox.backends.docker import DockerExecutor

async with DockerExecutor() as executor:
    await executor.add_shares(CopyShare(source=Path("results.csv"), dest="/workspace/results.csv"))
    result = await executor.run_code("import os; print(os.listdir('/workspace'))")

MountShare (bind-mount) must be added before the first run_code() call, since Docker can't bind-mount into a running container:

from asta_sandbox import MountShare

async with DockerExecutor() as executor:
    await executor.add_shares(MountShare(source=Path("my_data"), dest="/workspace/data"))
    result = await executor.run_code("import os; print(os.listdir('/workspace/data'))")

ModalKernelExecutor supports the same MountShare / CopyShare / CloudShare API. InProcessExecutor does not support file shares. See docs/components.md for the full per-type × backend matrix.


Per-call timeout

Pass timeout_seconds to limit how long a single run_code() call may run:

result = await executor.run_code(
    "import time; time.sleep(10)",
    timeout_seconds=5,
)
print(result.success)      # False
print(result.error.etype)  # "TimeoutError"

InProcessExecutor enforces a soft limit (asyncio cancels the wait; the underlying thread may still run). Remote backends (Docker, Modal) kill the sandbox/process, giving a hard deadline.


For a full description of all types, backend capabilities, and component relationships, see docs/components.md.


Publishing to PyPI

Publishing is manual. You need a PyPI API token (Account settings → API tokens).

cd packages/sandbox

# 1. Bump version in pyproject.toml (PEP 440: 0.1.0, 0.1.0.dev1, 0.2.0a1, ...)
# 2. Build and publish:

rm -rf dist/
uv build
uv publish --token pypi-YOUR_TOKEN_HERE

Dev/pre-release versions (0.1.0.dev1, 0.1.0a1, etc.) are not installed by pip install asta-sandbox by default — users must pin the exact version or pass --pre.

Metadata

Release files for asta-sandbox 0.1.3

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

Source distribution (sdist)

Source distribution for asta-sandbox 0.1.3
File Size Uploaded
asta_sandbox-0.1.3.tar.gz 24.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for asta-sandbox 0.1.3
File Interpreter ABI Platform
asta_sandbox-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 58.2 kB

Release files / asta_sandbox-0.1.3.tar.gz

Download URL asta_sandbox-0.1.3.tar.gz
Size 24.7 kB
Tags Source
SHA-256 checksum
How to use checksums
7dbf0c8f62d41053dd7db3cad4e4fef921d43800cea9227554e691bf27d77753
BLAKE2b-256 checksum
How to use checksums
068fc0fb2b579a8c8c596e91bcf4cedc77cc33b29c34a3c0314325e66dfde621
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.19

Release files / asta_sandbox-0.1.3-py3-none-any.whl

Download URL asta_sandbox-0.1.3-py3-none-any.whl
Size 33.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4b0596d70517230fb6bd0ee3c8373edc442ded0fb555f3a7468126eb9d0f0783
BLAKE2b-256 checksum
How to use checksums
0c4a81cf79c318ac762d931bbb2785dd943fedd8a4e674588b8134683bf0c306
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.19
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