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:
Max 2026-03-03 23:50:47 +08:00
parent ac87852f4e
commit 7772cc588f
7 changed files with 686 additions and 147 deletions

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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`.