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

182 lines
11 KiB
Markdown

# 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.<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.