Ir para o conteúdo

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

  1. 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.
  2. 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.
  3. 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.
  4. O relatório. Cada achado com a sua regra, a sua severidade, a linha do agent.yaml de onde vem, as suas entradas OWASP e a sua correção, e depois Gate: passed ou Gate: 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.it nã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.