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

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.

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.1.tar.gz (20.8 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.1-py3-none-any.whl (29.2 kB view details)

Uploaded Python 3

File details

Details for the file asta_sandbox-0.1.1.tar.gz.

File metadata

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

File hashes

Hashes for asta_sandbox-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e041cf971e6b986654ea0fdcbf2b1bc8de810db434572fd06b3b24d06529e580
MD5 5385f42a1fe96b0172abd9fb244837cf
BLAKE2b-256 0b01695dc43d169ee816e32cfe4798712813145faa176aa9248ae97f96f50d56

See more details on using hashes here.

File details

Details for the file asta_sandbox-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for asta_sandbox-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5ed7645e3f29566dbdad1bc0131507b6074fc13044374ca380c0818c46ef3cf7
MD5 035a393ca7e3580498e3809848ddc682
BLAKE2b-256 84ab9440475a0a3ebfd3f65bfe2b40b87bc575c7c94fcfe54e06a1c64c0da547

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