- Refactor GPT-5 test cases to improve clarity and maintainability. - Comment out tests for temperature handling in GPT-5, indicating they are temporarily disabled. - Remove the obsolete hostexec test file to clean up the codebase. - Enhance the sandbox manager to support host execution capabilities and improve lifecycle management. Made-with: Cursor
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(cfg Config) error
Initializes the global Manager singleton. Must be called once at startup.
err := sandbox.Init(sandbox.Config{
Pool: []sandbox.Pool{
{
Name: "docker",
Addr: "tai://192.168.1.10:9100",
MaxPerUser: 5,
MaxTotal: 20,
IdleTimeout: 30 * time.Minute,
MaxLifetime: 24 * time.Hour,
StopTimeout: 5 * time.Second,
},
},
})
M
func M() *Manager
Returns the global Manager. Panics if Init was not called.
mgr := sandbox.M()
Config
type Config struct {
Pool []Pool
}
Pool
type Pool struct {
Name string
Addr string // "tai://host:port", "tunnel://host:port", or Docker socket
Options []tai.Option // tai.Client options
MaxPerUser int // 0 = unlimited
MaxTotal int // 0 = unlimited
IdleTimeout time.Duration // 0 = no idle cleanup
MaxLifetime time.Duration // 0 = no max lifetime
StopTimeout time.Duration // SIGTERM grace period; 0 = DefaultStopTimeout (2s)
}
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: "docker",
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. Unlike Create, no container is provisioned —
the Host is available as long as the pool's Tai server reports host_exec capability.
Returns ErrPoolNotFound if the pool does not exist, or an error if the pool has no host_exec.
host, err := sandbox.M().Host(ctx, "remote")
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: "docker",
Labels: map[string]string{"project": "demo"},
})
Remove
func (m *Manager) Remove(ctx context.Context, id string) error
Force-removes a sandbox (SIGKILL + delete). Revokes container tokens.
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)
AddPool
func (m *Manager) AddPool(ctx context.Context, p Pool) error
Registers a new pool at runtime.
err := sandbox.M().AddPool(ctx, sandbox.Pool{
Name: "k8s-gpu",
Addr: "tai://10.0.0.5:9100",
MaxTotal: 10,
})
RemovePool
func (m *Manager) RemovePool(ctx context.Context, name string, force bool) error
Removes a pool. Returns ErrPoolInUse if the pool has running boxes and force=false.
With force=true, all boxes in the pool are removed first.
Pools
func (m *Manager) Pools() []PoolInfo
Returns all registered pools and their status.
for _, p := range sandbox.M().Pools() {
fmt.Printf("pool=%s addr=%s connected=%v boxes=%d\n",
p.Name, p.Addr, p.Connected, p.Boxes)
}
SetGRPCPort
func (m *Manager) SetGRPCPort(port int)
Sets the local gRPC port injected into container env vars (YAO_GRPC_ADDR). Default: 9099.
SetWorkspaceManager
func (m *Manager) SetWorkspaceManager(wm *workspace.Manager)
Links the workspace manager. When CreateOptions.WorkspaceID is set, the Manager uses it
to resolve the workspace's bound node and route the container to the correct pool.
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, "docker", "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, "docker", "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, "docker", "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:6080/websockify?container=xxx"
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:8080/proxy/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, "remote")
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)
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), "tcp"
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 // empty = default pool
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 // overrides pool default
StopTimeout time.Duration // overrides pool default
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
}
PoolInfo
type PoolInfo struct {
Name string
Addr string
Connected bool
Boxes int
MaxPerUser int
MaxTotal int
IdleTimeout time.Duration
MaxLifetime time.Duration
}
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
}
Errors
var (
ErrNotAvailable = errors.New("sandbox: not available (no pools configured)")
ErrNotFound = errors.New("sandbox: not found")
ErrLimitExceeded = errors.New("sandbox: limit exceeded")
ErrPoolNotFound = errors.New("sandbox: pool not found")
ErrPoolInUse = errors.New("sandbox: pool has running boxes")
)
Helper Functions
CreateContainerTokens
func CreateContainerTokens(sandboxID, owner string, scopes []string) (access, refresh string, err error)
Creates an OAuth token pair for a sandbox container.
RevokeContainerTokens
func RevokeContainerTokens(refresh string) error
Revokes a container refresh token.
BuildGRPCEnv
func BuildGRPCEnv(pool *Pool, sandboxID, access, refresh string, grpcPort int) map[string]string
Builds environment variables injected into sandbox containers:
| Variable | Description |
|---|---|
YAO_SANDBOX_ID |
Sandbox identifier |
YAO_TOKEN |
Access token for gRPC auth |
YAO_REFRESH_TOKEN |
Refresh token for token rotation |
YAO_GRPC_ADDR |
gRPC server address (auto-derived) |
Address derivation logic:
tai://host:port→host:porttunnel://...→127.0.0.1:<grpcPort>- Local/default →
127.0.0.1:<grpcPort>