45 · Protegido e provado¶
Idiomas
O seletor lê turnos escritos em inglês e em português do Brasil (pt-BR). O idioma é escolhido, nunca adivinhado a partir do turno: na página Settings do console (Language the selector reads), em language numa requisição de plano, ou no settings.language do seu arquivo de vocabulário; sem nenhum deles, inglês. Um turno em português pode ser digitado com os acentos ou sem eles. O português europeu não é uma gramática que o seletor tenha, e um turno em qualquer outra língua não é lido.
Um agente do stalwart-agents é protegido enquanto roda e provado antes de rodar. Duas verificações cercam o
seu modelo, ambas decididas por código nativo na sua máquina -- o runtime licenciado que o pacote traz -- e
nunca por um modelo: o roteador decide se o modelo é chamado, e a
verificação de ferramentas decide cada chamada. Antes do primeiro turno do agente, o caçador de bugs examina o
seu grafo em busca de um caminho que passe por fora de qualquer uma das duas. Este capítulo é como as duas
verificações decidem, o que isso garante e como a prova roda.
Duas verificações e uma prova¶
- O roteador lê cada turno antes que qualquer modelo o veja (abaixo).
- A verificação de ferramentas decide cada chamada de ferramenta, tenha sido escolhida pelo roteador ou pelo modelo (abaixo).
- A prova examina cada rota pelos seus agentes em busca de um caminho até uma ferramenta sem a sua verificação, a sua confirmação ou a etapa que ela precisa seguir (abaixo).
A API hospedada não decide nada enquanto o seu agente roda. Onde o runtime não consegue responder, um
plano ou uma verificação levanta SelectorUnavailableError e a chamada não é feita: o agente
nunca recorre a uma chamada não verificada.
O roteador: se um modelo é chamado¶
Cada turno é lido num plano a partir do que as suas ferramentas declaram. Para onde o turno vai, gerado a partir do próprio roteador:
| O que o roteador leu | Para onde o turno vai |
|---|---|
| uma etapa de ferramenta que se resolve | direto para a etapa de ferramenta protegida, com a chamada escrita a partir das palavras da pessoa: nenhuma chamada ao modelo |
uma pergunta cujos valores que faltam as ferramentas de listagem do catálogo produzem (chain) |
para a etapa de ferramenta protegida com essas chamadas, uma rodada de cada vez, e depois o plano que os resultados delas fazem, no lugar da pergunta |
| uma pergunta | o turno termina com uma pergunta fixa, e o modelo não é chamado |
uma escrita submetida à pessoa (confirm) |
para a etapa de ferramenta protegida com a chamada que o sim da pessoa faria; a confirmação dela é a única pergunta feita |
recusado (declined) |
o turno termina com uma recusa que nomeia a regra; o modelo nunca é visitado |
| um cálculo | depois que as leituras de que depende rodaram, o próprio modelo do agente escreve a função e o sandbox a executa, na sua máquina |
| um cálculo que esta máquina não consegue rodar | recusado, com o motivo começando por compute off: |
não lido (unread) |
primeiro o único pedido para dizer de novo; depois o que on_unread diz |
Um turno que nada lê recebe uma vez o pedido para ser dito de novo. Depois disso, o on_unread do agente
decide:
| Configuração | O que acontece com um turno que nada lê |
|---|---|
refuse |
o turno termina com uma recusa; nenhum modelo é chamado |
model |
o turno vai para o modelo do agente, e toda chamada que ele pede é verificada |
ask |
a pessoa é perguntada se o modelo pode receber o turno, e ele só vai para o modelo com o sim dela |
answer |
uma pergunta é respondida a partir dos seus documentos (capítulo 44); sem resposta fundamentada, o turno é recusado |
Onde um agente não diz nada, cada conversa adota no seu primeiro turno a configuração da página Configurações do
seu console, ou refuse.
Quando nenhum modelo é chamado¶
O roteador responde aos turnos que lê sem modelo. Um modelo só é chamado onde as suas configurações enviam um turno para ele, então nenhum modelo é chamado quando tudo isto vale:
- os agentes não nomeiam nenhum
model; on_unreadérefuse, ouanswercom o compositorextract(capítulo 44);- nenhum agente tem
hands_off_to; - as respostas são as suas formulações:
replies: {composer: template}(capítulo 42).
A verificação de ferramentas: cada chamada, quem quer que a tenha escolhido¶
A verificação roda no runtime, uma vez por chamada, com o catálogo, o rulebook e o histórico da sessão, e a primeira regra que se aplica decide. As regras, na ordem em que o runtime as toma (o capítulo 32 dá o exemplo de cada uma):
| Ordem | O que é verificado | Motivos com que responde | Exemplo |
|---|---|---|---|
| 1 | a ferramenta está no catálogo e este agente pode alcançá-la; com vários agentes, a chamada nomeia o seu agente; com a licença vencida, uma escrita é recusada, ou toda chamada quando a configuração do tenant diz refuse |
unknown_tool, agent_required, licence_lapsed |
delete_order, que o catálogo não tem: refuse, unknown_tool |
| 2 | o histórico da sessão se verifica, entrada por entrada, pela assinatura que cada entrada carrega sobre as anteriores | history_invalid |
um histórico cuja primeira entrada foi editada à mão: refuse, history_invalid |
| 3 | os argumentos contra o schema da ferramenta; um argumento que o schema não declara é recusado | argument_schema |
refund_card com um note que ela não declara: refuse, argument_schema em /note |
| 4 | um destino -- um endereço, uma URL, um host -- num argumento de uma ferramenta não declarada sends_out |
undeclared_destination |
refund_card com https://refunds.invalid/4411 como pedido: refuse, undeclared_destination |
| 5 | as regras de ordem da ferramenta sobre o histórico verificado: after (e um argumento ligado ao resultado daquela chamada), not_after, at_most |
order_rule |
uma ferramenta declarada after: [get_order], chamada antes de qualquer get_order rodar na sessão: refuse, order_rule, nomeando after get_order |
| 6 | um desfazer nomeia o passo que desfaz, esse passo rodou, o desfazer é o declarado para ele, cada argumento ligado vem desse passo, e o passo é desfeito uma única vez | compensation_spent, compensation_mismatch |
refund_card enviado uma segunda vez como desfazer de uma cobrança já desfeita: refuse, compensation_spent |
| 7 | os checks da própria ferramenta sobre os argumentos da chamada e os resultados que ferramentas anteriores devolveram, os seus limites por período (at_most: {count, per}) e o que pode gastar (spend), contra o que o host contou; nunca num desfazer |
condition_failed, condition_unknown, usage_unknown, limit_reached, spend_unknown, spend_exceeded |
create_order com quantity: 7 onde checks diz at_most: {from: check_stock.in_stock} e o estoque é 5: refuse, condition_failed; um quarto pedido num dia limitado a 3: refuse, limit_reached |
| 8 | o portão do rulebook, como o rulebook compilado do tenant decide a chamada | a regra do rulebook que a recusou | um reembolso que o fluxo do rulebook recusa: refuse, com aquela regra como motivo e o seu fluxo no detalhe |
| 9 | marcas menos confiáveis do que a ferramenta exige, numa escrita; recusada aqui quando a taint_policy do tenant é refuse |
tainted_call |
com taint_policy: refuse, um reembolso proposto depois de ler a caixa de entrada: refuse, tainted_call |
| 10 | um token de confirmação que cobre exatamente esta chamada, com estes argumentos, neste ponto do histórico | confirmed |
o mesmo reembolso, apresentado com o token que a pessoa aprovou: allow, confirmed |
| 11 | todo motivo pelo qual uma escrita precisa de uma pessoa, juntos; uma escrita sem nenhum deles ainda é confirmada, por write |
gate_clarify, argument_origin, tainted_call, irreversible_pivot, schema_partial, write |
o reembolso com um valor que o modelo escreveu: confirm, argument_origin: amount; o mesmo valor digitado pela pessoa: confirm, write |
| 12–13 | um desfazer que passou por todos os passos acima; senão, uma chamada que nada acima reteve | compensation, allow |
get_order, uma leitura: allow; refund_card como o desfazer que uma execução que falhou envia: allow, compensation |
Ela dá uma de três respostas:
allow: a ferramenta roda uma vez, com uma chave de idempotência.confirm: uma pessoa é perguntada, vendo cada argumento e de onde veio o seu valor; a chamada é verificada de novo com o token assinado que a aprovação dela carrega (capítulo 35).refuse: nada roda. Onde etapas anteriores da execução já tinham escrito algo, essas escritas são desfeitas, a última primeiro (capítulo 34).
O que é garantido, e o que não é¶
Garantido. Uma chamada recusada não roda nada. Uma chamada confirmada roda só com os argumentos exatos que a
pessoa viu. O histórico não pode ser descartado, editado ou reordenado sem que a verificação diga
history_invalid. Nenhum modelo é chamado a menos que as suas configurações encaminhem um turno a ele.
Não garantido. A verificação julga nomes, schemas, marcas e o histórico, nunca o que uma resposta significa
(o capítulo 42 confere as respostas com o que foi feito). Uma ferramenta alcançável por fora do
agente pode ser chamada contornando a verificação, a menos que o gateway fique na frente dela.
Agent(..., verify=False) pula a prova.
O rulebook sob o qual ele roda¶
O rulebook: de um agente -- Colang 1.0 ou 2.x, uma planilha de regras ou a pasta que o stalwart rules compile
escreveu -- é aquilo contra o que o seu grafo é provado e o que a sua verificação de ferramentas aplica, de modo
que as regras provadas são as regras sob as quais o agente roda:
- Na inicialização, antes do primeiro turno, o agente compila o seu rulebook no registro que a verificação
de ferramentas avalia (
POST /v1/rulebooks/compile, ouPOST /v1/rule-sheets/compilepara uma planilha), uma vez para o seu cliente; uma pasta que ostalwart rules compileescreveu é lida como está, sem chamada. Nada fica guardado no serviço: toda verificação leva o registro ao lado do catálogo do agente. stalwart verifyo compila do mesmo jeito e prova o grafo contra ele (capítulo 46).- Onde ele não pode ser compilado -- sem serviço, um rulebook que o compilador recusa
(
rulebook_unsupported, nomeando a linha), ou uma organização que só envia registros compilados (policy_input_refused) -- o agente não roda. Ele levantaRulebookNotHeld, dizendo por quê.
Os dois balcões da loja, com um rulebook que só deixa um reembolso rodar para um pedido que está pago:
define user ask refund
"estorne o pedido 4411"
"devolva o dinheiro do cliente"
define flow refunds
user ask refund
if $order_paid
execute refund_card
else
bot refuse
# As duas centrais da loja, com um rulebook que decide quando um estorno pode rodar.
agents:
desk:
model: "anthropic:claude-sonnet-5-5"
tools: [get_order, charge_card, send_receipt, read_inbox]
hands_off_to: [billing]
on_unread: refuse
billing:
privilege: high
tools: [refund_card]
tools:
- python: shop.tools
rulebook: ./policy.co # provado contra ele, e compilado para a verificação de ferramentas
A verificação de ferramentas decide sob ele, com o contexto que a sua aplicação fornece:
desk = Agent.from_file("agent.yaml")
catalogue = desk.catalogue() # as ferramentas dos dois balcões, feitas nesta máquina
rulebook = desk.rules().record # policy.co, compilado uma vez para este agente
with Client() as client:
billing = client.session("billing", catalogue=catalogue, rulebook=rulebook)
billing.read("estorne 40,00 do pedido 4411")
for paid in (False, True):
check = billing.check_tool_call("refund_card", {"order_id": "4411", "amount": 40.00},
origins={"order_id": "typed", "amount": "typed"},
context={"order_paid": paid})
print(f"order_paid {paid}:", check.decision, ", ".join(check.reasons), check.detail.get("flow", ""))
order_paid False: refuse refuse refunds
order_paid True: confirm write
O reembolso do pedido não pago é recusado pelo fluxo refunds do rulebook; o do pedido pago passa pelo rulebook
e é confirmado, como toda escrita é.
As ações de um rulebook nomeiam ferramentas com execute <ferramenta>, como faz o fluxo refunds, ou com
bot execute_<ferramenta>, a mesma chamada escrita como o registro a nomeia. Qualquer outra linha bot é algo
que o bot diz, não uma chamada de ferramenta.
Os fatos do rulebook¶
Uma regra como if $order_paid lê um fato: verdadeiro ou falso, por nome, enviado com cada verificação. Um
fato que nada informa é falso, então a regra recusa. Há dois jeitos de informar um, e um agente pode usar os
dois:
- O seu código diz, quando a sua aplicação sabe:
Agent.from_file("agent.yaml", context=...), ouAgent(..., context=...), recebe os fatos como um mapeamento, ou como uma função do usuário que os devolve, chamada a cada verificação para dá-los como estão naquele momento. Uma verificação feita pelo cliente ou por uma sessão recebecontext=em cada chamada (capítulo 24). - A própria ferramenta do agente diz:
facts:noagent.yaml, ouAgent(..., facts=...), lê um fato de um campo do resultado de uma ferramenta, na resposta mais recente dessa ferramenta na conversa. A loja lê os seus dois fatos deget_order(capítulo 30):
facts:
card_on_file: {from: get_order.card, same: order_id}
order_paid: {from: get_order.status == paid, same: order_id}
from é <ferramenta>.<campo>, verdadeiro quando o campo é verdadeiro, um número diferente de 0, ou um
texto, lista ou mapeamento não vazio; ou <ferramenta>.<campo> == <valor>, verdadeiro quando o campo é igual
ao valor. Até a ferramenta responder, o fato é falso, então a cobrança da loja só é permitida depois que
mostre o pedido 4411 mostrou um cartão.
same: order_id amarra o fato à chamada verificada: só conta uma resposta de get_order para o pedido que
a chamada nomeia, então o status de um pedido nunca permite o reembolso de outro. Deixe-o de fora só para um
fato sobre a conversa como um todo, como a conta do usuário. Um fato nunca é lido de uma ferramenta que
reads_outside: qualquer um poderia escrever o que ela devolve. Onde o seu código e o arquivo informam o
mesmo fato, vale o do arquivo.
Se você já usa LangGraph
O middleware de guarda, build_stalwart_graph e Guard.call_with também recebem context=, e
stalwart_agents.facts.Context(facts=agent.facts) lê para eles os facts: do arquivo nas respostas das
ferramentas (capítulo 27).
A prova¶
Os agentes são compilados num grafo (qls-graph/1) e no seu catálogo. Cada obrigação -- esta ferramenta nunca
roda sem a sua verificação, esta escrita nunca roda sem confirmação depois de ler algo de fora -- é uma pergunta
que o caçador responde sobre cada rota:
- alcançável: um achado, com um caminho testemunha mapeado de volta à linha do
agent.yaml(capítulo 31); - inalcançável: a obrigação vale, até a profundidade declarada (capítulo 31);
- indeciso: relatado como indeciso, nunca como limpo.
Ela roda de três formas:
- Automaticamente.
Agent(..., verify=True), o padrão, prova o grafo no nosso serviço antes do primeiro turno, uma vez por grafo: uma reconstrução do mesmo grafo é respondida pelo cache. Um achado do tipo Vulnerabilidade levantaNotVerified, com os achados, e nada roda. Uma prova que não pôde rodar também levanta, nunca dá um resultado limpo. - Como portão de CI.
stalwart verify agent.yamlfaz o lint, compila, caça e imprime o relatório, e sai com 1 quando o portão reprova (capítulo 46). O agente da loja do capítulo 30:
$ stalwart verify agent.yaml
stalwart verify agent.yaml
Compiled the rulebook policy.co: the tool check enforces the rules verified here.
Lints from the runtime (none of these blocks the agent):
write_not_retry_safe (warning) on charge_card: a write with no idempotency, so an ambiguous failure goes to a person
write_without_result_schema (warning) on charge_card: a write with no result schema, so its outcome could not be reported
compensation_not_retry_safe (warning) on charge_card: 'refund_card' undoes it and is not retry-safe
write_not_retry_safe (warning) on refund_card: a write with no idempotency, so an ambiguous failure goes to a person
write_without_result_schema (warning) on refund_card: a write with no result schema, so its outcome could not be reported
write_without_result_schema (warning) on send_receipt: a write with no result schema, so its outcome could not be reported
compensation_out_of_reach (warning) on charge_card: agent 'desk' can call it but not 'refund_card', which undoes it, so a run of that agent cannot undo it
Code checks: no finding in 4 file(s) (rules d656f9ab32d3).
Warnings: 2 findings that do not block this agent. Look at each and weigh it; the agent can run as it is.
QG023 Medium on cycle:desk_model (agent.yaml:3)
the cycle through 'desk_model' calls the model or a tool, and nothing declares a budget: the framework's recursion limit of 60 is the run's only ceiling
OWASP LLM06:2026, ASI08:2026
Fix: Declare a budget for the agent or the run, or add an exit the model cannot avoid -- a counter in the state, declared on the edge as exit_after.
QG105 Review on refund_card (agent.yaml:10)
the write 'refund_card' has no compensation, is not irreversible, and runs on a path with another write
OWASP LLM03:2026, ASI08:2026
Fix: Give 'refund_card' an undo -- `undo:` in agent.yaml or `compensation` in the catalogue, which may be the tool itself with its arguments swapped -- or say a failed run may leave it done (`undo: none`), or mark it irreversible.
Rules with nothing to read, so nothing raised:
QG017: no argument is declared as code or a shell command
QG018: no argument is declared as SQL, a query, a path, a URL or a template
QG019: no tool is declared confidential
QG020: no retrieval from a store declared multi-tenant
QG021: no tool is declared to write a store declared persistent
QG106: no tool declares a source or a server, and the catalogue declares no modules
QG107: no agent in the catalogue declares a remote channel
Gate: passed. Nothing blocks this agent: the 2 warnings above are for you to look at and weigh.
$ echo $?
0
- Pela API.
Client().verify_graph(graph, catalogue=..., rulebook=..., session_steps=...)envia o grafo quedesk.export()devolve e o catálogo quedesk.catalogue()devolve, como um jobgraph(capítulo 47).
As regras que ela prova¶
Cada regra que a caça do grafo e do rulebook de um agente levanta, gerada a partir do catálogo do caçador.
Reprova o portão é aquilo a que o stalwart verify submete um build; impede o Agent(...) é aquilo com que
a prova automática se recusa a rodar:
| Regra | Título | Tipo | Reprova o portão | Impede o Agent(...) |
|---|---|---|---|---|
QG001 |
Missing check | Vulnerabilidade | sim | sim |
QG002 |
Escapable guard | Vulnerabilidade | sim | sim |
QG003 |
Multi-turn escape | Vulnerabilidade | sim | sim |
QG004 |
Permissive by default | Bug | não | não |
QG005 |
Unshippable policy | Bug | sim | não |
QG006 |
Unverified region | Bug | não | não |
QG007 |
Read before written | Vulnerabilidade | não | sim |
QG008 |
Call without execute | Vulnerabilidade | não | sim |
QG009 |
Undefined bot message | Vulnerabilidade | não | sim |
QG010 |
Dead action | Vulnerabilidade | não | sim |
QG011 |
Confused deputy | Vulnerabilidade | sim | sim |
QG012 |
Cascading injection | Vulnerabilidade | sim | sim |
QG013 |
Irreversible before undoable | Bug | não | não |
QG014 |
Order rule escapable | Vulnerabilidade | sim | sim |
QG015 |
Undeclared route | Bug | não | não |
QG016 |
Unmodelled path | Vulnerabilidade | sim | sim |
QG017 |
Model writes code that runs | Vulnerabilidade | sim | sim |
QG018 |
Model writes a query or a destination | Vulnerabilidade | não | sim |
QG019 |
Confidential data leaves | Vulnerabilidade | não | sim |
QG020 |
Retrieval not bound to the user | Vulnerabilidade | não | sim |
QG021 |
Untrusted content persisted | Vulnerabilidade | não | sim |
QG022 |
Rail with a published advisory | Vulnerabilidade | sim | sim |
QG023 |
Unbounded loop | Bug | não | não |
QG101 |
Unchecked action | Ponto de atenção de segurança | não | não |
QG104 |
Check missing against its peers | Ponto de atenção de segurança | não | não |
QG105 |
Unrecoverable write | Ponto de atenção de segurança | não | não |
QG106 |
Unpinned tool source | Ponto de atenção de segurança | não | não |
QG107 |
Unauthenticated agent channel | Ponto de atenção de segurança | não | não |
QG312 |
Engine disagreement | Bug | sim | não |
A página de cada regra diz o que ela prova e o que não prova.
Os limites¶
- Um modelo escolhe livremente. A prova lê uma etapa de modelo como capaz de escolher qualquer ferramenta e qualquer argumento que receba; as verificações seguram o que ele escolhe, e nada prova que as escolhas dele sejam boas.
- Etapas que não são nossas são opacas. Uma etapa que a sua equipe escreve é lida como capaz de fazer tudo o que as rotas declaradas dela permitem (capítulo 31).
- Uma prova limpa vale até a profundidade que declara: o limite de etapas da execução e o número de turnos que o documento de achados nomeia.
- A prova cobre o grafo, o catálogo e o rulebook, não o código das suas ferramentas; as verificações de código do capítulo 46 leem esse código só em busca de problemas de rede e de credenciais.
- O rulebook decide só as ferramentas que os seus fluxos chamam por
execute. Uma ferramenta que nenhum fluxo governa é decidida pelo resto da verificação.