yao/sandbox/v2/docs/API.md
Max ce0a97c0af feat(sandbox/v2): refactor benchmarks and tests to use TaiID
- Updated benchmark functions to utilize TaiID instead of pool names for improved consistency and accuracy in tests.
- Refactored test cases across various files to ensure compatibility with the new TaiID structure.
- Enhanced setup functions to accept pointers to poolConfig for better memory management.
- Removed deprecated config struct and adjusted related documentation to reflect the changes in the sandbox architecture.

Made-with: Cursor
2026-03-09 02:50:28 +08:00

17 KiB

Sandbox V2 — Go API Reference

Package: github.com/yaoapp/yao/sandbox/v2

Sandbox V2 manages sandboxes through a pool of Tai nodes. Two primary abstractions:

  • Box — a container (Docker or K8s pod). Created via Manager.Create.
  • Host — the Tai host machine itself. Obtained via Manager.Host (no Create needed).

Supports workspace mounting, VNC, WebSocket proxying, and HostExec.


Initialization

Init

func Init()

Initializes the global Manager singleton. Must be called once at startup. No configuration is needed — node discovery is handled by tai/registry.

sandbox.Init()

M

func M() *Manager

Returns the global Manager. Panics if Init was not called.

mgr := sandbox.M()

Node Discovery

Sandbox V2 no longer uses a static pool configuration. Nodes are discovered dynamically through tai/registry. Each Tai node registers itself with a unique TaiID (e.g. "192.168.1.10-19100" for direct mode, "local" for Docker). The TaiID is used as the Pool identifier in CreateOptions, ListOptions, Host(), ImageExists(), etc.


Lifecycle Policies

type LifecyclePolicy string

const (
    OneShot     LifecyclePolicy = "oneshot"     // removed after first Exec
    Session     LifecyclePolicy = "session"     // removed after idle timeout
    LongRunning LifecyclePolicy = "longrunning" // stopped after idle, removed after max lifetime
    Persistent  LifecyclePolicy = "persistent"  // never auto-cleaned
)

Manager

Start

func (m *Manager) Start(ctx context.Context) error

Recovers existing containers from all pools and starts the background cleanup loop (1 min interval).

ctx := context.Background()
err := sandbox.M().Start(ctx)

Close

func (m *Manager) Close() error

Stops the cleanup loop and closes all pool connections.

Create

func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)

Creates and starts a new sandbox container. Returns a Box handle.

box, err := sandbox.M().Create(ctx, sandbox.CreateOptions{
    Image:   "alpine:latest",
    Owner:   "user-123",
    Pool:    "192.168.1.10-19100",  // TaiID from registry
    Policy:  sandbox.Session,
    WorkDir: "/workspace",
    Env:     map[string]string{"LANG": "en_US.UTF-8"},
    Memory:  512 * 1024 * 1024, // 512MB
    CPUs:    1.0,
    VNC:     true,
    Labels:  map[string]string{"project": "demo"},
    Ports: []sandbox.PortMapping{
        {ContainerPort: 8080, HostPort: 0, Protocol: "tcp"},
    },
    IdleTimeout: 15 * time.Minute,
    StopTimeout: 3 * time.Second,
    WorkspaceID: "ws-abc",
    MountMode:   "rw",
    MountPath:   "/workspace",
})

Host

func (m *Manager) Host(ctx context.Context, pool string) (*Host, error)

Returns a Host handle for the given pool (identified by TaiID). Unlike Create, no container is provisioned — the Host is available as long as the Tai server reports host_exec capability. Returns ErrPoolNotFound if the TaiID is not registered, ErrPoolMissing if the pool argument is empty, or an error if the node has no host_exec.

host, err := sandbox.M().Host(ctx, "192.168.1.10-19100")

Get

func (m *Manager) Get(ctx context.Context, id string) (*Box, error)

Returns an existing sandbox by ID. Returns ErrNotFound if absent.

box, err := sandbox.M().Get(ctx, "sb-12345")

GetOrCreate

func (m *Manager) GetOrCreate(ctx context.Context, opts CreateOptions) (*Box, error)

Returns existing sandbox by opts.ID or creates a new one.

box, err := sandbox.M().GetOrCreate(ctx, sandbox.CreateOptions{
    ID:    "sb-session-xyz",
    Image: "alpine:latest",
    Owner: "user-123",
})

List

func (m *Manager) List(ctx context.Context, opts ListOptions) ([]*Box, error)

Returns all sandboxes matching the given filters. Empty fields = no filter.

boxes, err := sandbox.M().List(ctx, sandbox.ListOptions{
    Owner: "user-123",
    Pool:  "192.168.1.10-19100",
    Labels: map[string]string{"project": "demo"},
})

Remove

func (m *Manager) Remove(ctx context.Context, id string) error

Force-removes a sandbox (SIGKILL + delete).

err := sandbox.M().Remove(ctx, "sb-12345")

Cleanup

func (m *Manager) Cleanup(ctx context.Context) error

Removes idle/expired sandboxes based on lifecycle policies. Called automatically by the cleanup loop, but can also be invoked manually.

Heartbeat

func (m *Manager) Heartbeat(sandboxID string, active bool, processCount int) error

Updates a sandbox's last-active timestamp. Called by the gRPC heartbeat service.

err := sandbox.M().Heartbeat("sb-12345", true, 3)

Pools

func (m *Manager) Pools() []registry.NodeSnapshot

Returns all registered Tai nodes from the tai/registry.

for _, n := range sandbox.M().Pools() {
    fmt.Printf("tai_id=%s mode=%s addr=%s status=%s\n",
        n.TaiID, n.Mode, n.Addr, n.Status)
}

ImageExists

func (m *Manager) ImageExists(ctx context.Context, pool, ref string) (bool, error)

Reports whether the given image ref exists on the target pool node. Returns (true, nil) when the pool has no image service (e.g. K8s — kubelet handles pulls).

exists, err := sandbox.M().ImageExists(ctx, "192.168.1.10-19100", "alpine:latest")

PullImage

func (m *Manager) PullImage(ctx context.Context, pool, ref string, opts ImagePullOptions) (<-chan taisandbox.PullProgress, error)

Pulls an image to the target pool node. Returns a channel of taisandbox.PullProgress (from github.com/yaoapp/yao/tai/sandbox). Returns (nil, nil) when the pool has no image service (e.g. K8s).

PullProgress fields: Status string, Layer string, Current int64, Total int64, Error string.

ch, err := sandbox.M().PullImage(ctx, "192.168.1.10-19100", "myapp:v2", sandbox.ImagePullOptions{
    Auth: &sandbox.RegistryAuth{
        Username: "user",
        Password: "pass",
        Server:   "registry.example.com",
    },
})
for p := range ch {
    fmt.Printf("pull: %s layer=%s %d/%d\n", p.Status, p.Layer, p.Current, p.Total)
}

EnsureImage

func (m *Manager) EnsureImage(ctx context.Context, pool, ref string, opts ImagePullOptions) error

Checks if the image exists; if not, pulls it and blocks until complete.

err := sandbox.M().EnsureImage(ctx, "192.168.1.10-19100", "alpine:latest", sandbox.ImagePullOptions{})

Box

A Box is a handle to a running sandbox container.

Accessors

func (b *Box) ID() string
func (b *Box) Owner() string
func (b *Box) ContainerID() string
func (b *Box) Pool() string
func (b *Box) WorkspaceID() string

Exec

func (b *Box) Exec(ctx context.Context, cmd []string, opts ...ExecOption) (*ExecResult, error)

Runs a command and waits for completion. If the box policy is OneShot, the box is auto-removed after execution.

result, err := box.Exec(ctx, []string{"python3", "-c", "print('hello')"},
    sandbox.WithWorkDir("/workspace"),
    sandbox.WithEnv(map[string]string{"PYTHONPATH": "/lib"}),
    sandbox.WithTimeout(30*time.Second),
)
fmt.Printf("exit=%d stdout=%s stderr=%s\n", result.ExitCode, result.Stdout, result.Stderr)

Stream

func (b *Box) Stream(ctx context.Context, cmd []string, opts ...ExecOption) (*ExecStream, error)

Runs a command with real-time streaming I/O.

stream, err := box.Stream(ctx, []string{"bash"})
go io.Copy(os.Stdout, stream.Stdout)
go io.Copy(os.Stderr, stream.Stderr)
fmt.Fprintln(stream.Stdin, "echo hello")
stream.Stdin.Close()
exitCode, _ := stream.Wait()

Attach

func (b *Box) Attach(ctx context.Context, port int, opts ...AttachOption) (*ServiceConn, error)

Connects to a service running inside the sandbox via WebSocket proxy.

conn, err := box.Attach(ctx, 8080,
    sandbox.WithProtocol("ws"),
    sandbox.WithPath("/api/stream"),
    sandbox.WithHeaders(map[string]string{"Authorization": "Bearer xxx"}),
)
defer conn.Close()
conn.Write([]byte(`{"action":"subscribe"}`))
data, _ := conn.Read()

VNC

func (b *Box) VNC(ctx context.Context) (string, error)

Returns the VNC WebSocket URL for the sandbox (requires VNC: true at creation).

url, err := box.VNC(ctx)
// url = "ws://tai-host:16080/vnc/xxx/ws"

Proxy

func (b *Box) Proxy(ctx context.Context, port int, path string) (string, error)

Returns the HTTP proxy URL for a service on the given port.

url, err := box.Proxy(ctx, 3000, "/api/health")
// url = "http://tai-host:8099/container-id:3000/api/health"

Workspace

func (b *Box) Workspace() workspace.FS

Returns a workspace.FS interface (github.com/yaoapp/yao/tai/workspace) for file operations on the sandbox's workspace volume. The interface embeds fs.FS, fs.StatFS, fs.ReadFileFS, fs.ReadDirFS, io.Closer, and adds write methods (WriteFile, Remove, RemoveAll, Rename, MkdirAll).

ws := box.Workspace()
data, _ := ws.ReadFile("main.py")
ws.WriteFile("output.txt", []byte("result"), 0644)
ws.MkdirAll("src/pkg", 0755)
ws.Remove("tmp.log")

Start / Stop / Remove

func (b *Box) Start(ctx context.Context) error
func (b *Box) Stop(ctx context.Context) error
func (b *Box) Remove(ctx context.Context) error
box.Stop(ctx)   // SIGTERM with grace period, then SIGKILL
box.Start(ctx)  // restart a stopped sandbox
box.Remove(ctx) // force remove

Info

func (b *Box) Info(ctx context.Context) (*BoxInfo, error)

Returns current sandbox status from the underlying container runtime.

info, err := box.Info(ctx)
fmt.Printf("status=%s processes=%d vnc=%v created=%s\n",
    info.Status, info.ProcessCount, info.VNC, info.CreatedAt)

Host

A Host represents a Tai host machine execution environment, distinct from Box (containers). No Create call is needed — a Host is available as long as the pool's Tai server reports host_exec.

Accessors

func (h *Host) Pool() string

Exec

func (h *Host) Exec(ctx context.Context, cmd string, args []string, opts ...HostExecOption) (*HostExecResult, error)

Runs a command directly on the Tai host machine via HostExec gRPC.

host, _ := sandbox.M().Host(ctx, "192.168.1.10-19100")
result, err := host.Exec(ctx, "git", []string{"status"},
    sandbox.WithHostWorkDir("/data/repos/project"),
    sandbox.WithHostEnv(map[string]string{"GIT_AUTHOR_NAME": "bot"}),
    sandbox.WithHostTimeout(10000),        // 10s
    sandbox.WithHostMaxOutput(1024*1024),   // 1MB
)
fmt.Printf("exit=%d stdout=%s duration=%dms\n",
    result.ExitCode, string(result.Stdout), result.DurationMs)

Stream

func (h *Host) Stream(ctx context.Context, cmd string, args []string, opts ...HostExecOption) (*HostExecStream, error)

Runs a command on the Tai host and streams stdout/stderr in real time via HostExec gRPC ExecStream. Returns a HostExecStream with separate channels for stdout and stderr.

host, _ := sandbox.M().Host(ctx, "192.168.1.10-19100")
stream, err := host.Stream(ctx, "tail", []string{"-f", "/var/log/app.log"},
    sandbox.WithHostWorkDir("/data"),
    sandbox.WithHostTimeout(60000),
)
go func() {
    for chunk := range stream.Stderr {
        fmt.Fprintf(os.Stderr, "%s", chunk)
    }
}()
for chunk := range stream.Stdout {
    fmt.Printf("%s", chunk)
}
exitCode, err := stream.Wait()

To cancel a long-running stream early:

stream.Cancel()

Workspace

func (h *Host) Workspace(sessionID string) workspace.FS

Returns a workspace.FS for the given session on the host. Files are stored under dataDir/{sessionID}/ on the Tai host, accessed via Volume gRPC (independent of container bind mounts).

ws := host.Workspace("ws-abc")
ws.WriteFile("input.txt", []byte("data"), 0644)
data, _ := ws.ReadFile("output.txt")
entries, _ := ws.ReadDir(".")

ExecOption Functions

func WithWorkDir(dir string) ExecOption
func WithEnv(env map[string]string) ExecOption
func WithTimeout(timeout time.Duration) ExecOption

AttachOption Functions

func WithProtocol(protocol string) AttachOption  // "ws" (default) or "sse"
func WithPath(path string) AttachOption           // URL path on the target service
func WithHeaders(headers map[string]string) AttachOption

HostExecOption Functions

func WithHostWorkDir(dir string) HostExecOption
func WithHostEnv(env map[string]string) HostExecOption
func WithHostStdin(data []byte) HostExecOption
func WithHostTimeout(ms int64) HostExecOption
func WithHostMaxOutput(bytes int64) HostExecOption

Types

CreateOptions

type CreateOptions struct {
    ID          string
    Owner       string
    Labels      map[string]string
    Pool        string              // TaiID from registry (required unless WorkspaceID routes to a node)
    Image       string              // required
    WorkDir     string              // default "/workspace"
    User        string              // container user
    Env         map[string]string
    Memory      int64               // bytes; 0 = unlimited
    CPUs        float64             // 0 = unlimited
    VNC         bool
    Ports       []PortMapping
    Policy      LifecyclePolicy     // default Session
    IdleTimeout time.Duration       // 0 = no idle cleanup
    MaxLifetime time.Duration       // 0 = no max lifetime
    StopTimeout time.Duration       // SIGTERM grace period; 0 = DefaultStopTimeout (2s)
    WorkspaceID string              // workspace to mount; empty = none
    MountMode   string              // "rw" (default) or "ro"
    MountPath   string              // default "/workspace"
}

ListOptions

type ListOptions struct {
    Owner  string
    Pool   string
    Labels map[string]string
}

PortMapping

type PortMapping struct {
    ContainerPort int
    HostPort      int    // 0 = auto-assign
    HostIP        string
    Protocol      string // "tcp" (default), "udp"
}

ExecResult

type ExecResult struct {
    ExitCode int
    Stdout   string
    Stderr   string
}

ExecStream

type ExecStream struct {
    Stdout io.ReadCloser
    Stderr io.ReadCloser
    Stdin  io.WriteCloser
    Wait   func() (int, error) // blocks until exit; returns exit code
    Cancel func()              // kills the process
}

ServiceConn

type ServiceConn struct {
    Read   func() ([]byte, error)
    Write  func(data []byte) error
    Events <-chan []byte
    URL    string
    Close  func() error
}

BoxInfo

type BoxInfo struct {
    ID           string
    ContainerID  string
    Pool         string
    Owner        string
    Status       string          // "running", "stopped", etc.
    Policy       LifecyclePolicy
    Labels       map[string]string
    Image        string
    CreatedAt    time.Time
    LastActive   time.Time
    ProcessCount int
    VNC          bool
}

ImagePullOptions / RegistryAuth

type ImagePullOptions struct {
    Auth *RegistryAuth // nil = anonymous
}

type RegistryAuth struct {
    Username string
    Password string
    Server   string
}

HostExecResult

type HostExecResult struct {
    ExitCode   int
    Stdout     []byte
    Stderr     []byte
    DurationMs int64
    Error      string
    Truncated  bool
}

HostExecStream

type HostExecStream struct {
    Stdout <-chan []byte
    Stderr <-chan []byte
    Wait   func() (int, error) // blocks until exit; returns exit code
    Cancel func()              // cancels the stream context
}

Errors

var (
    ErrNotAvailable = errors.New("sandbox: not available (no pools configured)")
    ErrNotFound     = errors.New("sandbox: not found")
    ErrPoolNotFound = errors.New("sandbox: pool not found")
    ErrPoolMissing  = errors.New("sandbox: pool name is required")
)

Helper Functions

BuildGRPCEnv

func BuildGRPCEnv(mode, addr, sandboxID string) map[string]string

Builds environment variables injected into sandbox containers. The gRPC port is read from config.Conf.GRPC.Port (defaults to 9099).

  • mode — the TaiNode.Mode ("local", "direct", "tunnel")
  • addr — the TaiNode.Addr (e.g. "tai://192.168.1.10:19100" for direct mode)
  • sandboxID — the container's sandbox identifier
Variable Description
YAO_SANDBOX_ID Sandbox identifier
YAO_GRPC_ADDR gRPC server address (auto-derived)

Address derivation logic:

  • localhost.docker.internal:<grpcPort>
  • direct with tai://host:porthost:port
  • tunnel127.0.0.1:<grpcPort>

Token injection (YAO_TOKEN, YAO_REFRESH_TOKEN) is the caller's responsibility via CreateOptions.Env. See IMPL.md "OAuth Decoupling" for details.