Nota · a quien corresponda O_SDK es un wrapper Docker no oficial de Oasis · SolarNET.HuB: unos ficheros para levantarlo en contenedores. Hoy lleva Oasis 1.3.8 y sostiene un pub de verdad; lo probado y lo que sigue en obras, en ¿Quién llega? Software libre, work in progress, sin prisa.
Skip to content

Protocolo del HUB clearnet (nodo de soporte) ​

Este repo es el sitio del HUB (C:\S_LAB\o-sdk, https://github.com/alephscriptorium-eng/O_SDK.git). El HUB vive en https://pub.escrivivir.co/c y su puerta es la Sala 04 del Scriptorium (/hub/).

Estado · ACTIVADO 2026-09-13 19:55 UTC en pub.escrivivir.co (WP-O46, rama wp/O46-hub-nodo-soporte). Cuenta de soporte azofaifo-scriptorium-skin-bot-1 = @KM+ZBipR18VSyjNTFjAOnsmz6EiobGYHb3ZCZ4ZxQYI=.ed25519. Journal --mode server+hub 20:02 UTC. Plan, dosier y hallazgos de gates y deploy: ARCHIVO/DISCO/oasis-clearweb/v2.md

  • dosier/; reporte plan/REPORTES/WP-O46-hub-nodo-soporte.md (asientos D-O13/D-O14). Pendientes: ajuste de memoria a las 24 h (§7), caché a los 7 días (§6), rollback nivel 1 no ensayado (decisión del custodio: deja /c sin servicio ~1 min). Upgrade 1.0.8 → 1.1.2 desplegado 2026-09-17 11:47 UTC (WP-O97, rama upgrade/oasis-1.1.2): pub y HUB en la imagen b6181cb7, ambos feed id sin cambios, /c/assets/* cacheado (§5.5), Sala 04 con 16 tipos. Reporte plan/REPORTES/WP-O97-upgrade-oasis-1.1.2.md.

Checklist operativo para activar, operar, mantener en disco y llevar a través de los upgrades el HUB web de solo lectura de Oasis (/c, 23 tipos de contenido desde 1.2.3, con Calendars, Campaigns, Emergencies, Housing, Maps y Rooms, más el contenido de tribus en /c/tribe/<id>; 17 desde 1.2.1, con Files; 16 desde 1.1.2; 12 en 1.0.8, ./clearnet.md). Deriva del plan v2 del 2026-09-13 y de su revisión adversarial (20 hallazgos, dosier/05-revision-adversarial.md).

Modelo mental. El pub (oasis-pub-scriptorium, modo server) solo replica y, desde 1.2.2, hace de centralita acotada de Phone y Rooms (§14). El HUB lo sirve otra cuenta SSB, un nodo de soporte, en su propio contenedor oasis-pub-hub: misma imagen que el pub, command: ["backend"], es decir backend.js --public con el sbot embebido y un .ssb propio en /srv/oasis/oasis-hub. Delante va oasis-pub-hub-cache (nginx, caché en disco acotada) y el Caddy compartido enruta solo @hub. El pub no cambia: ni imagen, ni entrypoint, ni servicio en el compose, ni un reinicio. Para la red SSB el HUB es un habitante más: redime un invite del pub, recibe follow-back y replica a hops 2.

0. Cómo hemos llegado aquí (evolución) ​

CuándoHitoDónde queda
Oasis 1.0.xUpstream añade el HUB clearnet: /c, modo --public, opt-in por habitante./clearnet.md (doc upstream)
Ciclo 0.9.6→1.0.8Receta v0 (vivió en UPGRADE-PROTOCOL.md §8): proxy de Caddy al backend del pub con un modo server-hub del entrypoint (sbot + backend en el mismo contenedor). Nunca activadaretirada; historia al pie de esta tabla
2026-09-13 · v1Plan «Sala 04»: backend dentro del contenedor del pub, supervisor sbot+backend, rebuild de ~3 GB, riesgo R0 (el getter de SSB_server.js podía crear un segundo sbot sobre el mismo .ssb)ARCHIVO/DISCO/oasis-clearweb/v1.md
2026-09-13 · v2Tres correcciones del custodio: a) el sbot del pub no se toca · b) el HUB es otra cuenta SSB en contenedor propio · c) estado y caché en el volumen de datos con utilidad de control. Revisión adversarial F1-F20v2.md, dosier/
⏳Deploy WP-O46 (gates locales G1-G7 → VPS)§10 de este doc + journal

El hallazgo que hizo posible la v2 sin tocar el pub: backend.js no es un cliente puro. src/client/gui.js requiere SSB_server y su getter crea un sbot embebido si no existe (solo cae al socket unix si el arranque falla por LOCK). Con un ~/.ssb propio, node backend.js es un nodo SSB completo en un solo proceso. Lo que en la v1 era el riesgo R0 es en la v2 el mecanismo.

Tabla completa v1↔v2: dosier/08-v1-vs-v2.md.

Historia de la v0, para no repetirla. La receta que vivió en UPGRADE-PROTOCOL.md §8 (ciclo 0.9.6→1.0.8) proxyaba al backend del pub con un modo server-hub del entrypoint y enrutaba /qr/*. Se retiró el 2026-09-13 (D-O13) por la v2: el HUB es un nodo de soporte con identidad propia en su propio contenedor (command: ["backend"], misma imagen, sin rebuild), el sbot del pub no se toca y /qr/* queda fuera. Siguen valiendo los hechos del proxy, recogidos en §2 y §4: /assets/* entero colisiona con la landing, --allow-host es obligatorio para /clearnet, las URLs de nuestro feed llevan %40%2F sin decodificar y /c/blob/* es inmutable.

1. Invariantes (lo que todo cambio futuro debe conservar) ​

  • El pub no se toca. Ningún servicio nuevo monta nada de /srv/oasis/oasis-pub; el servicio oasis-pub del compose, su imagen y su entrypoint no cambian; el pub no se reinicia en ningún paso (deploy, upgrade del HUB, rollback).
  • Identidad propia. El secret del HUB nace en /srv/oasis/oasis-hub/ssb-data en el primer arranque; nunca se copia del pub. Feed id del HUB ≠ feed id del pub.
  • Solo lectura hacia fuera. --public (todo no-GET → 403), nginx (405 a no-GET), Caddy enruta solo /c, /c/*, /clearnet y /assets/{styles,themes,images}/*. OASIS_HUB_PUBLIC=false existe solo para el bootstrap del invite y nunca con @hub activo en Caddy.
  • El feed del HUB solo debe contener cuatro tipos propios, todos públicos: contact (follow al pub), pub (lo publica ssb-invite al aceptar el invite, con la dirección del pub), about (§11) y oasisVersion (el backend publica uno cada vez que arranca en una versión distinta de la última que anunció: +1 por upgrade y +1 por rollback). Ningún cifrado. Se mide con bash devops/scripts/upgrade-gates.sh --remote snapshot <tag> (línea tipos: del HUB), que cuenta por autor e incluye los cifrados como (cifrado).

    El control anterior era vacío. grep -a -c '"private":true' flume/log.offset da 0 siempre: un mensaje cifrado se guarda como "content":"….box", sin ese campo. La primera medida buena (2026-10-01) encontró 4 cifrados propios en el feed del HUB del host (y 1 en el del bot de cartera y 1 en el del pub). Son anteriores a este control y no se pueden retirar. Qué son (reproducido en local el 2026-10-01): el backend trae avisos automáticos —el «bot político» (checkPoliticalChanges), el de empleo, recordatorios— que corren con cualquier petición, también la del healthcheck, y cuando cambia el gobierno o el ciclo se envían a sí mismos un mensaje cifrado. Hasta 1.1.4 no se podían apagar. Desde 1.1.10 notifyBot respeta inboxMutedBots de la config: en un nodo de soporte van todos silenciados (§2). El invariante vuelve a ser alcanzable: ningún cifrado nuevo, y upgrade-gates.sh check lo comprueba en cada upgrade.

  • El pub solo publica el contact del follow-back al redimir el invite (función normal de un pub). Secuencia del feed del pub antes/después = +1, una vez.
  • Todo el estado nuevo bajo /srv/oasis/oasis-hub/* (volumen de datos, 40 GB). df -h / igual antes y después: sin rebuild, solo docker pull nginx:alpine.
  • Delta del fork en src/: solo los guards contados. El HUB vive en la zona wholesale (pub/**, devops/**); desde WP-O132 (D-O30) lleva un guard propio en src/views/clearnet_view.js (idioma y tema del visor por petición, selector de tema), el sexto de UPGRADE-PROTOCOL.md §2, con invariante en upgrade-invariants.d/hub.tsv y gate en upgrade-gates.sh hub --strict.
  • CA-ANTI-AUTORIDAD (D-O6): el opt-in viaja con el feed del habitante; el HUB no decide quién aparece y no se lista a sí mismo (su about no lleva vis_*).
  • La cuenta se declara con nombre propio de bot de soporte (D-O20, que supera la forma de D-O14): nick corto que dice tipo y pub, cadena completa en la descripción (AGENTES.md §5). Cada bot que se conecte al pub toma el siguiente cardinal y se registra en la ficha de instancia (§11).
  • Fail-open en topología, fail-closed en capacidades (D-O7): cualquier nodo que replique puede servir un HUB; lo que no puede es escribir.

2. Piezas (fuente única) y contrato de rutas ​

PiezaFicheroQué fijaEstado
Servicios oasis-hub + hub-cachepub/docker-compose.pub.ymlmisma imagen, command: ["backend"], env OASIS_*, mem_limit, healthcheck a /c/inhabitant/@AAAA…=.ed25519 (no a /c, que es O(N·k)), sin ports, sin profiles⏳ WP-O46
Identidad y replicaciónpub/config/hub/ssb-config (bind :ro a ~/.ssb/config)caps.shs del ciclo, pub:false, friends.hops:3 (D-O15: con 2 el HUB solo alcanzaba los 3 seguidos directos del pub y /c quedaba vacío; los habitantes con Clearnet están a 3 saltos, seguidos por La Plaza), sin seeds (un seed al pub mete su clave en gossip.json antes del invite y el backend responde alreadyFederated sin redimirlo; la entrada persistente del pub la deja pub-link.js --remember en conn.json), conn.populatePubs: false (WP-O143: el nodo deja de descubrir pubs nuevos por los mensajes pub de la red, conn-scheduler.js; los ya conocidos en conn.json siguen: «pub de la casa garantizado, externos permitidos», D-O38), connections completo (mergeDeep reemplaza arrays enteros) con incoming.net.host: 0.0.0.0 obligatorio (el entrypoint pasa --host 0.0.0.0; otro valor = crash loop «conflicting connection settings»)✅ G2
Normalizar conn.json tras el invitepub/tools/pub-link.js --forget-seeded <KEY44> · --remember <addr> · --connect <addr> (viaja por stdin, node -; pub/tools no está montado en el HUB). hub-conn-fix.js queda subsumido en estas tres acciones (WP-O143, D-O37)tras invite.accept, ssb-invite deja la dirección del pub con el seed y sin key: el HUB reconecta con la identidad desechable y el pub no replica; sin key nunca reconecta tras un reinicio. La secuencia es forget addr:SEED · remember {key,type:pub,autoconnect:true} · connect✅ G3
Enlace con el pub (WP-O143, D-O38)start_pub_link del docker-entrypoint.sh (modos client y backend; OASIS_PUB_LINK_HOST, por defecto oasis-pub, vacío = desactivado; OASIS_PUB_LINK_FIRST_S 30, OASIS_PUB_LINK_EVERY_S 60, OASIS_PUB_LINK_ADDR opcional para una instancia sin libreta) + pub/tools/pub-link.js --ensure · libreta del pub (upgrade-gates.sh link pub --yes: el pub guarda a sus nodos sin autoconnect y conecta solo con el que no tenga el enlace; con autoconnect marcaría a un nodo ya conectado y habría dos conexiones por clave, medido en local)El cupo: ssb-conn mantiene por nodo un número de plazas salientes para pares que no son room (4 fijas hasta 1.3.2, maintainConnections(4, …); desde 1.3.3, connections de la oasis-config.json del nodo, 5-30, 10 por defecto, que network_pause.js sirve como oasisConnQuota() y la GUI fija en /settings/connections); las entrantes no cuentan (parche de upstream); con las plazas llenas no marca a nadie hasta rotar (~2 h/√N, expulsando al de menor oasisPeerRank, src/server/network_pause.js: desde 1.3.3 primero quien nos sigue, luego versión, seguidos compartidos, total). autoconnect solo filtra candidatos: no hay par prioritario (desde 1.3.3 el pub, que sigue a sus nodos, encabeza el ranking, pero el cupo sigue sin reservarle plaza), y syncPeerBook con pub: true (network_pause.js:179-186) marca autoconnect: true a todo pub descubierto. Un nodo de soporte con 4 externos arriba puede quedarse horas sin el pub con conn.json sano. Dos capas, homogéneas para HUB y bot: (1) el pub recuerda a sus nodos (net:<servicio>:8008~shs:<KEY44>, {type:'pub', autoconnect:true}): para ellos la conexión es entrante y no gasta su cupo (el pub gasta una saliente por nodo); (2) el keepalive del entrypoint pide --ensure <host> cada 60 s: busca la clave del pub en conn.peers() (in u out) y, si falta, conn.connect (abre una 5.ª saliente que el scheduler no corta pero sí puede rotar: por eso es periódico). 0 publicaciones; log [pub-link] solo en cambios de estado. El healthcheck no cambia: un enlace caído es un gate, no una salud de contenedor. Se mide por conn.peers() del nodo (pub_link en la foto, pares: en hub-wallet.sh status, LINK en deploy-status.sh), nunca por el log del pub✅ WP-O143
Visorpub/config/hub/oasis-config.json (bind :ro a ~/.ssb/oasis/oasis-config.json; hasta 1.2.3 a src/configs/oasis-config.json, que desde 1.2.4 config-manager.js solo copia una vez si el del estado falta)copia del de src/configs/ con 7 claves fijadas: aiMod/aiNavMod:off, ssbLogStream.limit:20000, lanBroadcasting:false, inboxMutedBots (todos los avisos automáticos silenciados: sin esto el HUB se envía cifrados a sí mismo, §1) y, por D-O25, themes.current y language (tema e idioma por defecto del visor; valores de instancia: en la casa Dark-SNH, igual al original, y es → 6 diferencias efectivas en 1.1.10). walletPub salió en 1.1.10. Se regenera con node pub/scripts/regen-node-configs.js, no se parchea (§5.2)✅ regenerado en 1.1.10
Caché HTTPpub/config/hub/nginx.conf.templateproxy_cache_path … max_size=${HUB_CACHE_MAX_SIZE} inactive=7d, caché negativa (404 30 s, 5xx 5 s, WP-O53), proxy_hide_header de las cabeceras del backend, limit_req por XFF, location / { return 404; }; en /assets/* y en /c/assets/* (visor 1.1.2) proxy_ignore_headers Cache-Control (koa-static manda max-age=0). D-O25 (1.1.10): Accept-Language, Cookie y Accept-Encoding no se reenvían al HUB; location propia para /c/sitemap.xml y /c/rss/* que corrige las URLs a https con sub_filter. En local Windows el cache dir es el volumen nombrado hub_http_cache (los bind mounts rompen proxy_cache); en el VPS es la ruta bind✅ G4 · U4
Edgepub/caddy/Caddyfile (bloque @hub del vhost del pub)reverse_proxy hub-cache:80; transport http {…} en multilínea; /qr/* fuera; /assets/* entero no (fanzine.css es de la landing); desde 1.1.2 el visor usa /c/assets/*, ya cubierto por /c/*; header {} global intacto✅ G1/G4
Variablespub/.env.vps.example, .env.local.example, .env.example; .env.prod del VPSOASIS_HUB_{SSB_DATA,LOGS,HTTP_CACHE}_DIR, OASIS_HUB_{SSB_CONFIG,OASIS_CONFIG}_FILE, OASIS_HUB_ALLOW_HOST, HUB_CACHE_MAX_SIZE, OASIS_HUB_MEM_LIMIT, OASIS_HUB_NODE_OPTIONS, OASIS_HUB_PUBLIC (comentada)⏳
Validación de rutaspub/scripts/common.sh (validate_vps_persistent_paths, ensure_runtime_dirs) · devops/scripts/verify-debian13-base.sh (check_layout)las tres rutas OASIS_HUB_*_DIR absolutas bajo /srv/oasis en layout canónico⏳
Control de discodevops/scripts/hub-disk.sh (npm devops:hub-disk)status · check · prune-blobs · prune-cache · --json (§6)⏳
Puertapub/site/hub/ (Sala 04) + vestíbulo pub/site/scriptorium/ + Accesos en pub/site/index.htmlestático, cero JS, plantilla pub/site-templates/poster⏳
Gobiernoplan/BACKLOG.md WP-O46/WP-O47 · plan/DECISIONES.md D-O13 · CHANGELOG.md✅ 2026-09-13
/srv/oasis/                       volumen de datos (40 GB, ext4 por UUID en fstab)
├── oasis-pub/                    estado del pub (INTACTO)
├── teatro/                       obras estáticas (TEATRO-PROTOCOL)
└── oasis-hub/                    estado del nodo de soporte
    ├── ssb-data/                 secret · db2/ (flume/ en ≤ 1.1.x) · blobs/ · ebt/ · conn.json · gossip.json · oasis-first-contact
    ├── logs/                     bind de /app/logs (sin escritores; simetría con el pub)
    └── http-cache/               proxy_cache de nginx (max_size + inactive=7d)
/opt/oasis-scriptorium/OASIS_PUB/ código y configs (disco de sistema, 25 GB, layout vivo pre-refactor)
    ├── docker-compose.pub.yml    + oasis-hub, hub-cache
    ├── caddy/Caddyfile           + bloque @hub
    └── config/hub/               ssb-config · oasis-config.json · nginx.conf.template

Regla de los binds de fichero (Caddyfile, ssb-config, oasis-config.json, nginx.conf.template): sobrescribir in place (cat > fichero), nunca reemplazar el inode (mv, editores que hacen rename): el contenedor seguiría viendo el fichero viejo. Y después, lo que cada uno pida para releerlo: Caddy validate + reload; ssb-config y oasis-config.json, recrear el HUB; la plantilla de nginx, recrear hub-cache (se renderiza al arrancar: un reload no la relee).

Regla del visor (D-O25): en clearnet la presentación es del pub. Los valores por defecto del visor (tema, idioma) los fija la config del HUB; lo que elige el visitante viaja en la URL (?lang=), nunca en cabeceras ni cookies. Motivo: la caché sirve la misma respuesta a todo el que pide la misma URL; si el backend decide algo por una cabecera (Accept-Language) o una cookie (theme, las de módulos), el primer visitante decide por todos durante lo que dure la entrada. Upstream ya lo hace así con el tema (el visor lee themes.current de la config, no la cookie); desde 1.1.10 no lo hace con el idioma (cae en Accept-Language sin emitir Vary). Por eso, a partir del ciclo 1.1.10 (WP-O113), nginx no reenvía al HUB Accept-Language ni Cookie, y language es clave fijada de la config. Cualquier detección nueva «por visitante» que traiga upstream se trata igual (headers en el diff de comportamiento, §5.1).

3. Activación (resumen; la secuencia completa está en v2.md «Secuencia de deploy») ​

G0  gates locales con npm run pub:local:up (G1 sintaxis · G2 arranque · G3 invite ·
    G4 caché/cabeceras · G5 aislamiento · G6 memoria · G7 hub-disk) — bloqueantes
1   VPS: líneas base (deploy-status, backup del pub, df/docker stats, secuencia del feed
    del pub, nº de contact) · mkdir /srv/oasis/oasis-hub/{ssb-data,logs,http-cache} con el
    uid de oasis · PRE-CREAR ssb-data/oasis-first-contact (evita el PM de bienvenida) ·
    OASIS_HUB_* en .env.prod (backup previo)
2   subir compose + config/hub/* + Caddyfile in place (SIN reload todavía) · docker pull nginx:alpine
3   bootstrap en dos fases, nada expuesto: OASIS_HUB_PUBLIC=false up -d --no-deps oasis-hub →
    invite.create en el pub → reescribir host a la IP DEL BRIDGE del pub (docker inspect; ssb-ref
    exige IP o dominio con punto, 'oasis-pub' no vale) → POST /settings/invite/accept con Referer
    (siempre 302: verificar POR ESTADO: conn.json del HUB + contact +1 en el pub) →
    pub-link.js --forget-seeded <KEY44> · --remember 'net:oasis-pub:8008~shs:<KEY44>' ·
    --connect (normaliza conn.json; sin esto no replica ni reconecta) → docker restart
    oasis-pub-hub → verificar el enlace POR SONDA, no por el log del pub:
    upgrade-gates.sh --remote link hub --yes (o snapshot: hub pub_link=connected) →
    POST /profile/edit (about según §12: nombre y descripción de la ficha de instancia, sin
    vis_*) → up -d --no-deps oasis-hub (PUBLIC=true) → POST invite/accept = 302 con
    ?error=…public mode… (400 sin Referer; nunca 200) = gate antes de exponer
4   up -d --no-deps hub-cache → MISS/HIT desde la red Docker → caddy validate + caddy reload
    → healthz de los 5 vhosts ajenos
5   site (Sala 04, vestíbulo, Accesos) con deploy-site.sh y fusión de los valores vivos
6   cierre: deploy-status · hub-disk.sh status --json · deploy-log.sh --mode server+hub ·
    a las 24 h ajustar OASIS_HUB_MEM_LIMIT · a los 7 días HUB_CACHE_MAX_SIZE/hops

Reglas fijas: siempre docker compose --env-file .env.prod -f docker-compose.pub.yml up -d --no-deps <svc>; nunca el deploy.sh del VPS (hace up --build de todo); Caddy = validate + reload, nunca restart pub-web (WP-O74); el healthcheck del compose no debe pegar a /c.

4. Verificación (matriz mínima, pública) ​

bash
H=https://pub.escrivivir.co; FEED='%40%2FsnvahvaTP5obvdiYva9OX9NNnTSlUQbEUBSuDRzrZA%3D.ed25519'
c() { curl -sS -o /dev/null -w "%{http_code} %{content_type}  $*\n" "$@"; }
c $H/ ; c $H/assets/fanzine.css ; c $H/public/status         # landing intacta (estático)
c $H/c ; c "$H/c/inhabitant/$FEED" ; c $H/clearnet           # 200 · 200 (URI con %40%2F sin decodificar) · 302
curl -sI $H/c | grep -i x-cache-status ; curl -sI $H/c | grep -i x-cache-status   # MISS · HIT
curl -sI $H/c | grep -iE '^(x-frame-options|content-security-policy)' | sort | uniq -c   # 1 de cada; script-src 'none'
c -X POST $H/c                                               # 405 (nginx); 403 si llegara al backend
for p in settings profile publish update json/x qr/x; do curl -sI $H/$p | grep -ci x-cache-status; done   # 0 ×6
curl -s "$H/c/inhabitant/@nadie.ed25519" | grep -c "not accessible"   # 1 — es 200 por diseño upstream

En el VPS: docker ps → oasis-pub-scriptorium, oasis-pub-hub, oasis-pub-hub-cache healthy · feed id del pub sin cambios · feed del HUB sin cifrados nuevos (upgrade-gates.sh, §1) · df -h / = línea base · hub-disk.sh check → 0. Aislamiento: docker stop oasis-pub-hub no afecta a whoami/invite del pub; docker stop del pub deja /c sirviendo STALE desde la caché.

5. Upgrades de Oasis con el HUB activo (anexo de UPGRADE-PROTOCOL.md) ​

El HUB lleva un guard en src/ (el sexto, el del visor: §1) y además depende de comportamientos de upstream que no son API estable. Un upgrade puede romperlo sin tocar ninguno de los 6 guards. El orden y las puertas los da UPGRADE-PROTOCOL.md; aquí está lo que es propio del HUB.

5.1 Invariantes y diff (UPGRADE §3.1). Lo que el HUB necesita de upstream está en devops/scripts/upgrade-invariants.d/hub.tsv (antes vivía aquí como un bloque de greps) y se evalúa sobre el árbol de trabajo:

bash
bash devops/scripts/upgrade-behaviour-diff.sh $OLD_REF $NEW_REF --section annex    # ninguna línea «!»
bash devops/scripts/upgrade-behaviour-diff.sh $OLD_REF $NEW_REF --section routes,headers,config,deps

Los invariantes dicen que algo sigue existiendo; no dicen que se comporte igual. Para el HUB el comportamiento que importa sale de cuatro secciones del diff, y cada una tiene su consecuencia:

SecciónQué mirarConsecuencia
routesGET nuevas bajo /c/salen solas al clearnet (Caddy y nginx enrutan /c/*): decidir si se publican, darles location propia si no son HTML cacheable, paridad en la Sala 04
headerscabeceras, cookies, accept-language, ctx.protocol en código que sirve /cregla D-O25 (§2): si la respuesta depende de algo que no está en la URL, la caché la sirve a quien no toca. Cabeceras de respuesta nuevas: a proxy_hide_header si Caddy ya las fija
configdefaults de oasis-config.json, server-config.jsonregenerar la copia del HUB (§5.2)
depsssb-conn, ssb-conn-hub, ssb-invite, network_pause.jspub-link.js y la sonda dependen de su API (peers, dbPeers, remember, connect, inbound); el cupo y el ranking viven ahí (§2). Si cambia alguno: annex (link.tsv) sin ! + repetir el gate G3 en local (invite + docker restart + link hub → connected) + drill de caída: pub-link.js --disconnect oasis-pub en el nodo → check --wait --expect 0 verde con pub_link=connected en < 120 s y 2 líneas [pub-link] en su log (ausente → connected); SSB_ACTION=peers en el pub: cada key ≤ 1 vez (sin stream duplicado)

Si un invariante cae o una de esas líneas afecta, la pieza de §2 se adapta en la misma rama del upgrade y se repite el gate del visor (upgrade-gates.sh --local hub --strict, UPGRADE §3.4 U4). Cambios que hay que esperar de upstream: el nombre del flag de bienvenida (el HUB publicaría un PM en el siguiente arranque), rutas nuevas bajo /c/*, cabeceras nuevas del backend, claves nuevas en oasis-config.json, y cualquier detección «por visitante» (idioma, tema, dispositivo).

5.2 Ficheros derivados de upstream que se regeneran en cada upgrade.

  • pub/config/hub/oasis-config.json es una copia de src/configs/oasis-config.json con unas pocas claves fijadas (la lista vigente, en la fila «Visor» de §2). Tras el overlay: node pub/scripts/regen-node-configs.js la regenera (y la plantilla del bot de cartera) e imprime qué claves difieren del original: deben ser solo las fijadas. --check sale 1 si no está al día. Una clave que upstream retira desaparece sola de la copia: se regenera, no se parchea. La lista de inboxMutedBots sale de INBOX_BOTS en src/models/pm_model.js: si upstream añade un aviso nuevo, entra al regenerar. Dónde se monta (desde 1.2.4, WP-O135): config-manager.js lee la config del nodo en ~/.ssb/oasis/oasis-config.json y la de src/configs/ solo la copia una vez si aquella falta; el bind del compose va al sitio nuevo. Con el bind viejo, la regeneración dejaría de surtir efecto tras el primer arranque en 1.2.4+. Lo que el backend escriba por su cuenta (saveConfig) falla contra el bind :ro, como antes; si deja un oasis-config.json.tmp-* en ~/.ssb/oasis/, se retira.
  • pub/config/hub/ssb-config reemplaza arrays enteros de src/configs/server-config.json (mergeDeep): si upstream cambia connections.incoming/outgoing, decidir si se replica. Orden de fusión en ssb_config.js (guard): ~/.ssb/config ← server-config.json ← el fichero de OASIS_SERVER_CONFIG_OVERRIDE; pub, HUB y bot apuntan el override a su propio ~/.ssb/config, así que lo que diga ese fichero manda. Caso vigente (1.2.3): upstream abre outgoing.onion: [{transform: shs}] (marcar .onion de otros pubs); los tres nodos llevan "onion": [] y no salen por Tor (no hay proxy en el stack). Replicarlo es una DECISIÓN del custodio y pide proxy; hasta entonces, [].
  • pub/config/hub/nginx.conf.template: lo que digan routes y headers. Se renderiza con envsubst al arrancar hub-cache: tras cambiarla (in place) hay que recrear el contenedor (up -d --no-deps --force-recreate hub-cache); nginx -s reload no la relee. Antes, nginx -t de la plantilla renderizada en un contenedor desechable.
  • Rotación de ciclo (UPGRADE-PROTOCOL.md §5): caps.shs también vive en pub/config/hub/ssb-config. Añadirlo al lockstep. Cambiar el cap no rompe el follow (está en los logs), pero HUB y pub deben rotar juntos. El caps.shs del wallet-bot (pub/config/wallet-bot/ssb-config, WP-O102) entra en el mismo lockstep.

5.3 Deploy del upgrade con el HUB activo. La imagen es compartida: tras build oasis-pub y up -d --no-deps oasis-pub, el HUB sigue corriendo la imagen vieja (Docker no recrea contenedores por un retag). Orden:

bash
C="docker compose --env-file .env.prod -f docker-compose.pub.yml"
$C build oasis-pub && $C up -d --no-deps oasis-pub        # pub primero (UPGRADE §4)
bash devops/scripts/deploy-status.sh                       # pub healthy, feed id igual
$C up -d --no-deps oasis-hub                               # HUB después; hub-cache no cambia
docker logs --since 2m oasis-pub-hub | grep -iE 'EROFS|Another Oasis|error'   # nada
docker exec oasis-pub-hub grep -o '"id": *"[^"]*"' /home/oasis/.ssb/secret   # mismo feed id del HUB
bash devops/scripts/hub-disk.sh prune-cache                # si el upgrade reindexó: no servir indexingView 60 s
# matriz §4 · deploy-log.sh … --mode server+hub

Mientras el HUB se recrea, /c sirve STALE desde nginx; el pub no depende del HUB. Rollback del upgrade con HUB: retag de la imagen vieja + up -d --no-deps --no-build oasis-pub oasis-hub. El rollback publica otro oasisVersion por nodo, también el del pub (lo anuncia el propio sbot): pide GO. El orden completo con bots (pub → HUB → bots), los backups previos y las puertas de GO están en UPGRADE-PROTOCOL.md §4; las comprobaciones de este bloque se hacen allí con upgrade-gates.sh --remote check y hub --strict.

5.4 Si cambia la identidad del pub (recuperación, RECOVERY-PROTOCOL.md): el HUB apunta al pub por clave en conn.json. Con el HUB parado, vaciar conn.json y gossip.json (la clave vieja en gossip.json haría que el nuevo invite respondiera alreadyFederated) y repetir el bootstrap del invite (§3 paso 3). El secret del HUB no cambia.

5.5 Hechos del ciclo 1.0.8→1.1.2 (2026-09-17). Todos los greps de §5.1 ≥ 1 sobre el árbol nuevo; ssb-* en src/server/package.json sin cambios (G3 no se repitió); oasis-config.json y server-config.json sin cambios upstream (la copia del HUB no se regeneró; diff = las 4 diferencias efectivas); cabeceras del backend iguales (proxy_hide_header intacto). Lo que sí cambió y cómo se adaptó:

  • El visor pasa de /assets/images a /c/assets/images (app.use(mount("/c/assets", assets)) nuevo en middleware.js). Caddy /c/* ya lo cubría; en nginx caía en la location genérica de /c sin proxy_ignore_headers Cache-Control → MISS perpetuo. Se añadió location ~ ^/c/assets/ (1 d). Las rutas /assets/(styles|themes|images) se mantienen por compatibilidad.
  • Cuatro rutas de detalle nuevas: /c/bookmarks/:id, /c/feed/:id, /c/market/:id, /c/wiki/:id (12 → 16 tipos; la guía upstream clearnet.md lista 15, omite bookmarks). Paridad en la Sala 04.
  • /c/qr/:feedId nuevo (PNG con Cache-Control: no-store): entra por /c/* y nginx no lo cachea. Codifica http://localhost:3000/qr-action/follow/… (QR_ACTION_BASE es constante): inútil desde el clearnet pero inocuo. La política no cambia: /qr/* sigue fuera.
  • Opt-in por habitante: tres claves nuevas en visibilityPrefs (clearnetMarket, clearnetFeed, clearnetWiki); /profile/clearnet-toggle desaparece (activar cualquier módulo hace público al habitante; apagarlos todos lo retira).
  • onboarding_model.js: el flag oasis-first-contact sigue; añade adopt(feedId). SSB_server.js: el detalle técnico de lock/corrupción solo se imprime con --debug/OASIS_DEBUG.
  • Overlay en Windows: con core.autocrlf los ficheros nuevos salen CRLF en el árbol de trabajo; git diff normaliza, pero un reemplazo literal con \n no casa. Detectar el EOL antes de editar.

6. Disco: mantener el tamaño del HUB ​

Ruta (bajo /srv/oasis/oasis-hub/)Crece conPodableQuién acota
el log: ssb-data/db2/log.bipf (Oasis ≥ 1.2; antes ssb-data/flume/log.offset, que tras migrar es una guarda de texto)replicación (hops 2, incluye .box privados ilegibles)nofriends.hops en ssb-config
índices: ssb-data/db2/indexes, db2/jit (antes ssb-data/flume/*), ssb-data/ebt/el logsí (rebuild automático)—
ssb-data/blobs/ por encima del techoreplicación y visorsí (se vuelven a pedir)blobCache.pubMaxMB en la config del nodo (desde 1.2.1; CAPACIDAD.md §3)
ssb-data/blobs/cada GET /c/blob/<id> ausente (blobs.want en segundo plano) + blobs replicados. Desde 1.2.4 upstream lo limitaba a lo que el nodo ya tiene (getLocal); de 1.2.5 a 1.3.2 el séptimo guard (D-O31) reponía el want; desde 1.3.3 upstream lo pide solo (wantClearnetBlob: al pedirlo y al indexar, acotado a la lista blanca clearnetBlobAllowed) y responde «pendiente» con no-store mientras llega (guard retirado en 1.3.8, D-O40)sí: content-addressed, want repone al siguiente GEThub-disk.sh prune-blobs, blobs.max 50 MB (guard del fork)
http-cache/tráfico websínginx max_size + inactive=7d; prune-cache
stdout de los contenedores (/var/lib/docker, disco de sistema)tráfico y replicaciónsílogging.max-size/max-file del compose

Herramienta: bash devops/scripts/hub-disk.sh <cmd> (npm devops:hub-disk), SSH al VPS vía lib-host.sh, read-only salvo prune-*:

ComandoQué haceCuándo
statusdf -h / /srv/oasis, opciones del mount (noatime), du de flume o db2, blobs, cache y logs, y del pub (comparativa), nº de blobs, docker stats, docker system dfsemanal y antes/después de cualquier deploy
checkumbrales HUB_DISK_SOFT_PCT=75 / HUB_DISK_HARD_PCT=90 sobre /srv/oasis y / → exit 0/1/2deploy-status.sh lo imprime. No hay temporizador en el host: se corre al medir
prune-blobs [--older-than N=30] [--dry-run]borra blobs no accedidos en N días (-atime; con noatime cae a -mtime). Nunca toca el log (db2/, flume/), secret, conn.json, gossip*.jsonsoft en /srv/oasis
prune-cachevacía http-cache/ + nginx -s reloadtras un rebuild de índices; casi nunca por espacio
--json{ts, srvOasisPct, rootPct, hubFlumeBytes, hubDb2Bytes, hubBlobsBytes, hubCacheBytes}apéndalo a devops/logs/hub-disk.jsonl (no versionado)

Cadencia: status --json en el cierre del deploy (línea base), a las 24 h, a los 7 días y después semanal; check en cada deploy-status. Con 30 días de --json se decide el tope duro (WP-O47).

Árbol de decisión (check ≠ 0):

  1. /srv/oasis ≥ 75 %: status → ¿qué domina?
    • http-cache/ → bajar HUB_CACHE_MAX_SIZE en .env.prod y up -d --no-deps hub-cache (nginx purga solo hasta el nuevo tope).
    • blobs/ → prune-blobs --dry-run, luego prune-blobs. Después de una poda grande, un reinicio del HUB es más rápido (el entrypoint hace chown -R de blobs/ en cada arranque).
    • db2/ (flume/ en ≤ 1.1.x) → el log replicado no se poda. Opciones por orden: friends.hops 3→2 en ssb-config + up -d --no-deps oasis-hub (acota el crecimiento futuro, no reduce lo replicado) · como último recurso, con el HUB parado, vaciar ssb-data/ conservandosecret, config, conn.json, gossip.json, oasis-first-contact y dejar que re-replique (identidad y follow intactos; /c sirve indexingView mientras tanto → prune-cache al terminar).
    • oasis-pub/ (el pub, no el HUB) → no es de este protocolo; pub/BACKLOG.md «métricas de disco».
  2. / ≥ 75 %: el HUB no escribe en / salvo los logs json de Docker (ya acotados). Es presión de imágenes/builder: docker system df, docker image prune -f && docker builder prune -f (UPGRADE-PROTOCOL.md §4).
  3. ≥ 90 % en cualquiera: hacer lo anterior ahora; si es /srv/oasis, prune-cache da aire inmediato sin coste (la caché se rehace sola).

Nunca borrar: secret (identidad del HUB), config, conn.json/gossip*.json (cómo encuentra al pub), oasis-first-contact. Backup: el HUB es re-derivable de la red; lo único que vale la pena copiar una vez, tras el bootstrap, es secret + conn.json (devops/backups/oasis-hub/, no versionado). Sin ellos el rollback nivel 2 es «cuenta nueva + otro invite», no una pérdida.

Tope duro (imagen loop de tamaño fijo o cuota ext4 sobre /srv/oasis/oasis-hub) y mem_limit del pub → WP-O47, con datos de 30 días.

7. Memoria (el riesgo real del VPS de 4 GB) ​

oasis-hub arranca con mem_limit generoso (OASIS_HUB_MEM_LIMIT, 1536m) y NODE_OPTIONS=--max-old-space-size ≈ 0,7·límite. A las 24 h: docker stats → mem_limit = ceil(1,5 · pico) (mín. 768m). Presupuesto: pub + mem_limit + 128m (nginx) + ~150m (caddy) + ~100m (panel) ≤ 3 GB. Si no cabe, el orden de rebajas es ssbLogStream.limit → hops 3→2 → nunca el pub (con hops 2 /c se queda sin habitantes salvo que el pub siga directamente a gente con Clearnet). El pub tiene mem_limit desde WP-O121 (OASIS_PUB_MEM_LIMIT, 1024m). Todo mem_limit lleva memswap_limit igual (D-O41, WP-O148): sin él Docker concede el doble en swap y el presupuesto no vale. El swap del host (2 GB en el disco raíz, swappiness 10; CAPACIDAD.md §2) absorbe los picos que no caben; un nodo que vive en swap es una señal, no una solución. Cambiar un límite en caliente: docker update --memory X --memory-swap X más el valor en .env.prod para el próximo recreate (ECOIN §7); nunca un up solo por esto.

8. Rollback ​

  • Nivel 1 (< 2 min, sin tocar el pub): restaurar caddy/Caddyfile.bak-hub-<fecha> in place
    • caddy validate + caddy reload → /c deja de existir públicamente. Opcional $C stop oasis-hub hub-cache.
  • Nivel 2 (retirar): $C rm -sf oasis-hub hub-cache; restaurar docker-compose.pub.yml.bak-hub-<fecha>; /srv/oasis/oasis-hub se conserva (identidad reutilizable) o se borra si se abandona la cuenta. El contact del pub hacia el HUB queda en su log (append-only); opcional unfollow.
  • El .ssb del pub no está montado en ningún servicio nuevo: ningún rollback lo toca.

9. Realidad del VPS — leer antes de tocar ​

  • Layout vivo /opt/oasis-scriptorium/OASIS_PUB (pre-refactor, sin git); .env.prod solo allí. host.env lleva ya esa ruta (el layout medido, no el canónico): los scripts de devops/ no necesitan ningún export previo. Qué ficheros del host se apartan del repo lo dice deploy-status.sh, bloque «Deriva» (UPGRADE-PROTOCOL.md §0.3).
  • Dentro de la imagen viva el árbol también es pre-refactor: ssb-admin.js está en /app/OASIS_PUB/tools/ssb-admin.js, no en /app/pub/tools/… (MODULE_NOT_FOUND en el deploy del 2026-09-13). Es la ruta que usa el scripts/whoami.sh vivo. Cuando se migre el layout y se reconstruya la imagen, volverá a /app/pub/tools/.
  • El compose vivo tiene el mount del Teatro añadido a mano (TEATRO-PROTOCOL.md §6): subir el compose del repo diffeado contra el vivo, con backup *.bak-hub-<fecha>.
  • site/scriptorium/index.html vivo lleva dos valores propios: fusionar, no pisar.
  • Caddy frontea 6 vhosts: tras el reload, healthz de los 5 ajenos.
  • su es PID 1 en la imagen: SIGKILL a ~2 s del stop; afecta también al HUB (los índices de flume se reconstruyen solos).

10. Registro ​

Activación 2026-09-13 (registro). Journal: deploy-log.sh … --version 1.0.8 --cycle 6 --mode server+hub a las 20:02:48 UTC (gitSha 7ed4641). Línea base de disco (devops/logs/hub-disk.jsonl, no versionado): {"ts":"2026-09-13T20:01:43Z","srvOasisPct":15,"rootPct":49,"hubFlumeBytes":211139,"hubBlobsBytes":0,"hubCacheBytes":92927,"hubMemMiB":112}. Memoria al cierre: HUB 113 MiB / 1,5 GiB · pub 88 MiB · Caddy 74 MiB · nginx 5 MiB. Feed del pub sin cambios (contact +1 hacia el HUB). Pub StartedAt 2026-09-12 17:02 UTC antes y después: no se reinició. Backups en el VPS con sufijo .bak-hub-2026-09-13 (compose, .env.prod, Caddyfile, tools/hub-conn-fix.js (retirado en WP-O143: subsumido en pub-link.js --forget-seeded/--remember/--connect), site/index.html, site/scriptorium/index.html) y config/hub/ssb-config.bak-hub-2026-09-13-seeds. Reporte: plan/REPORTES/WP-O46-hub-nodo-soporte.md. Pendiente: revisión adversarial con la contraevidencia de dosier/05 · merge a main.

Desviaciones del plan durante el deploy (todas corregidas en la rama y en v2.md): ruta viva de ssb-admin.js en la imagen (/app/OASIS_PUB/tools/, §9) · seeds retirado del ssb-config (bloqueaba el invite con alreadyFederated) · hub-conn-fix.js (hoy subsumido en pub-link.js --forget-seeded, WP-O143) filtraba la entrada con seed por host (7ed4641) · el contador de contact del pub con grep -c no se mueve en un log binario (usar grep -a -o | wc -l o buscar "contact":"<HUB_ID>") · el site se subió fichero a fichero con merge a tres bandas en vez de deploy-site.sh (riesgo 10 del plan).

Upgrade 2026-09-19 (registro). 1.1.2 → 1.1.4 (WP-O106): estado migrado por upstream a ssb-data/oasis/**, private = 0, feed id igual. La cuenta pasa a llamarse clearnet.escrivivir.co (§12; segundo about del feed). Journal --version 1.1.4 --mode server+hub+wallet.

Cada upgrade posterior: §5 completo + una línea del journal con --mode server+hub. Cada semana: §6 status --json. Riesgos abiertos heredados del plan (v2.md «Riesgos y decisiones abiertas»): robots.txt para /c/blob/, 200 que parecen 404 (upstream, WP-O83), Cache-Control en los HTML de /c, indexing gate cacheado 60 s tras rebuild.

11. Serie de bots de soporte ​

Un pub delega funciones en bots: cada uno es una cuenta SSB propia, con su .ssb en el volumen de datos (<datos>/<servicio>/), en contenedor propio (D-O13), que redime un invite del pub (§3), se declara en su about y no se lista a sí mismo. El pub no se toca para añadir uno.

  • Nombres: convención en AGENTES.md §5 (D-O20, que supera la forma de D-O14): forma libre, corta, que diga tipo y pub; la cadena tipo → piel → pub va en la descripción.
  • Cardinal: único en la serie del pub, no se reutiliza. Va en la descripción, no en el nick.
  • Registro: el de cada despliegue vive en su ficha de instancia. El de la casa: INSTANCIA-SCRIPTORIUM.md §3-§4 (bot 1 = este HUB; bot 2 = hub-wallet, cuyo protocolo es ECOIN-PROTOCOL.md y reutiliza el bootstrap de §3 tal cual).
  • Alta de uno nuevo: fila en la ficha de instancia → contenedor propio + estado en el volumen → bootstrap de §3 → about con §12 → transcribir el literal en la ficha.
  • Bot en modo server (sin backend; p. ej. el bot secretaría de plantillas, TEMPLATE-PROTOCOL §4.5): el bootstrap de §3 se hace por el socket, no por HTTP: pub/tools/ssb-probe.js con SSB_ACTION=invite-accept y SSB_INVITE='<host:port:@key~seed>' (precedente UPGRADE U6) en lugar de POST /settings/invite/accept; reiniciar antes de pub-link.js --forget-seeded/--remember/--connect (un nodo server escribe conn.json al parar: recién aceptado el invite está vacío) y reiniciar después; el about, con ssb-admin.js publish-about (§12, variante sin formulario). No hay ventana PUBLIC=false que abrir ni cerrar: nada escucha en HTTP. El feed queda con contact + pub (de invite.accept) + oasisVersion (del reinicio): es lo esperado, no una desviación (medido, WP-O131).
  • Mudanza o baja de un bot (ejercido: el bot nº 3 de la casa, VPS → máquina operadora, WP-O135, D-O33). Un feed SSB no se retira; lo que se muda es la identidad y lo que se retira es su infraestructura. Orden fijo, un comando por paso: (1) foto base de los nodos que se quedan (GATE_NODES sin el que se va: check no sabe comparar una base con un nodo que ya no existe) y pub-feed-seq.sh <feed> (lo que el pub tiene de él); (2) parar el origen (compose --profile X stop <svc>; desde aquí no vuelve a arrancar); (3) copiar ssb-data entero (secret, db2/, blobs/, blobs_push/, ebt/, conn.json ygossip.json, oasis/**) más lo que montara aparte (assets, logs), con sha256 de cada fichero en origen y verificación en destino; (4) leer el log copiado en frío (pub/tools/log-bipf.js en un docker run --rm --entrypoint sh -v …:ro): S = el seq del pub, D 0, tipos iguales a la foto; si no, no se arranca; (5) arrancar el destino con la misma versión o superior (si cambia, publica un oasisVersion: PERMISO) y comprobar que el pub acepta el seq nuevo como continuación (pub-feed-seq.sh); conn.remember del pub por nombre (pub-link.js --remember, 0 publicaciones); (6) retirar el origen: compose --profile X rm -sf <svc> (nunca --profile X down: tumba todo lo sin perfil), comentar su bloque de env in place, tgz de sus datos con sha256 → copia local → retirar del host; check de los que se quedan = Δ0; journal sin él. Baja definitiva = los mismos pasos sin el (5): el archivo guarda secret + keyring + log completo y la reactivación exige ese log entero (RECOVERY §4). Lo que no se hace: unfollow del pub (contact), about de despedida, tombstones por «limpiar» (cada uno es un mensaje; si se decide, TEMPLATE §9).

12. Poner o cambiar el nombre de un bot (about) ​

Vale para cualquier bot de la serie. Irreversible (AGENTES.md §3): el about es append-only; un cambio publica otro about y el anterior queda en el log. Gana el último que firma el propio feed. El feed id no cambia.

Precondiciones: texto literal (name + description) aprobado por el custodio y escrito en la ficha de instancia como propuesto; nombre dentro de los máximos de AGENTES.md §5; descripción < 6 KB y sin ángulos ni etiquetas (el saneado los elimina; el formulario no comprueba el límite de mensaje SSB).

  1. Ventana no pública. El formulario de perfil no existe en modo público. Sin tocar el .env del host: el entorno del shell manda sobre --env-file, así que basta OASIS_HUB_PUBLIC=false $C up -d --no-deps oasis-hub (o OASIS_WALLET_BOT_PUBLIC=false con oasis-wallet-bot); el paso 5 es el mismo up sin la variable. Esperar a que GET /profile/edit dé 200 desde el loopback antes de publicar. En un bot con ruta pública, durante la ventana el edge sigue sirviendo lo que tenga en caché; mantenerla corta.

  2. Contar antes los mensajes del propio feed, por tipo. Un bot con hops > 0 replica los about de media red: contar "type":"about" a secas no sirve (corrección del 2026-09-19, WP-O106). El autor va antes del contenido en cada registro:

    bash
    bash devops/scripts/upgrade-gates.sh --remote snapshot <etiqueta>     # mensajes propios por tipo, en flume o en db2

    (Con Oasis ≥ 1.2 el log es db2/log.bipf, binario: un grep sobre flume/log.offset da 0 sin error, porque ese fichero es ya la guarda de la migración.)

  3. Publicar, una vez, desde el loopback del contenedor, multipart, sin ningún campo vis_* (el handler los reconstruye todos: sin ellos quedan en falso, que es lo querido en un bot) y con Host y el host del Referer idénticos:

    bash
    docker exec <contenedor> curl -s -o /dev/null -w '%{http_code}\n' \
      -H 'Host: localhost:3000' -H 'Referer: http://localhost:3000/profile/edit' \
      -F 'name=<nombre>' -F 'description=</tmp/desc.txt' \
      http://localhost:3000/profile/edit

    La descripción viaja en un fichero (-F 'campo=</ruta' lee su contenido): una línea, UTF-8, sin salto final; entra al contenedor por stdin (printf '%s' "$DESC" | docker exec -i <ctr> sh -c 'cat > /tmp/desc.txt') y se borra después. Así no hay comillas ni acentos que escapar a través de ssh y docker exec. No con docker cp desde una ruta del host: en Git Bash /tmp/desc.txt es C:\tmp, el cp falla y el $(cat …) queda vacío: en el drill de WP-O131 un about salió sin descripción por eso. Antes del paso 4, leer el mensaje publicado y comprobar que description no está vacía.

    Con imagen (avatar): el mismo formulario admite -F 'image=@/tmp/avatar.png;type=image/png' (backend.js:6299-6303 → publishProfileEdit, main_models.js:1967-1988: sube el blob y lo pone en el mismo about). Copiar el PNG al contenedor con docker cp y borrarlo después. Sin formulario (bot en modo server, HUB §11): node /app/pub/tools/ssb-admin.js publish-about '<nombre>' '<descripción>' --image /tmp/avatar.png desde cd /app/src/server como oasis; publica un único about con name, description e image, sin deviceSource ni visibilityPrefs. Solo png/jpg/webp (/c/blob no sirve SVG). El blob id es &<base64(sha256 del fichero)>.sha256: se calcula antes y se compara después.

    Bot de cartera (tiene dirección ECOin publicada): añadir -F 'vis_wallet=on'. Sin ese campo, y mientras el bot no esté anunciado como pub de RBU, los clientes no pueden usar su dirección para pagarle o donarle desde la UI (sharedEcoAddress, backend.js). No publica una segunda dirección: esa rama comprueba antes hasPublishedAddress (ensayado en local el 2026-09-19: +1 about, wallet sigue en 1). El resto de vis_* se quedan sin enviar.

    Esperado: 302. Un 403 o 400 = Host/Referer; un 500 = descripción demasiado larga. No reintentar a ciegas: primero el paso 4, por si el mensaje sí salió.

  4. Contar después: exactamente +1 about, con el name nuevo, y el mismo número de mensajes wallet que antes. Si hay +2, se reporta; no se arregla publicando más.

  5. Volver a público: *_PUBLIC=true + up -d --no-deps <servicio> + el gate de modo público del protocolo del bot (HUB §4; ECOIN §3 paso 10). Cualquier 200 donde se espera 302 = parar.

  6. Ver el nombre. nameCache es memoria de cada proceso: el propio bot lo muestra tras el reinicio del paso 5; otro nodo (un cliente) lo verá cuando replique el mensaje y, si ya tenía el nombre viejo en caché, tras reiniciarse.

  7. Registrar: en la ficha de instancia, el literal pasa de propuesto a publicado con fecha, y el nombre anterior a «nombres anteriores».

13. Snapshot del pub (Oasis ≥ 1.2) ​

Un cliente que acepta un invite le pide al pub, por RPC, un fichero con el historial empaquetado y lo ingiere de golpe en vez de replicar mensaje a mensaje. Es un acelerador del primer arranque: si el pub responde «not available», el cliente replica como siempre.

Por qué es una pieza del fork. Upstream da por hecho que el pub es sbot + backend público sobre el mismo .ssb: el backend construye el fichero y el sbot lo sirve. Aquí el pub es solo sbot (§1) y los backends públicos son otros nodos: sin hacer nada, el pub serviría un fichero que nadie le construye y el HUB y los bots construirían, cada 6 h y cargando su log entero en memoria, ficheros que nadie pide. Decisión D-O27:

NodoQué haceCómo
pubconstruye y sirvepub/tools/snapshot-build.js dentro de su contenedor, lanzado por su propio entrypoint cada OASIS_PUB_SNAPSHOT_HOURS horas. Lee el log por el socket; no arranca un backend, no publica
HUB y botsni construyen ni arrancan desde unoOASIS_SNAPSHOT=off en el compose (interruptor del fork en backend.js)

Qué lleva el fichero. Cabecera OASISSN1 y, en gzip, un registro de metadatos y uno por mensaje del log del pub, tal cual están (los privados, cifrados). Nada más: el formato admite además registros de blob y de fichero de estado que el cliente escribe en su .ssb sin validar, y el snapshot del pub no los lleva nunca. Cada mensaje lleva su firma y el cliente la valida. Solo el nivel completo (snapshot.oasissn): el «reciente» de upstream pesa casi lo mismo y el cliente bajaría dos veces.

A quién lo sirve. Al propio nodo y a quien el pub sigue; a cualquier otro, not allowed. El pub sigue a quien redime un invite suyo. No sale por HTTP.

bash
bash devops/scripts/pub-snapshot.sh status          # fichero, tamaño, edad, mensajes · solo lectura
bash devops/scripts/pub-snapshot.sh build           # (re)construye ahora · escribe en el .ssb del pub
bash devops/scripts/pub-snapshot.sh off --yes       # lo retira: el pub vuelve a decir «not available»

Quién lo reconstruye. El propio contenedor del pub, como en upstream lo hace el propio pub. Su entrypoint, en modo server, lanza un proceso de fondo que llama a snapshot-build.js a los dos minutos de arrancar y después cada OASIS_PUB_SNAPSHOT_HOURS horas. El compose lo deja en 6; 0 es «no ofrecerlo». No hay nada que instalar en el host: ni cron ni systemd. Quien despliega un pub con este compose ya lo ofrece. Si una construcción falla (el sbot aún arrancando o migrando), reintenta a los diez minutos. Cada pasada deja una línea [snapshot] {…} en el log del contenedor.

  • Activarlo o retirarlo en un host vivo es cambiar OASIS_PUB_SNAPSHOT_HOURS en el env del pub y recrearlo (misma versión: no publica). Ofrecer un servicio nuevo desde el pub pide GO. Después, upgrade-gates.sh --remote check <foto> debe dar «sin publicaciones».
  • Techo: SNAPSHOT_MAX_MB (1024 por defecto). Por encima no se publica y queda el anterior; upstream rechaza en el cliente más de 2 GiB. Crece con el log: CAPACIDAD.md.
  • Memoria: constante (dos pasadas por el log en streaming). Corre dentro del contenedor del pub y cuenta para su mem_limit.
  • Alta de un nodo de soporte nuevo (§3, ECOIN-PROTOCOL.md §3): con OASIS_SNAPSHOT=off el POST /settings/invite/accept no arranca desde el snapshot (medido: el log del nodo no salta).
  • En cada upgrade: el formato y la ruta no son API estable; upgrade-invariants.d/db2.tsv los vigila y el gate US (UPGRADE-PROTOCOL.md §3.4) lo mide.

14. Centralita del pub: Phone y Rooms (Oasis ≥ 1.2.2) ​

Desde 1.2.2 el sbot carga un plugin phone (src/server/phone_module.js): llamadas de voz y salas entre habitantes, cifradas extremo a extremo. Dos nodos que se alcanzan hablan directo. Si no, la llamada pasa por un pub al que ambos están conectados, o por un pub que sigue el llamado: el pub reenvía la señalización (cifrada para el destinatario) y empalma el audio sin poder abrirlo. Una sala vive en el pub al que estaba conectado quien la creó. Ve quién llama a quién y cuándo; no el contenido. No abre puertos ni instala nada: va por las conexiones SSB que el pub ya tiene.

El plugin vive en el sbot, así que un pub que es solo sbot ya hace de centralita al subir de versión. Decisión de esta arquitectura:

NodoQué haceCómo
pubretransmite y aloja salas, acotadoclave phone en su ssb-config (pub/config/ssb/config): { "relay": true, "roomMax": 12 }. Sin relayOpen: solo sirve si sigue a uno de los dos interlocutores (quien redimió un invite suyo)
HUB y botsnadaphone.relay = false en su oasis-config.json; lo fija pub/scripts/regen-node-configs.js. No tienen audio ni puerto SSB público
  • De dónde lee la política: primero la config del sbot (config.phone), que llega por el guard OASIS_SERVER_CONFIG_OVERRIDE; si no existe, el oasis-config.json de la imagen, que trae relay: true y aforo 50. Un pub sin la clave retransmite con los valores de fábrica.
  • Apagarla: "phone": { "relay": false } y recrear el pub (misma versión: no publica). Las llamadas directas siguen; las salas que hayan elegido este pub como sede dejan de admitir gente, porque el pub sigue anunciando el servicio y lo rechaza al entrar.
  • Comprobar lo aplicado: phone.roomInfo({ rid }) es anónima y de solo lectura; devuelve { count, max } y max es el aforo vigente.
  • HUB y bots publican un about al subir a 1.2.2 (medido en local): un backend público anuncia una vez visibilityPrefs.phone = "off" en su propio perfil, con el primer refresco de fondo tras atender peticiones, no en el arranque. Va en el delta declarado del upgrade (UPGRADE-PROTOCOL.md §3.4). El pub, solo sbot, no publica nada.
  • El pub no tiene número ni se le puede llamar: sin dispositivo de audio el plugin ignora los timbres. Lo mismo HUB y bots.
  • Carga: red, no disco. Cifras en CAPACIDAD.md §4. El plugin no limita el número de salas ni de llamadas simultáneas.
  • No ensayable en contenedores: una llamada real pide micrófono y altavoz en los dos extremos (PulseAudio, PipeWire o ALSA). Se prueba con dos clientes de escritorio.
  • En cada upgrade: nada de esto es API estable; upgrade-invariants.d/phone.tsv lo vigila.