# Report 0001 — unibus: servicio LAN + Fase 2 (transporte sobre el bus) - **Fecha:** 06/06/2026 - **Autor:** Claude (Opus 4.8) - **Ámbito:** `projects/message_bus/apps/unibus` (sub-repo `dataforge/unibus`) y `~/DataProyects/Github/agents_and_robots` (Gitea `egutierrez/agents_and_robots`) - **Estado:** done (con un gap acotado documentado en Fase 2) ## Resumen Dos entregas. **A)** `membershipd` pasa de proceso manual loopback a service systemd real alcanzable por la LAN: flag `--bind` que gobierna a la vez el HTTP de control y el NATS embebido, unit systemd-user con `Restart=always`, bloque `service:` completo y health verificado desde la IP de la LAN. Esto desbloquea que un teléfono/PC remoto conecte al bus. **B)** Fase 2 (branch-by-abstraction): se introduce una interfaz `Transport` + un tipo `InboundMessage` neutral (sin mautrix), se desacoplan las firmas del core de agentes, se implementa un `unibusTransport` sobre `pkg/client` de unibus, y un feature flag elige Matrix vs unibus por bot. Pre-requisito cumplido: el `Frame` de unibus gana threading/reply/reaction de forma aditiva. Trabajo en ramas TBD: `issue/fase2-transport-service` (unibus) y `issue/unibus-transport` (agents_and_robots). Master nunca tocado directamente. ## Cambios ### A — Servicio (unibus) | Cambio | Archivo | Por qué | |---|---|---| | Flag `--bind` (default 127.0.0.1) aplicado a HTTP + NATS | `cmd/membershipd/main.go` | Un solo flag gobierna la alcanzabilidad de ambos planos; `0.0.0.0` expone a la LAN | | `embeddednats.StartHost(storeDir, host, port)` (y `Start` backward-compatible) | `pkg/embeddednats/embeddednats.go` | Controlar la interfaz del NATS embebido sin romper playground/tests | | systemd-user unit `Restart=always` | `deploy/unibus-membershipd.service` | Service persistente; `on-failure` NO reinicia tras un SIGTERM limpio (exit 0) — el gotcha de `sqlite_api` | | `install.sh` idempotente + `README.md` | `deploy/` | build + symlink unit + enable/restart en un comando; operar/health documentado | | Bloque `service:` completo | `app.md` | `systemd-user`, `restart_policy: always`, health `/healthz`, `is_local_only: false`; valida `fn doctor services-spec` | ### Pre-Fase 2 — Frame threading (unibus, aditivo) | Cambio | Archivo | Por qué | |---|---|---| | Campos `ThreadID`, `ReplyTo` (omitempty) + tipo `REACT` | `pkg/frame/frame.go` | Bots de chat necesitan replies, hilos y reacciones | | `PublishReply`, `React` (refactor de `Publish` a `publishFrame` compartido) | `pkg/client/client.go` | API de threading; la reacción viaja en el payload cifrado (confidencial en rooms E2E) | | Tests: round-trip threaded, back-compat de wire/firma, error path, reply+react E2E | `pkg/frame/*_test.go`, `pkg/client/client_test.go` | golden + edge + error path | Aditivo de verdad: un frame sin threading serializa **sin** las claves `thr`/`re`, así que su wire y su firma Ed25519 son idénticos a antes → no rompe `Subscribe`/`Publish` ni los tests existentes. ### B — Fase 2 (agents_and_robots) | Cambio | Archivo | Por qué | |---|---|---| | `Transport` interface + `InboundMessage` neutral + `Select`/`FlagEnabled` | `pkg/transport/` | Seam neutral (cero mautrix) entre el core del agente y el fabric de mensajería | | Flag `unibus-transport` (default off) | `dev/feature_flags.json` | Convivencia: con el flag ON cada bot opta in; el resto sigue en Matrix | | `handleEvent(…, *event.Event)` → `handleInbound(…, transport.InboundMessage)` (Agent + Robot) | `agents/handler.go`, `agents/robot.go` | Quitar `*event.Event` de las firmas; `inboundToMsgCtx` es el único punto de conversión a `decision.MessageContext` | | `RawMatrixClient() *mautrix.Client` → `Scanner() RoomScanner` | `agents/runtime.go`, `cmd/launcher/main.go` | Quitar el handle al cliente mautrix de la API pública; capability read-only | | Listener entrega `InboundMessage` (no el evento) | `shell/matrix/listener.go` | El evento mautrix ya no cruza la frontera al core | | `unibusTransport` + `DemoEchoHandler` (módulo Go anidado) | `shell/transportunibus/` | Implementa `Transport` sobre `pkg/client`; subjects `agent..{in,out}` | ## Verificación ### A — Servicio (evidencia ejecutable) ``` $ systemctl --user is-active unibus-membershipd.service active $ systemctl --user status unibus-membershipd.service # (extracto) Active: active (running) ExecStart=…/membershipd --bind 0.0.0.0 $ ss -tln | grep -E ':8470|:4250' LISTEN 0 4096 *:8470 *:* # HTTP control en todas las interfaces LISTEN 0 4096 *:4250 *:* # NATS data plane en todas las interfaces $ curl -fsS -w " [HTTP %{http_code}]\n" http://127.0.0.1:8470/healthz {"status":"ok"} [HTTP 200] $ curl -fsS -w " [HTTP %{http_code}]\n" http://192.168.1.129:8470/healthz # IP LAN {"status":"ok"} [HTTP 200] $ ./fn doctor services-spec | grep unibus OK unibus systemd-user 8470 /healthz unibus-membershipd.service lucas-linux - ``` **Cliente NATS conecta por IP LAN y publica/recibe** (worker→chat, ambos contra `nats://192.168.1.129:4250` + `http://192.168.1.129:8470`, vía el service systemd): ``` [chat] subscribed to "proc.test.ticks"; waiting for messages [proc.test.ticks] DGCob41a: tick 1 @ 2026-06-06T16:07:16Z [proc.test.ticks] DGCob41a: tick 2 @ 2026-06-06T16:07:17Z [proc.test.ticks] DGCob41a: tick 3 @ 2026-06-06T16:07:18Z [proc.test.ticks] DGCob41a: tick 4 @ 2026-06-06T16:07:19Z ``` ### Pre-Fase 2 — Frame (unibus) ``` $ cd projects/message_bus/apps/unibus $ CGO_ENABLED=0 go build ./... && CGO_ENABLED=0 go vet ./... # exit 0 $ CGO_ENABLED=0 go test ./... ok github.com/enmanuel/unibus/pkg/frame ok github.com/enmanuel/unibus/pkg/client ok github.com/enmanuel/unibus/pkg/membership ``` Tests nuevos: `TestThreadingRoundTrip` (golden), `TestNonThreadedWireBackCompat` (edge: asegura que `thr`/`re` no aparecen en frames no-threaded), `TestUnmarshalRejectsGarbage` (error path), `TestThreadedReplyAndReaction` (reply + reacción E2E en room cifrada ModeMatrix). ### B — Fase 2 (agents_and_robots) Toda la compilación/test del repo `agents` se hace con `-tags goolm` (olm puro-Go) porque `libolm` (CGO) no está instalado en esta máquina; en producción se usa `libolm`. ``` # Módulo anidado del transporte unibus (flag ON / camino unibus): $ cd shell/transportunibus && go test -tags goolm ./... ok github.com/enmanuel/agents/shell/transportunibus # TestDemoBotOverUnibus: un user-peer publica en agent.demo.in; el bot, movido por # unibusTransport, recibe un InboundMessage y responde "echo: hola bus" / "pong" en # agent.demo.out. TestRunStopsOnContextCancel: lifecycle (error path). # Módulo principal (flag OFF / camino Matrix sigue vivo): $ go build -tags goolm ./... # exit 0 $ go vet -tags goolm ./agents/ ./shell/matrix/ ./pkg/transport/ # exit 0 $ go test -tags goolm ./pkg/transport/ ./agents/ ok github.com/enmanuel/agents/pkg/transport # TestSelect (flag ON/OFF) + TestFlagEnabled ok github.com/enmanuel/agents/agents # core decoplado compila+pasa $ go test -tags goolm ./... # 26 paquetes ok, 1 FAIL pre-existente (ver gaps) ``` ## Gaps / pendientes 1. **Segundo host físico real para A.** El round-trip de A se probó desde esta misma máquina contra su IP de LAN `192.168.1.129` (no loopback), lo que ejercita el bind `0.0.0.0`. No había un segundo PC/teléfono disponible para conectar desde otra máquina; como los sockets escuchan en `*` (todas las interfaces, confirmado por `ss`), el comportamiento desde un host remoto es idéntico. El agente de frontend puede confirmarlo desde el móvil. 2. **El Agent "pesado" todavía no corre sobre unibus.** Conflicto real de dependencias entre ecosistemas: `unibus` depende de `fn-registry`, cuyo módulo raíz exige `maunium.net/go/mautrix v0.28.0`, incompatible con el `shell/matrix` del repo `agents` (escrito para `v0.21.1`; MVS forzaría v0.28 y rompe 3 call-sites de la API mautrix). Por eso el `unibusTransport` vive en un **módulo Go anidado** (`shell/transportunibus/go.mod`): tira de la mautrix nueva transitivamente sin recompilar el código Matrix del padre, y ambos ecosistemas conviven. Consecuencia: el core del Agent (módulo principal) no puede instanciar `unibusTransport` directamente sin reintroducir el conflicto. El seam neutral (`pkg/transport.Transport`) sí está y el bot demo corre sobre el bus; **migrar el Agent completo a unibus requiere primero subir `agents` a mautrix v0.28** (migración aparte, fuera de alcance de esta tanda). 3. **`Scanner() RoomScanner` sigue devolviendo tipos mautrix** en sus métodos (`*mautrix.RespJoinedRooms`, …). El escaneo de rooms es una capability intrínsecamente de Matrix (enumera rooms de Matrix); la neutralización quita el handle al cliente crudo y lo sustituye por una interfaz read-only mínima, pero no puede ser 100% neutral porque el unibus no participa en ese escaneo. 4. **`libolm` no instalado** en esta máquina → el repo `agents` se compila/test con `-tags goolm` (olm puro-Go). En producción se usa `libolm`. Pendiente: instalar `libolm-dev` si se quiere validar el path libolm localmente. 5. **Test pre-existente en rojo, ajeno a este trabajo:** `shell/logger/TestCleanOldLogs` falla porque es dependiente de fecha (espera logs de marzo 2026 que la limpieza por antigüedad borra estando hoy a 06/06/2026). Ya fallaba en el baseline antes de tocar nada; no toqué `shell/logger`. Issue separado recomendado. 6. **Sin push a remoto.** El trabajo queda consolidado en `master` local de ambos repos; el push a Gitea queda para `/full-git-push` del operador. - `agents_and_robots`: `master` = merge `--no-ff` de `issue/unibus-transport` (commit `aa595f5`), build + tests verdes; rama borrada. - `unibus`: **condición de carrera entre dos agentes en el mismo repo.** El agente de frontend trabajaba en paralelo en el mismo working tree de unibus y nuestros `git checkout` se interleavearon: mis commits A1/A2/A3 (`--bind`, deploy, service block) acabaron directamente en `master`, y los de threading + bump (`2209283`, `69079d1`) cayeron sobre la rama del otro agente `issue/frontend-web-movil`. Como esos dos commits son lineales sobre mi A3 (`b2e6712`) y NO contenían trabajo del otro agente, consolidé `master` con `git merge --ff-only 69079d1` (sin duplicar commits, sin tocar la rama del frontend). Estado final: `master` de unibus contiene TODO mi trabajo (A1-A3 + threading + bump 0.3.0), tests `pkg/...` verdes, working tree con el WIP del frontend (mobile/playground/android/web) intacto. **Lección operativa:** dos agentes Claude en el mismo repo comparten HEAD/índice; hace falta worktrees separados (`git worktree`) o repos clonados por agente para evitarlo.