Skip to content

Modal Sandbox Backend

autodiscovery_modal provides helpers for running isolated code in Modal ephemeral sandboxes. Each call creates a fresh sandbox, executes the code, and terminates the sandbox. No state persists between calls.

The main types re-exported from asta_sandbox:

  • ModalEphemeralExecutor: Async executor that creates a fresh Modal sandbox per run_code() call.
  • CloudShare: Mounts a cloud storage bucket prefix into the sandbox filesystem.
  • build_sandbox_image / build_modal_ephemeral_image: Build Modal images with IPython and optional extra packages.

Usage Examples

Basic execution

import asyncio
from autodiscovery_modal import ModalEphemeralExecutor, build_sandbox_image

image = build_sandbox_image(extra_packages=["numpy", "pandas", "matplotlib", "matplotlib-inline"])

executor = ModalEphemeralExecutor(app_name="my-app", image=image)

async def main():
    await executor.start()
    result = await executor.run_code("print('hello')\n1 + 1")
    print(result.stdout)
    print(result.success)
    await executor.shutdown()

asyncio.run(main())

Execution with a GCS bucket mount

Mount a store prefix at its store-shaped path: key users/<uid>/... at /mnt/data/users/<uid>/..., the same path the job containers see it at. AutoDiscovery's modal backend mounts the run's users/<uid>/jobs/<jid>/data/ prefix this way, sets DATASET_ROOT to it, and chdirs into it before each cell so generated code can open datasets by bare filename.

import asyncio
import modal
from autodiscovery_modal import ModalEphemeralExecutor, CloudShare, build_sandbox_image

image = build_sandbox_image(extra_packages=["pandas"])

cloud_share = CloudShare(
    dest="/mnt/data/users/u1/jobs/j1/data",
    bucket="my-bucket",
    key_prefix="users/u1/jobs/j1/data/",
    read_only=True,
    bucket_endpoint_url="https://storage.googleapis.com",
    modal_secret=modal.Secret.from_name("gcs-my-bucket"),
)

executor = ModalEphemeralExecutor(
    app_name="my-app",
    image=image,
    environment={"DATASET_ROOT": "/mnt/data/users/u1/jobs/j1/data"},
)

async def main():
    await executor.start()
    await executor.add_shares(cloud_share)
    result = await executor.run_code(
        "import os, pathlib; print(list(pathlib.Path(os.environ['DATASET_ROOT']).iterdir()))"
    )
    print(result.stdout)
    await executor.shutdown()

asyncio.run(main())

Capturing matplotlib figures

Figures produced by plt.show() are returned as rich_outputs on the result:

import asyncio
from autodiscovery_modal import ModalEphemeralExecutor, build_sandbox_image

image = build_sandbox_image(extra_packages=["matplotlib", "matplotlib-inline"])

executor = ModalEphemeralExecutor(app_name="my-app", image=image)

async def main():
    await executor.start()
    result = await executor.run_code(
        "import matplotlib.pyplot as plt\nplt.plot([1,2,3])\nplt.show()"
    )
    for ro in result.rich_outputs:
        if "image/png" in ro.data:
            print("Got PNG figure")
    await executor.shutdown()

asyncio.run(main())

Notes

  • Each run_code() call creates a new sandbox. No variables, imports, or data persist between calls.
  • CloudShare mounts are scoped per executor instance. Call add_shares() before run_code().
  • Use build_sandbox_image(extra_packages=[...]) to bake in Python packages. Modal caches image layers by content hash, so unchanged images are free to reuse.
  • For the full asta_sandbox API (including SandboxBase, ExecutionResult, RichOutput), see the asta-sandbox package.