46 · Verificando um agente na CI: stalwart verify¶
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.
stalwart verify agent.yaml é a verificação em que um build reprova. É a prova que um agente executa antes
do seu primeiro turno (capítulo 45), transformada num comando com um status de
saída, de modo que um pull request que abre um caminho por fora de uma verificação, ou que acrescenta código
que desliga a verificação de um certificado, para antes de ser mesclado. Este capítulo é o comando completo: o
que ele executa e onde, um workflow, um relatório que passa e um que reprova, e cada opção e status de saída.
Quatro etapas, uma na sua máquina¶
- As verificações de código, na sua máquina. Cada arquivo Python, JavaScript, TypeScript e Go da pasta em
que está o
agent.yamlé lido em busca de problemas de segurança de rede e de credenciais, pelo runtime que o pacote traz. Qualquer achado reprova o portão. O seu código nunca sai da máquina: só a contagem de achados por regra vai com a caça. - O catálogo e as regras. O catálogo -- cada ferramenta com as suas palavras -- é feito e passa pelo lint
na sua máquina, e o
rulebook:do arquivo é compilado no registro que a verificação de ferramentas aplica. As regras verificadas aqui são as regras sob as quais o seu agente roda (capítulo 45). Nada do agente fica guardado no serviço. - A caça, nos nossos servidores. O grafo dos agentes, o catálogo e o rulebook são enviados como um único
job
hunt(POST /v1/solves, com o seu portão de qualidade ligado), e o caçador de bugs examina cada rota pelos seus agentes. O solver roda nos servidores da Stalwart e nunca 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 vem, as suas entradas OWASP e a sua correção, e depoisGate: passedouGate: failed. O capítulo 31 lê um relatório linha por linha.
Uma caça que não pôde rodar -- sem rota até o serviço, um job que terminou sem documento de achados -- é um erro com status de saída 3. Nunca é um resultado limpo.
Do que ele precisa¶
A sua chave em QLS_API_KEY e uma licença ativada na máquina, como todo comando exceto stalwart licence,
stalwart doctor e stalwart check precisa. Um runner de CI é uma máquina nova a cada vez, então o job ativa
a licença primeiro e libera o seu lugar por último, aconteça o que acontecer no meio: um lugar liberado fica
livre para a próxima execução na hora (capítulo 38).
Um workflow do GitHub Actions¶
# .github/workflows/agent.yml: o agente da loja verificado em cada pull request.
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 # um runner é uma máquina nova a cada vez
- run: stalwart verify agent.yaml --replay --keep findings/
- if: always() # a vaga é liberada, aconteça o que acontecer acima
run: stalwart licence release
- if: always()
uses: actions/upload-artifact@v4
with:
name: findings
path: findings/
QLS_API_KEY é um segredo do repositório com uma chave da sua conta (capítulo 5). A
etapa de liberação e o upload levam if: always(), então rodam também depois de uma verificação reprovada: o
lugar é liberado, e os achados que --keep escreveu ficam guardados com a execução.
Um relatório que passa¶
O agente da loja do capítulo 30, verificado como o workflow o verifica:
$ 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
Os achados aqui são um hotspot de revisão e um achado médio, sob regras que não reprovam o portão (capítulo 31), então o portão passa e o comando sai com 0. A saída está em inglês, como o produto a escreve.
Um relatório que reprova¶
Um colega acrescenta ao mesmo projeto uma função auxiliar que busca a página de rastreamento de uma transportadora, com a verificação do certificado desligada:
"""Uma função auxiliar que um colega acrescentou ao projeto da loja: a página de rastreio da transportadora."""
import httpx
def tracking_page(order_id: str) -> str:
"""Busca a página de rastreio da transportadora para um pedido."""
page = httpx.get(f"https://tracking.carrier.example/{order_id}", verify=False)
return page.text
Ela não é uma ferramenta, e nenhum agente a chama. As verificações de código leem todos os arquivos do projeto mesmo assim, já que uma ferramenta pode importar qualquer um deles:
$ 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: 1 finding(s); each one fails, and only changing the code clears it.
QC002 CWE-295 shop/tracking.py:8 This code turns TLS verification off (`verify=False`), at shop/tracking.py:8.
shop/tracking.py:8 turns TLS verification off (`verify=False`)
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: failed. This agent is blocked by the code checks above. The warnings do not block it.
$ echo $?
1
QC002 nomeia o arquivo e a linha, e o portão reprova com status de saída 1. Remover verify=False resolve;
nada mais resolve. Um achado das verificações de código não tem nível de aviso, nem comentário que o dispense,
nem opção que o pule.
A mesma verificação a partir do Python¶
Para um pipeline que não é um shell, verify é a mesma verificação, e render o mesmo relatório:
from stalwart_agents import render, verify
result = verify("agent.yaml")
print(render(result), end="")
print("exit status:", result.exit_code) # 0 o portão passou, 1 ele falhou
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.
exit status: 0
result.exit_code é o status com que o comando sai: 0 quando o portão passou, 1 quando reprovou. Um arquivo
que não pode ser lido, uma chave ou licença ausente e uma caça que não pôde rodar são levantados como
AgentFileError, LicenceRequired e HunterUnavailable (capítulo 30).
verify devolve um VerifyResult: os lints, os achados, o resumo da varredura e gate_failed.
As opções¶
Geradas a partir do próprio parser do comando:
| Opção | O que o produto passa a fazer |
|---|---|
--keep |
escreve a exportação, o catálogo e os achados no diretório |
--dependencies |
o arquivo de lock dos pacotes que você instala (requirements.txt, pyproject.toml, uv.lock ou poetry.lock), conferido contra o registro de avisos |
--replay |
reproduz a testemunha de cada achado de grafo nos seus agentes, com toda ferramenta substituída por um stub; um achado que não se reproduz reprova o portão |
--export-only |
escreve o grafo dos agentes, como o verificador o lê, no arquivo, e para |
--no-register |
verifica sem registrar o catálogo -- nenhuma versão de catálogo é usada, e o serviço mantém a última versão registrada; os lints vêm do runtime, sem os três que só o registro roda |
--keep DIR escreve agent.graph.json, agent.catalogue.json e agent.findings.json; stalwart redteam lê
o último (capítulo 33). --export-only FILE não compila nada e não caça nada. O --no-register
ainda é aceito, para que um script que o passa rode, e não muda nada: nada do agente fica guardado no serviço.
Os status de saída¶
Gerados a partir do próprio comando:
| Status | Significa |
|---|---|
0 |
concluído: para o verify e o redteam, o portão se manteve; com --graph ou --mcp, a prova se manteve |
1 |
o portão reprovou, ou a prova encontrou um caminho que passa pelas verificações |
2 |
o arquivo ou o comando não pôde ser lido |
3 |
um verificador ou um serviço necessário não pôde ser alcançado, então nada foi verificado |
Para o verify, 2 é um arquivo que não pode ser lido, ou nenhuma chave ou nenhuma licença; 3 é uma caça ou uma
verificação de código que não pôde rodar, então nada foi verificado.
Os limites¶
- Os limites do capítulo 45 valem aqui (capítulo 45): o modelo é lido como capaz de escolher qualquer coisa, as suas próprias funções como capazes de fazer qualquer coisa, e uma caça limpa vale até a profundidade que declara (capítulo 31).
- Uma caça que não pôde rodar nunca é um resultado limpo. A caça é do serviço, então um build sem rota até
api.stalwart.itnão consegue passar o portão: sai com 3, nunca com 0. - As verificações de código leem segurança de rede e tratamento de credenciais, em Python, JavaScript, TypeScript e Go, e nada mais sobre o que o seu código faz.