yao/tai/docs/registry.md
Max e633640998 refactor(sandbox/v2): transition from pool to node configuration
- Updated benchmark and test functions to utilize node configurations instead of pool configurations for improved clarity and consistency.
- Refactored related setup functions and test cases to align with the new node-based architecture.
- Adjusted error messages and documentation to reflect the transition from pool to node terminology.

Made-with: Cursor
2026-03-09 03:20:09 +08:00

178 lines
5.2 KiB
Markdown

# Package `registry`
In-memory registry for Tai nodes. Manages both **direct** (network-reachable) and **tunnel** (WebSocket-bridged) connections. Used server-side by Yao to track all connected Tai instances.
## Architecture
```
Direct Mode: Yao ── TCP ──> Tai (gRPC/HTTP/Docker/VNC)
Tunnel Mode: Yao <── WS ── Tai (control channel)
Yao <── WS ── Tai (data channels, on-demand)
```
## Types
### TaiNode
```go
type TaiNode struct {
TaiID string
MachineID string
Version string
Auth AuthInfo
System SystemInfo
Mode string // "direct" | "tunnel"
Addr string // direct: "tai-host"; tunnel: empty
YaoBase string // tunnel: Yao server base URL
Ports map[string]int // {"grpc":19100, "http":8099, ...}
Capabilities map[string]bool // {"docker":true, "host_exec":true}
ControlConn *websocket.Conn // tunnel: WS control channel
Status string // "online" | "offline" | "connecting"
ConnectedAt time.Time
LastPing time.Time
DisplayName string
}
```
### NodeSnapshot
Read-only copy of `TaiNode` safe to use outside locks. Returned by `Get()` and `List()`.
```go
type NodeSnapshot struct {
TaiID, MachineID, Version string
Auth AuthInfo
System SystemInfo
Mode, Addr, YaoBase string
Ports map[string]int
Capabilities map[string]bool
Status string
ConnectedAt, LastPing time.Time
DisplayName string
}
```
### AuthInfo
```go
type AuthInfo struct {
Subject string
UserID string
ClientID string
Scope string
TeamID string
TenantID string
}
```
### SystemInfo
```go
type SystemInfo struct {
OS string `json:"os"`
Arch string `json:"arch"`
Hostname string `json:"hostname"`
NumCPU int `json:"num_cpu"`
TotalMem int64 `json:"total_mem,omitempty"`
}
```
## Registry API
### Init / Global
```go
func Init(logger *slog.Logger)
func Global() *Registry
```
`Init` creates the global singleton (once). `Global` returns it (nil before Init).
### Register / Unregister
```go
func (r *Registry) Register(node *TaiNode)
func (r *Registry) Unregister(taiID string)
```
`Register` adds or replaces a node, setting `Status="online"` and recording timestamps. `Unregister` closes all tunnel listeners and the control connection, then removes the node.
### Query
```go
func (r *Registry) Get(taiID string) (*NodeSnapshot, bool)
func (r *Registry) List() []NodeSnapshot
func (r *Registry) ListByTeam(teamID string) []NodeSnapshot
```
### Heartbeat
```go
func (r *Registry) UpdatePing(taiID string)
```
### Health Check
```go
func (r *Registry) StartHealthCheck(done <-chan struct{}, interval, timeout, cleanupAfter time.Duration)
```
Runs a background goroutine that:
1. Marks direct-mode nodes as `"offline"` if `LastPing` exceeds `timeout`
2. Auto-unregisters nodes that stay offline longer than `timeout + cleanupAfter`
## Tunnel API
For tunnel-mode nodes, the registry manages on-demand TCP-over-WebSocket channels.
### RequestChannel
```go
func (r *Registry) RequestChannel(taiID string, targetPort int) (channelID string, result chan net.Conn, err error)
```
Sends an `{"type":"open", "channel_id":"...", "target_port":...}` command to the node's control WebSocket. Returns a channel that receives the `net.Conn` when Tai connects back with the data channel. Times out after 30 seconds.
### AcceptDataChannel
```go
func (r *Registry) AcceptDataChannel(channelID, taiID string, conn net.Conn) error
```
Called when Tai establishes a data WebSocket for a pending channel. Validates `taiID` ownership and delivers the connection to the waiting `RequestChannel` caller.
### WriteControlJSON
```go
func (r *Registry) WriteControlJSON(taiID string, v interface{}) error
```
Thread-safe JSON write to a node's control WebSocket.
### OpenLocalListener
```go
func (r *Registry) OpenLocalListener(taiID string, targetPort int) (net.Listener, error)
```
Creates a `127.0.0.1:0` TCP listener. Every accepted connection is automatically bridged through the tunnel to `targetPort` on the Tai node. Returns the listener so the caller can read `ln.Addr()` to get the ephemeral port.
## Connection Flow (Tunnel)
```
1. Tai → Yao: WebSocket upgrade to GET /ws/tai (Bearer auth)
2. Tai → Yao: sends {"type":"register", "tai_id":"xxx", ...} on WS
3. Yao: Register(node) with Mode="tunnel", ControlConn=ws
4. Yao → Tai: sends {"type":"registered", "tai_id":"xxx"}
5. Client → Yao: tai.New("tunnel://tai-abc123")
6. Yao: OpenLocalListener("tai-abc123", 19100) → 127.0.0.1:54321
7. Yao: grpc.Dial("passthrough:///127.0.0.1:54321") → triggers accept
8. Yao: RequestChannel("tai-abc123", 19100) → sends {"type":"open"} on control WS
9. Tai: receives "open", dials localhost:19100, connects data WS to GET /ws/tai/data/:channel_id
10. Yao: AcceptDataChannel(channelID, taiID, conn) → bridges local TCP ↔ data WS
11. gRPC traffic flows transparently through the tunnel
```
### Keep-alive
Tai sends `{"type":"ping"}` periodically on the control channel. Yao replies `{"type":"pong"}` and updates `LastPing`.