Cua Docs

Create a sandbox pool with TypeScript

Create a reusable Fleet pool and capture a sandbox screenshot from browser or Node.js TypeScript.

Use @trycua/fleet to create a reusable sandbox pool from TypeScript. This guide creates a one-replica Linux pool, claims a sandbox, captures a screenshot, and releases the claim while leaving the pool warm.

The package provides separate runtime entry points:

  • @trycua/fleet/browser loads the Fleet WebAssembly module in a browser.
  • @trycua/fleet/node loads the same WebAssembly module from disk and provides a fetch-based HTTP transport.

Prerequisites#

  • Node.js 20 or newer.
  • A run.cua.ai access token with permission to manage sandbox pools.
  • A globally unique, lowercase DNS-label pool name.

This guide uses the public Linux computer-server image:

public.ecr.aws/k5j5w0x5/cua-ubuntu-24.04@sha256:82702ebdd32d1f8fc05f2ea409a7c67d0ba9f8f8e4e9f1a89ce40989d5f4475d

Browser code can read every VITE_* value bundled into the application. Use the browser example only in a trusted local or internal application with a short-lived, narrowly scoped token. Keep long-lived credentials in Node.js or exchange them for a short-lived token on your backend.

Choose a runtime#

Install the Fleet lifecycle SDK and a TypeScript runner:

npm install @trycua/fleet
npm install --save-dev tsx typescript

Set the token and a unique pool name:

export CUA_FLEET_ACCESS_TOKEN="<your-access-token>"
export CUA_POOL_NAME="my-team-js-pool"

Save this script as create-pool.ts:

create-pool.ts
import { writeFile } from 'node:fs/promises';
import {
  CreateClaimRequestBuilder,
  CreatePoolRequestBuilder,
  CreateTemplateRequestBuilder,
  CyclopsTokenProviderConfigurationBuilder,
  OsGymSandboxTemplateSpecBuilder,
  OsGymSandboxWarmPoolSpecBuilder,
  SandboxServiceBuilder,
  SandboxTemplateRefBuilder,
  VmTemplateBuilder,
  createFleetClient,
  type Claim,
} from '@trycua/fleet/node';
 
const IMAGE =
  'public.ecr.aws/k5j5w0x5/cua-ubuntu-24.04' +
  '@sha256:82702ebdd32d1f8fc05f2ea409a7c67d0ba9f8f8e4e9f1a89ce40989d5f4475d';
const BASE_URL = process.env.CUA_FLEET_BASE_URL ?? 'https://run.cua.ai';
const ACCESS_TOKEN = requiredEnv('CUA_FLEET_ACCESS_TOKEN');
const POOL_NAME = requiredEnv('CUA_POOL_NAME');
const TEMPLATE_NAME = `${POOL_NAME}-template`;
 
function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing environment variable: ${name}`);
  return value;
}
 
function screenshotBase64(responseBody: ArrayBuffer): string {
  const text = new TextDecoder().decode(responseBody);
  const dataLine = text.split('\n').find((line) => line.startsWith('data: '));
  if (!dataLine) throw new Error(`No screenshot data frame: ${text.slice(0, 200)}`);
 
  const payload = JSON.parse(dataLine.slice(6));
  if (!payload.success) throw new Error(payload.error ?? 'Screenshot failed');
 
  const encoded = payload.image_data ?? payload.result?.image_data;
  if (typeof encoded !== 'string') throw new Error('Screenshot response has no image data');
  return encoded;
}
 
const configuration = new CyclopsTokenProviderConfigurationBuilder()
  .baseUrl(BASE_URL)
  .poolPollIntervalMs(5_000n)
  .poolPollLimit(120)
  .claimPollIntervalMs(5_000n)
  .claimPollLimit(120)
  .build();
const client = await createFleetClient(configuration, ACCESS_TOKEN);
 
let claim: Claim | undefined;
try {
  const service = new SandboxServiceBuilder().name('server').targetPort(8000).build();
  const vm = new VmTemplateBuilder()
    .containerDiskImage(IMAGE)
    .cpuCores(4)
    .memory('4Gi')
    .services([service])
    .build();
  const templateSpec = new OsGymSandboxTemplateSpecBuilder().vmTemplate(vm).build();
  const templateRef = new SandboxTemplateRefBuilder().name(TEMPLATE_NAME).build();
  const poolSpec = new OsGymSandboxWarmPoolSpecBuilder()
    .replicas(1)
    .sandboxTemplateRef(templateRef)
    .build();
 
  const pool = await client.createPool(
    new CreatePoolRequestBuilder().namespace(POOL_NAME).spec(poolSpec).build(),
  );
  const template = await client.createTemplate(
    new CreateTemplateRequestBuilder()
      .namespace(POOL_NAME)
      .name(TEMPLATE_NAME)
      .spec(templateSpec)
      .build(),
  );
 
  console.log(`Created pool ${pool.metadata.name}`);
  console.log(`Created template ${template.metadata.name}`);
 
  claim = await client.createClaim(
    new CreateClaimRequestBuilder().pool(pool).build(),
  );
  const sandbox = await client.waitClaim(claim);
 
  const requestBody = await new Blob([
    JSON.stringify({ command: 'screenshot' }),
  ]).arrayBuffer();
  const response = await client.serviceRequest(sandbox, 'server', '/cmd', {
    method: 'POST',
    url: 'https://service.invalid/cmd',
    headers: [{ name: 'content-type', value: 'application/json' }],
    body: requestBody,
    timeoutSecs: 60n,
  });
  if (response.status < 200 || response.status >= 300) {
    throw new Error(`Screenshot request failed with HTTP ${response.status}`);
  }
 
  await writeFile('sandbox.png', Buffer.from(screenshotBase64(response.body), 'base64'));
  console.log('Wrote sandbox.png');
} finally {
  try {
    // Release the claim but leave the reusable pool warm.
    if (claim) await client.deleteClaim(claim);
  } finally {
    client.uniffiDestroy();
  }
}

Run it:

npx tsx create-pool.ts

The script writes sandbox.png in the current directory. The Fleet client creates and waits for the claim, routes POST /cmd through the authenticated server service proxy, and deletes the claim before closing.

Keep or delete the pool#

Both examples release only the claim, so the one-replica pool remains warm for the next task. This is the normal reusable-pool lifecycle.

The low-level Fleet SDK creates resources rather than reconciling them. Reusing the same pool name in the creation script returns an already-exists error. To reuse the pool, skip the creation calls and load it with:

const pool = await client.getPool(POOL_NAME);

To remove the pool permanently, retain the pool and template values and run the following after every claim has been released:

await client.deletePool(pool);
await client.deleteTemplate(template);

Pool names are globally unique across Cua accounts. If creation returns HTTP 403, choose another pool name; changing the claim name does not resolve a pool namespace conflict.

Use autoscaling instead of one replica#

Replace .replicas(1) with an autoscaling policy and omit replicas:

import { WarmPoolAutoscalingBuilder } from '@trycua/fleet/node';
 
const autoscaling = new WarmPoolAutoscalingBuilder()
  .minPoolSize(0)
  .initialPoolSize(2)
  .maxPoolSize(10)
  .build();
 
const poolSpec = new OsGymSandboxWarmPoolSpecBuilder()
  .autoscaling(autoscaling)
  .sandboxTemplateRef(templateRef)
  .build();

Use the matching @trycua/fleet/browser import in browser code. With a minimum size of zero, the first claim after an idle period cold-starts a sandbox.