# Verificação do estado remoto desconhecido após uma desconexão SSH

Uma desconexão SSH não significa que o comando remoto falhou. Significa que o cliente perdeu as evidências necessárias para classificar o que aconteceu. Essa diferença é irritante quando você executou `uname` e cara quando executou uma migração de banco de dados, uma implantação, uma troca de credenciais ou um comando que chama uma API externa.

A solução não é aumentar o timeout. É criar uma sequência de verificação remota com uma identidade persistente para a execução, transições de estado explícitas e uma prova de conclusão específica para o efeito. Com isso, uma reconexão muda a pergunta de «Devo executar novamente?» para «O que diz o registro dessa execução e o que ela alterou?»

## Uma conexão interrompida deixa três respostas honestas

Depois que um cliente SSH informa um reset, timeout, pipe quebrado ou EOF abrupto, o comando está em um de três estados: nunca começou, começou e continua em andamento, ou terminou. O código de saída no cliente não distingue essas situações de forma confiável.

Existem vários limites entre o seu shell e o programa remoto:

- O shell local inicia `ssh`.
- O cliente envia uma solicitação de canal SSH e os bytes do comando.
- O servidor aceita a solicitação e inicia um shell ou programa remoto.
- Esse programa executa o trabalho real.
- O programa termina, e o sshd envia a saída e o status de saída de volta.

Uma falha de rede depois de qualquer um desses limites pode produzir um erro local. Se a falha ocorrer antes do início do programa remoto, nada aconteceu. Se ocorrer depois que o programa confirmou uma alteração, mas antes do retorno do status de saída, a alteração aconteceu e o cliente ainda informa uma falha.

Por isso, uma mensagem local como `Connection reset by peer` é evidência de transporte, não evidência do resultado da operação. Ela diz que o cliente não conseguiu terminar a conversa SSH. Não diz se a operação remota foi executada.

O próprio manual de configuração do OpenSSH faz uma observação relacionada sobre canais inativos: fechar uma sessão não garante que os processos de shell associados tenham parado. Portanto, um timeout de canal não é um mecanismo de controle de trabalhos.

O hábito que causa danos é tratar um resultado ambíguo como uma operação que falhou. É compreensível, porque a maioria das ferramentas de linha de comando nos ensina a interpretar um status de saída diferente de zero como «execute novamente». Uma falha de transporte SSH quebra esse atalho.

## Entrega do comando e conclusão do comando são afirmações diferentes

Há pelo menos quatro afirmações sobre um comando remoto que vale a pena provar: envio, início, conclusão e efeito. As equipes costumam registrar uma delas e presumir que têm as quatro. 

Envio significa que o cliente tentou mandar o comando. O terminal local sabe disso, mas é a afirmação mais fraca. Início significa que o wrapper remoto criou uma evidência persistente antes de trabalhar. Conclusão significa que o wrapper registrou um resultado final. Efeito significa que o estado remoto ou externo pretendido agora corresponde ao resultado solicitado.

Uma listagem de processos prova menos do que parece. Ver um PID pode indicar apenas que existe agora um processo com nome semelhante. Isso não prova que ele pertence à sua solicitação, que não tenha confirmado a parte importante da operação ou que uma nova tentativa seja segura. Depois que um processo termina, a reutilização de PIDs torna registros antigos ainda menos úteis.

Os códigos de saída têm uma limitação parecida. O POSIX define `wait` como uma forma de o shell obter o status de um processo filho que ele conhece. Essa relação existe dentro do shell remoto. Quando a conexão SSH desaparece, o shell local perde o caminho até esse status. Uma sessão SSH posterior não consegue recriá-lo com `wait`; precisa ler um registro persistido pela primeira execução.

Mantenha estas afirmações separadas nos runbooks e nas saídas da automação:

1. «O cliente não conseguiu confirmar a conclusão.»
2. «A execução `r-20260722-1842-a91f` começou no host remoto.»
3. «Essa execução registrou o status de saída 0.»
4. «O marcador de implantação informa a versão `2026.07.22.3`.»

A quarta afirmação pode ser a única que responde à pergunta operacional real. Um comando de cópia de arquivos precisa de um checksum ou de um caminho final com o conteúdo esperado. Uma migração precisa da versão do esquema ou de uma entrada no histórico de migrações. Uma solicitação a uma API de pagamentos ou tickets precisa de um registro de idempotência nessa API, não apenas de uma linha em um log local.

## Coloque a identidade da execução no host remoto antes de começar

Um ID de execução persistente transforma uma reconexão vaga em uma consulta objetiva. Gere-o antes de chamar o SSH, passe-o ao wrapper remoto e faça com que todos os artefatos fiquem em um caminho derivado dele.

Não use apenas um timestamp. Dois agentes podem começar no mesmo segundo, os relógios podem divergir e timestamps não são bons identificadores opacos. Combine um timestamp com dados aleatórios ou use um gerador de UUID disponível no seu ambiente. O ID precisa ser fornecido novamente para a verificação e aparecer em todas as entradas de log relevantes.

Este fragmento de shell cria um diretório de execução, grava a operação solicitada, registra um marcador de início e preserva os dois fluxos de saída. Ele espera que um comando seja passado depois de `--`. Mantenha o wrapper em um local controlado, como `/usr/local/sbin/run-recorded`, em vez de copiá-lo de forma improvisada para cada string de comando.

```sh
#!/bin/sh
set -eu

run_id=$1
shift
[ "$1" = "--" ]
shift

base=/var/lib/recorded-runs
run_dir="$base/$run_id"

case "$run_id" in
  *[!A-Za-z0-9._-]*|'')
    printf '%s\n' "invalid run id" >&2
    exit 64
    ;;
esac

if ! mkdir "$run_dir" 2>/dev/null; then
  printf '%s\n' "run already exists: $run_id" >&2
  exit 75
fi

umask 077
printf '%s\n' "$*" > "$run_dir/request"
date -u +%Y-%m-%dT%H:%M:%SZ > "$run_dir/started_at"
printf '%s\n' "started" > "$run_dir/state"
printf '%s\n' "$$" > "$run_dir/pid"

set +e
"$@" >"$run_dir/stdout" 2>"$run_dir/stderr"
status=$?
set -e

printf '%s\n' "$status" > "$run_dir/exit_status"
date -u +%Y-%m-%dT%H:%M:%SZ > "$run_dir/finished_at"
printf '%s\n' "finished" > "$run_dir/state"
exit "$status"
```

A chamada `mkdir` faz mais do que cuidar da organização. A criação falha se esse ID de execução já existir, funcionando como uma reivindicação simples de criação única. Isso impede que duas invocações com o mesmo ID executem silenciosamente o trabalho duas vezes. Ela não resolve trabalhos concorrentes que usam IDs diferentes, o que exige um lock separado ou uma restrição no nível da aplicação.

A ordem importa. O wrapper grava `started_at`, `state` e `pid` antes de executar a carga. Registra `exit_status` antes de mudar o estado para `finished`. Um verificador que encontre `finished` sem um status de saída deve tratar o registro como danificado, não como bem-sucedido. Um verificador que encontre um diretório de execução, mas nenhum `started_at`, deve tratá-lo como uma falha de configuração incompleta.

Não grave `finished` por meio de um trap do shell e considere o trabalho concluído. Uma falha súbita do host, do armazenamento ou do sistema de arquivos, assim como um encerramento forçado, pode impedir a execução dos traps. Um marcador final é uma evidência quando está presente, mas a ausência dele não prova que a carga não terminou.

## Verifique uma execução em uma ordem que não engane você

Reconecte-se primeiro com um comando de status somente leitura. Não se reconecte lançando novamente a carga com os mesmos argumentos e esperando que a resposta fique clara.

Um verificador útil deve classificar o registro como `absent`, `running`, `finished` ou `damaged`. Este exemplo usa o formato de diretório acima e imprime fatos que uma pessoa ou um agente pode avaliar.

```sh
#!/bin/sh
set -eu

run_id=$1
run_dir="/var/lib/recorded-runs/$run_id"

if [ ! -d "$run_dir" ]; then
  printf '%s\n' 'state=absent'
  exit 0
fi

if [ ! -f "$run_dir/started_at" ]; then
  printf '%s\n' 'state=damaged reason=missing-start-marker'
  exit 2
fi

if [ -f "$run_dir/finished_at" ] && [ -f "$run_dir/exit_status" ]; then
  printf '%s\n' 'state=finished'
  printf 'exit_status=%s\n' "$(cat "$run_dir/exit_status")"
  printf 'started_at=%s\n' "$(cat "$run_dir/started_at")"
  printf 'finished_at=%s\n' "$(cat "$run_dir/finished_at")"
  exit 0
fi

if [ -f "$run_dir/pid" ]; then
  pid=$(cat "$run_dir/pid")
  if kill -0 "$pid" 2>/dev/null; then
    printf 'state=running pid=%s\n' "$pid"
    exit 0
  fi
fi

printf '%s\n' 'state=damaged reason=no-finish-record-and-pid-not-live'
exit 2
```

Execute-o como um novo comando SSH:

```sh
ssh ops@host /usr/local/sbin/check-recorded-run r-20260722-1842-a91f
```

A saída deve ser semelhante a uma destas:

```text
state=absent
```

```text
state=running pid=48192
```

```text
state=finished
exit_status=0
started_at=2026-07-22T18:42:19Z
finished_at=2026-07-22T18:47:03Z
```

O estado incômodo `damaged` pertence ao protocolo. Deixá-lo de fora força o verificador a transformar a falta de evidências em um palpite otimista. Se o host reiniciou enquanto o comando era executado, `kill -0` falhará e nenhum marcador final existirá. A resposta correta é inspecionar o efeito pretendido e os logs da aplicação, depois decidir se é necessária uma ação de reconciliação.

Não faça o verificador usar `ps | grep`. Ele encontrará processos não relacionados, os nomes dos comandos podem mudar e os formatos de saída variam. `kill -0` é apenas uma indicação de que o PID registrado está ativo. Não é uma prova de conclusão, por isso o verificador confere os artefatos finais antes de consultar o PID.

## Mesmo um status de saída finalizado pode não provar o efeito desejado

O registro final do wrapper prova o que o wrapper observou, não necessariamente o que o mundo externo aceitou. Isso fica evidente com comandos que enviam solicitações.

Imagine um script remoto que cria um registro DNS por meio de uma API e depois grava `exit_status=0`. O script pode receber sucesso da API antes que um resolvedor veja o novo registro. Um script de implantação pode terminar com sucesso depois de enviar uma atualização que mais tarde falha nas verificações de saúde. Uma ferramenta de banco de dados pode informar uma conexão bem-sucedida enquanto uma instrução dentro de um procedimento com várias etapas é confirmada e uma instrução posterior falha.

Toda operação precisa de uma pós-condição compatível com seu efeito. Ela deve ser segura para ser lida repetidamente e específica o bastante para rejeitar um resultado antigo ou não relacionado.

Para uma implantação, grave o ID de execução em um manifesto de release e consulte a versão ativa no serviço. Para uma alteração de banco de dados, consulte a tabela de migrações pelo identificador e pelo checksum da migração. Para um artefato gerado, compare um digest SHA-256 calculado previamente depois que o arquivo for colocado no caminho final. Para uma solicitação de API, forneça o token de idempotência do provedor quando houver essa opção e depois consulte o recurso criado por esse token ou por um ID de solicitação que você tenha armazenado.

O pior desenho é um script que emite «concluído» depois de enviar uma solicitação e trata essa palavra como evidência. O arquivo stdout informa o que um processo imprimiu. Uma pós-condição informa o que o sistema contém agora.

Essa diferença também mostra quando uma operação não pode ser automatizada com segurança apenas por SSH. Se o comando remoto chama um serviço de terceiros que não oferece controles de idempotência e não permite procurar uma solicitação anterior, pode ser impossível classificar uma chamada interrompida. Envolva-a em uma aprovação humana ou em um processo compensatório. Mais tentativas não criarão as evidências que faltam.

## Idempotência é melhor do que teatro de recuperação

Um protocolo de verificação reduz a incerteza. Um comando idempotente reduz o custo dessa incerteza. Você precisa dos dois.

Uma operação idempotente chega ao mesmo estado desejado quando é aplicada novamente com a mesma solicitação. `mkdir -p /srv/app/cache` se aproxima desse modelo. `useradd deploy` não, a menos que o script primeiro verifique se a conta existente tem as propriedades esperadas. `curl -X POST /orders` não é idempotente, a menos que o serviço entenda um token de idempotência e trate um token repetido como a mesma solicitação.

Não confunda «o comando provavelmente não faz nada na segunda execução» com idempotência. Um comando de implantação pode sobrescrever um arquivo da mesma maneira duas vezes, mas disparar uma reinicialização em ambas. Uma ferramenta de migração pode reconhecer seu próprio histórico e ainda executar uma inicialização perigosa antes de verificar isso. Entenda o comportamento do comando e teste o caso de interrupção.

Estruture as solicitações em torno de um identificador de operação estável. Passe o mesmo ID ao wrapper remoto e, quando possível, ao sistema de destino. Um wrapper de release remoto poderia criar `/var/lib/recorded-runs/$run_id/effect` somente depois que o endpoint da versão ativa informar a versão solicitada. Uma chamada a uma API de provisionamento poderia usar `run_id` como valor de idempotência. Assim, uma nova tentativa SSH pode consultar os dois sistemas sobre a mesma unidade de trabalho.

Há uma regra prática: repita livremente uma leitura, repita uma criação apenas quando houver uma restrição persistente de unicidade e repita uma alteração em várias etapas somente depois que sua pós-condição tiver classificado a execução anterior. Isso é mais lento do que reenviar um comando às cegas, mas muito mais rápido do que limpar uma infraestrutura duplicada.

## Colocar o comando em segundo plano apenas muda o lugar do problema

`nohup`, `&`, `disown`, `tmux`, `screen` e os gerenciadores de serviços resolvem partes diferentes do problema. Nenhum deles transforma uma solicitação remota incerta em um resultado verificado.

`nohup` ajuda um processo a sobreviver a um sinal de desligamento em configurações comuns de shell. Um simples `nohup task &` ainda deixa você com arquivos de saída, um PID e nenhum registro estruturado de conclusão, a menos que você acrescente um. Ele também cria uma nova ambiguidade para o chamador: o shell remoto iniciou `nohup` ou a conexão morreu antes disso?

`tmux` e `screen` mantêm um ambiente interativo ativo. Funcionam bem quando um operador precisa se reconectar e inspecionar manualmente um comando longo. Funcionam mal como contrato de automação porque os nomes das sessões colidem, o histórico da tela não é um esquema de resultados e um terminal destacado não informa a outro sistema se o efeito solicitado ocorreu.

Um gerenciador de serviços é mais forte quando o trabalho é realmente um serviço ou um trabalho em fila. Por exemplo, um comando remoto pode enviar uma unidade nomeada e uma consulta posterior pode examinar seu ciclo de vida e seus logs. Use esse modelo quando o host já tiver um responsável operacional pelos trabalhos. Não instale um gerenciador de serviços para um comando administrativo de cinco segundos apenas para evitar a criação de um pequeno registro de execução.

A divisão útil é simples. Mantenha reparos interativos em um multiplexador de terminal. Coloque cargas agendadas ou de longa duração sob um gerenciador de serviços. Use um wrapper com registro para comandos imperativos cujo chamador SSH precise de uma resposta confiável depois de se reconectar.

## Keepalives reduzem a espera, mas não fecham a janela de ambiguidade

Os keepalives do cliente OpenSSH fazem conexões mortas serem percebidas mais cedo. Eles não garantem que um comando não tenha sido aceito antes da falha do caminho de rede.

Para hosts em que um cliente travado desperdiça tempo, uma configuração como esta é razoável:

```text
Host production-*
    ServerAliveInterval 20
    ServerAliveCountMax 3
    TCPKeepAlive yes
```

`ServerAliveInterval` envia mensagens no nível da aplicação pelo canal SSH criptografado quando nenhum dado chega. Se o cliente não receber respostas suficientes, ele encerra a conexão em vez de esperar indefinidamente. O OpenSSH documenta essa opção separadamente dos keepalives TCP, que operam na camada de transporte.

Use a configuração para limitar quanto tempo o chamador espera antes de iniciar a verificação. Não a descreva como garantia de entrega do comando. A desconexão ainda pode ocorrer depois que o host remoto aceita o comando e antes que o cliente receba o resultado.

O multiplexing exige o mesmo cuidado. `ControlMaster` e `ControlPersist` podem reutilizar uma conexão de rede existente para vários comandos SSH. Isso reduz o custo de configuração, mas uma conexão mestre quebrada também pode afetar várias chamadas ao mesmo tempo. O manual do OpenSSH informa que um mestre persistente continua em segundo plano depois que o cliente original sai. Isso é útil operacionalmente, mas não acrescenta evidências de conclusão para um comando enviado por ele.

Para automação, defina um timeout explícito de conexão, limites de atividade compatíveis com o ambiente e uma rota de verificação independente da sessão SSH original. Uma falha rápida só é útil se a próxima ação for uma consulta de status, não uma nova tentativa cega.

## Crie o teste de falha antes de precisar dele às duas da manhã

Um protocolo que nunca foi interrompido é apenas um esboço de projeto. Teste-o com uma operação segura, mas lenta o bastante para permitir o corte da conexão em pontos diferentes.

Comece com uma carga que grave um arquivo de progresso numerado, faça pausas entre as etapas e grave um marcador de efeito final. Inicie-a pelo wrapper. Mate o cliente SSH local depois que o marcador remoto `started_at` aparecer, reconecte-se e execute o verificador. Repita encerrando a carga remota antes que ela grave `finished_at`. Por fim, simule uma reinicialização do host se o seu ambiente permitir.

As classificações esperadas devem ser explícitas:

- Antes de o wrapper reivindicar o ID de execução, a verificação retorna `absent`.
- Durante a execução da carga, a verificação retorna `running`.
- Depois da conclusão normal, ela retorna `finished` com o status registrado.
- Depois de uma interrupção forçada ou perda do host, ela retorna `damaged`, seguida de uma verificação da pós-condição.

Teste também o envio duplicado. Invoque o mesmo ID de execução duas vezes quase ao mesmo tempo. Uma chamada deve vencer a reivindicação do diretório; a outra deve retornar um resultado de duplicata inequívoco sem executar a carga. Depois tente dois IDs diferentes direcionados ao mesmo recurso. Se isso criar uma condição de corrida, o wrapper precisa de um lock específico do recurso ou o sistema de destino precisa de uma regra de unicidade.

Mantenha os registros de execução por tempo suficiente para cobrir sua janela operacional de novas tentativas. Se um trabalho puder ser repetido um dia depois, mas o host apagar os registros após uma hora, você incluiu um cronômetro na sua incerteza. Proteja também os registros contra alterações casuais. A conta que verifica uma execução não deve poder editar `exit_status` nem substituir o log da carga. Em sistemas compartilhados, separe as funções de remetente, executor e leitor quando o modelo operacional permitir.

Quando agentes de IA invocam SSH, o isolamento de credenciais e a verificação de comandos precisam funcionar juntos. O Sallyport mantém as credenciais SSH no cofre e pode registrar a ação solicitada por um agente, enquanto o wrapper remoto fornece a resposta persistente sobre o próprio trabalho. O registro da ação informa qual processo pediu o comando; o ID remoto da execução informa o que aconteceu depois que a conexão se tornou incerta.

## A decisão segura sobre uma nova tentativa tem quatro resultados

Depois de uma desconexão, classifique antes de agir. Há quatro resultados úteis, e apenas um deles é uma nova tentativa automática.

Se a execução estiver `absent`, o wrapper remoto nunca criou seu registro. Você pode enviar a mesma operação com o mesmo ID se confiar no comportamento de criação única do wrapper. Se o comando puder ter sido executado fora do wrapper, examine primeiro o destino, porque o wrapper não pode provar o que o contornou.

Se a execução estiver `running`, espere ou cancele por um caminho de controle específico da operação. Não inicie outra cópia. Uma política de timeout pertence à carga ou ao gerenciador de trabalhos, não a uma segunda invocação SSH que dispute com a primeira.

Se a execução estiver `finished`, avalie o status de saída e depois verifique a pós-condição quando a operação tiver efeitos externos relevantes. Um status de saída zero junto com uma pós-condição falha significa que a operação falhou. Trate a verificação do efeito como autoridade.

Se a execução estiver `damaged`, pare de tratá-la como um problema de nova tentativa. É trabalho de reconciliação. Inspecione logs, entradas de journal, estado do destino e qualquer registro de idempotência. Decida se deve reparar o efeito parcial, marcar a operação como concluída ou enviar uma nova execução que trate explicitamente o estado observado. Essa decisão pode exigir uma pessoa, porque o registro ausente removeu a prova da qual a automação depende.

A mudança que vale a pena é pequena: todo comando cuja repetição possa causar danos recebe um ID de execução, um registro remoto de início, um registro remoto final e uma verificação do estado do destino. O SSH ainda vai desconectar. Sua automação não precisará mais fingir que sabe o que aconteceu.
