nomos+web: streaming, provider routing, event gap-fill, embedded UI; fix approval FK & session context
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Agent (cmd/nomos):
- Stream LLM tokens via NewStreaming; emit text_delta then final text.
- OpenRouter provider routing: data_collection=deny (ZDR) + require_parameters;
  NOMOS_PROVIDER_SORT opt-in; Exacto via model suffix.
- Multi-turn: reload session history into context; UI passes session id.
- Fix agent_activity logging (agent_id/session_id) and mcpClient data race.

Events (live control-room feed):
- approval.created (mcp), approval.decided (api), execution.completed/failed
  (approved-action path), signal.raised/resolved + health.changed (scheduler,
  transition-gated).

Fixes:
- createApproval FK violation (reuse execution entity) — the agent's only
  write path; log the previously-swallowed errors.

Web UI:
- Embed web/dist via //go:embed (single binary); Dockerfile builds SPA into
  the Go stage; committed .gitkeep placeholder keeps backend-only builds green.
- Caddy: Authentik-gated /agent/* -> nomos so the UI reaches the agent
  same-origin in production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 15:22:27 +02:00
parent 2b3aa248b1
commit e8e230b4a5
34 changed files with 3267 additions and 134 deletions

View File

@@ -15,6 +15,9 @@ import (
"log/slog"
"math/big"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"sync"
"time"
@@ -66,7 +69,7 @@ type secretsBackend interface {
// holds a dedicated pooled connection for LISTEN. Callers MUST cancel ctx
// before closing the pool — otherwise the held connection never releases
// and pool.Close() deadlocks.
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Handler {
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config, uiHandler http.Handler) http.Handler {
s := &Server{
pool: pool,
cfg: cfg,
@@ -148,6 +151,24 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
}
r.With(combinedAuth(cfg)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken, nomosAgentID))
r.Get("/ui/*", func(w http.ResponseWriter, req *http.Request) {
if uiHandler != nil {
uiHandler.ServeHTTP(w, req)
}
})
r.Get("/ui", func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/ui/", http.StatusMovedPermanently)
})
r.Get("/", func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/ui/", http.StatusMovedPermanently)
})
if nomosURL := os.Getenv("NOMOS_PROXY_URL"); nomosURL != "" {
target, _ := url.Parse(nomosURL)
proxy := httputil.NewSingleHostReverseProxy(target)
r.Mount("/agent", http.StripPrefix("/agent", proxy))
}
return r
}
@@ -484,10 +505,10 @@ func requestLogger(next http.Handler) http.Handler {
// ListenAndServe runs the API server with graceful shutdown on ctx cancel
// (SG4): stop accepting, drain in-flight for up to 30s, then exit.
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config) error {
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config, uiHandler http.Handler) error {
srv := &http.Server{
Addr: cfg.APIListen,
Handler: NewHandler(ctx, pool, cfg),
Handler: NewHandler(ctx, pool, cfg, uiHandler),
ReadHeaderTimeout: 10 * time.Second,
}