98839cd8a8
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)
65 lines
1.6 KiB
Go
65 lines
1.6 KiB
Go
// Package api — in-memory pub/sub bus for SSE broadcast.
|
|
//
|
|
// TODO(v0.2): if a second consumer (e.g. from another VPS) is added,
|
|
// replace this in-memory bus with NATS or Redis pub/sub. For now
|
|
// (1 local client) the overhead of an external broker is unwarranted.
|
|
package api
|
|
|
|
import (
|
|
"sync"
|
|
)
|
|
|
|
// Event is a generic event payload (JSON-serialisable).
|
|
type Event = any
|
|
|
|
// Bus is a simple in-memory pub/sub hub.
|
|
// Topics are arbitrary strings (e.g. "status", "logs/agent-id").
|
|
type Bus struct {
|
|
mu sync.RWMutex
|
|
subs map[string][]chan Event
|
|
}
|
|
|
|
// NewBus creates an initialised Bus.
|
|
func NewBus() *Bus {
|
|
return &Bus{subs: make(map[string][]chan Event)}
|
|
}
|
|
|
|
// Subscribe returns a channel that receives events published to topic.
|
|
// The channel is buffered (32) to avoid blocking the publisher.
|
|
func (b *Bus) Subscribe(topic string) <-chan Event {
|
|
ch := make(chan Event, 32)
|
|
b.mu.Lock()
|
|
b.subs[topic] = append(b.subs[topic], ch)
|
|
b.mu.Unlock()
|
|
return ch
|
|
}
|
|
|
|
// Unsubscribe removes ch from topic and closes it.
|
|
func (b *Bus) Unsubscribe(topic string, ch <-chan Event) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
list := b.subs[topic]
|
|
for i, c := range list {
|
|
if c == ch {
|
|
close(c)
|
|
b.subs[topic] = append(list[:i], list[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Publish sends ev to all subscribers of topic.
|
|
// Non-blocking: if a subscriber channel is full, the event is dropped for that subscriber.
|
|
func (b *Bus) Publish(topic string, ev Event) {
|
|
b.mu.RLock()
|
|
list := b.subs[topic]
|
|
b.mu.RUnlock()
|
|
for _, ch := range list {
|
|
select {
|
|
case ch <- ev:
|
|
default:
|
|
// drop for this slow subscriber
|
|
}
|
|
}
|
|
}
|