yao/sandbox
Max c26d764857 fix(sandbox/v2): oneshot containers never cleaned up by watcher
Three related bugs caused oneshot containers to run indefinitely:

1. watcher.go: switch b.policy had no case OneShot, so even when the
   idle timeout fired, no remove action was emitted.

2. watcher.go + manager.go/recoverBoxes: idleTimeoutD was only set for
   Session and LongRunning on recovery; OneShot defaulted to 0, which
   caused the watcher to hit the `timeout <= 0` early-return and skip
   all checks entirely.

3. agent/sandbox/v2/options.go: same gap — opts.IdleTimeout == 0 guard
   only filled defaults for Session and LongRunning.

Fix: add DefaultOneShotIdleTimeout (30 min), wire it in recoverBoxes
and options.go, and add case OneShot → Remove in watcher.go.

Made-with: Cursor
2026-03-29 23:01:21 +08:00
..
bridge feat(sandbox): add persistent Docker sandbox for external CLI agents 2026-01-29 19:23:50 +08:00
docker feat(sandbox/v2): enhance connector integration and VNC configuration 2026-03-10 23:28:31 +08:00
ipc Refactor session cleanup logic to ensure single execution and improve resource management 2026-02-26 15:35:47 +08:00
proxy Fix sandbox compatibility, claude-proxy streaming, and rename playwright to browser 2026-02-06 12:31:07 +08:00
v2 fix(sandbox/v2): oneshot containers never cleaned up by watcher 2026-03-29 23:01:21 +08:00
vncproxy feat(sandbox/v2): enhance connector integration and VNC configuration 2026-03-10 23:28:31 +08:00
config.go Update IPC socket path in tests to match configuration change 2026-02-09 00:07:48 +08:00
config_test.go Enhance Sandbox Configuration for CI and Testing 2026-01-29 20:13:40 +08:00
DESIGN-PLAYWRIGHT-VNC.md Fix sandbox compatibility, claude-proxy streaming, and rename playwright to browser 2026-02-06 12:31:07 +08:00
DESIGN.md refactor: remove x-grpc-upstream, benchmark CI job, and harden sandbox v2 2026-03-07 21:41:48 +08:00
errors.go feat(sandbox): add persistent Docker sandbox for external CLI agents 2026-01-29 19:23:50 +08:00
helpers.go Fix sandbox compatibility, claude-proxy streaming, and rename playwright to browser 2026-02-06 12:31:07 +08:00
helpers_test.go Fix sandbox compatibility, claude-proxy streaming, and rename playwright to browser 2026-02-06 12:31:07 +08:00
manager.go feat(sandbox/v2): enhance connector integration and VNC configuration 2026-03-10 23:28:31 +08:00
manager_test.go Implement MCP Configuration and Tool Integration for Sandbox 2026-01-30 19:57:31 +08:00
PLAN.md feat(sandbox): add persistent Docker sandbox for external CLI agents 2026-01-29 19:23:50 +08:00
README.md Add Chrome support to sandbox module and update documentation 2026-02-06 17:48:49 +08:00
SPEC.md Implement gRPC support in the Yao SDK 2026-03-04 13:17:48 +08:00
types.go Add sandbox ID and VNC URL methods to sandbox executor 2026-02-05 21:46:07 +08:00

Yao Sandbox

Sandbox provides persistent Docker containers as isolated execution environments for external CLI agents like Claude Code.

Overview

The sandbox module enables Yao to safely run external AI coding agents (like Claude CLI) in isolated Docker containers. Each user+chat session gets its own container with:

  • Persistent workspace for code and dependencies
  • IPC communication via Unix sockets
  • Resource limits (CPU, memory)
  • Security isolation
  • VNC remote desktop for visual transparency (optional)

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Yao Server                            │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │                   Sandbox Manager                       │  │
│  │                                                         │  │
│  │   - GetOrCreate(userID, chatID) → container             │  │
│  │   - Exec/Stream commands in container                   │  │
│  │   - Filesystem operations (read, write, copy)           │  │
│  │                                                         │  │
│  └────────────────────────┬────────────────────────────────┘  │
│                           │                                   │
│  ┌────────────────────────┴────────────────────────────────┐  │
│  │                    VNC Proxy Service                     │  │
│  │                                                          │  │
│  │   - GET /v1/sandbox/{id}/vnc        → VNC status        │  │
│  │   - GET /v1/sandbox/{id}/vnc/client → noVNC page        │  │
│  │   - GET /v1/sandbox/{id}/vnc/ws     → WebSocket proxy   │  │
│  └──────────────────────────────────────────────────────────┘  │
│                           │                                   │
│           ┌───────────────┼───────────────┐                   │
│           ▼               ▼               ▼                   │
│    ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│    │ sandbox- │ │ sandbox- │ │ sandbox- │ │ sandbox- │       │
│    │ claude   │ │ browser  │ │ desktop  │ │ chrome   │       │
│    │ (No VNC) │ │ (VNC)    │ │ (VNC)    │ │ (VNC)    │       │
│    └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘       │
│         │            │            │            │              │
│    ─────┴────────────┴────────────┴────────────┴────         │
│                   Unix Socket IPC                             │
│             (one socket per container)                        │
└───────────────────────────────────────────────────────────────┘

Quick Start

Build Docker Images

cd sandbox/docker

# Build base image
./build.sh claude

# Build VNC-enabled images
./build.sh browser      # Browser (Playwright) + Fluxbox + VNC
./build.sh desktop      # XFCE Desktop + VNC
./build.sh chrome       # Real Chrome + CDP + VNC (amd64 only)

# Build all images
./build.sh all

Usage

import "github.com/yaoapp/yao/sandbox"

// Create manager
config := sandbox.DefaultConfig()
config.Init("/path/to/yao/data")

manager, err := sandbox.NewManager(config)
if err != nil {
    log.Fatal(err)
}
defer manager.Close()

// Get or create container
container, err := manager.GetOrCreate(ctx, "user123", "chat456")
if err != nil {
    log.Fatal(err)
}

// Execute command
result, err := manager.Exec(ctx, container.Name, []string{"echo", "hello"}, nil)
fmt.Println(result.Stdout) // "hello\n"

// Write file
err = manager.WriteFile(ctx, container.Name, "/workspace/test.txt", []byte("content"))

// Read file
data, err := manager.ReadFile(ctx, container.Name, "/workspace/test.txt")

Configuration

Environment Variables

Variable Default Description
YAO_SANDBOX_IMAGE yao/sandbox-claude:latest Docker image
YAO_SANDBOX_WORKSPACE {YAO_DATA_ROOT}/sandbox/workspace Workspace directory
YAO_SANDBOX_IPC {YAO_DATA_ROOT}/sandbox/ipc IPC socket directory
YAO_SANDBOX_MAX 100 Max concurrent containers
YAO_SANDBOX_IDLE_TIMEOUT 30m Idle timeout
YAO_SANDBOX_MEMORY 2g Memory limit
YAO_SANDBOX_CPU 1.0 CPU limit
YAO_SANDBOX_VNC_PORT_MAPPING false Enable VNC port mapping (for Docker Desktop)

Docker Desktop (macOS/Windows)

Docker Desktop runs containers in a LinuxKit VM, so container IPs are not directly accessible from the host. Enable VNC port mapping for local development:

export YAO_SANDBOX_VNC_PORT_MAPPING=true
export YAO_SANDBOX_IMAGE="yaoapp/sandbox-claude-browser:latest"

When enabled, VNC ports (6080, 5900) are automatically mapped to random available host ports on 127.0.0.1.

Docker Images

Image VNC Description
yaoapp/sandbox-base:latest Base image with git, curl, yao-bridge
yaoapp/sandbox-claude:latest + Claude CLI, Node.js 20, Python 3.11
yaoapp/sandbox-claude:full + Go 1.23
yaoapp/sandbox-claude-browser:latest + Playwright, Fluxbox, VNC (~3.4GB)
yaoapp/sandbox-claude-desktop:latest + XFCE Desktop, VNC (~3.1GB)
yaoapp/sandbox-claude-chrome:latest + Real Chrome, CDP, PyAutoGUI, VNC (~3.4GB, amd64 only)

IPC Communication

Sandbox containers communicate with Yao via Unix sockets using the MCP (Model Context Protocol) JSON-RPC format. The yao-bridge binary inside containers bridges stdio ↔ socket.

Supported methods:

  • initialize - Handshake
  • tools/list - List available tools
  • tools/call - Execute a tool

VNC Remote Desktop

VNC-enabled images (playwright, desktop) provide real-time visibility into Claude's operations.

API Endpoints

Endpoint Description
GET /v1/sandbox/{id}/vnc VNC status (ready/starting/unavailable)
GET /v1/sandbox/{id}/vnc/client noVNC HTML client page
GET /v1/sandbox/{id}/vnc/ws WebSocket proxy to container VNC

View Modes

  • Interactive (default): User can use keyboard and mouse
  • View-only (?viewonly=true): User can only watch

For detailed design, see DESIGN-PLAYWRIGHT-VNC.md.

Directory Structure

sandbox/
├── bridge/          # yao-bridge source
├── docker/          # Dockerfiles and build script
│   ├── base/
│   ├── claude/
│   ├── browser/     # Browser (Playwright) + VNC image
│   ├── desktop/     # XFCE Desktop + VNC image
│   ├── chrome/      # Real Chrome + CDP + VNC image (amd64 only)
│   │   ├── config/  # Chrome preferences, stealth scripts
│   │   └── tests/   # LLM-driven browser automation demos
│   ├── vnc/         # Shared VNC scripts
│   └── build.sh
├── ipc/             # IPC system
│   ├── manager.go
│   ├── session.go
│   └── types.go
├── vncproxy/        # VNC proxy service
│   ├── proxy.go
│   ├── config.go
│   └── proxy_test.go
├── config.go        # Configuration
├── errors.go        # Error types
├── helpers.go       # Helper functions
├── manager.go       # Main manager
└── types.go         # Type definitions

Testing

# Load environment variables first
source env.local.sh

# Unit tests (no Docker required)
go test -v ./sandbox/... -run "^Test.*Validation|^Test.*Generation|^Test.*Parsing"

# All tests (requires Docker)
go test -v ./sandbox/...

# VNC proxy tests only
go test -v ./sandbox/vncproxy/...

Security

  • Containers run as non-root user
  • --cap-drop ALL removes all capabilities
  • no-new-privileges prevents privilege escalation
  • Only workspace directory is mounted
  • Per-session IPC sockets with authorized tools only