Remove DESIGN.md file from Tai Go SDK, eliminating outdated documentation on the SDK's architecture, usage, and package layout.
This commit is contained in:
parent
ac87852f4e
commit
7772cc588f
7 changed files with 686 additions and 147 deletions
147
tai/DESIGN.md
147
tai/DESIGN.md
|
|
@ -1,147 +0,0 @@
|
|||
# Tai Go SDK
|
||||
|
||||
Go client library for [Tai](https://github.com/yaoapp/tai) — the universal runtime bridge for Yao Sandbox.
|
||||
|
||||
## Overview
|
||||
|
||||
Provides a unified API for container lifecycle, filesystem operations, HTTP proxy, and VNC access.
|
||||
Supports two modes via a single entry point:
|
||||
|
||||
- **Local** (`docker://` or `""`) — direct Docker daemon connection
|
||||
- **Remote** (`tai://host`) — via Tai Server proxy (Docker, K8s)
|
||||
|
||||
All sub-packages follow the same pattern: **interface + Remote/Local implementations**.
|
||||
|
||||
## Package Layout
|
||||
|
||||
```
|
||||
yao/tai/
|
||||
├── tai.go # Client, New(), Option, Close()
|
||||
├── volume/ # Volume IO + Sync
|
||||
├── workspace/ # Go fs.FS wrapper over volume.Volume
|
||||
├── sandbox/ # Container lifecycle (Create/Start/Stop/Exec/Remove)
|
||||
│ ├── sandbox.go # Interface + shared types
|
||||
│ ├── local.go # Direct Docker socket
|
||||
│ ├── docker.go # Docker via Tai proxy
|
||||
│ ├── docker_core.go # Shared Docker SDK logic
|
||||
│ └── k8s.go # Kubernetes via Tai TCP proxy
|
||||
├── proxy/ # HTTP reverse proxy URL resolution
|
||||
└── vnc/ # VNC WebSocket URL resolution
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
import "github.com/yaoapp/yao/tai"
|
||||
|
||||
// Local — default Docker socket
|
||||
c, _ := tai.New("")
|
||||
|
||||
// Local — explicit address
|
||||
c, _ := tai.New("docker:///var/run/docker.sock")
|
||||
c, _ := tai.New("docker://192.168.1.50:2375")
|
||||
|
||||
// Remote — via Tai Server (Docker runtime, default)
|
||||
c, _ := tai.New("tai://192.168.1.100")
|
||||
|
||||
// Remote — via Tai Server (K8s runtime)
|
||||
c, _ := tai.New("tai://10.0.0.5", tai.K8s,
|
||||
tai.WithKubeConfig("/path/to/kubeconfig.yml"),
|
||||
tai.WithNamespace("sandbox"),
|
||||
)
|
||||
|
||||
defer c.Close()
|
||||
|
||||
// Container lifecycle
|
||||
id, _ := c.Sandbox().Create(ctx, sandbox.CreateOptions{
|
||||
Image: "node:20",
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
})
|
||||
c.Sandbox().Start(ctx, id)
|
||||
|
||||
// Filesystem
|
||||
ws := c.Workspace("session-1")
|
||||
ws.WriteFile("app.js", []byte("console.log('hi')"), 0644)
|
||||
data, _ := ws.ReadFile("app.js")
|
||||
|
||||
// HTTP proxy URL
|
||||
url, _ := c.Proxy().URL(ctx, id, 3000, "/api/health")
|
||||
|
||||
// VNC URL
|
||||
vncURL, _ := c.VNC().URL(ctx, id)
|
||||
```
|
||||
|
||||
## Address Protocol
|
||||
|
||||
| Prefix | Mode | Description |
|
||||
|--------|------|-------------|
|
||||
| `""` | Local | Platform default Docker socket |
|
||||
| `docker://...` | Local | Direct Docker daemon (socket or TCP) |
|
||||
| `tai://host` | Remote | Via Tai Server, all services proxied |
|
||||
|
||||
## Sub-Package Interfaces
|
||||
|
||||
### volume.Volume
|
||||
|
||||
File IO and directory sync between Yao and the container workspace.
|
||||
|
||||
- `ReadFile`, `WriteFile`, `Stat`, `ListDir`, `Remove`, `Rename`, `MkdirAll`
|
||||
- `SyncPush` (Yao -> Tai), `SyncPull` (Tai -> Yao)
|
||||
- **Remote**: gRPC to Tai `:9100`
|
||||
- **Local**: direct disk IO under `dataDir/{sessionID}/`
|
||||
|
||||
### workspace.FS
|
||||
|
||||
Go `fs.FS`-compatible interface wrapping `volume.Volume`, adding write operations.
|
||||
|
||||
### sandbox.Sandbox
|
||||
|
||||
Container lifecycle: `Create`, `Start`, `Stop`, `Remove`, `Exec`, `Inspect`, `List`.
|
||||
|
||||
- **Local**: direct Docker socket, handles VNC port mapping and capabilities
|
||||
- **Docker**: via Tai `:2375` (Docker Engine API proxy)
|
||||
- **K8s**: via Tai `:6443` (kube-apiserver TCP proxy, single-container Pod per sandbox)
|
||||
|
||||
### proxy.Proxy
|
||||
|
||||
HTTP service URL resolution: `URL(ctx, containerID, port, path)`.
|
||||
|
||||
- **Remote**: `http://tai-host:8080/{id}:{port}/{path}`
|
||||
- **Local**: `http://127.0.0.1:{hostPort}/{path}` via `sandbox.Inspect`
|
||||
|
||||
### vnc.VNC
|
||||
|
||||
VNC WebSocket URL resolution: `URL(ctx, containerID)`.
|
||||
|
||||
- **Remote**: `ws://tai-host:6080/vnc/{id}/ws`
|
||||
- **Local**: `ws://127.0.0.1:{vncHostPort}/ws` via `sandbox.Inspect`
|
||||
|
||||
## Options
|
||||
|
||||
```go
|
||||
tai.Docker // Docker runtime (default, can omit)
|
||||
tai.K8s // Kubernetes runtime
|
||||
tai.WithPorts(Ports{}) // custom port mapping
|
||||
tai.WithHTTPClient(hc) // custom HTTP client
|
||||
tai.WithDataDir(dir) // workspace root (Local mode)
|
||||
tai.WithKubeConfig(path) // kubeconfig file path (K8s runtime)
|
||||
tai.WithNamespace(ns) // namespace for K8s (default "default")
|
||||
```
|
||||
|
||||
## Default Ports
|
||||
|
||||
| Service | Default Port |
|
||||
|---------|-------------|
|
||||
| gRPC (Volume + Gateway) | 9100 |
|
||||
| HTTP Proxy | 8080 |
|
||||
| VNC Router | 6080 |
|
||||
| Docker API Proxy | 2375 |
|
||||
| K8s API Proxy | 6443 |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `github.com/yaoapp/tai/volume/pb` — gRPC proto types
|
||||
- `google.golang.org/grpc`
|
||||
- `github.com/pierrec/lz4/v4` — sync compression
|
||||
- `github.com/docker/docker` — Docker SDK
|
||||
- `k8s.io/client-go` + `k8s.io/api` + `k8s.io/apimachinery` — Kubernetes SDK
|
||||
111
tai/docs/README.md
Normal file
111
tai/docs/README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Tai SDK
|
||||
|
||||
Go client library for the [Tai](https://github.com/YaoApp/tai) runtime bridge. Provides unified access to container sandboxes, volume IO, HTTP proxy, and VNC routing — transparently working in both **Local** (direct Docker) and **Remote** (via Tai server) modes.
|
||||
|
||||
## Package Layout
|
||||
|
||||
| Package | Import Path | Description |
|
||||
|---------|-------------|-------------|
|
||||
| `tai` | `github.com/yaoapp/yao/tai` | Top-level client, `New()`, options, `Close()` |
|
||||
| `sandbox` | `github.com/yaoapp/yao/tai/sandbox` | Container lifecycle (Create/Start/Stop/Exec/Remove) |
|
||||
| `volume` | `github.com/yaoapp/yao/tai/volume` | File IO and directory sync |
|
||||
| `workspace` | `github.com/yaoapp/yao/tai/workspace` | `fs.FS`-compatible filesystem over Volume |
|
||||
| `proxy` | `github.com/yaoapp/yao/tai/proxy` | HTTP reverse proxy URL resolution |
|
||||
| `vnc` | `github.com/yaoapp/yao/tai/vnc` | VNC WebSocket URL resolution |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Local Mode (direct Docker)
|
||||
|
||||
```go
|
||||
c, err := tai.New("")
|
||||
// or: tai.New("unix:///var/run/docker.sock")
|
||||
// or: tai.New("tcp://192.168.1.50:2375")
|
||||
defer c.Close()
|
||||
|
||||
id, _ := c.Sandbox().Create(ctx, sandbox.CreateOptions{
|
||||
Name: "my-sandbox",
|
||||
Image: "alpine:latest",
|
||||
Cmd: []string{"sleep", "300"},
|
||||
})
|
||||
c.Sandbox().Start(ctx, id)
|
||||
```
|
||||
|
||||
### Remote Mode (via Tai server, Docker runtime)
|
||||
|
||||
```go
|
||||
c, err := tai.New("tai://192.168.1.100")
|
||||
defer c.Close()
|
||||
|
||||
result, _ := c.Sandbox().Exec(ctx, id, []string{"echo", "hello"}, sandbox.ExecOptions{})
|
||||
fmt.Println(result.Stdout) // "hello\n"
|
||||
```
|
||||
|
||||
### Remote Mode (via Tai server, K8s runtime)
|
||||
|
||||
```go
|
||||
c, err := tai.New("tai://192.168.1.100", tai.K8s,
|
||||
tai.WithKubeConfig("/path/to/kubeconfig.yml"),
|
||||
tai.WithNamespace("default"),
|
||||
tai.WithPorts(tai.Ports{K8s: 6443}),
|
||||
)
|
||||
defer c.Close()
|
||||
```
|
||||
|
||||
## Address Protocols
|
||||
|
||||
| Address | Mode | Description |
|
||||
|---------|------|-------------|
|
||||
| `""` | Local | Platform default Docker socket |
|
||||
| `unix:///var/run/docker.sock` | Local | Explicit Unix socket |
|
||||
| `tcp://host:port` | Local | Explicit TCP Docker daemon |
|
||||
| `npipe:////./pipe/docker_engine` | Local | Windows named pipe |
|
||||
| `docker://host:port` | Local | Docker scheme |
|
||||
| `tai://host` | Remote | Connect via Tai server |
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `WithPorts(Ports{...})` | Override Tai service ports | gRPC=9100, HTTP=8080, VNC=6080 |
|
||||
| `WithHTTPClient(*http.Client)` | Custom HTTP client for proxy/VNC | `http.DefaultClient` |
|
||||
| `WithDataDir(path)` | Volume storage root (Local mode) | `/tmp/tai-volumes` |
|
||||
| `WithKubeConfig(path)` | Kubeconfig file path (K8s mode, **required**) | - |
|
||||
| `WithNamespace(ns)` | K8s namespace | `"default"` |
|
||||
|
||||
## Default Ports
|
||||
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| gRPC | 9100 | Volume IO + Gateway |
|
||||
| HTTP | 8080 | HTTP reverse proxy |
|
||||
| VNC | 6080 | VNC WebSocket router |
|
||||
| Docker | 2375 | Docker API proxy |
|
||||
| K8s | 6443 | Kubernetes API proxy |
|
||||
|
||||
## Client API
|
||||
|
||||
```go
|
||||
c.Volume() // volume.Volume
|
||||
c.Workspace(sessionID) // workspace.FS
|
||||
c.Sandbox() // sandbox.Sandbox
|
||||
c.Proxy() // proxy.Proxy
|
||||
c.VNC() // vnc.VNC
|
||||
c.IsLocal() // bool
|
||||
c.Close() // error
|
||||
```
|
||||
|
||||
## Runtime Constants
|
||||
|
||||
```go
|
||||
tai.Docker // default — use Docker runtime via Tai
|
||||
tai.K8s // use Kubernetes runtime via Tai
|
||||
```
|
||||
|
||||
## Sub-Package Documentation
|
||||
|
||||
- [sandbox.md](sandbox.md) — Container lifecycle management
|
||||
- [volume.md](volume.md) — File IO and sync
|
||||
- [workspace.md](workspace.md) — fs.FS-compatible filesystem
|
||||
- [proxy.md](proxy.md) — HTTP reverse proxy
|
||||
- [vnc.md](vnc.md) — VNC WebSocket routing
|
||||
86
tai/docs/proxy.md
Normal file
86
tai/docs/proxy.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Package `proxy`
|
||||
|
||||
HTTP reverse proxy URL resolution. Resolves service URLs for containers so that HTTP services running inside sandboxes can be accessed from the host.
|
||||
|
||||
## Interface
|
||||
|
||||
```go
|
||||
type Proxy interface {
|
||||
URL(ctx context.Context, containerID string, port int, path string) (string, error)
|
||||
Healthz(ctx context.Context) error
|
||||
}
|
||||
```
|
||||
|
||||
## Implementations
|
||||
|
||||
| Implementation | Constructor | Mode | URL Pattern |
|
||||
|----------------|-------------|------|-------------|
|
||||
| **Remote** | `NewRemote(host, port, hc)` | Via Tai HTTP proxy | `http://tai-host:8080/{containerID}:{port}/{path}` |
|
||||
| **Local** | `NewLocal(sb)` | Direct host port lookup | `http://127.0.0.1:{hostPort}/{path}` |
|
||||
|
||||
## Constructors
|
||||
|
||||
### NewRemote
|
||||
|
||||
```go
|
||||
func NewRemote(host string, port int, hc *http.Client) Proxy
|
||||
```
|
||||
|
||||
Creates a Proxy that routes through Tai's HTTP reverse proxy. URLs are constructed by combining the Tai server address with the container ID and port.
|
||||
|
||||
- `host` — Tai server hostname/IP
|
||||
- `port` — Tai HTTP proxy port (default 8080)
|
||||
- `hc` — custom HTTP client, `nil` uses `http.DefaultClient`
|
||||
|
||||
### NewLocal
|
||||
|
||||
```go
|
||||
func NewLocal(sb sandbox.Sandbox) Proxy
|
||||
```
|
||||
|
||||
Creates a Proxy that resolves URLs by inspecting the container's port mappings via `sandbox.Inspect`. Looks up the host port bound to the requested container port.
|
||||
|
||||
Returns an error if the requested port is not mapped.
|
||||
|
||||
## Methods
|
||||
|
||||
### URL
|
||||
|
||||
```go
|
||||
URL(ctx context.Context, containerID string, port int, path string) (string, error)
|
||||
```
|
||||
|
||||
Resolves an HTTP URL to reach a service running on `port` inside the given container.
|
||||
|
||||
**Remote example:** container `abc123` port `3000` path `/api/health`
|
||||
→ `http://tai-host:8080/abc123:3000/api/health`
|
||||
|
||||
**Local example:** container `abc123` port `3000` mapped to host port `32768`
|
||||
→ `http://127.0.0.1:32768/api/health`
|
||||
|
||||
### Healthz
|
||||
|
||||
```go
|
||||
Healthz(ctx context.Context) error
|
||||
```
|
||||
|
||||
Checks the health of the proxy backend.
|
||||
|
||||
- **Remote**: sends `GET /healthz` to the Tai HTTP proxy server
|
||||
- **Local**: always returns `nil` (no external dependency)
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
c, _ := tai.New("tai://192.168.1.100")
|
||||
defer c.Close()
|
||||
|
||||
// Get URL for a web service running on port 3000
|
||||
url, _ := c.Proxy().URL(ctx, containerID, 3000, "/api/status")
|
||||
resp, _ := http.Get(url)
|
||||
|
||||
// Health check
|
||||
if err := c.Proxy().Healthz(ctx); err != nil {
|
||||
log.Fatal("Tai HTTP proxy is down:", err)
|
||||
}
|
||||
```
|
||||
182
tai/docs/sandbox.md
Normal file
182
tai/docs/sandbox.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Package `sandbox`
|
||||
|
||||
Container lifecycle management. Provides a unified `Sandbox` interface with three implementations:
|
||||
|
||||
| Implementation | Constructor | Backend | Mode |
|
||||
|----------------|-------------|---------|------|
|
||||
| **Local** | `NewLocal(addr)` | Direct Docker daemon | Local |
|
||||
| **Docker** | `NewDocker(addr)` | Docker via Tai proxy | Remote |
|
||||
| **K8s** | `NewK8s(addr, opts)` | Kubernetes via Tai proxy | Remote |
|
||||
|
||||
## Interface
|
||||
|
||||
```go
|
||||
type Sandbox interface {
|
||||
Create(ctx context.Context, opts CreateOptions) (id string, err error)
|
||||
Start(ctx context.Context, id string) error
|
||||
Stop(ctx context.Context, id string, timeout time.Duration) error
|
||||
Remove(ctx context.Context, id string, force bool) error
|
||||
Exec(ctx context.Context, id string, cmd []string, opts ExecOptions) (*ExecResult, error)
|
||||
Inspect(ctx context.Context, id string) (*ContainerInfo, error)
|
||||
List(ctx context.Context, opts ListOptions) ([]ContainerInfo, error)
|
||||
Close() error
|
||||
}
|
||||
```
|
||||
|
||||
## Constructors
|
||||
|
||||
### NewLocal
|
||||
|
||||
```go
|
||||
func NewLocal(addr string) (Sandbox, error)
|
||||
```
|
||||
|
||||
Connects directly to a Docker daemon. `addr` can be:
|
||||
- `""` — platform default (Unix socket on Linux/macOS, named pipe on Windows)
|
||||
- `"unix:///var/run/docker.sock"` — explicit Unix socket
|
||||
- `"tcp://host:port"` — explicit TCP
|
||||
|
||||
Pings the daemon on creation; returns an error if unreachable.
|
||||
|
||||
### NewDocker
|
||||
|
||||
```go
|
||||
func NewDocker(addr string) (Sandbox, error)
|
||||
```
|
||||
|
||||
Connects to Docker Engine API through Tai's Docker proxy. `addr` should be `"tcp://tai-host:2375"`.
|
||||
|
||||
### NewK8s
|
||||
|
||||
```go
|
||||
func NewK8s(addr string, opts ...K8sOption) (Sandbox, error)
|
||||
```
|
||||
|
||||
Connects to Kubernetes through Tai's TCP proxy. Each sandbox maps to a single-container Pod.
|
||||
|
||||
**Parameters:**
|
||||
- `addr` — `"host:port"` pointing to Tai's K8s proxy endpoint
|
||||
- `opts.KubeConfig` — path to kubeconfig file (**required**). Relative paths are resolved to absolute.
|
||||
- `opts.Namespace` — Kubernetes namespace (default `"default"`)
|
||||
|
||||
The constructor overrides the kubeconfig's `server` field to point at `addr`, enables insecure TLS (since Tai does TCP passthrough), and verifies connectivity by querying the namespace.
|
||||
|
||||
All pods created by K8s sandbox are labeled with `managed-by: yao-tai-sdk`.
|
||||
|
||||
## Types
|
||||
|
||||
### CreateOptions
|
||||
|
||||
```go
|
||||
type CreateOptions struct {
|
||||
Name string // container/pod name
|
||||
Image string // container image
|
||||
Cmd []string // entrypoint command
|
||||
Env map[string]string // environment variables
|
||||
Binds []string // volume binds (Docker only)
|
||||
WorkingDir string // working directory
|
||||
Memory int64 // memory limit in bytes, 0 = no limit
|
||||
CPUs float64 // CPU limit, 0 = no limit
|
||||
VNC bool // enable VNC port mapping (Local only)
|
||||
Ports []PortMapping // port mappings (Docker only)
|
||||
}
|
||||
```
|
||||
|
||||
### PortMapping
|
||||
|
||||
```go
|
||||
type PortMapping struct {
|
||||
ContainerPort int // port inside the container
|
||||
HostPort int // port on the host, 0 = random
|
||||
HostIP string // host bind address, default "127.0.0.1"
|
||||
Protocol string // "tcp" (default) or "udp"
|
||||
}
|
||||
```
|
||||
|
||||
### ContainerInfo
|
||||
|
||||
```go
|
||||
type ContainerInfo struct {
|
||||
ID string // container/pod ID
|
||||
Name string // container/pod name
|
||||
Image string // image name
|
||||
Status string // "created", "running", "exited", "removing" (Docker)
|
||||
// "Pending", "Running", "Succeeded", "Failed" (K8s)
|
||||
IP string // container/pod IP address
|
||||
Ports []PortMapping // mapped ports (Docker only)
|
||||
}
|
||||
```
|
||||
|
||||
### ExecOptions
|
||||
|
||||
```go
|
||||
type ExecOptions struct {
|
||||
WorkDir string // override working directory
|
||||
Env map[string]string // additional environment variables
|
||||
}
|
||||
```
|
||||
|
||||
### ExecResult
|
||||
|
||||
```go
|
||||
type ExecResult struct {
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
}
|
||||
```
|
||||
|
||||
### ListOptions
|
||||
|
||||
```go
|
||||
type ListOptions struct {
|
||||
All bool // include stopped containers
|
||||
Labels map[string]string // filter by labels
|
||||
}
|
||||
```
|
||||
|
||||
### K8sOption
|
||||
|
||||
```go
|
||||
type K8sOption struct {
|
||||
Namespace string // default "default"
|
||||
KubeConfig string // path to kubeconfig file (required)
|
||||
}
|
||||
```
|
||||
|
||||
## Behavioral Differences
|
||||
|
||||
| Behavior | Docker (Local/Remote) | K8s |
|
||||
|----------|----------------------|-----|
|
||||
| `Create` returns | container ID (hash) | pod name |
|
||||
| `Start` | starts a stopped container | polls until pod leaves Pending (up to 30s) |
|
||||
| `Stop` | stops with timeout, container persists | deletes the pod with grace period |
|
||||
| `Remove(force=true)` | force-removes | deletes with grace period 0 |
|
||||
| `Exec` | Docker exec API | `kubectl exec` via SPDY |
|
||||
| `Inspect.Ports` | populated from Docker | always empty |
|
||||
| `List` | filters by `tai-sdk=true` label | filters by `managed-by=yao-tai-sdk` label |
|
||||
| `Binds` | supported | not supported |
|
||||
| `VNC` flag | auto port-maps 6080 on macOS/Windows | not applicable |
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
sb, _ := sandbox.NewLocal("")
|
||||
defer sb.Close()
|
||||
|
||||
id, _ := sb.Create(ctx, sandbox.CreateOptions{
|
||||
Name: "worker",
|
||||
Image: "alpine:latest",
|
||||
Cmd: []string{"sleep", "300"},
|
||||
Env: map[string]string{"FOO": "bar"},
|
||||
Memory: 256 * 1024 * 1024, // 256 MB
|
||||
})
|
||||
|
||||
sb.Start(ctx, id)
|
||||
|
||||
result, _ := sb.Exec(ctx, id, []string{"echo", "$FOO"}, sandbox.ExecOptions{})
|
||||
fmt.Println(result.Stdout)
|
||||
|
||||
sb.Stop(ctx, id, 10*time.Second)
|
||||
sb.Remove(ctx, id, false)
|
||||
```
|
||||
94
tai/docs/vnc.md
Normal file
94
tai/docs/vnc.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Package `vnc`
|
||||
|
||||
VNC WebSocket URL resolution. Resolves WebSocket URLs for VNC sessions running inside containers, enabling remote desktop access to sandbox environments.
|
||||
|
||||
## Interface
|
||||
|
||||
```go
|
||||
type VNC interface {
|
||||
URL(ctx context.Context, containerID string) (string, error)
|
||||
Ping(ctx context.Context, containerID string) error
|
||||
}
|
||||
```
|
||||
|
||||
## Implementations
|
||||
|
||||
| Implementation | Constructor | Mode | URL Pattern |
|
||||
|----------------|-------------|------|-------------|
|
||||
| **Remote** | `NewRemote(host, port, hc)` | Via Tai VNC router | `ws://tai-host:6080/vnc/{containerID}/ws` |
|
||||
| **Local** | `NewLocal(sb)` | Direct host port lookup | `ws://127.0.0.1:{hostPort}/ws` |
|
||||
|
||||
## Constructors
|
||||
|
||||
### NewRemote
|
||||
|
||||
```go
|
||||
func NewRemote(host string, port int, hc *http.Client) VNC
|
||||
```
|
||||
|
||||
Creates a VNC that routes through Tai's VNC WebSocket router.
|
||||
|
||||
- `host` — Tai server hostname/IP
|
||||
- `port` — Tai VNC router port (default 6080)
|
||||
- `hc` — custom HTTP client for Ping, `nil` uses `http.DefaultClient`
|
||||
|
||||
### NewLocal
|
||||
|
||||
```go
|
||||
func NewLocal(sb sandbox.Sandbox) VNC
|
||||
```
|
||||
|
||||
Creates a VNC that resolves URLs by inspecting the container's port mappings. Looks for container port **6080** (the standard noVNC port) in the port mappings.
|
||||
|
||||
Returns an error if port 6080 is not mapped. On macOS and Windows (Docker Desktop), the Local sandbox automatically maps port 6080 when `CreateOptions.VNC` is `true`.
|
||||
|
||||
## Methods
|
||||
|
||||
### URL
|
||||
|
||||
```go
|
||||
URL(ctx context.Context, containerID string) (string, error)
|
||||
```
|
||||
|
||||
Returns a WebSocket URL for connecting to the container's VNC session.
|
||||
|
||||
**Remote:** `ws://tai-host:6080/vnc/abc123/ws`
|
||||
**Local:** `ws://127.0.0.1:32769/ws`
|
||||
|
||||
### Ping
|
||||
|
||||
```go
|
||||
Ping(ctx context.Context, containerID string) error
|
||||
```
|
||||
|
||||
Checks if the VNC endpoint is reachable by making an HTTP GET request to the WebSocket URL. Useful for verifying that the VNC server inside the container is ready before connecting a client.
|
||||
|
||||
- **Remote**: sends GET to `http://tai-host:6080/vnc/{containerID}/ws`
|
||||
- **Local**: resolves the host port via Inspect, then sends GET
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
c, _ := tai.New("tai://192.168.1.100")
|
||||
defer c.Close()
|
||||
|
||||
// Create a sandbox with VNC enabled
|
||||
id, _ := c.Sandbox().Create(ctx, sandbox.CreateOptions{
|
||||
Name: "desktop",
|
||||
Image: "yaoapp/sandbox-claude:latest",
|
||||
VNC: true,
|
||||
})
|
||||
c.Sandbox().Start(ctx, id)
|
||||
|
||||
// Wait for VNC to be ready
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := c.VNC().Ping(ctx, id); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// Get the WebSocket URL for a noVNC client
|
||||
url, _ := c.VNC().URL(ctx, id)
|
||||
fmt.Println(url) // ws://192.168.1.100:6080/vnc/desktop/ws
|
||||
```
|
||||
120
tai/docs/volume.md
Normal file
120
tai/docs/volume.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Package `volume`
|
||||
|
||||
File IO and directory synchronization. Provides a `Volume` interface with two implementations:
|
||||
|
||||
| Implementation | Constructor | Backend | Mode |
|
||||
|----------------|-------------|---------|------|
|
||||
| **Local** | `NewLocal(root)` | Direct filesystem | Local |
|
||||
| **Remote** | `NewRemote(conn)` | gRPC to Tai :9100 | Remote |
|
||||
|
||||
## Interface
|
||||
|
||||
```go
|
||||
type Volume interface {
|
||||
ReadFile(ctx context.Context, sessionID, path string) (data []byte, perm os.FileMode, err error)
|
||||
WriteFile(ctx context.Context, sessionID, path string, data []byte, perm os.FileMode) error
|
||||
Stat(ctx context.Context, sessionID, path string) (*FileInfo, error)
|
||||
ListDir(ctx context.Context, sessionID, path string) ([]FileInfo, error)
|
||||
Remove(ctx context.Context, sessionID, path string, recursive bool) error
|
||||
Rename(ctx context.Context, sessionID, oldPath, newPath string) error
|
||||
MkdirAll(ctx context.Context, sessionID, path string) error
|
||||
|
||||
SyncPush(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error)
|
||||
SyncPull(ctx context.Context, sessionID, localDir string, opts ...SyncOption) (*SyncResult, error)
|
||||
|
||||
Close() error
|
||||
}
|
||||
```
|
||||
|
||||
All paths are **relative** to the session's workspace root. The `sessionID` identifies the workspace partition — in Local mode this maps to `<root>/<sessionID>/`, in Remote mode the Tai server manages the path.
|
||||
|
||||
## Constructors
|
||||
|
||||
### NewLocal
|
||||
|
||||
```go
|
||||
func NewLocal(root string) Volume
|
||||
```
|
||||
|
||||
Creates a Volume backed by the local filesystem. Files are stored under `<root>/<sessionID>/`.
|
||||
|
||||
### NewRemote
|
||||
|
||||
```go
|
||||
func NewRemote(conn *grpc.ClientConn) Volume
|
||||
```
|
||||
|
||||
Creates a Volume backed by Tai's gRPC Volume service. The connection should target Tai's gRPC port (default 9100). Uses lz4 compression for `SyncPush`/`SyncPull` bulk transfers.
|
||||
|
||||
## Types
|
||||
|
||||
### FileInfo
|
||||
|
||||
```go
|
||||
type FileInfo struct {
|
||||
Path string
|
||||
Size int64
|
||||
Mtime time.Time
|
||||
Mode fs.FileMode
|
||||
IsDir bool
|
||||
}
|
||||
```
|
||||
|
||||
### SyncResult
|
||||
|
||||
```go
|
||||
type SyncResult struct {
|
||||
FilesSynced int
|
||||
BytesTransferred int64
|
||||
Duration time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
## Sync Options
|
||||
|
||||
```go
|
||||
volume.WithForceFull() // skip snapshot cache, diff against actual disk
|
||||
volume.WithExcludes("*.log", ".DS_Store") // glob patterns to exclude
|
||||
```
|
||||
|
||||
## File Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `ReadFile` | Read file contents and permissions |
|
||||
| `WriteFile` | Write file with specified permissions (creates parent dirs) |
|
||||
| `Stat` | Get file/directory metadata |
|
||||
| `ListDir` | List directory contents (one level) |
|
||||
| `Remove` | Delete file or directory (`recursive=true` for tree) |
|
||||
| `Rename` | Move/rename a file or directory |
|
||||
| `MkdirAll` | Create directory tree |
|
||||
|
||||
## Sync Operations
|
||||
|
||||
| Method | Direction | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `SyncPush` | local → remote | Upload a local directory to the session workspace |
|
||||
| `SyncPull` | remote → local | Download the session workspace to a local directory |
|
||||
|
||||
Both sync methods use snapshot-based diffing to transfer only changed files. Use `WithForceFull()` to bypass the cache and force a full transfer.
|
||||
|
||||
Remote sync uses **lz4 compression** on the wire, streaming files via gRPC bidirectional streaming.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
vol := volume.NewLocal("/data/volumes")
|
||||
defer vol.Close()
|
||||
|
||||
// Write a file
|
||||
vol.WriteFile(ctx, "session-1", "main.py", []byte("print('hi')"), 0644)
|
||||
|
||||
// Read it back
|
||||
data, perm, _ := vol.ReadFile(ctx, "session-1", "main.py")
|
||||
|
||||
// Sync a local directory to the session
|
||||
result, _ := vol.SyncPush(ctx, "session-1", "/tmp/project",
|
||||
volume.WithExcludes("node_modules", ".git"),
|
||||
)
|
||||
fmt.Printf("synced %d files (%d bytes)\n", result.FilesSynced, result.BytesTransferred)
|
||||
```
|
||||
93
tai/docs/workspace.md
Normal file
93
tai/docs/workspace.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Package `workspace`
|
||||
|
||||
Provides an `fs.FS`-compatible filesystem abstraction over `volume.Volume`. This allows session workspaces to be used with any Go standard library function that accepts `fs.FS`, such as `fs.WalkDir`, `template.ParseFS`, or `http.FS`.
|
||||
|
||||
## Interface
|
||||
|
||||
```go
|
||||
type FS interface {
|
||||
fs.FS // Open(name) (fs.File, error)
|
||||
fs.StatFS // Stat(name) (fs.FileInfo, error)
|
||||
fs.ReadFileFS // ReadFile(name) ([]byte, error)
|
||||
fs.ReadDirFS // ReadDir(name) ([]fs.DirEntry, error)
|
||||
io.Closer // Close() error
|
||||
|
||||
WriteFile(name string, data []byte, perm os.FileMode) error
|
||||
Remove(name string) error
|
||||
RemoveAll(name string) error
|
||||
Rename(oldname, newname string) error
|
||||
MkdirAll(name string, perm os.FileMode) error
|
||||
}
|
||||
```
|
||||
|
||||
## Constructor
|
||||
|
||||
```go
|
||||
func New(vol volume.Volume, sessionID string) FS
|
||||
```
|
||||
|
||||
Creates an FS backed by the given Volume for the specified session. The returned FS works transparently whether `vol` is Local or Remote.
|
||||
|
||||
Typically accessed through the top-level client:
|
||||
|
||||
```go
|
||||
c, _ := tai.New("tai://host")
|
||||
ws := c.Workspace("session-123")
|
||||
```
|
||||
|
||||
## Read Operations (fs.FS compatible)
|
||||
|
||||
All read operations comply with the `fs.FS` contract. Paths must be valid according to `fs.ValidPath` — forward slashes, no leading slash, no `..` segments.
|
||||
|
||||
| Method | Standard Interface | Description |
|
||||
|--------|--------------------|-------------|
|
||||
| `Open(name)` | `fs.FS` | Opens a file or directory |
|
||||
| `Stat(name)` | `fs.StatFS` | Returns file metadata |
|
||||
| `ReadFile(name)` | `fs.ReadFileFS` | Reads entire file contents |
|
||||
| `ReadDir(name)` | `fs.ReadDirFS` | Lists directory entries |
|
||||
|
||||
`Open` returns an in-memory `fs.File` for regular files (entire content loaded on open) and a directory handle for directories.
|
||||
|
||||
## Write Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `WriteFile(name, data, perm)` | Write file contents with permissions |
|
||||
| `Remove(name)` | Delete a single file or empty directory |
|
||||
| `RemoveAll(name)` | Delete a file or directory tree recursively |
|
||||
| `Rename(old, new)` | Move/rename a file or directory |
|
||||
| `MkdirAll(name, perm)` | Create directory tree (perm currently unused) |
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
c, _ := tai.New("tai://192.168.1.100")
|
||||
defer c.Close()
|
||||
|
||||
ws := c.Workspace("project-abc")
|
||||
|
||||
// Write files
|
||||
ws.WriteFile("src/main.go", []byte("package main"), 0644)
|
||||
ws.MkdirAll("src/utils", 0755)
|
||||
|
||||
// Read with standard fs.FS
|
||||
data, _ := fs.ReadFile(ws, "src/main.go")
|
||||
|
||||
// Walk the tree
|
||||
fs.WalkDir(ws, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
fmt.Println(path)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Use with Go templates
|
||||
tmpl, _ := template.ParseFS(ws, "templates/*.html")
|
||||
|
||||
// Clean up
|
||||
ws.RemoveAll("src")
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `Open` on a regular file reads the entire content into memory. For large files, prefer `ReadFile` or Volume's `ReadFile` directly.
|
||||
- `Close()` is a no-op — the underlying Volume's lifecycle is managed by the `tai.Client`.
|
||||
- Path validation follows `fs.ValidPath` rules. Invalid paths return `fs.ErrInvalid`.
|
||||
Loading…
Add table
Reference in a new issue