# Guardrails — o que o código garante quando o prompt já falhou

> **Scope:** stacks=["dotnet"]
> **Layer:** 1 (on-keyword)
> **Keywords:** guardrail, guard de tool, regra proibida, validacao de pertinencia, fail-open guard, argumento nao confiavel, choke-point, veredito ternario
> **Read by Claude in:** implement (ao escrever regra de negócio que o modelo poderia violar) e review (ao avaliar o que garante o comportamento)

**Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não traz um guard de exemplo; o padrão é destilado de dois repositórios em produção, com o local de cada evidência citado na seção correspondente. Last-verified: 2026-09-08.

---

## Regra proibida vira código

**Instrução que já falhou ao vivo não se reforça escrevendo-a de novo em maiúsculas.**

Quando uma regra tem consequência real — não falar de um assunto, não prometer um prazo, não citar
um valor — ela sai do prompt e vira uma condição em C#. O prompt continua existindo para *orientar*;
o código existe para *garantir*. São papéis diferentes, e confundi-los é como se descobre em
produção que "o modelo geralmente obedece".

Três formas medidas em campo, em ordem crescente de força:

| Forma | Onde acontece | O que garante |
|---|---|---|
| **Gatilho literal avaliado antes do turno** | No compositor, sobre o texto de entrada | O assunto proibido nunca chega ao modelo com as tools que o executariam |
| **Veredito calculado em .NET antes do turno e reescrito depois sobre a resposta** | Antes e depois da chamada | O número que o produto publica é o número que o backend calculou, não o que o modelo escreveu |
| **Dado resolvido e pontuado em .NET, com o agente apenas copiando** | Fora do modelo | O modelo não tem oportunidade de errar a conta |

**O motivo empírico está escrito no próprio código de campo**, e é a frase mais útil desta seção:
*"o modelo obedece bem a 'uma entrada por elemento desta lista', mas erra feio quando precisa julgar
sozinho quais blocos têm dado"* — com a medição ao lado: uma saída esperada de dez itens saía com
dois.

A leitura correta disso não é "o modelo é ruim". É: **o modelo é bom em transformação enumerada e
ruim em julgamento de suficiência.** Dê-lhe a lista; faça o julgamento você.

---

## Onde fica o choke-point

Três lugares legítimos, e cada um pega uma classe diferente de problema. Escolher o errado é o que
faz um guard parecer instalado sem estar.

**(a) Antes do turno, no compositor.** Pega o que não deve nem ser oferecido: a fase errada, a tool
que este tenant não tem, o assunto proibido. É o mais barato — o modelo nem é chamado — e o único
que evita gastar dinheiro com uma chamada que já nasceu errada.

**(b) No middleware de invocação de função.** Pega o que o modelo pediu e não deveria ter pedido, e
decide **o que volta ao modelo**. Duas exigências de forma:

- **Vocabulário fechado de desfecho.** `ok` · `error` · `invalid_args` · `limit` · `unavailable`.
  Fechado porque a alternativa — devolver a mensagem da exceção — dá ao modelo texto livre para
  interpretar, e o modelo interpreta.
- **Argumento obrigatório ausente é barrado sem executar a tool**, lendo `required` do próprio
  schema JSON da função. Não se escreve a lista de obrigatórios num segundo lugar: ela já existe, e
  a segunda cópia é a que diverge.

**(c) Depois do run, como backstop.** É o único caminho em que o **código** invoca a implementação
da tool diretamente, quando a resposta indica que algo obrigatório não foi feito. Serve como rede,
nunca como mecanismo principal: um guard que só existe em (c) deixa o modelo decidir e depois
conserta, pagando as duas coisas.

O **mecanismo** de middleware — os três níveis, o registro no builder, a ordem — é de
`ai-agents-middleware-patterns`. Este standard usa esse mecanismo e não o repete.

---

## Argumento de tool é input não confiável

**Argumento vindo do modelo é entrada de usuário, com a agravante de ser gerada por algo que não
tem intenção.**

A regra: **identificadores e valores com efeito vêm da closure do turno, nunca do modelo.** A tool é
construída já sabendo em qual conversa, em qual tenant e para qual registro ela opera; o modelo só
fornece o que é genuinamente decisão dele.

Os dois lados existem no acervo, e vale ver os dois:

- **Padrão:** as tools de um produto são construídas sobre closures, com a regra escrita no código —
  *"o modelo nunca fornece o identificador da conta, o identificador do contato nem valores
  monetários"*.
- **Padrão, no outro projeto:** o filtro de tenant da busca vetorial *"sempre vem do caller, nunca
  do LLM"*.

O caso que essa regra impede é direto: uma tool que aceita o identificador do registro como
argumento é uma tool que, com o prompt certo na conversa errada, opera sobre o registro de outra
pessoa. Não é um risco hipotético de segurança — é um bug de correção que aparece como suporte.

Corolário: **valide o que sobra.** O que o modelo legitimamente decide (um texto, uma categoria, um
valor dentro de uma faixa) ainda passa pelo schema e pelas regras de domínio, como qualquer entrada.

---

## Pertinência, não presença

Esta é a seção difícil, e a que separa um guard de um `!= null`.

O guard fraco pergunta: *o campo veio preenchido?*
O guard forte pergunta: **o que veio se sustenta?**

Três formas medidas, em ordem crescente de custo e de força:

**(1) Rubrica determinística que soma 100.** As bandas espelham o que o prompt pediu, há uma
whitelist explícita de "sem dados" (para que ausência legítima não seja punida como erro), e — o
detalhe que faz a diferença — **os pontos que o backend já calculou são conferidos por igualdade**,
não por plausibilidade. O validador **acumula todos os erros** em vez de parar no primeiro, e
classifica o resultado em **incompleto × inválido**, que são coisas diferentes: o primeiro pede mais
dado, o segundo pede rejeição. Referência de campo: o maior arquivo da camada de agentes daquele
projeto, com 343 linhas, é justamente esse validador.

**(2) Trava anti-alucinação por proveniência.** A afirmação do modelo só sobrevive se houver
**rastro de origem**: um "encontrei" só permanece verdadeiro se o identificador correspondente
estiver no sink de captura — isto é, se alguma tool de fato o raspou. Sem rastro, o resultado é
**rebaixado** para "não encontrado", **preservando o texto de raciocínio original para auditoria**.
Preservar é parte do padrão: apagar o raciocínio destrói a única pista de por que o modelo
alucinou.

**(3) Corte de confiança, com o caso real que o justifica.** O modelo casou um perfil apenas pelo
nome e devolveu confiança 85, usando **a ausência de concorrentes como evidência a favor** — um
raciocínio que soa razoável e é inválido. O remédio foi um mínimo de confiança **por rede**, não um
mínimo global: a mesma pontuação significa coisas diferentes em fontes diferentes.

O que as três têm em comum: **elas comparam com algo que não veio do modelo** — uma conta do
backend, um rastro de execução, um limiar calibrado por fonte. Um guard que só olha para a resposta
do modelo está pedindo ao modelo que se avalie.

---

## Quando o guard não consegue decidir

A seção que o campo obriga, porque é o caso que mais acontece e o menos escrito.

**O acervo responde fail-open, sempre.** Configuração ausente resolve para "pode"; configuração
ilegível resolve para "pode"; os caminhos auxiliares (persona, sumarização, análise de memória, base
de conhecimento, multimodal) devolvem vazio ou nulo em qualquer falha, e o turno segue.

**Este standard ratifica o fail-open — com uma condição dura: o escape é registrado, nunca
silencioso.**

Ratifica porque a alternativa é pior no caso concreto: um guard que fecha quando não sabe derruba a
conversa por causa de uma configuração ausente, e o custo de um falso bloqueio numa conversa ao vivo
é maior que o de uma permissão a mais que alguém vai auditar.

A condição é dura porque sem ela o fail-open é indistinguível de um bug.

**Precedente que mostra a forma certa:** um guard **deliberadamente permeável** — rejeita a primeira
tentativa, a segunda passa — **com o escape instrumentado**, num contador dedicado. A permeabilidade
foi uma decisão de produto; a instrumentação é o que a torna revisável. *Guard permeável e medido é
engenharia; guard permeável e mudo é bug.*

**O anti-padrão, com número do campo:** **81 de 83 blocos `catch (Exception ex)` sem leitura do
`ex`**, no mesmo projeto. Isso é fail-open sem instrumentação em escala — e está registrado como
dívida pelo próprio time, o que significa que nem eles conseguem responder o que está sendo
engolido.

### Veredito ternário

O resultado de um guard é `pass` / `reject` / `not_evaluated`, com **`reject` vencendo `pass` na
mesma execução**.

Um booleano perde exatamente a informação que o fail-open produz: *não deu para avaliar*. Com
booleano, "não avaliado" vira "aprovado", e a métrica de aprovação passa a incluir tudo o que o
guard não conseguiu olhar — que é a métrica errada com a aparência da certa.

`not_evaluated` também é o que permite responder "quantas vezes este guard sequer rodou?", que é a
primeira pergunta a fazer quando um guard "nunca reprova nada".

---

## Anti-patterns

| Anti-pattern | Por quê é problema | Solução |
|---|---|---|
| Regra crítica reforçada em maiúsculas no prompt | O modelo geralmente obedece; "geralmente" não é garantia | Vira condição em código |
| Pedir ao modelo que julgue quais blocos têm dado | Medido: dez itens esperados, dois entregues | Enumere você; peça transformação, não julgamento |
| Guard só depois do run | Paga a chamada errada e depois conserta | Choke-point antes do turno, quando possível |
| Devolver a mensagem da exceção ao modelo | Texto livre que o modelo interpreta | Vocabulário fechado de desfecho |
| Lista de argumentos obrigatórios escrita num segundo lugar | A segunda cópia diverge do schema | Leia `required` do próprio schema da função |
| Identificador ou valor vindo do modelo | Com o prompt certo, opera no registro de outra pessoa | Closure do turno |
| Guard que checa presença (`!= null`) | Aprova qualquer coisa preenchida, inclusive alucinação | Compare com algo que não veio do modelo |
| Validador que para no primeiro erro | Uma correção por rodada; o custo vira o número de rodadas | Acumule todos, e separe incompleto de inválido |
| Rebaixar uma alucinação apagando o raciocínio | Destrói a única pista de por que aconteceu | Rebaixe preservando o raciocínio para auditoria |
| Limiar de confiança global | A mesma pontuação significa coisas diferentes por fonte | Limiar por fonte |
| Fail-open silencioso | Indistinguível de bug; 81 de 83 `catch` sem leitura | Fail-open **registrado** |
| Veredito booleano | "Não avaliado" vira "aprovado", e a métrica mente | `pass` / `reject` / `not_evaluated` |

---

## Checklist (verifiable by morph-eval)

- [ ] Toda regra com consequência real existe como condição em código, não apenas no prompt
- [ ] O choke-point escolhido está nomeado no `decisions.md`, com o que ele pega
- [ ] O middleware de tool devolve **vocabulário fechado** de desfecho
- [ ] Argumento obrigatório ausente é barrado **sem** executar a tool, lendo o schema da própria função
- [ ] Nenhum identificador com efeito vem do modelo — todos vêm da closure do turno
- [ ] Ao menos um guard compara a saída com algo que **não** veio do modelo
- [ ] Validador acumula erros e distingue incompleto de inválido
- [ ] Todo caminho fail-open **registra** o escape num contador ou span
- [ ] O veredito é ternário, e `reject` vence `pass` na mesma execução
- [ ] Existe forma de responder "quantas vezes este guard rodou, e quantas ele não conseguiu avaliar"

---

## References

- `ai-agents-middleware-patterns` — **dono do mecanismo** de middleware de invocação de função
  (os três níveis, o registro, a ordem). Este standard o usa e não o repete
- `ai-agents-conversational-agent-with-phases` — o choke-point (a) na prática: compositor,
  allow-list por fase e teto de tool calls
- `ai-agents-agent-spec` — a spec do agente, onde as tools do turno são declaradas
- `ai-agents-structured-output` — o schema cuja seção `required` o guard lê
- `ai-agents-observability-patterns` — onde o escape registrado vira span e métrica
- `ai-agents-production` — o panorama que hoje cita guardrails como um item de checklist

> **Lacuna declarada.** O par compilável deste standard seria um guard de exemplo em
> `templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/`, com o teste que o vê barrar um argumento
> obrigatório ausente **sem** executar a tool. **Ele ainda não existe** no ai-kit. Por isso o
> cabeçalho declara verificação documental e não carimba `provado por`.

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/guardrails.md v1.0 (2026-09-08)*
