Run AI-generated code in Depot Sandboxes

Use the Sandbox SDK to run generated JavaScript in a separate microVM, collect bounded output, and clean up after each attempt.

Last updated 2026-11-07

An agent needs somewhere to run the code it writes. Depot Sandboxes gives each attempt a Linux microVM with its own filesystem and processes. Your application creates the environment, sends the files, runs the code, and gets the result back through the SDK. Depot manages the machines underneath it.

Here's how to run JavaScript generated by your agent through that loop. The controller runs on your machine or a trusted CI job. The generated module only runs inside the sandbox. We'll give the sandbox two vCPUs, 4 GB of memory, a ten-minute lifetime, and no organization cache credentials or tailnet access.

Prepare the execution image

Sandboxes boot Depot snapshot images. You build one in a Depot CI job: the job runs your setup steps on Depot's base image, then depot/snapshot-action captures the machine and pushes it to Depot Registry. A standard container image, such as one from docker build, won't boot in a sandbox even if you push it to Depot Registry.

Depot CI's default image already includes Node.js 22 at /usr/local/bin/node, so this job only confirms the runtime the controller will call before taking the snapshot. Save it as .depot/workflows/sandbox-image.yml in a trusted repository:

on:
  workflow_dispatch:

jobs:
  snapshot:
    runs-on: depot-ubuntu-24.04
    steps:
      - name: Confirm the runtime
        run: /usr/local/bin/node --version
      - uses: depot/snapshot-action@v1
        with:
          image: YOUR_ORG_ID.registry.depot.dev/sandbox/node-eval:v1

Merge the workflow into your default branch and run it once. depot-ubuntu-24.04 is an x86_64 sandbox, which matches the amd64 architecture the controller requests below.

Copy the snapshot's digest from the action's final Pushed to ... (sha256:...) log line, or from the tag's details in Registry. Set SANDBOX_IMAGE_REF to YOUR_ORG_ID.registry.depot.dev/sandbox/node-eval@sha256:... using that digest, without the https:// prefix the log line prints. The controller will start every attempt from this exact image. Run the workflow again when you change what the image contains, then update the digest after checking the new snapshot.

Keep generated install scripts out of this workflow. If a task needs extra dependencies, add reviewed install steps before the snapshot action or install them inside the disposable sandbox.

Set up the controller

In a separate controller directory, install the SDK. Use Node.js 22 or newer for this example:

npm init -y
npm install --save-exact @depot/sandbox@0.1.0-beta.7

Save the JavaScript you want to run as candidate.mjs in the controller directory. This example uploads one self-contained file from your agent's output. The controller reads it as data and only executes it inside the sandbox.

Run the code through the SDK

Load a Depot API token into DEPOT_TOKEN through your secret manager, and set DEPOT_ORG_ID to the organization that owns the image. Keep these values in the trusted controller environment. Don't pass them through the sandbox's env or embed them in the image.

Save this as evaluate.mjs:

import {open, rm, writeFile} from 'node:fs/promises'
import {createClient, Sandbox} from '@depot/sandbox'

const token = process.env.DEPOT_TOKEN
const orgID = process.env.DEPOT_ORG_ID
const imageRef = process.env.SANDBOX_IMAGE_REF
if (!token || !orgID || !imageRef) {
  throw new Error('Set DEPOT_TOKEN, DEPOT_ORG_ID, and SANDBOX_IMAGE_REF')
}

// Bound each input read, including if a file grows while being read.
/** @param {string} path */
async function readInput(path) {
  const file = await open(path, 'r')
  try {
    const buffer = Buffer.alloc(64 * 1024 + 1)
    let size = 0
    while (size < buffer.length) {
      const {bytesRead} = await file.read(buffer, size, buffer.length - size, size)
      if (bytesRead === 0) break
      size += bytesRead
    }
    if (size > 64 * 1024) throw new Error('Input exceeds 64 KiB')
    return buffer.subarray(0, size)
  } finally {
    await file.close()
  }
}

await rm('result.json', {force: true})
const candidate = await readInput('candidate.mjs')
const client = createClient({token, orgID})
const sandbox = await Sandbox.create(client, {
  runtime: {imageRef},
  architecture: 'amd64',
  resources: {vcpus: 2, memoryMb: 4096, diskGb: 100},
  timeoutMinutes: 10,
  disableTailnet: true,
  disableDepotCache: true,
})
console.log(`Created sandbox ${sandbox.sandboxId}`)

try {
  const fs = sandbox.fs()
  await fs.mkdir('/tmp/eval', {recursive: true})
  await fs.writeFile('/tmp/eval/candidate.mjs', candidate)

  const command = await sandbox.runCommand({
    cmd: '/usr/local/bin/node',
    args: ['/tmp/eval/candidate.mjs'],
    cwd: '/tmp/eval',
  })

  const chunks = []
  let capturedBytes = 0
  let truncated = false
  const outputLimit = 64 * 1024
  for await (const chunk of command.logs()) {
    const bytes = Buffer.from(chunk.data)
    const remaining = outputLimit - capturedBytes
    if (bytes.length > remaining) truncated = true
    const kept = bytes.subarray(0, remaining)
    chunks.push(kept)
    capturedBytes += kept.length
    if (truncated) break
  }
  if (truncated) throw new Error('Command exceeded the 64 KiB output limit')

  const finished = await command.wait()
  await writeFile(
    'result.json',
    JSON.stringify(
      {
        sandboxId: sandbox.sandboxId,
        exitCode: finished.exitCode,
        output: Buffer.concat(chunks).toString('utf8'),
      },
      null,
      2,
    ),
  )
  console.log(`Remote command exited with code ${finished.exitCode}; see result.json`)
  process.exitCode = finished.exitCode === 0 ? 0 : 1
} finally {
  try {
    await sandbox.kill()
  } catch {
    console.error(`Could not confirm cleanup of sandbox ${sandbox.sandboxId}; check its status in Depot`)
    process.exitCode = 1
  }
}

Run the controller:

node evaluate.mjs

Each run creates a fresh sandbox and writes the command's exit code and captured output to result.json. The controller exits unsuccessfully if the remote command fails.

The SDK executes the program directly with the supplied arguments. It doesn't interpolate the generated source into a shell command, and it doesn't search PATH, so cmd must be an absolute path. Output stays in a JSON file instead of being printed raw into CI logs, where text from a program could be interpreted as workflow commands or terminal escape sequences.

A command that exits with a nonzero code still produces a result. A stream error, excessive output, or SDK failure fails the controller and reaches cleanup. The sandbox's lifetime limit starts at creation, including provisioning. It also limits the run if the controller process is interrupted before finally executes. A cleanup failure is reported because it may need attention; a sandbox that already expired can also reject kill().

Keep the controller separate from the candidate

Run evaluate.mjs from a trusted checkout or service. An untrusted pull request mustn't be able to replace the controller, change its dependency lockfile, choose the execution image, or run a setup script alongside the Depot token. Send only the candidate files into the remote environment. The sandbox doesn't protect a controller that executes the candidate locally first.

This example disables automatic Depot Cache configuration because it otherwise supplies organization cache credentials. It also disables joining the organization's Tailscale network and doesn't select any CI secrets. disableTailnet doesn't disable public internet access. If your task needs restricted egress, enforce and verify that policy separately before giving it sensitive data.

Treat result.json as untrusted feedback. A program controls its output and can tamper with files in its execution environment. The result helps an agent decide what to try next; it isn't permission to publish a package, deploy to production, or execute returned commands in the controller. Keep those decisions in your existing review and deployment process.

For real repositories, give each attempt a fresh environment and only the files and credentials it needs. Avoid restoring a workspace containing secrets into an untrusted run. Once the basic loop works, the Sandbox SDK reference covers filesystem operations, snapshots, resources, and lifecycle control.

If you want Depot to manage the coding agent and its session as well as the execution environment, start with Managed Agents. The SDK is for applications that need to control that loop themselves.

Start building with Depot
in minutes