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 argumentrefund_carddoes not declare, is refused at/note;delete_orderis not in the catalogue;- a web address in the order number is a destination, and
refund_carddoes 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,contextandcomputed: 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>orresult:<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 aget_orderran in this session. A call that was allowed but failed does not count. Anaftercan 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'sresult, or the check refuses it naming the binding;not_after: [read_inbox], ornot_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).