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)
| File | Size | Uploaded | |
|---|---|---|---|
| asta_sandbox-0.1.3.tar.gz | 24.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|