yao/sandbox/v2/IMPL.md
Max dd08881027 Update DESIGN.md and IMPL.md for Sandbox V2 enhancements
- Add workspace integration as a new feature in Sandbox V2, allowing for persistent user storage.
- Update the Manager API to include new methods for workspace management and image handling.
- Refactor the Pool struct to include additional options and improve lifecycle management.
- Revise implementation status in IMPL.md to reflect completed phases and optimizations, ensuring clarity on the current state of the project.

These changes enhance the functionality and documentation of Sandbox V2, improving user experience and system architecture.
2026-03-05 21:28:55 +08:00

11 KiB

Sandbox V2 — Implementation Status

Reference: DESIGN.md


Phase 1: Core Module — DONE

tai SDK Prerequisites — DONE

Step Package What Status
0 tai/sandbox Labels, User in CreateOptions + ContainerInfo DONE
1 tai/sandbox ExecStream (Docker + K8s) DONE
2 tai/proxy Connect (WS/SSE, Local + Remote) DONE
3 tai/sandbox Image interface (Exists, Pull, Remove, List) DONE
4 tai/tai.go Client: Sandbox(), Image(), Proxy(), VNC(), Volume(), Workspace() DONE
5 yao/grpc Heartbeat RPC (proto + handler) DONE

sandbox/v2 Core — DONE

File What Status
sandbox.go Init(), M(), global singleton DONE
manager.go Manager: Create/Get/GetOrCreate/List/Remove/Cleanup/Close, Start (container recovery), AddPool/RemovePool/Pools, Heartbeat, SetGRPCPort, SetWorkspaceManager, ImageExists/PullImage/EnsureImage DONE
box.go Box: Exec, Stream, Attach, Workspace, VNC, Proxy, Start/Stop/Remove, Info, touch/lastActiveTime/idleTimeout/maxLifetime/stopTimeout DONE
types.go LifecyclePolicy (OneShot/Session/LongRunning/Persistent), Pool, PoolInfo, PortMapping, CreateOptions (with WorkspaceID/MountMode/MountPath), ListOptions, ExecOption/ExecResult/ExecStream, AttachOption/ServiceConn, ImagePullOptions/RegistryAuth, BoxInfo, DefaultStopTimeout DONE
config.go Config struct DONE
errors.go ErrNotAvailable, ErrNotFound, ErrLimitExceeded, ErrPoolNotFound, ErrPoolInUse DONE
grpc.go CreateContainerTokens, RevokeContainerTokens, BuildGRPCEnv DONE

workspace Module — DONE

File What Status
workspace.go Workspace struct, CreateOptions, ListOptions, UpdateOptions, NodeInfo, MountMode, metadata marshal/unmarshal DONE
manager.go Manager: Create/Get/List/Update/Delete, ReadFile/WriteFile/ListDir/Remove/FS, Nodes/AddPool/RemovePool, NodeForWorkspace/MountPath DONE
errors.go ErrNotFound, ErrNodeMissing, ErrNodeOffline, ErrHasMounts DONE

Tests — DONE

File Coverage Status
sandbox/v2
sandbox_test.go Init, M, singleton DONE
manager_test.go Create, Get, GetOrCreate, List, Remove, pool limits (MaxTotal, MaxPerUser), multi-pool DONE
manager_lifecycle_test.go Start (recovery), Cleanup, idle tracking, Heartbeat DONE
box_test.go Exec, Info, Workspace (ReadFile/WriteFile), status DONE
box_attach_test.go Attach WS echo, Attach SSE events, VNC URL, VNC Connect (RFB handshake) DONE
box_workspace_test.go Workspace mount, file I/O through Box, invalid ID DONE
box_image_test.go ImageExists (Docker+K8s), PullImage (progress+K8s no-op), EnsureImage, bad ref DONE
grpc_test.go Token creation/revocation, env var building (local vs remote) DONE
bench_test.go ContainerLifecycle, Create, Exec, ExecHeavy, Remove, Info, StopStart, WorkspaceReadWrite DONE
testutils_test.go testPools (local/remote/k8s), setupManager, createTestBox, ensureTestImage DONE
export_test.go ResetForTest DONE
workspace
workspace_test.go Create (auto/explicit ID, labels, invalid node), Get, List (owner/node filter), Update (name/labels), Delete, Nodes, NodeForWorkspace, AddPool/RemovePool, MountPath DONE
fileio_test.go ReadWriteFile, nested paths, ListDir, Remove, fs.FS (ReadFile, WriteFile, MkdirAll, Rename, WalkDir, Remove, NotFound) DONE
bench_test.go WriteFile, ReadFile, ReadWriteCycle, WriteLargeFile, ListDir, FSWalkDir, CreateDelete DONE
testutils_test.go testPools, setupManagerForPool, clientForPool, localClient, setupManagerMultiNode, createWorkspace DONE

CI — DONE

Job Contents Status
SandboxV2Test Consolidated: image pre-pull → tai-test → sandbox/v2 (local+remote+k8s) → workspace (local+remote) DONE
BenchmarkSandboxV2 Parallel: performance tests for sandbox/v2 + workspace DONE
GRPCTest Independent: gRPC tests (unchanged) DONE

Performance Optimizations — DONE

Optimization Before After Impact
Remove redundant Stop in Manager.Remove() 2.14s 177ms 12x faster Docker remove
Container CMD trap SIGTERM 2s+ stop near-instant Graceful shutdown on Stop
K8s Start: respect ctx deadline 30s hardcoded ctx-aware + 60s default Proper timeout propagation
K8s Pod spec: Args vs Command CMD overridden ENTRYPOINT preserved Correct container behavior

Phase 2: Process + JSAPI — PENDING

Task Package Detail
process.go sandbox/v2 Register sandbox.* process namespace (sandbox.Create, sandbox.Exec, sandbox.ReadFile, etc.)
process.go workspace Register workspace.* process namespace
jsapi/sandbox.go sandbox/v2/jsapi V8 JSAPI Sandbox() constructor (registered in gou runtime)
cmd/start.go integration yao Call sandbox.Init(config.Conf.Sandbox) + sandbox.M().Start(ctx) in startup sequence
Heartbeat bridge yao/grpc Wire gRPC Heartbeat handler → sandbox.M().Heartbeat()
Wire openapi/oauth sandbox/v2/grpc.go CreateContainerTokens currently generates random strings; RevokeContainerTokens is a no-op. Replace with real openapi/oauth issue/revoke calls

Process Registration (planned)

sandbox.pool.Add        sandbox.pool.Remove     sandbox.pool.List
sandbox.Create          sandbox.Get             sandbox.GetOrCreate
sandbox.Remove          sandbox.List
sandbox.Start           sandbox.Stop
sandbox.Exec            sandbox.Stream          sandbox.Attach
sandbox.ReadFile        sandbox.WriteFile       sandbox.ListDir
sandbox.RemoveFile      sandbox.MkdirAll
sandbox.VNC             sandbox.Proxy
sandbox.EnsureImage     sandbox.ImageExists     sandbox.PullImage

workspace.Create        workspace.Get           workspace.List
workspace.Update        workspace.Delete
workspace.ReadFile      workspace.WriteFile     workspace.ListDir
workspace.Remove        workspace.FS
workspace.Nodes

JSAPI (planned)

// Sandbox
var sb = Sandbox("my-workspace", {
    image: "yaoapp/workspace:latest",
    owner: "user-123"
})
sb.Exec(["go", "build", "./..."])
sb.ReadFile("src/main.go")
sb.WriteFile("src/main.go", "package main\n...")
sb.Stream(["npm", "run", "dev"], function(chunk) { ... })
var conn = sb.Attach(3000, { protocol: "ws", path: "/ws" })
sb.Info()
sb.Stop()
sb.Start()
sb.Remove()

// Workspace
var ws = Workspace("my-workspace")
ws.ReadFile("src/main.go")
ws.WriteFile("src/main.go", "package main\n...")
ws.ListDir("src/")
ws.Remove("tmp.txt")

Phase 3: Agent Integration — PENDING

Task Detail
Agent creates Box via sandbox.M().GetOrCreate() Replace infraSandbox.Manager
Agent uses Box.Workspace() for file I/O Replace Docker Copy/bind mount reads
Agent uses Box.Exec() for commands Replace Docker exec
Agent uses Box.VNC() / Box.Proxy() Replace vncproxy
Agent injects Box as SandboxExecutor ctx.sandbox JSAPI unchanged for hooks

Phase 4: Cutover — PENDING

Task Detail
Move sandbox/v2 → sandbox Rename package
Delete old sandbox code manager.go, ipc/, bridge/, vncproxy/, docker/
Delete DESIGN-REMOTE.md Superseded by tai.Client
Update cmd/start.go Use new init path

Implementation Details

Container CMD

All V2 containers use a SIGTERM-aware sleep as PID 1:

sh -c "trap 'exit 0' TERM; while :; do sleep 86400 & wait $!; done"

This ensures:

  • Container stays alive indefinitely (no hardcoded sleep infinity)
  • Exits immediately on SIGTERM (no 2s wait)
  • Works on both Docker and K8s

Container Labels

Manager injects these labels at creation time:

managed-by=yao-sandbox
sandbox-id=<id>
sandbox-owner=<owner>
sandbox-pool=<pool>
sandbox-policy=<policy>
workspace-id=<workspace-id>    (if WorkspaceID set)

Used by Manager.Start() to discover and recover existing containers after restart.

Workspace Bind Mount

When CreateOptions.WorkspaceID is set:

1. NodeForWorkspace(wsID) → node name
2. Force pool = node name
3. MountPath(wsID) → hostDir
4. Bind: hostDir:/workspace:rw

Multi-Mode Testing

testPools() returns all available pool configurations:

func testPools() []poolConfig {
    pools := []poolConfig{{Name: "local", Addr: testLocalAddr()}}
    if addr := os.Getenv("SANDBOX_TEST_REMOTE_ADDR"); addr != "" {
        pools = append(pools, poolConfig{Name: "remote", Addr: addr})
    }
    if host := os.Getenv("TAI_TEST_K8S_HOST"); host != "" {
        // ... K8s pool with kubeconfig, namespace, ports
        pools = append(pools, poolConfig{Name: "k8s", ...})
    }
    return pools
}

Every test iterates over all available pools:

func TestSomething(t *testing.T) {
    for _, pc := range testPools() {
        t.Run(pc.Name, func(t *testing.T) {
            m := setupManagerForPool(t, pc)
            // test logic
        })
    }
}

Benchmark Helpers

func setupManagerForBench(b *testing.B, pc poolConfig) *sandbox.Manager
func ensureTestImageBench(b *testing.B, m *sandbox.Manager, pool string)
func createBoxForBench(b *testing.B, m *sandbox.Manager) *sandbox.Box

K8s-specific behavior:

  • BenchmarkStopStart: skipped (K8s Stop deletes Pod)
  • Create/Lifecycle benchmarks: 120s timeout for K8s Pod scheduling

File Inventory

sandbox/v2 (7 source + 10 test = 17 files)

File Lines Purpose
sandbox.go ~25 Global singleton
manager.go ~620 Manager implementation
box.go ~230 Box implementation
types.go ~170 Type definitions
config.go ~5 Config struct
errors.go ~10 Error definitions
grpc.go ~55 Token/env injection
testutils_test.go ~130 Test helpers
sandbox_test.go ~30 Singleton tests
manager_test.go ~250 CRUD tests
manager_lifecycle_test.go ~120 Lifecycle tests
box_test.go ~200 Box tests
box_attach_test.go ~260 Attach/VNC tests
box_workspace_test.go ~285 Workspace tests
box_image_test.go ~120 Image tests
grpc_test.go ~80 Token tests
bench_test.go ~230 Benchmarks

workspace (3 source + 4 test = 7 files)

File Lines Purpose
workspace.go ~80 Types + metadata
manager.go ~320 Manager implementation
errors.go ~10 Error definitions
testutils_test.go ~90 Test helpers
workspace_test.go ~325 CRUD tests
fileio_test.go ~235 File I/O tests
bench_test.go ~150 Benchmarks