Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt
Use this file to discover all available pages before exploring further.
Duet’s SSH server is built on Charm’s Wish framework, which provides a composable middleware system for building SSH applications.
Server Structure
The server is defined in /internal/server/server.go:
type Server struct {
addr string
hostKeyPath string
roomManager *room.Manager
logger *log.Logger
}
Initialization
The server is created with connection details and a room manager instance:
func New(addr, hostKeyPath, workerURL string) *Server {
logger := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "duet",
})
var aiClient *ai.Client
if workerURL != "" {
aiClient = ai.NewClient(workerURL)
}
mgr := room.NewManager(workerURL, aiClient, logger)
return &Server{
addr: addr,
hostKeyPath: hostKeyPath,
roomManager: mgr,
logger: logger,
}
}
Key Points:
- Default address is
:2222
- Host key path defaults to
.ssh/id_ed25519 (auto-generated if missing)
- AI client is optional (only created if worker URL provided)
- Room manager is shared across all connections
Wish Server Configuration
The server is configured with Wish middleware:
func (s *Server) Start() error {
srv, err := wish.NewServer(
wish.WithAddress(s.addr),
wish.WithHostKeyPath(s.hostKeyPath),
wish.WithMiddleware(
bubbletea.Middleware(s.teaHandler),
logging.Middleware(),
),
)
if err != nil {
return fmt.Errorf("failed to create server: %w", err)
}
// ...
}
Middleware Stack
Duet uses two middleware layers:
- Bubble Tea Middleware: Handles TUI creation for each SSH session
- Logging Middleware: Logs connection events and errors
Middleware executes in order from top to bottom. The Bubble Tea middleware is the primary handler.
Bubble Tea Handler
The teaHandler function is called for each new SSH connection:
func (s *Server) teaHandler(sess ssh.Session) (tea.Model, []tea.ProgramOption) {
username := sess.User()
if username == "" {
username = "guest"
}
renderer := bubbletea.MakeRenderer(sess)
pty, _, _ := sess.Pty()
if pty.Term == "xterm-ghostty" {
renderer.SetColorProfile(termenv.TrueColor)
}
s.logger.Info("final renderer",
"profile", renderer.ColorProfile(),
"hasDark", renderer.HasDarkBackground(),
)
return ui.New(renderer, s.roomManager, username), []tea.ProgramOption{
tea.WithAltScreen(),
}
}
- Username: Extracted from SSH session (defaults to “guest”)
- Renderer: Created per-session for style rendering
- PTY Info: Terminal type and color profile detection
- Alt Screen: Uses alternate screen buffer (terminal state is preserved on exit)
Color Profile Detection
The handler detects terminal capabilities:
if pty.Term == "xterm-ghostty" {
renderer.SetColorProfile(termenv.TrueColor)
}
This ensures proper color rendering for clients with different terminal emulators.
Lifecycle Management
Graceful Shutdown
The server listens for interrupt signals and shuts down gracefully:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
s.logger.Info("Starting SSH server", "address", s.addr)
if err := srv.ListenAndServe(); err != nil {
s.logger.Error("Server error", "error", err)
}
}()
<-ctx.Done()
s.logger.Info("Shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
Shutdown Process:
- Wait for SIGINT or SIGTERM signal
- Log shutdown message
- Create 10-second timeout context
- Call
srv.Shutdown() to close active connections
- Return any shutdown errors
Connection Lifecycle
SSH Client Connect
↓
Wish accepts connection
↓
Middleware stack processes session
↓
teaHandler creates Bubble Tea model
↓
tea.NewProgram(model).Run()
↓
Model.Init() → ScreenLaunch
↓
User interacts (create/join room)
↓
User disconnects or quits
↓
Model.cleanup() called
↓
room.LeaveRoom() removes client
↓
SSH session closes
Authentication
Duet uses public key authentication via the host key:
# Start server (generates host key if missing)
./duet -addr :2222 -hostkey .ssh/id_ed25519
# Connect as any user (username becomes display name)
ssh user@localhost -p 2222
Host Key Generation
Wish automatically generates an Ed25519 host key if the file doesn’t exist:
wish.WithHostKeyPath(s.hostKeyPath)
The key is stored at the specified path and reused for subsequent starts.
Per-Session Isolation
Each SSH session gets:
- Unique Bubble Tea model instance (
ui.New(...))
- Unique client ID (
uuid.New().String())
- Separate event channel for room notifications
- Independent terminal subscription when joining a room
Shared across sessions:
- Room Manager (singleton)
- Terminal instances (one per room, shared by all clients in that room)
- AI Client (if configured)
Configuration Options
Command-line flags in main.go:
addr := flag.String("addr", ":2222", "SSH server address")
hostKeyPath := flag.String("hostkey", ".ssh/id_ed25519", "Path to SSH host key")
workerURL := flag.String("worker", "", "Duet CF Worker base URL")
Example Usage
# Default configuration
./duet
# Custom port and host key
./duet -addr :3000 -hostkey /etc/duet/host_key
# With AI worker integration
./duet -worker https://duet-worker.example.workers.dev
Error Handling
The server handles errors at multiple levels:
// Server creation errors
if err := srv.ListenAndServe(); err != nil {
s.logger.Error("Server error", "error", err)
}
// Shutdown errors
if err := srv.Shutdown(shutdownCtx); err != nil {
return err
}
Client-level errors are handled in the Bubble Tea model’s Update method via ErrorMsg messages.
Security Considerations
- Host Key: Securely store the host key file with appropriate permissions (0600)
- No Password Auth: Only public key authentication is supported
- Workspace Isolation: Each room uses a separate workspace directory
- PTY Restrictions: Terminal processes run in isolated directories
For production deployments, consider:
- Rate limiting connections
- Authentication via SSH keys
- Network-level access controls
- Monitoring and logging