Files
unibus/app.md
T
egutierrez e1a7402ff1 chore: bump unibus to 0.9.0 (live user-add + clientcheck)
New capability membershipd user add --store kv against a live cluster plus
cmd/clientcheck end-to-end verification (issue 0011 gaps, report 0012). Adds
the capability growth log entry.

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

16 KiB
Raw Blame History

name, lang, domain, version, description, tags, uses_functions, uses_types, framework, entry_point, dir_path, repo_url, service, e2e_checks
name lang domain version description tags uses_functions uses_types framework entry_point dir_path repo_url service e2e_checks
unibus go infra 0.9.0 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.
service
messaging
nats
e2e
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
cmd/membershipd projects/message_bus/apps/unibus https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus
port health_endpoint health_timeout_s systemd_unit systemd_scope restart_policy runtime pc_targets is_local_only
8470 /healthz 3 unibus-membershipd.service user always systemd-user
lucas-linux
false
id cmd timeout_s
build CGO_ENABLED=0 go build ./... 180
id cmd timeout_s
vet CGO_ENABLED=0 go vet ./... 120
id cmd timeout_s
unit CGO_ENABLED=0 go test ./... 180
id cmd health timeout_s
smoke 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 & http://127.0.0.1:18470/healthz 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):

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.9.0 (2026-06-07) — cierre de los gaps que el despliegue del cluster (report 0011) dejó abiertos (report 0012). (GAP A) Nueva capability membershipd user add|list|revoke --store kv: alta/baja de usuarios contra el KV replicado del cluster EN MARCHA, sin el procedimiento de parar-sembrar-rearrancar. Usa la conexión interna privilegiada — el daemon persiste su identidad de servicio con --internal-id-file (cada nodo genera/carga la suya, 0600 junto a las claves TLS) y la CLI, ejecutada por loopback en un nodo, presenta esa nkey que el autenticador reconoce con permisos plenos de JetStream; ninguna identidad de usuario normal puede tocar los buckets KV_UNIBUS_* bajo la ACL por-subject. El alta es idempotente (re-alta de la misma clave = ErrUserExists explícito, sin sobrescribir ni elevar rol), commitea con quórum 2/3 (HA, imprime followers_current) y rechaza un destino remoto sin --ca (igual que migrate-to-kv). (GAP B) Nuevo cmd/clientcheck: verificación end-to-end real con un cliente autenticado (identidad operator, nkey+TLS+https) que crea una room E2E, publica y recibe descifrado contra el cluster vivo, incluido un nodo parado a media transmisión donde el cliente hace failover a un superviviente y sigue recibiendo con cero pérdida (quórum 2/3) — el plano de datos que el chaos test del 0011 nunca probó. (GAP C) Runbook deploy/cluster/README.md corregido: el orden de arranque "magnus solo y verifica healthz" deadlockeaba (un nodo solo no tiene quórum del meta-group y nunca sirve healthz); se documenta el arranque por quórum, que R1 es un SPOF inservible (ir directo a R3) y la nueva vía de alta con el cluster vivo. La plantilla de deploy (unit + deploy-cluster.sh) emite ya INTERNAL_ID_FILE y el flag. Verificado contra los 3 VPS reales (magnus + homer + datardos); posture enforce+ACL+TLS+R3 intacta.
  • v0.8.0 (2026-06-07) — completar y endurecer el cluster (issue 0006, fases 0006a0006g) que cierra los bloqueantes de la auditoría dedicada del cluster (report 0008) y cablea el control plane descentralizado que 0003 dejó a medias. (0006a) Se cablea el nonce replicado en el binario: un nodo con --cluster-name usa el bucket JetStream KV compartido obligatoriamente (fail-fast si no se crea), cerrando el replay cross-node (N3); el "ciclo bootstrap" se resuelve con una identidad interna efímera que el authenticator reconoce (full perms) y una conexión in-process privilegiada. (0006b) Se cierra la fuga del control plane por $JS.API.> (N2): la ACL pasa a un allow-set cerrado por-room (JS API solo de los streams UNIBUS_<room> del peer), dejando KV_UNIBUS_*/OBJ_* fuera del set y, por tanto, denegados. (0006c) Se cablea el store KV descentralizado (--store kv|sqlite, default sqlite = baseline idéntico) con un storeHolder fail-closed que rompe el ciclo bootstrap del authenticator. (0006d) Posture homogénea: un nodo rechaza unirse al cluster sin enforce, y /healthz publica la posture (N1). (0006e) Todos los clientes llaman RefreshSession tras cambios de membresía (N4), de modo que la ACL es usable bajo enforce sin desactivarla. (0006f) Bajos: secreto de cluster fuera de argv (--cluster-pass-file/env + inyección en routes), migrate-to-kv rechaza target remoto sin --ca, y docs de CA separada para routes + R1 SPOF vs R3 HA. (0006g) Material de deploy del cluster de 3 nodos (magnus+homer+datardos) en deploy/cluster/ (certs, unit, script de despliegue dry-run, runbook) — sin tocar ningún VPS. Toda la regresión de auditorías previas + los ataques 0008 siguen verdes; govulncheck 0 alcanzables. Branch-by-abstraction: con --store sqlite el single-node sigue idéntico y desplegable en todo momento.
  • 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.