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
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.2.dev1.tar.gz.
File metadata
- Download URL: asta_sandbox-0.1.2.dev1.tar.gz
- Upload date:
- Size: 20.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a0ceda6ad2ef9650b99941961cb9cd6a234e0e5f2aeba372feb27521afbf557
|
|
| MD5 |
6ff550f649cc71e12185815981f445a9
|
|
| BLAKE2b-256 |
6ad79291a89803ebc15d511417f04c6c2a482c22bd63313bcccdc98d408f8e02
|
File details
Details for the file asta_sandbox-0.1.2.dev1-py3-none-any.whl.
File metadata
- Download URL: asta_sandbox-0.1.2.dev1-py3-none-any.whl
- Upload date:
- Size: 29.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66c540c885dcb8f368961184508b3c14062b069166d5ecaeee2d2801e6e35f4d
|
|
| MD5 |
99032341d2d2afbf87a4b3ce4b2fae86
|
|
| BLAKE2b-256 |
e6e7edd836fe6c1fbabb7ba19ab347fdef19dbc88cc38dc0b89601b2a2b6a9dc
|