Dynamic sandboxes
In addition to the function interface, Modal has a direct interface for defining containers at runtime and running arbitrary code inside them.
This can be useful if, for example, you want to:
- Run code generated by a language model with a list of dynamically generated requirements.
- Check out a git repository and run a command against it, like a test suite, or
npm lint
. - Use Modal to orchestrate containers that don’t have or use Python.
Each individual job is called a Sandbox, and can be created using the
Sandbox.create
constructor:
from modal import Mount, Image, Sandbox
sb = Sandbox.create(
"bash",
"-c",
"cd /repo && pytest .",
image=Image.debian_slim().pip_install("pandas"),
mounts=[Mount.from_local_dir("./my_repo", remote_path="/repo")],
timeout=600, # 10 minutes
)
sb.wait()
if sb.returncode != 0:
print(f"Tests failed with code {sb.returncode}")
print(sb.stderr.read())
It’s useful to note that the
Sandbox
object returned
above has an interface similar to Python’s
asyncio.subprocess.Process
API, and can be used in a similar way.
Parameters
Sandboxes support nearly all configuration options found in regular modal.Function
s.
Refer to Sandbox.create
for further documentation
on Sandbox parameterization.
Dynamically defined environments
Note that any valid Image
or Mount
can be used with a sandbox, even if those
images or mounts have not previously been defined. This also means that images
and mounts can be built from requirements at runtime. For example, you could
use a language model to write some code and define your image, and then spawn a
sandbox with it. Check out devlooper
for a concrete example of this.
Returning or persisting data
Modal Volumes or
CloudBucketMounts can be attached to
sandboxes. If you want to give the caller access to files written by the
sandbox, you could create an ephemeral Volume
that will be garbage
collected when the app finishes:
with modal.Volume.ephemeral() as vol:
sb = modal.Sandbox.create(
"bash",
"-c",
"echo foo > /cache/a.txt",
volumes={"/cache": vol},
)
sb.wait()
for data in vol.read_file("a.txt"):
print(data)
Alternatively, if you want to persist files between sandbox invocations (useful
if you’re building a stateful code interpreter, for example), you can use create
a persisted Volume
with a dynamically assigned label:
session_id = "example-session-id-123abc"
vol = modal.Volume.from_name(f"vol-{session_id}", create_if_missing=True)
sb = modal.Sandbox.create(
"bash",
"-c",
"echo foo > /cache/a.txt",
volumes={"/cache": vol},
)
Isolation and security
Sandboxes can be used to run untrusted code, such as code from third parties or
generated by a language model. Unlike regular Function
runners, Sandbox
runners do not have the ability to spawn new containers, or otherwise perform
operations in your workspace. These runners are also torn down after execution,
and never reused across calls. A sandbox’s network access can also be blocked
with block_network=True
.
Input
Sandboxes support accepting input via the stdin
attribute on the sandbox
object. The stdin
handle is a
StreamWriter object.
sandbox = modal.Sandbox.create(
"bash",
"-c",
"while read line; do echo $line; done",
)
sandbox.stdin.write(b"Hello\n")
sandbox.stdin.drain() # flush line
sandbox.stdin.write(b"world!\n")
sandbox.stdin.drain() # flush line
sandbox.stdin.write_eof()
sandbox.stdin.drain() # flush EOF
sandbox.wait()
for line in sandbox.stdout:
print(line)
Output
The interface for accessing Sandbox output is via the stdout
and stderr
attributes on the Sandbox object. These are
LogsReader
objects,
and they support streaming output or reading it all in one call.
The read
method fetches all logs until EOF and returns the entire output
stream.
sandbox = modal.Sandbox.create("echo", "hello")
sandbox.wait()
print(sandbox.stdout.read())
To stream output take advantage of the fact that stdout
and stderr
are
iterable. Note that this snippet uses an asynchronous iteration.
import asyncio
from modal import Sandbox
async def run():
sandbox = await Sandbox.create.aio(
"bash",
"-c",
"for i in $(seq 1 10); do echo foo $i; sleep 0.1; done"
)
async for line in sandbox.stdout:
print(line)
asyncio.run(run())