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/browserloads the Fleet WebAssembly module in a browser.@trycua/fleet/nodeloads 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:82702ebdd32d1f8fc05f2ea409a7c67d0ba9f8f8e4e9f1a89ce40989d5f4475dBrowser 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 typescriptSet 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:
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.tsThe 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.