🐳 Proyecto 4: tercer ejecutor
Hasta aquí el curso habló de dos runtimes. Pero ya existe un tercero corriendo en tu máquina desde hace semanas: el dsh-sandbox, el agente de DeepSeek en contenedor, con modelo local o remoto. Tiene tres skills copiadas a mano (drift garantizado) y ninguna capa de contexto. Este proyecto agrega un destino --dsh a la fuente canónica de skills, le da al dsh un prime que le indica leer los archivos correctos, y hace el readback allí dentro — sin juntar nunca ~/projetos con un proveedor remoto.
🎯 El proyecto en una pantalla
dsh-sandbox — y que sus skills salen de la misma fuente canónica que Claude y Codex, sin copia manual.sync-skills.sh con destino --dsh y drift check, una skill prime en ~/projetos/dsh-skills, y un readback ejecutado en el panel con modelo local y comparado con el remoto.AGENTS.md, context/, tasks/current.md y handoffs/latest.md por su nombre; y ningún paso usó remoto-projetos.🐳 Qué es el dsh-sandbox
El dsh-sandbox es @deepseek-ai/dsh en la versión 0.1.1-rc.1 corriendo en Docker, con panel en 127.0.0.1:9080 y contenedor dsh-orchestrator-runtime-1. La versión está congelada dentro de work/node_modules a propósito: un arranque no vuelve al registry, y actualizar es un acto explícito. Todo el control pasa por un único script en la raíz del proyecto — ./dsh — con cinco verbos.
Lo que importa para este curso es el diseño de los modos. No existe "encender todo": el montaje de disco y el proveedor de modelo están atados. En el modo local, ~/projetos se monta dentro del contenedor y el único proveedor es Ollama en la propia máquina — tus archivos están ahí, pero no sale ni un byte. En el modo remoto, el proveedor es OpenRouter y ~/projetos no se monta — el modelo es mejor, pero no ve nada tuyo. La combinación de ambos existe (remoto-projetos) y pide escribir la palabra CONFIRMO, porque es exactamente la combinación peligrosa.
Lee el diagrama siguiendo las flechas que entran al contenedor. Local trae tus archivos pero ata el modelo a la máquina; remoto libera el modelo pero deja los archivos afuera. El bloque rojo es la única combinación que junta ambos, y existe justamente para que sea difícil activarla por accidente.
Objetivo: levantar dsh en el modo correcto y confirmar que el panel responde antes que cualquier otra cosa.
cd ~/projetos/dsh-sandbox
# ver en qué estado está ahora (puede llevar días en ejecución)
./dsh status
# para este proyecto: modo local (projetos montados + solo Ollama)
./dsh local
# el panel solo escucha en loopback; desde fuera, usa un túnel SSH
curl -sf -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9080
# esperado: 200
# contenedor y límite de memoria
docker ps --filter name=dsh-orchestrator-runtime-1
# al terminar
./dsh off
Cómo verificar: ./dsh status indica el modo activo y el curl devuelve 200. Si necesitas acceder desde otra máquina, el camino es ssh -L 9080:127.0.0.1:9080 …: dsh rechaza el bind en 0.0.0.0 por decisión de los autores, y la app no tiene ninguna autenticación.
El contenedor no es el costo: pesa unos 85 MB en reposo, con un límite de 3 GB. El peso real es el modelo cargado en el host, sin límite: de 17 GB a 45 GB según lo que elijas. Ese es el número que decide si la máquina aguanta, no Docker.
Conceptos clave
Un consumidor más del mismo contexto portátil, junto a Claude y Codex.
Montaje y proveedor se deciden juntos, nunca por separado.
0.1.1-rc.1 instalada localmente; el arranque no busca actualizaciones.
El panel no tiene login; acceso remoto solo por túnel SSH.
🧬 Copiar a mano es drift garantizado
Hoy dsh tiene tres skills, y las tres llegaron ahí con cp: formato-curso-v2, formato-curso-v5 y capa-inema (dependencia de las dos primeras). Viven en ~/projetos/dsh-skills, montado en el contenedor como /work/.dsh/skills. El formato es compatible (es el mismo SKILL.md de Claude) y por eso la copia funciona. El problema no es que no funcione hoy; es que no siga igual mañana.
Llámalo drift: corriges un bug en la skill de Claude, pruebas, publicas, y la copia de dsh se queda en la versión anterior. Nada se rompe de forma ruidosa. dsh simplemente genera un curso con el patrón de hace dos meses y lo descubres en el output. El diagnóstico es claro sobre la escala del problema: ya son cuatro consumidores de skills, no dos: Claude (117), Codex (27), dsh (3 copias manuales) y openpcbotv3 (16 propias). Sin una fuente canónica, cada uno diverge por su cuenta, y la divergencia crece con el tiempo.
Una fuente canónica a la izquierda, un build en el medio, cuatro destinos a la derecha. Los dos primeros ya salen del build; el tercero (dsh) es lo que este proyecto agrega; el cuarto queda para después. La caja de la derecha es lo que convierte esto en una garantía: sin drift, tienes cuatro copias y ninguna prueba de que son iguales.
✓ Skill generada por el build
- ✓Corregiste en la fuente: un
buildy uninstallactualizan todos los destinos. - ✓El
driftlo detecta antes de que lo detecte el usuario. - ✓Respaldo automático del destino anterior en cada instalación.
- ✓Una sola revisión sirve a los cuatro consumidores.
✗ Skill copiada a mano
- ✗Nadie recuerda cuál copia es la más reciente.
- ✗La divergencia aparece en el output, meses después, y parece un bug del modelo.
- ✗Corregir se vuelve cuatro ediciones, y olvidas una.
- ✗Sin verificador, "está sincronizado" es fe, no un hecho.
Cuidado con el nombre de la carpeta: el destino es ~/projetos/dsh-skills, y no ~/projetos/skills — este último ya es el repositorio git inematds/skills del curso de Agent Skills. Copiar al nombre equivocado contamina un repo publicado. Verifica la ruta antes de ejecutar cualquier cp -a.
Conceptos clave
Copias que empiezan iguales y divergen sin que nadie lo note.
El único lugar que se edita; todo lo demás se genera.
Claude, Codex, dsh y openpcbotv3 — todos queriendo la misma skill.
El drift no genera error; genera un resultado equivocado con apariencia de correcto.
🔧 Agregar el destino --dsh a sync-skills.sh
El scripts/sync-skills.sh que ya usaste en la Trilha 2 tiene cuatro verbos: import, build, drift e install. Asume dos runtimes todo el tiempo, con el bucle for rt in claude codex y la ruta $HOME/.$rt/skills/$s — un truco elegante que funciona porque las dos carpetas tienen el mismo formato de nombre. El dsh rompe ese patrón: su destino es ~/projetos/dsh-skills/<nome>, que no encaja en la fórmula.
La buena noticia es que el dsh acepta el mismo SKILL.md de Claude. Así que no hace falta un nuevo objetivo de polyskill build: lo que se copia al dsh es el dist/claude/<nome> que el build ya produce. El cambio es pequeño y quirúrgico — una función que resuelve la ruta de destino por runtime, el caso --dsh en el install, y la inclusión del dsh en el bucle del drift, que es la parte que realmente vale la pena.
Objetivo: aplicar el patch que agrega el tercer destino, manteniendo el drift como verificación de los tres.
# --- en scripts/sync-skills.sh, justo después del `mkdir -p skills dist` ---
# resuelve la carpeta de destino de cada runtime (dsh se sale del patrón ~/.<rt>/skills)
dest_for() {
case "$1" in
claude|codex) printf '%s/.%s/skills/%s\n' "$HOME" "$1" "$2" ;;
dsh) printf '%s/projetos/dsh-skills/%s\n' "$HOME" "$2" ;;
*) return 1 ;;
esac
}
# el dsh consume el MISMO formato de Claude: el origen es dist/claude
src_for() {
case "$1" in dsh) echo claude ;; *) echo "$1" ;; esac
}
# --- en el verbo `drift`: cambiar `for rt in claude codex` por: ---
for rt in claude codex dsh; do
out="$d/dist/$(src_for "$rt")/$s"; tgt="$(dest_for "$rt" "$s")"
[ -d "$out" ] && [ -d "$tgt" ] || { echo "[no instalada] $s → $rt"; continue; }
if diff -rq "$out" "$tgt" >/dev/null; then echo "[ok] $s → $rt"; else echo "[DRIFT] $s → $rt"; rc=1; fi
done
# --- en el verbo `install`: cambiar `for rt in claude codex` por: ---
for rt in claude codex dsh; do
case "$tgt" in --both) [ "$rt" = dsh ] && continue ;; --all) ;; --$rt) ;; *) continue ;; esac
out="skills/$s/dist/$(src_for "$rt")/$s"; [ -d "$out" ] || { echo "ejecuta build antes: $out"; exit 1; }
dest="$(dest_for "$rt" "$s")"
[ -e "$dest" ] && cp -a "$dest" "$(dirname "$dest")/.$s.bak-$(date +%s)"
mkdir -p "$(dirname "$dest")"; cp -a "$out" "$dest"; echo "instalada: $dest"
# preservado del original: Codex también descubre en ~/.agents/skills
[ "$rt" = codex ] && [ -d "$HOME/.agents/skills" ] && cp -a "$out" "$HOME/.agents/skills/$s" && echo "reflejada: ~/.agents/skills/$s"
done
Cómo verificar: bash -n scripts/sync-skills.sh pasa sin error de sintaxis, y scripts/sync-skills.sh drift pasa a imprimir tres líneas por skill en lugar de dos — la tercera terminando en → dsh.
Objetivo: traer las tres skills copiadas a mano al flujo canónico y dejar el drift en cero.
cd ~/projetos/agente-claude-codex
# 1. guardar las copias actuales antes de sobrescribir (son el último estado bueno conocido)
cp -a ~/projetos/dsh-skills ~/projetos/dsh-skills.bak-$(date +%Y%m%d)
# 2. importar a la fuente canónica (a partir del original de Claude)
scripts/sync-skills.sh import formato-curso-v2 formato-curso-v5 capa-inema
scripts/sync-skills.sh build
# 3. instalar ahora también en el dsh
for s in formato-curso-v2 formato-curso-v5 capa-inema; do
scripts/sync-skills.sh install "$s" --dsh
done
# 4. la prueba
scripts/sync-skills.sh drift; echo "rc=$?"
# esperado: solo líneas [ok], rc=0
Cómo verificar: rc=0 y ninguna línea [DRIFT]. Si aparece drift justo después del install, la copia a mano tenía alguna edición local que nunca volvió a la fuente — compara con el respaldo del paso 1 y lleva la diferencia a skills/<nome>/ antes de continuar.
Por qué --both sigue significando dos: el patch mantiene --both como Claude+Codex e introduce --all para los tres. Quien ya tiene la costumbre de escribir --both no se lleva la sorpresa de una instalación en un lugar nuevo: cambiar el comportamiento de un flag existente es el tipo de trampa que solo aparece tres semanas después.
Conceptos clave
El dsh no vive en ~/.<runtime>/skills; necesita un resolvedor.
Formato compatible: no hace falta ningún target de build nuevo.
Un destino que no entra en el drift no está realmente en el flujo.
Agregar --all en lugar de cambiar lo que hace --both.
🧭 Darle contexto al dsh: la skill de prime
Con las skills resueltas, falta el contexto. El dsh escanea $DSH_HOME/skills y nada más: no tiene auto-load de AGENTS.md ni de CLAUDE.md. Si abres el panel dentro de un proyecto migrado, ve los archivos (en modo local) pero no sabe que debería abrirlos. Como no hay hook, no hay evento de apertura ni instrucción global, la única palanca disponible es la que ya usa: una skill.
De ahí la skill de prime. No hace nada más que pedir la lectura, en el orden correcto: AGENTS.md, luego context/, luego tasks/current.md, luego handoffs/latest.md, el mismo orden que Claude y Codex siguen por instrucción. Es la misma idea del tema 5 del Proyecto 2: cuando el evento no existe, el comportamiento se convierte en lectura pedida explícitamente. Y como el dsh acepta el formato de Claude, esta skill sale de la misma fuente canónica y va a los tres destinos.
Escribir el prime en la fuente canónica
Un SKILL.md corto en skills/prime/, con el orden de lectura y el formato de la respuesta.
Build e install en los tres destinos
install prime --all lleva la misma skill a Claude, Codex y dsh.
Levantar el dsh en modo local
Sin ~/projetos montado no hay nada que leer; el readback tiene que hacerse en modo local.
Hacer las 5 preguntas en el panel
Las mismas del readback de la Ruta 2. El criterio es citar el archivo, no la belleza de la respuesta.
Objetivo: crear la skill de prime e instalarla en los tres destinos, incluido el dsh.
cd ~/projetos/agente-claude-codex
mkdir -p skills/prime
cat > skills/prime/SKILL.md <<'MD'
---
name: prime
description: Carga el contexto del proyecto antes de cualquier trabajo. Úsala al
comienzo de toda sesión, antes de la primera edición, y siempre que pierdas el hilo.
---
# Prime — lee antes de actuar
Lee, en este orden, y solo entonces responde:
1. `AGENTS.md` en la raíz del proyecto — las reglas estables.
2. `context/overview.md` — los hechos verificados. Si existe
`context/decisions/`, lee también la decisión más reciente.
3. `tasks/current.md` — lo que está en curso ahora.
4. `handoffs/latest.md` — lo que hizo la última sesión y cuál es la próxima acción.
Si alguno de estos archivos no existe, di cuál falta en lugar de inventar.
Cierra el prime con cuatro líneas, cada una citando el archivo de donde vino:
- **Proyecto:** qué es, en una frase.
- **Estado:** qué está listo y qué está pendiente.
- **Próxima acción:** la frase exacta escrita en `handoffs/latest.md`.
- **Regla que más importa aquí:** la restricción de `AGENTS.md` que
más afecta a la próxima acción.
MD
scripts/sync-skills.sh build
scripts/sync-skills.sh install prime --all
ls ~/projetos/dsh-skills/prime/SKILL.md
Cómo verificar: el ls encuentra el archivo, y scripts/sync-skills.sh drift muestra [ok] prime → dsh junto con claude y codex. Luego, en el panel en 127.0.0.1:9080 con el dsh en modo local, pide "usa la skill prime en el proyecto /projetos/<tu-piloto>" y comprueba que las cuatro líneas vuelvan con el nombre de archivo en cada una.
Atención a los puentes de ruta: dentro del contenedor, ~/projetos aparece como /projetos y las skills se ven en /work/.dsh/skills. Hay dos symlinks en work/ justamente para que los ~/... escritos en las skills se resuelvan allí dentro; aparecen rotos cuando los miras desde el host, y eso es lo esperado. Al pedir algo desde el panel, usa la ruta de adentro.
Conceptos clave
La skill que reemplaza el auto-load inexistente por lectura pedida.
AGENTS → context → tasks/current → handoffs/latest, siempre igual.
Symlinks en work/ que hacen que ~/… se resuelva dentro del contenedor.
Una respuesta sin nombre de archivo no cuenta como lectura comprobada.
🔒 Seguridad: 269 secretos y la regla que no se negocia
La auditoría contó 269 archivos de secretos dentro de ~/projetos, entre ellos los dos .env que todo el curso trata como fuente única de API keys: wifi/.env y openpcbotv2/.env. Montar ese árbol en un contenedor cuyo proveedor de modelo es remoto significa darle a un servicio de terceros la posibilidad de leer cualquiera de ellos, porque el agente decide por su cuenta qué archivos abrir. No existe el "no va a mirar": la única garantía es la que está en el diseño.
Por eso montaje y proveedor van atados. La decisión se tomó una sola vez, en el script, y no en cada sesión: local da acceso a los archivos y mantiene el modelo dentro de la máquina; remoto libera el modelo y saca los archivos de escena. La tercera opción existe y pide escribir CONFIRMO, y la regla de este proyecto es corta: nunca uses remoto-projetos para readback. El readback es justamente el ejercicio de pedirle a un agente que lea archivos de tu disco; es el peor momento posible para tener un proveedor externo en la línea.
✓ Combinaciones permitidas
- ✓
localpara readback y para generar cursos: archivos dentro, modelo dentro. - ✓
remotopara comparar la calidad de la prosa con contenido pegado a mano. - ✓Panel solo en
127.0.0.1; desde afuera, únicamente por túnel SSH. - ✓
./dsh offal terminar, para no dejar un montaje activo sin necesidad.
✗ Nunca
- ✗
remoto-projetospara readback: junta los 269 secretos con un proveedor externo. - ✗Exponer el panel con
--host 0.0.0.0: la app no tiene ninguna autenticación. - ✗Copiar un
.envdentro dedsh-skills"para que la skill lo encuentre". - ✗Escribir
CONFIRMOen automático: la fricción existe para que te detengas a pensar.
Objetivo: confirmar, antes de ejecutar el readback, que el modo activo es el seguro, y medir el tamaño de la superficie.
cd ~/projetos/dsh-sandbox
# cuántos archivos de secretos expondría el montaje (solo el conteo, nunca el contenido)
find ~/projetos -maxdepth 3 \( -name '.env' -o -name '.env.*' -o -name '*credential*' \) \
-type f 2>/dev/null | wc -l
# el modo activo, antes de pedirle cualquier cosa al panel
./dsh status
# verificación directa: ¿existe el montaje de /projetos en este contenedor?
docker inspect dsh-orchestrator-runtime-1 \
| python3 -c 'import json,sys; [print(m["Source"], "->", m["Destination"]) for m in json.load(sys.stdin)[0]["Mounts"]]'
# el panel no puede estar escuchando fuera del loopback
ss -ltnp 2>/dev/null | grep 9080
# esperado: 127.0.0.1:9080 — si aparece 0.0.0.0:9080, apágalo YA con ./dsh off
Cómo verificar: si status dice local, inspect muestra /projetos montado y ss muestra solo 127.0.0.1, puedes seguir. Cualquier combinación distinta es motivo para detenerte y corregir antes de escribir la primera pregunta en el panel.
Sobre network_mode: host: el contenedor corre sin aislamiento de red porque dsh solo acepta bind en 127.0.0.1 y, con bridge, el proxy de Docker no alcanza el loopback interno. La consecuencia es que ve todos los servicios de loopback del host: Ollama en 11434, iccmonit en 9003 y lo que esté en marcha. No es un detalle cosmético: elimina la sensación de "está aislado porque es un contenedor". El aislamiento real aquí viene de los modos, no de la red.
Conceptos clave
Quien ve los archivos no habla con un proveedor externo, y viceversa.
La superficie medida de ~/projetos; el número que justifica la regla.
Escribir CONFIRMO existe para impedir el accidente, no el uso.
Con network_mode: host, toda la red del host queda visible.
📉 Expectativa honesta: lo que entrega el modelo local
El modelo local de esta máquina es qwen3.8:27b, unos 18 GB cargados en el host, sin límite. Compite por RAM con inemavox, y el historial de caídas por OOM está registrado en el monitoreo; no es una hipótesis. Si vas a ejecutar readback en dsh, haz el preflight: revisa la memoria libre antes y no dejes una síntesis de voz pesada en curso.
Sobre la calidad, el mensaje del diagnóstico es directo: las skills se escribieron para el comportamiento de Claude, y con un modelo local el resultado se queda corto. Pero "se queda corto" hay que medirlo en el eje correcto. Lo que este proyecto prueba es la portabilidad de contexto, y el criterio es citar los archivos correctos. Una prosa más pobre, una estructura más floja y menos matices son límites del modelo. Confundir ambas cosas lleva a la conclusión equivocada: "la portabilidad falló", cuando lo que falló fue la expectativa sobre la calidad de la redacción.
🧪 La prueba que separa el modelo de la portabilidad
Ejecuta la misma pregunta en los dos modos. En remoto, dsh no ve ~/projetos, así que pega el contenido de los archivos en la propia pregunta. Si la respuesta mejora mucho, el cuello de botella era el modelo; si sigue sin citar ningún archivo, ahí sí el problema es tu capa de contexto.
| Eje | Criterio | Conclusión si falla |
|---|---|---|
| Portabilidad | Cita AGENTS.md, tasks/current.md, handoffs/latest.md por su nombre | Falta el prime, el montaje o los archivos |
| Fidelidad | La próxima acción coincide con la escrita en el handoff | Leyó por encima; acorta los archivos |
| Calidad de la prosa | Texto organizado y sin repeticiones | Límite del modelo: no invalida nada |
| Estabilidad | Terminó sin OOM | Infra: modelo demasiado grande para la RAM libre |
Objetivo: hacer el preflight de memoria y registrar el resultado del readback en los dos modos, sin sacar conclusiones apresuradas.
# preflight: ¿cabe ahora el modelo de ~18 GB?
free -g | awk '/Mem:/ {print "livre:", $7, "GB"}'
# regla práctica: con menos de 24 GB libres, no levantes el 27b junto con inemavox
# quién ya está ocupando memoria
ps -eo rss,comm --sort=-rss | head -5 | awk '{printf "%6.1f GB %s\n", $1/1048576, $2}'
# modelos cargados en Ollama en este momento
ollama ps
# después del readback, registra el resultado junto a los otros dos runtimes
cd ~/projetos/<seu-piloto>
mkdir -p relatorios
$EDITOR relatorios/readback-dsh-local.md # qué archivos se citaron y la próxima acción que leyó
$EDITOR relatorios/readback-dsh-remoto.md # misma pregunta, contexto pegado a mano
Cómo verificar: los dos informes existen y el veredicto está escrito en una frase: "citó los cuatro archivos, prosa inferior a Claude" es aprobación; "no citó ningún archivo en los dos modos" es reprobación de la capa de contexto, y entonces el problema está en el prime o en el montaje, no en el modelo.
✅ Criterios de aceptación (marca con evidencia)
- ☐
sync-skills.sh driftdevuelverc=0con los tres destinos listados. Evidencia: salida del comando. - ☐Las 3 skills que antes se copiaban a mano ahora las genera el build. Evidencia:
[ok] … → dshpara cada una. - ☐La skill
primeexiste en los tres destinos. Evidencia:lsen las tres rutas. - ☐El readback en dsh cita los cuatro archivos por su nombre. Evidencia:
relatorios/readback-dsh-local.md. - ☐Ningún paso usó
remoto-projetos. Evidencia:./dsh statusregistrado antes de cada ronda. - ☐Ningún OOM durante el ejercicio. Evidencia: el preflight anotado y el inemavox detenido o intacto.
⚠️ Riesgos de este proyecto
- •OOM: el modelo de ~18 GB arranca sin tope y compite con el inemavox por la misma RAM.
- •Usar
remoto-projetospor comodidad y exponer los 269 secretos. - •Copiar a
~/projetos/skillsen lugar dedsh-skillsy contaminar un repo publicado. - •Que el
install --dshsobrescriba una copia con una edición local que nunca volvió a la fuente. - •Concluir "la portabilidad falló" mirando la calidad de la prosa en lugar de las citas.
↩️ Rollback en un comando
- ✓Skills de dsh:
rm -rf ~/projetos/dsh-skills && mv ~/projetos/dsh-skills.bak-AAAAMMDD ~/projetos/dsh-skills. - ✓Script:
git checkout -- scripts/sync-skills.shdeshace el patch completo. - ✓Prime:
rm -rf skills/primeen la fuente y en los destinos; nada más depende de ella. - ✓Contenedor:
./dsh offapaga todo y desmonta; Claude y Codex siguen intactos. - ✓Memoria:
ollama stop qwen3.8:27blibera los ~18 GB al instante.
Conceptos clave
Medir la memoria libre antes de arrancar el modelo, no después del bloqueo.
Citar los archivos correctos; la calidad de la prosa es otro eje.
Antes de culpar a la portabilidad, prueba con un modelo mejor.
El dsh no reemplaza a Claude ni a Codex; demuestra que el contexto viaja.
Autoevaluación (opcional): en el readback del dsh en modo local, el modelo citó AGENTS.md, tasks/current.md y handoffs/latest.md, pero el texto quedó repetitivo y mal organizado. ¿Qué concluyes?
🎯 Resumen del proyecto
local y remoto que atan montaje y proveedor.--dsh en sync-skills.sh, con las tres skills entrando al drift check junto con Claude y Codex.remoto-projetos para readback; y el criterio es citar los archivos correctos, no la belleza de la prosa.Próximo proyecto:
3.5 — Workspace de cliente: Venn, alcance y canarios