# Sandboxes

> Short-lived machines for one task each. A sandbox starts from a template, runs commands and holds files, and is removed at its deadline.

A sandbox is a machine with a deadline. Create it from a template, run commands in it, read and write its files, and let it go: at its deadline Rytiv removes the machine and everything on its disk. Each sandbox has an id like `sbx-1a2b3c4d5e6f` and its own address.

## Create a sandbox

**CLI**

```bash
ry sandbox create app:web --ttl 30m
```

**curl**

```bash
curl -X POST https://api.rytiv.app/v1/sandboxes \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"template": "app:web", "ttlSeconds": 1800}'
```

**TypeScript**

```ts
const res = await fetch("https://api.rytiv.app/v1/sandboxes", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RYTIV_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ template: "app:web", ttlSeconds: 1800 }),
});
const sandbox = await res.json();
console.log(sandbox.id, sandbox.state, sandbox.url);
```

`POST /v1/sandboxes` answers when the machine runs, with `201` and the sandbox. The answer also carries `createMs`, the time from your request to the running machine, and, for a sandbox with signed access, an `accessToken`.

### Templates

A sandbox boots the image of a template you already have:

| Template | Boots |
| --- | --- |
| `app:<name>` | The image an app of your organization runs right now. The app must run a machine. |
| `build:<id>` | A build of your organization that produced a machine image (not a static site). |

Nothing else is inherited from the template: no environment variables and no secrets of the app. Pass what the sandbox needs in `env`.

### Request

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `template` | string | required | `app:<name>` or `build:<id>` |
| `ttlSeconds` | integer | `300` | Lifetime from now, 10 to 86400 |
| `vcpus` | integer | `1` | 1 to 16 |
| `memoryMb` | integer | `512` | 128 to 16384 |
| `diskMb` | integer | | 512 to 131072 |
| `env` | object | `{}` | Up to 128 variables, names like `MY_VAR`. A few `RYTIV_` names are reserved. |
| `ports` | integer[] | `[]` | Up to 8 more ports to reach from outside |
| `internalPort` | integer | the template app's port, else `3000` | The port `url` points at |
| `readiness` | `boot` or `port` | `boot` | Ready when the machine boots, or when `internalPort` answers |
| `access` | `signed` or `public` | `signed` | Who can open the sandbox's URLs, see [Access](#access) |
| `idleSeconds` | integer | `300` | 10 to 86400, see the note below |
| `metadata` | object | `{}` | Up to 16 key/value pairs for your own bookkeeping |
| `egress` | object | allow all | Outbound network, see [Network](#network) |

Unknown fields are refused with `400`.

> **Idle sleep:** `idleSeconds` is accepted and stored with the sandbox. Whether a region's hosts put an idle sandbox to sleep is part of the region's configuration; do not build on a sandbox being asleep. Its deadline (`ttlSeconds`) is what ends it.

### The sandbox object

| Field | Meaning |
| --- | --- |
| `id` | `sbx-` and 12 hex digits |
| `template` | The template it was created from |
| `state` | `starting`, `running`, `sleeping` or `ended` |
| `createdAt`, `expiresAt` | When it was created, and its deadline |
| `endedAt`, `endReason` | When and why it ended: `expired`, `killed`, `failed` or `app-deleted` |
| `machineId` | The machine behind the sandbox |
| `vcpus`, `memoryMb`, `readiness`, `access`, `metadata`, `egress` | As requested |
| `url` | `https://<id>.rytiv.app` |
| `ports` | One `{port, url}` per extra port, with `url` like `https://<port>-<id>.rytiv.app` |

## Access

With `access: "signed"` (the default), the sandbox's URLs only answer requests that carry a token. The create answer contains one:

```json
"accessToken": { "token": "...", "expiresAt": "...", "header": "x-rytiv-access" }
```

Send it in the header named by `header`. It is valid for the lifetime the sandbox was created with, at most 24 hours. With `access: "public"`, anyone with the URL can open it.

## Run a command

**CLI**

```bash
ry sandbox exec sbx-1a2b3c4d5e6f -- uname -a
```

**curl**

```bash
curl -X POST https://api.rytiv.app/v1/sandboxes/sbx-1a2b3c4d5e6f/exec \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cmd": ["uname", "-a"]}'
```

**TypeScript**

```ts
const res = await fetch(`https://api.rytiv.app/v1/sandboxes/${id}/exec`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RYTIV_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ cmd: ["uname", "-a"] }),
});
const { exitCode, stdout, stderr } = await res.json();
```

`POST /v1/sandboxes/:id/exec` waits for the command and answers with its result.

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `cmd` | string or string[] | required | A string runs through `/bin/sh -c`, an array is the argument list (no shell) |
| `cwd` | string | | An absolute path |
| `env` | object | | More environment for this command |
| `timeoutMs` | integer | `60000` | 1000 to 600000, and never past the sandbox's deadline |
| `stdin` | string | | Up to 1 MiB |

The answer has `exitCode`, `signal`, `timedOut`, `durationMs`, `stdout` and `stderr`. Each output stream is cut at 1 MiB, and `stdoutTruncated` or `stderrTruncated` say so.

`ry sandbox exec` exits with the command's own exit code, `124` on a timeout, `128 + n` when signal `n` ended it, and `255` for an error of its own.

## Files

| Method | Path | Body and answer |
| --- | --- | --- |
| `GET` | `/v1/sandboxes/:id/files?path=/abs/path` | The file's bytes |
| `PUT` | `/v1/sandboxes/:id/files?path=/abs/path` | Send the bytes. Optional `mode` (default `0644`) and `mkdirs=1`. Answers `{path, bytes, sha256}`. |
| `GET` | `/v1/sandboxes/:id/files/list?path=/abs/dir` | `{path, entries}`, each entry `{name, type, size, mode, mtimeMs}` |

Paths are absolute and may not contain `..`. One file is at most 64 MiB.

```bash
curl -X PUT "https://api.rytiv.app/v1/sandboxes/sbx-1a2b3c4d5e6f/files?path=/tmp/hello.txt" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  --data-binary "hello"
```

## Network

`egress` decides what the sandbox may reach:

| `mode` | Meaning |
| --- | --- |
| `allow-all` | The default |
| `deny-all` | Nothing, not even DNS |
| `allowlist` | Only the listed `domains` (up to 100, a leading `*.` allowed) and IPv4 `cidrs` (up to 10), on ports 80 and 443 |

The policy is fixed for the sandbox's lifetime.

```json
{ "template": "app:web", "egress": { "mode": "allowlist", "domains": ["api.github.com", "*.npmjs.org"] } }
```

## Extend, list, end

| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/v1/sandboxes/:id/timeout` | `{ttlSeconds}`: the new deadline is now plus `ttlSeconds` (10 to 86400). Calling it twice does not add up. |
| `GET` | `/v1/sandboxes` | Running sandboxes, newest first. `?state=all` adds the ones that ended in the last 14 days. |
| `GET` | `/v1/sandboxes/:id` | One sandbox |
| `DELETE` | `/v1/sandboxes/:id` | Ends it now. `204`, or `202` if it is already ending. Its machine and disk are gone. |

```bash
ry sandbox timeout sbx-1a2b3c4d5e6f 1h
ry sandbox list --all
ry sandbox kill sbx-1a2b3c4d5e6f
```

## Limits and errors

An organization can run 100 sandboxes at a time by default; the next create answers `429` with `too-many-sandboxes`.

| Status | Code | When |
| --- | --- | --- |
| `404` | `template-not-found` | No such app or build in your organization |
| `409` | `template-not-runnable` | The build is a static site, or the app runs no machine |
| `404` | `sandbox-not-found` | No such sandbox in your organization |
| `409` | `sandbox-ended`, `sandbox-starting` | The sandbox has ended, or is not running yet |
| `429` | `too-many-sandboxes` | The organization's limit is reached |
| `503` | `no-capacity` | No room right now, try again |

See the [CLI reference](https://docs.rytiv.com/reference/cli/sandbox.md) for every flag of `ry sandbox`.
