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 cliente (nodo personal) ​

Este repo es el sitio del cliente (C:\S_LAB\o-sdk, https://github.com/alephscriptorium-eng/O_SDK.git). El cliente se levanta con el compose de la raíz (docker-compose.yml, proyecto o-sdk, servicio oasis-client); el pub, el HUB y el entorno scriptorium viven en pub/ (proyecto oasis-pub-scriptorium) y no comparten puertos, nombres, redes ni directorios de volumes-dev/ con el cliente (ver §0).

Estado. El protocolo se ha ejercido de punta a punta: alta e importación de una identidad existente (WP-O98, 2026-09-17), cartera (WP-O103, WP-O108), subida a 1.1.10 en el ensayo con identidad desechable (WP-O113) y subida del cliente real de 1.1.4 a 1.2.2, con migración del log a db2 (WP-O124, 2026-10-05). Los datos de esas ejecuciones —feed, secuencias, rutas, copias— están en sus reportes (plan/REPORTES/), no aquí. Qué se puede encargar hoy a un agente y con qué garantías: puerta Cliente Oasis.

Checklist operativo para dar de alta un cliente fresco, traer una identidad SSB existente sin bifurcar su feed, sincronizarlo con el pub, subirlo de versión, verificarlo y volver atrás. Deriva de UPGRADE-PROTOCOL.md (misma imagen, tres modos) y de RECOVERY-PROTOCOL.md §4 (regla de oro), y del primer alta+importación hecha en este repo (WP-O98, 2026-09-17).

Modelo mental. Solo hay una cosa irremplazable: el secret. Y una cosa que conviene no perder: tu propio log (db2/log.bipf desde Oasis 1.2; antes flume/log.offset), porque lo que el pub no tenga de tu feed no existe. Todo lo demás (índices, ebt/, conn.json, config, blobs) es derivable. Oasis no tiene guardia contra la bifurcación del feed: si la GUI arranca con tu secret y un log vacío, publica (en 1.1.2, el PM de bienvenida a los 3 s) un mensaje con sequence: 1 → fork irreversible frente a lo que guarda el pub. Por eso importar = ficheros con el cliente parado + sbot puro hasta sincronizar + GUI al final.

0. Estado y convivencia con el pub local ​

bash
docker compose -p o-sdk ps -a                    # cliente: oasis-client (+ ecoin-wallet con el perfil ecoin, §8)
docker compose -p oasis-pub-scriptorium ps -a    # pub+HUB local: no debe ocupar 3000/8008 (usa 8009/8088/8443/8788 con pub/.env.local)
ls volumes-dev/                                  # cliente: ssb-data ai-models logs client-state · pub: oasis-pub oasis-hub teatro
docker volume inspect o-sdk_oasis-ssb-data-dev --format '{{index .Options "device"}}'   # = <repo>\volumes-dev\ssb-data (o no existe aún)
docker volume ls --filter label=o-sdk.role=client-wallet   # o-sdk-client-ecoin-data: la wallet.dat del cliente (§8), si se creó
ClientePub + HUB (local)
Compose / proyectodocker-compose.yml · o-sdkpub/docker-compose.pub.yml · oasis-pub-scriptorium
Contenedoresoasis-client, ecoin-walletoasis-pub-scriptorium, oasis-pub-hub, oasis-pub-hub-cache, oasis-pub-web, oasis-pub-panel-api
Puertos host3000, 8008 (ecoin-wallet no publica ninguno: RPC 7474 y P2P 7408 solo en la red del compose, §8)con pub/.env.local: 8009, 8088, 8443, 8788, 3001 (maint-ui)
volumes-dev/ssb-data, ai-models, logs, client-state (la wallet.dat no vive aquí: volumen externo o-sdk-client-ecoin-data, §8)oasis-pub/*, oasis-hub/*, teatro
Redo-sdk_oasis-networkoasis-pub-scriptorium_oasis_pub_net
Imageno-sdk-oasis-client (mismo Dockerfile)oasis-pub-scriptorium:latest

Regla: el pub local siempre con npm run pub:local:* (--env-file .env.local). Sin --env-file el compose del pub toma los defaults del VPS (8008/80/443) y choca con el cliente y con Windows.

1. Alta fresca (identidad nueva) ​

bash
npm run build                    # imagen o-sdk-oasis-client (10-20 min la primera vez)
npm run setup                    # volumes-dev/{ssb-data,ai-models,logs,client-state/banking}; sin esto el bind falla
# Modelo IA (4,08 GB): tres opciones
#   a) copiar uno existente a volumes-dev/ai-models/oasis-42-1-chat.Q4_K_M.gguf (head -c4 = GGUF; sha256 igual)
#   b) dejar que el entrypoint lo descargue (3,8 GB, solarnethub.com) en el primer arranque
#   c) sin IA: OASIS_SKIP_AI_MODEL=true en un docker-compose.override.yml no versionado
docker compose up -d oasis-client
docker compose logs -f oasis-client   # parches 3/3 ✓ · [Version: X.Y.Z] · secret creado si no existía

Qué hace el primer arranque: el entrypoint (root) crea y chowna .ssb, models, logs; escribe ~/.ssb/config si falta (cap aleatorio que src/configs/server-config.json pisa: el cap de red es el del repo); aplica 3 parches a node_modules; backend.js levanta el sbot embebido, que crea secret si no existe, la GUI en :3000 y (bajo demanda) la IA en :4001. La GUI publica el PM de bienvenida y crea oasis-first-contact.

Healthcheck del alta: §5. El primer día: npm run client:backup-keys y guarda la copia fuera de la máquina. npm run up = setup + up -d: no lo uses durante una importación (§2).

2. Importar una identidad existente ​

Con el cliente parado (docker compose stop oasis-client) y volumes-dev/ssb-data vacío (o --force, que lo aparta a ssb-data.pre-import-<ts> sin borrar nada):

bash
npm run client:import-identity -- --from "C:/ruta/al/.ssb/viejo" --with-blobs --dry-run   # verifica sin copiar
npm run client:import-identity -- --from "C:/ruta/al/.ssb/viejo" --with-blobs [--force]
Se copia (lista blanca)Por qué
secretla identidad
el log: db2/log.bipf con su guarda flume/log.offset (origen en Oasis ≥ 1.2) o flume/log.offset (origen anterior)tu feed completo (y el de tus pares): elimina la ventana de fork por construcción. Un log flume lo migra a db2 el primer arranque, y esa migración no tiene vuelta: la versión anterior ya no arranca sobre esa carpeta
gossip.jsondirecciones/claves de pubs conocidos: el cliente vuelve a encontrar al pub solo
keys/claves de tribus (no derivables)
blobs/ (opcional)avatares/adjuntos; content-addressed, re-descargables
No se copiaPor qué
flume/* y db2/{indexes,jit} (índices), ebt/, blobs_push/vistas derivadas; las de otra versión hacen que el arranque muera con «Another Oasis instance is already running» (isLockError trata cualquier OpenError de leveldb como lock) o isCorruptStoreError
conn.jsonse regenera desde gossip.json; corrupto bloquea conexiones
configlo escribe el entrypoint; server-config.json manda (caps, connections, hops)
socket, manifest.json, node_modules, *.nul-damaged-bakresiduos

El script además: verifica el origen (secret con id = @public, sin bytes NUL; log.offset por frames con client/scripts/lib/inspect-log-offset.js → tailOk, badFrames: 0, mySeq), guarda un backup verificado en devops/backups/client/<ts>/ (sha256), copia, crea oasis-first-contact = <feed>\n<ISO>\nwelcome=done\n (formato onboarding_model.adopt; apaga el PM de bienvenida y el banner), verifica sha256 y frames tras la copia (y una muestra de blobs por hash) y deja el manifiesto volumes-dev/ssb-data/.import-<ts>.txt.

Después no arranques la GUI: §3.

Solo secret (sin log): válido si no tienes el log (RECOVERY-PROTOCOL.md modelo mental: el pub guarda tu feed). El script exige log.offset; para ese caso copia el secret a mano, crea el flag igual y ve a §3: el sbot puro traerá tu feed del pub. Lo que el pub no tenga se pierde.

3. Sincronizar con el pub antes de la GUI (sbot puro) ​

bash
npm run client:sync-only -- start              # modo `server` del entrypoint: solo sbot, nada publica
docker logs -f oasis-sync-only                 # CONNECTED net:pub.escrivivir.co:8008 · sin "Another Oasis"/"corrupt"
npm run client:sync-only -- status --pub --watch   # cada 30 s hasta SYNC-OK
npm run client:sync-only -- stop               # exige log.offset estable 60 s; verifica el frame final
docker compose up -d oasis-client              # ahora sí. (El propio sbot puro ya publicó `oasisVersion` en seq N+1 a los 7 s:
                                               #  SSB_server.js solo lo hace si el log NO está vacío; el pub lo acepta como continuación)

start tiene un gate: si el sbot no se identifica como el feed del secret (ID-MISMATCH), se para solo (habría creado otra identidad sobre un montaje equivocado). status compara el seq propio según el fichero, según el sbot y según el pub (devops/scripts/pub-feed-seq.sh, solo lectura):

VeredictoSignificaQué hacer
SYNC-OKseq local = seq pub, log.offset estable ≥ 5 min, pub visto conectadostop y arrancar la GUI
AHEADlocal > pub (publicaste offline)normal: el sbot empuja al pub (EBT es bidireccional); repetir
BEHINDlocal < pubno arrancar la GUI; esperar; si no avanza en 10 min docker restart oasis-sync-only
PUB-UNKNOWNel pub no tiene el feed ni lo siguenpm run devops:invite -- 1 y SSB_INVITE='…' npm run client:sync-only -- invite (redime desde el sbot puro; publica un contact, legítimo porque local ≥ pub). Nunca join-prod-client.sh (necesita GUI)
ID-MISMATCHel sbot no es tu feedparar; revisar docker volume inspect o-sdk_oasis-ssb-data-dev

La parada acaba en SIGKILL (PID 1 del contenedor es su): por eso stop exige estabilidad y comprueba el último frame con inspect-log-offset.js. log.offset es append-only: un kill sin escritura en curso es inocuo.

4. Upgrade del cliente ​

Misma imagen y mismo ciclo que el pub: UPGRADE-PROTOCOL.md §1-§3 (rama upgrade/oasis-X.Y.Z, overlay, guards). En el cliente hay dos casos: el upgrade ordinario entre versiones con el mismo motor (≥ 1.2 → ≥ 1.2.4; el primero del cliente real es 1.2.2 → 1.3.0, WP-O145) y el salto de motor (≤ 1.1.x → ≥ 1.2), ya pasado en el cliente real (WP-O124) y descrito aparte en §4.4.

4.1 Dónde vive la config desde Oasis 1.2.4 ​

  • La config viva es ~/.ssb/oasis/oasis-config.json (en el host, volumes-dev/ssb-data/oasis/oasis-config.json: dentro del bind de ssb-data, persiste sola). Desde 1.2.4 config-manager.js la lee y la escribe ahí; de src/configs/oasis-config.json solo copia el contenido una vez (si la viva no existe) y después lo ignora. Su saveConfig escribe con rename: un symlink en el sitio nuevo se rompería y un bind de fichero fallaría. Por eso la viva es un fichero regular y no se enlaza nada.
  • Qué hace el entrypoint (persist_client_state; corre si OASIS_CLIENT_STATE_DIR está definida, en todos los modos, también server para sync-only.sh), antes de que Oasis arranque:
    • si la viva no existe, la siembra = default de la imagen + deep-merge del legado client-state/oasis-config.json (gana el legado). Así la copia «una sola vez» de upstream nunca llega a hacerse con el default pelado de la imagen;
    • si la viva existe, funde el default de la imagen con ella (gana lo vivo): las claves nuevas de upstream entran, lo que la GUI guardó se conserva;
    • deja la viva en modo 600; src/configs/oasis-config.json queda como default de la imagen (si quedó un enlace de un entrypoint anterior, se repone el default);
    • el legado client-state/oasis-config.json es de solo lectura: sembró una vez y no se vuelve a tocar (se retira en un WP posterior).
  • En los modos con backend (full/client/backend) OASIS_CONFIG_FILE pasa a la viva: wire_wallet_config («el entorno manda»: ECOIN_RPC_URL/USER/PASS, OASIS_WALLET_FEE, OASIS_WALLET_WIRING=manual) y setup_oasis_config (modules.aiMod según modelo) escriben ahí en cada arranque. Vaciar ECOIN_RPC_URL en el .env y recrear sigue dejando de usar la cartera (§8.8).
  • HUB y bot no cambian: su bind :ro ya está en el sitio nuevo; las escrituras siguen tolerándose.
  • Secreto. Las credenciales RPC (wallet.user/pass) viven ahora en ssb-data/oasis/oasis-config.json (600): viajan con cualquier copia de ssb-data. El backup de la GUI las excluye (NOT_BACKED_UP en src/models/backup_model.js).

Antes de este diseño el entrypoint enlazaba src/configs/oasis-config.json → /app/state/oasis-config.json: en ≥ 1.2.4 ese enlace se leía una sola vez y la GUI escribía en otro sitio, dos verdades (el drill lo mostró: language «es» en .ssb/oasis y «en» en client-state).

4.2 Receta (upgrade ordinario) ​

bash
docker tag o-sdk-oasis-client:latest o-sdk-oasis-client:<ver-vieja>   # rollback preparado. Crear ANTES del build: hoy no existe la etiqueta 1.2.2
docker compose stop oasis-client                                       # copia en frío con el cliente PARADO:
cp -a volumes-dev/ssb-data     volumes-dev/ssb-data.pre-<ver-nueva>-<ts>      #   lleva la config viva (600: material sensible; volumes-dev/ está ignorado)
cp -a volumes-dev/client-state volumes-dev/client-state.pre-<ver-nueva>-<ts>  #   el legado
npm run build && docker compose up -d oasis-client                     # .ssb, ai-models y client-state son binds: se preservan
docker compose logs oasis-client | grep -A2 'Persistiendo estado'      # «config viva: default de la imagen + lo vivo (gana lo vivo)»; la 1.ª vez bajo ≥ 1.2.4: «SEMBRADA … + el legado»
  • Con el log intacto la GUI arranca directa: su oasisVersion cae en seq N+1.
  • Si el upgrade obligó a apartar los índices (db2/indexes y db2/jit): Settings › Rebuild database (POST /settings/rebuild, no toca el log). Si se apartó el log entero, entonces §3 antes de la GUI.
  • La config viva y el estado bancario (ssb-data/oasis/, §8.4) sobreviven al rebuild; las claves nuevas que traiga upstream en oasis-config.json entran por la fusión del default de la imagen.
  • Phone y Rooms (Oasis ≥ 1.2.2) no funcionan en el cliente en contenedor: /phone responde «not available on this device» porque el contenedor no tiene acceso al audio de la máquina.
  • Healthcheck: §5 (con npm run client:ecoin:verify: V4 lee la viva, V5 comprueba su sitio). Journal: no aplica (el journal es del pub).

4.3 Delta declarado y cómo medirlo ​

Como en el pub (UPGRADE-PROTOCOL.md, upgrade-gates.sh): foto por tipo antes y después, y la suma de los Δ por tipo tiene que cuadrar con el Δ de mySeq; si no cuadra, la medida no vale. Delta esperado al subir el cliente real de 1.2.2 a 1.3.0:

TipoΔPor qué
oasisVersion+1SSB_server.js lo publica al arrancar con el log no vacío
about+1clearnetSince: syncClearnetSince a los 3 s del arranque, porque las preferencias de visibilidad publicadas no lo llevan. Otro about por phone solo si la config llegase con visibility distinta de whole (la viva la trae: no se espera)
karmaScore+0..1
wallet0el mapa de direcciones ya está presente (§8.2)
pub-invites0pub: false
bash
# ANTES (cliente parado, sobre el bind) y DESPUÉS (en caliente, dentro del contenedor): mismos tipos
for t in oasisVersion about karmaScore wallet pub-invites; do node client/scripts/lib/count-feed-type.js volumes-dev/ssb-data "$t"; done
npm run client:inspect-log -- '<tu-feed>'                                                   # tailOk, mySeq
for t in oasisVersion about karmaScore wallet pub-invites; do docker exec -i oasis-client node - /home/oasis/.ssb "$t" < client/scripts/lib/count-feed-type.js; done
npm run devops:pub-feed-seq -- '<tu-feed>'                                                  # el pub recibe el seq nuevo

Medido en el cliente real el 2026-10-15 (1.2.2 → 1.3.0): mySeq 111 → 114 = oasisVersion+1 about+1 karmaScore+1, el about con visibilityPrefs.clearnetSince; nada más (reporte WP-O145).

Cualquier otro tipo con Δ ≠ 0, o un Δ mayor que el declarado, es desviación: parada y reporte (no se «arregla» publicando nada). El reporte del WP lleva las dos fotos.

4.4 Salto de motor (≤ 1.1.x → ≥ 1.2), ya pasado ​

El primer arranque migra el log a db2/ y borra el viejo: no hay vuelta por retag. Antes: copia en frío de volumes-dev/ssb-data y client-state con el cliente parado, y ensayo de la migración sobre una copia del log sin el secret, en un contenedor sin red (PUB/UPGRADE-PROTOCOL.md §0.5 y §4, «ensayo de migración»). Con identidad real, GO del custodio. Si quedaron índices de flume/, Settings › Rebuild database. El cliente real lo pasó en WP-O124 (1.1.4 → 1.2.2); desde entonces todo upgrade es ordinario (§4.2) y el rollback por retag vale (§6).

5. Healthcheck ​

bash
docker ps --filter name=oasis-client --format '{{.Status}}'                       # healthy
curl -s http://localhost:3000/settings | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1   # versión
grep -o '"id": *"[^"]*"' volumes-dev/ssb-data/secret                             # = tu feed
npm run client:inspect-log -- '<tu-feed>'                                         # tailOk, mySeq
npm run devops:pub-feed-seq -- '<tu-feed>'                                        # seq_pub ≤ seq local (+ oasisVersion)
curl -s -X POST -H 'Referer: http://localhost:3000/settings' http://localhost:3000/settings/verify -o /dev/null && \
  curl -s http://localhost:3000/settings | grep -i -A3 'verification'             # sin gaps / broken links / forks (mine)
docker compose logs oasis-client | grep -c 'welcome-pm'                           # 0 tras una importación
npm run client:test-ai                                                            # POST /ai dentro del contenedor
test -f volumes-dev/ssb-data/oasis/oasis-config.json && \
  docker exec oasis-client stat -c '%a %F' /home/oasis/.ssb/oasis/oasis-config.json   # config viva presente: «600 regular file» (§4.1)
npm run client:ecoin:verify                                                       # V4 lee la viva · V5: regular a 600, src/configs sin enlace, legado presente (§8.3)

Perfil sin nombre/avatar tras importar no es pérdida de identidad: los about son mensajes del log; llegan con la replicación.

6. Rollback ​

  • Upgrade fallido: docker compose stop oasis-client → docker tag o-sdk-oasis-client:<ver-vieja> o-sdk-oasis-client → docker compose up -d --no-build oasis-client. .ssb intacto (binds). Salvo si el upgrade cambió de motor (≤ 1.1.x → ≥ 1.2): la imagen vieja no arranca sobre un log migrado; el rollback es restaurar la copia en frío de §4.4, no el retag.
  • Salvedad de la config al volver de ≥ 1.2.4 a ≤ 1.2.3 (p. ej. 1.3.0 → 1.2.2 por retag): la imagen vieja vuelve a leer src/configs → /app/state (el legado, §8.4), así que lo que la GUI guardó bajo la versión nueva (está en la viva, ssb-data/oasis/oasis-config.json) no se ve hasta volver a subir; la viva queda intacta en el bind y la siguiente subida la encuentra tal cual. El .env sigue mandando en los dos sitios. Entre dos versiones ≥ 1.2.4 las dos leen la viva: sin salvedad.
  • Importación fallida (log corrupto al arrancar, ID-MISMATCH): sync-only stop --now; volver a volumes-dev/ssb-data.pre-import-<ts> (si lo había) o re-importar desde el backup devops/backups/client/<ts>/; en último caso solo secret + flag + §3.
  • Nunca truncar el log (db2/log.bipf; flume/log.offset en ≤ 1.1.x) ni tocar secret. Ningún camino de este protocolo cambia el secret.

7. Retirar instalaciones antiguas ​

Solo tras §5 en verde unos días y con backup verificado fuera de la máquina:

bash
docker rm oasis-server-dev                                   # contenedor del cliente antiguo (otro repo)
docker rmi alephscript-clean-oasis-dev                       # su imagen (4,36 GB)
docker volume rm alephscript-clean_oasis-ssb-data-dev alephscript-clean_oasis-ai-models-dev alephscript-clean_oasis-logs-dev \
                 blockchaincomport_oasis-ssb-data-dev blockchaincomport_oasis-ai-models-dev blockchaincomport_oasis-logs-dev
docker network rm alephscript-clean_oasis-network blockchaincomport_oasis-network

Los volúmenes con nombre son punteros a directorios bind: borrarlos no borra los datos. El directorio físico de origen se conserva como backup frío (solo lectura) hasta decisión del custodio; sus *.nul-damaged-bak (5,7 GB) son los primeros candidatos a borrar.

7.1 Cerrojo de identidad mudada ​

Si el secret de un cliente se muda a otra máquina (o a otro sitio de esta), este cliente no puede volver a arrancar: dos instancias vivas de la misma identidad, o arrancar una con un log más corto que el de la red, bifurcan el feed (AGENTES.md §3, RECOVERY-PROTOCOL.md §4, HUB-PROTOCOL.md §11 «mudanza o baja»). Tampoco se sube de versión (publicaría un oasisVersion con esa identidad) ni se le importa nada encima. Cómo se deja cerrado, de forma que un agente o una persona que llegue después no pueda arrancarlo por descuido:

  1. Marcador en los datos: volumes-dev/ssb-data/IDENTIDAD-MUDADA (texto: fecha, decisión, dónde vive ahora la identidad, cómo se levanta). Está dentro del bind: viaja con cualquier copia de ssb-data y lo verá el entrypoint cuando el cerrojo pase a la imagen (siguiente ciclo; hoy el entrypoint no lo mira).
  2. Scripts: client/scripts/lib/identity-lock.sh (client_identity_lock_check), llamado al empezar por setup.sh, sync-only.sh, import-identity.sh y ecoin-init.sh: con el marcador presente imprimen su contenido y salen con 78. OASIS_IDENTITY_LOCK_OVERRIDE=1 lo salta a sabiendas (queda dicho en la salida).
  3. Compose: un docker-compose.override.yml local, no versionado (.gitignore) que Compose carga solo junto a docker-compose.yml: para oasis-client sustituye entrypoint/command por el aviso y exit 78, sin healthcheck y sin restart. Así docker compose up oasis-client crea un contenedor que no arranca Oasis, y un docker start oasis-client posterior vuelve a dar el aviso. El drill (-f explícitos) no lo carga; npm run build no se ve afectado. El contenedor anterior se retira con docker compose rm -sf oasis-client (los datos son binds y volúmenes nombrados: no se tocan; es el mismo paso que HUB §11 da al origen de una mudanza).
  4. Registro: asiento en plan/DECISIONES.md, fila en la ficha de instancia, y el aviso en UPGRADE-PROTOCOL.md §4 («Cliente») para que ningún ciclo lo suba.

Se levanta solo cuando la mudanza está resuelta (una única instancia, con el log del destino al día): se borra el override, se borra el marcador y se recrea el contenedor. En la demo, el cliente real de la máquina operadora está bloqueado desde 2026-10-18 (D-O39): ficha de instancia §5.

8. ECOin en el cliente ​

Estado · en main desde el 2026-09-18 (WP-O103). Ensayado de punta a punta con una identidad desechable (§8.9): montaje, publicación única, recreate, rebuild, down -v, backup y restore. Es un protocolo para cualquier habitante que quiera su cartera: no depende de ninguna identidad concreta. Publicar la dirección es siempre un acto manual, único y tuyo: nada de esta sección lo hace por ti. Doc hermana (el lado del banco): PUB/ECOIN-PROTOCOL.md. Asiento: D-O19. Se escribió sobre Oasis 1.1.2.

Puesta al día para Oasis 1.1.4 (2026-09-19, WP-O108), ensayada con identidad desechable. Tres cosas cambiaron respecto a 1.1.2 y gobiernan esta sección: (1) con la cartera cableada, abrir la GUI publica tu dirección sola (§8.2); (2) el estado bancario vive en ~/.ssb/oasis/banking, dentro de ssb-data (§8.4); (3) el banco se autodescubre (§8.7).

Modelo mental. Oasis lee la cartera solo de su oasis-config.json (wallet.{url,user,pass,fee}; desde 1.2.4 la viva, ~/.ssb/oasis/oasis-config.json, §4.1 y §8.4); las variables ECOIN_RPC_* no las lee nadie salvo el entrypoint, que las escribe ahí. Un mensaje SSB wallet con tu dirección es permanente y solo sirve si conservas la wallet.dat que tiene su clave: una dirección publicada sin copia de su cartera queda huérfana para siempre. Por eso el orden es siempre cartera → backup → publicar. En 1.1.4 «publicar» ocurre al abrir la GUI: el backup va antes de ese momento.

El cliente tiene ECOin independiente del VPS, en dos niveles. El valor por defecto es el primero sin nada cableado: wallet.url = "", ningún RPC saliente, /banking sin latencia.

(i) Solo dirección(ii) Cartera propia
Sirve paraaparecer, recibir y reclamar RBUademás saldo, envíos e historial (/wallet/* usa RPC)
ecoind propiosolo el rato de generar la direccióncorriendo siempre (perfil ecoin)
.env raízCOMPOSE_PROFILES= · ECOIN_RPC_URL=COMPOSE_PROFILES=ecoin · ECOIN_RPC_URL=http://ecoin-wallet:7474
Quién paga la RBUel ecoind del banco (bot-2), nunca el tuyoídem
Costeninguno en reposo~50 MB de cadena, ~13 min de sync inicial (medido en el VPS), hasta 512 MB de memoria

En los dos niveles la dirección sale de una wallet.dat tuya. Nunca publiques una dirección cuya cartera no controlas ni tienes respaldada.

8.1 Nivel (i): solo dirección ​

Se arranca ecoind sin cablear Oasis (ECOIN_RPC_URL vacía), se saca una dirección, se respalda la cartera, se para, y se da de alta la dirección a mano una vez.

bash
npm run ecoin:build                                  # antepone ecoin:fetch-deb (sha256 del .deb verificado; imagen de WP-O102)
npm run client:ecoin:init -- --mode address          # .env raíz con credenciales generadas + volumen externo o-sdk-client-ecoin-data
npm run ecoin:up                                     # ecoin-init.sh --ensure + up -d --wait ecoin-wallet (healthy)
npm run ecoin:address                                # getnewaddress → APÚNTALA. Cada llamada genera OTRA: llámalo una vez
npm run client:wallet:backup                         # devops/backups/client-wallet/<TS>/ con sha256 y manifiesto (§8.6)
npm run ecoin:stop                                   # la wallet.dat queda en el volumen; no hace falta que siga corriendo

Después, con la GUI arriba, una sola vez: http://localhost:3000/banking?filter=addresses → añadir tu feed y la dirección apuntada. Ese formulario (POST /banking/addresses) publica el mensaje wallet sin RPC y no es idempotente: cada envío publica otro mensaje permanente. Comprueba después con npm run client:ecoin:verify que hay exactamente 1 mensaje wallet en tu feed.

Copia el backup fuera de la máquina antes de publicar. Para pasar más adelante al nivel (ii) basta client:ecoin:init -- --mode own: la cartera es la misma y la dirección publicada sigue valiendo.

8.2 Nivel (ii): cartera propia ​

bash
npm run ecoin:build
npm run client:ecoin:init -- --mode own              # COMPOSE_PROFILES=ecoin · ECOIN_RPC_URL=http://ecoin-wallet:7474
npm run ecoin:up                                     # healthy (hasta ~70 s); sincroniza la cadena en segundo plano
npm run client:wallet:backup                         # ANTES de abrir la GUI
docker compose up -d oasis-client                    # recrea el cliente: el entrypoint cablea wallet.url/user/pass (§8.3)
npm run client:ecoin:verify                          # RPC por DNS de servicio, config cableada, estado, nº de mensajes wallet

Abrir la GUI publica tu dirección (medido en 1.1.4). Con wallet.url respondiendo RPC, la primera visita a / o /wallet (en el cliente real /banking sola no bastó) pide una dirección a tu ecoind (getnewaddress), la guarda en ~/.ssb/oasis/banking/wallet-addresses.json y publica el mensaje wallet en tu feed: irreversible y sin preguntar. Ensayo: 0 → 1 mensaje tras los primeros GET; sigue en 1 tras más visitas, un --force-recreate y nuevas visitas (upstream comprueba hasPublishedAddress). Consecuencias:

  • El npm run client:wallet:backup va antes de abrir la GUI. Vale: la dirección sale del keypool de la cartera (claves pregeneradas), así que ese backup ya contiene su clave. Haz otro después.
  • No des de alta la dirección a mano en /banking?filter=addresses: ya está publicada y ese POST no es idempotente (publicaría un segundo mensaje).
  • Si no quieres publicar todavía, no cablees la cartera: wallet.url vacía = nada de esto ocurre.
  • Compruébalo: npm run client:ecoin:verify -- --expect-wallet-msgs 1.

8.3 Qué hace el entrypoint con cada variable ​

docker-entrypoint.sh (raíz; zona wholesale, fuera de los guards de src/) solo actúa si OASIS_CLIENT_STATE_DIR está definido. El pub, el HUB y bot-2 no lo definen: para ellos nada cambia. Orden: persist_client_state (en todos los modos, también server) → wire_wallet_config → setup_oasis_config (los dos últimos solo con backend: full/client/backend). Todos escriben sobre la config viva, ~/.ssb/oasis/oasis-config.json (§4.1): desde Oasis 1.2.4 es la única que config-manager.js lee y escribe.

VariableQué hace el entrypointClave de oasis-config.json (la viva)
OASIS_CLIENT_STATE_DIR (/app/state)activa todo lo de esta tabla; crea y chowna el directorio. Si la viva no existe la siembra (default de la imagen + deep-merge del legado /app/state/oasis-config.json, gana el legado); si existe, funde el default de la imagen con ella (gana lo vivo). Modo 600. src/configs/oasis-config.json queda como default de la imagen (sin enlace; si lo había, se repone). El legado es de solo lecturael fichero entero
OASIS_BANKING_DIRretirada en 1.1.4: no la definas. banking_model.js la honra y backend.js no: tendrías dos mapas de direcciones. El entrypoint la desactiva (unset) y, si encuentra un banking/ de 1.1.2 en el state dir, lo copia una vez a ~/.ssb/oasis/banking/ sin pisar—
ECOIN_RPC_URLsi está definida, aunque vacía, se asigna tal cual; pasa antes por la guarda anti-remotowallet.url
ECOIN_RPC_USERse asigna junto a la URL; nunca se imprimewallet.user
ECOIN_RPC_PASSse asigna junto a la URL; nunca se imprimewallet.pass
OASIS_WALLET_FEEopcional; si viene, se asignawallet.fee
OASIS_WALLET_PUB_IDretirada en 1.1.4: el banco se autodescubre (§8.7). Si queda una clave walletPub en la config persistida, el entrypoint la borra—
ECOIN_RPC_ALLOW_REMOTEsolo el valor i-know desactiva la guarda anti-remoto—
OASIS_WALLET_WIRINGmanual → el entrypoint no toca wallet.*—
ECOIN_MEM_LIMITno la lee el entrypoint: es el mem_limit de ecoin-wallet en el compose (512m por defecto)—
COMPOSE_PROFILESno la lee el entrypoint: ecoin hace que docker compose up -d levante también ecoin-wallet—
  • Guarda anti-remoto. El host de ECOIN_RPC_URL debe ser ecoin-wallet, localhost, 127.0.0.1 o host.docker.internal. Cualquier otro → error en el log y wallet.url = "".
  • Env manda. Lo que teclees en /settings/wallet se pisa en el siguiente arranque con lo que diga el entorno: wire_wallet_config escribe sobre la viva en cada arranque, el mismo fichero que la GUI lee. Es deliberado: la GUI no sabe vaciar wallet.url (solo el entrypoint puede poner "") y así la fuente de las credenciales es una sola, el .env raíz (ignorado por git); su copia de trabajo queda en la viva (600, §8.4). Escape: OASIS_WALLET_WIRING=manual y gestionas la cartera desde la GUI bajo tu responsabilidad.
  • El resto de la config de la GUI (tema, idioma, módulos) no se pisa: la GUI escribe en la viva desde sus rutas y el entrypoint solo le funde encima el default de la imagen, donde gana lo vivo (§8.4).

8.4 Estado persistente: ssb-data/oasis y el legado client-state ​

Desde Oasis 1.2.4 todo el estado del cliente vive en ssb-data/oasis/; client-state queda como legado:

  • ssb-data/oasis/oasis-config.json, la config viva (~/.ssb/oasis/oasis-config.json en el contenedor; §4.1): config de la GUI + cableado de la cartera. La GUI la reescribe entera desde sus rutas (saveConfig, por rename: por eso es un fichero regular, ni symlink ni bind de fichero); el entrypoint la siembra la primera vez y en cada arranque le funde el default de la imagen (gana lo vivo) y le aplica el entorno (wallet.*, modules.aiMod). Modo 600: lleva las credenciales RPC y viaja con cualquier copia de ssb-data (volumes-dev/ está ignorado por git).
  • ssb-data/oasis/** (desde Oasis 1.1.3, src/configs/state-manager.js): banking/ (mapa de direcciones, épocas, libro de pagos), flags/, content/, peers/, keys/… Upstream lo migra solo al arrancar desde ~/.ssb/ y src/configs/. Viaja con tu identidad: el backup de ssb-data ya lo incluye, y client:import-identity -- --force lo aparta junto con ella. La cartera no se toca.
  • ./volumes-dev/client-state → /app/state, legado de solo lectura: su oasis-config.json es lo que persistía hasta 1.2.2 (entonces por symlink desde src/configs/). Sirvió para sembrar la viva la primera vez que el cliente arrancó con el entrypoint de WP-O145 y no se vuelve a leer ni a escribir: lo que cambies ahí no surte efecto. Se retira en un WP posterior; hasta entonces setup y ecoin-init siguen creando el directorio porque el compose lo bindea. Si vienes de un cliente 1.1.2 con client-state/banking/, el entrypoint lo copia una vez a ssb-data/oasis/banking/ y conserva el original.

8.5 La wallet.dat: volumen nombrado externo ​

wallet.dat y la cadena viven en el volumen Docker o-sdk-client-ecoin-data (etiqueta o-sdk.role=client-wallet), declarado external en el compose. Ya no existe volumes-dev/ecoin-data.

  • Por qué no un bind: Berkeley DB sobre un bind de Windows (virtiofs) es frágil y lento; una wallet.dat corrupta es dinero perdido.
  • Por qué external: docker compose down -v no borra volúmenes externos. Lo crea client:ecoin:init, no el compose; el cliente arranca igual sin el perfil y sin que el volumen exista.
  • Contrapartida: un volumen nombrado vive dentro de la VM de Docker Desktop (WSL2) y se pierde con un factory reset. El backup al host (§8.6) es obligatorio, no opcional.
  • ecoin-wallet no publica puertos: el RPC (7474) y el P2P (7408, solo saliente) quedan en o-sdk_oasis-network. El 12000 desaparece. Quien quiera P2P entrante añade 7408:7408 en un docker-compose.override.yml no versionado. El RPC no se publica nunca.
  • Credenciales: generadas por client:ecoin:init (usuario oasis- + 8 hex, contraseña de 48 hex) en el .env raíz; la imagen arranca fail-closed (ECOIN_REQUIRE_CREDS=1 rechaza vacío y ecoinrpc). ecoin:info, ecoin:balance y ecoin:address no llevan credenciales en la línea de comandos (ecoind las lee de su conf) y ecoin:system no vuelca rpcpassword.

8.6 Backup y restore de la cartera ​

bash
npm run client:wallet:backup                                              # en caliente: backupwallet → docker cp → sha256 contenedor = host
npm run client:wallet:backup -- --cold                                    # con ecoind parado
npm run client:wallet:restore -- devops/backups/client-wallet/<TS>        # aparta la actual a wallet.dat.pre-restore-<TS>; nunca sobrescribe

Cada backup deja en devops/backups/client-wallet/<TS>/ la cartera (wallet-<TS>.dat), SHA256SUMS.txt, MANIFEST.json (getinfo.blocks, direcciones, imagen, volumen) y RESTORE.txt; npm run client:wallet:backup -- --verify seguido del directorio recomprueba los sha256 sin tocar Docker. El restore exige que el contenedor exista (npm run ecoin:up al menos una vez). Tras un restore: validateaddress de la dirección publicada debe dar ismine: true.

  • La copia no está cifrada y es material de claves: fuera de git (ya ignorado), fuera de cualquier imagen y con una copia fuera de la máquina.
  • Keypool: una copia antigua solo conoce las claves pregeneradas hasta ese momento. Haz backup después de cada tanda de direcciones nuevas (y tras abrir la GUI por primera vez en el nivel ii).
  • Cuándo: antes de publicar la dirección, antes de un upgrade, antes de cualquier down.
  • npm run client:backup-keys respalda la identidad SSB, no la cartera (avisa si el volumen existe).

8.7 Tu banco: se descubre solo ​

Desde Oasis 1.1.3 no se configura ningún banco. Tu cliente mira los anuncios pubAvailability que ha replicado y elige: primero el pub del invite de SolarNetHub si está disponible y reciente; si no, el anuncio disponible más nuevo (caducan a los 3 días). /banking muestra la lista de pubs con su pool y un botón para donar. OASIS_WALLET_PUB_ID y --pub-id ya no hacen nada.

  • Para ver un pub en la lista tienes que replicar su anuncio: síguelo o ten a alguien a tus saltos que lo siga. El de la casa es @ecoin.escrivivir.co (PUB/INSTANCIA-SCRIPTORIUM.md), encendido desde el 2026-09-19.
  • Cada claim es un mensaje ubiClaim: irreversible. Lo paga cualquier pub con el motor encendido y fondos que te vea y te considere elegible: feed con ≥ 30 días, dirección publicada y actividad.
  • Quien paga es el ecoind del pub hacia tu dirección publicada: para cobrar basta el nivel (i).

8.8 Avisos ​

  • Nunca apuntes ECOIN_RPC_URL al ecoind del VPS. Su RPC es HTTP plano y es un monedero único sin cuentas: tus credenciales viajarían en claro y quien las tenga maneja los fondos del banco. La guarda anti-remoto lo rechaza; ECOIN_RPC_ALLOW_REMOTE=i-know existe para laboratorio, no para esto.
  • Prohibido docker system prune --volumes (y docker volume prune): es lo único que borra el volumen externo con tu wallet.dat si ecoin-wallet está parado. npm run downDELETEVOLS y npm run cleanDELETEVOLS pasan antes por client/scripts/guard-destroy.sh: lista qué sobrevive, exige escribir BORRAR y se niega si hay wallet.dat sin backup de menos de 24 h.
  • Los mensajes SSB son permanentes: wallet (tu dirección) y ubiClaim no se borran ni se editan. Una dirección equivocada o duplicada se queda en tu feed y en el pub.
  • Abrir la GUI con el nivel (ii) activo llama a getnewaddress (§8.2): backup antes.
  • «Desconectar cartera» (Settings, desde Oasis 1.1.10) no es un interruptor: es una publicación. El botón (POST /settings/wallet/disconnect) vacía wallet.{url,user,pass} en la config de la GUI y publica un mensaje wallet con la dirección vacía, que anula tu dirección para toda la red: nadie puede pagarte hasta que publiques otra. En este cliente, además, el entrypoint vuelve a cablear la cartera desde el .env en el siguiente arranque y la siguiente visita a la GUI publica la dirección otra vez: dos mensajes permanentes por un clic. Para dejar de usar la cartera, quita ECOIN_RPC_* del .env (§8.1); no uses ese botón. Con el puente de loopback (§8.10) el botón responde desde el navegador del host.
  • .env raíz: lo comparte todo el compose de la raíz. El pub usa su propio --env-file y no lo lee.
  • Al pasar de cartera propia a solo dirección, para antes ecoind: npm run ecoin:stop. Un docker compose down sin --profile ecoin no ve el servicio ecoin-wallet: lo deja corriendo y la red queda «still in use».
  • Para retirar la cartera: vaciar ECOIN_RPC_URL en el .env raíz y recrear (docker compose up -d oasis-client) sigue valiendo: el entrypoint escribe wallet.url = "" sobre la viva en el arranque. En modo solo dirección wallet.url queda vacía, pero wallet.user y wallet.pass siguen en volumes-dev/ssb-data/oasis/oasis-config.json (600), no en client-state: inocuo (no hay RPC), pero es un fichero con secretos que viaja con ssb-data.

Medido en el drill del 2026-09-18 (identidad desechable, proyecto o-sdk-drill): el healthcheck del contenedor pide / sin seguir el 302 y no genera direcciones; la dirección la fija la primera visita a /wallet, /banking o /activity (un navegador que abre / sí sigue la redirección). En el drill ecoind reutilizó su dirección por defecto (0 nuevas); en el VPS creó dos: no des por fijo ese número. La publicación automática no ocurre en 1.1.2 (fallo de upstream): se publica con el alta manual de §8.1, una sola vez. Tras recrear el contenedor, reconstruir la imagen y down -v siguieron la misma dirección, la config de la GUI y un único mensaje wallet; backup y restore devolvieron ismine: true.

8.9 Drill con identidad desechable ​

Todo lo anterior se ensaya antes con una identidad que no es la tuya, en un proyecto compose aparte que no comparte nada con el cliente real: proyecto o-sdk-drill, contenedores oasis-client-drill y ecoin-wallet-drill, puertos 3100:3000 y 8108:8008, binds en volumes-dev/drill/*, volumen externo o-sdk-drill-ecoin-data, env-file client/.env.drill (no versionado), sin GPU y con OASIS_SKIP_AI_MODEL=true. La identidad se crea vacía, con gossip.json vacío y sin invite: no replica con nadie y no ensucia la red.

bash
# SIEMPRE desde la raíz del repo, con los DOS -f (el drill es un override del compose raíz) y el --env-file
export MSYS_NO_PATHCONV=1
D="docker compose -p o-sdk-drill -f docker-compose.yml -f client/docker-compose.drill.yml --env-file client/.env.drill"
mkdir -p volumes-dev/drill/{ssb-data,ai-models,logs,client-state/banking}
[ -f volumes-dev/drill/ssb-data/gossip.json ] || echo "[]" > volumes-dev/drill/ssb-data/gossip.json
ECOIN_ENV_FILE=client/.env.drill ECOIN_VOLUME=o-sdk-drill-ecoin-data ECOIN_STATE_DIR=volumes-dev/drill/client-state \
  npm run client:ecoin:init -- --mode own          # credenciales y volumen del drill, no los reales
$D up -d --build
CLIENT_CONTAINER=oasis-client-drill ECOIN_CONTAINER=ecoin-wallet-drill HOST_GUI_PORT=3100 npm run client:ecoin:verify
ECOIN_CONTAINER=ecoin-wallet-drill npm run client:wallet:backup
ECOIN_VOLUME=o-sdk-drill-ecoin-data ECOIN_CONTAINER=ecoin-wallet-drill GUARD_DATA_DIR=volumes-dev/drill \
  bash client/scripts/guard-destroy.sh && $D down -v   # el volumen externo sobrevive
#Qué se comprueba en el drill
D1ecoin:fetch-deb y build
D2docker port ecoin-wallet-drill vacío; el host no alcanza 7474/7408/12000; RPC 200 por DNS de servicio desde oasis-client-drill; ecoinrpc → 401
D3config cableada y sitio de la config (client:ecoin:verify V4 sobre la viva; V5: fichero regular a 600, src/configs sin enlace)
D4/wallet con saldo 0 y dirección; publicar una vez; exactamente 1 mensaje wallet
D5--force-recreate y rebuild: sobreviven tema, config de cartera y mapa; sigue habiendo 1 mensaje
D6down -v con guard: wallet.dat intacta
D7backup → volumen nuevo (otra dirección) → restore → ismine: true
D8sin perfil: arranca, /banking sin latencia, cableado omitido
D9guarda anti-remoto: URL remota → wallet.url = ""
D10siembra desde el legado: con volumes-dev/drill/ssb-data/oasis/oasis-config.json ausente y un client-state/oasis-config.json con una clave reconocible (p. ej. language), el primer arranque la deja en la viva; el log dice «SEMBRADA … + el legado»
D11la viva sobrevive a un recreate: cambiar una clave en la viva como haría la GUI (/settings, o editar ssb-data/oasis/oasis-config.json con el cliente parado) → --force-recreate → la clave sigue; el legado client-state/oasis-config.json no ha cambiado (sha256 igual)
D12el entorno llega a la viva: cambiar OASIS_WALLET_FEE en client/.env.drill → recreate → wallet.fee nuevo en la viva (V4)
D13client:ecoin:verify V4/V5 en verde tras D10-D12
D14sin restos de escritura: ls -a volumes-dev/drill/ssb-data/oasis/ no tiene *.tmp-* ni *.corrupt-*

Puerta: solo con D1-D14 en verde y confirmación expresa del custodio se pasa a la identidad real, en este orden: client:backup-keys → client:ecoin:init → ecoin:up → backup de cartera antes de publicar → publicar una vez → 1 mensaje wallet en el feed y replicado al pub → después, --pub-id de bot-2. El primer claim real es otra confirmación aparte.

8.10 La GUI desde el navegador del host: el puente de loopback ​

Oasis solo acepta las acciones sensibles (reclamar o rechazar la RBU, Wallet, Settings, update) si la petición llega desde 127.0.0.1 (isLoopbackRequest, src/backend/backend.js): upstream da por hecho que la GUI corre en la misma máquina que el backend. En el cliente dockerizado el navegador del host entra por el mapeo de puertos y el backend ve la IP del bridge: 403 con el cuerpo vacío («Se denegó el acceso a localhost»). Apareció el 2026-09-19 con el primer «Claim UBI» pulsado en la UI; hasta entonces esas peticiones se habían hecho siempre desde dentro del contenedor.

Sin tocar src/: con OASIS_LOOPBACK_PROXY_PORT=3001 el entrypoint arranca un reenviador TCP :3001 → 127.0.0.1:3000, y el compose publica la GUI como 127.0.0.1:3000:3001. El backend ve un cliente local y la garantía de «solo esta máquina» se conserva en el host. Nunca publiques ese puerto en 0.0.0.0: darías a toda la LAN las acciones que Oasis reserva al dueño del nodo. Pub, HUB y bots no definen la variable.

Comprobar: docker port oasis-client → 3001/tcp -> 127.0.0.1:3000; el log dice «Puente de loopback»; y en Banking → UBI, «Claim UBI» deja de dar 403. Un reclamo es un mensaje ubiClaim al mes, irreversible; el botón de al lado, «Refuse UBI», renuncia a la del mes y tampoco se deshace. Tras pulsar, la página puede no refrescarse: cuenta el mensaje en el feed antes de volver a pulsar.

8.11 En qué interfaz escucha el sbot del cliente (Oasis ≥ 1.3.2) ​

Desde 1.3.2 src/server/ssb_config.js solo fija el host de las conexiones entrantes cuando el nodo es pub (0.0.0.0) o cuando lanBroadcasting está apagado (127.0.0.1); con pub: false y lanBroadcasting: true (el default del cliente) lo deja sin fijar y multiserver elige. Medido en el drill de WP-O146: el sbot escucha en [::]:8008 (todas las interfaces, /proc/net/tcp6) y la GUI en 0.0.0.0:3001; el puerto publicado por el compose sigue respondiendo. Antes era 0.0.0.0:8008. No es una desviación ni cambia el puente de loopback (§8.10). HUB y bot no lo notan: su ssb-config lleva host: "0.0.0.0".

9. Piezas ​

PiezaFichero
Compose del clientedocker-compose.yml (raíz)
Directorios de volúmenesclient/scripts/setup.sh (npm run setup)
Importar identidadclient/scripts/import-identity.sh (npm run client:import-identity)
Integridad y seq del logclient/scripts/lib/inspect-log-offset.js (npm run client:inspect-log)
Sbot puro / sincronizaciónclient/scripts/sync-only.sh (npm run client:sync-only)
Sonda dentro del contenedorpub/tools/ssb-probe.js (por stdin; pub, HUB o cliente; siempre docker exec -u oasis -e HOME=/home/oasis)
Seq de un feed en el pubdevops/scripts/pub-feed-seq.sh (npm run devops:pub-feed-seq)
Backup de identidadclient/scripts/backup-keys.sh (npm run client:backup-keys)
Prueba de IAclient/scripts/test-ai-service.sh (npm run client:test-ai)
ECOin: credenciales, volumen y modoclient/scripts/ecoin-init.sh (npm run client:ecoin:init; --mode address o --mode own, --pub-id, --ensure)
ECOin: backup y restore de la carteraclient/scripts/backup-wallet.sh (npm run client:wallet:backup, npm run client:wallet:restore)
ECOin: verificación de los CAclient/scripts/ecoin-verify.sh (npm run client:ecoin:verify)
ECOin: guarda de borradoclient/scripts/guard-destroy.sh (antes de downDELETEVOLS y cleanDELETEVOLS)
ECOin: cableado de la configdocker-entrypoint.sh (persist_client_state, wire_wallet_config, setup_oasis_config)
ECOin: plantilla de entorno.env.example (raíz) → .env (ignorado)
ECOin: drillclient/docker-compose.drill.yml + client/.env.drill (no versionado)
ECOin: imagen de ecoindecoin/ (endurecida en WP-O102; se reutiliza tal cual)
Identidad GPG del usuarioclient/identity/init-gpg-key.sh