Skip to content

32 · What every tool call is checked against

Every call your agent makes to a tool is checked before it runs, whoever asked for it: the person's turn, the model, or an undo after a failure. The check answers allow, confirm (a person must approve it first) or refuse, with the reasons. This chapter is the check's order, and what each step means.

Language support

The selector understands turns written in English. Portuguese is its next grammar, and has not shipped yet; until it does, a turn written in Portuguese is not read as Portuguese.

Where the check runs

The check runs in the licensed runtime on your machine, with the catalogue and the rulebook you registered (chapter 31). Your agent calls it at its guarded tool step, once per call, and acts on the answer: it runs an allowed call once, waits for a person on confirm, and runs nothing on refuse. Nothing about the call -- its tool, its arguments, the turn -- is sent to us to decide it.

The order

The steps are taken in this order, and the first that answers ends the check. Generated from the runtime's own check:

Order What is checked Reasons it answers with Example
1 the tool is in the catalogue and this agent may reach it; with several agents, the call names its agent; under a lapsed licence, a write is refused, or every call where the tenant's setting says refuse unknown_tool, agent_required, licence_lapsed delete_order, which the catalogue does not have: refuse, unknown_tool
2 the session's history verifies, entry by entry, by the signature each entry carries over the ones before it history_invalid a history whose first entry was edited by hand: refuse, history_invalid
3 the arguments against the tool's schema; an argument the schema does not declare is refused argument_schema refund_card with a note it does not declare: refuse, argument_schema at /note
4 a destination -- an address, a URL, a host -- in an argument of a tool not declared sends_out undeclared_destination refund_card with https://refunds.invalid/4411 as its order: refuse, undeclared_destination
5 the tool's order rules over the verified history: after (and an argument bound to that call's result), not_after, at_most order_rule a tool declared after: [get_order], called before any get_order ran in the session: refuse, order_rule, naming after get_order
6 an undo names the step it undoes, that step ran, the undo is the one declared for it, each bound argument comes from that step, and the step is undone once compensation_spent, compensation_mismatch refund_card sent a second time as the undo of a charge already undone: refuse, compensation_spent
7 the rulebook's gate, as the tenant's compiled rulebook decides the call the rulebook's rule that refused it a refund the rulebook's flow refuses: refuse, with that rule as the reason and its flow in the detail
8 marks less trusted than the tool requires, on a write; refused here where the tenant's taint_policy is refuse tainted_call under taint_policy: refuse, a refund proposed after reading the inbox: refuse, tainted_call
9 a confirmation token that covers exactly this call, with these arguments, at this point of the history confirmed the same refund, presented with the token the person approved: allow, confirmed
10 every reason a write needs a person, together; a write with none of them is still confirmed, for write gate_clarify, argument_origin, tainted_call, irreversible_pivot, schema_partial, write the refund with an amount the model wrote: confirm, argument_origin: amount; the same amount typed by the person: confirm, write
11–12 an undo that passed every step above; otherwise, a call nothing above held back compensation, allow get_order, a read: allow; refund_card as the undo a failed run sends: allow, compensation

So a refusal always names the first thing wrong: a call to a tool the catalogue does not have is refused for that, whatever its arguments; a call that passes steps 1 to 8 with a valid approval is allowed at 9, without asking again; and a write that reaches step 10 is confirmed, with every reason at once.

One call, every check

The shop's refund, asked about in each of the ways a run can ask. The billing desk's session reads the person's turn, refund order 4411 40.00, and each call is checked with that session's history:

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")       # one conversation with the billing desk
    billing.read("refund order 4411 40.00")   # what the person typed

    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": "goodwill"},
                                 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"     # the history, edited by hand
    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

Line by line:

  • the amount the model wrote asks the person, naming the argument: argument_origin: amount;
  • the amount the person typed -- it is in their turn -- still asks, because every write is confirmed: the reason is just write;
  • presented with the token from that confirmation, the same call is allow, confirmed;
  • an approval presented after the amount changed covers nothing: the check asks again;
  • note, an argument refund_card does not declare, is refused at /note;
  • delete_order is not in the catalogue;
  • a web address in the order number is a destination, and refund_card does not send anything out;
  • a refund proposed after reading the inbox asks, and its question says so;
  • a history edited by hand is refused before anything else is read.

Where each argument came from

Every argument of a write carries its origin, and the check trusts some origins and not others:

  • typed: the person typed it. Trusted only where the value is in their turns, as they typed it: a value claimed as typed that no turn holds is asked about;
  • reply, context and computed: the person's answer to a question, a value your application put in the run's context (such as the signed-in customer), and a calculation the run made. Trusted;
  • result:<step> or result:<step>.<field>: a field of an earlier call's result. Trusted where that call ran in this session and its tool's result schema declares the field;
  • model, and anything else: the model wrote it. Asked about, by name.

An undeclared argument is refused. A tool's schema says what its arguments are, and an argument it does not declare is one nobody wrote a rule for: a note on a refund, a cc on a receipt. Refusing it at step 3 means the rules that follow are never asked about a value they cannot see.

The session's history

The check reads the session's history: every call checked so far, with its step, its tool, its agent, the digests of its arguments and their origins, the marks it carried, the check's answer, and what became of it. The caller carries the history and we sign it: each answer adds the call's entry and a token that signs it together with every entry before it, with your licence's keys. The next check verifies those signatures.

history_invalid means the signatures did not verify: an entry was edited, removed or reordered, or the history is another session's. Nothing else is read, and the answer starts a new history at step 0. What became of a call -- ran, failed, outcome_unknown or compensated -- is the caller's report, and the next entry's link carries it.

Order rules

A tool's order words are checked over the verified history (chapter 30):

  • after: [get_order]: the tool runs only once a get_order ran in this session. A call that was allowed but failed does not count. An after can bind an argument to that earlier call's result, so a refund names a charge this session made, not one the model wrote: the argument's origin must be that call's result, or the check refuses it naming the binding;
  • not_after: [read_inbox], or not_after: [untrusted]: the tool never runs once the named tool was allowed, or once anything from outside was read. A read counts whatever became of it: what it returned was read;
  • at_most: 1: the tool runs at most once in the session.

Each is refused as order_rule, with the rule it broke in the detail, in the words the hunt's QG014 uses.

Undos bound to what they undo

An undo a failed run sends is checked like any call, and also against the step it undoes: that step ran, the undo is the one its tool declares, each argument the undo binds comes from that step's arguments or result, and the step has not been undone already. An undo that passes is allowed without a confirmation, unless your rulebook asks a question about it: the person approved the write it undoes. One sent twice is compensation_spent; one that does not match its step is compensation_mismatch (chapter 34).

The rulebook's gate

Your rulebook decides the call at step 7, as it was compiled at registration. A flow that refuses the call refuses it, naming its rule. A flow that asks for a question makes the call a confirmation, as gate_clarify.

Untrusted values

A value from a tool that reads_outside, or from an agent with less privilege than the tool requires, carries a mark (chapter 33). A write carrying a mark less trusted than the tool is held: by default it is confirmed, its question saying where the mark came from; where your organisation's taint_policy is refuse, it is refused. Reads are never held for a mark.

Confirmations and their tokens

A confirm comes with a token that covers exactly that call: the tool, the digest of its arguments, the session and the point of the history it was asked at, and an expiry (by default a day). The person's approval sends the call back with the token, and the check allows it at step 9. A changed argument asks again: its digest is no longer the one the token covers, so the token covers nothing, and the call is checked from the start. Once the approved call has run, the history has moved on and the token covers nothing either.

An irreversible write in a run whose earlier writes would be undone if it failed adds irreversible_pivot, and its question ends: Nothing undoes this step, and the steps after it cannot undo it.

The limits

  • The check trusts the caller's report of whether a call ran. It decides whether a call may run; what became of it is the host's report, carried in the history. The gateway sees it, because the gateway makes the call itself.
  • What stays on your machine: your agents' conversations, their tool calls and results, a calculation's data, function and result, and the documents your agents answer from and their index. We keep no session, no tool call's arguments and no tool result, and no turn's words except those a feedback record carries. (chapter 16)
  • A confirmation proves a person approved the call shown, with those arguments. It does not prove they read it.
  • The check governs the calls your agent makes. A tool reachable another way can be called around it (chapter 37).