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>
184 lines
4.6 KiB
Go
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")
|
|
}
|