yao/sandbox/v2/jsapi/API.md
Max e633640998 refactor(sandbox/v2): transition from pool to node configuration
- Updated benchmark and test functions to utilize node configurations instead of pool configurations for improved clarity and consistency.
- Refactored related setup functions and test cases to align with the new node-based architecture.
- Adjusted error messages and documentation to reflect the transition from pool to node terminology.

Made-with: Cursor
2026-03-09 03:20:09 +08:00

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()
const info = host.Exec(["uname", "-a"])
console.log(info.stdout) // same ExecResult as box

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
  node_id:      "192.168.1.10-19100", // optional — TaiID from registry (required unless workspace_id routes to a node)
  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.node_id)
}

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 node_id (TaiID) and labels
const gpu = sandbox.List({ node_id: "10.0.0.5-19100", labels: { team: "ml" } })

Each element in the returned array:

{
  id:            "sb-xxx",
  container_id:  "abc123...",
  node_id:       "192.168.1.10-19100",
  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(nodeID?) → Computer

Get a Computer (kind = "host") for executing commands directly on the Tai host machine (no container). Only available when the node's Tai server has host_exec capability. The nodeID argument is the TaiID (e.g. "192.168.1.10-19100").

const host = sandbox.Host("192.168.1.10-19100")

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.display_name, 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. Proxy() covers HTTP, WebSocket, and SSE — use it for all protocol access to container/host services.

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.node_id string TaiID (e.g. "192.168.1.10-19100", "local")

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"data is a string chunk from stdout
  • type = "stderr"data is a string chunk from stderr
  • type = "exit"data is 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 via host_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 a proxy URL for a service port. Supports HTTP, WebSocket (ws://), and SSE — the Tai proxy handles protocol upgrades automatically.

  • 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.node_id)    // TaiID
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. For box computers created with a workspace_id option, the workspace is already bound at creation time — calling BindWorkplace overrides it.

pc.BindWorkplace("ws-project-abc")

pc.Workplace() → WorkspaceFS | null

Access the workspace filesystem bound via BindWorkplace(). Returns null if no workspace is bound. ("Workplace" is the binding on a Computer; "Workspace" is the filesystem it points to.)

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.Info() → BoxInfo — box-only

Get current container runtime status (process count, last active time, etc.). For node-level identity info (OS, CPU, capabilities), use ComputerInfo() instead. 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"
  node_id:       "192.168.1.10-19100", // TaiID
  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:         "tai://192.168.1.100:19100",
  status:       "online",          // "online" | "offline" | "connecting"
  display_name: "GPU Node",        // optional human-readable name for UI
  node_id:      "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("10.0.0.5-19100")

const result = host.Exec(["nvidia-smi"])
console.log(result.stdout)

const train = host.Exec(["python3", "train.py", "--epochs=10"], {
  workdir: "/workspace/ml",
  env: { CUDA_VISIBLE_DEVICES: "0,1" },
  timeout: 3600000
})
if (train.exit_code !== 0) throw new Error("training failed: " + train.stderr)

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("10.0.0.5-19100")

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("192.168.1.10-19100")
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.display_name === "gpu"  // n.display_name is optional label for UI
})

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"])