Remote Sandboxes for a hosted product
Remote Sandboxes run the code your product executes for a customer, such as an agent's tool calls or a user's script, inside sandboxes in the customer's own cloud: isolated environments, such as microVMs, next to their data. Your product stays hosted; only the sandboxes run there.
It's the hosted-product counterpart of Deployment UX: instead of deploying your software, the customer's admin sets up sandboxes in one account.
If you already deploy into the customer's cloud, declare a Sandbox resource in alien.ts instead.
Sequence diagram. Lanes: Your backend (your cloud), Sandbox Gateway (sandbox.alien.dev), Customer cloud (sandboxes). Steps: 1 · exec + external ID; 2 · run in sandbox; 3 · stdout, stderr, exit; 4 · streamed frames.
Your backend creates sandboxes and runs commands through the Sandbox Gateway, an API Alien hosts at https://sandbox.alien.dev. It names the customer by your external ID, your own stable ID for them.
The code runs in the customer's account, and the sandbox's egress policy controls its network access. Commands, their output, and file contents pass through the gateway on the way between your backend and the sandbox.
Enable the gateway
This happens once. In the dashboard, open Remote Sandboxes, click Enable, then create a server API key. From the CLI:
alien projects capabilities enable remote-sandbox --max-session-lifetime-seconds 3600
alien api-keys create --for remote-sandbox --description productionSave the key as ALIEN_SANDBOX_API_KEY on your backend.
Creating setup links and reading connection status need your regular Alien API key (ALIEN_API_KEY), used by the client from Set up a client.
--max-session-lifetime-seconds caps one sandbox session, up to 8 hours (28800). It defaults to the saved value, or 3600.
Sandbox image
Without an image, sandboxes use Alien's default image for each cloud. To use your own, pass one of these, not both:
alien projects capabilities enable remote-sandbox --image public.ecr.aws/acme/analysis:v1
alien projects capabilities enable remote-sandbox --src ./sandbox--imagetakes a public image. Itslinux/arm64variant runs on AWS, and alinux/amd64variant also runs on Azure.- For Google Cloud, Alien adds the sandbox agent to a copy of the
linux/amd64variant. It skips that copy when the project's default Google Cloud manager is a private manager, or when the image's layers total more than 512 MiB compressed. For those cases, and for a private image, use--srcwithFROM <image>. --srctakes a folder with a Dockerfile (--dockerfilefor another name). The CLI builds it locally with Docker and pushes it to the project's private repository on each cloud.- The
linux/arm64build for AWS is required. Azure and Google Cloud get alinux/amd64build when the project has a manager on that cloud and the base image has that architecture. See Images for one Dockerfile that builds for both. - An unchanged
--srcfolder reuses the image already pushed. Pass--rebuildto build and push anyway.
The command prints Skipped <cloud>: <reason> for each cloud the image doesn't reach, such as a cloud with no manager, or a Google Cloud copy still building. --json output leaves these lines out. Run the command without either flag to keep the saved image and change only the lifetime.
Add a Connect sandboxes button
This runs on your server, in the route your Connect sandboxes button calls. Create a setup link with the sandbox item and send the customer to it.
The link opens the deployment portal, where their admin runs initial setup. The request is POST /v1/deployment-groups/setup-links:
import { alien, PROJECT } from "./alien"
export async function connectSandboxLink(customer: { id: string; slug: string }) {
const link = await alien.setupLinks.create({
project: PROJECT,
externalId: customer.id,
name: customer.slug,
setupItems: [{ item: "sandbox", required: true }],
})
return link.deploymentLink
}In the portal, the customer sees Sandbox: "Run the code this product executes on your behalf inside your own cloud." Their admin runs initial setup in their account, which creates the sandbox environment: microVMs on AWS, and the cloud's sandbox service on Google Cloud and Azure.
Create a sandbox
This runs on your server, when your product first needs to run code for the customer. Call POST /v1/sandboxes on the gateway with the sandbox key and the customer's external ID:
export const SANDBOX_API = "https://sandbox.alien.dev/v1"
export function headers(customerId: string) {
return {
Authorization: `Bearer ${process.env.ALIEN_SANDBOX_API_KEY}`,
"X-Alien-External-ID": customerId,
"Content-Type": "application/json",
}
}
export async function createSandbox(customerId: string) {
const res = await fetch(`${SANDBOX_API}/sandboxes`, {
method: "POST",
headers: headers(customerId),
body: JSON.stringify({}),
})
if (!res.ok) throw new Error(`create failed: ${res.status} ${await res.text()}`)
const sandbox = (await res.json()) as { sandboxId: string; state: string }
return sandbox.sandboxId // persist it before you run anything
}The body accepts egress, env, and timeoutMs (the sandbox's lifetime). Persist the returned sandboxId: every later call uses it, and a new process can reconnect with it.
Run a command
This runs on your server, each time your product executes code. POST /v1/sandboxes/{id}/exec streams newline-delimited JSON frames. stdout and stderr frames carry base64 data, and the stream ends with one exit or error frame:
import { SANDBOX_API, headers } from "./sandbox"
type Frame =
| { type: "stdout" | "stderr"; seq: number; data: string } // data is base64
| { type: "exit"; code: number; truncated: boolean }
| { type: "error"; code: string; message: string; retryable: boolean }
export async function run(customerId: string, sandboxId: string, script: string) {
const res = await fetch(`${SANDBOX_API}/sandboxes/${sandboxId}/exec`, {
method: "POST",
headers: headers(customerId),
body: JSON.stringify({ command: "python3", args: ["-c", script], timeoutMs: 30_000 }),
})
if (!res.ok) throw new Error(`exec failed: ${res.status}`)
let stdout = ""
for (const line of (await res.text()).split("\n")) {
if (!line.trim()) continue // keepalive
const frame = JSON.parse(line) as Frame
if (frame.type === "stdout") stdout += Buffer.from(frame.data, "base64").toString()
if (frame.type === "exit") return { code: frame.code, stdout }
if (frame.type === "error") throw new Error(`${frame.code}: ${frame.message}`)
}
throw new Error("SANDBOX_OUTCOME_UNKNOWN: do not repeat the command")
}timeoutMs is required on every command. A stream that ends without an exit or error frame means the outcome is unknown: the command may have run, so don't repeat it automatically. For commands that outlive one request, start a job instead.
API
All paths are under https://sandbox.alien.dev/v1 and take Authorization: Bearer $ALIEN_SANDBOX_API_KEY plus X-Alien-External-ID.
| Method | Path | Purpose |
|---|---|---|
GET | /capabilities | Operations new sandboxes support |
POST, GET | /sandboxes | Create; list |
GET, PUT, DELETE | /sandboxes/{id} | Get; get or create; terminate |
POST | /sandboxes/{id}/exec | Run a command, streamed |
POST | /sandboxes/{id}/jobs | Start a long-running command |
GET, DELETE | /sandboxes/{id}/jobs/{jobId} | Read output (?sinceSeq=); cancel |
GET | /sandboxes/{id}/files?path= | Read a file |
POST, PUT | /sandboxes/{id}/files | Write files |
POST | /sandboxes/{id}/pause, /resume, /snapshot | Pause, resume, snapshot |
Limits: 64 KiB JSON request bodies, 32 MiB per file, 64 MiB per write, and 24 hours per job. Listing isn't available on AWS or Azure, so keep the IDs you create. Use /capabilities to check what a backend supports before you rely on jobs, files, or pause.
Show the connection
This runs on your server, when your settings page loads. Read capabilities.remoteSandbox from the capability overview, GET /v1/projects/{idOrName}/project-capabilities/overview, with the capability helper from Customer-managed keys:
import { capability } from "./capability"
export async function sandboxStatus(customerId: string) {
const sandbox = await capability(customerId, "remoteSandbox")
return { state: sandbox?.state ?? "not-connected", update: sandbox?.sandboxUpdate }
}state is not-connected, setting-up, connected, needs-attention, or revoked. sandboxUpdate says whether the customer's sandbox runs your current image: up-to-date, behind, blocked, update-failed, bundle-failed, or platform-removed.
Test it
Try it as your own test customer before you ship:
alien onboard "Test customer" --external-id org_test --setup-items sandbox
export CUSTOMER_ID=org_test
alien examples remote-sandbox --operation create
alien examples remote-sandbox --operation exec --command 'python3 --version'
alien logs --source remote-sandbox --since 1hOpen the printed link and run initial setup in a cloud account you control. alien examples prints a ready-to-run curl request for each operation.
Then create a sandbox from your backend, run a command, and confirm the output and exit code reach your product. See Sandbox for the Sandbox resource you can also declare in your own stack.
Next
- Customer buckets: give sandboxes data to work on
- Brand the portal: white-label the portal the customer sees
- Sandbox: sandboxes inside a deployment