Files
message_bus/reports/0001-2026-06-06-unibus-service-fase2-transport.md
T
egutierrez 57a1602e8f chore: auto-commit (1 archivos)
- reports/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:32 +02:00

11 KiB

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.ClientScanner() 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.<name>.{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.