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)
This commit is contained in:
2026-05-22 21:19:10 +02:00
parent 1f90953ccc
commit 98839cd8a8
10 changed files with 1110 additions and 7 deletions
+36
View File
@@ -20,6 +20,7 @@ import (
"github.com/spf13/cobra"
"github.com/enmanuel/agents/devagents"
"github.com/enmanuel/agents/internal/api"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/pkg/decision"
"github.com/enmanuel/agents/pkg/orchestration"
@@ -28,6 +29,7 @@ import (
agentlog "github.com/enmanuel/agents/shell/logger"
orchshell "github.com/enmanuel/agents/shell/orchestration"
shellsecurity "github.com/enmanuel/agents/shell/security"
"github.com/enmanuel/agents/shell/process"
// Blank imports: each agent self-registers its rules via init().
_ "github.com/enmanuel/agents/agents/assistant-bot"
@@ -46,6 +48,8 @@ func main() {
configPaths []string
logLevel string
logDir string
apiPort int
apiKey string
)
root := &cobra.Command{
@@ -268,6 +272,28 @@ func main() {
scanCancel()
}
// ── HTTP API (optional) ──
if apiPort > 0 {
key := apiKey
if key == "" {
key = os.Getenv("AGENTS_API_KEY")
}
if key == "" {
logger.Warn("api-port set but AGENTS_API_KEY is empty — HTTP API disabled (set AGENTS_API_KEY in .env)")
} else {
// Build a process.Manager that reflects the live launcher state.
// The manager uses run/ for PID files and agents/*/config.yaml for discovery.
mgr := newProcessManager(logDir)
srv := api.New(mgr, key, apiPort, logger)
go func() {
if err := srv.Run(ctx); err != nil {
logger.Error("api server stopped", "err", err)
}
}()
logger.Info("http api enabled", "port", apiPort)
}
}
// Supervised loop: wait for all agents, and if the parent context is
// still alive (i.e. no SIGINT/SIGTERM received), reload them and keep
// going. Protects against the launcher exiting cleanly when all
@@ -286,6 +312,10 @@ func main() {
"Log level: debug | info | warn | error")
root.Flags().StringVar(&logDir, "log-dir", "logs",
`Log directory (logs/<agent>/YYYY-MM-DD.jsonl). Use "stdout" for console only`)
root.Flags().IntVar(&apiPort, "api-port", 0,
"HTTP API port (0 = disabled). Requires AGENTS_API_KEY env var.")
root.Flags().StringVar(&apiKey, "api-key", "",
"HTTP API Bearer key (overrides AGENTS_API_KEY env var)")
if err := root.Execute(); err != nil {
os.Exit(1)
@@ -364,6 +394,12 @@ func newLogger(level string) *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: parseLogLevel(level)}))
}
// newProcessManager creates a process.Manager scoped to the current working
// directory, used by the HTTP API to reflect the live launcher state.
func newProcessManager(logDir string) *process.Manager {
return process.NewManager("run", "agents/*/config.yaml", "bin/launcher")
}
// isSpecialConfig checks whether a config path belongs to a middleware special
// (e.g. orchestrator) by detecting a "special:" top-level key with a non-empty
// id. This avoids config.Load() failing with "agent.id is required" when the