Spec canónica de protocols/session-ipc/SESSION_PROTOCOL.md, copiada sin reescribir.
protocols/session-ipc/SESSION_PROTOCOL.mdPágina generada desde
protocols/session-ipc/SESSION_PROTOCOL.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere.
Frame, sobre, handshake y status son los de spire (MKS2508/spire, spec/ENVELOPE_V2.md).
Este documento fija lo del daemon: qué subjects atiende, quién puede pedirlos, qué hace cada uno
y con qué errores de negocio responde. Los campos exactos (tipos, patrones, topes) están en los
contratos de packages/api-contracts/src/bus/media-daemon.ts: spire valida contra ellos la
petición y la respuesta a los dos lados (TS con TypeBox, Zig con el espejo generado).
| Subject | Kind | Emisores admitidos (BUS_ROUTES) | Tope payload | Respuesta |
|---|---|---|---|---|
cmd.media.createSession | cmd | playback-svc, styx-ops | 8 KiB | { result: { sessionId, endpoint, certHash?, transportType, fileSize? } } o { error } |
cmd.media.openMoqtSession | cmd | styx-ops | 1 KiB | { result: { sessionId } } o { error } |
cmd.media.openPackaging | cmd | playback-svc, styx-ops | 8 KiB | ver messages/media-engine.md |
cmd.media.closePackaging | cmd | playback-svc, styx-ops | 1 KiB | { closed } |
qry.media.probe | qry | catalog-svc, styx-ops | 32 KiB | ver messages/media-engine.md |
cmd.media.seek | cmd | styx-ops | 1 KiB | { found } |
cmd.media.cancel | cmd | styx-ops | 1 KiB | { cancelled } |
cmd.media.closeSession | cmd | styx-ops | 1 KiB | { closed } |
qry.media.ping | qry | styx-ops | 256 B | { sessions, packagingSessions } |
cmd.media.openIngest | cmd | playback-svc, styx-ops | 1 KiB | ver messages/ingest.md |
cmd.media.pollIngestEvents | cmd | playback-svc, styx-ops | 32 KiB | ver messages/ingest.md |
cmd.media.closeIngest | cmd | playback-svc, styx-ops | 1 KiB | { closed } |
actor: forbidden (ninguna lleva un JWT de usuario: la
autorización del usuario la decide playback-svc antes y la lleva el SCT del data plane) y un
token bucket por (emisor, subject) de 1000/s, ráfaga 1000 (los harness abren y cierran
sesiones en bucle).styx-ops es la identidad de operación y de los harness (soak, gate, R-02), fuera del keyring
de producción. Un subject sin emisor de servicio hoy sólo lo admite a él (criterio
load-bearing: nada de consumidores "previstos").status = denied) y se audita. Un handler sin policy no compila en
el daemon (el espejo generado trae la policy de cada ruta).status del sobre de respuesta (denied,
invalid_request con {"path","rule"}, rate_limited, too_large, deadline_exceeded,
handler_error, unavailable). No llegan al handler. En Bun, @styx/bus los da como
BUS_<STATUS>; playback-svc los mapea a MediaDaemonErrorCode
(DAEMON_SOCKET_UNREACHABLE, DAEMON_TIMEOUT, DAEMON_REPLY_INVALID,
DAEMON_PROTOCOL_ERROR).status = ok y { error: { code, message, recoverable } } en el payload
(code ^[A-Z0-9_]{1,48}$). Una respuesta trae result o error, nunca los dos.
CAPABILITY_* es un rechazo del binding, no del asset (playback-svc:
DAEMON_BINDING_REJECTED frente a DAEMON_ASSET_REJECTED).cmd.media.createSessionAbre una sesión de playback de un asset. Versión de contrato 2. Petición: rootId (id de una
raíz de STYX_MEDIA_ROOTS, [A-Za-z0-9._-]{1,64}), relPath (1..4095 bytes, relativa a esa raíz)
y, juntos o ninguno, capActor (actorOf(actorId), 32 hex) y capResource
(resourceDigest(asset), 64 hex).
Raíces fd-relativas (dec-0117 I7): el daemon abre cada raíz de
STYX_MEDIA_ROOTS (id=/ruta[:ro][:trusted],…, hasta 16) una vez, como fd de directorio, al
arrancar; la tabla entra entera o no entra (fail-closed). relPath se abre con
openat2(RESOLVE_BENEATH|RESOLVE_NO_SYMLINKS|RESOLVE_NO_MAGICLINKS|RESOLVE_NO_XDEV) bajo ese fd
(recorrido O_NOFOLLOW fuera de Linux) y se exige fichero regular del mismo dev que la raíz.
Un rootId desconocido, .., una ruta absoluta, un symlink en cualquier componente u otro
dispositivo → ASSET_OPEN_FAILED con message path_traversal. Por el IPC nunca viaja una
ruta absoluta: playback-svc traduce el sourceUri de catalog con la misma tabla. La sesión
guarda el fd abierto: sirve el objeto (dev, ino) que abrió aunque después se cambie lo que hay
en esa ruta.
Binding (dec-0117 I2): con capActor/capResource el daemon liga la sesión a ese actor y
recurso antes de responder; sólo un SCT (protocols/capability-token/) con ese sid,
actor y resource la abre. Uno solo → CAPABILITY_BINDING_INVALID sin crear nada. Si el
binding no se puede registrar → CAPABILITY_BIND_FAILED y la sesión se deshace. Sin binding la
sesión existe pero ningún cliente puede abrirla (deny-by-default).
Endpoint según el data plane adjunto:
https://<host>:<wt_port>/styx/<sessionId>, certHash = SHA-256 hex
(sin prefijo) del cert, transportType: "webtransport".https://<host>:<h3_port>/styx/<sessionId>, transportType: "h3".http+unix://<socket>/<sessionId>, sin certHash ni
fileSize, transportType: "http+unix".<host> es STYX_MEDIA_WT_HOST en todos los modos.fileSize: tamaño del asset que abrió el daemon (antes lo calculaba Bun con un stat propio,
una segunda apertura fuera del path guard; fuera).
Error ASSET_OPEN_FAILED (con el nombre del error de apertura en message: path_traversal,
file_not_found, not_regular, …) si el asset no se abre.
cmd.media.openMoqtSessionAbre una sesión MoQT ligada a un actor y a un namespace o pista (dec-0117 I2/I3), sin asset: la
sesión a la que se ata un SCT de publish o de lectura MoQT. capActor y capResource son
obligatorios. Respuesta sess_<32 hex> (CSPRNG). CAPABILITY_BIND_FAILED si no se puede ligar.
cmd.media.closeSession la cierra y revoca sus SCT.
cmd.media.seek{ sessionId, timeUs, requestId }. Registra requestId → sessionId (para que un cancel
posterior llegue en O(1)) y reposiciona la sesión bajo el mutex del manager. found dice si el
daemon conocía la sesión.
cmd.media.cancel{ requestId, sessionId?, packagingId? } (versión de contrato 2): cancelación por request,
no global (r04, r20 §3.6). Alcanza:
sessionId registrada con ese requestId. Un requestId sólo existe
dentro de su sesión (SEC-Z14): el mismo id en otra sesión no se toca, y sin sessionId no se
cancela ninguna lectura de sesión;x-styx-request-id: <requestId> (todos
los que estén en vuelo con ese id, sólo los de packagingId si viene): en cola o
produciéndose responden 503 Cancelled, enviándose se resetean. La sesión sigue sirviendo.cancelled dice si algo se canceló (si la request ya terminó es un no-op).
cmd.media.closeSession{ sessionId }. Orden: revoca primero (desliga el sess_* de la autoridad: los SCT de esa
sesión dejan de valer, también en flujos largos ya abiertos, dec-0117), desengancha la sesión de
WebTransport y HTTP/3 (una petición en vuelo ve "sin wiring", sin UAF) y la cierra.
Idempotente: closed: false si el daemon no la conocía (TKT-009: un segundo cierre responde
dentro del plazo, no cuelga).
qry.media.pingVida y conteo: sessions (abiertas por este socket) y packagingSessions.
cmd.media.openPackaging, cmd.media.closePackaging, qry.media.probe)Motor de medios dual (dec-0110): ver messages/media-engine.md.
qry.media.probe da los facts de un asset por el motor, en proceso: sustituye al ffprobe que
workers-svc lanzaba como proceso (cmd.workers.probe, retirado) y su consumidor es el escaneo
de catalog-svc.
cmd.media.openIngest, cmd.media.pollIngestEvents, cmd.media.closeIngest)Subida de ficheros con el SDK de conduit (dec-0121): ver messages/ingest.md.
| v1 | v2 |
|---|---|
Frame [u32 len][0x01][JSON con "type"] | Frame de spire con sobre firmado (0x02); 0x01 se rechaza |
| Cualquier proceso con acceso al socket mandaba lo que quisiera | SO_PEERCRED + handshake mutuo + firma por mensaje + allowlist por subject + anti-replay |
requestId de correlación en cada mensaje | correlation_id del sobre (el requestId sólo queda donde es de negocio: seek/cancel) |
Frame Error{code,message,recoverable} | status del sobre (transporte/policy/contrato) + error en el payload (negocio) |
Eventos SessionCreated, TransportEndpoint, PipelineReady, Progress, Metrics, Closed | Fuera: nadie los consumía (playback-svc los descartaba); lo útil va en la respuesta |
SubscribeTransportSignals + broadcast por fd a Bun, que los republicaba en JetStream | El daemon publica evt.media.transportSignals en NATS él mismo (spire-zig, su NKey) y realtime-svc los consume |
SubscribeCacheMetrics, MediaEngines | Fuera: sin consumidor (el motor lo elige playback-svc por sesión en openPackaging) |
MediaProbe | Fuera en v2 por no tener consumidor; vuelve como qry.media.probe con catalog-svc como emisor (dec-0110 ME1) |
Close → CloseReply{reason} | closeSession → { closed } |
Ping → Pong | qry.media.ping → conteos |