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.2.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.2-py3-none-any.whl (29.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: asta_sandbox-0.1.2.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.2.tar.gz
Algorithm Hash digest
SHA256 746911311b09921a77dec5fc51685bf22a1ae6d44e2e3d91e7aca2c12dd73970
MD5 77719e84b8bfbf610182229ec708a7df
BLAKE2b-256 d205d870bd28fea8688bca8322cb6e2a1cf075279d4557d51589517752c7af00

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for asta_sandbox-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 24735e585f95ef30ead6cde94ae620f1a220d54bb426e55dfe725f6feb72b100
MD5 acce171cda63515e57323360044badfa
BLAKE2b-256 6712290e5b5bd85036bb5b903cfed14127156180bdf463a70317e04750305da0

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