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, proyectoo-sdk, serviciooasis-client); el pub, el HUB y el entorno scriptorium viven enpub/(proyectooasis-pub-scriptorium) y no comparten puertos, nombres, redes ni directorios devolumes-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.bipfdesde Oasis 1.2; antesflume/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 tusecrety un log vacío, publica (en 1.1.2, el PM de bienvenida a los 3 s) un mensaje consequence: 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
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ó| Cliente | Pub + HUB (local) | |
|---|---|---|
| Compose / proyecto | docker-compose.yml · o-sdk | pub/docker-compose.pub.yml · oasis-pub-scriptorium |
| Contenedores | oasis-client, ecoin-wallet | oasis-pub-scriptorium, oasis-pub-hub, oasis-pub-hub-cache, oasis-pub-web, oasis-pub-panel-api |
| Puertos host | 3000, 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 |
| Red | o-sdk_oasis-network | oasis-pub-scriptorium_oasis_pub_net |
| Imagen | o-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)
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íaQué 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):
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é |
|---|---|
secret | la 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.json | direcciones/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 copia | Por 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.json | se regenera desde gossip.json; corrupto bloquea conexiones |
config | lo escribe el entrypoint; server-config.json manda (caps, connections, hops) |
socket, manifest.json, node_modules, *.nul-damaged-bak | residuos |
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.mdmodelo mental: el pub guarda tu feed). El script exigelog.offset; para ese caso copia elsecreta 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)
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):
| Veredicto | Significa | Qué hacer |
|---|---|---|
| SYNC-OK | seq local = seq pub, log.offset estable ≥ 5 min, pub visto conectado | stop y arrancar la GUI |
| AHEAD | local > pub (publicaste offline) | normal: el sbot empuja al pub (EBT es bidireccional); repetir |
| BEHIND | local < pub | no arrancar la GUI; esperar; si no avanza en 10 min docker restart oasis-sync-only |
| PUB-UNKNOWN | el pub no tiene el feed ni lo sigue | npm 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-MISMATCH | el sbot no es tu feed | parar; 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 dessb-data, persiste sola). Desde 1.2.4config-manager.jsla lee y la escribe ahí; desrc/configs/oasis-config.jsonsolo copia el contenido una vez (si la viva no existe) y después lo ignora. SusaveConfigescribe conrename: 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 siOASIS_CLIENT_STATE_DIRestá definida, en todos los modos, tambiénserverparasync-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.jsonqueda como default de la imagen (si quedó un enlace de un entrypoint anterior, se repone el default); - el legado
client-state/oasis-config.jsones de solo lectura: sembró una vez y no se vuelve a tocar (se retira en un WP posterior).
- si la viva no existe, la siembra = default de la imagen + deep-merge del legado
- En los modos con backend (
full/client/backend)OASIS_CONFIG_FILEpasa a la viva:wire_wallet_config(«el entorno manda»:ECOIN_RPC_URL/USER/PASS,OASIS_WALLET_FEE,OASIS_WALLET_WIRING=manual) ysetup_oasis_config(modules.aiModsegún modelo) escriben ahí en cada arranque. VaciarECOIN_RPC_URLen el.envy recrear sigue dejando de usar la cartera (§8.8). - HUB y bot no cambian: su bind
:roya está en el sitio nuevo; las escrituras siguen tolerándose. - Secreto. Las credenciales RPC (
wallet.user/pass) viven ahora enssb-data/oasis/oasis-config.json(600): viajan con cualquier copia dessb-data. El backup de la GUI las excluye (NOT_BACKED_UPensrc/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)
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
oasisVersioncae en seq N+1. - Si el upgrade obligó a apartar los índices (
db2/indexesydb2/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 enoasis-config.jsonentran por la fusión del default de la imagen. - Phone y Rooms (Oasis ≥ 1.2.2) no funcionan en el cliente en contenedor:
/phoneresponde «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 | +1 | SSB_server.js lo publica al arrancar con el log no vacío |
about | +1 | clearnetSince: 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 | |
wallet | 0 | el mapa de direcciones ya está presente (§8.2) |
pub-invites | 0 | pub: false |
# 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 nuevoMedido 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
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..ssbintacto (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.envsigue 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 avolumes-dev/ssb-data.pre-import-<ts>(si lo había) o re-importar desde el backupdevops/backups/client/<ts>/; en último caso solosecret+ flag + §3. - Nunca truncar el log (
db2/log.bipf;flume/log.offseten ≤ 1.1.x) ni tocarsecret. Ningún camino de este protocolo cambia elsecret.
7. Retirar instalaciones antiguas
Solo tras §5 en verde unos días y con backup verificado fuera de la máquina:
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-networkLos 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:
- 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 dessb-datay lo verá el entrypoint cuando el cerrojo pase a la imagen (siguiente ciclo; hoy el entrypoint no lo mira). - Scripts:
client/scripts/lib/identity-lock.sh(client_identity_lock_check), llamado al empezar porsetup.sh,sync-only.sh,import-identity.shyecoin-init.sh: con el marcador presente imprimen su contenido y salen con 78.OASIS_IDENTITY_LOCK_OVERRIDE=1lo salta a sabiendas (queda dicho en la salida). - Compose: un
docker-compose.override.ymllocal, no versionado (.gitignore) que Compose carga solo junto adocker-compose.yml: paraoasis-clientsustituyeentrypoint/commandpor el aviso yexit 78, sin healthcheck y sinrestart. Asídocker compose up oasis-clientcrea un contenedor que no arranca Oasis, y undocker start oasis-clientposterior vuelve a dar el aviso. El drill (-fexplícitos) no lo carga;npm run buildno se ve afectado. El contenedor anterior se retira condocker 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). - Registro: asiento en
plan/DECISIONES.md, fila en la ficha de instancia, y el aviso enUPGRADE-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
maindesde 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 dessb-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 variablesECOIN_RPC_*no las lee nadie salvo el entrypoint, que las escribe ahí. Un mensaje SSBwalletcon tu dirección es permanente y solo sirve si conservas lawallet.datque 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 para | aparecer, recibir y reclamar RBU | además saldo, envíos e historial (/wallet/* usa RPC) |
ecoind propio | solo el rato de generar la dirección | corriendo siempre (perfil ecoin) |
.env raíz | COMPOSE_PROFILES= · ECOIN_RPC_URL= | COMPOSE_PROFILES=ecoin · ECOIN_RPC_URL=http://ecoin-wallet:7474 |
| Quién paga la RBU | el ecoind del banco (bot-2), nunca el tuyo | ídem |
| Coste | ninguno 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.
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 corriendoDespué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
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 walletAbrir la GUI publica tu dirección (medido en 1.1.4). Con
wallet.urlrespondiendo RPC, la primera visita a/o/wallet(en el cliente real/bankingsola no bastó) pide una dirección a tuecoind(getnewaddress), la guarda en~/.ssb/oasis/banking/wallet-addresses.jsony publica el mensajewalleten tu feed: irreversible y sin preguntar. Ensayo: 0 → 1 mensaje tras los primeros GET; sigue en 1 tras más visitas, un--force-recreatey nuevas visitas (upstream compruebahasPublishedAddress). Consecuencias:
- El
npm run client:wallet:backupva 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.urlvací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.
| Variable | Qué hace el entrypoint | Clave 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 lectura | el fichero entero |
OASIS_BANKING_DIR | retirada 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_URL | si está definida, aunque vacía, se asigna tal cual; pasa antes por la guarda anti-remoto | wallet.url |
ECOIN_RPC_USER | se asigna junto a la URL; nunca se imprime | wallet.user |
ECOIN_RPC_PASS | se asigna junto a la URL; nunca se imprime | wallet.pass |
OASIS_WALLET_FEE | opcional; si viene, se asigna | wallet.fee |
OASIS_WALLET_PUB_ID | retirada 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_REMOTE | solo el valor i-know desactiva la guarda anti-remoto | — |
OASIS_WALLET_WIRING | manual → el entrypoint no toca wallet.* | — |
ECOIN_MEM_LIMIT | no la lee el entrypoint: es el mem_limit de ecoin-wallet en el compose (512m por defecto) | — |
COMPOSE_PROFILES | no la lee el entrypoint: ecoin hace que docker compose up -d levante también ecoin-wallet | — |
- Guarda anti-remoto. El host de
ECOIN_RPC_URLdebe serecoin-wallet,localhost,127.0.0.1ohost.docker.internal. Cualquier otro → error en el log ywallet.url = "". - Env manda. Lo que teclees en
/settings/walletse pisa en el siguiente arranque con lo que diga el entorno:wire_wallet_configescribe sobre la viva en cada arranque, el mismo fichero que la GUI lee. Es deliberado: la GUI no sabe vaciarwallet.url(solo el entrypoint puede poner"") y así la fuente de las credenciales es una sola, el.envraíz (ignorado por git); su copia de trabajo queda en la viva (600, §8.4). Escape:OASIS_WALLET_WIRING=manualy 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.jsonen el contenedor; §4.1): config de la GUI + cableado de la cartera. La GUI la reescribe entera desde sus rutas (saveConfig, porrename: 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 dessb-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/ysrc/configs/. Viaja con tu identidad: el backup dessb-dataya lo incluye, yclient:import-identity -- --forcelo aparta junto con ella. La cartera no se toca../volumes-dev/client-state→/app/state, legado de solo lectura: suoasis-config.jsones lo que persistía hasta 1.2.2 (entonces por symlink desdesrc/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 entoncessetupyecoin-initsiguen creando el directorio porque el compose lo bindea. Si vienes de un cliente 1.1.2 conclient-state/banking/, el entrypoint lo copia una vez assb-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.datcorrupta es dinero perdido. - Por qué
external:docker compose down -vno borra volúmenes externos. Lo creaclient: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-walletno publica puertos: el RPC (7474) y el P2P (7408, solo saliente) quedan eno-sdk_oasis-network. El 12000 desaparece. Quien quiera P2P entrante añade7408:7408en undocker-compose.override.ymlno versionado. El RPC no se publica nunca.- Credenciales: generadas por
client:ecoin:init(usuariooasis-+ 8 hex, contraseña de 48 hex) en el.envraíz; la imagen arranca fail-closed (ECOIN_REQUIRE_CREDS=1rechaza vacío yecoinrpc).ecoin:info,ecoin:balanceyecoin:addressno llevan credenciales en la línea de comandos (ecoindlas lee de su conf) yecoin:systemno vuelcarpcpassword.
8.6 Backup y restore de la cartera
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 sobrescribeCada 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-keysrespalda 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
ecoinddel pub hacia tu dirección publicada: para cobrar basta el nivel (i).
8.8 Avisos
- Nunca apuntes
ECOIN_RPC_URLalecoinddel 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-knowexiste para laboratorio, no para esto. - Prohibido
docker system prune --volumes(ydocker volume prune): es lo único que borra el volumen externo con tuwallet.datsiecoin-walletestá parado.npm run downDELETEVOLSynpm run cleanDELETEVOLSpasan antes porclient/scripts/guard-destroy.sh: lista qué sobrevive, exige escribirBORRARy se niega si haywallet.datsin backup de menos de 24 h. - Los mensajes SSB son permanentes:
wallet(tu dirección) yubiClaimno 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íawallet.{url,user,pass}en la config de la GUI y publica un mensajewalletcon 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.enven 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, quitaECOIN_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. .envraíz: lo comparte todo el compose de la raíz. El pub usa su propio--env-filey no lo lee.- Al pasar de cartera propia a solo dirección, para antes
ecoind:npm run ecoin:stop. Undocker compose downsin--profile ecoinno ve el servicioecoin-wallet: lo deja corriendo y la red queda «still in use». - Para retirar la cartera: vaciar
ECOIN_RPC_URLen el.envraíz y recrear (docker compose up -d oasis-client) sigue valiendo: el entrypoint escribewallet.url = ""sobre la viva en el arranque. En modo solo direcciónwallet.urlqueda vacía, perowallet.userywallet.passsiguen envolumes-dev/ssb-data/oasis/oasis-config.json(600), no enclient-state: inocuo (no hay RPC), pero es un fichero con secretos que viaja conssb-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.
# 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 |
|---|---|
| D1 | ecoin:fetch-deb y build |
| D2 | docker 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 |
| D3 | config 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 |
| D6 | down -v con guard: wallet.dat intacta |
| D7 | backup → volumen nuevo (otra dirección) → restore → ismine: true |
| D8 | sin perfil: arranca, /banking sin latencia, cableado omitido |
| D9 | guarda anti-remoto: URL remota → wallet.url = "" |
| D10 | siembra 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» |
| D11 | la 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) |
| D12 | el entorno llega a la viva: cambiar OASIS_WALLET_FEE en client/.env.drill → recreate → wallet.fee nuevo en la viva (V4) |
| D13 | client:ecoin:verify V4/V5 en verde tras D10-D12 |
| D14 | sin 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
| Pieza | Fichero |
|---|---|
| Compose del cliente | docker-compose.yml (raíz) |
| Directorios de volúmenes | client/scripts/setup.sh (npm run setup) |
| Importar identidad | client/scripts/import-identity.sh (npm run client:import-identity) |
| Integridad y seq del log | client/scripts/lib/inspect-log-offset.js (npm run client:inspect-log) |
| Sbot puro / sincronización | client/scripts/sync-only.sh (npm run client:sync-only) |
| Sonda dentro del contenedor | pub/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 pub | devops/scripts/pub-feed-seq.sh (npm run devops:pub-feed-seq) |
| Backup de identidad | client/scripts/backup-keys.sh (npm run client:backup-keys) |
| Prueba de IA | client/scripts/test-ai-service.sh (npm run client:test-ai) |
| ECOin: credenciales, volumen y modo | client/scripts/ecoin-init.sh (npm run client:ecoin:init; --mode address o --mode own, --pub-id, --ensure) |
| ECOin: backup y restore de la cartera | client/scripts/backup-wallet.sh (npm run client:wallet:backup, npm run client:wallet:restore) |
| ECOin: verificación de los CA | client/scripts/ecoin-verify.sh (npm run client:ecoin:verify) |
| ECOin: guarda de borrado | client/scripts/guard-destroy.sh (antes de downDELETEVOLS y cleanDELETEVOLS) |
| ECOin: cableado de la config | docker-entrypoint.sh (persist_client_state, wire_wallet_config, setup_oasis_config) |
| ECOin: plantilla de entorno | .env.example (raíz) → .env (ignorado) |
| ECOin: drill | client/docker-compose.drill.yml + client/.env.drill (no versionado) |
ECOin: imagen de ecoind | ecoin/ (endurecida en WP-O102; se reutiliza tal cual) |
| Identidad GPG del usuario | client/identity/init-gpg-key.sh |