feat(agent): add WebSocket transport with HTTP fallback
Add Transport interface abstraction supporting WebSocket (via CF Durable Objects) and HTTP (direct to origin) with automatic failover. - Transport interface: Register, SendHeartbeat, SendProgress, Events() - HTTPTransport: thin adapter over existing Client - WSTransport: gorilla/websocket with auth handshake, readLoop, reconnect - HybridTransport: tries WS first, falls back to HTTP, reconnects in bg - Daemon refactored to always use Transport (no dual-path forks) - ProgressReporter accepts StatusReporter interface - deriveWSURL skips localhost/dev (returns "" → HTTP-only) - API key passed in WS query param for connection auth - Fixed: reconnectOnce race (mutex+bool), authDone double-close (sync.Once) - Fixed: forwardWSEvents goroutine leak (select with stop signal) - 20 transport tests + 2 E2E tests (full lifecycle, hybrid failover)
This commit is contained in:
parent
5e80911501
commit
5f337eebd7
10 changed files with 1646 additions and 64 deletions
53
internal/agent/transport.go
Normal file
53
internal/agent/transport.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package agent
|
||||
|
||||
import "context"
|
||||
|
||||
// Transport abstracts the communication protocol between the agent and server.
|
||||
// Both WebSocket (via CF Durable Object) and HTTP (direct to origin) implement this.
|
||||
type Transport interface {
|
||||
// Connect establishes the transport connection.
|
||||
Connect(ctx context.Context) error
|
||||
|
||||
// Close tears down the connection gracefully.
|
||||
Close() error
|
||||
|
||||
// Mode returns the current transport mode ("ws" or "http").
|
||||
Mode() string
|
||||
|
||||
// Register sends agent registration and returns user info + features.
|
||||
Register(ctx context.Context, req RegisterRequest) (*RegisterResponse, error)
|
||||
|
||||
// SendHeartbeat sends a periodic keep-alive.
|
||||
SendHeartbeat(ctx context.Context, req HeartbeatRequest) (*HeartbeatResponse, error)
|
||||
|
||||
// SendProgress reports download progress for a task.
|
||||
SendProgress(ctx context.Context, update StatusUpdate) (*StatusResponse, error)
|
||||
|
||||
// ClaimTasks polls for new tasks (HTTP mode only; WS receives via Events).
|
||||
ClaimTasks(ctx context.Context, agentID string) (*TasksResponse, error)
|
||||
|
||||
// Deregister notifies the server of graceful shutdown.
|
||||
Deregister(ctx context.Context, agentID string) error
|
||||
|
||||
// ReportUpgradeResult reports upgrade outcome.
|
||||
ReportUpgradeResult(ctx context.Context, result UpgradeResult) error
|
||||
|
||||
// Events returns a channel that emits server-initiated events.
|
||||
// In HTTP mode this channel is never written to (polling handles it).
|
||||
// In WS mode, tasks/upgrade/control arrive here.
|
||||
Events() <-chan ServerEvent
|
||||
}
|
||||
|
||||
// ServerEvent represents a server-initiated message received via WebSocket.
|
||||
type ServerEvent struct {
|
||||
Type string // "tasks", "upgrade", "control", "disconnected"
|
||||
Tasks *TasksResponse // populated when Type == "tasks"
|
||||
Upgrade *UpgradeSignal // populated when Type == "upgrade"
|
||||
Control *ControlAction // populated when Type == "control"
|
||||
}
|
||||
|
||||
// ControlAction represents a server push for task control.
|
||||
type ControlAction struct {
|
||||
Action string `json:"action"` // "pause", "resume", "cancel", "stream"
|
||||
TaskID string `json:"taskId"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue