Docs

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 production

Save 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
  • --image takes a public image. Its linux/arm64 variant runs on AWS, and a linux/amd64 variant also runs on Azure.
  • For Google Cloud, Alien adds the sandbox agent to a copy of the linux/amd64 variant. 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 --src with FROM <image>.
  • --src takes a folder with a Dockerfile (--dockerfile for another name). The CLI builds it locally with Docker and pushes it to the project's private repository on each cloud.
  • The linux/arm64 build for AWS is required. Azure and Google Cloud get a linux/amd64 build 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 --src folder reuses the image already pushed. Pass --rebuild to 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:

lib/connect-sandbox.ts
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:

lib/sandbox.ts
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:

lib/sandbox-exec.ts
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.

MethodPathPurpose
GET/capabilitiesOperations new sandboxes support
POST, GET/sandboxesCreate; list
GET, PUT, DELETE/sandboxes/{id}Get; get or create; terminate
POST/sandboxes/{id}/execRun a command, streamed
POST/sandboxes/{id}/jobsStart 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}/filesWrite files
POST/sandboxes/{id}/pause, /resume, /snapshotPause, 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:

lib/sandbox-status.ts
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 1h

Open 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

On this page