Protegido e provado
Um agente stalwart-agents tem duas verificações em volta do seu modelo, cada uma decidida por código nativo determinístico e não por um modelo, e o seu grafo é provado antes de rodar. As duas verificações rodam no seu próprio host: no runtime licenciado que o wheel stalwart-agents traz, ou no qls-router que você roda no seu ambiente. O serviço hospedado não decide nada em tempo de execução. Onde nem o runtime nem um router responde, um plano ou uma verificação levanta SelectorUnavailableError: a chamada é bloqueada, nunca liberada.
O router decide se um modelo é chamado
Cada turno é lido antes que qualquer modelo o veja, e cada passo em que ele é lido segue um de quatro caminhos:
- Uma ferramenta. O passo vai para o nó de ferramentas protegido, sem chamar modelo.
ask. O turno termina com uma pergunta fixa.declined. O turno termina com uma recusa, e o modelo nunca é visitado.unread. O agente pede à pessoa que diga de novo, uma vez; depois disso,on_unreaddecide:refuse,model,askouanswer.
Nenhum modelo é chamado, nunca, quando o agente não tem model, tem on_unread: refuse, não tem hands_off_to, e respostas escritas por composer: template e composer: extract.
Cada chamada de ferramenta é verificada
Cada chamada de ferramenta é verificada antes de rodar, quer o router quer o modelo a tenha escolhido. A primeira regra que se aplica decide:
- Catálogo. A ferramenta precisa existir e estar nas
allowed_toolsdo agente. Com a licença vencida, escritas são recusadas. - Histórico. A cadeia assinada sobre o histórico da sessão precisa ser válida, ou a chamada é recusada como
history_invalid. - Argumentos. Precisam corresponder ao JSON schema da ferramenta.
- Destinos. Uma URL ou outro destino é recusado, a menos que a ferramenta seja declarada
sends_out. - Regras de ordem.
after,not_aftereat_most, avaliadas sobre o histórico verificado. - Compensação. Um desfazer precisa estar ligado ao passo que desfaz, e só pode ser gasto uma vez.
- As suas regras. O rulebook registrado com o catálogo -- o rulebook Colang de um agente ou a sua planilha de regras --, com condições como
amount_overcalculadas a partir dos próprios argumentos da chamada, para que o host não possa sobrepô-las. - Contaminação. Uma escrita que carrega uma entrada menos confiável do que a ferramenta exige é confirmada por padrão, ou recusada onde as suas configurações mandam.
- Confirmação assinada. Um token assinado que cobre exatamente esta ferramenta e estes argumentos libera a chamada.
- Padrão. Qualquer outra escrita é confirmada. Uma escrita nunca é liberada em silêncio.
A resposta é uma de três:
allow. A ferramenta roda uma vez, sob uma chave de idempotência.confirm. A pessoa é consultada, e a chamada é verificada de novo com um token assinado.refuse. Nada roda. Onde passos anteriores da execução escreveram algo, essas escritas são desfeitas, da última para a primeira.
Uma escrita espera pela pessoa, e só roda com os argumentos que ela viu:
from decimal import Decimal
from stalwart_agents import Agent, tool
def get_order(order_id: str) -> dict: ... # a read: no words needed
@tool(writes=True, undo="refund_card")
def charge_card(order_id: str, amount: Decimal) -> dict: ...
@tool(writes=True)
def refund_card(order_id: str, amount: Decimal) -> dict: ...
desk = Agent("desk", tools=[get_order, charge_card, refund_card], rulebook="policy.co")
reply = desk.chat("charge order 4411 30 pounds", user="u-17")
for waiting in reply.waiting: # the write waits: decided `confirm`
print(waiting.kind, waiting.wording) # confirm Run charge_card?
reply = desk.confirm("u-17", "approve") # checked again, under the signed token
O que é garantido, e o que não é
- Garantido. Uma chamada recusada não roda nada. Uma chamada confirmada só roda com os argumentos exatos que a pessoa viu. O histórico não pode ser descartado nem reescrito sem ser detectado. Nenhum modelo é chamado a menos que a configuração encaminhe um turno para ele.
- Não garantido. A verificação julga nomes, schemas, rótulos e histórico, não o que uma resposta significa. Uma ferramenta alcançável fora do agente pode ser chamada diretamente, contornando a verificação, a menos que um gateway esteja na frente dela.
verify=Falsepula a prova abaixo.
As regras contra as quais ele é provado são as regras sob as quais ele roda
O rulebook de um agente é o arquivo Colang (1.0 ou 2.x) contra o qual o seu grafo é provado. Na inicialização, e a cada stalwart verify, ele é registrado com o catálogo (POST /v1/catalogues, campo rulebook), onde o serviço o compila no registro que a verificação de ferramentas aplica. O registro traz o digest do próprio arquivo, de modo que um agente cujo rulebook já está registrado não registra nada, e um cujo rulebook mudou o registra de novo antes de rodar. Onde ele não pode ser registrado, o agente não roda: levanta RulebookNotHeld. Um tenant cujo próprio build emite IR registra o registro (rulebook_ir) no lugar.
agents:
desk:
tools: [get_order, charge_card, refund_card]
rulebook: ./policy.co # proved against, and registered for the tool check
from stalwart_agents.client import Client
# What an agent's start-up does where the newest catalogue does not hold its rulebook:
with open("policy.co", encoding="utf-8", newline="") as policy:
Client().register_catalogue(tools, agent="desk", rulebook=policy.read())
O rulebook de um agente e a sua planilha de regras são um único registro no catálogo: um agente que declara um rulebook o registra de novo na sua próxima inicialização, no lugar de uma planilha salva depois.
Provando um agente antes de rodar
O agente é compilado em uma exportação de grafo (qls-graph/1) e no seu catálogo. O solver transforma cada obrigação em uma violação e pergunta se algum caminho pelo grafo a alcança:
- Alcançável. Um achado, com um caminho testemunha mapeado de volta para a linha do
agent.yamlque o declarou. - Inalcançável. A obrigação vale, até o número de turnos verificados.
- Indecidido. Relatado como indecidido, nunca como limpo.
Automaticamente, antes do primeiro turno. verify=True é o padrão: o grafo é provado no modo graph hospedado antes de rodar, uma vez por grafo, e qualquer achado do tipo Vulnerability levanta NotVerified sem que nada rode.
from stalwart_agents import Agent, NotVerified
desk = Agent.from_file("agent.yaml")
try:
desk.chat("refund order 4411", user="u-17")
except NotVerified as refused: # nothing ran
for finding in refused.findings:
print(finding["rule"], finding.get("action")) # QG012 issue_refund
Como um portão no CI. stalwart verify agent.yaml roda as verificações de código na sua máquina, registra o catálogo com o seu rulebook e faz o caçador de bugs do serviço caçar falhas no grafo e no rulebook. Ele imprime os achados por severidade e termina com Gate: passed ou Gate: failed; um portão reprovado sai com 1. --replay reproduz a testemunha de cada achado com todas as ferramentas substituídas por stubs, para confirmar que ele é real. A próxima seção é o comando por completo.
$ stalwart verify agent.yaml --replay
stalwart verify agent.yaml
Registered as catalogue version 7, with the rulebook policy.co: the tool check enforces the rules verified here.
Lints at registration: none
Code checks: no finding in 12 file(s) (rules 3f9a1c2b7d40).
QG012 High on issue_refund (agent.yaml:7)
Replay: reproduced
The instruction entered one agent and the privileged call is made by another, after a hand-off: each
agent did what it was asked, and the less trusted one steered the more trusted one.
OWASP LLM01:2026, ASI07:2026, ASI08:2026
Fix: Bind the tool to the guarded tool node in the acting agent, so a call carrying a label from the
handing agent is confirmed or refused; or route the hand-off through a confirmation.
Gate: failed
$ echo $?
1
Pela API. POST /v1/solves com mode: graph recebe a exportação e o catálogo, um rulebook opcional, e session_steps como operando; Client().verify_graph(...) o envia. Um grafo LangGraph que você mesmo construiu é exportado com export_graph(app, catalogue, model_nodes=[...]).
import json
from stalwart_agents import Agent
from stalwart_agents.client import Client
desk = Agent.from_file("agent.yaml")
with open("desk.graph.json", "w") as graph, open("desk.catalogue.json", "w") as catalogue:
json.dump(desk.export(), graph)
json.dump(desk.catalogue(), catalogue)
result = Client().verify_graph("desk.graph.json", catalogue="desk.catalogue.json",
rulebook="policy.co", session_steps=2)
for finding in result.findings:
print(finding.severity, finding.rule)
{"mode": "graph",
"inputs": {"export": {"upload_id": "…"}, "catalogue": {"upload_id": "…"},
"rulebook": {"upload_id": "…"}},
"operands": {"session_steps": 2}}
Verificando um agente no CI: stalwart verify
stalwart verify agent.yaml é a mesma prova, como um portão que pode reprovar um build. Ele roda em quatro passos, e só o primeiro roda na sua máquina:
- As verificações de código, na sua máquina: o Python, o JavaScript, o TypeScript e o Go do projeto são lidos em busca de problemas de segurança de rede e de credenciais, e só as contagens por regra saem dela. Um achado reprova o portão.
- O registro: o catálogo, com o rulebook contra o qual o grafo é provado, como uma nova versão que a verificação de ferramentas aplica.
- A caçada, nos servidores da Stalwart: o grafo, o seu catálogo e o rulebook em um único job
hunt, sob o seu portão de qualidade. O solver nunca roda na sua máquina, e nada além do pacote é instalado. - O relatório: cada achado com a sua regra, a sua severidade, a linha do
agent.yamlde onde ele vem, as suas entradas OWASP e a sua correção, e depoisGate: passedouGate: failed.
Ele precisa do que todo comando exceto stalwart licence precisa: QLS_API_KEY e uma licença ativada na máquina. Um runner de CI é uma máquina nova a cada vez, então o job a ativa primeiro e libera o seu lugar por último, aconteça o que acontecer no meio:
# .github/workflows/agent.yml
name: agent
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
env:
QLS_API_KEY: ${{ secrets.QLS_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: {python-version: "3.13"}
- run: pip install stalwart-agents
- run: stalwart licence activate
- run: stalwart verify agent.yaml --replay --keep findings/ --dependencies uv.lock
- if: always()
run: stalwart licence release
- if: always()
uses: actions/upload-artifact@v4
with: {name: findings, path: findings/}
Um agente limpo passa, e o seu rulebook é o que a verificação de ferramentas passa a aplicar:
$ stalwart verify agent.yaml
stalwart verify agent.yaml
Registered as catalogue version 8, with the rulebook policy.co: the tool check enforces the rules verified here.
Lints at registration: none
Code checks: no finding in 12 file(s) (rules 3f9a1c2b7d40).
No findings.
Gate: passed
A mesma verificação a partir do Python, para um pipeline que não é um shell:
from stalwart_agents import render, verify
result = verify("agent.yaml", replay=True, dependencies="uv.lock")
print(render(result))
raise SystemExit(result.exit_code) # 0 passed, 1 failed
| Opção | O que faz |
|---|---|
--replay |
reproduz a testemunha de cada achado nos seus agentes, cada ferramenta um stub; um achado que não se reproduz reprova o portão |
--keep DIR |
escreve a exportação, o catálogo e os achados no diretório |
--dependencies FILE |
o arquivo de lock dos pacotes que você instala -- requirements.txt, pyproject.toml, uv.lock ou poetry.lock --, conferido contra o registro de avisos de segurança |
--export-only FILE |
escreve o grafo dos agentes, como o verificador o lê, no arquivo, e para |
| Código de saída | O que significa |
|---|---|
| 0 | o portão passou |
| 1 | o portão reprovou: um achado que o portão cobra, um achado das verificações de código, ou uma reprodução que não se repetiu |
| 2 | o arquivo não pode ser lido, ou não há chave ou licença |
| 3 | a caçada não pôde rodar, então nada foi verificado -- nunca um resultado limpo |
O que é provado
| Regra | Prova que não existe caminho em que |
|---|---|
| QG001 | uma ferramenta é alcançável sem verificação, ou uma escrita confirmada pode ser alcançada sem a sua confirmação |
| QG014 | uma regra de ordem after, not_after ou at_most pode ser escapada |
| QG011 | uma escrita segue uma leitura não confiável sem confirmação |
| QG012 | uma injeção em cascata, ou uma escrita depois de uma passagem vinda de um agente de privilégio menor |
| QG003 | qualquer uma das anteriores vale dentro de um turno mas quebra entre turnos |
| QG017–QG023 | valores escritos pelo modelo chegam a código, SQL, caminhos, URLs, armazenamentos ou chamadas de saída, ou um laço não tem orçamento |
| QG013, QG105 | relatados como Bug e Hotspot; nenhum dos dois impede Agent(...) |
Limites. Nós de modelo são lidos como capazes de escolher qualquer coisa, e nós que não são da Stalwart são opacos. A prova automática cobre um turno; passe session_steps para ir mais fundo. A prova cobre o grafo e o catálogo, não o código das próprias ferramentas, que as verificações de código leem.