dec-0123

Vista generada de dec-0123: Release engineering: tren versionado + componentes SemVer, artefactos únicos, manifiesto firmado, canales nightly/stable, runtime adapters y CLI `styx` de instalación y actualización

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0123-release-engineering.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0123-release-engineering.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoLOCKED
Fecha2026-10-01
Ficherodocs/decisions/dec-0123-release-engineering.md

Enmendado o sustituido por: dec-0133

Por qué importa (del frontmatter del ADR):

Fija cómo se versiona, construye, firma, publica, instala y actualiza Styx (track/release-engineering): versión global del tren + SemVer por componente + versiones de cable, binarios por componente como único artefacto (las imágenes OCI se derivan de esos mismos bytes), styx-release.json firmado como única unidad instalable, canales nightly/stable, CLI styx con una capa de runtime adapters (systemd nativo, Podman Quadlet, Docker Compose y más) sin supervisor propio, Postgres por runtime decidido con datos. Sin este ADR, cada executor de la lane release elegiría por su cuenta CalVer o SemVer, Changesets o planner, tags móviles o digests, un runtime único, y el instalador nacería sobre tags :latest que saltan migraciones y el orden de arranque del cable.

Nodos del roadmap que lo citan en refs: track/release-engineering

Páginas de la documentación que lo citan: Ciclo de vida (especificado), Releases (especificado), Comandos del CLI (especificado), Salida JSON y códigos de salida (especificado), Styx (especificado), Actualizar y versiones (especificado), Desplegar la nightly (especificado), Desplegar styx en Coolify (especificado), Instalar (especificado), Visión (especificado), Primer arranque (especificado)

Texto del ADR

Leído de docs/decisions/dec-0123-release-engineering.md, el fichero canónico.

dec-0123 — Release engineering: tren versionado + componentes SemVer, artefactos únicos, manifiesto firmado, canales nightly/stable, runtime adapters y CLI styx de instalación y actualización

  • Fecha: 2026-10-01
  • Estado: LOCKED (2026-10-03). waxin, vía AskUserQuestion, lockea el ADR con sus enmiendas del 2026-10-02: Coolify en tier 1, el tren en 0.0.0 alfa hasta estar listos, canales nightly (builds clean/dirty) y stable, y beta como checkpoint opcional. Entra también la enmienda de dec-0133 a D1 (los alfas son 0.0.0-alpha.N, lockeado el mismo día); la de D5 sigue pendiente de su P5. Las preguntas de §9 que siguen abiertas quedan diferidas de forma explícita, cada una a la lane o al ticket que la necesita. Todo está en Lock (2026-10-03, waxin). Donde el resto del ADR y el lock difieren, gana el lock.
  • Enmienda 2026-10-01 (runtime-agnóstico), antes de cualquier lock, por las decisiones 5 y 6 de waxin de abajo:
    • D4 pasa a artefactos únicos: por componente y arquitectura se construye un binario; la imagen OCI se deriva de ese mismo binario, byte a byte.
    • D6 deja de suponer Docker: el CLI instala sobre el runtime que haya o que elija el usuario.
    • Nuevos D9 (capa de runtime adapters bajo el CLI, sin supervisor propio), D10 (set soportado por plataforma, en tiers), D11 (Postgres por runtime, decidido con datos medidos, incluida la evaluación de PGlite) y D12 (matriz de pruebas: el gate de release corre el mismo e2e en cada runtime soportado).
    • §9 pierde lo que waxin ya respondió y gana las preguntas que abre la enmienda.
    • Los insumos medidos están en docs/track/release-engineering/evidence/research/.
  • Enmienda 2026-10-02 (waxin, vía AskUserQuestion), antes del lock. Lo que sigue quedó decidido ese día y el lock del 2026-10-03 lo mantiene:
    • Coolify sube a tier 1, con plantilla propia: es el despliegue real de waxin en su VPS (D10, D12).
    • El resto de niveles se confirma: tier 1 = systemd nativo, Podman Quadlet, Docker Compose y Coolify; tier 2 = Incus y Kubernetes/Helm; tier 3 sin cambios; macOS y Windows diferidos (§9 Q13, respondida).
    • Postgres: styx-postgres empaquetado en el modo nativo (D11). PGlite sólo para tests, CI y un perfil demo (§9 Q10, respondida).
    • Versionado (punto 13 de las decisiones de waxin del 2026-10-01, .claude/workflows/archive/orquestacion-2026-10/decisiones-pendientes.md en la rama ops/orquestacion). Se aplica en D1 y D5:
      • no se empieza a versionar hasta estar listos, o casi, para las alfas previas al MVP. Antes sería overhead: sólo se deja preparado. Hasta entonces la versión es 0.0.0 en estado alfa, indefinida;
      • las nightly son las builds de desarrollo, marcadas clean o dirty según el working tree, en estado alfa;
      • las releases son normalmente sólo stable; beta opcional, como checkpoint mientras se desarrolla;
      • canales principales: nightly y stable. Un tercero no tiene sentido hasta que el proyecto crezca mucho. Sirven sobre todo para métricas, tests y uso propio.
  • Enmienda de dec-0133 (propuesta el 2026-10-02, LOCKED el 2026-10-03): la entidad release del model. D1: el primer v0.y.z lo corta waxin más adelante; hasta entonces el tren es 0.0.0 en estado alfa y los checkpoints previos son los alfas 0.0.0-alpha.N (alpha.1 = 0.0.0-alpha.1, el MVP); las versiones de componente son marcadores congelados en 0.0.0 hasta ese corte. D5 (pendiente de la P5 de dec-0133): un alpha o beta con channel: stable recorre el pipeline de stable sin mover styx-release:stable. Coolify, tier 1 por la enmienda de arriba, es el primer despliegue real: la aplicación del sitio de docs abre alpha.0 (track/release-engineering#26).
  • Propone: lane release-engineering (rama w7/release-eng). Es la síntesis de tres diseños independientes: UX del usuario final, ingeniería de versiones, y supply chain y canales.
  • Nodo: track/release-engineering. Plan en docs/track/release-engineering/plans/00-plan.md.
  • Decisiones de waxin que cumple (2026-10-01):
    1. Versionado global y por servicio.
    2. Prioridad absoluta: que sea muy fácil de instalar y de actualizar para el usuario final.
    3. Canales nightly (automático desde master) y stable (tag tras gates de release).
    4. Arranca ya, en paralelo a la AppSec.
    5. Runtime totalmente agnóstico: Coolify o Docker Compose para quien los quiera; "Linux first" sobre systemd o lo más nuevo de 2026 que siga siendo load-bearing; OCI en Podman, Incus y Kubernetes; en cada plataforma lo que mejor le encaje. No un supervisor nativo universal propio. El coste de soportar varios no importa: se evalúa con datos (rendimiento, pros y contras) cada runtime. r17 obliga a todos: un contenedor o proceso independiente por Authority, sin modo monolito.
    6. Postgres en modo nativo: lo decide este ADR con datos. waxin pidió evaluar PGlite (WASM, una conexión, in-process) frente al modelo actual de bases separadas por servicio con roles migrator/svc.
  • Cita:
    • r16: frontier salvo blocker concreto.
    • r17: microservicios split-duro, un contenedor por authority.
    • r01: Bun decide, Zig ejecuta.
    • r28: cada capa termina en un consumidor real.
    • dec-0103: zkit, SDKs fijados por hash.
    • dec-0110: motor dual, libav sólo como librería.
    • dec-0117 I8 y dec-0118 §7/§9: contenedores mínimos y mínimo privilegio.
    • dec-0120 / dec-0121: session-ipc v2 sobre spire, conduit wire v1.
    • Deuda d12: toolchain Zig sin release.
    • protocols/schema-versioning/README.md: contratos cerrados.

Contexto

Styx ya es una base usable: seis servicios Elysia 2 con bundle AOT, el daemon Zig, la web TanStack Start, infra endurecida (postgres, valkey, NATS con NKeys, otel) y una AppSec en curso. Lo que falta para que alguien que no sea waxin lo ejecute son estos hechos del árbol (8f2c795):

HechoDónde
Instalar exige checkout, Bun y Zig, más unos 10 pasos a mano. deploy:secrets, build:media-daemon, compose, bus:streams y host-firewall.sh apply hay que repetirlos tras cada arranque. Además: middlewares de Traefik copiados a mano y un network connect al proxy de Coolify.deploy/README.md
No hay registro de imágenes. Los compose de apps usan build:, no image:.deploy/docker-compose.apps.yml
Las versiones no significan nada: todos los apps/* están en 0.0.1, la raíz en 0.0.0 y el daemon en 0.1.0. Ningún binario reporta su versión. Hay un único tag git, checkpoint/f0a1-quality-gate.package.json, native/zig/build.zig.zon
Changesets está instalado pero ignora @styx/catalog-svc, con access: restricted y sin consumidor de changeset version..changeset/config.json
El daemon sólo se compila para x86_64-linux-musl.scripts/build-media-daemon.ts:22
apps/web no tiene Dockerfile ni servicio en compose. apps/cli es un README sin código.docker/, apps/cli/README.md
CI: sólo guard.yml, con permissions: contents: read. check:supply-chain prohíbe cualquier write..github/workflows/guard.yml, scripts/check-supply-chain.ts
Bun: .bun-version y packageManager dicen 1.4.2; el entorno de trabajo usa 1.4.3 (lockfile v2)..bun-version, package.json:76
El único camino de despliegue es Docker Engine + Compose v2. No hay unidades systemd, ni Quadlet, ni chart, ni binarios por servicio: los servicios sólo existen como bundle AOT dentro de una imagen oven/bun.deploy/, docker/*.Dockerfile
Las interfaces versionadas ya existen en el código: frame session-ipc 0x02, version por contrato del bus en BUS_ROUTES, sobre spire v2, SCT v1, conduit wire v1, media-object 0.1, journal drizzle por servicio.protocols/*/version-history.md, packages/api-contracts/src/bus/

Los tres diseños coinciden en lo estructural:

  • un manifiesto firmado que fija todo por digest;
  • SemVer por componente;
  • un CLI que instala y actualiza;
  • build único local y CI;
  • multi-arch amd64 y arm64;
  • canales con promoción.

Divergen en seis puntos: el esquema de la versión global, la herramienta de bump, el nombre y la forma del CLI, cómo se reconstruye para stable, el proxy y la firma. Este ADR elige en cada uno.

Decisión

D1 — Tres ejes de versión; el manifiesto los ata

EjeEsquemaFuente únicaAudiencia
Global (tren)SemVer 0.y.z. 1.0.0 lo declara waxin cuando el contrato de instalación sea estable.tag git v0.y.zusuario final, notas de release, styx update
ComponenteSemVer propio. En 0.x: MINOR = incompatible o con migración; PATCH = compatible.package.json#version (servicios, web, CLI), build.zig.zon + -Dversion (daemon, styx-upload), deploy/VERSIONoperador, styx status, changelog por componente
Cable / esquemaenteros propios, sin cambios respecto a hoysession-ipc frame, version por contrato del bus, sobre spire, SCT, conduit wire, media-object, journal drizzlecompatibilidad entre componentes, la comprueba el CLI
  • Componente = lo que se despliega o se instala por separado:

    • los servicios con código (hoy seis; extensions-svc entra cuando tenga código, por r28);
    • media-daemon, web, styx-upload;
    • deploy: el modelo de despliegue (D9), nats/, seccomp/, initdb, host-firewall.sh y las plantillas de los adapters;
    • styx, el CLI;
    • styx-postgres, el Postgres empaquetado del modo nativo (D11).

    packages/* y protocols/* no son componentes: van dentro de los bundles o son interfaces. El registro único es release/components.yml, con los paths de cada componente y una lista ignore. Lo vigila check:release-map: un path que no pertenece a nada falla.

  • El tren no se deriva aritméticamente de los componentes. v0.4.0 significa "este conjunto de versiones, probado junto".

    • El tren sube MINOR si sube MINOR algún componente, si hay migración, si cambia una interfaz que otro componente requiere o si hay un paso de upgrade.
    • En los demás casos sube PATCH.
    • Regla de producto: un PATCH de tren siempre se puede aplicar sin pensar, incluso con auto-update.
  • Tags git:

    • v0.y.z para el tren, anotado. Un ruleset hace que sólo waxin lo cree y que no se pueda mover ni borrar.
    • <componente>/v<x.y.z> por componente, al estilo de un monorepo Go.
    • El nightly no crea tags: su identidad es el sha.
  • Nightly: <próxima>-nightly.<YYYYMMDD>.<run>+g<sha12>. La fecha la pone CI al construir; no es una fecha de roadmap. En los tags OCI el + pasa a -.

  • Enmienda 2026-10-02 (waxin, decidido):

    • Cuándo empieza el tren. Hasta estar listos, o casi, para las alfas previas al MVP, no hay tren versionado. La versión es 0.0.0 en estado alfa, indefinida, y lo de este D1 queda preparado (registro, metadatos, planner) sin cortar versiones. Los alfas son 0.0.0-alpha.N y el primer v0.y.z lo corta waxin más adelante (lock del 2026-10-03).
    • Nightly = build de desarrollo, en estado alfa, marcada clean o dirty según el working tree del que sale. La marca va en la versión (metadatos de build, junto a g<sha12>) y en /version. El formato exacto lo fija la lane A (tickets 01–02). Un nightly de CI sale de un checkout limpio y siempre es clean; dirty sólo aparece en builds locales.

D2 — El bump por componente se calcula; Changesets se retira

scripts/release/plan.ts decide el nivel de cada componente. Toma el máximo de tres fuentes:

  1. Detectores de interfaz. No se pueden bajar.
    • Cambia la version de un contrato que el componente provee o requiere → MINOR.
    • Migración SQL nueva → MINOR. Si es contract, además un paso de upgrade.
    • Cambio de frame IPC, sobre spire, SCT o conduit wire → MINOR en el productor y en sus consumidores.
    • Variable de entorno o secreto nuevo requerido → MINOR, más una nota de upgrade.
    • Repin de SDK, imagen base o toolchain → al menos PATCH.
  2. Conventional Commits sobre los paths del componente y sus dependencias transitivas. El repo ya los usa.
    • feat, fix, perf, sec, build o refactor → PATCH.
    • ! o el trailer Breaking: → MINOR.
    • docs, test, gov y chore → nada.
  3. Override humano en release/overrides/*.md. Sólo puede subir el nivel.

Lo que decide el componente son los paths tocados, no el scope del commit. Una huella de árbol evita reconstruir lo que no cambió: el sha256 de ls-tree de los paths y las deps, más el toolchain y la base. Si la huella no cambia, el nightly reutiliza el digest anterior. plan.ts --explain <comp> imprime el porqué de cada nivel. Es la evidencia del bump.

Los mismos extractores alimentan dos guards de PR nuevos, que viven en guard.yml:

  • check:interfaces:
    • falla si la huella de un contrato cambia y su version no sube;
    • falla si un requires no lo provee nadie.
  • check:migrations:
    • falla con DROP, RENAME o ALTER TYPE sin la cabecera -- styx:kind=contract;
    • falla si se edita una migración ya publicada.

Changesets se retira: .changeset/, @changesets/cli y los scripts changeset*. Hoy ignora lo que se despliega, no publica nada y pide intención a mano donde hay detectores objetivos. Tiene 0 consumidores reales; DELETE > @deprecated. La prosa de usuario sale de los trailers Release-Note:, Breaking:, Upgrade-Step: y Security:.

D3 — styx-release.json: la única unidad instalable

Lo genera scripts/release/manifest.ts, con --check en CI; nunca se edita a mano. Lleva:

  • manifestVersion, train, channel, source{repo, commit, sourceDateEpoch};
  • components{<comp>: {version, binaries{amd64, arm64}: {sha256, size, url}, image (digest del índice OCI), platforms{amd64, arm64}, treeHash, interfaces{provides, requires}, db{migration, snapshot}, changelog}}. El sha256 del binario es el mismo que el del fichero dentro de la imagen (D4);
  • infra{postgres, valkey, nats, otel-collector}: digest de índice para los runtimes OCI y binario por arquitectura con sha256 para el modo nativo (D11);
  • runtimes{<runtime>: {tier, adapterVersion, minHostVersion}}: qué runtimes soporta este tren y con qué tier lo probó el gate (D10, D12);
  • wire{session-ipc, conduit, sct, spireEnvelope, busRoutesHash};
  • dependencies{zkit, spire, conduit, quic-zig}: commit más hash del .zon;
  • toolchain{bun, zig, zigTarballSha256, zigTarballOci};
  • upgrade{minUpgradeFrom, steps[], order[], reversibleWithoutRestore, minCli};
  • gates{spec, attestation}, sbom[].

Reglas:

  • Ningún despliegue de usuario usa tags móviles. El CLI resuelve canal → manifiesto → digests y sha256. Los ficheros de cada runtime (compose, Quadlet, unidades systemd, chart…) se generan desde el manifiesto y el modelo de despliegue (D9), con image: …@sha256: o con el binario verificado por sha256. Watchtower y el :latest quedan fuera, porque saltarían migraciones y el orden de arranque.
  • Compatibilidad = el conjunto exacto del tren. Los contratos del bus son objetos cerrados (schema-versioning), así que no hay skew N-1 en el bus: emisor y receptor salen en el mismo tren. Sí se garantiza que el código N-1 corre sobre el esquema de BD N (expand/contract, D7), y eso es lo que hace barato el rollback.
  • Se publica como artefacto OCI ghcr.io/mks2508/styx-release:<ver|nightly|stable> vía ORAS, que es la fuente primaria porque el CLI sólo habla con un registro. Los binarios por componente y arquitectura van al mismo registro, también como artefactos ORAS. Hay un espejo en GitHub Releases.
  • Firma:
    • cosign sign-blob --bundle keyless con el OIDC de GitHub Actions.
    • El verificador exige el issuer y la identidad del workflow (release-build.yml@refs/heads/master|refs/tags/v*), además de las extensiones de repo y trigger del certificado. Un fork u otro workflow del repo no pasa.
    • El CLI embebe el trusted root de Sigstore y verifica el bundle offline. Encaja con la red deny-by-default de dec-0117 I8.
  • Anti-rollback:
    • los punteros de canal (stable, nightly) van firmados;
    • el CLI rechaza un tren menor que el instalado salvo --allow-downgrade;
    • consulta una revoked.json firmada antes de instalar o actualizar, porque Sigstore no revoca.

D4 — Build: un punto de entrada, artefactos únicos, multi-arch sin QEMU, reproducible

Artefacto único (enmienda runtime-agnóstico): por componente y arquitectura se construye un binario, y todo lo demás se deriva de esos bytes. La imagen OCI del componente es una base mínima por digest más ese binario, copiado tal cual; el modo nativo instala el mismo fichero. El gate lo comprueba: el sha256 del binario dentro de la imagen es el del manifiesto (mutante: una imagen con otro binario da rojo). Así, lo que se prueba en un runtime es exactamente lo que corre en los demás.

  • Un solo script: bun run release:build [--component X] [--arch amd64,arm64] [--push] (scripts/release/build.ts). Se usa igual en local y en CI, con la lección de test-zig-tsan.sh: una implementación y CI la llama. build:media-daemon pasa a ser un caso de él. En local, sin --push, construye una imagen amd64 de prueba.
  • Servicios Bun y web:
    • El bundle AOT actual (target: 'bun') se compila una vez en la plataforma del runner con bun build --compile --target=bun-linux-{x64,arm64}[-musl], que embebe el runtime Bun fijado. Resultado: un ejecutable por servicio y arquitectura, sin Bun en el host.
    • Si se usa la variante glibc o la musl se decide con los datos de la lane E (tamaño, RSS, arranque y compatibilidad de las dependencias nativas); el ADR no la fija de antemano.
    • El healthcheck lo responde el propio binario (<svc> healthcheck), no wget ni un .js aparte. El test/dockerfile-healthcheck.test.ts se actualiza conservando su motivo.
    • La imagen se construye sin RUN: sólo FROM (base mínima por digest de índice multi-arch), COPY del binario de su TARGETARCH, ENV y USER. buildx produce amd64 y arm64 sin emular, y el build es reproducible: SOURCE_DATE_EPOCH = fecha del commit, rewrite-timestamp=true y frontend de Dockerfile por digest.
    • web es el servidor SSR de TanStack Start compilado igual, no vite preview. Gana un docker/web.Dockerfile derivado de su binario y entra en el modelo de despliegue con el mismo endurecimiento.
    • Los binarios Bun no admiten MemoryDenyWriteExecute (el JIT de JSC escribe código). Los adapters lo aplican sólo al daemon y lo documentan como excepción, no como degradación silenciosa.
  • Daemon Zig:
    • Se cross-compila desde x86_64 con el toolchain ya fijado por sha256 y minisign: ReleaseSafe, {x86_64,aarch64}-linux-musl, -Dcpu=baseline (obligatorio para Pi 4/5 y Ampere), -Dversion y -Dcommit.
    • Build gemelo: dos builds con cachés limpias deben dar el mismo sha256, o el release se para.
    • La suite arm64 corre bajo qemu-aarch64 user-mode. El smoke nativo arm64 es gate de stable.
    • La imagen sale sin RUN en plataforma destino, con TARGETARCH, y lleva el mismo binario estático que instala el modo nativo.
  • d12: el tarball del toolchain Zig verificado se archiva en ghcr.io/mks2508/styx-toolchain-zig (ORAS, retención infinita) y setup-zig-dev lo usa como último mirror. Un stable sigue siendo reconstruible aunque ziglang.org retire el dev build. Esto no resuelve d12; lo hace inofensivo para release.
  • Versión dentro del artefacto:
    • servicios: GET /version (policy: 'public', sólo componente, versión, tren y commit), version en /health, y service.version y styx.train en OTel;
    • daemon y styx-upload: --version e --interfaces, y metadata en el handshake spire, para que playback-svc falle explícito ante un daemon incompatible;
    • labels OCI org.opencontainers.image.* más dev.styx.{component,train,interfaces}.
  • Supply chain por imagen:
    • SBOM CycloneDX de tres fuentes: BuildKit con BUILDKIT_SBOM_SCAN_STAGE, metafile de Rolldown cruzado con bun.lock (lo que de verdad está en el bundle) y build.zig.zon, que syft no entiende;
    • provenance de BuildKit mode=max y una provenance propia del daemon;
    • firma cosign keyless recursiva sobre el digest del índice;
    • gate de licencias sin GPL. FFmpeg LGPL sólo como librería dinámica (ver D8);
    • escaneo de vulnerabilidades con grype sobre el SBOM: informativo en nightly, rojo en stable para High/Critical con fix, salvo VEX revisado.

D5 — Canales: nightly automático, stable promovido por digest

  • Nightly:
    • Se dispara con schedule diario más workflow_dispatch. Se salta si master no cambió y exige guard.yml verde en ese sha.
    • Llama a release-build.yml (reutilizable). Es el único workflow que empuja y firma.
    • Corre los gates nightly contra los artefactos publicados (digests y sha256), instalados con el CLI en cada runtime de tier 1 (D12): e2e gate, styx verify (check:infra-live empaquetado), ingesta e2e, web VOD, suite arm64 en qemu, SBOM completo, licencias y verificación de firma de ida y vuelta. Los de tier 2, render + validate + smoke.
    • En verde mueve :nightly y styx-release:nightly. En rojo no mueve nada y abre un issue rodante.
  • Stable:
    • Se dispara con un tag v* que crea waxin sobre un sha con un candidato construido por release-build.yml, normalmente el nightly de ese sha.
    • No se recompila para publicar: un gate de reproducibilidad reconstruye en limpio y exige los mismos digests.
    • Gates de stable (release/gates.yml):
      • los de nightly;
      • test:zig:tsan con canario y audit:safety;
      • instalación limpia con el CLI en cada runtime de tier 1;
      • upgrade desde el stable anterior con datos sembrados, y rollback con fallo inyectado, también en cada runtime de tier 1;
      • smoke de cada runtime de tier 2;
      • smoke arm64 nativo;
      • vulnerabilidades;
      • nodos del model marcados bloqueantes, que requiere el campo releaseBlocking (ver §9).
    • Un gate no ejecutado cuenta como rojo. Cada gate tiene su mutante.
    • Después viene la aprobación en el environment release, con waxin como revisor, y la promoción por digest con crane tag (nunca imagetools create, que deja la firma huérfana). Por último se firman el manifiesto stable, la GitHub Release y styx-release:stable.
  • Enmienda 2026-10-02 (waxin, decidido): los canales principales son nightly y stable. Una release es normalmente sólo stable. Una beta es opcional: un checkpoint durante el desarrollo que se corta con el mismo pipeline de stable (mismos gates, sin mover styx-release:stable). No hay un canal beta permanente hasta que el proyecto crezca mucho.
  • PR de release: scripts/release/version.ts aplica el plan (versiones, CHANGELOG por componente, notas del tren) en un PR release/v0.y.z. Es el único punto de revisión humana del contenido. Mergear el PR y poner el tag corta el stable.
  • Hotfix: rama release/0.y desde el tag, con cherry-pick y el mismo pipeline. Sólo admite PATCH: un detector que pida MINOR bloquea el hotfix.
  • Retención:
    • stable para siempre;
    • los últimos 30 nightly más los referenciados por un stable;
    • sha-* sin nightly verde, 7 días.
    • La limpieza tiene en cuenta índices multi-arch y referrers de cosign; las acciones genéricas de "borrar untagged" están prohibidas.
  • Permisos CI:
    • guard.yml sigue en contents: read;
    • sólo pueden escribir los jobs de release-build.yml, nightly.yml, release.yml y retention.yml que declaren environment:.
    • check:supply-chain se enmienda con esa allowlist cerrada y con las reglas runner-no-run, base-is-index, buildkit-pinned y release-tools-pinned, cada una con su mutante.

D6 — Instalar y actualizar: un comando y un CLI styx

El usuario final necesita Linux amd64 o arm64 con uno de los runtimes de D10: systemd (modo nativo, sin contenedores), Podman con Quadlet o Docker Engine con Compose v2; o un Incus, un clúster Kubernetes o un Coolify que ya tenga. Nada más: ni checkout, ni Bun, ni Zig.

curl -fsSL https://<base>/install.sh | sh                                # stable, runtime detectado
curl -fsSL https://<base>/install.sh | sh -s -- --channel nightly
curl -fsSL https://<base>/install.sh | sh -s -- --runtime podman         # runtime forzado
  • install.sh es corto, POSIX, auditable y versionado en el repo.
    • Descarga styx-linux-<arch>, SHA256SUMS y su bundle de firma.
    • Verifica con sha256sum -c (la misma regla que check:supply-chain aplica a CI) y la firma con cosign si está presente.
    • Sin verificación válida aborta: no hay --insecure.
    • La documentación da la vía equivalente en tres comandos, sin pipe a shell.
  • CLI styx en apps/cli, que hoy es un TARGET sin código. Se compila con bun build --compile para bun-linux-{x64,arm64}, va firmado y también se publica como imagen OCI para los runtimes que lanzan tareas en contenedor (Coolify, Kubernetes; D6.7). No es un supervisor: no queda residente y no vigila procesos. Habla con cada runtime por su adapter (D9), que usa la API nativa (socket de Docker o de Podman, D-Bus de systemd, API de Incus, API de Kubernetes), no el parseo de un CLI. Reutiliza el TS ya probado moviéndolo a paquetes: gen-deploy-secrets, bus-streams, check-infra-live y check-deploy.
    1. styx install: asistente idempotente; dos ejecuciones dan un no-op con informe.

      • Preflight: runtimes disponibles y sus versiones (detect() de cada adapter), cgroups v2, puertos 80/443 y UDP 4433-4435, disco, reloj, arch, GPU visible y, en modo nativo, un Postgres del sistema utilizable (D11).
      • Elige el runtime: el que pida --runtime; si no, el primero disponible en el orden por defecto de la plataforma (D10), que fija la evidencia de la lane E y no esta prosa.
      • Preguntas mínimas con defaults: dominio, email ACME, raíces de media :ro, OIDC y canal.
      • Layout común: /opt/styx/releases/<tren>/ más el symlink current, /etc/styx/styx.toml, /etc/styx/secrets/ (0400, dueño por Authority) y /var/lib/styx/{backups,state.json}. Cada adapter añade lo suyo (unidades, Quadlets, compose, chart) bajo releases/<tren>/<runtime>/.
      • Secretos generados por el CLI, entregados a cada Authority como fichero por el mecanismo nativo del runtime (secrets: de compose, Secret= de Quadlet, credentials de systemd, Secret montado en Kubernetes).
      • Arranque ordenado: infra → bus:streams → *-migrate → servicios → proxy, expresado con las dependencias del runtime (Requires=/After= y Type=oneshot en systemd y Quadlet, depends_on: condition en compose, Job previo en Kubernetes).
      • El firewall deja de ser un paso "tras cada arranque": una unidad styx-firewall.service donde hay systemd, y en modo nativo además IPAddressDeny=/IPAddressAllow= por unidad.
      • Cierra con styx verify, que es check:infra-live empaquetado y parametrizado por runtime. Si falla, la instalación no se da por buena.
    2. styx update es una transacción:

      1. verifica el puntero y el manifiesto;
      2. calcula la ruta (escalones si hace falta por minUpgradeFrom);
      3. muestra las notas "Antes de actualizar";
      4. hace pre-pull por digest; el sistema sigue sirviendo y, si falla aquí, nada cambió;
      5. hace backup automático;
      6. corre bus:streams y *-migrate;
      7. intercambia current y recrea sólo lo que cambia, en el upgrade.order del manifiesto (activate() del adapter);
      8. pasa el health gate: healthchecks, /version igual al manifiesto, handshake spire con el daemon y styx verify;
      9. rollback automático si algo falla.

      state.json hace la transacción reanudable tras un corte. --dry-run imprime el plan.

    3. styx rollback tiene dos formas:

      • rápido (swap del symlink y activate() de la release anterior) si el tren tenía migraciones sólo expand (reversibleWithoutRestore);
      • con restore del backup previo en caso contrario, avisando de la pérdida de lo escrito después del backup.
    4. styx backup create|list|restore|prune:

      • pg_dump -Fc por base con el rol migrador, no el superusuario (encaja con dec-0118 §7);
      • config y manifiesto;
      • secretos cifrados con age a una clave de recuperación que se muestra una vez en el install;
      • Valkey no se incluye: es estado efímero.
    5. styx status (con --json): tren, canal, versión y digest por componente, salud, update disponible y último backup.

    6. styx doctor, styx logs, styx config, styx secrets rotate, styx channel, styx self-update y styx uninstall. Todo lo destructivo lleva --dry-run y confirmación (apps/cli/README.md).

    7. styx render --runtime <r> produce los ficheros de un runtime sin instalar nada, para quien quiera aplicarlos con sus propias herramientas: compose por digest para Coolify (Raw Compose Deployment, DS8-01) o un NAS, chart de Helm, módulo de NixOS. Las tareas one-shot de esos runtimes se lanzan con la imagen del CLI (ghcr.io/mks2508/styx-cli@sha256:… <tarea>).

  • Auto-update es opt-in: un timer nativo del host (timer systemd donde lo hay) en la ventana que elija el usuario; donde el runtime no tiene timer (Coolify, Kubernetes) sólo existe el modo notify.
    • Modos notify (default), patch (sólo PATCH de tren con reversibleWithoutRestore) y minor.
    • Nunca salta minUpgradeFrom, nunca cambia de canal y nunca actualiza con reproducción activa.

D7 — Migraciones expand/contract

Entre trenes stable, una migración que borra o renombra se parte en dos: expand en el tren N y contract en N+1 o después. Cada .sql lleva la cabecera -- styx:kind=expand|contract|data. El planner deriva reversibleWithoutRestore y los pasos de upgrade, y check:migrations lo enforcea. Los *-migrate llevan lock_timeout, statement_timeout y advisory lock por base. Se mantiene lo que ya es bueno: DDL sólo con <svc>_migrator, runtime sin DDL, one-shot antes del runtime.

D8 — libav y licencias

El primer stable sale con engine=zig: binario estático musl, stub libav, sin código LGPL enlazado. libav llega como variante de imagen media-daemon:<v>-libav, con FFmpeg 8 dinámico (--enable-shared, sin gpl ni nonfree, lo que ya asserta build-ffmpeg.sh), las .so en la imagen junto a LICENSE.LGPL y la URL y el sha del tarball fuente (LGPL §6). El manifiesto marca engine. Respeta dec-0110: el motor dual sigue configurable; sólo cambia qué variante se distribuye por defecto.

D9 — Runtime adapters bajo el CLI, sin supervisor propio

Styx no trae un proceso que supervise procesos. El CLI renderiza y delega: genera los ficheros nativos de cada runtime y le deja a él el ciclo de vida (systemd, Podman, dockerd, incusd, kubelet). Esto cumple la decisión 5 de waxin y r17: cada runtime expresa una unidad por Authority con sus propias herramientas.

  • Modelo de despliegue (deploy/model/, nuevo, versionado con el componente deploy): describe cada Authority una sola vez, sin decir cómo la ejecuta cada runtime:

    • binario o imagen, uid y gid, puertos y protocolos (TCP, UDP público para QUIC);
    • redes lógicas (internal, edge, public-udp) y el socket de control del daemon con su grupo;
    • secretos (siempre como fichero), volúmenes y raíces de media :ro;
    • dispositivos (gpu: none|optional|required), límites de memoria, CPU y pids;
    • one-shots (*-migrate, bus:streams) con su orden, healthcheck y upgrade.order.

    El compose de deploy/ pasa a ser un render de este modelo; check:deploy valida el render y un guard nuevo (check:deploy-render) falla si el compose versionado difiere de lo que genera el modelo.

  • Interfaz RuntimeAdapter (TS en apps/cli, un módulo por runtime):

    OperaciónContrato
    detect()¿está el runtime, en qué versión, con cgroups v2, qué GPU ve?
    render()modelo + manifiesto + config del host → ficheros nativos. Puro y determinista; se prueba con snapshots.
    validate()comprueba los invariantes sobre lo renderizado (generaliza check:deploy) con la herramienta del runtime si existe.
    stage(release)descarga y verifica (sha256 y firma) sin tocar lo que corre.
    runOneShot(name)migraciones y bus:streams, en orden, como tarea del runtime.
    activate()swap atómico y recreación sólo de lo que cambió, en upgrade.order.
    health()readiness por Authority y /version igual al manifiesto.
    rollback()vuelve a la release anterior con el mecanismo del runtime.
    metrics()RAM y CPU por Authority leídas de su cgroup; alimenta la lane E.
  • Común a todos, por encima del adapter: backup y restore, generación y rotación de secretos, bus:streams, styx verify y la transacción reanudable de styx update (state.json, D6.2). La lógica de update no se duplica por runtime.

  • Invariantes que todo adapter debe expresar (los mismos que hoy cumple el compose):

    • I-a, r17: una unidad (contenedor o proceso) por Authority, sin agrupar;
    • I-b, dec-0117 I8: uid fijo, rootfs de sólo lectura, sin capabilities, no-new-privileges, seccomp o filtro de syscalls, topes de pids, memoria y CPU;
    • I-c, dec-0118 §7: secretos sólo como fichero; en prod una variable en claro no arranca;
    • I-d, DS-02: base por servicio, roles <svc>_migrator y <svc>_svc, *-migrate antes del servicio;
    • I-e, DS3-01/02: red interna aislada, sólo lo enrutado toca el edge, socket del daemon 0660 con grupo compartido sólo con playback y catalog;
    • I-f: el daemon publica UDP 4433-4435 y accede a /dev/dri o NVENC sin relajar el resto.

    Un adapter que no pueda expresar un invariante falla en validate() con el paso que falta, nunca degrada en silencio. Ejemplo: si la plantilla one-click de Coolify sólo entrega secretos como variables de entorno (la encuesta no lo pudo confirmar; lo verifica el adapter), que resolveSecretEnv rechaza en prod; el adapter añade un one-shot styx-cli init que materializa los secretos como ficheros en un volumen, y si no puede, se niega.

  • Mecanismo de update y rollback por runtime:

    • systemd nativo: systemd-sysupdate con imágenes sysext/confext firmadas (dm-verity) cuando el host lo soporta, que mantiene N versiones y vuelve a la anterior sin descargar; en hosts sin sysupdate, el mismo layout releases/<tren>/ + symlink current + daemon-reload. La elección la hace detect(), no el usuario.
    • Podman Quadlet: el CLI reescribe Image=…@sha256: y hace daemon-reload; el podman auto-update de Podman no se usa para elegir versión (saltaría el manifiesto), sólo su rollback por unidad si el CLI lo delega.
    • Compose: symlink current + docker compose up -d por digest.
    • Incus: una instancia OCI por Authority (un contenedor de sistema con todas dentro no cumple r17); snapshot de cada instancia antes de activar y rollback = restaurar el snapshot.
    • Kubernetes: helm upgrade --atomic con el Job *-migrate como hook previo.

D10 — Set soportado por plataforma

PlataformaTier 1: e2e completo en cada release (D12)Tier 2: render + validate + smokeTier 3: render + validate estático, documentado
Linux servidor u homelab, amd64/arm64systemd nativo (unidades endurecidas + credentials + sysupdate/sysext) · Podman Quadlet · Docker ComposeIncus (OCI) · Kubernetes (Helm, k3s)módulo de NixOS
NAS——TrueNAS (Custom App YAML), Unraid (Compose Manager), Synology (Container Manager): todos consumen el compose renderizado
PaaSCoolify (plantilla propia; enmienda 2026-10-02)——
macOSdiferidodiferidodiferido: launchd nativo por Authority (la GPU no llega a una VM Linux)
Windowsdiferidodiferidodiferido: WSL2 + Compose
  • Por qué tres en tier 1. Cubren los tres modelos de host Linux que existen en 2026: sin contenedores (systemd nativo, lo más nuevo que sigue siendo load-bearing: sysupdate, sysext, credentials cifrables con TPM2, firewall por unidad), contenedores sin daemon integrados en systemd (Quadlet) y el ecosistema Docker, del que salen Coolify y los NAS. waxin pidió soportar varios y medirlos; el coste no es criterio.
  • Enmienda 2026-10-02 (waxin, decidido): Coolify sube a tier 1, con plantilla propia. Es el despliegue real de waxin en su VPS (donde ya viven Jellyfin, Coolify y el Storage Box), así que su e2e completo entra en D12 como el de los otros tres. Por qué una plantilla propia y no sólo "Raw Compose": Coolify es la primera instalación de verdad, y el adapter (D9) tiene que expresar los invariantes I-a…I-f allí también, incluidos los secretos como fichero (one-shot styx-cli init o validate() se niega). Si la plantilla one-click pública sigue esperando a imágenes públicas es §9 Q1; la plantilla propia del tier 1 no depende de eso. El resto de niveles se confirma: tier 2 = Incus y Kubernetes/Helm; macOS y Windows, diferidos.
  • El orden por defecto de styx install cuando hay varios runtimes no lo fija este texto: lo fija la evidencia de la lane E (RE12). Hasta entonces el orden provisional es systemd nativo → Quadlet → Compose, el de la encuesta cualitativa.
  • Tier 2 y 3 no son de segunda: comparten artefactos y modelo; lo que cambia es cuánto prueba el gate en cada release. Un runtime sube de tier cuando su e2e entra en D12.
  • arm64: todos los Linux de tier 1 y 2. armv7/32-bit queda fuera (no hay Bun armv7).

D11 — Postgres por runtime, decidido con datos

Medido el 2026-10-01 con el código real de catalog, sources e identity (informe y salidas en docs/track/release-engineering/evidence/research/): PGlite 0.5.8 (motor PostgreSQL 18.3 en WASM), in-process y con pglite-socket 0.2.11, frente a un PG 18.4 relocatable, un PG17 en OCI y el PG16 del sistema. Una ejecución por backend con ±20 % de ruido: los números orientan; la lane E los repite con N ≥ 5.

MedidaPGlite in-processpglite-socketPG18 relocatable (nativo)PG17 OCI
Roles migrator/svc y audit append-only en el motor (I-d)no: superusuario, SET ROLE reversible por el propio procesono: ignora usuario, contraseña y basesí, deniega todo lo que debesí
Concurrencia con el pool de bun:sqluna conexiónsólo max:1 + prepare:false; con max:5, SQLSTATE 26000 y cuelguessísí
Durabilidad ante corte de luzno: 0 fsync, ni forzadonosí (fsync=on)sí
Throughput mixto, 16 workers616–742 ops/s689 ops/s2722 ops/s2104 ops/s
RSS en reposo375–420 MB por servicio290–360 MB por instancia≈133 MB el clúster con las 3 bases127 MiB
Otrospico de ≈1,1 GB en initdb; backup sólo en formato INSERTun COPY … TO PROGRAM deja la instancia colgada hasta reiniciarinitdb 0,7 s, arranque 113 ms—

Decisión:

  • PGlite no es el Postgres de ningún runtime soportado. Rompe I-d en el motor, no tiene durabilidad frente a corte de luz, serializa cada servicio en una conexión y, para los tres servicios, ocupa unas 8 a 10 veces más RAM en reposo que un clúster nativo con las tres bases. pglite-socket, además, rompe el aislamiento entre conexiones del pool. Queda para tests y CI efímeros (driver drizzle-orm/pglite in-process, en los tests que no prueban roles) y para un perfil demo sin datos reales, con aviso explícito de que no hay durabilidad ni aislamiento de roles (§9 Q10, respondida por waxin el 2026-10-02: sí al demo).
  • Modo nativo (systemd): styx-postgres, un componente propio: PostgreSQL 18.x compilado por el pipeline de release desde el tarball firmado de postgresql.org, con OpenSSL 3, ICU actual, --with-systemd y rutas relativas $ORIGIN. No se distribuye el relocatable de npm medido: lleva OpenSSL 1.1.1 (fuera de soporte) e ICU 60.
    • Unidad propia endurecida (Type=notify, ProtectSystem=strict, StateDirectory=), sólo socket Unix (listen_addresses=''), scram-sha-256 con la contraseña como credential de systemd.
    • Un clúster con las tres bases y el mismo 10-styx-roles.sh; styx-<svc>-migrate (Type=oneshot) corre antes de styx-<svc> por Requires=/After=.
    • Actualización mayor con pg_upgrade --link y los binarios viejo y nuevo lado a lado; el A/B de sysupdate da la vuelta atrás.
    • Alternativa: --postgres=system si el preflight detecta un Postgres del sistema ≥ 16; el CLI crea bases y roles con el mismo script. No es el default: la versión varía por distro, su actualización mayor no la controla styx y el clúster es compartido.
  • Runtimes OCI (Quadlet, Compose, Coolify, Incus, Kubernetes): la imagen oficial postgres por digest de índice, como hoy. En Kubernetes, un StatefulSet con la misma imagen; quien ya opere CloudNativePG puede apuntar a su Cluster. Si el OCI debe usar también la imagen de styx-postgres para tener un solo Postgres en todos los runtimes es la §9 Q11.
  • Infra nativa restante (NATS, Valkey, OTel Collector): binarios por arquitectura con sha256 en el manifiesto, tomados de las releases firmadas de upstream cuando existen y compilados por el pipeline cuando no; cada uno con su unidad endurecida y escuchando sólo en socket Unix o en loopback. Qué se compila y qué se toma de upstream lo fija la lane B con licencias (sin GPL) y SBOM.
  • Backup igual en todos los runtimes: pg_dump -Fc por base con el rol migrador (D6.4).

D12 — Matriz de pruebas: el mismo e2e en cada runtime soportado

  • Un solo e2e, independiente del runtime: instala con styx install --runtime <r> desde los artefactos publicados y sólo habla con la instalación por el CLI y por los endpoints públicos. El harness recibe --runtime y nada más cambia. Es la regla de "un artefacto probado": lo que se instala en la prueba es lo que instala el usuario.

  • Suite por runtime:

    • instalación limpia → styx verify verde;
    • e2e gate, ingesta e2e y web VOD;
    • update N-1 → N con datos sembrados y datos intactos;
    • rollback con fallo inyectado en el health gate;
    • backup y restore;
    • reboot del host y todas las Authority de vuelta sin pasos manuales;
    • test de invariantes de D9 sobre lo instalado de verdad (no sólo sobre el render): una unidad por Authority, hardening efectivo, secretos como fichero.
  • Matriz:

    RuntimePR (guard.yml)NightlyStable
    systemd nativo, amd64render + validate (snapshots)suite completasuite completa + upgrade desde el stable anterior
    Podman Quadlet, amd64render + validatesuite completaídem
    Docker Compose, amd64render + validatesuite completaídem
    tier 1, arm64—suite arm64 del daemon en qemuinstall + verify + smoke en arm64 nativo (§9 Q8)
    Coolify, amd64render + validatesuite completasuite completa + upgrade desde el stable anterior
    Incus, k3s/Helmrender + validateinstall + verify + smokeídem
    NixOS, NASrender + validate estático—render + validate
  • Un runtime soportado sin su fila verde es un gate rojo, como cualquier gate no ejecutado (D5). Bajar un runtime de tier exige cambiar este ADR, no saltarse la fila.

  • Cada invariante de D9 tiene su mutante: un render con dos Authority en una unidad, un secreto como variable de entorno o un contenedor sin cap_drop deben dar rojo en validate() y en el test sobre lo instalado.

  • Benchmark (lane E): con el mismo host y la misma release, mide por runtime la RAM en reposo (suma de memory.current de cada cgroup más el propio runtime), los tiempos de instalación, update y rollback (con la ventana sin servicio medida por sondas HTTP y QUIC), el data plane con y sin /dev/dri, la superficie (systemd-analyze security, capabilities efectivas, syscalls permitidas), el arranque en frío tras reboot y el disco. Y por backend de BD, lo de D11 con N ≥ 5, mediana e intervalo. Los resultados son evidencia del nodo y fijan el orden por defecto de D10.

Descartado (y por qué)

OpciónDiseñoMotivo del descarte
CalVer YYYY.M.PATCH para el tren, estilo Home Assistant1Comunica antigüedad y presupone cadencia. Styx no tiene cadencia y la regla de casa prohíbe inventar fechas. SemVer 0.x dice lo que importa al que actualiza: si puede hacerlo sin pensar (PATCH) o si tiene que leer las notas (MINOR). Queda como alternativa si waxin lo prefiere (§9 Q2); el resto del diseño no cambia.
Una sola versión copiada en todos los paquetes—Pierde qué cambió de verdad, reconstruye y republica todo en cada release y hace inútil la huella de árbol.
Changesets como fuente del bump, quitando el ignore y añadiendo un package.json falso al daemon1, 3Pide intención manual donde hay detectores objetivos, necesita un paquete npm ficticio para versionar Zig y no ve los cambios de interfaz. Los detectores + Conventional Commits + overrides que sólo suben cubren lo mismo con evidencia (--explain).
release-please2 (mencionado)No conoce el grafo de workspace de Bun, ni Zig, ni los detectores. El patrón de PR de release sí se adopta.
Tags móviles (:stable, :latest) en el compose de usuario o Watchtower—Saltan migraciones y el orden de arranque del cable. Los tags móviles sólo sirven para descubrir.
Stable recompilado desde el tag—Rompe "se publican los bytes probados". Se reconstruye sólo como gate de reproducibilidad.
Build arm64 con QEMU—Es lento, frágil con Bun y no reproducible (apk add en destino). El runner sin RUN y el cross-compile de Zig lo hacen innecesario. QEMU queda sólo para la suite de tests arm64.
Runners arm64 nativos para compilar1Son gratis sólo en repos públicos y no hacen falta para compilar. Se reservan para el smoke de stable.
imagetools create para promocionar—Envuelve el manifest en un índice nuevo, cambia el digest y deja la firma huérfana. Se usa crane tag / regctl.
minisign con clave custodiada como firma primaria1 (doble firma)Exige custodia y rotación manual de una clave. cosign keyless liga el artefacto al workflow exacto sin clave que robar. minisign queda como plan B si Rekor público no es aceptable con el repo privado (§9 Q3).
Imagen styx-deploy aparte con las herramientas de host3El CLI compilado ya las lleva dentro. Para Coolify se publica el mismo CLI como imagen: un artefacto menos que versionar y firmar.
Nombre styxctl en tools/2apps/cli ya existe como TARGET con el nombre styx y sus reglas (dry-run, salida estable). Un segundo CLI duplicaría ownership.
CLI en Zig sobre zkit desde el principio (frontier r16)1El blocker es concreto y material: reescribir la generación de secretos (NKeys, SCT keys, proof headers, dueños por uid) y check:infra-live, que ya están probados en TS, retrasaría el primer stable sin ganancia para el usuario. Queda como candidato para una segunda iteración, con el binario Bun como vara de medir (tamaño y arranque).
Auto-update activado por defecto, estilo Coolify1Hay BD de usuario y el rollback de una migración contract exige restore. El default es notificar; PATCH automático sólo si se activa.
Skew N-1 en el bus (handlers que sirven v_n y v_n-1)2schema-versioning decide contratos cerrados y despliegue conjunto. Hacer skew exigiría doble versión en cada handler. Fuera de alcance: el tren es la unidad atómica. La ventana de inconsistencia durante el recreate secuencial queda en segundos y está documentada.
Publicar packages/* en npm—Son internos (private). Si algún día se publica plugin-sdk o source-sdk, se promueve a componente con su tag.
Un supervisor nativo universal propio (un styxd que arranque y vigile todas las Authority)enmiendaLo descarta waxin (2026-10-01). Además duplicaría lo que ya hacen bien systemd, Podman, dockerd o kubelet (reinicios, cgroups, logs, dependencias) y añadiría un proceso privilegiado más que auditar. El CLI renderiza y delega (D9).
Un solo runtime soportado (sólo Docker Compose, el de hoy)enmiendaContradice la decisión de waxin de un runtime totalmente agnóstico. Deja fuera a los hosts sin Docker (Fedora/RHEL con Podman, instalaciones sin contenedores) y no permite comparar con datos.
Imágenes OCI construidas aparte de los binarios nativos (bundle sobre oven/bun en la imagen, binario compilado para nativo)enmiendaSerían dos artefactos distintos por componente: lo probado en un runtime no sería lo que corre en otro. Con un binario y la imagen derivada de él, el sha256 es el mismo en todas partes (D4).
PGlite como Postgres por defecto del modo nativoenmiendaMedido (D11): roles en disciplina de código y no en el motor (rompe DS-02 y el audit append-only), sin fsync, una conexión por servicio, entre 1/4 y 1/3 del throughput y unas 8 a 10 veces más RAM para los tres servicios. Queda para tests, CI y, si se quiere, demo.
pglite-socket como Postgres por servicioenmiendaMedido (D11): ignora usuario, contraseña y base; con el pool de bun:sql rompe el aislamiento entre conexiones, da SQLSTATE 26000 o se cuelga; un COPY … TO PROGRAM lo deja sin servicio hasta reiniciar. No es un Postgres para datos reales.
Distribuir el Postgres relocatable de npm (@embedded-postgres)enmiendaEs el más rápido medido, pero lleva OpenSSL 1.1.1 (fuera de soporte) e ICU 60 y su versión es beta. Se compila styx-postgres desde las fuentes (D11).
Postgres del sistema como default del modo nativoenmiendaLa versión cambia por distro, su actualización mayor la decide la distro (rompe el A/B y el rollback de D6) y el clúster es compartido con otras apps. Queda como --postgres=system.
Distribuir la imagen de SO del appliance1track/appliance-pi3 es un cliente. La imagen de SO es de F-APPLIANCE-OS (r59). Este ADR cubre el servidor.

Consecuencias

  • Instalar pasa de unos 10 pasos con checkout, Bun y Zig a un comando más un asistente, en el runtime que tenga el usuario. Actualizar pasa a un comando con backup y rollback automáticos, la misma transacción en todos los runtimes.
  • Cada release se prueba entera en cuatro runtimes (D12; Coolify entra en tier 1 por la enmienda del 2026-10-02) y con smoke en dos más: el coste de CI crece, y waxin lo acepta de forma explícita. A cambio, el default por plataforma se elige con datos medidos y no con preferencias.
  • Aparece deploy/model/ como fuente del despliegue. El compose de deploy/ pasa a ser un render guardado por check:deploy-render.
  • Aparece un componente nuevo que mantener, styx-postgres (build propio de PostgreSQL, con su propio seguimiento de CVE). Es el precio de tener una versión única probada en el modo nativo.
  • Los servicios pasan de bundle sobre oven/bun a binario compilado con el runtime Bun embebido; su tamaño y su RSS los mide la lane E antes de fijar la variante glibc o musl.
  • Aparecen dos guards de PR que hoy dependen de disciplina: check:interfaces y check:migrations. También check:release-map, y check:supply-chain se amplía.
  • deploy/docker-compose.apps.yml pasa a image: ${STYX_IMG_*:?}, generado desde deploy/model/. El build: se mueve a un override de dev/CI (docker-compose.apps.build.yml), con lo que el flujo de dev cambia en una línea.
  • Toca ficheros que la AppSec también toca: docker/, deploy/ y check-supply-chain.ts. Las lanes B y C esperan a que la AppSec integre sus pasadas sobre esos ficheros, o se coordinan por worktree (plan §4). Las lanes A y E son aditivas y pueden avanzar antes del lock.
  • Se crean release/ en la raíz (registro de componentes, gates, overrides, notas, vex) y scripts/release/.
  • Un stable no se publica con hallazgos P0/P1 abiertos de la AppSec. Se mecaniza con releaseBlocking en cuanto waxin apruebe el campo (§9 Q6).

§9 — Preguntas abiertas para waxin (diferidas por el lock del 2026-10-03)

Respondidas por waxin el 2026-10-01 y fuera de esta lista: versionado global y por servicio (D1), prioridad de instalar y actualizar fácil (D6), canales nightly y stable (D5), runtime agnóstico sin supervisor propio y con varios runtimes medidos (D9, D10, D12; eso responde también si el tier 1 lleva dos o tres runtimes: lleva tres), y que el Postgres del modo nativo lo decida el ADR con datos (D11). Las de abajo siguen abiertas; Q4 y Q8 se reformulan por la enmienda y Q10–Q13 son nuevas.

  1. Q1 — Visibilidad. ¿Repo o paquetes GHCR públicos?
    • Sin artefactos públicos no hay instalación anónima, en ningún runtime.
    • Además decide los runners arm64 gratis, las GitHub attestations, si Rekor público (registra el nombre del repo y del workflow) es aceptable y si se puede publicar la plantilla one-click de Coolify.
    • Alternativa: paquetes GHCR públicos con el repo privado (visibilidad independiente), o un registro o bucket propio.
  2. Q2 — Esquema del tren. Respondida (waxin, 2026-10-02 y 2026-10-03): SemVer. Hasta las alfas previas al MVP la versión es 0.0.0 alfa y los nightly son builds de desarrollo clean/dirty (D1); los alfas son 0.0.0-alpha.N (lock de dec-0133) y el primer v0.y.z se corta más adelante. Texto original: SemVer 0.y.z (recomendado) o CalVer.
  3. Q3 — Firma. cosign keyless (recomendado si Q1 = público) o clave gestionada (minisign o KMS).
  4. Q4 — Proxy de entrada. Coolify ya es un runtime más (tier 1 desde la enmienda del 2026-10-02, con su propio proxy), no el camino por defecto. Queda decidir si el tren trae su propio proxy (con styx-proxy-proof, styx-ingest-limits y ACME ya generados) como default en todos los runtimes de tier 1, o si en cada runtime se integra con el proxy que ya tenga el host (Traefik, Caddy, el ingress del clúster). Recomendado: proxy propio por defecto e integración como opción. Toca DS3-01/DS8-01.
  5. Q5 — OIDC. identity-svc exige un IdP, y un usuario sin IdP no puede entrar. ¿El tren incluye un OP (como el de dev) como opción por defecto de styx install? Es la mayor fricción de UX después del proxy.
  6. Q6 — releaseBlocking. Añadir el campo al esquema del model (check:model) y decidir qué nodos bloquean el primer stable. Como mínimo, las AppSec track/identity/sec y track/byte-runtime/sec sin P0/P1.
  7. Q7 — Primer stable. engine=zig por defecto (recomendado) o con la variante libav.
  8. Q8 — Runners del gate. La matriz de D12 necesita, por release, cuatro runtimes de tier 1 en hosts limpios (systemd nativo, Quadlet, Compose y una instancia de Coolify) y un arm64 nativo.
    • Opción A: runners alojados de GitHub (ubuntu-24.04 y ubuntu-24.04-arm), gratis sólo si el repo es público (Q1); hay que verificar la versión de Podman y de systemd que traen.
    • Opción B: VMs efímeras en hardware de waxin (por ejemplo, VMs de Incus y una Pi 4/5) con evidencia firmada.
    • armv7/32-bit queda fuera: no hay Bun armv7.
  9. Q9 — Bun. Alinear .bun-version, packageManager y las bases de los Dockerfiles a la versión con la que se regenera el lock (1.4.3). Nunca un bun.lock de 1.3.x.
  10. Q10 — Perfil demo con PGlite. Respondida (waxin, 2026-10-02): PGlite para tests, CI y demo (D11). Texto original: Los datos descartan PGlite como Postgres de cualquier runtime soportado (D11). ¿Se quiere además un perfil demo ("probar styx sin instalar nada", sin datos reales, con aviso de que no hay durabilidad ni aislamiento de roles), o PGlite se queda sólo en tests y CI? Recomendado: sólo tests y CI hasta que haya demanda.
  11. Q11 — Postgres en los runtimes OCI. ¿Imagen oficial postgres por digest (como hoy, recomendado mientras styx-postgres no haya pasado RE11) o la imagen derivada de styx-postgres, para que haya un solo Postgres probado en todos los runtimes?
  12. Q12 — Qué decide el benchmark. ¿Los datos de la lane E sólo ordenan el runtime por defecto de cada plataforma, o además fijan umbrales que suspenden un runtime (por ejemplo, RAM en reposo máxima de la instalación completa, ventana máxima sin servicio en un update)? Si son umbrales, los valores los pone waxin; el ADR no inventa cifras.
  13. Q13 — Plataformas diferidas. Respondida (waxin, 2026-10-02): macOS y Windows diferidas; Incus y Kubernetes en tier 2; Coolify sube a tier 1 con plantilla propia (D10). Texto original: Confirmar que macOS (launchd nativo, por la GPU) y Windows (WSL2 + Compose) quedan diferidas hasta que haya demanda, y que Incus, Kubernetes y Coolify entran en tier 2 y no en tier 1.

Lock (2026-10-03, waxin)

Decisión de waxin vía AskUserQuestion (2026-10-03), apuntada en el §33 de .claude/workflows/archive/orquestacion-2026-10/decisiones-pendientes.md (rama ops/orquestacion):

  1. Se lockea el ADR con sus enmiendas del 2026-10-02:
    • Coolify en tier 1, con plantilla propia (D10, D12). Tier 1 = systemd nativo, Podman Quadlet, Docker Compose y Coolify; tier 2 = Incus y Kubernetes/Helm; macOS y Windows diferidos.
    • 0.0.0 alfa hasta estar listos (D1): no se cortan versiones de tren hasta las alfas.
    • Canales nightly y stable (D5). La nightly es la build de desarrollo, marcada clean o dirty. Las releases son normalmente sólo stable.
    • Beta opcional, como checkpoint con el pipeline de stable.
    • Postgres: styx-postgres en el modo nativo; PGlite para tests, CI y demo (D11).
  2. Versión de los alfas (lock de dec-0133, el mismo día): 0.0.0-alpha.N; alpha.1 = 0.0.0-alpha.1 es el MVP. El primer v0.y.z lo corta waxin más adelante. Las versiones de componente quedan en 0.0.0 hasta ese corte (deploy/VERSION y los package.json de los componentes Bun; los componentes Zig se alinean en un cambio aparte).
  3. Diferidas de forma explícita, sin bloquear nada de lo que ya avanza: Q1 (visibilidad), Q3 (firma), Q4 (proxy de entrada), Q5 (OIDC incluido), Q6 (releaseBlocking), Q7 (libav en el primer stable), Q8 (runners del gate), Q9 (Bun), Q11 (Postgres en OCI) y Q12 (qué decide el benchmark). Cada una se decide con waxin, vía AskUserQuestion, cuando la lane o el ticket que la necesita llegue a ella; las que tocan publicar (Q1, Q3, Q6, Q7, Q8) antes del primer stable.
  4. Enmienda de dec-0133 a D5 (un alpha o beta por stable no mueve styx-release:stable): pendiente de la P5 de dec-0133.