From 11ef3fe9dda7bc8348ac3d3b6013d517624b539c Mon Sep 17 00:00:00 2001 From: agent Date: Thu, 4 Jun 2026 23:36:50 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20scaffold=20of=20unibots=20?= =?UTF-8?q?=E2=80=94=20bot=20platform=20consuming=20unibus,=20first=20bot?= =?UTF-8?q?=20=3D=20echo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 13 ++ app.md | 133 ++++++++++++ cmd/echobot/echobot_integration_test.go | 267 ++++++++++++++++++++++++ cmd/echobot/main.go | 99 +++++++++ go.mod | 39 ++++ go.sum | 75 +++++++ 6 files changed, 626 insertions(+) create mode 100644 .gitignore create mode 100644 app.md create mode 100644 cmd/echobot/echobot_integration_test.go create mode 100644 cmd/echobot/main.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..136dc9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Per-PC writable runtime state (never distributed). +local_files/ +*.id +*.db +*.db-shm +*.db-wal +jetstream/ +blobs/ + +# Build artifacts +/echobot +*.exe +registry.db diff --git a/app.md b/app.md new file mode 100644 index 0000000..2b9e120 --- /dev/null +++ b/app.md @@ -0,0 +1,133 @@ +--- +name: unibots +lang: go +domain: infra +version: 0.1.0 +description: "Plataforma de bots que consumen el bus unibus; primer bot = eco (bot sin LLM que demuestra los dos patrones de conversación del bus)." +tags: [bots, messaging, unibus-client] +uses_functions: [] +uses_types: [] +framework: "" +entry_point: "cmd/echobot" +dir_path: "projects/message_bus/apps/unibots" +repo_url: "" +e2e_checks: + - id: build + cmd: "CGO_ENABLED=0 go build ./..." + timeout_s: 180 + - id: vet + cmd: "CGO_ENABLED=0 go vet ./..." + timeout_s: 120 + - id: unit + cmd: "CGO_ENABLED=0 go test ./..." + timeout_s: 180 +--- + +## Qué es + +`unibots` es la plataforma de **bots** que consumen el bus de mensajería +[`unibus`](../unibus/). Un **bot** es todo peer automatizado del bus, con o sin +LLM. Un **bot agente** es un bot que contiene un LLM (no aplica todavía aquí). El +término único en código, nombres y docs es **bot**. + +El primer bot es el **bot eco** (`cmd/echobot`): un bot **sin LLM** que se une al +bus y devuelve cada mensaje prefijado con `"echo: "`. Existe para demostrar de +forma autónoma los **dos patrones de conversación** que el bus expone, sin +depender de ningún modelo: + +| Patrón | Subject | Cómo | Pareja | +|---|---|---|---| +| **Chat** (bot↔humano) | `room.echo` (cleartext, `room.ModeNATS`) | `Subscribe` a la room + `Publish` la respuesta | cualquier peer en el mismo subject | +| **RPC** (bot↔proceso) | `rpc.echo` | request/reply de NATS (`Client.Reply` registra el responder) | cualquier proceso que haga `Client.Request` | + +`unibots` es **código de aplicación**, no funciones del registry: orquesta la +librería cliente de `unibus` (`pkg/client`) y no reimplementa nada. Por eso +`uses_functions` está vacío — el crypto/transporte lo aporta `unibus`, que a su +vez importa las primitivas del registry. + +Referencia: [[convencion-bot-vs-agente]]. Consumidor de [[unibus]]. + +## Ejemplo + +Lanzar el echobot contra un `membershipd` corriendo (NATS embebido en `:4250`, +HTTP en `:8470`, los defaults productivos de unibus): + +```bash +cd projects/message_bus/apps/unibus + +# 1. Bus de membresía/claves (NATS embebido + control plane HTTP) +go run ./cmd/membershipd + +# 2. En otra terminal: el bot eco (defaults: nats://127.0.0.1:4250, http://127.0.0.1:8470) +cd ../unibots +go run ./cmd/echobot +# Loguea al arrancar: endpoint id, subjects de chat y rpc, y a qué bus apunta. +``` + +Probarlo desde otra terminal sin escribir más Go: + +```bash +# Modo RPC (bot<->proceso) con la CLI de NATS contra el mismo NATS embebido: +# nats --server nats://127.0.0.1:4250 request rpc.echo "ping" +# -> recibe "echo: ping" + +# Modo chat (bot<->humano): cualquier peer que publique en el subject room.echo +# (p.ej. el `chat` de unibus apuntando a ese subject, o un cliente propio) recibe +# de vuelta "echo: ". +``` + +Apuntar a un bus distinto: + +```bash +go run ./cmd/echobot \ + --nats-url nats://mi-host:4222 \ + --ctrl-url http://mi-host:8470 \ + --room-subject room.demo \ + --rpc-subject rpc.demo +``` + +## Cuando usarla + +- Cuando quieras **validar que un bus unibus está vivo** end-to-end (chat y RPC) + sin montar un bot agente con LLM. +- Como **plantilla mínima** para escribir un bot nuevo: copia `cmd/echobot`, + cambia la lógica del handler (en vez de `"echo: " + body`, llama a tu servicio, + base de datos o, más adelante, a un LLM para convertirlo en bot agente). +- Para **demostrar los dos patrones** del bus (chat por room cleartext vs RPC + request/reply) a alguien que aprende la arquitectura. + +## Gotchas + +- **Guard anti-bucle (crítico).** El handler de chat ignora los mensajes cuyo + `frame.Frame.Sender == c.Endpoint().ID`. Sin este guard, el bot se haría eco de + su propio `"echo: ..."` indefinidamente (y dos echobots en el mismo subject + entrarían en un bucle infinito). El test `TestChatEcho` verifica que nunca + aparece `"echo: echo: hola"`. +- **Cleartext comparte subject, no room id.** El bot usa `room.ModeNATS` + (cleartext, efímero, sin firma). NATS enruta por **subject**, así que el bot + conversa con cualquier peer en el mismo subject aunque cada uno tenga su propio + `room_id` (mismo patrón que el worker/chat de unibus). No hay "unirse a una room + por nombre": cada `CreateRoom` produce un ULID nuevo mapeado al subject. +- **Modo RPC sí está soportado.** La librería de unibus expone request/reply: + `Client.Request(subject, body, timeout)` y `Client.Reply(subject, handler)` + (cleartext v1, sobre `rpc.*`). El echobot registra un responder con `Reply`. No + hubo que omitir ni inventar nada en unibus. +- **Identidad = secreto crítico.** `local_files/echobot.id` contiene las claves + privadas (Ed25519 + X25519), se escribe 0600. Perderlo no rompe el eco (es + cleartext) pero cambia la identidad del bot. Está gitignorado. +- **Build sin CGO.** Igual que unibus: `CGO_ENABLED=0`, sin `fts5` ni `gcc`. El + crypto del registry (`cybersecurity`) y el driver SQLite pure-Go compilan + limpio. +- **Los tests usan puertos propios aislados.** El test de integración levanta un + `membershipd` con NATS embebido en puertos libres (`:0`) bajo `t.TempDir()`, + nunca en `8470/4250` ni en los del playground del usuario; todo se limpia por + handle vía `t.Cleanup`. + +## Convención de subjects (heredada de unibus) + +``` +proc.. telemetría/coordinación de procesos +rpc. request/reply (rpc.echo) +room. chat humano/grupo (room.echo) +agent..{in,out} inbox/outbox de bot agente (futuro) +``` diff --git a/cmd/echobot/echobot_integration_test.go b/cmd/echobot/echobot_integration_test.go new file mode 100644 index 0000000..80a7550 --- /dev/null +++ b/cmd/echobot/echobot_integration_test.go @@ -0,0 +1,267 @@ +package main + +import ( + "net" + "net/http" + "net/http/httptest" + "path/filepath" + "sync" + "testing" + "time" + + cs "fn-registry/functions/cybersecurity" + + "github.com/enmanuel/unibus/pkg/blobstore" + "github.com/enmanuel/unibus/pkg/client" + "github.com/enmanuel/unibus/pkg/embeddednats" + "github.com/enmanuel/unibus/pkg/frame" + "github.com/enmanuel/unibus/pkg/membership" + "github.com/enmanuel/unibus/pkg/room" +) + +// testHarness boots an isolated embedded NATS server + in-process membershipd on +// their OWN free ports (never the productive 8470/4250 nor the user's running +// playground on 7700/8480/4260) and tears everything down by handle. This mirrors +// the unibus client_test harness so the echobot is exercised against the real bus. +type testHarness struct { + natsURL string + ctrlURL string +} + +func freePort(t *testing.T) int { + t.Helper() + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("free port: %v", err) + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port +} + +func newHarness(t *testing.T) *testHarness { + t.Helper() + dir := t.TempDir() + + ns, err := embeddednats.Start(filepath.Join(dir, "js"), freePort(t)) + if err != nil { + t.Fatalf("embedded nats: %v", err) + } + + store, err := membership.Open(filepath.Join(dir, "unibus.db")) + if err != nil { + ns.Shutdown() + t.Fatalf("membership store: %v", err) + } + blobs, err := blobstore.New(filepath.Join(dir, "blobs")) + if err != nil { + store.Close() + ns.Shutdown() + t.Fatalf("blob store: %v", err) + } + srv := membership.NewServer(store, blobs) + httpts := httptest.NewServer(srv) + + t.Cleanup(func() { + httpts.Close() + store.Close() + ns.Shutdown() + ns.WaitForShutdown() + }) + + return &testHarness{natsURL: embeddednats.ClientURL(ns), ctrlURL: httpts.URL} +} + +func waitHealth(t *testing.T, ctrlURL string) { + t.Helper() + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err := http.Get(ctrlURL + "/healthz") + if err == nil && resp.StatusCode == 200 { + resp.Body.Close() + return + } + if resp != nil { + resp.Body.Close() + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("membershipd never became healthy") +} + +func mustIdentity(t *testing.T) cs.Identity { + t.Helper() + id, err := cs.GenerateIdentity() + if err != nil { + t.Fatalf("generate identity: %v", err) + } + return id +} + +func waitFor(mu *sync.Mutex, slice *[]string, pred func([]string) bool, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + mu.Lock() + cp := append([]string(nil), (*slice)...) + mu.Unlock() + if pred(cp) { + return true + } + time.Sleep(25 * time.Millisecond) + } + return false +} + +func snapshot(mu *sync.Mutex, slice *[]string) []string { + mu.Lock() + defer mu.Unlock() + return append([]string(nil), (*slice)...) +} + +func contains(rs []string, want string) bool { + for _, r := range rs { + if r == want { + return true + } + } + return false +} + +// startEchobot wires up the echobot's chat + rpc behaviour against the given bus, +// using the same logic the main() entry point runs. It returns the bot client and +// its endpoint id so callers can assert the anti-loop guard. Cleanup is registered +// on the test. +func startEchobot(t *testing.T, h *testHarness, roomSubject, rpcSubject string) (*client.Client, string) { + t.Helper() + bot, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t)) + if err != nil { + t.Fatalf("connect echobot: %v", err) + } + selfID := bot.Endpoint().ID + + chatRoom, err := bot.CreateRoom(roomSubject, room.ModeNATS) + if err != nil { + bot.Close() + t.Fatalf("echobot create chat room: %v", err) + } + chatSub, err := bot.Subscribe(chatRoom, func(f frame.Frame, plaintext []byte) { + if f.Sender == selfID { + return // anti-loop guard + } + _ = bot.Publish(chatRoom, []byte("echo: "+string(plaintext))) + }) + if err != nil { + bot.Close() + t.Fatalf("echobot subscribe chat: %v", err) + } + rpcSub, err := bot.Reply(rpcSubject, func(body []byte) []byte { + return []byte("echo: " + string(body)) + }) + if err != nil { + chatSub.Unsubscribe() + bot.Close() + t.Fatalf("echobot reply: %v", err) + } + + t.Cleanup(func() { + rpcSub.Unsubscribe() + chatSub.Unsubscribe() + bot.Close() + }) + return bot, selfID +} + +// TestChatEcho: a "human" peer publishes "hola" on the echo subject; the echobot +// replies "echo: hola". Asserts the human receives the echo and that the echobot +// never echoes its own messages (no infinite loop). +func TestChatEcho(t *testing.T) { + h := newHarness(t) + waitHealth(t, h.ctrlURL) + + const subject = "room.echo.test" + _, botID := startEchobot(t, h, subject, "rpc.echo.test") + + human, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t)) + if err != nil { + t.Fatalf("connect human: %v", err) + } + defer human.Close() + + humanRoom, err := human.CreateRoom(subject, room.ModeNATS) + if err != nil { + t.Fatalf("human create room: %v", err) + } + + var mu sync.Mutex + var received []string + var echoSenders []string + hsub, err := human.Subscribe(humanRoom, func(f frame.Frame, plaintext []byte) { + mu.Lock() + received = append(received, string(plaintext)) + if string(plaintext) == "echo: hola" { + echoSenders = append(echoSenders, f.Sender) + } + mu.Unlock() + }) + if err != nil { + t.Fatalf("human subscribe: %v", err) + } + defer hsub.Unsubscribe() + + // Let both subscriptions settle before publishing. + time.Sleep(200 * time.Millisecond) + + if err := human.Publish(humanRoom, []byte("hola")); err != nil { + t.Fatalf("human publish: %v", err) + } + + if !waitFor(&mu, &received, func(rs []string) bool { return contains(rs, "echo: hola") }, 2*time.Second) { + t.Fatalf("human never received the echo; got %v", snapshot(&mu, &received)) + } + + // The echo must come from the bot, not the human (sanity on routing). + mu.Lock() + senders := append([]string(nil), echoSenders...) + mu.Unlock() + for _, s := range senders { + if s != botID { + t.Fatalf("echo came from %q, expected echobot %q", s, botID) + } + } + + // Anti-loop: give the bus time to spin if the guard were broken, then assert + // the bot did not re-echo its own "echo: hola" into "echo: echo: hola". + time.Sleep(500 * time.Millisecond) + for _, r := range snapshot(&mu, &received) { + if r == "echo: echo: hola" { + t.Fatalf("anti-loop guard broken: bot echoed its own message (%q)", r) + } + } +} + +// TestRPCEcho: a process peer issues Request(rpc-subject, "ping") and gets back +// "echo: ping". The unibus client library exposes request/reply, so this mode is +// fully supported (see client.go: Client.Request / Client.Reply). +func TestRPCEcho(t *testing.T) { + h := newHarness(t) + waitHealth(t, h.ctrlURL) + + const rpcSubject = "rpc.echo.test" + startEchobot(t, h, "room.echo.test", rpcSubject) + + caller, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t)) + if err != nil { + t.Fatalf("connect caller: %v", err) + } + defer caller.Close() + + // Give the responder time to subscribe. + time.Sleep(150 * time.Millisecond) + + resp, err := caller.Request(rpcSubject, []byte("ping"), 2*time.Second) + if err != nil { + t.Fatalf("rpc request: %v", err) + } + if got, want := string(resp), "echo: ping"; got != want { + t.Fatalf("rpc echo mismatch: got %q want %q", got, want) + } +} diff --git a/cmd/echobot/main.go b/cmd/echobot/main.go new file mode 100644 index 0000000..199ed3b --- /dev/null +++ b/cmd/echobot/main.go @@ -0,0 +1,99 @@ +// Command echobot is the first bot of the unibots platform: a bot WITHOUT an +// LLM that demonstrates the two conversation patterns of the unibus bus. +// +// - Chat mode (bot<->human): the bot joins a cleartext room (room.ModeNATS) +// on a shared subject and echoes back every message it sees, prefixed with +// "echo: ". It never echoes its own messages (anti-loop guard), so two +// echobots on the same subject do not spin forever. +// - RPC mode (bot<->process): the bot registers a NATS request/reply +// responder on an rpc.* subject that returns "echo: " + the request body. +// +// echobot is application code that consumes the unibus client library; it is +// not a reusable registry function. The bus is the neighbouring `unibus` app. +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "syscall" + + "github.com/enmanuel/unibus/pkg/client" + "github.com/enmanuel/unibus/pkg/frame" + "github.com/enmanuel/unibus/pkg/room" +) + +func main() { + var ( + natsURL = flag.String("nats-url", "nats://127.0.0.1:4250", "NATS data-plane URL of the unibus bus") + ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8470", "membershipd control-plane HTTP URL") + roomSubject = flag.String("room-subject", "room.echo", "cleartext chat subject the bot listens on (bot<->human)") + rpcSubject = flag.String("rpc-subject", "rpc.echo", "request/reply subject the bot responds on (bot<->process)") + idFile = flag.String("id-file", "./local_files/echobot.id", "path to the bot's long-term identity file") + ) + flag.Parse() + + logger := log.New(os.Stderr, "[echobot] ", log.LstdFlags|log.Lmsgprefix) + + id, err := client.LoadOrCreateIdentity(*idFile) + if err != nil { + logger.Fatalf("load/create identity %q: %v", *idFile, err) + } + + c, err := client.New(*natsURL, *ctrlURL, id) + if err != nil { + logger.Fatalf("connect to bus (nats=%s ctrl=%s): %v", *natsURL, *ctrlURL, err) + } + defer c.Close() + + self := c.Endpoint() + + // --- Chat mode (bot<->human) -------------------------------------------- + // A cleartext room mapped to the shared subject. NATS fans out by subject, + // so the bot shares the conversation with any peer on the same subject even + // if their room ids differ (same pattern as unibus worker/chat). + chatRoom, err := c.CreateRoom(*roomSubject, room.ModeNATS) + if err != nil { + logger.Fatalf("create chat room on subject %q: %v", *roomSubject, err) + } + + chatSub, err := c.Subscribe(chatRoom, func(f frame.Frame, plaintext []byte) { + // Anti-loop guard: never echo our own messages, or two echobots (or a + // single bot seeing its own publish) would loop forever. + if f.Sender == self.ID { + return + } + reply := "echo: " + string(plaintext) + if err := c.Publish(chatRoom, []byte(reply)); err != nil { + logger.Printf("chat: publish echo failed: %v", err) + return + } + logger.Printf("chat: echoed %q -> %q (from %s)", string(plaintext), reply, f.Sender) + }) + if err != nil { + logger.Fatalf("subscribe to chat room: %v", err) + } + defer chatSub.Unsubscribe() + + // --- RPC mode (bot<->process) ------------------------------------------- + // NATS request/reply: a responder on the rpc subject returns "echo: " + body. + rpcSub, err := c.Reply(*rpcSubject, func(body []byte) []byte { + reply := "echo: " + string(body) + logger.Printf("rpc: %q -> %q", string(body), reply) + return []byte(reply) + }) + if err != nil { + logger.Fatalf("register rpc responder on %q: %v", *rpcSubject, err) + } + defer rpcSub.Unsubscribe() + + logger.Printf("echobot up: endpoint=%s bus(nats=%s ctrl=%s) chat-subject=%q rpc-subject=%q", + self.ID, *natsURL, *ctrlURL, *roomSubject, *rpcSubject) + + // --- Loop until SIGINT/SIGTERM, then shut down cleanly ------------------ + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + s := <-sig + logger.Printf("received %v, shutting down", s) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6392005 --- /dev/null +++ b/go.mod @@ -0,0 +1,39 @@ +module github.com/enmanuel/unibots + +go 1.25.0 + +// unibots consumes the unibus client library directly from the neighbouring app. +replace github.com/enmanuel/unibus => ../unibus + +// unibus's pkg/client imports fn-registry/functions/cybersecurity transitively; +// without this replace the dependency does not resolve. +replace fn-registry => ../../../../ + +require ( + fn-registry v0.0.0-00010101000000-000000000000 + github.com/enmanuel/unibus v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.3 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/highwayhash v1.0.3 // indirect + github.com/nats-io/jwt/v2 v2.5.8 // indirect + github.com/nats-io/nats-server/v2 v2.10.22 // indirect + github.com/nats-io/nats.go v1.37.0 // indirect + github.com/nats-io/nkeys v0.4.7 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.7.0 // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.47.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cba8fff --- /dev/null +++ b/go.sum @@ -0,0 +1,75 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE= +github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= +github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7VX846ibxDA= +github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= +github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= +github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=