- Change Docker image references in the unit-test workflow from yaoapp/tai:1.2.0 to yaoapp/tai:latest for consistency and to ensure the latest features and fixes are utilized. - Update related documentation to reflect the unified Computer interface in the sandbox, replacing Box and Host references with Computer. Made-with: Cursor
16 KiB
Sandbox JavaScript API
All methods are available on the global sandbox object. No constructor needed.
Quick Start
// Create a container computer
const pc = sandbox.Create({ image: "node:20", owner: "user-123" })
const result = pc.Exec(["node", "-e", "console.log('hello')"])
console.log(result.stdout) // "hello\n"
pc.Remove()
// Or use the host directly (no container)
const host = sandbox.Host()
host.Exec(["ls", "-la", "/workspace"])
Both sandbox.Create() and sandbox.Host() return a Computer object with the same interface. The kind property tells you which type it is.
Static Methods
sandbox.Create(options) → Computer
Create a new sandbox container. Returns a Computer (kind = "box"). If options.id is set and a sandbox with that ID already exists, returns the existing one (GetOrCreate semantics).
const pc = sandbox.Create({
image: "node:20", // required — container image
owner: "user-123", // required — owner identifier
pool: "gpu", // optional — pool name (default: first pool)
id: "my-sandbox", // optional — if set, uses GetOrCreate
workdir: "/app", // optional — working directory
user: "1000:1000", // optional — UID:GID
env: { NODE_ENV: "dev" },// optional — environment variables
memory: 536870912, // optional — memory limit in bytes (512MB)
cpus: 1.5, // optional — CPU limit
vnc: true, // optional — enable VNC desktop
ports: [ // optional — port mappings
{ container_port: 3000, host_port: 3000, host_ip: "", protocol: "tcp" }
],
policy: "session", // optional — "oneshot"|"session"|"longrunning"|"persistent"
idle_timeout: 600000, // optional — idle timeout in ms (10min)
stop_timeout: 30000, // optional — stop timeout in ms
workspace_id: "ws-abc", // optional — bind a workspace
mount_mode: "rw", // optional — "rw"|"ro"
mount_path: "/workspace", // optional — mount path in container
labels: { team: "backend" } // optional — custom labels
})
sandbox.Get(id) → Computer | null
Get an existing sandbox by ID. Returns a Computer (kind = "box") or null if not found.
const pc = sandbox.Get("my-sandbox")
if (pc) {
console.log(pc.kind, pc.id, pc.owner, pc.pool)
}
sandbox.List(filter?) → BoxInfo[]
List all sandboxes, optionally filtered.
// All sandboxes
const all = sandbox.List()
// Filter by owner
const mine = sandbox.List({ owner: "user-123" })
// Filter by pool and labels
const gpu = sandbox.List({ pool: "gpu", labels: { team: "ml" } })
Each element in the returned array:
{
id: "sb-xxx",
container_id: "abc123...",
pool: "default",
owner: "user-123",
status: "running", // "running"|"stopped"|"creating"|...
image: "node:20",
vnc: false,
policy: "session",
labels: { team: "backend" },
created_at: "2026-03-07T10:00:00Z",
last_active: "2026-03-07T10:05:00Z",
process_count: 2
}
sandbox.Delete(id) → void
Remove a sandbox and its container.
sandbox.Delete("my-sandbox")
sandbox.Host(pool?) → Computer
Get a Computer (kind = "host") for executing commands directly on the Tai host machine (no container). Only available when the pool's Tai server has host_exec capability.
const host = sandbox.Host() // default pool
const gpu = sandbox.Host("gpu") // specific pool
sandbox.GetNode(taiID) → NodeInfo | null
Get information about a registered node by its Tai ID.
const node = sandbox.GetNode("tai-abc123")
if (node) {
console.log(node.status, node.system.hostname)
}
sandbox.Nodes() → NodeInfo[]
List all registered nodes.
const nodes = sandbox.Nodes()
nodes.forEach(function(n) {
console.log(n.tai_id, n.status, n.pool, n.system.os)
})
sandbox.NodesByTeam(teamID) → NodeInfo[]
List nodes belonging to a specific team.
const nodes = sandbox.NodesByTeam("team-001")
Computer Object
Returned by sandbox.Create(), sandbox.Get(), and sandbox.Host(). This is the unified interface for all execution environments — containers and bare-metal hosts.
Use the kind property to check the type. Methods marked box-only throw an error when called on a host computer.
Properties (read-only)
| Property | Type | Description |
|---|---|---|
pc.kind |
string | "box" or "host" |
pc.id |
string | Sandbox ID (box-only; empty for host) |
pc.owner |
string | Owner identifier (box-only; empty for host) |
pc.pool |
string | Pool name |
pc.Exec(cmd, options?) → ExecResult
Execute a command and wait for it to finish.
const result = pc.Exec(["ls", "-la", "/app"])
console.log(result.exit_code) // 0
console.log(result.stdout) // file listing
Options:
pc.Exec(["python3", "train.py"], {
workdir: "/workspace/ml",
env: { CUDA_VISIBLE_DEVICES: "0" },
stdin: "input data",
timeout: 300000, // ms
max_output: 10485760 // bytes (10MB)
})
Return value:
{
exit_code: 0,
stdout: "...", // UTF-8 string
stderr: "...", // UTF-8 string
duration_ms: 1234, // execution time in ms
error: "", // error message (empty on success)
truncated: false // true if output was truncated by max_output
}
pc.Stream(cmd, callback) / pc.Stream(cmd, options, callback)
Execute a command with streaming output via callback. The call blocks until the process exits.
Callback signature: function(type, data)
type = "stdout"→datais a string chunk from stdouttype = "stderr"→datais a string chunk from stderrtype = "exit"→datais the exit code (number)
pc.Stream(["npm", "run", "dev"], function(type, data) {
if (type === "stdout") console.log(data)
if (type === "stderr") console.log("[ERR]", data)
if (type === "exit") console.log("exited:", data)
})
// With options
pc.Stream(["npm", "test"], {
workdir: "/app",
env: { CI: "true" },
timeout: 60000
}, function(type, data) {
console.log(type, data)
})
pc.VNC() → string
Get the VNC WebSocket URL.
- Box: routes to the container's VNC server (
:5900) - Host: routes to the Tai host via
__host__identifier (configurable viahost_vnc_port)
const url = pc.VNC()
// Box: "ws://tai-host:16080/vnc/container-id/ws"
// Host: "ws://tai-host:16080/vnc/__host__/ws"
If no VNC server is running, the WebSocket connection will fail — handle this in the caller.
pc.Proxy(port, path?) → string
Get an HTTP proxy URL for a service port.
- Box: routes to
container-ip:{port} - Host: routes to
127.0.0.1:{port}on the Tai machine via__host__
const url = pc.Proxy(3000)
// Box: "http://tai-host:8099/container-id:3000/"
// Host: "http://tai-host:8099/__host__:3000/"
const url = pc.Proxy(8080, "/api/v1")
// Box: "http://tai-host:8099/container-id:8080/api/v1"
// Host: "http://tai-host:8099/__host__:8080/api/v1"
pc.ComputerInfo() → ComputerInfo
Get identity and registry information.
const info = pc.ComputerInfo()
console.log(info.kind) // "box" or "host"
console.log(info.pool) // pool name
console.log(info.system.os) // "linux" | "windows" | "darwin"
console.log(info.status) // "running" | "stopped" | ...
Returns a ComputerInfo object.
pc.BindWorkplace(workspaceID) → void
Bind a workspace to this computer for the current session.
pc.BindWorkplace("ws-project-abc")
pc.Workplace() → WorkspaceFS | null
Access the workspace bound via BindWorkplace(). Returns null if no workspace is bound.
pc.BindWorkplace("ws-project-abc")
const ws = pc.Workplace()
ws.ReadFile("config.yml")
ws.WriteFile("output.json", JSON.stringify(data))
See WorkspaceFS Object for the full method list.
pc.Attach(port, options?) → string — box-only
Get a WebSocket or SSE endpoint URL for a service running inside the container. Throws on host computers.
const wsURL = pc.Attach(3000, { protocol: "ws", path: "/ws" })
// "ws://tai-host:8099/container-id:3000/ws"
const sseURL = pc.Attach(8080, { protocol: "sse", path: "/events" })
// "http://tai-host:8099/container-id:8080/events"
Options:
{
protocol: "ws" | "sse", // default "ws"; affects URL scheme (ws:// vs http://)
path: "/ws" // optional URL path suffix
}
pc.Info() → BoxInfo — box-only
Get current container status information. Throws on host computers.
const info = pc.Info()
console.log(info.status, info.process_count, info.last_active)
Returns the same structure as elements in sandbox.List().
pc.Start() → void — box-only
Start a stopped container. Throws on host computers.
pc.Start()
pc.Stop() → void — box-only
Stop a running container. Throws on host computers.
pc.Stop()
pc.Remove() → void — box-only
Remove the container. Throws on host computers.
pc.Remove()
ComputerInfo Object
Returned by pc.ComputerInfo(). Read-only snapshot of a Computer's identity and state.
{
kind: "box", // "box" | "host"
pool: "default",
tai_id: "tai-abc123",
machine_id: "m-xyz",
version: "1.2.3",
mode: "direct", // "direct" | "tunnel"
status: "running",
capabilities: { docker: true, k8s: false, host_exec: true },
system: {
os: "linux",
arch: "amd64",
hostname: "gpu-server-01",
num_cpu: 16,
total_mem: 68719476736
},
// Box-only fields (empty/zero for host)
box_id: "sb-xxx",
container_id: "abc123...",
owner: "user-123",
image: "node:20",
policy: "session",
labels: { team: "backend" }
}
NodeInfo Object
Returned by sandbox.GetNode(), sandbox.Nodes(), sandbox.NodesByTeam(). Read-only view of a registered Tai node.
{
tai_id: "tai-abc123",
machine_id: "m-xyz",
version: "1.2.3",
mode: "direct", // "direct" | "tunnel"
addr: "192.168.1.100",
status: "online", // "online" | "offline" | "connecting"
pool: "gpu",
connected_at: "2026-03-07T08:00:00Z",
last_ping: "2026-03-07T10:05:00Z",
ports: {
grpc: 19100,
http: 8099,
vnc: 16080,
docker: 12375,
k8s: 16443,
host_vnc: 5900 // VNC port on host for __host__ routing
},
capabilities: {
docker: true,
k8s: false,
host_exec: true
},
system: {
os: "linux",
arch: "amd64",
hostname: "gpu-server-01",
num_cpu: 16,
total_mem: 68719476736 // bytes (64GB)
}
}
WorkspaceFS Object
Returned by pc.Workplace(), workspace.Get(), and workspace.Create().
Properties (read-only)
| Property | Type | Description |
|---|---|---|
ws.id |
string | Workspace ID |
ws.name |
string | Workspace name |
ws.node |
string | Node name |
Methods
| Method | Returns | Description |
|---|---|---|
ws.ReadFile(path) |
string |
Read file content as UTF-8 string |
ws.WriteFile(path, data, perm?) |
void |
Write string data to file. perm defaults to 0644 |
ws.ReadDir(path?) |
DirEntry[] |
List directory contents. Defaults to root |
ws.Stat(path) |
FileInfo |
Get file/directory metadata |
ws.MkdirAll(path, perm?) |
void |
Create directory tree. perm defaults to 0755 |
ws.Remove(path) |
void |
Remove a file |
ws.RemoveAll(path) |
void |
Remove a file or directory recursively |
ws.Rename(from, to) |
void |
Rename/move a file or directory |
Return types:
// DirEntry
{ name: "main.go", is_dir: false, size: 1234 }
// FileInfo
{ name: "main.go", size: 1234, is_dir: false, mod_time: "2026-03-07T10:00:00Z" }
Examples
Run a build and check output
const pc = sandbox.Create({
image: "golang:1.23",
owner: "ci-bot",
workspace_id: "ws-project-abc"
})
const build = pc.Exec(["go", "build", "./..."], {
workdir: "/workspace",
timeout: 120000
})
if (build.exit_code !== 0) {
console.log("Build failed:", build.stderr)
pc.Remove()
throw new Error("build failed")
}
const test = pc.Exec(["go", "test", "./..."], {
workdir: "/workspace",
env: { CGO_ENABLED: "0" }
})
console.log("Tests:", test.exit_code === 0 ? "PASS" : "FAIL")
pc.Remove()
Stream a long-running process
const pc = sandbox.Create({
image: "node:20",
owner: "user-123",
policy: "session"
})
pc.Exec(["npm", "install"], { workdir: "/app" })
pc.Stream(["npm", "run", "dev"], { workdir: "/app" }, function(type, data) {
if (type === "stdout") console.log(data)
if (type === "stderr") console.log("[ERR]", data)
if (type === "exit") console.log("dev server exited:", data)
})
Host execution for GPU workloads
const host = sandbox.Host("gpu")
const result = host.Exec(["nvidia-smi"])
console.log(result.stdout)
host.Exec(["python3", "train.py", "--epochs=10"], {
workdir: "/workspace/ml",
env: { CUDA_VISIBLE_DEVICES: "0,1" },
timeout: 3600000
})
Uniform interface — same code for box and host
function runTask(pc, cmd, opts) {
const result = pc.Exec(cmd, opts)
if (result.exit_code !== 0) {
throw new Error(pc.kind + " exec failed: " + result.stderr)
}
return result.stdout
}
// Works the same for both
const box = sandbox.Create({ image: "node:20", owner: "u1" })
const host = sandbox.Host("gpu")
runTask(box, ["node", "-e", "console.log('hi')"])
runTask(host, ["echo", "hello"])
VNC and HTTP proxy
const pc = sandbox.Create({
image: "kasmweb/chrome:latest",
owner: "user-123",
vnc: true
})
// Get VNC desktop URL
const vncURL = pc.VNC()
// "ws://tai-host:16080/vnc/container-id/ws"
// Get HTTP proxy to a web service inside the container
const appURL = pc.Proxy(3000)
// "http://tai-host:8099/container-id:3000/"
// Same methods work on host
const host = sandbox.Host()
const hostVNC = host.VNC()
// "ws://tai-host:16080/vnc/__host__/ws"
Query cluster nodes
const nodes = sandbox.Nodes()
// Find online GPU nodes
const gpuNodes = nodes.filter(function(n) {
return n.status === "online" && n.pool === "gpu"
})
console.log("Available GPU nodes:", gpuNodes.length)
gpuNodes.forEach(function(n) {
console.log(
n.tai_id,
n.system.hostname,
n.system.num_cpu + " CPUs",
Math.round(n.system.total_mem / 1073741824) + "GB RAM"
)
})
Workspace file operations
const pc = sandbox.Create({
image: "node:20",
owner: "user-123"
})
pc.BindWorkplace("ws-my-project")
const ws = pc.Workplace()
ws.MkdirAll("src/utils")
ws.WriteFile("src/main.go", 'package main\n\nfunc main() {\n\tprintln("hello")\n}\n')
ws.WriteFile("go.mod", "module myproject\n\ngo 1.23\n")
const entries = ws.ReadDir("src/")
entries.forEach(function(e) {
console.log(e.name, e.is_dir ? "(dir)" : e.size + " bytes")
})
const content = ws.ReadFile("src/main.go")
console.log(content)
Permission check pattern
const auth = Authorized()
if (!auth) throw new Error("not authenticated")
const pc = sandbox.Get(id)
if (!pc) throw new Error("sandbox not found")
if (pc.owner !== auth.user_id) throw new Error("permission denied")
pc.Exec(["ls", "-la"])