Caddy strips /api via `handle_path /api/*` before forwarding to membershipd, so the SPA's GET /api/directory arrives as GET /directory. The route was registered with the /api prefix, so the stripped request hit no route and returned 404 in production: the directory never resolved and uniweb fell back to short ids. Every other control-plane route is registered without the prefix; this aligns directory with them. The unit test passed despite the bug because it requested /api/directory, the same wrong path as the registration. Corrected the request paths to /directory so the test now exercises the real production path (verified: reverting the registration to /api/directory now makes TestDirectoryGolden fail with 404). Bump 0.15.0 -> 0.15.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
27 KiB
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.15.1 | 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. |
|
|
cmd/membershipd | projects/message_bus/apps/unibus | https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus |
|
|
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 roomKsellada 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. - Gestión de usuarios: storage unificado, alta por dos vías. El allowlist de
usuarios vive en el MISMO store que las rooms (
pkg/membership.Store): SQLite en single-node, JetStream KV replicado (UNIBUS_users) en cluster. ElServerya tiene ese store privilegiado abierto (es quien sirve el KV en cada nodo), así que exponeGET/POST /usersyPOST /users/{signpub}/revokecomo API HTTP admin-only, simétrica con las rutas de rooms: el panel de administración firma como admin y el server ejecuta la mutación contra el mismo store. El panel NO necesita--db, ni la identidad interna, ni correr en un nodo del cluster; funciona idéntico en single-node y cluster. La autorización es default-deny: solo un firmante que el store confirma comorole == "admin"activo pasa, cualquier otro recibe 403 (encima de la firma+nonce+TLS ya existentes). La CLImembershipd user add --store kvsigue existiendo SOLO para sembrar el admin #0 (bootstrap del huevo-gallina: sin un admin sembrado no hay quién firme el primerPOST /users); a partir de ahí toda la gestión es HTTP admin-only. El alta es idempotente igual que la CLI: re-alta de una clave ya registrada = 409, sin sobrescribir ni elevar rol; el revoke es un flip de status (sin hard-delete), auditable. - 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
CreateRoomproduce 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.
PublishMediacifra, sube al object store y publica solo unBlobRef. El receptor, si veFrame.Blob != nil, descarga y descifra conFetchMedia. El frame de media NO lleva payload inline (su nonce vive enBlobRef.Nonce);Subscribeno intenta descifrar payloads vacíos. - Forward secrecy depende del rekey.
KickrotaKa 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-storeapunta alocal_files/jetstream; borrarlo resetea el historial persistido. - Build sin CGO. Usa el driver
modernc.org/sqlite(pure-Go) y el paquetecybersecuritydel registry compila limpio conCGO_ENABLED=0. NO requierefts5nigcc.
Directorio de nombres (endpoint → handle)
Cada frame del bus lleva el endpoint id del remitente
(base64url(sha256(signPub)), sin padding — frame.EndpointID), no un nombre
legible. Para que un cliente muestre nombres en vez de hashes, el control-plane
expone la ruta del directorio. La SPA la llama como GET /api/directory, pero
Caddy hace handle_path /api/* y stripea /api antes de reenviar a
membershipd, así que el servidor la registra (como todas las rutas del
control-plane) SIN el prefijo: GET /directory:
- Auth: el mismo middleware de firma que el resto del control-plane
(cabeceras
X-Unibus-Pub/Ts/Nonce/SigsobreCanonicalRequest). NO es admin-only: cualquier usuario activo del bus (member o admin) puede leerlo. En modoenforce, una request sin firmar recibe 401 antes de llegar al handler. - Respuesta
{ "members": [ { "sign_pub", "endpoint", "handle", "role" } ] }, solo usuariosstatus=active. Elendpointlo computa el servidor desde elsign_pubcon la misma derivación que el bus, así que casa byte a byte con el sender id que el cliente ya tiene en cada mensaje. - CORS: cubierto por la allowlist
--cors-originsexistente (mismas cabeceras que el resto de rutas, sin caso especial).
Provisioning de bots / unibots
Dar de alta una identidad para un proceso automatizado es un solo comando.
Antes había que derivar un keypair a mano y pasar el sign_pub a user add;
ahora bot add lo hace todo: mintea una identidad de bus fresca (Ed25519 +
X25519, la misma derivación cs.GenerateIdentity que usan worker/chat),
registra su sign_pub en el allowlist con handle y role, y escribe las
credenciales a un fichero 0600 que el proceso lee para conectar.
# 1. Provisionar el bot (store sqlite local; usa --store kv contra un cluster vivo).
membershipd bot add --handle notifier --out ./local_files/notifier.id
# provisioned bot "notifier" role=member
# sign_pub: 97d5a903...b1d4
# endpoint: HU85l2onjrK4EoTLoBfJVkGEXMw9LAjNEjPWiDS8YwM
# credentials: ./local_files/notifier.id (0600)
# 2. El proceso arranca como ese usuario leyendo el --out (formato canónico
# pkg/client.LoadIdentity, sin conversión): el worker demo lo consume directo.
worker --id-file ./local_files/notifier.id --nats-url nats://127.0.0.1:4250 \
--ctrl-url http://127.0.0.1:8470
# 3. (opcional) Verlo en el directorio / en user list.
membershipd user list
Las credenciales (--out) quedan en el fichero indicado, con permisos 0600. Es
el secreto del bot: contiene las claves privadas, trátalo como una clave SSH
(ver Gotcha "Identidad = secreto crítico"). bot add rehúsa sobrescribir un
--out existente, y registra al usuario ANTES de escribir el fichero, de modo
que un fallo nunca deja un bot a medias.
Flags: --handle y --out obligatorios; --role admin|member (default member);
--store sqlite|kv y el resto de flags de conexión idénticos a user add.
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.15.1 (2026-06-14) — fix: la ruta del directorio se registraba con prefijo /api y Caddy lo stripeaba (404 en prod); corregida a /directory.
- v0.15.0 (2026-06-14) — nombres legibles + provisioning de bots de un comando.
(1) Nuevo
GET /api/directoryen el control-plane: cualquier usuario activo del bus (member o admin), autenticado con la misma firma Ed25519 que el resto de rutas, resuelve endpoint id → handle. Devuelve{members:[{sign_pub, endpoint, handle, role}]}solo de usuarios activos; el endpoint lo deriva el servidor conframe.EndpointID, casando byte a byte con el sender id de cada frame (paridad verificada contra el vector decmd/busvectors). (2) Nuevomembershipd bot add --handle <name> --out <path> [--role] [--store]: mintea identidad, la registra en el allowlist y escribe credenciales 0600 en formatoclient.LoadIdentity, de modo que un proceso (worker/clientcheck) conecta como ese usuario sin pasos manuales. Nuevo helper exportadopkg/client.WriteNewIdentity(no sobrescribe ficheros existentes). Todo aditivo; build/vet/test verdes. - v0.14.0 (2026-06-13) — prep para el cliente browser-nativo
uniweb(issue uniweb/0001, Fase 0), todo aditivo y opt-in: (1) el nats-server embebido puede exponer un listener WebSocket (WebsocketConfig) para que un navegador hable el protocolo NATS vianats.ws, igual que los peers TCP nativos; el authenticator nkey aplica también al WebSocket. (2) El control-plane (membershipd) gana una allowlist CORS opt-in (--cors-origins) para aceptar llamadas cross-origin del navegador; vacía = CORS off, sin cambios para clientes nativos. (3)cmd/busvectorsgenera vectores de test deterministas (endpoint id, firma Ed25519, AEAD ChaCha20-Poly1305, sealed-box, wire del Frame) como contrato de paridad para el port TypeScript. Peers Go/Kotlin existentes sin cambios; build/vet/test verdes. - v0.13.0 (2026-06-13) — el frontend web se separa a su propia app
uniweb(projects/message_bus/apps/uniweb, sub-repo Gitea propio). unibus deja de contener la SPA (web/) y el gateway web (cmd/webgw): ahora es estrictamente el plano del bus (membresía/claves, librería cliente y peers demo).uniwebconsume unibus como módulo Go viareplace github.com/enmanuel/unibus => ../unibus, importandopkg/{busauth,client,frame,room}, y mantiene su propioreplace fn-registrypara las primitivas de cybersecurity. Movimiento sin pérdida de capacidad: la misma SPA y el mismo gateway, solo que en su carpeta de servicio propia. unibus build/vet/test verdes tras la extracción. - v0.12.0 (2026-06-13) — frontend web wallet por usuario integrado a master. La
SPA gana un onboarding criptográfico: cada usuario deriva su identidad de forma
determinista desde una mnemónica BIP39 de 12 palabras (esquema HKDF →
Ed25519/X25519), cifrada at-rest en el dispositivo con AES-256-GCM, con caminos
join (invitación) / login (passphrase local) / recover (re-derivación en
dispositivo nuevo, sin admin). El gateway
cmd/webgw(REST + SSE) pasa de identidad única de operador a sesiones wallet por usuario con registro por token de invitación. Integraquick/web-joinsobre el master 0.11.0 (auto-merge deembeddednats.gosin conflictos; Go build/vet/test ypnpm buildverdes). - v0.11.0 (2026-06-07) — flag dedicado
UNIBUS_NATS_MONITORque abre el endpoint de monitoring HTTP del nats-server embebido (127.0.0.1:8222, loopback only) de forma DESACOPLADA del debug-log. Antes el monitoring solo se abría conUNIBUS_NATS_DEBUG=1, que además encendía el log verboso del nats-server (rutas/RAFT/subjects a journald en claro) — incompatible con el endurecimiento del issue 0007. El cómputo de los toggles se extrae a una función puranatsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor):MONITOR=1abre el endpoint dejando el log en silencio (NoLogtrue /Debugfalse), y se mantiene el acoplamiento inverso por compatibilidad (DEBUGsigue implicandoMONITOR). El bind loopback127.0.0.1queda hardcoded — el monitoring NUNCA es público y no lleva auth; lo lee un scraper local que empuja a VictoriaMetrics (dashboardunibus-natsenfleet_monitoring). Se versiona el cableado de deploy: drop-in systemd aditivomembershipd-cluster.service.d/nats-monitor.conf(Environment=UNIBUS_NATS_MONITOR=1) + sección "NATS server metrics" en el README del cluster con el runbook de activación rolling (magnus→homer→datardos) y gate de reconvergencia R3 (followers 2/2) entre nodos. Tests nuevos: tabla pura del desacoplamiento (monitor on ⇒ log NO debug; debug ⇒ monitor; default cerrado) + server real conMONITOR=1que confirma/varz200 en loopback:8222 y server sin flag con el endpoint cerrado. Cambios 100% aditivos: sin el flag el comportamiento es idéntico; build/test verdes. - v0.10.0 (2026-06-07) — API HTTP admin-only de gestión de usuarios, cerrando la
última asimetría del control plane: las rooms tenían superficie HTTP firmada
(
POST /rooms, etc.) pero los users solo se gestionaban por CLI local o acceso directo al store. Se añadenGET /users(lista completa, incluidos revocados),POST /users(alta{sign_pub, handle, role}: valida hex de 64 chars + role en{admin, member}, 409 idempotente que no sobrescribe ni eleva rol) yPOST /users/{signpub}/revoke(flip de status, sin hard-delete). Los tres pasan por un helperrequireAdmindefault-deny que confirma contra el store que el firmante autenticado es un userrole == "admin"activo (el endpoint id es un hash one-way de la clave, así que el contexto lleva ahora también elsign_pubhex del firmante para resolverGetUser); cualquier otro firmante recibe 403, encima de la firma+nonce+TLS+ enforce ya heredadas del middleware. NO se abre conexión KV nueva ni se usa la identidad interna: el server escribe vía sus.storeprivilegiado, el MISMO que las rooms (SQLite single-node, KVUNIBUS_usersen cluster).pkg/clientganaListUsers/AddUser/RevokeUser(tipo planoUserInfo) firmando como admin, así la pestaña Users del panel deja de necesitar--db/acceso KV directo. La CLImembershipd user add --store kvqueda SOLO para sembrar el admin #0 (bootstrap). La validación design_pubse unifica enmembership.ValidateSignPubHex, reusada por la CLI y los handlers. Tests nuevos: no-admin → 403 en los tres endpoints, roundtrip admin add→list→revoke, y validación (hex inválido → 400, role inválido → 400, re-alta → 409), más un test de cliente contra un membershipd embebido. Cambios 100% aditivos: el comportamiento single-node y de las rutas de rooms no cambia; vet/build/test verdes. - 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 bucketsKV_UNIBUS_*bajo la ACL por-subject. El alta es idempotente (re-alta de la misma clave =ErrUserExistsexplícito, sin sobrescribir ni elevar rol), commitea con quórum 2/3 (HA, imprimefollowers_current) y rechaza un destino remoto sin--ca(igual quemigrate-to-kv). (GAP B) Nuevocmd/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) Runbookdeploy/cluster/README.mdcorregido: 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 yaINTERNAL_ID_FILEy 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
0006a–0006g) 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-nameusa 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 streamsUNIBUS_<room>del peer), dejandoKV_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 unstoreHolderfail-closed que rompe el ciclo bootstrap del authenticator. (0006d) Posture homogénea: un nodo rechaza unirse al cluster sinenforce, y/healthzpublica la posture (N1). (0006e) Todos los clientes llamanRefreshSessiontras 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-kvrechaza 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) endeploy/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 sqliteel single-node sigue idéntico y desplegable en todo momento. - v0.7.0 (2026-06-07) — hardening de seguridad 2 (issue 0005, fases 0005a–0005e)
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/v2v2.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.processFrameahora descarta cualquier frame sin firma en una roomSignMsgs(antes verificaba solo si la firma venía presente, lo que permitía suplantarSenderconSig==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 arranquevalidateBootConfigahora exige--tls-cert/--tls-keyen bind no-loopback (un control plane público sin TLS servía metadata en claro). (0005e) Se cablea por fin enmembershipdla ACL por subject que ya existía huérfana desde 0003e (busauth.NewNkeyAuthenticatorACL+ nuevo adaptadorbusauth.PermissionsFromSubjectssobremembership.SubjectACLFor): un registrado no-miembro ya no puedeSubscribe(">")y captar los subjects/advisories de rooms ajenas. Residuales documentados:$JS.API.>sigue compartido (cierre completo = NATS accounts por identidad, diferido) y los clientes debenRefreshSessiontras 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 0003a–0003e), 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) +jetstreamStorenuevo sobre JetStream KV replicado (réplicas configurables R1→R3), conIsAuthorizedfail-closed ante pérdida de quorum.membershipd migrate-to-kvmueve 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 flagdecentralized(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.MaxBytesReaderpor ruta + rechazo porContent-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 exigehttpscuando hay CA; y los medios H6/H7/H12 (owner ligado al firmante,IsAuthorizedantes del nonce-cache con poda O(expired) + cap, errores genéricos al cliente). Cada hallazgo lleva su test adversarialTestAudit_*portado como regresión. - v0.4.0 (2026-06-07) — descubrimiento de rooms:
GET /members/{endpoint}/roomslista las rooms de un endpoint con su metadata y rol, yclient.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 haceJoin+Subscribe. Pieza base para que los bots deagents_and_robotshablen por el bus en vez de Matrix (modelo "todo son rooms", E2E). - v0.3.0 (2026-06-06) —
membershipdse 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.0un 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.shidempotente, y el bloqueservice:queda completo (systemd-user, restart always, health/healthz). ElFrame(pkg/frame) gana threading aditivo (ThreadID,ReplyTo) y un tipoREACT, conPublishReply/Reacten 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/Persisty 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.