Ir para o conteúdo

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

  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 o registro.
  2. 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).
  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: 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.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.