Files
tyto/backend/cmd/server/main.go
vikingowl 80f6e788f4 feat: implement server hub for multi-device agent management
Server Package (internal/server/):
- Registry: Agent registration with approval workflow, persistence
- Hub: Connection manager for connected agents, message routing
- GRPCServer: mTLS-enabled gRPC server with interceptors
- SSEBridge: Bridges agent metrics to browser SSE clients

Registry Features:
- JSON file-based persistence
- Agent lifecycle: pending -> approved -> connected -> offline
- Revocation support for certificate-based agent removal
- Automatic last-seen tracking

Hub Features:
- Bidirectional gRPC stream handling
- MetricsSubscriber interface for metric distribution
- Stale connection detection and cleanup
- Broadcast and per-agent command sending

gRPC Server:
- Unary and stream interceptors for auth
- Agent ID extraction from mTLS certificates
- Delegation to Hub for business logic

Agent Management API:
- GET/DELETE /api/v1/agents - List/remove agents
- GET /api/v1/agents/pending - Pending approvals
- POST /api/v1/agents/pending/:id/approve|reject
- GET /api/v1/agents/:id/metrics - Latest agent metrics
- GET /api/v1/agents/connected - Connected agents

Server Mode Startup:
- Full initialization of registry, hub, gRPC, SSE bridge
- Graceful shutdown with signal handling
- Agent mode now uses the agent package

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 07:53:23 +01:00

184 lines
4.6 KiB
Go

package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"tyto/internal/agent"
"tyto/internal/api"
"tyto/internal/config"
"tyto/internal/server"
"tyto/internal/sse"
)
func main() {
cfg := config.Load()
switch {
case cfg.IsAgent():
runAgent(cfg)
case cfg.IsServer():
runServer(cfg)
default:
runStandalone(cfg)
}
}
// runStandalone starts Tyto in single-host monitoring mode.
// This is the default mode with no database or agent support.
func runStandalone(cfg *config.Config) {
log.Printf("Starting Tyto in standalone mode on port %s", cfg.Port)
log.Printf("Reading from: proc=%s, sys=%s", cfg.ProcPath, cfg.SysPath)
log.Printf("Default refresh interval: %s", cfg.RefreshInterval)
if cfg.AuthEnabled {
log.Printf("Basic authentication enabled for user: %s", cfg.AuthUser)
}
if cfg.TLSEnabled {
log.Printf("TLS enabled with cert: %s", cfg.TLSCertFile)
}
broker := sse.NewBroker(cfg)
go broker.Run()
server := api.NewServer(cfg, broker)
var err error
if cfg.TLSEnabled {
log.Printf("Starting HTTPS server on port %s", cfg.Port)
err = server.RunTLS(cfg.TLSCertFile, cfg.TLSKeyFile)
} else {
err = server.Run()
}
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
// runServer starts Tyto in full server mode with database, agents, and auth.
func runServer(cfg *config.Config) {
log.Printf("Starting Tyto in server mode on port %s", cfg.Port)
log.Printf("gRPC port for agents: %d", cfg.Server.GRPCPort)
log.Printf("Database: %s", cfg.Database.Type)
// Set up signal handling
_, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Initialize agent registry
registryPath := "/var/lib/tyto/agents.json"
if cfg.Database.SQLitePath != "" {
registryPath = cfg.Database.SQLitePath + ".agents.json"
}
registry := server.NewRegistry(registryPath)
log.Printf("Agent registry initialized: %s", registryPath)
// Initialize Hub
hubConfig := &server.HubConfig{
RequireApproval: cfg.Server.Registration.RequireApproval,
AutoApprove: cfg.Server.Registration.AutoEnabled && !cfg.Server.Registration.RequireApproval,
}
hub := server.NewHub(registry, hubConfig)
hub.Start()
defer hub.Stop()
log.Println("Agent hub started")
// Initialize SSE bridge for multi-device streaming
bridge := server.NewSSEBridge(hub)
bridge.Start()
defer bridge.Stop()
// Initialize gRPC server for agent connections
grpcServer, err := server.NewGRPCServer(hub, &cfg.Server)
if err != nil {
log.Fatalf("Failed to create gRPC server: %v", err)
}
// Start gRPC server in background
go func() {
log.Printf("Starting gRPC server on port %d", cfg.Server.GRPCPort)
if err := grpcServer.Start(cfg.Server.GRPCPort); err != nil {
log.Printf("gRPC server error: %v", err)
}
}()
defer grpcServer.Stop()
// Initialize SSE broker for local metrics (also runs in server mode)
broker := sse.NewBroker(cfg)
go broker.Run()
// Initialize HTTP API server with agent management
apiServer := api.NewServer(cfg, broker)
// Add agent API routes
agentAPI := api.NewAgentAPI(registry, hub)
agentAPI.RegisterRoutes(apiServer.Router().Group("/api/v1"))
// Start HTTP server in background
go func() {
var err error
if cfg.TLSEnabled {
log.Printf("Starting HTTPS server on port %s", cfg.Port)
err = apiServer.RunTLS(cfg.TLSCertFile, cfg.TLSKeyFile)
} else {
log.Printf("Starting HTTP server on port %s", cfg.Port)
err = apiServer.Run()
}
if err != nil {
log.Printf("HTTP server error: %v", err)
}
}()
// Wait for shutdown signal
<-sigCh
log.Println("Shutting down server...")
cancel()
}
// runAgent starts Tyto as a lightweight agent that reports to a central server.
func runAgent(cfg *config.Config) {
if cfg.Agent.ID == "" {
log.Fatal("Agent ID is required in agent mode (set TYTO_AGENT_ID)")
}
if cfg.Agent.ServerURL == "" {
log.Fatal("Server URL is required in agent mode (set TYTO_SERVER_URL)")
}
log.Printf("Starting Tyto agent '%s'", cfg.Agent.ID)
log.Printf("Reporting to: %s", cfg.Agent.ServerURL)
log.Printf("Collection interval: %s", cfg.Agent.Interval)
// Set up signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Create agent
a := agent.New(cfg)
// Handle shutdown signal
go func() {
<-sigCh
log.Println("Received shutdown signal, stopping agent...")
a.Stop()
cancel()
}()
// Run agent
if err := a.Run(ctx); err != nil && err != context.Canceled {
log.Fatalf("Agent error: %v", err)
}
log.Println("Agent stopped")
}