Skip to main content

Shared code execution sandbox abstractions for Asta projects

Project description

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]"

Declaring as a dependency in a uv workspace

Once published to PyPI, install normally:

pip install "asta-sandbox[modal-kernel]"   # or [docker], [modal-ephemeral], [all]

Until then, or to pin a specific branch, declare it as a git source in a uv-managed workspace:

# pyproject.toml (workspace root)
[tool.uv.sources]
asta-sandbox = { git = "https://github.com/allenai/dv-core-asta-integration", subdirectory = "packages/sandbox", rev = "main" }

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.

import asyncio
from asta_sandbox.backends.modal_kernel import ModalKernelExecutor

async def main():
    async with ModalKernelExecutor(app_name="my-analysis") as executor:
        # Imports accumulate
        await executor.run_code("import pandas as pd")

        # Each subsequent call sees prior state
        await executor.run_code("df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})")
        result = await executor.run_code("print(df.describe())")

        print(result.stdout)
        print("success:", result.success)

asyncio.run(main())

With cloud-mounted buckets

Pass CloudMount 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 CloudMount per bucket with distinct mount_path values.

import asyncio
import modal
from asta_sandbox import CloudMount
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`.
mount = CloudMount(
    bucket="my-data-bucket",
    key_prefix="datasets/project/",
    mount_path="/data/project/",
    read_only=True,
    modal_secret=modal.Secret.from_name("aws-credentials"),
)

async def main():
    async with ModalKernelExecutor(app_name="my-analysis", cloud_mounts=[mount]) as 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)

asyncio.run(main())

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

Installing packages at runtime

async with ModalKernelExecutor(app_name="my-analysis") as executor:
    await executor.install_packages(("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.

import asyncio
from asta_sandbox.backends.modal_ephemeral import ModalEphemeralExecutor

async def main():
    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"

asyncio.run(main())

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)

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)

import asyncio
from asta_sandbox import InProcessExecutor

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

asyncio.run(main())

Per-call options

RunOptions overrides behaviour for a single run_code() call:

from asta_sandbox import RunOptions

result = await executor.run_code(
    "import time; time.sleep(10)",
    RunOptions(timeout_seconds=5, title="long cell"),
)
print(result.success)    # False
print(result.error.etype)  # "TimeoutError"
Field Default Notes
timeout_seconds None Per-call limit. For InProcessExecutor without use_subprocess, enforced as a soft limit (asyncio cancels the wait; the thread may still run). For remote backends, enforced by the sandbox/process kill.
use_subprocess False Spawn a fresh child process for the call (InProcessExecutor and ModalEphemeralExecutor). Provides hard timeout enforcement and stronger isolation for those backends.
allow_mime None Override MIME allowlist for rich outputs.
title "cell" Label carried through to result.metadata["title"].

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.

import asyncio
from asta_sandbox.backends.docker import DockerExecutor

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

asyncio.run(main())

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).


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


Publishing to PyPI

Publishing is manual for now. You need a PyPI account with Trusted Publisher configured for this repo, or a PyPI API token.

First-time setup (once per PyPI account):

  1. Create the package on PyPI via the Trusted Publishers UI (pypi.org → Your projects → Publishing), or generate an API token under Account settings.
  2. If using Trusted Publishers: register allenai/dv-core-asta-integration as the trusted publisher for asta-sandbox.

Publishing a release:

cd packages/sandbox

# Bump version in pyproject.toml (use PEP 440: 0.1.0, 0.1.0.dev1, 0.2.0a1, ...)
# Then:

uv build                                      # produces dist/
uv publish                                    # uses OIDC if available
# or: 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. Prefer a dev version for early testing and a proper 0.x.0 tag for anything shared more broadly.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asta_sandbox-0.1.0.dev2.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

asta_sandbox-0.1.0.dev2-py3-none-any.whl (32.2 kB view details)

Uploaded Python 3

File details

Details for the file asta_sandbox-0.1.0.dev2.tar.gz.

File metadata

  • Download URL: asta_sandbox-0.1.0.dev2.tar.gz
  • Upload date:
  • Size: 23.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.19

File hashes

Hashes for asta_sandbox-0.1.0.dev2.tar.gz
Algorithm Hash digest
SHA256 5273daeca7b6b753810dfab81d7aa63412b73fa1163966e2877d377754246734
MD5 016dd5d72855e1564170c981b2aefc14
BLAKE2b-256 f872e6ecf633b47f13653f0c7cbd597238566df121ba6af98027b8640d261d84

See more details on using hashes here.

File details

Details for the file asta_sandbox-0.1.0.dev2-py3-none-any.whl.

File metadata

File hashes

Hashes for asta_sandbox-0.1.0.dev2-py3-none-any.whl
Algorithm Hash digest
SHA256 796d75a5c8c8333134c431fc95a9cf7adbb2213fd6b04723e6b3ed8eb5cdb3b6
MD5 546a78c38069c29d9ca1b75f78d8982f
BLAKE2b-256 0600175cb687c8768e7a2d63881b911264aab98392634ab69311e27410bbeb23

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page