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 querefund_cardnão declara, é recusado em/note;delete_ordernão está no catálogo;- um endereço da web no número do pedido é um destino, e
refund_cardnã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,contextecomputed: 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>ouresult:<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 umget_orderrodou nesta sessão. Uma chamada permitida que falhou não conta. Umafterpode 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 oresultdaquela chamada, ou a verificação o recusa nomeando a ligação;not_after: [read_inbox], ounot_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 umfield(ou umtotaloucountsobre 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:eachnomeia a lista,keepas condições que cada registro cumpre, onde{from_row: <field>}compara com outro campo do mesmo registro eunknown: passmanté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}comperum decall,run,session,minute,hour,day,weekoumonth, 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 mesmopooldividindo uma contagem, junto com acurrencyda 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ãodone_whenefailed_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).