# O tratamento de solicitações com o cofre bloqueado pode repetir uma chamada?

Um cofre bloqueado precisa criar uma fronteira temporal rígida. Uma solicitação que chega a um gateway de ações antes de essa fronteira ser suspensa deve falhar naquele momento. Ela não deve ficar em uma fila, sobreviver a uma reconexão nem se tornar elegível porque alguém autentica depois no cofre.

Isso parece óbvio até você testar uma pilha de agente real. Agentes fazem novas tentativas. Clientes MCP se reconectam. Bibliotecas HTTP repetem chamadas depois que uma resposta é perdida. Workers preservam tarefas. Uma interface pode mostrar uma negação enquanto outro componente já reteve estado suficiente para executar a chamada mais tarde. Se você observar apenas o cartão de aprovação ou a transcrição do agente, pode não perceber a parte perigosa.

O teste descrito aqui responde a uma pergunta específica: o gateway descartou as chamadas recebidas enquanto o cofre estava bloqueado ou o desbloqueio fez alguma dessas chamadas antigas ser executada? Ele usa um receptor HTTP controlado, dois IDs de solicitação exclusivos e evidências dos dois lados do gateway. Faça esse teste antes de confiar a um agente autônomo qualquer endpoint capaz de alterar dinheiro, infraestrutura, código-fonte ou dados de clientes.

## O bloqueio do cofre deve interromper uma ação, não adiá-la

A regra esperada é simples: quando o cofre está bloqueado, o gateway nega toda ação que precise dele. O desbloqueio muda a resposta para uma chamada posterior. Ele não muda a resposta para uma chamada que já chegou.

Essa diferença importa porque uma solicitação tem um ciclo de vida. Um processo grava bytes em stdin. Um shim MCP analisa JSON-RPC. O gateway identifica uma ação configurada, pergunta se pode usar um segredo, injeta as credenciais se tiver permissão, abre uma conexão de saída e retorna um resultado. Um bug pode reter a solicitação em vários pontos desse caminho.

Um design seguro trata a decisão de bloqueio como definitiva para aquela chamada. O gateway pode registrar a negação, mas não deve manter uma função executável, um corpo de solicitação serializado, uma tarefa de saída ou um token de nova tentativa que possa ser executado mais tarde sob um cofre recém-aberto.

É comum misturar dois comportamentos diferentes:

- **Uma nova tentativa** é uma chamada nova enviada pelo agente depois que ele observa um erro ou uma mudança de estado.
- **Um replay** é a execução da chamada original, retida pelo gateway ou por um de seus auxiliares enquanto o acesso estava negado.

Uma nova tentativa pode ser legítima, embora ainda precise da autorização normal. Um replay atravessa uma fronteira de segurança sem uma nova decisão. Se você confundir os dois, pode aprovar o teste por engano ao desbloquear o cofre, ver uma solicitação chegar ao destino e presumir que ela veio de uma nova tentativa deliberada.

O Model Context Protocol não resolve esse problema de design. Seu transporte stdio usa mensagens JSON-RPC delimitadas por nova linha entre um processo de servidor iniciado pelo cliente e o próprio cliente. Solicitações JSON-RPC com um `id` recebem uma resposta correlacionada, enquanto notificações não recebem resposta. Essas regras de protocolo ajudam na correlação, mas não definem se um gateway pode preservar uma ação negada para executá-la mais tarde. O gateway precisa tomar essa decisão de forma explícita.

## O momento da chegada é anterior à chamada HTTP de saída

Uma solicitação chega quando o gateway tem informações suficientes para decidir se deve executá-la, não quando o servidor de destino recebe o tráfego. Se o cofre estiver bloqueado nesse ponto, a negação deve acontecer antes da injeção de credenciais e antes de o gateway entregar o trabalho a qualquer componente que possa sobreviver à decisão.

É aqui que os testes ficam descuidados. Alguém bloqueia o cofre, pede ao agente que chame uma API, espera um erro, desbloqueia e verifica se nenhuma solicitação aparece imediatamente. Esse teste não detecta novas tentativas atrasadas, workers bloqueados, pools de conexão nem temporizadores de nova tentativa do cliente. Também não detecta a possibilidade de a chamada ter chegado ao destino antes de a interface mostrar o erro.

Use três registros de tempo, coletados em locais independentes:

1. `T_lock`: quando foi confirmado que o cofre estava bloqueado.
2. `T_attempt`: quando o agente enviou o ID da solicitação antiga.
3. `T_unlock`: quando o cofre foi aberto novamente.

Continue observando o receptor depois de `T_unlock`. A espera deve ser maior que todas as novas tentativas e todos os timeouts configurados no cliente, no gateway e em qualquer intermediário. Se você não conhece esses valores, não escolha um intervalo curto só para obter um resultado tranquilizador. Descubra-os primeiro ou use um receptor que permaneça disponível tempo suficiente para revelar uma entrega atrasada.

Uma declaração de aceitação útil é mais precisa do que «a solicitação bloqueada falhou»:

> Para o ID de solicitação `locked-...`, o receptor controlado registra zero execuções antes e depois de `T_unlock`; para o ID `fresh-...`, enviado somente depois de `T_unlock`, o receptor registra exatamente uma execução.

Essa declaração captura os dois lados da falha. Ela detecta uma chamada antiga executada mais tarde e prova que o teste não falhou simplesmente porque o receptor ou a configuração da ação estava quebrada.

Não use o mesmo payload nas duas chamadas. Se ambas disserem `deploy=true`, você não conseguirá identificar qual delas chegou. Coloque o ID da solicitação no caminho da URL, em um campo JSON inofensivo e em um cabeçalho, se a ação configurada permitir. A redundância ajuda porque revela reescritas ou cache acidentais.

## É nas filas e nas novas tentativas que os defeitos de replay se escondem

Os defeitos de replay mais perigosos não são dramáticos. Eles costumam vir de código comum de confiabilidade, escrito por alguém que presumiu que uma falha de autorização se comporta como uma falha de rede temporária.

Considere uma sequência típica e problemática. O agente envia uma chamada de ferramenta MCP enquanto o cofre está bloqueado. O shim aceita a mensagem e cria um item interno de trabalho. A verificação do cofre retorna um erro de bloqueio, mas o worker o classifica como repetível porque o destino nunca foi alcançado. O chamador se desconecta ou a sessão termina. Mais tarde, o usuário desbloqueia o cofre. O worker acorda, encontra uma credencial utilizável e envia a solicitação HTTP original.

A interface pode parecer correta durante toda a sequência. O agente original recebeu um erro. O usuário viu que o cofre estava bloqueado. O destino recebeu uma credencial válida somente depois do desbloqueio. Mesmo assim, o gateway carregou uma ação através de uma fronteira onde ela deveria ter sido encerrada.

Estes são os padrões que vale a pena investigar:

- Um wrapper genérico de novas tentativas captura todos os erros, exceto entradas malformadas.
- Uma fila persistente de tarefas armazena a intenção antes da decisão do cofre.
- Um future ou promise espera pelo desbloqueio em vez de retornar um erro definitivo.
- Um caminho de reconexão reenvia uma solicitação em memória depois que o processo do cliente terminou.
- Um auxiliar em segundo plano mantém o estado de novas tentativas de forma independente do bloqueio do cofre.

A recomendação popular de «tentar novamente toda operação de rede que falhar» está errada nessa fronteira. Ela é popular porque falhas de transporte de rede são comuns e novas tentativas muitas vezes melhoram a entrega. Um cofre bloqueado não é uma falha de transporte. É uma recusa explícita de usar uma autoridade. Classifique-a como definitiva para aquela chamada.

Isso também se aplica ao cancelamento. A desconexão de um cliente não significa necessariamente que uma solicitação HTTP ou SSE foi cancelada. A especificação de transporte do MCP diz que uma desconexão pode ocorrer a qualquer momento e não deve ser interpretada por si só como cancelamento. Ela exige uma notificação explícita de cancelamento quando o cliente quer cancelar. Esse comportamento faz sentido para trabalhos longos, mas torna o estado local do gateway ainda mais importante: uma chamada negada não pode continuar executável apenas porque o estado do transporte ficou ambíguo.

## Crie um receptor que torne cada execução visível

Um receptor controlado fornece evidências melhores do que uma transcrição do chat do agente. Ele mostra se uma chamada de saída realmente chegou, qual ID ela carregava e quando chegou. Mantenha-o isolado da produção e faça com que seu único efeito colateral seja um registro local somente de acréscimo.

Execute este pequeno receptor Python em uma máquina e porta que o gateway consiga alcançar. Ele aceita solicitações POST, grava uma linha JSON para cada chegada e retorna uma resposta de sucesso inofensiva. De propósito, ele registra apenas um marcador curto do cabeçalho de autorização, não a própria credencial.

```python
# receiver.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime, timezone
import hashlib
import json

LOG = "receiver-events.jsonl"

class Receiver(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length).decode("utf-8", errors="replace")
        auth = self.headers.get("Authorization", "")
        auth_marker = hashlib.sha256(auth.encode()).hexdigest()[:12] if auth else None
        event = {
            "received_at": datetime.now(timezone.utc).isoformat(),
            "method": self.command,
            "path": self.path,
            "request_id": self.headers.get("X-Replay-Test-Id"),
            "auth_marker": auth_marker,
            "body": body,
        }
        with open(LOG, "a", encoding="utf-8") as log:
            log.write(json.dumps(event) + "\n")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(b'{"received":true}')

    def log_message(self, format, *args):
        return

HTTPServer(("127.0.0.1", 8787), Receiver).serve_forever()
```

Inicie-o com:

```bash
python3 receiver.py
```

O arquivo de saída terá um formato parecido com este:

```json
{"received_at":"2026-07-22T16:42:12.103841+00:00","method":"POST","path":"/replay-test/fresh-8f1c","request_id":"fresh-8f1c","auth_marker":"a4d7e02c1b9f","body":"{\"kind\":\"fresh\"}"}
```

Não coloque um segredo ativo no corpo da solicitação. Configure a ação de teste com uma credencial de teste dedicada e de baixo privilégio no cofre, deixando que o gateway a injete pelo canal HTTP normal. A impressão digital do cabeçalho do receptor prova apenas que algum valor de autorização chegou. Ela não grava o valor em um arquivo que pode sobreviver ao teste.

Antes do teste com o cofre bloqueado, envie uma solicitação comum enquanto ele estiver aberto. Confirme que o receptor a registra e que o caminho do endpoint configurado está correto. Depois, exclua `receiver-events.jsonl` ou mova-o para outro local. Começar com um registro vazio impede que uma solicitação anterior de configuração contamine o resultado.

## Execute o teste com duas chamadas deliberadamente diferentes

O teste precisa de um ID antigo, usado enquanto o cofre está bloqueado, e de um ID novo, criado somente depois do desbloqueio. Use rótulos com aparência aleatória, mas anote-os antes da execução. Rótulos fáceis de reconhecer facilitam a comparação na auditoria.

Por exemplo:

```text
old request ID:   locked-3d4a
fresh request ID: fresh-91ce
```

Prepare uma instrução para o agente ou uma solicitação de cliente MCP que chame sua ação HTTP configurada com estas informações:

```json
{
  "path": "/replay-test/locked-3d4a",
  "headers": {
    "X-Replay-Test-Id": "locked-3d4a"
  },
  "body": {
    "kind": "locked-period-attempt",
    "request_id": "locked-3d4a"
  }
}
```

O nome exato da ferramenta e o formato dos argumentos dependem da interface da ação que você configurou. Não simule um teste aprovado chamando o receptor diretamente com um comando de shell. A solicitação precisa passar pelo mesmo agente, shim MCP, gateway, cofre e caminho de ação HTTP que você pretende usar com confiança.

Agora execute a sequência sem improvisar:

1. Confirme que o registro do receptor está vazio e que o cofre está bloqueado.
2. Inicie um novo processo de agente e envie a chamada `locked-3d4a`.
3. Registre o erro no lado do agente e o horário. Não envie a solicitação novamente.
4. Mantenha o processo do agente ativo por um curto período de observação e depois encerre-o. Isso detecta comportamentos de nova tentativa imediatos e ligados ao processo.
5. Abra o cofre e espere durante todo o período de observação. Inspecione o registro do receptor repetidamente. `locked-3d4a` deve continuar ausente.
6. Somente depois dessa espera, inicie um novo processo de agente e envie `fresh-91ce`. O receptor deve registrar esse ID uma vez.

Mantenha as chamadas antiga e nova em processos de agente separados. Uma aprovação no nível da sessão ou um cache interno do cliente pode confundir o resultado. Você está testando se uma ação antiga consegue atravessar a fronteira do bloqueio, não se um único processo se lembra de algum estado de autorização.

Se a chamada bloqueada gerar um pedido de aprovação depois do desbloqueio sem que o agente envie uma nova chamada, pare. Isso é evidência de uma intenção retida. Se o receptor receber `locked-3d4a` em qualquer momento depois do desbloqueio, considere o teste de segurança reprovado, mesmo que o servidor tenha retornado 200 sem alterar dados.

## Inspecione os dois registros, mas não substitua o receptor pelos logs

Um diário do gateway ajuda a reconstruir o que ele acredita ter acontecido. O receptor prova o que aconteceu fora do gateway. Você precisa dos dois porque qualquer fonte isolada pode criar uma falsa sensação de segurança.

O Sallyport mantém um diário de sessões para as execuções dos agentes e um diário de atividades para chamadas individuais, ambos projetados a partir de um único registro de auditoria criptografado e encadeado por hash. Seu verificador offline está disponível por meio de `sp audit verify` e não precisa da chave do cofre para validar a cadeia. Execute esse comando depois do teste e preserve o resultado junto do registro do receptor e da transcrição do agente.

Use os registros para responder a perguntas concretas:

- A chamada antiga criou uma entrada de atividade e ela mostra um resultado negado?
- Qual processo de agente e qual autoridade de assinatura de código foram registrados pela sessão?
- Existe alguma atividade posterior com o ID de solicitação antigo, o caminho do endpoint ou uma janela de tempo correspondente?
- Uma nova sessão começou para a chamada nova feita depois do desbloqueio?
- A verificação de auditoria foi bem-sucedida para os registros coletados?

Não presuma que uma entrada de log dizendo «negado» prova que a solicitação nunca saiu da máquina. Um log registra a descrição que o gateway faz da própria decisão. O receptor controlado fornece a verificação independente. Da mesma forma, uma entrada ausente pode significar que seus termos de busca estavam errados, que os horários estavam fora de sincronia ou que o teste não usou a ação pretendida. Por isso a solicitação nova bem-sucedida é importante.

Para um teste de equipe repetível, salve quatro artefatos sob um único ID de execução: a transcrição do agente, o arquivo JSONL do receptor, a exportação do diário ou capturas de tela que identifiquem as chamadas e o resultado de `sp audit verify`. Evite colocar valores secretos em qualquer um deles.

## Notificações, lotes e reconexões precisam de casos separados

Um teste aprovado com uma solicitação não cobre todos os formatos de mensagem que um cliente de agente pode enviar. Solicitações com IDs são as mais fáceis de testar porque o JSON-RPC exige que a resposta carregue o mesmo ID. Notificações não têm ID e não recebem resposta, eliminando a prova normal de que o gateway as rejeitou. O JSON-RPC diz explicitamente que um servidor não deve responder a notificações.

Para um caminho compatível com notificações, dê ao payload de saída um marcador do lado do receptor, como `notification-77b2`. Bloqueie o cofre, faça a notificação ser emitida uma vez, desbloqueie e confirme que o marcador nunca chega. Não conclua que há segurança com base no silêncio da interface do cliente, porque o silêncio é o comportamento esperado do protocolo.

A entrada em lote merece um teste próprio se o cliente ou shim a aceitar. Coloque duas chamadas inofensivas no lote: uma enquanto o cofre está bloqueado e outra enviada somente depois do desbloqueio, em um lote separado. Não coloque chamadas antigas e novas no mesmo lote, porque um gateway que processe parte do lote antes de uma mudança de estado pode produzir um resultado impossível de interpretar.

Os testes de reconexão devem variar uma coisa por vez. Tente estes casos em execuções separadas:

- Mantenha o processo do agente ativo durante o desbloqueio.
- Encerre o processo do agente antes do desbloqueio.
- Reinicie apenas o cliente MCP ou o shim antes do desbloqueio.
- Desconecte o caminho de rede depois do erro de bloqueio e reconecte-o depois do desbloqueio.

A expectativa continua a mesma. O ID antigo do receptor nunca deve aparecer. Se um ID antigo surgir somente depois de uma reconexão, você encontrou um caminho de replay ligado à recuperação do transporte, e não à interface do cofre.

## A aprovação por sessão não pode consertar uma ação retida

A autorização por sessão e a aprovação por chamada decidem se uma ação atual pode prosseguir. Elas não tornam segura a retenção de uma ação rejeitada enquanto o cofre estava bloqueado.

A ordem importa. O bloqueio do cofre é absoluto: enquanto ele estiver bloqueado, toda ação é negada. Somente depois que o cofre abrir o gateway pode considerar o processo por trás de uma decisão de autorização da sessão ou pedir aprovação por chamada para uma credencial configurada. Inverter esse modelo mental leva as equipes a perguntar se um cartão de aprovação anterior deveria autorizar uma ação atrasada. Não deveria, porque a chamada bloqueada já não pode existir como trabalho executável.

Teste as fronteiras de forma independente. Primeiro, prove que uma chamada feita durante o bloqueio nunca é executada depois do desbloqueio. Depois, com o cofre aberto, teste se um novo processo de agente produz o comportamento esperado de autorização da sessão. Por fim, se uma credencial tiver a opção de aprovação por chamada, teste se cada novo uso pede aprovação novamente. Combinar os três testes em uma única execução longa dificulta identificar a origem das falhas.

Há também uma questão sutil de identidade do processo. Uma aprovação de sessão pertence a uma execução específica do processo do agente, não à ideia de «o mesmo assistente». Quando você reinicia um processo, trate-o como novo até que o gateway o identifique e autorize segundo suas próprias regras. Não deixe um script de teste esconder isso reutilizando um processo com uma conexão antiga.

## Transforme o resultado em um critério de lançamento

Execute este teste sempre que alterar o despacho do gateway, o código do ciclo de vida do cofre, o shim MCP, o comportamento de novas tentativas, a configuração do cliente HTTP ou o auxiliar que executa ações SSH. Um defeito de replay costuma entrar durante uma mudança de confiabilidade porque o código parece inofensivo na revisão: uma fila, um manipulador de reconexão ou uma cláusula `catch` ampla.

Uma versão deve ser reprovada se qualquer uma destas afirmações for falsa:

- O receptor controlado tem zero entradas para todos os IDs enviados enquanto o cofre estava bloqueado, inclusive depois que ele foi aberto.
- Um ID novo enviado depois do desbloqueio chega uma vez ao receptor pela mesma ação configurada.
- Um cliente ou agente reiniciado não consegue fazer o ID antigo aparecer.
- Os diários identificam os eventos esperados de negação e permissão sem duplicatas inexplicadas.
- A cadeia de auditoria é verificada para as evidências preservadas.

Quando possível, inclua o caso negativo em um teste de integração automatizado. O mecanismo de teste deve bloquear o cofre, enviar a chamada antiga, abrir o cofre, esperar durante uma janela limitada de novas tentativas e confirmar que o receptor não tem o ID antigo. Depois, deve enviar a chamada nova e confirmar uma chegada. Mantenha o receptor local e descartável para que o teste não tenha autoridade além do próprio arquivo de log.

O resultado ruim é fácil de descrever: uma pessoa abre o cofre para autorizar a próxima ação, e uma ação anterior é executada no lugar dela. Não aceite um gateway que apenas exiba o erro correto de bloqueio. Faça-o provar, com um observador externo, que esqueceu a solicitação antiga.
