#Studio
Studio is the browser view of your sandboxes: launch a command or an agent,
watch its output, type into its terminal, read its files and its audit
events. It is a client of the same API as the CLI and the SDKs, served by
sandbox-cli itself on a loopback port, and it talks to whichever endpoint
your context points at — a sandboxd on your Mac, on your Linux machine, or
a gateway in front of many.
sandbox-cli studio
# Studio: http://127.0.0.1:7080/#token=cb0fbb4f…
# studio: context local · Ctrl-C to stop
Open the address it prints. --context picks another endpoint, --port
another port. Closing Studio leaves every sandbox it started running; the
next Studio finds them again.
This page is the user's guide. Working on Studio itself — building it, its tests, how its routes are laid out — is studio/README.md.
#Screens
Nothing in Studio is a second implementation of the CLI: every screen is an API call or the same host-side code the CLI runs, so a rule the CLI keeps, Studio keeps.
| Screen | What it does |
|---|---|
| Sandboxes (home) | Every sandbox on the endpoint — started from Studio, the CLI or an SDK — each with what its agent is doing (working, waiting for you, idle) where it runs one, under a strip of how many are running and the vCPU, memory and disk given to them, against the machine's capacity where the endpoint reports it (a plain sandboxd does; a gateway gives totals). Searched, filtered by state, sorted, paged; each row shows what the sandbox was given — a click there opens its CPU and memory over the last hour, where the endpoint measures them —, a terminal button and a menu (open, open as a page, copy the id, suspend, terminate); select several to terminate them at once. Terminating, from the list or a sandbox's panel, opens a dialog listing what goes and asks for the sandbox's name (or terminate N for several) to be typed first. With none yet, a first-run panel with the code to start one. |
| A sandbox | A click on a row opens it in a panel beside the list: its up and down buttons step through the list, and it widens to two panes or opens as a page of its own (/sandbox?id=). Its overview — id, image, network, resources, lifecycle, labels, environment names, volumes, processes —; a real terminal (Terminal goes back to the shell open there, or starts one as sandbox-cli shell does; the tab lists every process with a terminal, an agent's console included, to attach to, and New shell starts another); a Desktop tab, where the sandbox's image has one (desktop.md): its screen, with a terminal and a browser, to see and use; its processes' logs from the first byte; its files; and its audit events. Wide, the overview stays beside the other tabs. A live sandbox is edited from its overview: name, labels and idle auto-stop in one dialog; its network (none, an allowlist from the groups and hosts, or open) where the endpoint can change a running sandbox's; and Resize, which replaces it with a copy at a template's size — its files, name, labels and network come across, its processes, memory and environment do not — where the endpoint takes disk snapshots (macOS; a Firecracker sandbox's size is fixed while it runs). |
| Playground | A command, an agent run unattended, or an agent's interactive console, in a fresh sandbox that starts in its own home directory, from the server's image, one you name (suggested: those the machine has built, those its sandboxes run, the desktop image) or a snapshot where the backend takes them, with a snapshot schedule if asked for, at a size from Templates — with the same config, profile, network policy, labels, volumes and agent login a sandbox-cli run would get. Beside the form, the same run written as CLI, curl, Python and TypeScript, to repeat it from a script. |
| Snapshots | Sandboxes captured to start new ones from — whole (memory, processes and disk) where the backend can, their files only where it cannot capture more (macOS) — with their kind and size; delete one. A running sandbox's panel takes one with Snapshot, and its overview's Snapshots section sets a schedule — every so often, the newest few kept, within the server's limits — and lists that sandbox's snapshots, scheduled or manual. |
| Agents | The agents Studio runs, each with a verified headless mode, one row each: whose login is saved, which API keys are set or saved, and the API it reaches. Open a row to add, change or remove an API key for any variable the agent reads. A key is write-only: it is kept in ~/.config/sandbox/agent-keys.json (yours only, 0600), never shown again, and used by an agent run — Studio's or the CLI's — when the environment does not set that variable; the environment's value wins. |
| Templates | Sizes to launch at — vCPUs, memory and disk — by name: micro, small, medium, large and xlarge built in, and any of your own, made, edited and deleted here (a built-in one is copied, not changed). The Playground's Size step offers them, and a launch sends the size as --cpus, --memory and --disk would; the endpoint's limits still apply, and a template above them is marked and refused. Kept in ~/.config/sandbox/studio.json, and read by the CLI too: sandbox-cli run --template large -- make (or agent claude --template medium) launches at the same size, with --cpus, --memory or --disk beside it overriding that field; sandbox-cli template ls lists them. |
| Images | On a plain sandboxd: the images sandboxes start from, each with its state, size, how many sandboxes use it and when it was installed; Download image installs one ahead of the first sandbox that wants it, with its progress; Start opens the Playground with it; Remove frees one nothing uses (the default and pooled images are locked); a failed download shows why, with Retry and Clear. Not offered through a gateway. |
| Volumes | Named volumes and where each is mounted. |
| Settings | The context, what its endpoint can deliver, allowlist groups and deny rules. A group is a named set of hosts — go, npm, an internal registry — edited in place; the Playground's Network step offers none, open, or an allowlist made of the groups you pick (those marked default are picked to start with, and are what a launch on an endpoint whose default is an allowlist gets), plus one-off hosts, with or without the built-in agents' APIs and registries (--no-baseline). An agent run always reaches its own API. Deny rules, each with a switch, apply to every launch with a network. The endpoint's policy still decides — a host it does not let a request add is refused at launch — and the Playground's code shows it all as --allow, --deny and --no-baseline. Kept in ~/.config/sandbox/studio.json; for rules every sandbox-cli run gets, use network.allow in ~/.config/sandbox/config.yaml. |
#A plain sandboxd, or a gateway
On load Studio asks GET /v1/whoami. A plain sandboxd answers 404 and
gets exactly the screens above. A gateway (fleet.md) answers with
the API key's user, tenant and scopes, and those decide the rest:
sandbox-cli studio --context fleet
sandbox-cli studio holds the API key and adds it to each call it proxies;
the browser never sees the key. It is the same Studio either way.
#Scopes and screens
| The key holds | Studio adds |
|---|---|
| any scope | Jobs (list, detail with each run's kept output and files), Services (list, detail with replicas, health and rollout), Secrets (names only), SSH (where to connect, the host key to pin), Account (user, tenant, current organisation, key id, scopes), the organisation switcher at the top of the sidebar and Members (list; add, remove and change roles as an owner) |
org:create |
Create organization in the switcher |
sandbox:create |
the Playground, submitting and cancelling jobs, deploying (a JSON spec) and scaling services, creating volumes, a sandbox's Terminal, Suspend and Snapshot |
sandbox:delete |
terminating sandboxes, deleting volumes and snapshots; with sandbox:create, removing a service |
sandbox:ssh |
adding and removing your SSH keys, issuing a short-lived access token for a sandbox (shown once) |
secrets:write |
setting and removing secrets. A value goes in a password field and is never shown: no call returns it |
admin |
everything above, plus Nodes (health, allocated capacity, cordon, uncordon, drain, add, remove), Lost sandboxes, Users & keys (issue and revoke API keys, any user's SSH keys), Organizations (every organisation, with its members and owners) and Audit |
An action the key's scopes do not allow is not offered, rather than offered
and refused. A screen the key may not have — an admin screen for a tenant's
key, or a gateway screen on a plain sandboxd — is missing from the sidebar
and the palette, and a typed URL shows a plain Not available page that makes
no request for it. That is a convenience, not the control: the gateway refuses
a call without its scope (403) whoever sends it.
What each gateway screen is about is on its own page: jobs and secrets, services, SSH, and for an admin, operations (nodes, drain, lost sandboxes, keys, the audit log).
#Organizations
On a gateway the top of the sidebar is the organisation switcher: the key's
own tenant and every organisation its user belongs to,
Create organization with org:create, and Members. The switcher
keeps its choice per browser and sends X-Sandbox-Org on every call Studio
makes, a terminal's included; switching clears what was loaded, so nothing of
the previous organisation stays on screen. If the remembered one is no longer
allowed — you were removed — Studio goes back to your key's own tenant and
says so. With nothing chosen in this browser, Studio starts in the context's
organisation (sandbox-cli org use). On a plain sandboxd there is no
switcher and Studio never asks for /v1/orgs.
#What Studio never shows
- A node's endpoint, which is dropped as the node list is read (and taken out of a node's error text).
- A secret value. No call returns one, and neither does any call return an agent's saved API key.
- A credential twice. A new API key's secret is in one answer only; Studio shows it once, with a copy button and a warning, and drops it when you click Done. An SSH access token is shown the same way.
#A hosted dashboard without the admin screens
Building Studio with NEXT_PUBLIC_STUDIO_ADMIN=off leaves the admin screens
out of the bundle altogether — their pages are not routes in that build and
nothing they import is included — so a Studio served to many tenants does not
carry the operator's UI at all:
NEXT_PUBLIC_STUDIO_ADMIN=off make studio build # a sandbox-cli whose Studio has no admin screens
The default build keeps them, for an operator running their own gateway.
npm run check:admin-off in studio/ (part of npm run check) makes such a
build and fails if any admin route, admin API path or admin screen title is
in it. The gateway's refusal stays the control; the build only means the
screens are not shipped.
#Hosted Studio
sandbox-cli studio host serves Studio on a public address to a gateway's
users. Nobody installs anything: each user opens an invite link, and from then
on acts as themselves, seeing only what their key may see.
# the UI for it: invite-link sign-in, no Studio token, no admin screens
make studio-hosted # writes studio/out-hosted
sandbox-cli studio host \
--gateway http://127.0.0.1:8443 \
--public-url https://studio.example.com \
--listen 127.0.0.1:7080 \
--ui-dir studio/out-hosted
Put a TLS proxy in front of the loopback port (a reverse proxy, or a tunnel
that terminates HTTPS), or give it --tls-cert and --tls-key and listen on
a public address itself.
Inviting a user is issuing them a key with --invite-url, which prints
the link that signs them in:
sudo -u sandbox-gateway sandbox-gateway --state … keys create --user alice --tenant alice \
--scope sandbox:read --scope sandbox:create --scope sandbox:delete \
--invite-url https://studio.example.com
# …
# invite: https://studio.example.com/#key=sgk_…
Give each user their own tenant: users without one share one quota and one set of secrets. The link is the key — send it as you would a password.
How a user signs in. The key rides in the link's fragment, which a browser
never sends to a server. The page posts it once to POST /api/session;
Studio asks the gateway who the key is (GET /v1/whoami), keeps the key in
memory and answers with an HttpOnly, SameSite=Strict session cookie
(__Host-sbx_studio over https). The key is then gone from the address bar
and from the page. Opening the link again, in any browser, signs in again.
| Every call | made with the signed-in user's key, so the gateway's ownership, tenants, quotas and scopes apply exactly as to their own CLI. Studio adds no isolation of its own and needs none. |
| What it holds | no credential of its own: no context, no key file. It reads nothing of the machine it runs on: no agent logins, no environment, no config. |
| Agents | an interactive agent logs in inside its sandbox; an unattended one runs as a job, with its API key stored under Secrets. The Playground offers commands and interactive agents. |
| Studio's settings | the host user's own — saved agent keys, saved templates, egress rules and allowlist groups — are not shown, not applied to hosted launches and cannot be changed: their routes do not exist when hosted. Users get the built-in sizes (micro to xlarge), name hosts to allow themselves, and keep agent keys under Secrets. |
| Admin keys | refused. An operator uses sandbox-cli studio on their own machine. |
| Sessions | end after --session-idle unused (12h) or --session-max-age (7 days), on Sign out, and at the next call after the key is revoked at the gateway. They are kept in memory: a restart signs everyone out. |
| Refused | any Host but --public-url's (and --allowed-host, for a proxy that rewrites it); any request that changes something, and every WebSocket, from another Origin; every /api call without a session — local Studio's token and a gateway key presented directly included; more than 5 failed sign-ins a minute. |
sandbox-cli studio host refuses plain http:// for the gateway or the public
URL anywhere but loopback, and a non-loopback --listen without a
certificate. npm run check:hosted in studio/ fails if the hosted build
carries the admin screens or local Studio's token handling.
Not yet: a sign-in form or single sign-on (the invite link is the sign-in), sessions that survive a restart, an agent's login kept between console runs, and counting failed sign-ins per client (behind a proxy every request comes from the proxy, so there is one count for everyone).
#How it is served, and who may use it
Studio is a static export embedded in sandbox-cli. sandbox-cli studio
serves it on a loopback port together with a small API, from the same
origin: the endpoint's API proxied to the current context, a WebSocket bridge
to a process's terminal, and launching a run through the same code the CLI
uses.
Anything that can reach Studio's API can start sandboxes, and agents with your saved logins. These are the three reasons a web page you happen to have open cannot:
- Loopback only, and a loopback Host. Studio listens on 127.0.0.1 and
answers only
Hostheaders naming loopback, so a page whose own name resolves to 127.0.0.1 — DNS rebinding — is refused: the name it dialled gives it away. - A token per launch. A loopback port is reachable by every user on the
machine. Every request to Studio's API needs the token in the address
sandbox-cli studioprints; it travels in the URL's fragment, which is never sent to a server, is kept in the tab'ssessionStorageonly, and is wiped from the address bar. - The browser never holds the endpoint's credential. Studio proxies
sandbox calls to the context's
sandboxdor gateway and adds that token or API key itself. A cross-origin request is refused outright, and a body that is not JSON — the shape of a request that skips a browser's preflight — is refused too.
#Checking it works
Studio's end-to-end suite runs the real sandbox-cli studio in front of a
sandboxd and a real gateway on the in-memory backend. What it cannot prove
— a job's kept file from a real guest, an SSH token logging in, a real
drain — is row 44 of testing/end-to-end.md.