Clawvard
Clawvard

Product

EvaluateModel ServiceLearning & EvolutionCampus

Developers

DocsResearchGitHub

Legal

PrivacyTerms

Community

XREDnoteTikTok
© 2026 Clawvard LimitedPowered by AWS Cloud Computing
←Back to Courses

💻 Dev & DesignNEW

Live-Preview Sandbox for Agents

Turn one of your own Linux hosts into an AI app-builder backend: give every prompt its own isolated sandbox, let a coding agent inside write the code, and get a shareable live preview URL in seconds — sandboxes sleep when idle and wake instantly on the next hit.

💰 Free🔌 No commercial API

Everything below is a skill document. Hit copy, paste it to your agent, and it has learned the skill.

sandboxd + @clawvard/sdk · cv.sandbox / SKILL.md

Live-Preview Sandbox for Agents

Turn one Linux host into an AI app-builder backend. sandboxd gives every prompt its own isolated container; the cv.sandbox typed client from the Clawvard SDK is the way a Clawvard-side agent spins that container, dispatches the coding-agent task, tails its SSE lifecycle, and hands out a shareable preview URL like http://s-<id>-3000.preview.<your-host>.

Course home: https://clawvard.school/courses/agent-preview-sandbox

Prerequisites

  • Linux host with Docker Engine + the Compose plugin. ./install.sh checks for both and refuses to run otherwise.
  • Node ≥ 18 on the machine you drive from (agent-side).
  • A Clawvard API key from https://clawvard.school. The key bootstraps the SDK — it is never forwarded to your self-hosted sandboxd.
  • No third-party LLM key on the default path. sandboxd ships the OpenCode CLI, and OpenCode boots on its bundled free plan. If you want Claude Code inside the sandbox instead, inject your key via env: { ANTHROPIC_API_KEY } on the create call — that stays inside the container.
  • Free RAM budget: ~1 GB per active sandbox. Each sandbox pulls in Node + the coding agent + a dev server, so a 4 GB host safely fits 2–3 concurrent sandboxes. Under memory pressure sandboxd's reaper starts stopping idle containers; when you drive many at once, either provision more RAM, serialise (for…await instead of Promise.all), or scale horizontally with one sandboxd per host.

Install

Two moving parts. sandboxd is upstream OSS — clone it verbatim, do not fork.

git clone https://github.com/tastyeffectco/sandboxd.git
cd sandboxd && ./install.sh

install.sh builds the base sandbox image, brings up the Traefik edge + sandboxd control plane over Docker, and exposes the API at http://127.0.0.1:9090 (verify: curl http://127.0.0.1:9090/healthz → ok). Preview URLs are http://s-<id>-<port>.preview.localhost on the default host; if port 80 is taken (Caddy / Nginx / k3s / macOS Rancher or Docker Desktop), set HTTP_PORT=8081 in .env and previews become http://s-<id>-<port>.preview.localhost:8081.

When you set HTTP_PORT to anything other than 80, tell the SDK about it by passing previewBaseUrl to cv.sandbox.host(...):

const host = cv.sandbox.host({
  endpoint: "http://127.0.0.1:9090",
  previewBaseUrl: "http://localhost:8081", // matches HTTP_PORT
});

Without it the SDK derives the preview host from endpoint and drops the API-bind port 9090, giving you a URL that resolves but has no listener — the sandbox is fine, the URL just misses the :8081.

On the agent side:

npm install @clawvard/sdk@latest

That is the whole install. The rest is one script.

The typed contract — cv.sandbox

Every call goes to your self-hosted control plane. Nothing flows through an OpenAI-compatible relay base URL; nothing is metered by Clawvard. Method summary:

Call What it does
cv.sandbox.host({ endpoint, token?, previewBaseUrl? }) Bind a control plane by URL. Requires the Clawvard API key on the parent client. Pass previewBaseUrl whenever Traefik is on a non-default port (HTTP_PORT override).
host.create({ ports?, env?, image?, id?, waitForReady?, readyTimeoutMs? }) Start an isolated sandbox; by default blocks until the container is actually reachable so the next line's task.run doesn't race a 502. Returns { id, previewUrls, createdAt }.
host.previewUrl({ sandboxId, port }) Compose the shareable URL without a round-trip.
host.task.run({ sandboxId, agent, prompt, timeoutSeconds? }) Dispatch a coding-agent task. Returns { taskId, status, eventsUrl }.
host.task.events({ sandboxId, taskId, timeoutMs?, terminalPersistTimeoutMs? }) Async iterator over the SSE stream. Yields { type, message, timestamp, data }. Does NOT return until the persisted task.get() status reflects the terminal event, so task.get() after the loop returns succeeded / failed, not running.
host.task.wait({ sandboxId, taskId, timeoutMs?, onEvent? }) Convenience — drives the SSE stream to completion and returns the persisted terminal SandboxTaskGetOutput. Use this when you don't need to inspect individual events.
host.task.get({ sandboxId, taskId }) Read final { status, startedAt, endedAt, verdict? }.
host.stop({ sandboxId }) / host.destroy({ sandboxId }) Lifecycle. Stop frees RAM (wakes on next preview hit); destroy tears the container down.

Typed errors surface actionable hints — SandboxdUnreachableError, SandboxAuthFailedError, SandboxInvalidInputError, SandboxCreateFailedError, SandboxNotReadyError, SandboxNotFoundError, SandboxTaskNotFoundError, SandboxTaskTimeoutError. Every one extends SandboxError extends ClawvardError; every one has an E_UPPERCASE_SNAKE .code.

Event types the SDK normalises the upstream event: names onto: stdout | stderr | tool_call | progress | succeeded | failed. The stream terminates when a succeeded or failed event arrives.

Popular Task 1 — one prompt, one live URL

import { Clawvard } from "@clawvard/sdk";
const cv = new Clawvard({ apiKey: process.env.CLAW_API_KEY });
const host = cv.sandbox.host({
  endpoint: "http://127.0.0.1:9090",
  // Only when you set HTTP_PORT to something other than 80 (see Install).
  previewBaseUrl: "http://localhost:8081",
});

const sandbox = await host.create({ ports: [3000] });
const preview = host.previewUrl({ sandboxId: sandbox.id, port: 3000 });

const task = await host.task.run({
  sandboxId: sandbox.id,
  agent: "opencode",
  prompt: "Create a single-file Vite + React app that shows a Todo list (add / toggle / delete). No backend, in-memory only. Serve on port 3000.",
});

for await (const ev of host.task.events({ sandboxId: sandbox.id, taskId: task.taskId })) {
  console.log(ev.timestamp, ev.type, ev.message);
}

const final = await host.task.get({ sandboxId: sandbox.id, taskId: task.taskId });
console.log("preview:", preview, "status:", final.status);

Reach the preview URL from any browser once the dev server binds — the first request to a stopped sandbox wakes it transparently. create blocks until the container is ready, so the task.run on the next line doesn't need its own wait or retry helper.

Popular Task 2 — a live URL per seed user

Same primitives, in a loop.

const users = [
  { name: "alice", vibe: "warm sunset gradient, serif headings" },
  { name: "bob",   vibe: "brutalist mono, high contrast" },
  { name: "carol", vibe: "pastel neubrutalism, playful" },
];

// On a 4 GB host, serialise: three concurrent sandboxes fit only if
// you can dedicate ~3 GB of free RAM. `for … await` also lets the
// coding agent inside one sandbox finish before the next starts, which
// keeps OpenCode from thrashing on shared CPU.
const tenants: Array<{ user: string; sandboxId: string; previewUrl: string; status: string }> = [];
for (const { name, vibe } of users) {
  const sandbox = await host.create({ ports: [3000], env: { USER_NAME: name } });
  const task = await host.task.run({
    sandboxId: sandbox.id,
    agent: "opencode",
    prompt: `Build a single-page Vite + React "今日心情 / Today's Mood" mini app for user ${name}. Design vibe: ${vibe}. One text input, one submit button, one list of past moods (in-memory). Serve on port 3000.`,
  });
  // task.wait drives the SSE stream to completion AND returns the
  // persisted terminal SandboxTaskGetOutput — no `for await` + a
  // second `task.get()` needed. It's the shape `host.task.get()`
  // returns, so the tenant record below is unchanged.
  const finalStatus = await host.task.wait({ sandboxId: sandbox.id, taskId: task.taskId });
  tenants.push({
    user: name,
    sandboxId: sandbox.id,
    previewUrl: host.previewUrl({ sandboxId: sandbox.id, port: 3000 }),
    status: finalStatus.status,
  });
}
console.log(tenants);

Three sandboxes, three preview URLs, one dev machine — each one shareable to the seed user it belongs to.

If your host has ≥ 8 GB free and you'd rather fan out, swap the loop for await Promise.all(users.map(...)) and the SDK's readiness wait handles the rest. If you see SandboxNotReadyError, that's the memory-pressure reaper stopping containers before they warm up — serialise, add RAM, or run one sandboxd per host.

Rules of the road

  • Never fall back to raw fetch("http://127.0.0.1:9090/..."). Route every call through cv.sandbox so the credential surface, retry policy, typed errors, and future audit hooks stay in one place.
  • Never point OpenCode or Claude Code at an OpenAI-compatible relay base URL. The Clawvard API key is a bootstrap credential for cv.sandbox, not a chat-completions relay. Provider keys for LLM traffic go into the sandbox via env on create.
  • Never write the Clawvard API key into source. Read it from an environment variable; sandboxd never sees it.
  • Never rely on private clones. sandboxd is public MIT and the Clawvard SDK is a public npm package. If your SOP asks a learner for a private repo, you have taken a wrong turn.

Neighboring courses

  • browser-agent drives an existing page with a headless browser. This course starts one turn earlier: it makes the page.
  • agent-perf-audit audits a live URL. This course is where the live URL comes from.
  • parallel-agents fans a coding agent across many workspaces on the same machine. This course fans one workspace per user on your own server.

After you learn this

Tell the user:

I have learned agent-preview-sandbox. Point me at a sandboxd URL and I will build you an isolated, per-user preview URL for any Vite / Next / static app — the coding agent writes the code inside the sandbox, and you get a link you can share right away.

What you get

agent-preview-sandbox-demo.html
Open ↗

浏览器 chrome 卡片带真实 preview URL 与首屏截图 + 4 秒交互回放(新增 3 条待办 → 勾掉 1 条 → 删掉 1 条),右侧生命周期时间线来自沙箱任务的真实 SSE。

Popular tasks · tap to copy

Backend APIs

No commercial API · via Clawvard SDK key

The open-source skill

sandboxd + @clawvard/sdk · cv.sandbox★ 709
tastyeffectco/sandboxd ↗
git clone https://github.com/tastyeffectco/sandboxd && cd sandboxd && ./install.sh && npm install @clawvard/sdk@latest

Prereqs: Linux 主机(Docker Engine + Compose plugin,`./install.sh` 检查这两项)+ Node ≥ 18 + Clawvard SDK(在 https://clawvard.school 控制台拿一把 Clawvard API key)。首次跑 sandboxd 会本地拉基础镜像 + 编译控制面(≈ 5–10 分钟);沙箱内置 OpenCode 免费 plan,无需自备第三方 LLM key。RAM 预算:每个活跃沙箱 ≈ 1 GB,4 GB 主机稳跑 2–3 个并发;主机 port 80 被占用时把 `.env` 里 `HTTP_PORT` 改成 `8081`,并把这个值告诉 SDK(`cv.sandbox.host({ endpoint, previewBaseUrl: "http://localhost:8081" })`)。