- reports/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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-repodataforge/unibus) y~/DataProyects/Github/agents_and_robots(Giteaegutierrez/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.<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
-
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 bind0.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 porss), el comportamiento desde un host remoto es idéntico. El agente de frontend puede confirmarlo desde el móvil. -
El Agent "pesado" todavía no corre sobre unibus. Conflicto real de dependencias entre ecosistemas:
unibusdepende defn-registry, cuyo módulo raíz exigemaunium.net/go/mautrix v0.28.0, incompatible con elshell/matrixdel repoagents(escrito parav0.21.1; MVS forzaría v0.28 y rompe 3 call-sites de la API mautrix). Por eso elunibusTransportvive 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 instanciarunibusTransportdirectamente 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 subiragentsa mautrix v0.28 (migración aparte, fuera de alcance de esta tanda). -
Scanner() RoomScannersigue 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. -
libolmno instalado en esta máquina → el repoagentsse compila/test con-tags goolm(olm puro-Go). En producción se usalibolm. Pendiente: instalarlibolm-devsi se quiere validar el path libolm localmente. -
Test pre-existente en rojo, ajeno a este trabajo:
shell/logger/TestCleanOldLogsfalla 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. -
Sin push a remoto. El trabajo queda consolidado en
masterlocal de ambos repos; el push a Gitea queda para/full-git-pushdel operador.agents_and_robots:master= merge--no-ffdeissue/unibus-transport(commitaa595f5), 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 nuestrosgit checkoutse interleavearon: mis commits A1/A2/A3 (--bind, deploy, service block) acabaron directamente enmaster, y los de threading + bump (2209283,69079d1) cayeron sobre la rama del otro agenteissue/frontend-web-movil. Como esos dos commits son lineales sobre mi A3 (b2e6712) y NO contenían trabajo del otro agente, consolidémastercongit merge --ff-only 69079d1(sin duplicar commits, sin tocar la rama del frontend). Estado final:masterde unibus contiene TODO mi trabajo (A1-A3 + threading + bump 0.3.0), testspkg/...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.