mybasev1.0.21

Sandbox client

Run untrusted code, automate Chromium, process files, and export private artifacts.

Create temporary isolated Node.js + Chromium workspaces from your app's server. Use them to run code you don't trust (user scripts, exercises, plugins), automate a browser, render screenshots and PDFs, process uploaded files, or give an AI agent a code interpreter and a browser. mybase supplies the compute and lifecycle; your app decides what runs. The SDK has no Playwright or model-provider dependency. Work submitted here appears in the project's Sandboxes → Runs dashboard alongside CLI and dashboard work.

Connect and create

On the Linux Docker host, first run mybase server sandbox --project myapp to prepare the worker and enable the project. Install the client and create a project-limited API key:

npm install @mybase-dev/sdk
mybase token create myapp-sandbox --project myapp --perm sandbox:write

Keep the key in your server's MYBASE_API_KEY environment variable. Never pass it or any other credential into the guest. The simplest use runs a script and reads its output:

import { createSandboxClient } from "@mybase-dev/sdk/sandbox";
const sandboxes = createSandboxClient({ apiUrl: "https://mybase.example.com", project: "myapp", apiKey: process.env.MYBASE_API_KEY! });
const box = await sandboxes.create({ key: crypto.randomUUID(), ttlSeconds: 120 });
try {
  const ready = await sandboxes.wait(box.id, AbortSignal.timeout(100_000));
  if (ready.state !== "ready") throw new Error(ready.reason ?? ready.state);
  const run = await sandboxes.execute(box.id, { key: "run", code: userCode, timeoutMs: 10_000 });
  const result = await sandboxes.waitExecution(box.id, run.id, AbortSignal.timeout(30_000));
  console.log(result.state, result.exitCode, result.stdout, result.stderr);
} finally { await sandboxes.terminate(box.id); }

The same workspace has a headless Chromium. This visits a page, exports a screenshot, and requests termination in finally:

const box = await sandboxes.create({ key: crypto.randomUUID(), ttlSeconds: 180 });
try {
  const ready = await sandboxes.wait(box.id, AbortSignal.timeout(100_000));
  if (ready.state !== "ready") throw new Error(ready.reason ?? ready.state);
  const run = await sandboxes.execute(box.id, {
    key: "inspect",
    code: `import { chromium } from 'playwright';
      const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
      const page = await browser.contexts()[0].newPage();
      await page.goto('https://example.com');
      console.log(await page.locator('body').innerText());
      await page.screenshot({path:'page.png'});
      await page.close(); await browser.close();`,
    timeoutMs: 30000,
  });
  const result = await sandboxes.waitExecution(box.id, run.id, AbortSignal.timeout(60_000));
  if (result.state !== "success") throw new Error(result.stderr || result.state);
  const artifact = await sandboxes.exportArtifact(box.id, "page.png", "image/png");
  const response = await sandboxes.artifact(box.id, artifact.id); // streamed binary Response
  // Stream response.body to your app's storage, or read these bounded bytes:
  const screenshot = new Uint8Array(await response.arrayBuffer());
} finally { await sandboxes.terminate(box.id); }

key is a required idempotency key, unique within a project for creation and within a sandbox for execution. Use 1–80 letters, digits, underscores, or hyphens. Retrying the same key with the same normalized input returns the same ID; changing its input returns 409. Retain keys across network retries instead of generating a fresh key each attempt. Keys/metadata are retained seven days.

Lifecycle and execution results

create and execute return promptly with durable queued records. wait polls until the sandbox leaves Queued/Starting; check for Ready, since provisioning may fail. waitExecution polls until an execution is terminal; check for Success rather than assuming the poll succeeded because it returned.

OptionDefaultRange
profile"browser"Only supported profile: Node.js 24, Playwright, and headless Chromium
ttlSecondsnull (no expiry)Optional positive integer seconds, starts at admission
idleSecondsnull (no idle expiry)Optional positive integer seconds
Execution timeoutMsnull (no timeout)Optional positive integer milliseconds
Client request timeoutMs30000Per HTTP request; does not set execution lifetime

Omitting a deadline or passing null disables it. Explicit deadlines have no fixed policy ceiling. Terminate sandboxes explicitly when finished. get(id) returns lifecycle state, reason, creation/start/finish/deadline timestamps (expiresAt: null without a lifetime), resource allocation, elapsed runtime, execution count, and exported bytes. list() returns the latest 100 sandboxes, newest first. Sandbox states are queued, provisioning, ready, stopping, terminated, expired, and failed. Terminal guests cannot restart.

executions(id) and execution(id, runId) return structured results with:

  • state: queued, running, success, failed, timed_out, or cancelled;
  • exitCode (nullable), startedAt, and finishedAt;
  • stdout, stderr, and truncated. Each output stream defaults to a host-configurable 64 KiB cap; output is collected at completion, not streamed live. Source code/command input is not returned in run history.

ESM code runs in /workspace with Playwright installed. Commands take an argument array and do not get an implicit shell:

const run = await sandboxes.execute(box.id, {
  key: "node-version",
  command: ["node", "--version"],
  timeoutMs: 5000,
});
const result = await sandboxes.waitExecution(box.id, run.id);
if (result.state !== "success") throw new Error(result.stderr || result.state);
console.log(result.exitCode, result.stdout);

Exactly one of code or command is required. One execution/transfer/CDP connection can use a sandbox at a time; competing operations return 409. Successful sequential executions share filesystem and browser state. There is no package-installation API or arbitrary guest-image option.

Connect an external browser client

An external app with Playwright can connect to the same guest Chromium using standard CDP. Install Playwright in your app only for this mode. Inside guest code it is already installed.

import { chromium } from "playwright";
const connection = await sandboxes.browser(box.id);
const browser = await chromium.connectOverCDP(connection.endpoint, { headers: connection.headers });
// Header includes normal API authentication plus single-use sandbox-scoped browser credential.
// Acquire within 30s; no fixed connection lifetime; exclusive with execution/files.
try {
  const page = await browser.contexts()[0].newPage();
  page.on("response", async response => { /* response.json(), response.body() */ });
  await page.goto("https://example.com");
  console.log(await page.title());
} finally {
  await browser.close();
}

The endpoint is a WebSocket CDP bridge, not Playwright's native protocol. Headers include normal API auth plus a single-use sandbox credential; keep them out of URLs, logs, and guest code. Redeem within 30 seconds. Established connections have no fixed lifetime (maxConnectionSeconds: null). Disconnect before running code, transferring files, or exporting artifacts. Issue a fresh credential to reconnect.

Pages can reach public TCP 80/443 destinations only, with private/infrastructure addresses denied. The service does not guarantee access to sites that block automated browsers and supplies no CAPTCHA bypass. The dashboard provides run inspection, not live video or human takeover.

Workspace files and retained artifacts

Workspace files are temporary. Transfer known relative paths (no leading /, dot segments, or backslashes); each file defaults to at most 8 MiB, configurable by the host owner:

await sandboxes.upload(box.id, "input.json", new TextEncoder().encode(JSON.stringify({ hello: "world" })));
const run = await sandboxes.execute(box.id, {
  key: "transform",
  code: `import { readFile, writeFile } from "node:fs/promises";
    const input = JSON.parse(await readFile("input.json", "utf8"));
    await writeFile("result.json", JSON.stringify({ ...input, processed: true }));`,
});
const result = await sandboxes.waitExecution(box.id, run.id);
if (result.state !== "success") throw new Error(result.stderr || result.state);

const temporary = await sandboxes.download(box.id, "result.json"); // binary Response
const artifact = await sandboxes.exportArtifact(box.id, "result.json", "application/json");
console.log(artifact.id, artifact.sha256, artifact.expiresAt);

await sandboxes.terminate(box.id);
// Exported artifacts remain downloadable after termination, until retention cleanup:
const saved = await sandboxes.artifact(box.id, artifact.id);
const json = await saved.json();

artifacts(id) lists retained file metadata: ID, name, type, byte size, SHA-256, and expiration. artifact(id, artifactId) streams an authenticated attachment. Exporting identical path/content returns the existing artifact; changed bytes create another version. There are no artifact-count, aggregate-byte, or daily export quotas. Artifact expiry is seven days; sandbox metadata also has seven-day retention. Disabling the service deletes artifacts and history.

The SDK does not enumerate workspace directories. Use code when directory inspection is needed. Export before termination, expiry, cancellation, or timeout: once the guest is gone, temporary files cannot be recovered. Private artifact files are in the project's existing Garage bucket.

Usage and cancellation

usage() returns active, allocatedSecondsToday, runtimeMsToday, executionsToday, artifactBytesToday, and legacy limits fields, all null to mean unlimited. There are no project/global sandbox-count, daily allocation/runtime, or execution-count quotas. Daily usage is attributed to the sandbox's UTC admission day. allocatedSecondsToday sums only explicit lifetimes; it is accounting, not a budget. CPU/memory/process, file, output, and concurrent-network controls are configured by the host owner through MYBASE_SANDBOX_CONFIG; see platform sandboxes.

All operations accept an optional final AbortSignal. Aborting cancels the client request or poll, not already admitted work. Explicitly call cancel(id, runId) or terminate(id) when you want to stop work. Execution cancellation and execution timeout destroy the entire guest and process tree. terminate returns the stop request state; poll get until terminal when you need confirmation that cleanup/capacity release has completed. Inspection calls do not renew idle time.

API/worker restarts can fail interrupted work. No automatic command replay or guest checkpoint exists. If a dispatch outcome is uncertain, inspect status and reason rather than starting the command again.

More recipes

HTML to PDF (invoices, reports): upload the HTML, render it, download the file.

await sandboxes.upload(box.id, "page.html", new TextEncoder().encode(html));
const run = await sandboxes.execute(box.id, {
  key: "pdf",
  code: `import { chromium } from "playwright";
    import { readFile } from "node:fs/promises";
    const browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
    const page = await browser.contexts()[0].newPage();
    await page.setContent(await readFile("page.html", "utf8"), { waitUntil: "networkidle" });
    await page.pdf({ path: "out.pdf", format: "A4", printBackground: true });
    await page.close(); await browser.close();`,
  timeoutMs: 30_000,
});
const result = await sandboxes.waitExecution(box.id, run.id);
if (result.state !== "success") throw new Error(result.stderr || result.state);
const pdf = await sandboxes.download(box.id, "out.pdf");

Grade or check a submission: upload the submission and a test file, run the tests with a command, and use exitCode and stdout as the result. Set timeoutMs so an infinite loop can't hold the sandbox; a timeout destroys the whole guest, so create a new sandbox per submission.

Tools for an AI agent: expose execute (code in, stdout/stderr out) and exportArtifact as tools in your model's tool loop, with one sandbox per conversation. examples/sandbox/agent-tools.ts has provider-neutral definitions. Keep model keys in your app, never in the code you send.

One sandbox per job: pair with queues or cron when work should run in the background. Use the job or message ID as the idempotency key so a retried job finds its existing sandbox instead of starting another.

Errors and permissions

API failures throw MybaseApiError with an HTTP status:

StatusCommon causeNext step
400Invalid profile, duration, path, code, or argument arrayCorrect the input
403Missing permission or wrong project scopeUse a key scoped to the project with the required permission
404Service off, ID absent, expired artifact, or runtime missingCheck the project/service and resource lifecycle
409Busy workspace, not ready, or key reused with different inputInspect state; wait for active work, or fix the key/input mismatch
429Too many concurrent admission requestsBriefly back off; avoid a retry storm
503Worker missing or unreachableRun/check host setup and worker logs

sandbox:read permits inspecting states, execution results, workspace downloads, retained artifacts, and usage. sandbox:write includes read plus creation, execution, uploads, export, cancellation, termination, browser credentials, and CDP upgrade. Every browser upgrade also checks normal API auth.

Agent notes

Use unique stable creation/execution keys for retries. Never blindly rerun uncertain commands. Check execution state, exit code, and truncated. Export within lifetime; downloads afterward use artifact IDs. See platform sandboxes documentation for isolation, deployment, limits, and recovery.

On this page