46 · Verificando um agente na CI: stalwart verify¶
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.
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 o registro. - O registro. O catálogo -- cada ferramenta com as suas palavras -- é registrado como uma nova versão, com
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). - 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: the shop agent checked on every 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 # a runner is a new machine every time
- run: stalwart verify agent.yaml --replay --keep findings/
- if: always() # the place is released whatever happened above
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
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
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:
"""A helper a teammate added to the shop's project: the carrier's tracking page for an order."""
import httpx
def tracking_page(order_id: str) -> str:
"""Fetch the carrier's tracking page for an order."""
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
Registered as catalogue version 2, 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: 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`)
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: failed
$ 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 the gate passed, 1 it failed
stalwart verify agent.yaml
Registered as catalogue version 3, 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
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).
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 |
--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 registra nada e não caça nada.
Os status de saída¶
Gerados a partir do próprio comando:
| Status | Significa |
|---|---|
0 |
concluído: para o verify, o portão se manteve; para o redteam, nada foi realizado |
1 |
o portão reprovou, ou uma campanha realizou um ataque |
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.