- Add VNC-enabled Docker images (playwright, desktop) with Xvfb, x11vnc, noVNC
- Implement VNC proxy service for WebSocket-based VNC access
- Add API endpoints: /sandbox/{id}/vnc, /vnc/client, /vnc/ws
- Support dynamic VNC port mapping for Docker Desktop (macOS/Windows)
- Add YAO_SANDBOX_VNC_PORT_MAPPING config option for local development
- Update build.sh to support building VNC images
- Include design document and updated README
Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
4.2 KiB
Go
127 lines
4.2 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// Config holds sandbox configuration
|
|
type Config struct {
|
|
Image string `json:"image,omitempty"` // Docker image, default: yao/sandbox-claude:latest
|
|
WorkspaceRoot string `json:"workspace_root,omitempty"` // Host workspace root directory
|
|
IPCDir string `json:"ipc_dir,omitempty"` // IPC socket directory
|
|
MaxContainers int `json:"max_containers,omitempty"` // Maximum concurrent containers
|
|
IdleTimeout time.Duration `json:"idle_timeout,omitempty"` // Idle timeout before stopping container
|
|
MaxMemory string `json:"max_memory,omitempty"` // Memory limit, e.g., "2g"
|
|
MaxCPU float64 `json:"max_cpu,omitempty"` // CPU limit, e.g., 1.0
|
|
|
|
// Container internal paths
|
|
ContainerWorkDir string `json:"container_workdir,omitempty"` // Container working directory, default: /workspace
|
|
ContainerIPCSocket string `json:"container_ipc_socket,omitempty"` // Container IPC socket path, default: /tmp/yao.sock
|
|
ContainerUser string `json:"container_user,omitempty"` // Container user, default: "" (use image default). Set to "0" for root.
|
|
|
|
// VNC port mapping (for Docker Desktop on macOS/Windows where container IPs are not directly accessible)
|
|
VNCPortMapping bool `json:"vnc_port_mapping,omitempty"` // Enable VNC port mapping to host, default: false
|
|
}
|
|
|
|
// DefaultConfig returns a Config with default values
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Image: "yaoapp/sandbox-claude:latest",
|
|
MaxContainers: 100,
|
|
IdleTimeout: 30 * time.Minute,
|
|
MaxMemory: "2g",
|
|
MaxCPU: 1.0,
|
|
ContainerWorkDir: "/workspace",
|
|
ContainerIPCSocket: "/tmp/yao.sock",
|
|
}
|
|
}
|
|
|
|
// Init initializes the config with defaults based on environment variables and data root
|
|
func (c *Config) Init(dataRoot string) {
|
|
// Image
|
|
if env := os.Getenv("YAO_SANDBOX_IMAGE"); env != "" {
|
|
c.Image = env
|
|
} else if c.Image == "" {
|
|
c.Image = "yaoapp/sandbox-claude:latest"
|
|
}
|
|
|
|
// Workspace root
|
|
if env := os.Getenv("YAO_SANDBOX_WORKSPACE"); env != "" {
|
|
c.WorkspaceRoot = env
|
|
} else if c.WorkspaceRoot == "" {
|
|
c.WorkspaceRoot = filepath.Join(dataRoot, "sandbox", "workspace")
|
|
}
|
|
|
|
// IPC directory
|
|
if env := os.Getenv("YAO_SANDBOX_IPC"); env != "" {
|
|
c.IPCDir = env
|
|
} else if c.IPCDir == "" {
|
|
c.IPCDir = filepath.Join(dataRoot, "sandbox", "ipc")
|
|
}
|
|
|
|
// Max containers - set default first if zero, then try env override
|
|
if c.MaxContainers == 0 {
|
|
c.MaxContainers = 100
|
|
}
|
|
if env := os.Getenv("YAO_SANDBOX_MAX"); env != "" {
|
|
if v, err := strconv.Atoi(env); err == nil && v > 0 {
|
|
c.MaxContainers = v
|
|
}
|
|
// Invalid env value: keep existing/default value
|
|
}
|
|
|
|
// Idle timeout - set default first if zero, then try env override
|
|
if c.IdleTimeout == 0 {
|
|
c.IdleTimeout = 30 * time.Minute
|
|
}
|
|
if env := os.Getenv("YAO_SANDBOX_IDLE_TIMEOUT"); env != "" {
|
|
if v, err := time.ParseDuration(env); err == nil && v > 0 {
|
|
c.IdleTimeout = v
|
|
}
|
|
// Invalid env value: keep existing/default value
|
|
}
|
|
|
|
// Max memory
|
|
if env := os.Getenv("YAO_SANDBOX_MEMORY"); env != "" {
|
|
c.MaxMemory = env
|
|
} else if c.MaxMemory == "" {
|
|
c.MaxMemory = "2g"
|
|
}
|
|
|
|
// Max CPU - set default first if zero, then try env override
|
|
if c.MaxCPU == 0 {
|
|
c.MaxCPU = 1.0
|
|
}
|
|
if env := os.Getenv("YAO_SANDBOX_CPU"); env != "" {
|
|
if v, err := strconv.ParseFloat(env, 64); err == nil && v > 0 {
|
|
c.MaxCPU = v
|
|
}
|
|
// Invalid env value: keep existing/default value
|
|
}
|
|
|
|
// Container internal paths
|
|
if env := os.Getenv("YAO_SANDBOX_CONTAINER_WORKDIR"); env != "" {
|
|
c.ContainerWorkDir = env
|
|
} else if c.ContainerWorkDir == "" {
|
|
c.ContainerWorkDir = "/workspace"
|
|
}
|
|
|
|
if env := os.Getenv("YAO_SANDBOX_CONTAINER_IPC"); env != "" {
|
|
c.ContainerIPCSocket = env
|
|
} else if c.ContainerIPCSocket == "" {
|
|
c.ContainerIPCSocket = "/tmp/yao.sock"
|
|
}
|
|
|
|
// Container user (for CI environments with UID mismatch)
|
|
if env := os.Getenv("YAO_SANDBOX_CONTAINER_USER"); env != "" {
|
|
c.ContainerUser = env
|
|
}
|
|
|
|
// VNC port mapping (for Docker Desktop on macOS/Windows)
|
|
if env := os.Getenv("YAO_SANDBOX_VNC_PORT_MAPPING"); env != "" {
|
|
c.VNCPortMapping = env == "true" || env == "1" || env == "yes"
|
|
}
|
|
}
|