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):
- Create the package on PyPI via the Trusted Publishers UI (pypi.org → Your projects → Publishing), or generate an API token under Account settings.
- If using Trusted Publishers: register
allenai/dv-core-asta-integrationas the trusted publisher forasta-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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file asta_sandbox-0.1.0.dev1.tar.gz.
File metadata
- Download URL: asta_sandbox-0.1.0.dev1.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7343a756557869a25279ad8c78262f70b119072f69ed49ed9fb07e288c83d0cb
|
|
| MD5 |
02e14eb57c922bd753847460515fb2e4
|
|
| BLAKE2b-256 |
d898893092fe0484d15de97a6f704e1017046dbe8f15629de76e7272426b6a85
|
File details
Details for the file asta_sandbox-0.1.0.dev1-py3-none-any.whl.
File metadata
- Download URL: asta_sandbox-0.1.0.dev1-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f791f4ee3462040f4c0d665abcf2344867005f1ea1434b285479d12fff96d75c
|
|
| MD5 |
57d3a4aa58d67e6cd9da6ee5a89c641e
|
|
| BLAKE2b-256 |
b7b0d607949820beb34c3086e61326f9d56f332ee89b2f980a47b8765bbb8cab
|