- 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
17 KiB
Sandbox V2 — Go API Reference
Package: github.com/yaoapp/yao/sandbox/v2
Sandbox V2 manages sandboxes through a set 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 static node 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
NodeID 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 nodes 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 node 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",
NodeID: "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, nodeID string) (*Host, error)
Returns a Host handle for the given node (identified by TaiID). Unlike Create, no
container is provisioned — the Host is available as long as the Tai server reports
host_exec capability. Returns ErrNodeNotFound if the TaiID is not registered,
ErrNodeMissing if the nodeID 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",
NodeID: "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)
Nodes
func (m *Manager) Nodes() []registry.NodeSnapshot
Returns all registered Tai nodes from the tai/registry.
for _, n := range sandbox.M().Nodes() {
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, nodeID, ref string) (bool, error)
Reports whether the given image ref exists on the target node.
Returns (true, nil) when the node 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, nodeID, ref string, opts ImagePullOptions) (<-chan taisandbox.PullProgress, error)
Pulls an image to the target node. Returns a channel of taisandbox.PullProgress
(from github.com/yaoapp/yao/tai/sandbox). Returns (nil, nil) when the node 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, nodeID, 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) NodeID() 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 node's Tai server reports host_exec.
Accessors
func (h *Host) NodeID() 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
NodeID 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
NodeID 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
NodeID 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 nodes registered)")
ErrNotFound = errors.New("sandbox: not found")
ErrNodeNotFound = errors.New("sandbox: node not found")
ErrNodeMissing = errors.New("sandbox: node ID 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— theTaiNode.Mode("local","direct","tunnel")addr— theTaiNode.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:
local→host.docker.internal:<grpcPort>directwithtai://host:port→host:porttunnel→127.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.