🐳 Projeto 4: terceiro executor
Até aqui o curso falou de dois runtimes. Mas já existe um terceiro rodando na sua máquina há semanas: o dsh-sandbox, o agente da DeepSeek em container, com modelo local ou remoto. Ele tem três skills copiadas à mão (drift garantido) e nenhuma camada de contexto. Este projeto acrescenta um destino --dsh à fonte canônica de skills, dá ao dsh um prime que manda ler os arquivos certos, e faz o readback lá dentro — sem nunca juntar ~/projetos com provedor remoto.
🎯 O projeto em uma tela
dsh-sandbox — e que as skills dele saem da mesma fonte canônica que Claude e Codex, sem cópia manual.sync-skills.sh com destino --dsh e drift check, uma skill prime em ~/projetos/dsh-skills, e um readback rodado no painel com modelo local e comparado com o remoto.AGENTS.md, context/, tasks/current.md e handoffs/latest.md pelo nome; e nenhum passo usou remoto-projetos.🐳 O que é o dsh-sandbox
O dsh-sandbox é o @deepseek-ai/dsh na versão 0.1.1-rc.1 rodando em Docker, com painel em 127.0.0.1:9080 e container dsh-orchestrator-runtime-1. A versão está congelada dentro de work/node_modules de propósito: um boot não volta ao registry, e atualizar é ato explícito. O controle todo passa por um script único na raiz do projeto — ./dsh — com cinco verbos.
O ponto que importa para este curso é o desenho dos modos. Não existe "ligar tudo": montagem de disco e provedor de modelo estão amarrados. No modo local, ~/projetos é montado dentro do container e o único provedor é o Ollama na própria máquina — os seus arquivos estão lá, mas nenhum byte sai. No modo remoto, o provedor é o OpenRouter e ~/projetos não é montado — o modelo é melhor, mas ele não vê nada seu. A combinação dos dois existe (remoto-projetos) e pede a palavra CONFIRMO digitada, porque é exatamente a combinação perigosa.
Leia o desenho pelas setas que entram no container. Local traz os seus arquivos mas prende o modelo na máquina; remoto solta o modelo mas deixa os arquivos de fora. O bloco vermelho é a única combinação que junta os dois — e existe justamente para ser difícil de acionar por acidente.
Objetivo: subir o dsh no modo certo e confirmar que o painel responde antes de qualquer outra coisa.
cd ~/projetos/dsh-sandbox
# ver em que estado ele está agora (pode estar no ar há dias)
./dsh status
# para este projeto: modo local (projetos montados + só Ollama)
./dsh local
# o painel só escuta em loopback; de fora, use túnel SSH
curl -sf -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9080
# esperado: 200
# container e teto de memória
docker ps --filter name=dsh-orchestrator-runtime-1
# ao terminar
./dsh off
Como verificar: ./dsh status diz o modo ativo e o curl devolve 200. Se precisar acessar de outra máquina, o caminho é ssh -L 9080:127.0.0.1:9080 … — o dsh recusa bind em 0.0.0.0 por decisão dos autores, e o app não tem autenticação nenhuma.
O container não é o custo: ele pesa uns 85 MB ocioso, com teto de 3 GB. O peso real é o modelo carregado no host, sem teto: de 17 GB a 45 GB dependendo do que você escolher. Esse é o número que decide se a máquina aguenta — não o Docker.
Conceitos-chave
Mais um consumidor do mesmo contexto portátil, ao lado de Claude e Codex.
Montagem e provedor decididos juntos, nunca em separado.
0.1.1-rc.1 instalada localmente; o boot não busca atualização.
O painel não tem login; acesso remoto só por túnel SSH.
🧬 Cópia à mão é drift garantido
Hoje o dsh tem três skills, e as três chegaram lá por cp: formato-curso-v2, formato-curso-v5 e capa-inema (dependência das duas primeiras). Elas moram em ~/projetos/dsh-skills, montado no container como /work/.dsh/skills. O formato é compatível — é o mesmo SKILL.md do Claude — e por isso a cópia funciona. O problema não é ela não funcionar hoje; é ela não continuar igual amanhã.
Chame isso de drift: você corrige um bug na skill do Claude, testa, publica, e a cópia do dsh fica na versão antiga. Nada quebra com barulho. O dsh simplesmente gera um curso com o padrão de dois meses atrás e você descobre no output. O diagnóstico é claro sobre a escala do problema: já são quatro consumidores de skills, não dois — Claude (117), Codex (27), dsh (3 cópias manuais) e o openpcbotv3 (16 próprias). Sem fonte canônica, cada um diverge sozinho, e a divergência cresce com o tempo.
Uma fonte canônica à esquerda, um build no meio, quatro destinos à direita. Os dois primeiros já saem do build; o terceiro (dsh) é o que este projeto acrescenta; o quarto fica para depois. A caixa da direita é o que transforma isso em garantia: sem drift, você tem quatro cópias e nenhuma prova de que são iguais.
✓ Skill gerada pelo build
- ✓Corrigiu na fonte, um
builde uminstallatualizam todos os destinos. - ✓O
driftacusa antes do usuário acusar. - ✓Backup automático do destino anterior a cada instalação.
- ✓Uma revisão só serve os quatro consumidores.
✗ Skill copiada à mão
- ✗Ninguém lembra qual cópia é a mais nova.
- ✗A divergência aparece no output, meses depois, e parece bug do modelo.
- ✗Corrigir vira quatro edições, e você esquece uma.
- ✗Sem verificador, "está sincronizado" é fé, não fato.
Cuidado com o nome da pasta: o destino é ~/projetos/dsh-skills, e não ~/projetos/skills — este último já é o repositório git inematds/skills do curso de Agent Skills. Copiar para o nome errado polui um repo publicado. Confira o caminho antes de rodar qualquer cp -a.
Conceitos-chave
Cópias que começam iguais e divergem sem ninguém perceber.
O único lugar que se edita; todo o resto é gerado.
Claude, Codex, dsh e openpcbotv3 — todos querendo a mesma skill.
Drift não gera erro; gera resultado errado com cara de certo.
🔧 Acrescentar o destino --dsh ao sync-skills.sh
O scripts/sync-skills.sh que você já usou na Trilha 2 tem quatro verbos: import, build, drift e install. Ele assume dois runtimes o tempo todo, com o laço for rt in claude codex e o caminho $HOME/.$rt/skills/$s — um truque elegante que funciona porque as duas pastas têm o mesmo formato de nome. O dsh quebra esse padrão: o destino dele é ~/projetos/dsh-skills/<nome>, que não cabe na fórmula.
A boa notícia é que o dsh aceita o mesmo SKILL.md do Claude. Então não é preciso um novo alvo de polyskill build: o que se copia para o dsh é o dist/claude/<nome> que o build já produz. A mudança é pequena e cirúrgica — uma função que resolve o caminho de destino por runtime, o caso --dsh no install, e a inclusão do dsh no laço do drift, que é a parte que realmente paga a conta.
Objetivo: aplicar o patch que acrescenta o terceiro destino, mantendo o drift como verificação dos três.
# --- em scripts/sync-skills.sh, logo depois do `mkdir -p skills dist` ---
# resolve a pasta de destino de cada runtime (dsh foge do padrão ~/.<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
}
# o dsh consome o MESMO formato do Claude: a origem é dist/claude
src_for() {
case "$1" in dsh) echo claude ;; *) echo "$1" ;; esac
}
# --- no verbo `drift`: trocar `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 "[não 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
# --- no verbo `install`: trocar `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 "rode 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"
done
Como verificar: bash -n scripts/sync-skills.sh passa sem erro de sintaxe, e scripts/sync-skills.sh drift passa a imprimir três linhas por skill em vez de duas — a terceira terminando em → dsh.
Objetivo: trazer as três skills copiadas à mão para dentro do fluxo canônico e zerar o drift.
cd ~/projetos/agente-claude-codex
# 1. guardar as cópias atuais antes de sobrescrever (elas são o estado bom conhecido)
cp -a ~/projetos/dsh-skills ~/projetos/dsh-skills.bak-$(date +%Y%m%d)
# 2. importar para a fonte canônica (a partir do original do Claude)
scripts/sync-skills.sh import formato-curso-v2 formato-curso-v5 capa-inema
scripts/sync-skills.sh build
# 3. instalar agora também no dsh
for s in formato-curso-v2 formato-curso-v5 capa-inema; do
scripts/sync-skills.sh install "$s" --dsh
done
# 4. a prova
scripts/sync-skills.sh drift; echo "rc=$?"
# esperado: só linhas [ok], rc=0
Como verificar: rc=0 e nenhuma linha [DRIFT]. Se aparecer drift logo depois do install, a cópia à mão tinha alguma edição local que nunca voltou para a fonte — compare com o backup do passo 1 e leve a diferença para skills/<nome>/ antes de seguir.
Por que --both continua significando dois: o patch mantém --both como Claude+Codex e introduz --all para os três. Quem já tem o hábito de digitar --both não é surpreendido por uma instalação em lugar novo — mudança de comportamento de flag existente é o tipo de armadilha que só aparece três semanas depois.
Conceitos-chave
O dsh não mora em ~/.<runtime>/skills; precisa de resolvedor.
Formato compatível: nenhum alvo novo de build é necessário.
Destino que não entra no drift não está de verdade no fluxo.
Acrescentar --all em vez de mudar o que --both faz.
🧭 Dar contexto ao dsh: a skill de prime
Skills resolvidas, falta o contexto. O dsh escaneia $DSH_HOME/skills e é só isso: ele não tem auto-load de AGENTS.md nem de CLAUDE.md. Se você abrir o painel dentro de um projeto migrado, ele vê os arquivos (no modo local) mas não sabe que deveria abri-los. Como não há hook, não há evento de abertura, e não há instrução global — a única alavanca disponível é a que ele já usa: uma skill.
Daí a skill de prime. Ela não faz nada além de mandar ler, na ordem certa: AGENTS.md, depois context/, depois tasks/current.md, depois handoffs/latest.md — a mesma ordem que o Claude e o Codex seguem por instrução. É a mesma ideia do tópico 5 do Projeto 2: quando o evento não existe, o comportamento vira leitura pedida explicitamente. E como o dsh aceita o formato do Claude, essa skill sai da mesma fonte canônica e vai para os três destinos.
Escrever o prime na fonte canônica
Um SKILL.md curto em skills/prime/, com a ordem de leitura e o formato da resposta.
Build e install nos três destinos
install prime --all leva a mesma skill para Claude, Codex e dsh.
Subir o dsh no modo local
Sem ~/projetos montado não há o que ler; o readback tem que ser no modo local.
Rodar as 5 perguntas no painel
As mesmas do readback da Trilha 2. O critério é citar o arquivo, não a beleza da resposta.
Objetivo: criar a skill de prime e instalá-la nos três destinos, incluindo o dsh.
cd ~/projetos/agente-claude-codex
mkdir -p skills/prime
cat > skills/prime/SKILL.md <<'MD'
---
name: prime
description: Carrega o contexto do projeto antes de qualquer trabalho. Use no
começo de toda sessão, antes da primeira edição, e sempre que perder o fio.
---
# Prime — leia antes de agir
Leia, nesta ordem, e só então responda:
1. `AGENTS.md` na raiz do projeto — as regras estáveis.
2. `context/overview.md` — os fatos verificados. Se houver
`context/decisions/`, leia também a decisão mais recente.
3. `tasks/current.md` — o que está em andamento agora.
4. `handoffs/latest.md` — o que a última sessão fez e qual é a próxima ação.
Se algum desses arquivos não existir, diga qual falta em vez de inventar.
Feche o prime com quatro linhas, cada uma citando o arquivo de onde veio:
- **Projeto:** o que é, em uma frase.
- **Estado:** o que está pronto e o que está pendente.
- **Próxima ação:** a frase exata escrita em `handoffs/latest.md`.
- **Regra que mais importa aqui:** a restrição do `AGENTS.md` que
mais afeta a próxima ação.
MD
scripts/sync-skills.sh build
scripts/sync-skills.sh install prime --all
ls ~/projetos/dsh-skills/prime/SKILL.md
Como verificar: o ls encontra o arquivo, e scripts/sync-skills.sh drift mostra [ok] prime → dsh junto com claude e codex. Depois, no painel em 127.0.0.1:9080 com o dsh em modo local, peça "use a skill prime no projeto /projetos/<seu-piloto>" e confira se as quatro linhas voltam com nome de arquivo em cada uma.
Atenção às pontes de caminho: dentro do container, ~/projetos aparece como /projetos e as skills são vistas em /work/.dsh/skills. Há dois symlinks em work/ justamente para que os ~/... escritos nas skills resolvam lá dentro — eles aparecem quebrados quando você olha do host, e isso é esperado. Ao pedir algo pelo painel, use o caminho de dentro.
Conceitos-chave
A skill que substitui o auto-load inexistente por leitura pedida.
AGENTS → context → tasks/current → handoffs/latest, sempre igual.
Symlinks em work/ que fazem ~/… resolver dentro do container.
Resposta sem nome de arquivo não conta como leitura comprovada.
🔒 Segurança: 269 segredos e a regra que não se negocia
A auditoria contou 269 arquivos de segredo dentro de ~/projetos — entre eles os dois .env que o curso inteiro trata como fonte única de API keys, wifi/.env e openpcbotv2/.env. Montar essa árvore num container cujo provedor de modelo é remoto significa dar a um serviço de terceiros a chance de ler qualquer um deles, porque o agente decide sozinho quais arquivos abrir. Não existe "ele não vai olhar": a única garantia é a que está no desenho.
É por isso que montagem e provedor são amarrados. A decisão foi tomada uma vez, no script, e não em cada sessão: local dá acesso aos arquivos e prende o modelo dentro da máquina; remoto solta o modelo e tira os arquivos de cena. A terceira opção existe e pede CONFIRMO digitado — e a regra deste projeto é curta: nunca use remoto-projetos para readback. O readback é justamente o exercício de mandar um agente ler arquivos do seu disco; é o pior momento possível para ter um provedor externo na linha.
✓ Combinações permitidas
- ✓
localpara readback e para gerar curso: arquivos dentro, modelo dentro. - ✓
remotopara comparar qualidade de prosa com conteúdo colado à mão. - ✓Painel só em
127.0.0.1; de fora, apenas por túnel SSH. - ✓
./dsh offao terminar, para não deixar montagem viva sem necessidade.
✗ Nunca
- ✗
remoto-projetospara readback — junta os 269 segredos com provedor externo. - ✗Expor o painel com
--host 0.0.0.0: o app não tem autenticação nenhuma. - ✗Copiar um
.envpara dentro dedsh-skills"para a skill achar". - ✗Digitar
CONFIRMOno automático — a fricção existe para você parar e pensar.
Objetivo: confirmar, antes de rodar o readback, que o modo ativo é o seguro — e medir o tamanho da superfície.
cd ~/projetos/dsh-sandbox
# quantos arquivos de segredo a montagem exporia (só a contagem, nunca o conteúdo)
find ~/projetos -maxdepth 3 \( -name '.env' -o -name '.env.*' -o -name '*credential*' \) \
-type f 2>/dev/null | wc -l
# o modo ativo — antes de pedir qualquer coisa ao painel
./dsh status
# checagem direta: a montagem de /projetos existe neste container?
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"]]'
# o painel não pode estar escutando fora do loopback
ss -ltnp 2>/dev/null | grep 9080
# esperado: 127.0.0.1:9080 — se aparecer 0.0.0.0:9080, derrube AGORA com ./dsh off
Como verificar: se o status disser local, o inspect mostrar /projetos montado e o ss mostrar só 127.0.0.1, pode seguir. Qualquer combinação diferente disso é motivo para parar e corrigir antes de digitar a primeira pergunta no painel.
Sobre o network_mode: host: o container roda sem isolamento de rede porque o dsh só aceita bind em 127.0.0.1 e, com bridge, o proxy do Docker não alcança o loopback interno. A consequência é que ele enxerga todos os serviços de loopback do host — Ollama em 11434, iccmonit em 9003, e o que mais estiver no ar. Não é um detalhe cosmético: some com a sensação de "está isolado porque é container". O isolamento real aqui vem dos modos, não da rede.
Conceitos-chave
Quem vê os arquivos não fala com provedor externo, e vice-versa.
A superfície medida de ~/projetos; o número que justifica a regra.
CONFIRMO digitado existe para impedir o acidente, não o uso.
Com network_mode: host, a rede do host está toda visível.
📉 Expectativa honesta: o que o modelo local entrega
O modelo local desta máquina é o qwen3.8:27b, cerca de 18 GB carregados no host, sem teto. Ele briga por RAM com o inemavox — e o histórico de travamentos por OOM está registrado no monitoramento, não é hipótese. Se você for rodar readback no dsh, faça o preflight: veja a memória livre antes, e não deixe uma síntese de voz pesada em andamento.
Sobre a qualidade, o recado do diagnóstico é direto: as skills foram escritas para o comportamento do Claude, e com modelo local o resultado fica aquém. Mas "aquém" precisa ser medido no eixo certo. O que este projeto testa é portabilidade de contexto, e o critério é citar os arquivos certos. Prosa mais pobre, estrutura mais frouxa e menos nuance são limite do modelo. Confundir os dois leva à conclusão errada — "a portabilidade falhou" quando o que falhou foi a expectativa de qualidade de escrita.
🧪 O teste que separa modelo de portabilidade
Rode a mesma pergunta nos dois modos. No remoto o dsh não enxerga ~/projetos, então cole o conteúdo dos arquivos na própria pergunta. Se a resposta melhorar muito, o gargalo era o modelo; se continuar sem citar arquivo nenhum, aí sim o problema é a sua camada de contexto.
| Eixo | Critério | Conclusão se falhar |
|---|---|---|
| Portabilidade | Cita AGENTS.md, tasks/current.md, handoffs/latest.md pelo nome | Falta prime, montagem ou os arquivos |
| Fidelidade | A próxima ação bate com a escrita no handoff | Leu por cima; encurte os arquivos |
| Qualidade da prosa | Texto organizado e sem repetição | Limite do modelo — não invalida nada |
| Estabilidade | Terminou sem OOM | Infra: modelo grande demais para a RAM livre |
Objetivo: fazer o preflight de memória e registrar o resultado do readback nos dois modos, sem tirar conclusão apressada.
# preflight: o modelo de ~18 GB cabe agora?
free -g | awk '/Mem:/ {print "livre:", $7, "GB"}'
# regra prática: menos de 24 GB livres, não suba o 27b junto com o inemavox
# quem já está segurando memória
ps -eo rss,comm --sort=-rss | head -5 | awk '{printf "%6.1f GB %s\n", $1/1048576, $2}'
# modelos carregados no Ollama neste momento
ollama ps
# depois do readback, registre o resultado ao lado dos outros dois runtimes
cd ~/projetos/<seu-piloto>
mkdir -p relatorios
$EDITOR relatorios/readback-dsh-local.md # quais arquivos foram citados, e a próxima ação que ele leu
$EDITOR relatorios/readback-dsh-remoto.md # mesma pergunta, contexto colado à mão
Como verificar: os dois relatórios existem e o veredito está escrito em uma frase — "citou os quatro arquivos, prosa inferior ao Claude" é aprovação; "não citou nenhum arquivo nos dois modos" é reprovação da camada de contexto, e aí o problema está no prime ou na montagem, não no modelo.
✅ Critérios de aceite (marque com evidência)
- ☐
sync-skills.sh driftdevolverc=0com os três destinos listados. Evidência: saída do comando. - ☐As 3 skills antes copiadas à mão agora são geradas pelo build. Evidência:
[ok] … → dshpara cada uma. - ☐A skill
primeexiste nos três destinos. Evidência:lsnos três caminhos. - ☐Readback no dsh cita os quatro arquivos pelo nome. Evidência:
relatorios/readback-dsh-local.md. - ☐Nenhum passo usou
remoto-projetos. Evidência:./dsh statusregistrado antes de cada rodada. - ☐Nenhum OOM durante o exercício. Evidência: o preflight anotado e o inemavox parado ou intocado.
⚠️ Riscos deste projeto
- •OOM: o modelo de ~18 GB sobe sem teto e briga com o inemavox pela mesma RAM.
- •Usar
remoto-projetospor conveniência e expor os 269 segredos. - •Copiar para
~/projetos/skillsem vez dedsh-skillse poluir um repo publicado. - •O
install --dshsobrescrever uma cópia que tinha edição local nunca devolvida à fonte. - •Concluir "a portabilidade falhou" olhando a qualidade da prosa em vez das citações.
↩️ Rollback em um comando
- ✓Skills do dsh:
rm -rf ~/projetos/dsh-skills && mv ~/projetos/dsh-skills.bak-AAAAMMDD ~/projetos/dsh-skills. - ✓Script:
git checkout -- scripts/sync-skills.shdesfaz o patch inteiro. - ✓Prime:
rm -rf skills/primena fonte e nos destinos; nada mais depende dela. - ✓Container:
./dsh offderruba tudo e desmonta; Claude e Codex seguem intactos. - ✓Memória:
ollama stop qwen3.8:27bdevolve os ~18 GB na hora.
Conceitos-chave
Medir a memória livre antes de subir o modelo, não depois do travamento.
Citar os arquivos certos; a qualidade da prosa é outro eixo.
Antes de culpar a portabilidade, teste com um modelo melhor.
O dsh não substitui Claude ou Codex; prova que o contexto viaja.
Auto-checagem (opcional): no readback do dsh em modo local, o modelo citou AGENTS.md, tasks/current.md e handoffs/latest.md, mas o texto ficou repetitivo e mal organizado. O que você conclui?
🎯 Resumo do projeto
local e remoto amarrando montagem e provedor.--dsh no sync-skills.sh, com as três skills entrando no drift check junto com Claude e Codex.remoto-projetos para readback; e o critério é citar os arquivos certos, não a beleza da prosa.Próximo projeto:
3.5 — Workspace de cliente: Venn, escopo e canários