Protocolossession-ipc (control Bun ↔ daemon)

Session protocol v2 — control Bun ↔ daemon Zig sobre spire (dec-0120)

Spec canónica de protocols/session-ipc/SESSION_PROTOCOL.md, copiada sin reescribir.

ImplementadoSin versión del tren todavía· generada desde protocols/session-ipc/SESSION_PROTOCOL.md

Página generada desde protocols/session-ipc/SESSION_PROTOCOL.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere.

Session protocol v2 — control Bun ↔ daemon Zig sobre spire (dec-0120)

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).

Subjects

SubjectKindEmisores admitidos (BUS_ROUTES)Tope payloadRespuesta
cmd.media.createSessioncmdplayback-svc, styx-ops8 KiB{ result: { sessionId, endpoint, certHash?, transportType, fileSize? } } o { error }
cmd.media.openMoqtSessioncmdstyx-ops1 KiB{ result: { sessionId } } o { error }
cmd.media.openPackagingcmdplayback-svc, styx-ops8 KiBver messages/media-engine.md
cmd.media.closePackagingcmdplayback-svc, styx-ops1 KiB{ closed }
qry.media.probeqrycatalog-svc, styx-ops32 KiBver messages/media-engine.md
cmd.media.seekcmdstyx-ops1 KiB{ found }
cmd.media.cancelcmdstyx-ops1 KiB{ cancelled }
cmd.media.closeSessioncmdstyx-ops1 KiB{ closed }
qry.media.pingqrystyx-ops256 B{ sessions, packagingSessions }
cmd.media.openIngestcmdplayback-svc, styx-ops1 KiBver messages/ingest.md
cmd.media.pollIngestEventscmdplayback-svc, styx-ops32 KiBver messages/ingest.md
cmd.media.closeIngestcmdplayback-svc, styx-ops1 KiB{ closed }
  • Todas las rutas del socket tienen 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").
  • Todo lo demás se deniega (status = denied) y se audita. Un handler sin policy no compila en el daemon (el espejo generado trae la policy de cada ruta).

Dos capas de error

  • Transporte, policy, contrato → 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).
  • Negocio → 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.createSession

Abre 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:

    • WebTransport arriba: https://<host>:<wt_port>/styx/<sessionId>, certHash = SHA-256 hex (sin prefijo) del cert, transportType: "webtransport".
    • Sólo HTTP/3 (m4#08): https://<host>:<h3_port>/styx/<sessionId>, transportType: "h3".
    • Sin data plane (dev/tests): 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.openMoqtSession

Abre 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:

  • la lectura de la sesión 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;
  • los segmentos de empaquetado pedidos por HTTP/3 con 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.ping

Vida y conteo: sessions (abiertas por este socket) y packagingSessions.

Empaquetado y probe (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.

Ingesta (cmd.media.openIngest, cmd.media.pollIngestEvents, cmd.media.closeIngest)

Subida de ficheros con el SDK de conduit (dec-0121): ver messages/ingest.md.

Lo que v2 quitó de v1

v1v2
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 quisieraSO_PEERCRED + handshake mutuo + firma por mensaje + allowlist por subject + anti-replay
requestId de correlación en cada mensajecorrelation_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, ClosedFuera: nadie los consumía (playback-svc los descartaba); lo útil va en la respuesta
SubscribeTransportSignals + broadcast por fd a Bun, que los republicaba en JetStreamEl daemon publica evt.media.transportSignals en NATS él mismo (spire-zig, su NKey) y realtime-svc los consume
SubscribeCacheMetrics, MediaEnginesFuera: sin consumidor (el motor lo elige playback-svc por sesión en openPackaging)
MediaProbeFuera 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 → Pongqry.media.ping → conteos