Vista generada de dec-0120: styx consume spire: control Bun↔daemon (session-ipc v2) y bus NATS
docs/decisions/dec-0120-spire-consumido-por-styx.mdVista generada desde
docs/decisions/dec-0120-spire-consumido-por-styx.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-09-30 |
| Fichero | docs/decisions/dec-0120-spire-consumido-por-styx.md |
Enmienda a: r07, r18, dec-0119
Por qué importa (del frontmatter del ADR):
Fija cómo consume styx el SDK spire: el socket de control Bun<->daemon pasa a session-ipc v2 (spire unix, sobre firmado, handshake, policy generada de BUS_ROUTES), el NATS de los seis servicios va por @styx/bus sobre spire, el daemon publica sus evt con spire-zig, la ACL de NATS por servicio se genera de la misma tabla, y ningún proceso habla NATS ni el socket del daemon por otro camino (guard). Los contratos siguen siendo @styx/api-contracts (r18, dec-0116).
Nodos del roadmap que lo citan en refs: track/spire, track/docs, track/oss-extraction
Páginas de la documentación que lo citan: Puesta en marcha de un nodo (implementado), Generadores de la documentación (especificado), Guardas del repositorio (implementado), Arquitectura (implementado), Dispositivos, mando y handoff (especificado), Protocolos binarios (implementado), Comunicaciones entre procesos (implementado), Seguridad del data plane (implementado), Modelo de amenazas (implementado), Bus y contratos (implementado), zkit, spire y conduit (implementado), Instalar (especificado), Seguir una reproducción por todos los servicios (especificado)
Leído de docs/decisions/dec-0120-spire-consumido-por-styx.md, el fichero canónico.
dec-0119, que este ADR aplica.MKS2508/spire, rama feat/zig-sdk (ADR 0001 del SDK, spec/ENVELOPE_V2.md). Este
ADR decide el consumo en styx, no el diseño del SDK.dec-0120 y conduit dec-0121 (orden fijado en el índice tras la
tanda 3).845017e, base con zkit integrado)native/zig/media-daemon/ipc/control.zig (2 564 líneas) implementaba un
protocolo JSON propio ([u32 len][0x01][JSON]) sin autenticar: cualquier proceso con acceso al
socket abría sesiones o las cerraba. Parsers de campos a mano, broadcast por fd de señales de
transporte y de métricas de caché, eventos que nadie consumía. En Bun,
media-daemon-client.ts (850) y transport-signals-client.ts (577) repetían el framing, más
el cliente de los harness (tools/soak-harness/lib/ipc.ts, 593) y el script Python del motor
de medios.bus.ts (conexión), su query-server.ts o cmd-server.ts
(suscripción, deserializeEnvelope, validación, respuesta) y sus clientes; 2 877 líneas en
total, sin firma ni identidad de servicio, contra un nats-server sin autenticación.@styx/api-contracts/bussubject, kind, version, maxBytes, JSON Schema de
petición y respuesta) en packages/api-contracts/src/bus/: nats.ts (entre servicios, con
los schemas de dominio que ya existían) y media-daemon.ts (socket de control).BUS_ROUTES (routes.ts) es la tabla única de autorización: servicio que atiende,
transporte, contrato, emisores admitidos y rate. De ella salen la policy de cada handler TS
(busHandler la toma de la tabla, el servicio no la escribe), la policy de cada handler del
daemon (generada) y la ACL de NATS (generada).styx-ops (operación y
harness, fuera del keyring de producción). Nada de consumidores "previstos".bun run gen:bus genera native/zig/media-daemon/ipc/contracts.generated.zig (structs con
validate() por @spire/bus/codegen + policies del socket) y
deploy/nats/nats-authorization.conf; bun run check:bus falla si lo versionado difiere.protocols/session-ipc v2SO_PEERCRED
(STYX_CONTROL_ALLOWED_UIDS/_GIDS), handshake mutuo firmado en 2 s, un sobre v2 firmado por
mensaje, router con los contratos y policies generados, presupuesto de memoria por mensaje.control.zig queda en lo del daemon: sesiones, binding de capabilities (dec-0117 I2),
empaquetado (dec-0110). Subjects cmd.media.{createSession, openMoqtSession, openPackaging, closePackaging, seek, cancel, closeSession} y qry.media.ping.status del sobre; negocio en
{ error } del payload.SessionCreated, TransportEndpoint,
PipelineReady, Progress, Metrics, Closed), Subscribe*TransportSignals,
SubscribeCacheMetrics, MediaEngines, MediaProbe. El stat que playback-svc hacía del
asset (segunda apertura fuera del path guard) lo sustituye fileSize en la respuesta.0x01 se rechaza. No hay modo de
compatibilidad (styx es el único cliente; mantener v1 dejaría abierto el socket sin
autenticar). Detalle: protocols/session-ipc/SESSION_PROTOCOL.md, version-history.md.@styx/buspackages/bus es el único punto de los servicios que toca spire: identidad desde el entorno
(SPIRE_SEED, SPIRE_KEYRING, SPIRE_NATS_NKEY_SEED; sin identidad no se arranca),
connectBusNats/startServiceBus, busHandler (policy de la tabla), request/emit/
emitAcked con traceparent del span activo, mapeo de status a BUS_*, cliente del daemon
(createDaemonBus) y @styx/bus/testing (router en proceso con identidades de prueba, para
que los tests de cada servicio pasen por la misma tubería que producción).test/no-direct-bus-transport.test.ts falla si algún fichero de apps/,
packages/ o tools/ importa nats, o si alguien fuera de @styx/bus importa
@spire/bus/nats o @spire/bus/unix. En Zig, audit:safety S1 ya no tiene allowlist de
bind/unlink para control.zig: un socket crudo en el daemon vuelve a ser un hallazgo.evt con spire-zigevt.media.transportSignals: el daemon sella cada muestra con su identidad y la publica en
NATS core (latest-wins, no JetStream) con su NKey (ResilientPublisher: conexión y escrituras
acotadas, descarta mientras NATS no está, reconecta con espera exponencial). realtime-svc la
consume; su ruta sólo admite a media-daemon. playback-svc deja de ser relay.NATS_URL el daemon no publica (no hay a quién).deploy/nats/nats.conf incluye la ACL generada: un usuario NKey por servicio que publica
exactamente lo que alguna allowlist le deja, suscribe sus handlers y su inbox
(_INBOX_<servicio>), y allow_responses sólo si atiende cmd/qry. Un servicio sin subjects de
publish recibe deny: [">"] (en nats-server una allow: [] no restringe nada; el bug salió
en el test de ACL y está arreglado en spire 3a3e1ad).$JS.API.STREAM.INFO.<su stream> y nada más de la API de JetStream; connectBusNats comprueba
al arrancar que sus streams existen con exactamente sus subjects y sin republish/mirror/
sources, y si no, no arranca. Los crea y los corrige la identidad de despliegue
styx-deploy (BUS_PROVISIONER: $JS.API.STREAM.{CREATE,INFO,UPDATE} de CATALOG y
SOURCES, nada más; no está en el keyring ni firma sobres) con bun run bus:streams, paso de
despliegue entre la infra y los servicios; su semilla no la recibe ningún contenedor de
servicio. Motivo (hallazgo del escéptico, reproducido): con STREAM.UPDATE, catalog-svc podía
añadir qry.identity.session a su stream con un republish hacia su inbox, leer el
accessToken que styx-ops manda en esa petición y contestarle con un PubAck; la ACL de
subscribe no lo impedía porque el que publica es el servidor.SPIRE_SEED/SPIRE_KEYRING/SPIRE_NATS_NKEY_SEED y a
nats-server las claves públicas SPIRE_NKEY_* (también la de styx-deploy); sin alguna,
compose no arranca. bun run bus:keys escribe un juego nuevo en deploy/.env (fuera de git,
idempotente).bun run test:bus-acl levanta un nats-server efímero con esa ACL y prueba que un servicio no
publica ni suscribe en subjects ajenos ni crea o cambia streams (el caso de arriba: rojo
con la ACL anterior, verde con ésta). Corre en la CI (guard.yml), igual que check:bus.deliver_subject, mirrors/sources) ni de cuenta de sistema:
cualquiera de ellos le deja al servidor copiar mensajes a donde el servicio sí puede leer. Y
los payloads del bus (por ejemplo el accessToken de qry.identity.session) viajan
firmados, no cifrados: el sobre v2 da autenticidad, integridad y anti-replay, no
confidencialidad; los lee en claro el broker y cualquier identidad que pueda suscribir el
subject. Cifrado extremo a extremo o TLS al broker: follow-up (dec-0119 §3.1, modo operador).| dec-0119 | Estado en styx |
|---|---|
| §1 todo lo que cruza un proceso pasa por spire | Cumplido para NATS y el socket de control (guard TS + S1 en Zig). Pendiente lo que traiga la lane conduit (ver Consecuencias). |
| §3.1 identidad NATS | NKey por servicio + ACL generada, deny por defecto, diff contra lo desplegado (check:bus, en CI). Streams: los servicios sólo INFO; crear/cambiar es de styx-deploy (§5). Payloads firmados, no cifrados (§5). No cumplido: modo operador con JWT de cuenta (nsc) y límites JetStream por cuenta; hoy los usuarios NKey van en la config del servidor. Follow-up. |
| §3.2 unix | SO_PEERCRED + handshake mutuo en 2 s. Pendiente: directorio 0750 / socket 0660 con grupo dedicado en el despliegue. |
| §4 deny-by-default | Cumplido: handler sin ruta no se construye (TS), sin policy no compila (Zig, generada). |
| §5 sobre v2, anti-replay | Cumplido (spire). Caché de nonces con cuota por emisor (allowlist antes del anti-replay; cada emisor admitido reserva burst + perSec × ventana): un emisor que inunda agota la suya y no la de otro. Daemon: ventana 10 s, 100/s ráfaga 100 → 1 110 por (emisor, ruta), 12 210 en total. En memoria: un consumidor por servicio hoy; Valkey al escalar réplicas (follow-up, sin cuota por emisor todavía). |
| §5.2 actor JWT | Todas las rutas actuales son actor: forbidden: ningún mensaje del bus actúa hoy en nombre de un usuario. |
| §6 validación de entrada y salida | Cumplido a los dos lados (TypeBox compilado; validate() generado en Zig). |
| §7 límites | Tamaño por contrato, rate por (emisor, subject), deadline. |
| §8 audit | A OTel y al log (sin payloads ni tokens). Los rechazos de SCT del daemon salen por el bus como evt.security.capabilityRejected (hook on_reject de spire.cap, SEC-Z12). Pendiente: evt.security.comms.*. |
| §9 SCT en spire-zig | Cumplido. El módulo security del daemon es spire.cap (sct + authority) más request_cap, que es de styx; playback-svc firma con @spire/bus/sct. Las copias de styx están borradas (security/{sct,authority,sct_testing,sct_vectors_test}.zig, service/capability/sct.ts) y el daemon exige que los vectores de spire sean protocols/capability-token/vectors.json byte a byte. El audit:safety de styx ya no recorre sct.zig (vive en spire): follow-up de un guard equivalente en spire. |
| §5.4 coste | Medido en el SDK (256 B): sellar 18.5/59 µs TS, 55.6/126 µs Zig; dispatch completo 89/282 µs TS, 127/260 µs Zig (p50/p99). Cabe; sin MAC de sesión. |
git diff 845017e..w4/spire, sin docs ni generados): producción TS −2 851, producción
Zig −2 042 (control.zig 2 564 → 490), tests −266 (TS −636, Zig +370: el control socket se
prueba ahora extremo a extremo con el router real), harness −370, infra y guards +294.
@styx/bus (+813) sustituye seis bus.ts y los query/cmd servers.git diff --numstat d758d71.., sin docs, lock ni generados): Zig
de producción −1 324 (SCT de styx borrado −1 377: authority 857, sct 482, sct_testing
38; control.zig +39 de rollback y ventana; security.zig +12), tests Zig +123 (rollback,
OOM y flood del socket +246; sct_vectors_test.zig −123, sus vectores los comprueba spire);
TS de producción −192 (sct.ts −265; ACL con provisioner, comprobación y provisión de
streams +73), tests TS +1 (codec SCT a spire −56, caso de streams en la ACL +55), scripts y
harness +38 (bus:streams, provisión en el gate y el web VOD). El daemon no deja nada detrás
en ningún camino de error de createSession/openMoqtSession (errdefer con rollback, y
onReplyFailure de spire si la respuesta no se puede sellar).tools/media-engine-bench/daemon-smoke.py (Python, protocolo v1, ya sin SCT desde dec-0117)
queda fuera de servicio hasta portarlo al cliente spire de los harness: follow-up.w4/conduit, dec-0121): se escribió sobre el IPC y el NATS a mano
(ingest_ipc.zig en control.zig, ingest-daemon-client.ts, ingest-event-publisher.ts,
consumidor durable de evt.media.ingestCompleted en catalog, principal-resolver.ts pidiendo
qry.identity.session). Al integrarse con esta rama hay que portarla: contratos
cmd.media.openIngest/… en media-daemon.ts y handlers en el router del daemon; rutas en
BUS_ROUTES (playback-svc → identity-svc qry.identity.session pasa a tener emisor de
servicio real); publicación con emitAcked; y consumo durable con ack/nak/term, que
@styx/bus todavía no ofrece (follow-up de spire: consumidor JetStream durable con la misma
tubería del router).CATALOG/SOURCES, que ya existía.docs/track/spire/evidence/ (y la del SDK en MKS2508/spire docs/evidence/2026-09-29-sdk/):
ACL en nats-server real (rojo con allow: [], verde con deny; rojo con STREAM.UPDATE en un
servicio, verde sin él), check:bus en CI con tres mutantes en rojo, rollback del daemon con
cinco mutantes en rojo, R-02 por el socket spire (5/5, controles de auth refutados), R-06 con el
daemon publicando por NATS bajo ACL (verde y rojo con la NKey de otro servicio), suites
Zig/TSAN/TS, guard con control negativo, e2e web VOD.