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:

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:

  1. Catálogo. A ferramenta precisa existir e estar nas allowed_tools do agente. Com a licença vencida, escritas são recusadas.
  2. Histórico. A cadeia assinada sobre o histórico da sessão precisa ser válida, ou a chamada é recusada como history_invalid.
  3. Argumentos. Precisam corresponder ao JSON schema da ferramenta.
  4. Destinos. Uma URL ou outro destino é recusado, a menos que a ferramenta seja declarada sends_out.
  5. Regras de ordem. after, not_after e at_most, avaliadas sobre o histórico verificado.
  6. Compensação. Um desfazer precisa estar ligado ao passo que desfaz, e só pode ser gasto uma vez.
  7. 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_over calculadas a partir dos próprios argumentos da chamada, para que o host não possa sobrepô-las.
  8. 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.
  9. Confirmação assinada. Um token assinado que cobre exatamente esta ferramenta e estes argumentos libera a chamada.
  10. Padrão. Qualquer outra escrita é confirmada. Uma escrita nunca é liberada em silêncio.

A resposta é uma de três:

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 é

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:

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:

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