Files
agents_and_robots/internal/api/poller.go
T
egutierrez 98839cd8a8 feat(api): HTTP API REST+SSE para gestion remota de agentes (issue 0128)
Nuevo paquete internal/api con servidor HTTP stdlib (sin gin/echo):
- Auth Bearer via AGENTS_API_KEY con subtle.ConstantTimeCompare
- REST: GET /health (sin auth), GET/POST /agents, /agents/{id}, /{id}/{start,stop,restart,logs}
- SSE: /sse/status (broadcast diffs cada 2s) y /sse/agents/{id}/logs (tail -f)
- Pubsub in-memory (TODO: NATS cuando haya 2do cliente)
- Tail de logfiles: retroalimenta ultimos 50KB + poll 200ms para streaming

Integracion en cmd/launcher/main.go:
- Flag --api-port (0=desactivado, 8487 en produccion)
- Flag --api-key (override de AGENTS_API_KEY env var)
- Si apiPort>0 y sin clave, WARN y deshabilita en vez de fallar

Systemd unit en systemd/agents_and_robots.service:
- Restart=always (no on-failure — evita que exit limpio mate el service)
- EnvironmentFile para AGENTS_API_KEY y demas tokens
- WorkingDirectory=/home/ubuntu/CodeProyects/agents_and_robots

app.md v0.2.0:
- port: 8487, health_endpoint: /health (fix drift anterior donde era null)
- e2e_checks: build, tests, smoke_health, smoke_auth
- Documentacion Traefik+DNS pendiente humano post-merge

Tests: 12 tests unitarios en internal/api (auth, health, bus, agents, logs)
Smoke: /health 200, /agents sin auth 401, /agents con key 200 — verificado local

Co-Authored-By: fn-constructor (agent)
2026-05-22 21:19:10 +02:00

92 lines
1.9 KiB
Go

package api
import (
"context"
"time"
"github.com/enmanuel/agents/shell/process"
)
// StatusDiff is published to the "status" topic when an agent's running state changes.
type StatusDiff struct {
AgentID string `json:"agent_id"`
OldStatus bool `json:"old_running"`
NewStatus bool `json:"new_running"`
PID int `json:"pid,omitempty"`
}
// pollStatus polls StatusAll every 2s and publishes StatusDiff events on changes.
func (s *Server) pollStatus(ctx context.Context) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
// Seed the previous state map.
prev := make(map[string]bool)
if statuses, err := s.mgr.StatusAll(); err == nil {
for _, st := range statuses {
prev[st.ID] = st.Running
}
}
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.checkAndPublishDiffs(prev)
}
}
}
func (s *Server) checkAndPublishDiffs(prev map[string]bool) {
statuses, err := s.mgr.StatusAll()
if err != nil {
return
}
for _, st := range statuses {
old, known := prev[st.ID]
if !known || old != st.Running {
s.bus.Publish("status", StatusDiff{
AgentID: st.ID,
OldStatus: old,
NewStatus: st.Running,
PID: st.PID,
})
prev[st.ID] = st.Running
}
}
// Handle agents that were removed (disappeared from scan)
current := make(map[string]bool, len(statuses))
for _, st := range statuses {
current[st.ID] = true
}
for id, wasRunning := range prev {
if !current[id] {
if wasRunning {
s.bus.Publish("status", StatusDiff{
AgentID: id,
OldStatus: true,
NewStatus: false,
})
}
delete(prev, id)
}
}
}
// agentInfoByID finds AgentInfo by ID in a StatusAll scan.
func agentInfoByID(mgr *process.Manager, id string) (*process.AgentInfo, error) {
agents, err := mgr.Scan()
if err != nil {
return nil, err
}
for i, a := range agents {
if a.ID == id {
return &agents[i], nil
}
}
return nil, nil
}