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 enhttps://pub.escrivivir.co/cy su puerta es la Sala 04 del Scriptorium (/hub/).
Estado · ACTIVADO 2026-09-13 19:55 UTC en
pub.escrivivir.co(WP-O46, ramawp/O46-hub-nodo-soporte). Cuenta de soporteazofaifo-scriptorium-skin-bot-1=@KM+ZBipR18VSyjNTFjAOnsmz6EiobGYHb3ZCZ4ZxQYI=.ed25519. Journal--mode server+hub20:02 UTC. Plan, dosier y hallazgos de gates y deploy:ARCHIVO/DISCO/oasis-clearweb/v2.md
dosier/; reporteplan/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/csin servicio ~1 min). Upgrade 1.0.8 → 1.1.2 desplegado 2026-09-17 11:47 UTC (WP-O97, ramaupgrade/oasis-1.1.2): pub y HUB en la imagenb6181cb7, ambos feed id sin cambios,/c/assets/*cacheado (§5.5), Sala 04 con 16 tipos. Reporteplan/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, modoserver) 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 contenedoroasis-pub-hub: misma imagen que el pub,command: ["backend"], es decirbackend.js --publiccon el sbot embebido y un.ssbpropio en/srv/oasis/oasis-hub. Delante vaoasis-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ándo | Hito | Dónde queda |
|---|---|---|
| Oasis 1.0.x | Upstream añade el HUB clearnet: /c, modo --public, opt-in por habitante | ./clearnet.md (doc upstream) |
| Ciclo 0.9.6→1.0.8 | Receta 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 activada | retirada; historia al pie de esta tabla |
| 2026-09-13 · v1 | Plan «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 · v2 | Tres 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-F20 | v2.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 serviciooasis-pubdel 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
secretdel HUB nace en/srv/oasis/oasis-hub/ssb-dataen 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/*,/clearnety/assets/{styles,themes,images}/*.OASIS_HUB_PUBLIC=falseexiste solo para el bootstrap del invite y nunca con@hubactivo 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) yoasisVersion(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 conbash devops/scripts/upgrade-gates.sh --remote snapshot <tag>(líneatipos: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.offsetda 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.10notifyBotrespetainboxMutedBotsde la config: en un nodo de soporte van todos silenciados (§2). El invariante vuelve a ser alcanzable: ningún cifrado nuevo, yupgrade-gates.sh checklo comprueba en cada upgrade. - El pub solo publica el
contactdel 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, solodocker 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 ensrc/views/clearnet_view.js(idioma y tema del visor por petición, selector de tema), el sexto deUPGRADE-PROTOCOL.md§2, con invariante enupgrade-invariants.d/hub.tsvy gate enupgrade-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 (suaboutno llevavis_*).- 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
| Pieza | Fichero | Qué fija | Estado |
|---|---|---|---|
Servicios oasis-hub + hub-cache | pub/docker-compose.pub.yml | misma 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ón | pub/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 invite | pub/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 |
| Visor | pub/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é HTTP | pub/config/hub/nginx.conf.template | proxy_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 |
| Edge | pub/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 |
| Variables | pub/.env.vps.example, .env.local.example, .env.example; .env.prod del VPS | OASIS_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 rutas | pub/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 disco | devops/scripts/hub-disk.sh (npm devops:hub-disk) | status · check · prune-blobs · prune-cache · --json (§6) | ⏳ |
| Puerta | pub/site/hub/ (Sala 04) + vestíbulo pub/site/scriptorium/ + Accesos en pub/site/index.html | estático, cero JS, plantilla pub/site-templates/poster | ⏳ |
| Gobierno | plan/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.templateRegla 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/hopsReglas 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)
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 upstreamEn 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 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,depsLos 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ón | Qué mirar | Consecuencia |
|---|---|---|
routes | GET 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 |
headers | cabeceras, cookies, accept-language, ctx.protocol en código que sirve /c | regla 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 |
config | defaults de oasis-config.json, server-config.json | regenerar la copia del HUB (§5.2) |
deps | ssb-conn, ssb-conn-hub, ssb-invite, network_pause.js | pub-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.jsones una copia desrc/configs/oasis-config.jsoncon unas pocas claves fijadas (la lista vigente, en la fila «Visor» de §2). Tras el overlay:node pub/scripts/regen-node-configs.jsla regenera (y la plantilla del bot de cartera) e imprime qué claves difieren del original: deben ser solo las fijadas.--checksale 1 si no está al día. Una clave que upstream retira desaparece sola de la copia: se regenera, no se parchea. La lista deinboxMutedBotssale deINBOX_BOTSensrc/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.jslee la config del nodo en~/.ssb/oasis/oasis-config.jsony la desrc/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 unoasis-config.json.tmp-*en~/.ssb/oasis/, se retira.pub/config/hub/ssb-configreemplaza arrays enteros desrc/configs/server-config.json(mergeDeep): si upstream cambiaconnections.incoming/outgoing, decidir si se replica. Orden de fusión enssb_config.js(guard):~/.ssb/config←server-config.json← el fichero deOASIS_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 abreoutgoing.onion: [{transform: shs}](marcar.onionde 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 diganroutesyheaders. Se renderiza conenvsubstal arrancarhub-cache: tras cambiarla (in place) hay que recrear el contenedor (up -d --no-deps --force-recreate hub-cache);nginx -s reloadno la relee. Antes,nginx -tde la plantilla renderizada en un contenedor desechable.- Rotación de ciclo (
UPGRADE-PROTOCOL.md§5):caps.shstambién vive enpub/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. Elcaps.shsdel 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:
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+hubMientras 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/imagesa/c/assets/images(app.use(mount("/c/assets", assets))nuevo enmiddleware.js). Caddy/c/*ya lo cubría; en nginx caía en lalocationgenérica de/csinproxy_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 upstreamclearnet.mdlista 15, omite bookmarks). Paridad en la Sala 04. /c/qr/:feedIdnuevo (PNG conCache-Control: no-store): entra por/c/*y nginx no lo cachea. Codificahttp://localhost:3000/qr-action/follow/…(QR_ACTION_BASEes 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-toggledesaparece (activar cualquier módulo hace público al habitante; apagarlos todos lo retira). onboarding_model.js: el flagoasis-first-contactsigue; añadeadopt(feedId).SSB_server.js: el detalle técnico de lock/corrupción solo se imprime con--debug/OASIS_DEBUG.- Overlay en Windows: con
core.autocrlflos ficheros nuevos salen CRLF en el árbol de trabajo;git diffnormaliza, pero un reemplazo literal con\nno casa. Detectar el EOL antes de editar.
6. Disco: mantener el tamaño del HUB
Ruta (bajo /srv/oasis/oasis-hub/) | Crece con | Podable | Quié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) | no | friends.hops en ssb-config |
índices: ssb-data/db2/indexes, db2/jit (antes ssb-data/flume/*), ssb-data/ebt/ | el log | sí (rebuild automático) | — |
ssb-data/blobs/ por encima del techo | replicación y visor | sí (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 GET | hub-disk.sh prune-blobs, blobs.max 50 MB (guard del fork) |
http-cache/ | tráfico web | sí | nginx max_size + inactive=7d; prune-cache |
stdout de los contenedores (/var/lib/docker, disco de sistema) | tráfico y replicación | sí | 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-*:
| Comando | Qué hace | Cuándo |
|---|---|---|
status | df -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 df | semanal y antes/después de cualquier deploy |
check | umbrales HUB_DISK_SOFT_PCT=75 / HUB_DISK_HARD_PCT=90 sobre /srv/oasis y / → exit 0/1/2 | deploy-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*.json | soft en /srv/oasis |
prune-cache | vacía http-cache/ + nginx -s reload | tras 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):
/srv/oasis≥ 75 %:status→ ¿qué domina?http-cache/→ bajarHUB_CACHE_MAX_SIZEen.env.prodyup -d --no-deps hub-cache(nginx purga solo hasta el nuevo tope).blobs/→prune-blobs --dry-run, luegoprune-blobs. Después de una poda grande, un reinicio del HUB es más rápido (el entrypoint hacechown -Rdeblobs/en cada arranque).db2/(flume/en ≤ 1.1.x) → el log replicado no se poda. Opciones por orden:friends.hops3→2 enssb-config+up -d --no-deps oasis-hub(acota el crecimiento futuro, no reduce lo replicado) · como último recurso, con el HUB parado, vaciarssb-data/conservandosecret,config,conn.json,gossip.json,oasis-first-contacty dejar que re-replique (identidad y follow intactos;/csirveindexingViewmientras tanto →prune-cacheal terminar).oasis-pub/(el pub, no el HUB) → no es de este protocolo;pub/BACKLOG.md«métricas de disco».
/≥ 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).- ≥ 90 % en cualquiera: hacer lo anterior ahora; si es
/srv/oasis,prune-cacheda 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 placecaddy validate+caddy reload→/cdeja de existir públicamente. Opcional$C stop oasis-hub hub-cache.
- Nivel 2 (retirar):
$C rm -sf oasis-hub hub-cache; restaurardocker-compose.pub.yml.bak-hub-<fecha>;/srv/oasis/oasis-hubse conserva (identidad reutilizable) o se borra si se abandona la cuenta. Elcontactdel pub hacia el HUB queda en su log (append-only); opcionalunfollow. - El
.ssbdel 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.prodsolo allí.host.envlleva ya esa ruta (el layout medido, no el canónico): los scripts dedevops/no necesitan ningúnexportprevio. Qué ficheros del host se apartan del repo lo dicedeploy-status.sh, bloque «Deriva» (UPGRADE-PROTOCOL.md§0.3). - Dentro de la imagen viva el árbol también es pre-refactor:
ssb-admin.jsestá 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 elscripts/whoami.shvivo. 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.htmlvivo lleva dos valores propios: fusionar, no pisar.- Caddy frontea 6 vhosts: tras el
reload,healthzde los 5 ajenos. sues PID 1 en la imagen: SIGKILL a ~2 s delstop; 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 esECOIN-PROTOCOL.mdy 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 →
aboutcon §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.jsconSSB_ACTION=invite-acceptySSB_INVITE='<host:port:@key~seed>'(precedente UPGRADE U6) en lugar dePOST /settings/invite/accept; reiniciar antes depub-link.js --forget-seeded/--remember/--connect(un nodoserverescribeconn.jsonal parar: recién aceptado el invite está vacío) y reiniciar después; elabout, conssb-admin.js publish-about(§12, variante sin formulario). No hay ventanaPUBLIC=falseque abrir ni cerrar: nada escucha en HTTP. El feed queda concontact+pub(deinvite.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_NODESsin el que se va:checkno sabe comparar una base con un nodo que ya no existe) ypub-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) copiarssb-dataentero (secret,db2/,blobs/,blobs_push/,ebt/,conn.jsonygossip.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.jsen undocker 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 unoasisVersion: PERMISO) y comprobar que el pub acepta el seq nuevo como continuación (pub-feed-seq.sh);conn.rememberdel 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;checkde los que se quedan = Δ0; journal sin él. Baja definitiva = los mismos pasos sin el (5): el archivo guardasecret+ keyring + log completo y la reactivación exige ese log entero (RECOVERY §4). Lo que no se hace: unfollow del pub (contact),aboutde 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).
Ventana no pública. El formulario de perfil no existe en modo público. Sin tocar el
.envdel host: el entorno del shell manda sobre--env-file, así que bastaOASIS_HUB_PUBLIC=false $C up -d --no-deps oasis-hub(oOASIS_WALLET_BOT_PUBLIC=falseconoasis-wallet-bot); el paso 5 es el mismoupsin la variable. Esperar a queGET /profile/editdé 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.Contar antes los mensajes del propio feed, por tipo. Un bot con
hops> 0 replica losaboutde 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:bashbash 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: ungrepsobreflume/log.offsetda 0 sin error, porque ese fichero es ya la guarda de la migración.)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 conHosty el host delRefereridénticos:bashdocker 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/editLa 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 ydocker exec. No condocker cpdesde una ruta del host: en Git Bash/tmp/desc.txtesC:\tmp, elcpfalla y el$(cat …)queda vacío: en el drill de WP-O131 unaboutsalió sin descripción por eso. Antes del paso 4, leer el mensaje publicado y comprobar quedescriptionno 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 mismoabout). Copiar el PNG al contenedor condocker cpy borrarlo después. Sin formulario (bot en modoserver, HUB §11):node /app/pub/tools/ssb-admin.js publish-about '<nombre>' '<descripción>' --image /tmp/avatar.pngdesdecd /app/src/servercomooasis; publica un únicoaboutconname,descriptioneimage, sindeviceSourcenivisibilityPrefs. Solo png/jpg/webp (/c/blobno 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 anteshasPublishedAddress(ensayado en local el 2026-09-19: +1about,walletsigue en 1). El resto devis_*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ó.Contar después: exactamente +1
about, con elnamenuevo, y el mismo número de mensajeswalletque antes. Si hay +2, se reporta; no se arregla publicando más.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.Ver el nombre.
nameCachees 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.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:
| Nodo | Qué hace | Cómo |
|---|---|---|
| pub | construye y sirve | pub/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 bots | ni construyen ni arrancan desde uno | OASIS_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 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_HOURSen 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): conOASIS_SNAPSHOT=offelPOST /settings/invite/acceptno 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.tsvlos 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:
| Nodo | Qué hace | Cómo |
|---|---|---|
| pub | retransmite y aloja salas, acotado | clave 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 bots | nada | phone.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 guardOASIS_SERVER_CONFIG_OVERRIDE; si no existe, eloasis-config.jsonde la imagen, que traerelay: truey 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 }ymaxes el aforo vigente. - HUB y bots publican un
aboutal subir a 1.2.2 (medido en local): un backend público anuncia una vezvisibilityPrefs.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.tsvlo vigila.