Dynamic sandboxes (beta)
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
spawn_sandbox
function on a running
app:
@stub.local_entrypoint()
def main():
sb = stub.spawn_sandbox(
"bash",
"-c",
"cd /repo && pytest .",
image=modal.Image.debian_slim().pip_install("pandas"),
mounts=[modal.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
spawn_sandbox
currently supports
the following parameters:
workdir
: Working directory for the sandbox. Defaults to/
.image
: The container image to use for the sandbox.mounts
: List of read-only mounts to mount into the sandbox.secrets
: List of secrets to make available to the sandbox. Be careful with this option, as it can expose secrets to untrusted code.network_file_systems
: Dictionary mapping mount paths to Network File Systems to be attached.gpu
: Optional configuration for GPU accelerator to attach.cpu
: Minimum number of cores the sandbox should be allocated.memory
: Minimum amount of memory the sandbox should be allocated.timeout
: Timeout in seconds before the sandbox is killed. Defaults to 300 seconds (5 minutes).
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 NetworkFileSystems can be attached to
sandboxes. If you want to give the caller access to files written by the
sandbox, you could create an ephemeral NetworkFileSystem
that will be garbage
collected when the app finishes:
@stub.local_entrypoint()
def main():
stub.nfs = modal.NetworkFileSystem.new()
sb = stub.spawn_sandbox(
"bash",
"-c",
"echo foo > /cache/a.txt",
network_file_systems={"/cache": stub.nfs},
)
sb.wait()
for data in nfs.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 NetworkFileSystem
with a dynamically assigned label:
stub.nfs = modal.NetworkFileSystem.persisted(f"vol-{session_id}")
sb = stub.spawn_sandbox(
"bash",
"-c",
"echo foo > /cache/a.txt",
network_file_systems={"/cache": stub.nfs},
)
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.
Output
The interface for accessing sandbox output is via the stdout
and stderr
attributes on the sandbox object. These are
LogsReader
objects,
and at the moment have a single method read
, which returns the entire output
stream. In the future, we plan to add support for streaming output and input.
sandbox = stub.spawn_sandbox("echo", "hello")
sandbox.wait()
print(sandbox.stdout.read())