45 · Protegido e provado¶
Idiomas
O seletor entende turnos escritos em inglês. O português é a sua próxima gramática e ainda não foi lançado; até lá, um turno escrito em português não é lido como português.
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, ou o
qls-router que você mesmo roda -- 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 nem o runtime nem o seu roteador conseguem
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 | 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 | 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 |
| 8 | 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 |
| 9 | 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 |
| 10 | 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 |
| 11–12 | 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) é aquilo contra o que o seu grafo é provado, e ele é registrado
com o catálogo para que a verificação de ferramentas o aplique, de modo que as regras provadas são as regras
sob as quais o agente roda:
stalwart verifyregistra o catálogo com o rulebook, compilado no registro que a verificação de ferramentas avalia (capítulo 46).- Na inicialização, antes do primeiro turno, um agente compara o digest do rulebook na versão registrada mais recente com o SHA-256 dos bytes do arquivo. Iguais, nada é registrado; diferentes ou ausente, o catálogo é registrado de novo com o rulebook antes que o agente rode.
- Sem
rulebook:, o agente ainda registra o seu catálogo na inicialização quando a versão mais recente não contém as suas ferramentas, cada uma como leitura ou escrita conforme declarada, para que a verificação de ferramentas as conheça. - Onde ele não pode ser registrado -- sem serviço, um rulebook que o compilador recusa
(
rulebook_unsupported, nomeando a linha), ou uma organização que só registra rulebooks como 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
"refund order 4411"
"give the customer their money back"
define flow refunds
user ask refund
if $order_paid
execute refund_card
else
bot refuse
# The shop's two desks, with a rulebook that decides when a refund may run.
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 # proved against, and registered for the tool check
A primeira inicialização o registra; a segunda o encontra já registrado:
desk = Agent.from_file("agent.yaml")
desk.chat("show me order 4411", user="u-17") # before this first turn, the rulebook is registered
with open("policy.co", "rb") as policy:
digest = hashlib.sha256(policy.read()).hexdigest()
with Client() as client:
held = client.catalogue() # the newest catalogue version
print("version", held.version, "holds policy.co:", held.rulebook_ir["policy_sha256"] == digest)
again = Agent.from_file("agent.yaml") # a second start of the same agent
again.chat("show me order 4412", user="u-18")
with Client() as client:
print("version", client.catalogue().version, "after a second start")
E a verificação de ferramentas decide sob ele, com o contexto que a sua aplicação fornece:
with Client() as client:
billing = client.session("billing")
billing.read("refund order 4411 40.00")
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", ""))
version 1 holds policy.co: True
version 1 after a second start
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. Uma linha
bot é algo que o bot diz, não uma chamada de ferramenta: um fluxo que só diz bot execute_refund_card não
governa nenhuma ferramenta.
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.yamlregistra, 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
Registered as catalogue version 1, with the rulebook policy.co: the tool check enforces the rules verified here.
Lints at registration (nothing fails on a lint):
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_without_compensation (warning) on refund_card: a write neither compensated nor marked irreversible
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_not_retry_safe (warning) on send_receipt: a write with no idempotency, so an ambiguous failure goes to a person
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 4b48b0081360).
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: Declare a compensation for 'refund_card' in the catalogue, or mark it irreversible.
QG105 Review on refund_card (policy.co:19)
the write 'refund_card' has no compensation, is not irreversible, and runs on a path with another write
OWASP LLM03:2026, ASI08:2026
Fix: Declare a compensation for 'refund_card' in the catalogue, 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
$ 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 | sim | sim |
QG019 |
Confidential data leaves | Vulnerabilidade | sim | sim |
QG020 |
Retrieval not bound to the user | Vulnerabilidade | sim | sim |
QG021 |
Untrusted content persisted | Vulnerabilidade | sim | 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.