Files
unibus/app.md
T
egutierrez 6976537842 chore(0005): bump unibus to 0.7.0, close issue 0005 (hardening 2)
Hardening 2 (issue 0005, fases 0005a-0005e) cierra los hallazgos nuevos de la
re-auditoría red-team (report 0006): bump de nats-server + toolchain (16 CVEs ->
0 alcanzables), drop de frames sin firma en rooms SignMsgs, limiter global de
bytes en vuelo contra el DoS por concurrencia, TLS obligatorio en bind publico, y
cableado de la ACL por subject que cierra el wildcard metadata leak. Detalle por
fase en el capability growth log del app.md y en el report 0007.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:17:41 +02:00

235 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: unibus
lang: go
domain: infra
version: 0.7.0
description: "Bus de mensajería unificado sobre NATS+JetStream con cifrado E2E por room (megolm/olm reducido): service de membresía/claves, librería cliente y peers demo."
tags: [service, messaging, nats, e2e]
uses_functions:
- generate_identity_go_cybersecurity
- seal_aead_go_cybersecurity
- open_aead_go_cybersecurity
- seal_key_box_go_cybersecurity
- open_key_box_go_cybersecurity
- sign_ed25519_go_cybersecurity
- verify_ed25519_go_cybersecurity
uses_types: []
framework: ""
entry_point: "cmd/membershipd"
dir_path: "projects/message_bus/apps/unibus"
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus"
service:
port: 8470
health_endpoint: /healthz
health_timeout_s: 3
systemd_unit: unibus-membershipd.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- lucas-linux
is_local_only: false
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
- id: smoke
cmd: "CGO_ENABLED=0 go build -o /tmp/unibus_membershipd ./cmd/membershipd && /tmp/unibus_membershipd --http-port 18470 --nats-port 14222 --db /tmp/unibus_smoke.db --store-dir /tmp/unibus_blobs --nats-store /tmp/unibus_js &"
health: "http://127.0.0.1:18470/healthz"
timeout_s: 30
---
## Qué es
`unibus` es un bus de mensajería unificado sobre NATS + JetStream. Una capa fina
encima de NATS aporta lo que NATS no tiene: membresía de rooms, invitaciones con
reparto de clave, y cifrado extremo a extremo (E2E) del payload por room, con
rotación de clave activa (forward secrecy) al expulsar a un miembro.
Todos los participantes —procesos/workers, interfaces de chat humanas, agentes
LLM— son peers de primera clase que hablan el mismo protocolo y se diferencian
solo por las rooms a las que se unen y por lo que hacen con lo que reciben.
### Piezas
| Componente | Ruta | Rol |
|---|---|---|
| `membershipd` | `cmd/membershipd` | Service (control plane): metadata de rooms, directorio de miembros, reparto de claves selladas, object store de media. Arranca NATS embebido si no se pasa `--nats-url`. |
| librería cliente | `pkg/client` | La API que consume cualquier peer. Crear/unirse a rooms, invitar, publicar/suscribir, request/reply, kick con rotación de clave, media cifrada. |
| `worker` | `cmd/worker` | Peer demo: publica un contador incremental cada segundo a una room cleartext. |
| `chat` | `cmd/chat` | Peer demo: suscriptor en vivo (modo simple) + demo de cifrado E2E y forward secrecy (`--demo-encrypted`). |
### Dos planos
- **Control plane**: HTTP autoritativo en `membershipd`. Quién está en cada room,
sus claves públicas, y la clave de room `K` sellada por epoch para cada miembro.
- **Data plane**: NATS. Los mensajes (frames) viajan por subject; los blobs de
media NO viajan por el bus, se cifran y se suben al object store, y por NATS
solo viaja una referencia (hash + nonce).
### Criptografía (importada del registry, NO reescrita)
El cifrado E2E se compone de las 7 primitivas del capability group
`e2e-messaging` (`docs/capabilities/e2e-messaging.md`), importadas del paquete
`fn-registry/functions/cybersecurity`. Esta app no reimplementa ninguna
primitiva criptográfica.
## Ejemplo
Demo end-to-end con `go run` (NATS embebido, nada que instalar):
```bash
cd projects/message_bus/apps/unibus
# 1. Service de membresía/claves (NATS embebido en :4250, HTTP en :8470)
go run ./cmd/membershipd
# 2. En otra terminal: peer publicador (proceso) — publica ticks cada 1s
go run ./cmd/worker
# 3. En otra terminal: peer suscriptor (chat humano) — imprime cada tick en vivo
go run ./cmd/chat
# 4. Demo de cifrado E2E + forward secrecy (contra el membershipd ya corriendo):
# A crea room cifrada, invita a B, A publica (B descifra), A expulsa a B,
# A publica de nuevo en el nuevo epoch (B ya NO puede descifrar).
go run ./cmd/chat --demo-encrypted
```
Para apuntar a un NATS externo en producción: `--nats-url nats://host:4222` en
`membershipd`, `worker` y `chat`.
## Cuando usarla
- Cuando necesites un tejido de mensajería donde procesos, humanos y agentes LLM
sean peers uniformes (mismo protocolo, distinta política por room).
- Cuando quieras rooms cifradas E2E con forward secrecy (paridad con Matrix) sin
montar un Synapse: `room.ModeMatrix`.
- Cuando quieras fan-out cleartext rápido para telemetría/coordinación de
procesos: `room.ModeNATS`.
- Como sustituto de la capa de transporte Matrix de `agents_and_robots` (fase
posterior; v1 valida el bus de forma autónoma).
## Gotchas
- **El service NO está endurecido (v1).** No hay TLS, ni rate-limit, ni auth en
las rutas GET de lectura. Confía en la red interna. Las rutas mutantes
(`/rooms`, `/invite`, `/rekey`) sí exigen firma Ed25519 del owner sobre los
bytes canónicos de la request. Endurecer es fase posterior.
- **Identidad = secreto crítico.** El archivo de identidad (`worker.id`,
`chat.id`) contiene las claves privadas (Ed25519 + X25519). Se escribe 0600.
Perderlo = mensajes ilegibles, sin recuperación. Trátalo como una clave SSH.
- **Las rooms reciben un ULID fresco al crearse.** No hay "crear o unirse por
nombre": cada `CreateRoom` produce un room nuevo. Los peers demo cleartext
comparten el *subject* (NATS enruta por subject), así que worker→chat funcionan
aunque cada uno tenga su propio room id mapeado al mismo subject.
- **La media no viaja por el bus.** `PublishMedia` cifra, sube al object store y
publica solo un `BlobRef`. El receptor, si ve `Frame.Blob != nil`, descarga y
descifra con `FetchMedia`. El frame de media NO lleva payload inline (su nonce
vive en `BlobRef.Nonce`); `Subscribe` no intenta descifrar payloads vacíos.
- **Forward secrecy depende del rekey.** `Kick` rota `K` a un epoch nuevo y la
re-sella solo para los miembros restantes. El expulsado pierde acceso a los
mensajes publicados después del kick, pero conserva los anteriores (las claves
de epochs pasados no se borran: cifraban datos que ya podía leer).
- **NATS embebido escribe JetStream en disco.** `--nats-store` apunta a
`local_files/jetstream`; borrarlo resetea el historial persistido.
- **Build sin CGO.** Usa el driver `modernc.org/sqlite` (pure-Go) y el paquete
`cybersecurity` del registry compila limpio con `CGO_ENABLED=0`. NO requiere
`fts5` ni `gcc`.
## Convención de subjects
```
proc.<svc>.<canal> telemetría/coordinación de procesos (proc.test.ticks)
rpc.<svc> request/reply (rpc.indexer)
room.<grupo> chat humano/grupo (room.general)
agent.<nombre>.{in,out} inbox/outbox de agente LLM (agent.scout.in)
```
## Capability growth log
- v0.7.0 (2026-06-07) — hardening de seguridad 2 (issue 0005, fases 0005a0005e)
que cierra los hallazgos nuevos de la re-auditoría red-team (report 0006) y
lleva el veredicto de exposición pública a "sí-con-condiciones". (0005a) Bump de
`github.com/nats-io/nats-server/v2` v2.10.22→v2.11.15 y de la toolchain a
go1.26.4: `govulncheck ./...` pasa de 16 vulnerabilidades alcanzables (14 del
servidor NATS embebido + 2 de la stdlib) a 0. (0005b) `client.processFrame`
ahora descarta cualquier frame sin firma en una room `SignMsgs` (antes verificaba
solo si la firma venía presente, lo que permitía suplantar `Sender` con
`Sig==nil`). (0005c) Nuevo limiter global de bytes en vuelo
(`pkg/membership.inflightLimiter`) que acota la memoria agregada que el control
plane bufferiza bajo concurrencia (el límite por-request y el rate-limit por-IP
no acotaban el total): un flood concurrente multi-IP se descarta con 503 en vez
de crecer sin techo (el RSS deja de escalar con N). (0005d) El guard de arranque
`validateBootConfig` ahora exige `--tls-cert/--tls-key` en bind no-loopback (un
control plane público sin TLS servía metadata en claro). (0005e) Se cablea por
fin en `membershipd` la ACL por subject que ya existía huérfana desde 0003e
(`busauth.NewNkeyAuthenticatorACL` + nuevo adaptador `busauth.PermissionsFromSubjects`
sobre `membership.SubjectACLFor`): un registrado no-miembro ya no puede
`Subscribe(">")` y captar los subjects/advisories de rooms ajenas. Residuales
documentados: `$JS.API.>` sigue compartido (cierre completo = NATS accounts por
identidad, diferido) y los clientes deben `RefreshSession` tras cambios de
membresía (chat/worker aún no lo hacen). El comportamiento de un solo nodo no
cambia y master sigue verde.
- v0.6.0 (2026-06-07) — descentralización / alta disponibilidad (issue 0003,
fases 0003a0003e), report 0006. El servidor NATS embebido gana soporte de
cluster con routes autenticadas (secreto de cluster) y TLS mutuo de nodo
(`pkg/embeddednats.ClusterConfig` + `busauth.RouteTLSConfig`, reusando la CA
del 0001). El control plane (`pkg/membership.Store`) pasa a interfaz por
branch-by-abstraction: `sqliteStore` (default) + `jetstreamStore` nuevo sobre
JetStream KV replicado (réplicas configurables R1→R3), con `IsAuthorized`
fail-closed ante pérdida de quorum. `membershipd migrate-to-kv` mueve el
estado SQLite→KV de forma idempotente con backup previo. Los blobs
(`pkg/blobstore.Store`, ahora interfaz) ganan un backend NATS Object Store
replicado además del disco. El cliente acepta listas de seeds NATS y de
control planes con failover/reconnect nativo, el anti-replay pasa a un store
de nonces compartido en KV con TTL (cierra el agujero de replay multi-nodo), y
se implementa la ACL por subject derivada de pertenencia (audit H4 residual:
`busauth.NewNkeyAuthenticatorACL` + `membership.SubjectACLFor` +
`client.RefreshSession`). Todo viaja detrás del flag `decentralized` (off):
el comportamiento de un solo nodo (SQLite + disco) no cambia y master sigue
verde. El despliegue multi-nodo real (0003f) lo ejecuta el humano.
- v0.5.0 (2026-06-07) — hardening de seguridad (issue 0004) que cierra los
hallazgos de la auditoría red-team (report 0004) y lleva el veredicto de
exposición pública de "NO" a "sí-con-condiciones". Anti-DoS pre-auth
(`http.MaxBytesReader` por ruta + rechazo por `Content-Length` + rate-limit
por IP + `MaxHeaderBytes`); guard de fail-open que prohíbe arrancar con bind
público o TLS sin `--bus-auth enforce`; autorización por pertenencia en los GET
de room (metadata y clave sellada solo para miembros / el propio endpoint);
rooms cleartext deshabilitadas en bind público (contenido siempre E2E, mínimo
defensivo del data plane mientras la ACL por subject llega con 0003); TLS en el
control plane HTTP con la CA propia y cliente que exige `https` cuando hay CA;
y los medios H6/H7/H12 (owner ligado al firmante, `IsAuthorized` antes del
nonce-cache con poda O(expired) + cap, errores genéricos al cliente). Cada
hallazgo lleva su test adversarial `TestAudit_*` portado como regresión.
- v0.4.0 (2026-06-07) — descubrimiento de rooms: `GET /members/{endpoint}/rooms`
lista las rooms de un endpoint con su metadata y rol, y `client.ListMyRooms()`
lo consume. El control plane es pull (no hay push de invitaciones), así que un
peer recién invitado a una room cifrada la descubre por polling y luego hace
`Join` + `Subscribe`. Pieza base para que los bots de `agents_and_robots`
hablen por el bus en vez de Matrix (modelo "todo son rooms", E2E).
- v0.3.0 (2026-06-06) — `membershipd` se convierte en service de verdad: flag
`--bind` (default 127.0.0.1) que gobierna a la vez el HTTP de control y el NATS
embebido (`embeddednats.StartHost`), de modo que con `--bind 0.0.0.0` un
teléfono o PC de la LAN conecta a ambos planos. Se añade un systemd-user unit
(`deploy/unibus-membershipd.service`, `Restart=always`) + `deploy/install.sh`
idempotente, y el bloque `service:` queda completo (systemd-user, restart
always, health `/healthz`). El `Frame` (pkg/frame) gana threading aditivo
(`ThreadID`, `ReplyTo`) y un tipo `REACT`, con `PublishReply`/`React` en el
cliente — la base para que bots de chat hablen por el bus (fase 2). Cambios
100% aditivos: el wire de los frames no-threaded es idéntico y los tests
existentes siguen verdes.
- v0.2.0 (2026-06-03) — el playground gana un benchmark de rendimiento
(`GET /api/bench`, SSE): un publisher inunda una room con miles de mensajes a
N subscribers y una gráfica en vivo anima el throughput. Expone las dos
políticas como flags independientes (JetStream/`Persist` y encriptación
E2E/`Encrypt`) más tamaño de payload, de modo que se mide el coste de cada
capa (core NATS vs JetStream vs E2E vs E2E+JetStream) usando la librería
cliente real, sin reimplementar nada.