8 min de leitura

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

Teste o tratamento de solicitações com o cofre bloqueado para confirmar que chamadas negadas de agentes de IA são descartadas, nunca ficam em uma fila nem são repetidas depois que o cofre é aberto.

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.

# 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:

python3 receiver.py

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

{"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

Revogue uma sessão rapidamente
Revogue uma execução de agente no diário de sessões quando esse processo precisar perder o acesso.

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:

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:

{
  "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

Autorize o processo, não a intenção
Autorize um novo processo de agente uma vez por sessão, depois de ver a autoridade de assinatura do código.

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

Mantenha as credenciais HTTP no cofre
Encaminhe chamadas HTTP do agente pelo app, sem expor credenciais bearer, basic ou de cabeçalhos personalizados.

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.

FAQ

Como provar que uma solicitação bloqueada foi descartada, e não atrasada?

Uma negação prova apenas que o gateway recusou uma chamada em determinado momento. Um teste de descarte vai além: desbloqueie o cofre, espere mais do que todas as janelas de novas tentativas relevantes e prove que o ID da solicitação antiga nunca chegou a um destino controlado. Depois, envie uma nova solicitação com outro ID para confirmar que o caminho de teste continua funcionando.

Um agente pode simplesmente tentar novamente depois que eu desbloquear o cofre?

Não. O gateway não pode impedir que um agente faça uma nova solicitação depois de descobrir que o cofre está aberto. Seu teste deve diferenciar a nova solicitação após o desbloqueio da solicitação antiga feita antes dele, usando IDs diferentes e um receptor que registre cada uma separadamente.

Um timeout é prova suficiente de que nenhuma ação aconteceu?

Um timeout é ambíguo porque o cliente pode tentar novamente, o gateway ainda pode estar processando a chamada ou o destino pode tê-la recebido antes de o timeout aparecer. Considere um timeout como falha no teste até que o registro do receptor e os registros do gateway mostrem o que aconteceu.

Como testar notificações JSON-RPC enquanto o cofre está bloqueado?

As notificações exigem mais atenção porque o JSON-RPC não fornece ao chamador uma resposta para correlacionar com o resultado. Para uma ação sensível, use no teste uma solicitação com ID ou adicione um marcador independente no receptor que torne a execução visível.

Reiniciar o agente torna segura uma solicitação antiga?

Um novo processo de agente precisa passar novamente pela autorização da sessão, se esse controle estiver ativado, mas isso por si só não prova que as chamadas feitas enquanto o cofre estava bloqueado foram descartadas. Teste a reinicialização do processo e o desbloqueio do cofre como mudanças de estado separadas. Depois, verifique se alguma delas libera uma intenção antiga.

As chaves de idempotência resolvem replays depois do desbloqueio?

Somente se o endpoint de destino tornar a entrega duplicada inofensiva e registrar a chave de idempotência. Uma chave de idempotência evita efeitos repetidos no destino, mas não autoriza o gateway a manter uma ação que chegou enquanto o cofre estava bloqueado.

Posso executar este teste em uma API de produção?

Use um receptor sob seu controle, um caminho de teste separado e dados sem valor operacional. Não aponte este teste para endpoints de produção de cobrança, implantação, DNS ou controle de código-fonte só porque eles já existem.

Qual é o teste manual mais rápido para detectar um replay?

Primeiro verifique no receptor controlado se o ID antigo não aparece. Depois do desbloqueio, envie um novo ID e confirme que ele chega uma vez. Se o ID antigo aparecer depois do desbloqueio, trate o resultado como uma falha de segurança, mesmo que o destino não tenha alterado nada.

Que evidências de auditoria devo guardar para este teste?

Você precisa de um registro que capture a negação ou o estado bloqueado, a identidade da sessão do agente e o valor de correlação da solicitação. Um registro de auditoria à prova de adulteração ajuda a mostrar que esses dados não foram editados depois, mas não substitui as evidências do receptor.

O que um mecanismo de bloqueio do cofre deve garantir?

Uma interface bem definida faz o gateway rejeitar a chamada antes que ela entre em qualquer fila persistente de trabalho ou mecanismo de novas tentativas. Abrir o cofre muda a permissão para chamadas que chegam depois. Isso não pode transformar chamadas rejeitadas anteriormente em trabalho elegível.

Sallyport

O Sallyport executa chamadas de API e comandos SSH pelo seu agente de IA. As chaves ficam em um cofre local no seu Mac; você aprova cada execução e toda ação vai para um registro selado.

© 2026 Sallyport · Código aberto sob Apache-2.0 · Oleg Sotnikov