Sandboxes
A sandbox is a hardware-isolated virtual machine with its own kernel, filesystem, network stack, and allocated vCPU, RAM, and disk, with isolation enforced by the hypervisor.
Sandboxes run as Linux (default) or macOS virtual machines. Every sandbox moves through a lifecycle and can have several different states. While the sandbox is running, it exposes a toolbox for code, command, and process execution, sessions, filesystem and git operations, computer use, language server protocol support, and log streaming.
Use sandboxes to run code written by an agent, build and test software, drive desktop through screenshots and input events, or spin up thousands of identical environments for autonomous, evaluation, and reinforcement learning workflows.
| Concept | Description |
|---|---|
| Sandbox | An isolated, dedicated virtual machine. |
| Image | The OCI image, Dockerfile build, or snapshot the sandbox boots from. |
| Resources | The vCPU, memory, and disk allocated to the sandbox. |
| Lifecycle | The states a sandbox can be in and the transitions between them. |
| Persistence | Deleting a sandbox discards its filesystem. Persistent macOS sandboxes can stop/start, pause/resume, fork. |
| Toolbox | A set of features for code, command, and process execution, sessions, filesystem and git operations, computer use, and language server protocol support. |
Create sandboxes
Section titled “Create sandboxes”Daytona provides methods to create sandboxes from the dashboard or programmatically.
- Go to Daytona Sandboxes ↗
- Click
- Select or
- Click
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create()import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const sandbox = await daytona.v2.create()daytona sandbox createcurl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}'Linux sandboxes
Section titled “Linux sandboxes”Linux sandboxes are Firecracker microVMs that run their own Linux kernel, and they are the default sandbox type. A Linux sandbox boots from an image that you import from a container registry, build from a Dockerfile, or capture from another sandbox. It can also start from a snapshot of another Linux sandbox. Each sandbox runs on an amd64 or arm64 CPU.
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create()import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const sandbox = await daytona.v2.create()daytona sandbox createcurl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{}'macOS sandboxes
Section titled “macOS sandboxes”macOS sandboxes are virtual machines running macOS on Apple silicon Mac computers. A macOS sandbox starts from a macOS image. By default it gets half of the Mac’s cores, memory and sandbox disk on a split reservation, or all of them on a whole reservation; a create can ask for a smaller size.
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create_macos("base-macos")import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const sandbox = await daytona.v2.createMacos('base-macos')curl 'https://api.daytona.com/sandboxes/macos' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"image": "base-macos"}'| Concept | Description |
|---|---|
| Sandbox | An isolated, dedicated virtual machine. |
| Image | The OCI image, Dockerfile build, or snapshot the sandbox boots from. |
| Resources | The vCPU, memory, and disk allocated to the sandbox. |
| Lifecycle | The states a sandbox can be in and the transitions between them. |
| Persistence | Deleting a sandbox discards its filesystem. Persistent macOS sandboxes can stop/start, pause/resume, fork. |
| Toolbox | A set of features for code, command, and process execution, sessions, filesystem and git operations, computer use, and language server protocol support. |
Resources
Section titled “Resources”Sandboxes have 0.125 vCPU, 1024MiB memory, and 512MiB disk by default. A sandbox may guarantee at most 16 vCPUs, 32GiB memory, and 256GiB disk.
| Resource | Unit | Default | Minimum | Maximum |
|---|---|---|---|---|
| CPU | vCPU | 0.125 | 0.125 | 16 |
| Memory | MiB | 1024 | 512 (when set explicitly) | 32768 |
| Disk | MiB | 512 | 10240 (when set explicitly) | 262144 |
from daytona import Daytonafrom daytona.v2 import Resources
daytona = Daytona()sandbox = daytona.v2.create( # memory and disk are GiB: 4 maps to memory_mib=4096, 20 to disk_mib=20480 resources=Resources(cpu=2, memory=4, disk=20),)import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const sandbox = await daytona.v2.create({ // memory and disk are GiB: 4 maps to memory_mib=4096, 20 to disk_mib=20480 resources: { cpu: 2, memory: 4, disk: 20 },})# --memory and --disk are MiBdaytona sandbox create --cpu 2 --memory 4096 --disk 20480curl 'https://api.daytona.com/sandboxes' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "cpu": 2, "memory_mib": 4096, "disk_mib": 20480}'Hardware virtualization
Section titled “Hardware virtualization”Server processors include instructions for running virtual machines directly on the hardware. A virtual machine monitor uses these instructions to create a guest with its own virtual processors, memory, and devices. The processor rejects any access from a guest to host memory or to another guest’s memory. Linux and macOS sandboxes run as virtual machines, each on the virtual machine monitor that its operating system needs.
| Sandbox | Host | Runtime |
|---|---|---|
| Linux | Linux with KVM | Firecracker virtual machine |
| macOS | Mac with Apple silicon | Virtualization framework virtual machine |
Linux exposes the processor’s virtualization through the Kernel-based Virtual Machine (KVM). Linux sandboxes run on Firecracker, a virtual machine monitor built to run many small, short-lived virtual machines on one host. Firecracker provides only the devices a server needs: virtual disks, a network card, and a serial console. Each Linux sandbox runs as one Firecracker process on a host, in its own network namespace, with its own virtual processors and memory.
macOS sandboxes run on Macs, because Apple’s license allows macOS virtual machines only on Apple hardware. Apple silicon includes hardware support for virtual machines, which macOS exposes through Apple’s Virtualization framework. Each macOS sandbox runs as its own virtual machine on its Mac, with its own virtual processors, memory, disk, and network card.
Lifecycle
Section titled “Lifecycle”Every sandbox moves through a lifecycle. A sandbox can have several different states. Each state reflects the status of your sandbox. A sandbox transitions between states in response to user actions, or automatically when one of its timeouts elapses.
See the lifecycle of Linux and macOS sandboxes.
| Feature | Linux | macOS |
|---|---|---|
| Stop and start | ✗ | ✓ persistent |
| Pause and resume | ✗ | ✓ persistent |
| Fork | ✗ | ✓ persistent |
| Delete | ✓ | ✓ |
| Filesystem persistence | ✗ | ✓ |
| Memory persistence | ✗ | ✓ persistent |
| Memory snapshot | ✓ warm template | ✗ |
| Filesystem snapshot | ✓ | ✓ persistent |
| Spot (Preemption) | ✓ | ✗ |
| Network policy | ✓ | ✓ |
| Desktop | ✓ | ✓ Screen Sharing |
Operations
Section titled “Operations”Daytona provides methods to manage existing sandboxes. Sandbox operations find sandboxes, read their current state and configuration, and update their metadata. They do not change the state of a sandbox or touch anything running inside it. To stop, pause, fork, or delete a sandbox, see lifecycle. To run commands or work with files inside a sandbox, use the toolbox.
List sandboxes
Section titled “List sandboxes”List sandboxes in the project, page by page. Unfiltered lists read the region-local database, so new sandboxes appear at once. To filter by name, state, labels, or resources, use search.
from daytona import Daytona
daytona = Daytona()page = daytona.v2.list()for sandbox in page.items: print(sandbox.id, sandbox.name, sandbox.state)page.next_cursorisNoneon the last page.- Pass
next_cursorback ascursorfor the next page. - Passing any argument other than
cursorandlimit, for examplename,status, orlabels, sends the call to the search index instead. - The search index is eventually consistent.
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const page = await daytona.v2.list()for (const sandbox of page.items) { console.log(sandbox.id, sandbox.name, sandbox.state)}page.nextCursoris unset on the last page.- Pass
nextCursorback ascursorfor the next page. - Passing any option other than
cursorandlimit, for examplename,status, orlabels, sends the call to the search index instead. - The search index is eventually consistent.
daytona sandbox list| Flag | Type | Description |
|---|---|---|
--limit | int | Sets the page size. The default is 100. |
--cursor | string | Continues from a previous page. |
- The command prints rows from responding regions even when another region fails.
curl 'https://api.daytona.com/sandboxes' \ --header 'Authorization: Bearer YOUR_API_KEY'| Field | Type | Description |
|---|---|---|
labels | string | Filters by a JSON-encoded object such as {"team":"research"}; only sandboxes whose labels contain every pair are returned. A malformed value returns 400. |
- Pass
next_cursorfrom the response ascursorto read the next page. - The response includes deleted sandboxes until retention removes them.
Iterate over all sandboxes
Section titled “Iterate over all sandboxes”Iterate over every sandbox in the project without handling page cursors. The iterator fetches the next page when the current one is exhausted and stops when the server returns no cursor.
from daytona import Daytona
daytona = Daytona()for sandbox in daytona.v2.iter(): print(sandbox.id, sandbox.name, sandbox.state)import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()for await (const sandbox of daytona.v2.iter()) { console.log(sandbox.id, sandbox.name, sandbox.state)}# Follow every page until the server returns no cursordaytona sandbox list --allSearch sandboxes
Section titled “Search sandboxes”Search sandboxes across every region. Search reads a central index that is eventually consistent: a create, update, or delete becomes searchable about 2 to 3 seconds after it is committed, so a sandbox created a moment ago may be missing. To read your own writes, use list without filters or get. Filters combine with AND, and deleted sandboxes stay in the results until retention removes them unless you filter by state.
from daytona import Daytona
daytona = Daytona()
# Free text over name and image, or an exact sandbox idpage = daytona.v2.search(q="my-sandbox")for sandbox in page.items: print(sandbox.id, sandbox.name, sandbox.state)| Parameter | Type | Description |
|---|---|---|
status | Sequence[str] | API state names such as running, not the SDK names returned in sandbox.state. |
The response has the following fields:
| Field | Description |
|---|---|
next_cursor | The cursor is tied to the filters and sort of its call. |
partial_regions | Lists the regions that are backfilling. Results from these regions can be incomplete. |
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// Free text over name and image, or an exact sandbox idconst page = await daytona.v2.search({ q: 'my-sandbox' })for (const sandbox of page.items) { console.log(sandbox.id, sandbox.name, sandbox.state)}| Parameter | Type | Description |
|---|---|---|
status | string | readonly string[] | API state names such as running, not the SDK names returned in sandbox.state. |
The response has the following fields:
| Field | Description |
|---|---|
nextCursor | The cursor is tied to the filters and sort of its call. |
partialRegions | Lists the regions that are backfilling. Results from these regions can be incomplete. |
# Free text over name and image, or an exact sandbox iddaytona sandbox search my-sandbox| Flag | Type | Description |
|---|---|---|
--status | string | Repeatable or comma-separated. Matches any listed value. |
--class | string | Repeatable or comma-separated. Matches any listed value. |
- The query is optional.
- Filter flags such as
--status,--class,--name-prefix, and--labelsselect sandboxes on their own or narrow the query.
# Free text over name and image, or an exact sandbox idcurl 'https://api.daytona.com/sandboxes/search?q=my-sandbox' \ --header 'Authorization: Bearer YOUR_API_KEY'| Field | Type | Description |
|---|---|---|
limit | integer | Sets the page size, from 1 to 200. The default is 100. |
sort | string | Sets the sort field. The default is created_at. |
order | string | Sets the sort order. The default is desc. |
- A
cursoris tied to the filters and sort of its request.
Get a sandbox
Section titled “Get a sandbox”Get a sandbox by its ID. The response includes the current state, resources, labels, and lifecycle settings. Get reads the control plane directly, so it reflects changes immediately, unlike search.
from daytona import Daytona
daytona = Daytona()
# Fetch the sandbox from the control planesandbox = daytona.v2.get("{sandboxId}")print(sandbox.state, sandbox.cpu, sandbox.memory, sandbox.disk)The response has the following fields:
| Field | Description |
|---|---|
memory | Gives the memory in GiB, rounded up to a whole GiB. The API reports memory in MiB as memory_mib. |
disk | Gives the disk in GiB, rounded up to a whole GiB. The API reports disk in MiB as disk_mib. |
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// Fetch the sandbox from the control planeconst sandbox = await daytona.v2.get('{sandboxId}')console.log(sandbox.state, sandbox.cpu, sandbox.memory, sandbox.disk)The response has the following fields:
| Field | Description |
|---|---|
memory | Gives the memory in GiB, rounded up to a whole GiB. The API reports memory in MiB as memory_mib. |
disk | Gives the disk in GiB, rounded up to a whole GiB. The API reports disk in MiB as disk_mib. |
daytona sandbox get {sandboxId}curl 'https://api.daytona.com/sandboxes/{sandboxId}' \ --header 'Authorization: Bearer YOUR_API_KEY'Find sandboxes by name
Section titled “Find sandboxes by name”Find sandboxes by the name they were created with. Names are not unique, so the result is a list, newest first, and it is empty when no sandbox matches. The lookup reads the control plane directly, so a sandbox created a moment ago is found, unlike with search.
The name is stored as the name label. Replacing a sandbox’s labels without it removes the sandbox from name lookups.
from daytona import Daytona
daytona = Daytona()
# Reuse a named sandbox, or create it when none existsfound = daytona.v2.find_by_name("my-sandbox")sandbox = found[0] if found else daytona.v2.create(name="my-sandbox")| Parameter | Type | Description |
|---|---|---|
name | str | Required. Exact match. A blank name raises BadRequestError before the request is sent. |
region | str | Sets the region to search. "local" or omitting it means the region of the API host. |
find_by_name()leaves out deleted and deleting sandboxes.
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// Reuse a named sandbox, or create it when none existsconst found = await daytona.v2.findByName('my-sandbox')const sandbox = found[0] ?? (await daytona.v2.create({ name: 'my-sandbox' }))| Parameter | Type | Description |
|---|---|---|
name | string | Required. Exact match. A blank name throws DaytonaError before the request is sent. |
options.region | string | Sets the region to search. 'local' or omitting it means the region of the API host. |
findByName()leaves out deleted and deleting sandboxes.
# Search by the name label: labels={"name":"my-sandbox"}curl 'https://api.daytona.com/sandboxes/search?labels=%7B%22name%22%3A%22my-sandbox%22%7D' \ --header 'Authorization: Bearer YOUR_API_KEY'- Unlike the SDK methods, the response can include deleting sandboxes. Use
statusto filter them out. - Search results can be briefly stale after a change.
Create an access token
Section titled “Create an access token”Create a short-lived access token for one sandbox. The token reaches the sandbox toolbox, SSH, desktop, and private preview ports, and can stop, pause, or delete the sandbox, limited to the operations and ports you grant. Give it to a collaborator, a CI job, or a browser instead of a project API key.
The token lasts one hour by default and at most 24 hours. It cannot be refreshed, revoked, or used to create other credentials. Create a new token when it expires.
# Grant file system and SSH access for 15 minutestoken = sandbox.create_access_token(operations=["toolbox:fs", "ssh"], ttl_seconds=900)print(token.access_token, token.expires_at)| Parameter | Type | Description |
|---|---|---|
operations | Sequence[str] | Lists the operations the token grants. Each operation is one of toolbox, toolbox:exec, toolbox:fs, toolbox:git, toolbox:lsp, desktop, ssh, sandbox:stop, sandbox:pause, or sandbox:delete. The default is every operation the API key can grant. [] grants preview ports only. Granting sandbox:delete requires the delete:sandboxes permission. |
ports | Sequence[int] | Sets the private preview ports that the token can reach. [] grants none. The default is all ports. |
ttl_seconds | int | Sets the token lifetime in seconds, at most 86400. The default is 3600. |
// Grant file system and SSH access for 15 minutesconst token = await sandbox.createAccessToken({ operations: ['toolbox:fs', 'ssh'], ttlSeconds: 900 })console.log(token.accessToken, token.expiresAt)| Parameter | Type | Description |
|---|---|---|
operations | string[] | Lists the operations the token grants. Each operation is one of toolbox, toolbox:exec, toolbox:fs, toolbox:git, toolbox:lsp, desktop, ssh, sandbox:stop, sandbox:pause, or sandbox:delete. The default is every operation the API key can grant. [] grants preview ports only. Granting sandbox:delete requires the delete:sandboxes permission. |
ports | number[] | Sets the private preview ports that the token can reach. [] grants none. The default is all ports. |
ttlSeconds | number | Sets the token lifetime in seconds, at most 86400. The default is 3600. |
# Grant file system and SSH access for 15 minutescurl 'https://api.daytona.com/sandboxes/{sandboxId}/access-token' \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{"operations": ["toolbox:fs", "ssh"], "ttl_seconds": 900}'| Field | Type | Description |
|---|---|---|
operations | string[] | Sets the operations that the token grants: any of toolbox, toolbox:exec, toolbox:fs, toolbox:git, toolbox:lsp, desktop, ssh, sandbox:stop, sandbox:pause, and sandbox:delete. [] grants preview ports only. The default is every operation that the API key can grant. |
ports | integer[] | Sets the private preview ports that the token can reach, from 1 to 65535, at most 128. [] grants none. The default is all ports. |
ttl_seconds | integer | Sets the token lifetime in seconds, at most 86400. The default is 3600. |
The response has the following fields:
| Field | Description |
|---|---|
access_token | The access token. |
token_type | The token type is Bearer. |
expires_at | The time when the token expires. |
operations | The operations that the token grants. |
ports | Lists the private preview ports the token can reach. The value is null when every port is granted. |
- The request requires the
write:sandboxespermission. - Authenticate with an API key.
- A sandbox access token cannot create another token.
Update labels
Section titled “Update labels”Update a sandbox’s labels. Labels are key-value strings for organizing sandboxes, and you can filter by them when you list or search. An update replaces the full label set; omitted labels are removed. A sandbox holds at most 100 labels, each key and value at most 512 bytes.
from daytona import Daytona
daytona = Daytona()sandbox = daytona.v2.create()
# Replace the full label setlabels = sandbox.set_labels(labels={"team": "research"})print(labels)| Parameter | Type | Description |
|---|---|---|
labels | Mapping[str, str] | Required. Keyword only. {} removes all labels. |
set_labels()updatessandbox.labels.
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()const sandbox = await daytona.v2.create()
// Replace the full label setconst labels = await sandbox.setLabels({ team: 'research' })console.log(labels)| Parameter | Type | Description |
|---|---|---|
labels | Record<string, string> | Required. {} removes all labels. |
setLabels()updatessandbox.labels.
# Replace the full label setdaytona sandbox update {sandboxId} --label team=research| Flag | Type | Description |
|---|---|---|
--label | string | Repeatable, one key=value pair per flag. |
--clear-labels | bool | Removes all labels. |
# Replace the full label setcurl 'https://api.daytona.com/sandboxes/{sandboxId}/labels' \ --request PUT \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --data '{ "labels": { "team": "research" } }'| Field | Type | Description |
|---|---|---|
labels | object | Replaces the full label set. {} removes all labels. A sandbox holds at most 100 labels. Each key and value is at most 512 bytes. |
Read the serial console log of a sandbox. For a running sandbox the log is read live from its runner; for a sandbox in any other state the persisted log is returned. macOS sandboxes do not serve serial console or entrypoint logs.
# Serial console log of the microVMdaytona sandbox logs {sandboxId}| Flag | Type | Description |
|---|---|---|
--follow | bool | Polls for new log output until interrupted. |
--entrypoint | bool | Reads the entrypoint logs instead of the serial console. |
# Serial console log of the microVMcurl 'https://api.daytona.com/sandboxes/{sandboxId}/console-logs' \ --header 'Authorization: Bearer YOUR_API_KEY'Entrypoint logs
Section titled “Entrypoint logs”Read the log of the sandbox entrypoint process as text. For a running sandbox the log is read live; for a sandbox in any other state the persisted log is returned. To page through the log or follow it, read the entrypoint process through the toolbox.
print(sandbox.entrypoint_logs())console.log(await sandbox.entrypointLogs())daytona sandbox logs {sandboxId} --entrypointcurl 'https://api.daytona.com/sandboxes/{sandboxId}/logs' \ --header 'Authorization: Bearer YOUR_API_KEY'Build logs
Section titled “Build logs”Read the output of the image build behind a sandbox. A sandbox has a build log when its image is built from a Dockerfile, including a build that failed before the sandbox was placed; a sandbox created from a registry image has none. When the sandbox reuses an image built earlier, the log is the output of that original build.
Following the log streams output while the build runs and ends when the build finishes. Stopping the reader does not stop the build. To read the log of an image without a sandbox, see image build logs.
from daytona import Daytonafrom daytona.v2 import ImageBuilder
daytona = Daytona()sandbox = daytona.v2.create_from_build(ImageBuilder.from_dockerfile("FROM python:3.12-slim"))
# Output retained so far, as textprint(sandbox.build_logs())
# Stream output until the build finishesresult = sandbox.follow_build_logs(on_logs=lambda chunk: print(chunk, end=""))print(result.status, result.reason)| Parameter | Type | Description |
|---|---|---|
on_logs | Callable[[str], None] | Receives each chunk of decoded output. This parameter is required. |
cursor | str | Resumes after the output a previous follow returned in cursor. |
timeout | float | Sets the follow limit in seconds. By default, there is no limit. |
The response has the following fields:
| Field | Description |
|---|---|
status | The status of the build. |
reason | The value is terminal, deadline, not_applicable, or resource_deleted. |
output_complete | Whether the build output is complete. |
cursor | Pass this value as cursor to resume after this output. |
build_logs()raisesNotFoundErrorwhen the sandbox has no image build.follow_build_logs()raisesBuildLogInterruptedErrorwhen the sandbox has no image build.BuildLogInterruptedErrorcarries the HTTP status and the last cursor.
import { Daytona } from '@daytona/sdk'import { ImageBuilder } from '@daytona/sdk/v2'
const daytona = new Daytona()const sandbox = await daytona.v2.createFromBuild(ImageBuilder.fromDockerfile('FROM python:3.12-slim'))
// Output retained so far, as textconsole.log(await sandbox.buildLogs())
// Stream output until the build finishesconst result = await sandbox.followBuildLogs({ onLogs: (chunk) => { process.stdout.write(chunk) } })console.log(result.status, result.reason)| Parameter | Type | Description |
|---|---|---|
onLogs | (chunk: string) => unknown | Required. Receives each chunk of decoded output. |
cursor | string | Resumes after the output a previous follow returned in cursor. |
timeout | number | Sets the follow limit in milliseconds, unlike the seconds that the Python SDK uses. By default, there is no limit. |
The response has the following fields:
| Field | Description |
|---|---|
status | The status of the build. |
reason | The value is terminal, deadline, not_applicable, or resource_deleted. |
outputComplete | Whether the build output is complete. |
cursor | Pass this value as cursor to resume after this output. |
buildLogs()throwsNotFoundErrorwhen the sandbox has no image build.followBuildLogs()throwsBuildLogInterruptedErrorwhen the sandbox has no image build.BuildLogInterruptedErrorcarries the HTTP status and the last cursor.
BUILD_LOGS_URL=$(curl 'https://api.daytona.com/sandboxes/{sandboxId}/build-logs-url' \ --header 'Authorization: Bearer YOUR_API_KEY' | jq -r '.url')
# Output retained so far, as textcurl "$BUILD_LOGS_URL?follow=false&format=text" \ --header 'Authorization: Bearer YOUR_API_KEY'
# Stream output until the build finishescurl --no-buffer "$BUILD_LOGS_URL?follow=true&format=sse" \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Accept: text/event-stream'- The first request returns
404when the sandbox has no image build. - A request returns
503when the build log is unavailable, including a build removed by garbage collection.