> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Run captured programs inside a sandbox (Docker, Apple Container, smolvm, Daytona, e2b, or any machine over SSH) you provision yourself.

The `pyodide` and `monty` runtimes run `python3` in-process on your machine.
A **sandbox runtime** does the opposite: it takes a whole command line and
runs it inside a separate machine, a local container or microVM or a cloud
sandbox. Reach for it when a line needs a real OS, heavy packages, or a GPU
that the in-process interpreters cannot give it.

Six sandbox runtimes ship from `@struktoai/mirage-node`, all with the same
surface:

| Runtime | Connects to | Config |
| - | - | - |
| [`docker`](/typescript/runtime/sandbox/docker) | A container you started (`docker run`) | `container` |
| [`apple_container`](/typescript/runtime/sandbox/apple_container) | A container you started with Apple's [`container`](https://github.com/apple/container), or one per agent | `container`, `containers` |
| [`smolvm`](/typescript/runtime/sandbox/smolvm) | A [smolvm](https://smolmachines.com) microVM you started | `machine` |
| [`daytona`](/typescript/runtime/sandbox/daytona) | A [Daytona](https://www.daytona.io) sandbox you created | `sandboxId`, `apiKey` |
| [`e2b`](/typescript/runtime/sandbox/e2b) | An [E2B](https://e2b.dev) sandbox you created | `sandboxId`, `apiKey` |
| [`ssh`](/typescript/runtime/sandbox/ssh) | Any machine running sshd | `host`, `username`, `identityFile` |

`docker`, `apple_container` and `smolvm` run on your own machine, Daytona
and e2b in the cloud, and `ssh` reaches anything you can already `ssh` into.
The isolation differs too: a Docker container shares its host's kernel, an
Apple Container and a smolvm microVM each boot their own, and an ssh machine
is whatever machine you point it at.

<Note>
  Looking for Sandlock? Mirage's
  [Sandlock adapter](/python/runtime/sandbox/sandlock) is available in `@struktoai/mirage-node` on Linux.
  It runs native programs, including Python and Node.js.
</Note>

## Routing: what goes to the sandbox

Sandbox runtimes default to `captures: ["@external"]`: only program names
Mirage does not resolve are delegated. Existing Mirage commands, pipes and
redirects stay in the workspace.

```yaml theme={null}
runtimes:
  - name: sandlock
    captures: ["@external"] # optional: this is the sandbox default
```

Named captures explicitly select a runtime for a program. For example,
`captures: ["python3", "node", "@external"]` selects native interpreters and
keeps the external fallback. Without those named captures, Mirage's
interpreter commands keep their normal runtime bindings.
Shell builtins such as `cd`, `export`, and `echo` stay in Mirage even when
named in `captures`.

A named capture is a program: `which gcc` prints `/usr/bin/gcc`, and that
file's comment names the runtime it runs on. The `@external` fallback takes
any word, so a name only the fallback would run has no file there: `which`,
`command -v` and `type` report it missing, as bash does for a name its
`command_not_found_handle` takes, and running it still delegates.

`python3 job.py | grep error > /logs/errors` delegates only `python3`;
Mirage executes `grep` and writes the VFS output. An explicit `captures: ["*"]`
opts into delegating the entire shell line.

The SDK exports the same marker:

```typescript theme={null}
import { EXTERNAL_COMMANDS } from '@struktoai/mirage-core';
import { SandlockRuntime } from '@struktoai/mirage-node';

const runtime = new SandlockRuntime({ captures: [EXTERNAL_COMMANDS] });
```

A delegated program receives its words the way bash hands them over: every
unquoted glob is expanded against the workspace first, whatever operand it
fills, so `python3 job.py *.csv` runs `python3 job.py a.csv b.csv`, and a
quoted or escaped glob stays literal. Command rules then judge that expanded
argv: `grep *.txt` is refused when a match that grep reads as a file is protected.
An interpreter's script is a file operand like any other, so `python3 job.py` is
judged on `job.py` however it is spelled, and the words after it stay its argv.

## The workspace inside the sandbox

For the job to see your mounts as ordinary files, the sandbox must serve
the workspace itself, and provisioning that is yours, like everything else
about the sandbox. Run Mirage inside it, with the **same mounts at the
same prefixes** as the host workspace, each FUSE-mounted at its own
prefix:

```yaml theme={null}
# sandbox.yaml, inside the sandbox
mounts:
  /data:
    vfs: s3
    config:
      bucket: my-bucket
      aws_access_key_id: ...
      aws_secret_access_key: ...
    backend: fuse
    mountpoint: /data
```

```bash theme={null}
mirage workspace create sandbox.yaml # entrypoint, or once by hand
```

A read then pulls from the backend and a write streams straight back to
it, with no upload or sync step: inside the sandbox, `/data` backed by S3
is a real directory. Because you write the sandbox-side config, it can
differ from the host's where it should: the endpoint that is
`127.0.0.1:9000` on your laptop is `host.docker.internal:9000` from inside
a container, credentials can be scoped down, and none of it ever travels
over the provider's exec API. The flip side is that keeping the prefixes
in step with the host workspace is your job; a sandbox serving different
mounts fails loud only when a path misses.

This needs an image with `fuse3` and Mirage plus your backends installed
(see [The sandbox image](#the-sandbox-image)). The in-sandbox mount is
served by the Python Mirage build, so the image is the same for both
language SDKs.

<Note>
  The `ssh` provider can skip all of this when the files live on the ssh
  machine itself: mount them over the ssh VFS at a prefix equal to
  their remote absolute path, and captured lines open them natively with no
  FUSE and no remote Mirage. See
  [Skip the FUSE](/typescript/runtime/sandbox/ssh#skip-the-fuse) on the SSH
  page.
</Note>

## Paths inside the sandbox

Mirage rewrites nothing: the line, its cwd, and every path in it pass
through verbatim. With the sandbox serving the same prefixes, `cd /data`
then `python3 train.py` works, and so does an absolute path like
`python3 /data/train.py`, because `/data` means the same thing on both
sides.

## The sandbox image

The sandbox needs `fuse3` plus Mirage with the backends you mount. The repo
ships a Dockerfile that builds this image from the current checkout, so it
always matches your code:

```bash theme={null}
docker build -f docker/sandbox/Dockerfile --target fuse -t mirage-python-fuse .
```

<Note>
  The shared image uses the Python Mirage CLI to serve workspace mounts
  inside the sandbox. Your host application can use either SDK, including
  TypeScript; it communicates with the sandbox through the runtime's
  transport. The image's implementation and the host SDK are independent.

  When building a custom image, install the programs your captured commands
  need (for example, Node.js for `node`) and serve workspace mounts at the
  same paths as the host workspace.
</Note>

By default it installs every mountable backend plus fuse. Narrow it to just
the backends you use for a smaller image, or extend it as a base:

```bash theme={null}
docker build -f docker/sandbox/Dockerfile --target fuse \
  --build-arg MIRAGE_EXTRAS=s3,postgres -t mirage-fuse-lean .
```

The one image is the shared base for every machine provider: run it directly
under Docker, Apple's `container` (build it with `container build`, same
flags) or smolvm, use it as a Daytona `image`/`snapshot` source, or use it as
an E2B template base. Continue with the provider page in the Sandbox
sidebar for connection and configuration details.

## Selecting in YAML

Sandbox runtimes are ordinary `runtimes` entries: a name, its captures, and
a `config` block describing the machine (mirroring a mount's `config`
block), with `workspace` as the in-process catch-all. Only the selected runtime
consumes its entry, so one file stays portable:

```yaml theme={null}
runtimes:
  - name: docker
    captures: ["python3"]
    config:
      container: my-sandbox # started with your own `docker run`
  - workspace
mounts:
  /data:
    vfs: s3
    config:
      bucket: ${AWS_S3_BUCKET}
      region: ${AWS_DEFAULT_REGION}
      aws_access_key_id: ${AWS_ACCESS_KEY_ID}
      aws_secret_access_key: ${AWS_SECRET_ACCESS_KEY}
```

## Resource limits

A captured line is a command like any other: the same `command_limits`
that guard `cat` or `grep` guard `python3`, including in the sandbox. A run
that exceeds `timeoutSeconds` answers exit 124; `maxBytes` and `maxLines`
cap its output the same way. There is no sandbox-specific limit surface.

<Warning>
  E2B cancellation and command timeouts attempt to kill the command and
  disconnect its output stream. Descendant processes may survive.

  Other providers may leave remote commands running after Mirage stops
  waiting. Provider timeouts and sandbox lifecycle limits still apply;
  configure sandbox creation and cleanup in your application.
</Warning>

## SDK loader overrides

Runtime subclasses that override `loadSdk()` import the SDK type from
the provider's `sdk` module:

```ts theme={null}
import type { E2bSdk } from '@struktoai/mirage-core/runtime/sandbox/e2b/sdk'
import type { DaytonaSdk } from '@struktoai/mirage-node/runtime/sandbox/daytona/sdk'
import type { Ssh2Sdk } from '@struktoai/mirage-node/runtime/sandbox/ssh/sdk'
```

These types live alongside the optional SDK loaders; `runtime` modules
export the runtime classes.

## Evaluating route-policy scripts

A sandbox runs programs; it does not evaluate expressions, so it cannot
run [route-policy scripts](/home/route-policy). To make one eligible, subclass it
and implement `eval` with your own transport. The
[docker eval examples](https://github.com/strukto-ai/mirage/tree/main/examples)
do exactly that by piping a small harness to the container's `python3 -`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.