package api import ( "net/http" "github.com/gin-gonic/gin" "tyto/internal/server" ) // AgentAPI handles agent management endpoints. type AgentAPI struct { registry *server.Registry hub *server.Hub } // NewAgentAPI creates a new agent API handler. func NewAgentAPI(registry *server.Registry, hub *server.Hub) *AgentAPI { return &AgentAPI{ registry: registry, hub: hub, } } // RegisterRoutes adds agent management routes to a router group. func (a *AgentAPI) RegisterRoutes(group *gin.RouterGroup) { agents := group.Group("/agents") { agents.GET("", a.listAgents) agents.GET("/:id", a.getAgent) agents.DELETE("/:id", a.removeAgent) agents.POST("/:id/revoke", a.revokeAgent) // Pending registrations agents.GET("/pending", a.listPending) agents.POST("/pending/:id/approve", a.approveAgent) agents.POST("/pending/:id/reject", a.rejectAgent) // Connected agents agents.GET("/connected", a.listConnected) agents.GET("/:id/metrics", a.getAgentMetrics) } } // AgentResponse is the API representation of an agent. type AgentResponse struct { ID string `json:"id"` Name string `json:"name,omitempty"` Hostname string `json:"hostname"` OS string `json:"os"` Architecture string `json:"architecture"` Version string `json:"version"` Capabilities []string `json:"capabilities,omitempty"` Status string `json:"status"` Connected bool `json:"connected"` LastSeen string `json:"lastSeen,omitempty"` RegisteredAt string `json:"registeredAt"` Tags []string `json:"tags,omitempty"` } func agentToResponse(agent *server.AgentRecord, connected bool) AgentResponse { lastSeen := "" if !agent.LastSeen.IsZero() { lastSeen = agent.LastSeen.Format("2006-01-02T15:04:05Z07:00") } registeredAt := "" if !agent.RegisteredAt.IsZero() { registeredAt = agent.RegisteredAt.Format("2006-01-02T15:04:05Z07:00") } return AgentResponse{ ID: agent.ID, Name: agent.Name, Hostname: agent.Hostname, OS: agent.OS, Architecture: agent.Architecture, Version: agent.Version, Capabilities: agent.Capabilities, Status: string(agent.Status), Connected: connected, LastSeen: lastSeen, RegisteredAt: registeredAt, Tags: agent.Tags, } } // listAgents returns all registered agents. func (a *AgentAPI) listAgents(c *gin.Context) { agents := a.registry.List() connectedIDs := make(map[string]bool) if a.hub != nil { for _, id := range a.hub.GetConnectedAgents() { connectedIDs[id] = true } } response := make([]AgentResponse, len(agents)) for i, agent := range agents { response[i] = agentToResponse(agent, connectedIDs[agent.ID]) } c.JSON(http.StatusOK, response) } // getAgent returns a specific agent. func (a *AgentAPI) getAgent(c *gin.Context) { id := c.Param("id") agent, exists := a.registry.Get(id) if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"}) return } connected := false if a.hub != nil { for _, connID := range a.hub.GetConnectedAgents() { if connID == id { connected = true break } } } c.JSON(http.StatusOK, agentToResponse(agent, connected)) } // removeAgent removes an agent registration. func (a *AgentAPI) removeAgent(c *gin.Context) { id := c.Param("id") if err := a.registry.Remove(id); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "removed"}) } // revokeAgent revokes an agent's registration. func (a *AgentAPI) revokeAgent(c *gin.Context) { id := c.Param("id") if err := a.registry.Revoke(id); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "revoked"}) } // listPending returns agents awaiting approval. func (a *AgentAPI) listPending(c *gin.Context) { agents := a.registry.ListPending() response := make([]AgentResponse, len(agents)) for i, agent := range agents { response[i] = agentToResponse(agent, false) } c.JSON(http.StatusOK, response) } // approveAgent approves a pending agent. func (a *AgentAPI) approveAgent(c *gin.Context) { id := c.Param("id") if err := a.registry.Approve(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "approved"}) } // rejectAgent rejects a pending agent. func (a *AgentAPI) rejectAgent(c *gin.Context) { id := c.Param("id") if err := a.registry.Reject(id); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "rejected"}) } // listConnected returns currently connected agents. func (a *AgentAPI) listConnected(c *gin.Context) { if a.hub == nil { c.JSON(http.StatusOK, []AgentResponse{}) return } connectedIDs := a.hub.GetConnectedAgents() response := make([]AgentResponse, 0, len(connectedIDs)) for _, id := range connectedIDs { if agent, exists := a.registry.Get(id); exists { response = append(response, agentToResponse(agent, true)) } } c.JSON(http.StatusOK, response) } // getAgentMetrics returns the latest metrics for an agent. func (a *AgentAPI) getAgentMetrics(c *gin.Context) { id := c.Param("id") if a.hub == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "hub not available"}) return } metrics, exists := a.hub.GetAgentMetrics(id) if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "no metrics available"}) return } c.JSON(http.StatusOK, metrics) }