Ir para o conteúdo

32 · Contra o que toda chamada de ferramenta é verificada

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.

Toda chamada que o seu agente faz a uma ferramenta é verificada antes de rodar, não importa quem a pediu: o turno da pessoa, o modelo, ou um desfazer depois de uma falha. A verificação responde allow, confirm (uma pessoa precisa aprová-la antes) ou refuse, com os motivos. Este capítulo é a ordem da verificação, e o que cada passo significa.

Onde a verificação roda

A verificação roda no runtime licenciado na sua máquina, com o catálogo e o rulebook que o seu agente leva (capítulo 31). O seu agente a chama no seu passo de ferramentas protegido, uma vez por chamada, e age conforme a resposta: roda uma vez uma chamada permitida, espera por uma pessoa em confirm, e não roda nada em refuse. Nada da chamada -- a ferramenta, os argumentos, o turno -- é enviado para nós decidirmos.

A ordem

Os passos são feitos nesta ordem, e o primeiro que responde encerra a verificação. Gerada a partir da própria verificação do runtime:

Ordem O que é verificado Motivos com que responde Exemplo
1 a ferramenta está no catálogo e este agente pode alcançá-la; com vários agentes, a chamada nomeia o seu agente; com a licença vencida, uma escrita é recusada, ou toda chamada quando a configuração do tenant diz refuse unknown_tool, agent_required, licence_lapsed delete_order, que o catálogo não tem: refuse, unknown_tool
2 o histórico da sessão se verifica, entrada por entrada, pela assinatura que cada entrada carrega sobre as anteriores history_invalid um histórico cuja primeira entrada foi editada à mão: refuse, history_invalid
3 os argumentos contra o schema da ferramenta; um argumento que o schema não declara é recusado argument_schema refund_card com um note que ela não declara: refuse, argument_schema em /note
4 um destino -- um endereço, uma URL, um host -- num argumento de uma ferramenta não declarada sends_out undeclared_destination refund_card com https://refunds.invalid/4411 como pedido: refuse, undeclared_destination
5 as regras de ordem da ferramenta sobre o histórico verificado: after (e um argumento ligado ao resultado daquela chamada), not_after, at_most order_rule uma ferramenta declarada after: [get_order], chamada antes de qualquer get_order rodar na sessão: refuse, order_rule, nomeando after get_order
6 um desfazer nomeia o passo que desfaz, esse passo rodou, o desfazer é o declarado para ele, cada argumento ligado vem desse passo, e o passo é desfeito uma única vez compensation_spent, compensation_mismatch refund_card enviado uma segunda vez como desfazer de uma cobrança já desfeita: refuse, compensation_spent
7 os checks da própria ferramenta sobre os argumentos da chamada e os resultados que ferramentas anteriores devolveram, os seus limites por período (at_most: {count, per}) e o que pode gastar (spend), contra o que o host contou; nunca num desfazer condition_failed, condition_unknown, usage_unknown, limit_reached, spend_unknown, spend_exceeded create_order com quantity: 7 onde checks diz at_most: {from: check_stock.in_stock} e o estoque é 5: refuse, condition_failed; um quarto pedido num dia limitado a 3: refuse, limit_reached
8 o portão do rulebook, como o rulebook compilado do tenant decide a chamada a regra do rulebook que a recusou um reembolso que o fluxo do rulebook recusa: refuse, com aquela regra como motivo e o seu fluxo no detalhe
9 marcas menos confiáveis do que a ferramenta exige, numa escrita; recusada aqui quando a taint_policy do tenant é refuse tainted_call com taint_policy: refuse, um reembolso proposto depois de ler a caixa de entrada: refuse, tainted_call
10 um token de confirmação que cobre exatamente esta chamada, com estes argumentos, neste ponto do histórico confirmed o mesmo reembolso, apresentado com o token que a pessoa aprovou: allow, confirmed
11 todo motivo pelo qual uma escrita precisa de uma pessoa, juntos; uma escrita sem nenhum deles ainda é confirmada, por write gate_clarify, argument_origin, tainted_call, irreversible_pivot, schema_partial, write o reembolso com um valor que o modelo escreveu: confirm, argument_origin: amount; o mesmo valor digitado pela pessoa: confirm, write
12–13 um desfazer que passou por todos os passos acima; senão, uma chamada que nada acima reteve compensation, allow get_order, uma leitura: allow; refund_card como o desfazer que uma execução que falhou envia: allow, compensation

Assim, uma recusa sempre nomeia a primeira coisa errada: uma chamada a uma ferramenta que o catálogo não tem é recusada por isso, sejam quais forem os seus argumentos; uma chamada que passa pelos passos 1 a 9 com uma aprovação válida é permitida no 10, sem perguntar de novo; e uma escrita que chega ao passo 11 é confirmada, com todos os motivos de uma vez.

Uma chamada, todas as verificações

O reembolso da loja, perguntado de cada uma das formas como uma execução pode perguntar. A sessão do balcão de cobrança lê o turno da pessoa, estorne 40,00 do pedido 4411, e cada chamada é verificada com o histórico dessa sessão:

def show(what, check):
    detail = {k: v for k, v in check.detail.items() if k in ("pointer", "rule", "destinations", "wording")}
    print(f"{what}: {check.decision} {', '.join(check.reasons)} {detail if detail else ''}".rstrip())


with Client() as client:
    billing = client.session("billing", catalogue=catalogue, rulebook=rulebook)       # uma conversa com a central de cobrança
    billing.read("estorne 40,00 do pedido 4411")   # o que a pessoa digitou

    refund = {"order_id": "4411", "amount": 40.00}
    show("the model wrote the amount",
         billing.check_tool_call("refund_card", refund, origins={"order_id": "typed", "amount": "model"}))
    asked = billing.check_tool_call("refund_card", refund, origins={"order_id": "typed", "amount": "typed"})
    show("the person typed the amount", asked)
    show("the person approved it",
         billing.check_tool_call("refund_card", refund, origins={"order_id": "typed", "amount": "typed"},
                                 confirmation=asked.confirmation))

    asked = billing.check_tool_call("refund_card", refund, origins={"order_id": "typed", "amount": "typed"})
    show("approved, then the amount changed",
         billing.check_tool_call("refund_card", {"order_id": "4411", "amount": 45.00},
                                 origins={"order_id": "typed", "amount": "model"}, confirmation=asked.confirmation))

    show("an argument the tool does not declare",
         billing.check_tool_call("refund_card", {**refund, "note": "cortesia"},
                                 origins={"order_id": "typed", "amount": "typed", "note": "model"}))
    show("a tool the catalogue does not have", billing.check_tool_call("delete_order", {"order_id": "4411"}))
    show("a destination in a tool that sends nothing out",
         billing.check_tool_call("refund_card", {"order_id": "https://refunds.invalid/4411", "amount": 40.00},
                                 origins={"order_id": "model", "amount": "typed"}))
    show("proposed after reading the inbox",
         billing.check_tool_call("refund_card", refund, origins={"order_id": "typed", "amount": "typed"},
                                 labels={"call": ["untrusted:read_inbox"]}))

    billing.history.entries[0]["tool"] = "get_order"     # o histórico, editado à mão
    show("the history edited by hand",
         billing.check_tool_call("refund_card", refund, origins={"order_id": "typed", "amount": "typed"}))
the model wrote the amount: refuse refuse
the person typed the amount: refuse refuse
the person approved it: refuse refuse
approved, then the amount changed: refuse refuse
an argument the tool does not declare: refuse argument_schema {'pointer': '/note', 'rule': 'additionalProperties'}
a tool the catalogue does not have: refuse unknown_tool
a destination in a tool that sends nothing out: refuse undeclared_destination {'destinations': ['https://refunds.invalid/4411']}
proposed after reading the inbox: refuse refuse
the history edited by hand: refuse history_invalid

Linha a linha:

  • o valor que o modelo escreveu pergunta à pessoa, nomeando o argumento: argument_origin: amount;
  • o valor que a pessoa digitou -- ele está no turno dela -- ainda pergunta, porque toda escrita é confirmada: o motivo é apenas write;
  • apresentada com o token daquela confirmação, a mesma chamada é allow, confirmed;
  • uma aprovação apresentada depois de o valor mudar não cobre nada: a verificação pergunta de novo;
  • note, um argumento que refund_card não declara, é recusado em /note;
  • delete_order não está no catálogo;
  • um endereço da web no número do pedido é um destino, e refund_card não envia nada para fora;
  • um reembolso proposto depois de ler a caixa de entrada pergunta, e a sua pergunta diz isso;
  • um histórico editado à mão é recusado antes de qualquer outra coisa ser lida.

De onde veio cada argumento

Todo argumento de uma escrita carrega a sua origem, e a verificação confia em algumas origens e não em outras:

  • typed: a pessoa o digitou. Confiável apenas onde o valor está nos turnos dela, como ela o digitou: um valor declarado como digitado que nenhum turno contém é perguntado;
  • reply, context e computed: a resposta da pessoa a uma pergunta, um valor que a sua aplicação pôs no contexto da execução (como o cliente autenticado), e um cálculo que a execução fez. Confiáveis;
  • result:<passo> ou result:<passo>.<campo>: um campo do resultado de uma chamada anterior. Confiável onde essa chamada rodou nesta sessão e o schema de resultado da sua ferramenta declara o campo;
  • model, e qualquer outra: o modelo o escreveu. Perguntado, pelo nome.

Um argumento não declarado é recusado. O schema de uma ferramenta diz quais são os seus argumentos, e um argumento que ele não declara é um para o qual ninguém escreveu regra: um note num reembolso, um cc num recibo. Recusá-lo no passo 3 significa que as regras seguintes nunca são consultadas sobre um valor que não conseguem ver.

O histórico da sessão

A verificação lê o histórico da sessão: toda chamada verificada até agora, com o seu passo, a sua ferramenta, o seu agente, os digests dos seus argumentos e das suas origens, as marcas que carregava, a resposta da verificação e o que aconteceu com ela. Quem chama carrega o histórico e nós o assinamos: cada resposta acrescenta a entrada da chamada e um token que a assina junto com todas as entradas anteriores, com as chaves da sua licença. A verificação seguinte confere essas assinaturas.

history_invalid significa que as assinaturas não se verificaram: uma entrada foi editada, removida ou reordenada, ou o histórico é de outra sessão. Nada mais é lido, e a resposta começa um novo histórico no passo 0. O que aconteceu com uma chamada -- ran, failed, outcome_unknown ou compensated -- é o relato de quem chama, e o elo da entrada seguinte o carrega.

Regras de ordem

As palavras de ordem de uma ferramenta são verificadas sobre o histórico verificado (capítulo 30):

  • after: [get_order]: a ferramenta só roda depois que um get_order rodou nesta sessão. Uma chamada permitida que falhou não conta. Um after pode ligar um argumento ao resultado daquela chamada anterior, para que um reembolso nomeie uma cobrança que esta sessão fez, e não uma que o modelo escreveu: a origem do argumento precisa ser o result daquela chamada, ou a verificação o recusa nomeando a ligação;
  • not_after: [read_inbox], ou not_after: [untrusted]: a ferramenta nunca roda depois que a ferramenta nomeada foi permitida, ou depois que qualquer coisa de fora foi lida. Uma leitura conta, aconteça o que acontecer com ela: o que ela devolveu foi lido;
  • at_most: 1: a ferramenta roda no máximo uma vez na sessão.

Cada uma é recusada como order_rule, com a regra que quebrou no detalhe, nas palavras que o QG014 da caça usa.

Condições, limites e resultados

Uma ferramenta também pode dizer só se, com que frequência, quanto e quando terminou. Essas palavras são lidas e verificadas quando o agent.yaml é carregado, e um erro nelas é recusado com a sua linha; a verificação ainda não as aplica a uma chamada.

tools:
  - name: create_order
    writes: true
    undo: cancel_order
    checks:                                   # cada uma precisa valer para a chamada seguir
      - {field: quantity, at_most: {from: check_stock.in_stock}}
      - {field: zip, one_of: ["94110", "94114"], because: "Só entregamos ali."}
  - name: search_jobs
    filter:                                   # os registros que uma leitura repassa
      each: jobs[]
      keep:
        - {field: salary_min, at_least: 70000}
        - {field: "required_skills[]", one_of: {from: "read_resume.skills[]"}}
    at_most: [{count: 30, per: hour}, {count: 200, per: day}]
  - name: place_order
    writes: true
    irreversible: true
    spend: [{field: amount, at_most: 25, per: call}, {field: amount, at_most: 500, per: month, pool: trading}]
    currency: USD
    outcome: {follow: get_order, same: order_id, done_when: [{field: status, is: filled}], give_up_after: 60}
  • checks: uma lista de condições que precisam valer todas. Uma condição nomeia um field (ou um total ou count sobre uma lista), um operador (is, is_not, one_of, none_of, at_least, at_most, under, over, between, before, after, contains) e aquilo com que compara: um valor, uma lista ou {from: <tool>.<field>}, o que uma ferramenta declarada devolveu antes. any: vale quando uma das suas condições vale. Um valor que ninguém conseguiu ler faz a verificação falhar. Dentro de {...}, coloque entre aspas um caminho com [];
  • filter: numa leitura, os registros que ela repassa: each nomeia a lista, keep as condições que cada registro cumpre, onde {from_row: <field>} compara com outro campo do mesmo registro e unknown: pass mantém um registro sem esse valor. Uma ferramenta que escreve não aceita filtro;
  • at_most: um número de chamadas por sessão, {count, per} com per um de call, run, session, minute, hour, day, week ou month, ou uma lista deles, um por período. Só o número sozinho é aplicado, como acima;
  • spend: o máximo que um campo pode somar por período, com as ferramentas do mesmo pool dividindo uma contagem, junto com a currency da ferramenta (USD, EUR, BRL...);
  • outcome: numa escrita, a leitura que a acompanha até o fim (follow), o campo que liga as duas (same), como são done_when e failed_when, e depois de quantos minutos desistir (give_up_after).

O steps de uma tarefa é in_order ou all_at_once; em all_at_once, cada escrita tem um undo ou um outcome, e uma marcada money, um spend. O where de um gatilho de mudança aceita condições, ou um filtro que dispara uma vez por registro mantido. O check: de uma planilha de regras fala as mesmas condições.

Seu próprio código e chamadas listadas à mão

code: executa o seu próprio código num espaço próprio e fechado, cada função uma ferramenta; ao contrário de python:, ele nunca é importado para dentro do agente. O seu sandbox: lista os sites que ele pode alcançar (network), os nomes das chaves que pode ler (secrets), os seus limits, as pastas que pode ler (mounts) e chamadas de sistema extras (allow). Sem level: advanced, um sandbox só pode ficar menor; level: advanced o amplia, e o host só inicia esse código depois que o dono aceita as configurações exatas com a sua chave de acesso. Algumas coisas nunca são permitidas, em nenhum nível: um contêiner privilegiado, a rede do host, as pastas e chaves da própria Stalwart, ou desligar a verificação, a auditoria ou o botão de parar. Uma função sem marcas @tool conta como uma escrita que nada desfaz. http: lista à mão as chamadas de um serviço web, cada uma {name, method, path}; um GET lê, e qualquer outro método diz se writes. Os dois são lidos e verificados; as suas chamadas ainda não são feitas.

Desfazeres ligados ao que desfazem

Um desfazer que uma execução que falhou envia é verificado como qualquer chamada, e também contra o passo que desfaz: esse passo rodou, o desfazer é o que a sua ferramenta declara, cada argumento que o desfazer liga vem dos argumentos ou do resultado desse passo, e o passo ainda não foi desfeito. Um desfazer que passa é permitido sem confirmação, a menos que o seu rulebook faça uma pergunta sobre ele: a pessoa aprovou a escrita que ele desfaz. Um enviado duas vezes é compensation_spent; um que não corresponde ao seu passo é compensation_mismatch (capítulo 34).

O portão do rulebook

O seu rulebook decide a chamada no passo 8, como foi compilado no registro. Um fluxo que recusa a chamada a recusa, nomeando a sua regra. Um fluxo que pede uma pergunta transforma a chamada numa confirmação, como gate_clarify. Os fatos que as suas regras leem vêm com a verificação (capítulo 45). Um desfazer amarrado ao seu passo no passo 6 não cabe ao rulebook decidir: ele desfaz uma escrita que esta verificação permitiu, então uma regra como reembolse só um pedido pago nunca impede que uma execução que falhou seja desfeita.

Valores não confiáveis

Um valor vindo de uma ferramenta que reads_outside, ou de um agente com menos privilégio do que a ferramenta exige, carrega uma marca (capítulo 33). Uma escrita que carrega uma marca menos confiável do que a ferramenta é retida: por padrão ela é confirmada, com a sua pergunta dizendo de onde veio a marca; onde a taint_policy da sua organização é refuse, ela é recusada. Leituras nunca são retidas por uma marca.

As confirmações e os seus tokens

Um confirm vem com um token que cobre exatamente aquela chamada: a ferramenta, o digest dos seus argumentos, a sessão e o ponto do histórico em que foi pedido, e uma validade (por padrão, um dia). A aprovação da pessoa envia a chamada de volta com o token, e a verificação a permite no passo 10. Um argumento mudado pergunta de novo: o seu digest já não é o que o token cobre, então o token não cobre nada, e a chamada é verificada desde o início. Depois que a chamada aprovada roda, o histórico segue em frente e o token também não cobre mais nada.

Uma escrita irreversível numa execução cujas escritas anteriores seriam desfeitas se ela falhasse acrescenta irreversible_pivot, e a sua pergunta termina com: Nothing undoes this step, and the steps after it cannot undo it.

Os limites

  • A verificação confia no relato de quem chama sobre se uma chamada rodou. Ela decide se uma chamada pode rodar; o que aconteceu com ela é o relato do host, carregado no histórico. O gateway o vê, porque o próprio gateway faz a chamada.
  • O que fica na sua máquina: as conversas dos seus agentes, as suas chamadas de ferramenta e os seus resultados, os dados, a função e o resultado de um cálculo, e os documentos de que os seus agentes respondem e o seu índice. Não guardamos nenhuma sessão, nenhum argumento de chamada de ferramenta e nenhum resultado de ferramenta, nem as palavras de um turno, exceto as que um registro de feedback carrega. (capítulo 16)
  • Uma confirmação prova que uma pessoa aprovou a chamada mostrada, com aqueles argumentos. Ela não prova que a pessoa a leu.
  • A verificação governa as chamadas que o seu agente faz. Uma ferramenta alcançável por outro caminho pode ser chamada contornando-a (capítulo 37).