Mudanças no schema da API: testes de contrato para agentes mais seguros
Mudanças no schema da API podem alterar as decisões de um agente de IA sem causar um erro. Use testes de contrato para campos, valores padrão, significado das respostas e novas tentativas.

Um agente pode transformar uma pequena mudança de API em um erro de ação real mais rápido do que um cliente operado por uma pessoa. Um campo de resposta renomeado pode fazer o agente selecionar todos os recursos em vez de apenas um. Um novo valor padrão pode ampliar uma consulta. Um objeto de status alterado pode parecer uma autorização para tentar novamente, e a nova tentativa pode repetir uma cobrança, uma implantação ou uma solicitação de exclusão.
O perigo é que essas falhas muitas vezes parecem normais no monitoramento convencional. O provedor retorna HTTP 200. O cliente não trava. O agente produz uma explicação plausível. Os testes de contrato precisam verificar o significado que o agente atribui aos dados da API, não apenas se o JSON pode ser analisado.
As decisões do agente tornam a compatibilidade uma propriedade de segurança
Um cliente de API usado por uma pessoa costuma falhar de forma visível quando uma resposta muda. Um botão mostra uma tabela vazia, aparece um erro no formulário e alguém investiga antes de tomar a próxima ação. Os agentes frequentemente transformam uma resposta diretamente em outra solicitação. Eles podem fazer essa conversão várias vezes antes que alguém veja uma transcrição.
Considere um agente de limpeza que chama GET /projects?state=inactive, lê o campo owner de cada item e pede aprovação antes de arquivar projetos fora de uma lista permitida. Mais tarde, o provedor troca owner por owner_id, mantendo o endpoint antigo e o mesmo código de status. Um analisador permissivo transforma um owner ausente em uma string vazia. Se a regra do agente disser que um proprietário vazio significa «sem atribuição», ele prepara uma solicitação de arquivamento muito mais ampla.
Isso não é uma falha de autenticação nem uma falha do prompt. É uma falha de interpretação na fronteira da API. O controle corretivo também deve ficar nesse ponto.
Trate qualquer campo que afete uma destas decisões como parte de um contrato de segurança:
- qual objeto o agente pode alterar
- se um objeto pode receber a ação
- o escopo, o valor ou o destino da ação
- se uma ação anterior foi concluída, falhou ou precisa ser repetida
- se o agente deve pedir autorização a uma pessoa antes de continuar
Essa distinção importa porque a compatibilidade de transporte é muito mais fraca que a compatibilidade comportamental. Um serviço pode preservar endpoint, método, esquema de autenticação e sintaxe JSON e ainda assim quebrar a decisão tomada depois da resposta. As equipes costumam chamar isso de mudança sem impacto porque os SDKs existentes continuam desserializando os dados. Para um chamador autônomo, esse rótulo pode ser perigosamente incompleto.
Crie um mapa pequeno de todos os valores de resposta que chegam a um seletor de ação, a uma decisão de autorização ou a um ramo de nova tentativa. Não comece com todas as propriedades de uma especificação de API gigantesca. Comece pelos valores cuja interpretação errada muda o que o agente pode fazer.
Uma diferença de schema não prova compatibilidade comportamental
As ferramentas de comparação de schemas detectam mudanças úteis, mas não conseguem dizer se uma mudança é segura para determinado agente. Elas comparam declarações. Um agente depende de semânticas que muitas vezes ficam fora dessas declarações.
Suponha que um provedor mude limit de um parâmetro opcional com valor padrão implícito de 100 para um parâmetro opcional com valor padrão implícito de 1000. Uma comparação padrão de OpenAPI pode não mostrar nenhuma mudança em propriedades obrigatórias. Ainda assim, um agente que omite limit agora pode examinar dez vezes mais objetos e enviar uma ação em lote para todos eles.
A OpenAPI Specification descreve um Schema Object como um superconjunto do vocabulário do JSON Schema e diz que suas propriedades fornecem informações para payloads de solicitação e resposta. Isso é material útil para documentação e validação. Não diz que state: "pending" permite uma nova tentativa nem que um limit omitido continua limitado a 100. Essas são afirmações sobre o fluxo de trabalho, e seus testes precisam declará-las em termos claros.
Da mesma forma, a anotação default do JSON Schema não obriga um validador ou cliente a inserir um valor. Muitos desenvolvedores presumem que isso acontece. A documentação do JSON Schema trata default como dado de anotação, não como um comando que altera uma instância. Se sua segurança depende de um valor, faça o cliente enviá-lo explicitamente e teste a solicitação exata enviada. Não espere que uma anotação do schema corrija uma omissão.
Use uma ferramenta de comparação como um sinal de alerta. Depois, classifique cada mudança identificada pelo caminho de ação que ela pode afetar:
- Um identificador renomeado pode mudar o alvo selecionado.
- A adição de um valor a um enum pode levar o analisador a um ramo não testado.
- A mudança de um valor padrão pode alterar o escopo sem nenhuma mudança no código da solicitação.
- A mudança de representação pode inverter o significado de conclusão ou falha.
O inverso também importa. Uma ferramenta pode informar a adição de um campo descritivo que nenhum agente lê. Isso merece revisão, mas não um congelamento da produção. A revisão de compatibilidade melhora quando acompanha os dados até uma decisão, em vez de tratar cada linha do schema como igualmente arriscada.
Os testes de contrato precisam fixar solicitações e decisões
Um teste de contrato útil tem duas partes: verifica a solicitação que o agente realmente envia e depois verifica a ação que o agente propõe ao ler uma resposta do provedor. Testar apenas uma das partes deixa um grande ponto cego.
Para valores padrão, capture uma solicitação HTTP real em um servidor de teste local ou em um ambiente de testes do provedor. O exemplo de Python a seguir usa httpx.MockTransport para inspecionar uma solicitação enviada. Ele evita a falha comum em que um cliente depende silenciosamente do valor padrão do provedor para uma operação destrutiva.
import httpx
seen = []
def handler(request: httpx.Request) -> httpx.Response:
seen.append({
"method": request.method,
"path": request.url.path,
"query": dict(request.url.params),
})
return httpx.Response(200, json={"items": []})
client = httpx.Client(transport=httpx.MockTransport(handler))
response = client.get(
"https://api.example.test/projects",
params={"state": "inactive", "limit": "100"},
)
assert response.status_code == 200
assert seen == [{
"method": "GET",
"path": "/projects",
"query": {"state": "inactive", "limit": "100"},
}]
A asserção importante não é a resposta 200. É o limit explícito. Se uma refatoração remover esse parâmetro, o teste falhará antes que um novo valor padrão do provedor possa ampliar a seleção.
Depois, teste a decisão. Mantenha a função de planejamento do agente separada do código que realiza a chamada HTTP, para que o teste possa inspecionar uma ação proposta sem executá-la.
from dataclasses import dataclass
@dataclass
class ArchivePlan:
project_ids: list[str]
requires_approval: bool
def plan_archives(items: list[dict], allowed_owners: set[str]) -> ArchivePlan:
targets = []
for item in items:
owner = item.get("owner")
if owner is None:
raise ValueError("provider response lacks owner")
if item["state"] == "inactive" and owner in allowed_owners:
targets.append(item["id"])
return ArchivePlan(targets, requires_approval=bool(targets))
items = [
{"id": "p17", "state": "inactive", "owner": "team-a"},
{"id": "p18", "state": "inactive", "owner": "team-b"},
]
plan = plan_archives(items, {"team-a"})
assert plan.project_ids == ["p17"]
assert plan.requires_approval is True
Esse teste faz uma escolha que o código permissivo costuma evitar: um owner ausente gera um erro. Para um campo usado na seleção de uma ação, falhe de forma segura. Retornar uma string vazia, None ou um valor alternativo presumido pode manter o processo em andamento, mas troca uma falha de integração detectável por um plano potencialmente inseguro.
Mantenha os fixtures pequenos o suficiente para que uma pessoa revisora entenda por que cada item aparece. Um fixture com mil objetos pode parecer com a produção, mas esconde a condição que você pretendia proteger.
Campos renomeados precisam de um comportamento de falha explícito
Um campo renomeado deve interromper o caminho de ação, a menos que você ofereça suporte deliberadamente aos dois nomes durante uma migração definida. O tratamento silencioso de alternativas parece resiliente em uma demonstração e cria semânticas não revisadas na produção.
A pior versão é esta:
owner = item.get("owner", "")
if owner not in blocked_owners:
archive(item["id"])
Quando owner desaparece, todos os itens parecem ter um proprietário que não está bloqueado. O analisador fez exatamente o que o código pediu. A pessoa engenheira que escreveu isso provavelmente queria evitar um KeyError. Essa pequena conveniência transformou a ausência de dados em permissão para agir.
Escreva testes para as três situações: o campo esperado, o campo antigo se houver uma promessa temporária de compatibilidade e nenhum dos dois campos. O terceiro teste deve indicar se o agente para, ignora o objeto ou pede esclarecimentos. Para identidade do alvo, estado de autorização e escopo da ação, parar costuma ser a resposta certa.
Se você oferecer um alias, deixe sua precedência clara e temporária:
def read_owner(item: dict) -> str:
if "owner" in item:
return item["owner"]
if "owner_id" in item:
return item["owner_id"]
raise ValueError("owner identity missing")
Esse código ainda precisa de um teste para dados contraditórios. Se os dois campos chegarem e divergirem, não escolha um silenciosamente. Gere um erro e peça ao provedor que resolva a ambiguidade. Uma camada de compatibilidade deve preservar um significado antigo conhecido, não inventar um critério de desempate para registros inconsistentes.
Às vezes as equipes argumentam que o parsing permissivo protege contra a evolução do provedor. Ele protege contra adições inofensivas quando você ignora campos desconhecidos. Não protege contra campos ausentes que controlam uma ação. Nesses casos, o comportamento deve ser o oposto: aceite informações extras por padrão, mas rejeite a ausência de um significado obrigatório.
Valores padrão e omissões exigem testes separados
Uma propriedade omitida, um valor nulo explícito e um valor explícito são três solicitações diferentes. Os agentes costumam misturá-los porque os serializadores de uso geral também fazem essa distinção desaparecer.
Um construtor de solicitações pode omitir dry_run quando seu valor interno é None. O provedor pode interpretar a omissão como false. Uma versão posterior do provedor pode mudar o significado da omissão para «usar a configuração da conta», e essa configuração pode ser falsa para um tenant e verdadeira para outro. O código do agente não mudou, mas a ação mudou.
Coloque as opções que influenciam decisões em uma de duas categorias. Para opções cujo comportamento seguro é conhecido, envie o valor sempre. Para opções que precisam de uma escolha da pessoa operadora, exija essa escolha antes de construir a solicitação. Evite uma terceira categoria chamada «deixar o servidor decidir» em ações destrutivas ou visíveis externamente.
Teste a serialização com uma tabela de casos exatos. O objetivo é inspecionar a representação transmitida, não apenas o objeto da linguagem antes da serialização.
| Intenção | Representação enviada | Significado esperado no provedor |
|---|---|---|
| Ler projetos inativos | state=inactive&limit=100 | Uma seleção limitada |
| Simular arquivamento | {"dry_run": true} | Nenhum arquivamento ocorre |
| Arquivar um projeto | {"project_ids":["p17"],"dry_run": false} | Apenas p17 pode ser alterado |
| Nenhuma escolha da pessoa operadora para o modo | A solicitação é rejeitada localmente | Nada é enviado ao provedor |
Seja igualmente rigoroso com a paginação. Uma resposta que adiciona next_cursor pode levar um agente a buscar automaticamente todas as páginas. Isso pode ser razoável para um relatório e imprudente para um planejador de ações. Teste tanto o número máximo de objetos que o planejador pode considerar quanto a condição que permite uma segunda página. Um cursor é um mecanismo de continuação, não um consentimento para um escopo ilimitado.
Os valores padrão do provedor também importam nas respostas. Se uma API começar a omitir archivable quando o valor for falso, um código como if item.get("archivable", True) mudará seu comportamento na direção insegura. Para um campo que concede permissão, use uma comparação explícita como item.get("archivable") is True. É menos elegante e muito mais fácil de auditar.
A validação das respostas deve preservar o significado, não apenas a estrutura
A validação de respostas deve distinguir dados malformados de dados desconhecidos, mas inofensivos. O rigor indiscriminado quebra clientes quando os provedores adicionam campos. A permissividade indiscriminada transforma a falta de evidências em uma suposição.
Defina um modelo de resposta estreito em torno dos valores que alimentam a próxima ação. Para cada valor, especifique o tipo, os estados permitidos e se a ausência interrompe o fluxo. Um identificador de projeto precisa de mais que string: o agente precisa de um identificador estável e não vazio que corresponda ao identificador enviado posteriormente na solicitação de arquivamento. Um status precisa de mais que string: o agente precisa de um estado enumerado com uma ação documentada para cada membro.
Por exemplo, este analisador lida com uma representação de status alterada sem se conceder permissão para tentar novamente:
ALLOWED_STATES = {"queued", "running", "succeeded", "failed"}
def retryable(job: dict) -> bool:
status = job.get("status")
if status not in ALLOWED_STATES:
raise ValueError(f"unknown job status: {status!r}")
return status == "failed" and job.get("retry_allowed") is True
Se o provedor mudar status de uma string para um objeto como {"phase":"failed"}, esse código para. A interrupção é correta até que alguém decida como a nova representação corresponde ao fluxo antigo. Se o provedor adicionar cancelled, parar também é correto até que a equipe decida se o cancelamento é terminal, permite uma nova tentativa ou exige uma pessoa.
Não transforme todo valor desconhecido de um enum em uma emergência em uma exibição somente de leitura. A consequência deve corresponder à ação. Um agente de relatórios pode identificar um estado desconhecido e continuar. Um agente que tentará novamente um trabalho de cobrança ou excluirá um recurso precisa parar antes de agir sobre um estado que não entende.
Teste também as relações entre os campos. Uma resposta pode ser estruturalmente válida e ainda conter uma combinação autocontraditória, como status: "succeeded" e retry_allowed: true. A validação do schema normalmente não expressa todas as invariantes de negócio. Um teste de contrato deve afirmar que um trabalho concluído não cria um plano de nova tentativa, independentemente de um booleano inconsistente.
Teste toda a rota da ação, não um mock conveniente
Os testes unitários dos analisadores são necessários, mas não provam que o agente implantado envia a solicitação pretendida pela rota real de credenciais e execução. Bibliotecas de serialização, wrappers, adaptadores de ferramentas e middleware de novas tentativas podem alterar o comportamento de formas que uma chamada direta de função não revela.
Execute na CI um servidor de contrato local que registre solicitações e retorne fixtures versionados. Aponte a configuração de ferramentas do agente para esse servidor. O teste deve conduzir uma instrução realista, esperar o plano proposto ou o registro da ação e verificar a sequência registrada: método, caminho, consulta, cabeçalhos seguros para inspeção, corpo e quantidade de ações.
Não coloque segredos reais nesse ambiente. Use uma credencial de teste sem autoridade e verifique que o agente nunca a recebe em seu prompt, no resultado da ferramenta, no texto de uma exceção ou em um trace. Um teste que registra cabeçalhos de solicitação sem cuidado pode recriar a exposição de credenciais que pretendia evitar.
Para agentes que fazem chamadas HTTP ou SSH externas por meio do Sallyport, a rota da ação pode manter as credenciais fora do agente e ainda preservar um resultado inspecionável. Essa fronteira não corrige uma interpretação errada da resposta, portanto execute os contratos de schema e de decisão antes de permitir a ação externa.
Inclua fixtures de falha, não apenas respostas esperadas. Simule as condições exatas que os provedores produzem durante mudanças: um campo de seleção ausente, um novo membro de enum, um resultado vazio com cursor de continuação, uma mudança de tipo de conteúdo e uma resposta 200 contendo um objeto de erro. Um corpo de erro com status 200 é especialmente comum em APIs antigas. Se o analisador presumir que toda resposta 200 contém o formato de sucesso, pode fabricar um plano vazio ou repetir uma solicitação que já foi concluída.
Ao testar novas tentativas, verifique o comportamento de idempotência. Faça o servidor de contrato retornar uma primeira resposta que expire depois de registrar a solicitação e uma segunda resposta para a nova tentativa. O teste deve provar que o cliente envia um token de idempotência quando a API oferece suporte a ele ou que para e pede confirmação quando não consegue saber se a primeira ação foi concluída. Repetir uma leitura geralmente é inofensivo. Repetir uma transferência, um e-mail, uma implantação ou uma solicitação de exclusão não é.
Os bloqueios de lançamento devem impedir quebras semânticas
Execute comparações de schemas, testes de contrato do provedor e testes de decisão do agente no caminho de mudança, tanto para produtores de API quanto para consumidores que são agentes. Um processo de lançamento que os executa apenas depois da implantação transforma os testes em documentação de incidente.
Para uma mudança do provedor, exija um registro de revisão que responda a quatro perguntas concretas: quais premissas dos consumidores mudam, qual era o comportamento antigo, por quanto tempo os dois comportamentos continuarão disponíveis e qual fixture demonstra o novo comportamento. Isso dá menos trabalho do que discutir uma regressão em produção com logs incompletos.
Para uma mudança no agente, execute a suíte existente de fixtures do provedor antes do merge. Se o agente agora usa um campo recém-adicionado, inclua fixtures para sua ausência e para valores fora do caminho esperado. Mudanças no texto do prompt também podem alterar os argumentos das ferramentas, portanto teste a chamada de ferramenta emitida pelo agente completo em vez de presumir que o planejador continuará escolhendo os mesmos parâmetros.
Fixtures versionados tornam a revisão prática. Armazene um identificador como projects-list-v1 com o par esperado de solicitação e resposta. Quando um provedor introduzir deliberadamente projects-list-v2, mantenha o fixture antigo até o fim da política de migração. Não substitua o JSON antigo e chame os testes de atuais. Você perderá a prova da compatibilidade que foi abandonada.
Um bloqueio útil informa as falhas em linguagem operacional. «Campo owner esperado ausente, planejamento do arquivamento interrompido» diz à pessoa revisora o que aconteceu. «ValidationError no caminho items.0» é melhor que nada, mas obriga a pessoa revisora a reconstruir o risco durante o lançamento.
As telas de aprovação não corrigem um plano enganoso
A aprovação humana continua sendo um bom controle para ações externas, mas chega tarde demais se o agente tiver criado o plano errado a partir de um contrato alterado. Uma pessoa que vê «Arquivar 847 projetos inativos» pode rejeitar a ação. Uma pessoa que vê «Arquivar o projeto p17» não consegue saber se p17 veio de um campo owner ausente, de um valor padrão ampliado ou de um analisador que confundiu cancelled com failed.
Faça com que os registros de aprovação incluam os dados de decisão que merecem revisão humana: identificadores dos alvos, quantidade, modo solicitado e os campos da resposta que tornaram o alvo elegível. Mantenha o registro compacto. Despejar JSON bruto para a pessoa aprovadora transfere a tarefa de análise do código para alguém cansado.
Preserve separadamente um trace que permita a uma pessoa engenheira reconstruir a ação. Capture a resposta do provedor ou um resumo protegido dela, a versão do analisador, a versão do fixture de contrato, a solicitação gerada e a resposta resultante. Um registro de auditoria à prova de adulteração ajuda depois do ocorrido, mas deve apontar para a fronteira da decisão, em vez de apenas registrar que uma chamada HTTP aconteceu.
Na próxima vez que uma equipe de API disser que uma mudança de resposta é cosmética, peça que execute a suíte de contratos do agente. Se a suíte falhar, a mudança tem um comportamento associado a ela. Corrija isso antes que uma resposta 200 cordial se transforme em uma ação insegura.
FAQ
Uma pequena renomeação de campo da API pode tornar um agente de IA inseguro?
Sim. A renomeação de um campo pode fazer o agente seguir um caminho alternativo, tratar um valor ausente como seguro ou enviar novas solicitações malformadas. A API pode continuar retornando 200, por isso as verificações comuns de disponibilidade não detectam o problema.
Snapshots das respostas da API são suficientes para garantir a segurança de um agente?
Os testes de snapshot ajudam a perceber mudanças, mas costumam gerar diferenças ruidosas e dizem pouco sobre o comportamento. Use-os para uma revisão deliberada e depois adicione asserções para os poucos campos e relações que controlam uma ação.
Devo usar contratos orientados pelo consumidor ou testes de OpenAPI?
Um contrato orientado pelo consumidor costuma ser mais adequado quando o fluxo do agente depende de uma API específica. Ele registra o que o consumidor precisa, enquanto o schema do provedor continua útil para documentar toda a superfície pública.
Qual é a diferença entre um campo JSON ausente e um campo nulo para um agente?
Um campo ausente significa que o produtor não enviou a informação. Um campo nulo significa que o produtor enviou o campo e declarou que não existe um valor. Os agentes não devem tratar esses estados como equivalentes, a menos que o contrato diga isso explicitamente.
Os testes de contrato devem rejeitar campos desconhecidos nas respostas?
Em geral, não. Campos desconhecidos costumam aparecer quando um provedor adiciona dados úteis, e rejeitá-los torna os clientes frágeis. Rejeite-os apenas quando aceitar um campo não reconhecido puder alterar uma ação sensível ou quando a resposta representar um vocabulário fechado de comandos.
Como testar uma mudança no valor padrão de uma API?
Trate um novo valor padrão como uma mudança de comportamento sempre que ele afetar escopo, paginação, permissões, custos, exclusões ou envios. Teste a solicitação com o campo omitido, pois é nesse caso que o padrão do provedor assume o controle.
Como testar o caminho real da API usado por um agente?
Teste a menor sequência real: a solicitação do agente, a injeção de credenciais ou o limite de autorização, a solicitação ao provedor, a resposta do provedor, o analisador e a ação proposta. Simular apenas a chamada final da API não revela erros de serialização ou de interpretação da resposta entre essas etapas.
A validação com JSON Schema garante um comportamento seguro do agente?
Não. A validação do schema confirma que o payload tem uma estrutura permitida, mas não prova que o agente escolheu a conta certa, interpretou um estado corretamente ou limitou a ação aos objetos pretendidos. Adicione asserções para essas decisões.
O que uma equipe de API deve fazer antes de remover um campo usado por agentes?
Mantenha o comportamento antigo disponível durante o período de migração publicado, emita sinais claros de descontinuação e execute as duas versões do contrato na CI enquanto os clientes migram. Não reutilize silenciosamente o nome de um campo para outro significado.
A aprovação humana pode compensar contratos de API quebrados?
A aprovação pode interromper uma chamada, mas não torna inteligível uma solicitação enganosa para a pessoa que vai aprová-la. Faça as verificações do schema antes de a chamada chegar a uma pessoa e mostre detalhes de aprovação que identifiquem o alvo e o escopo.