Let the boundary ask a person, not only say yes or no (#15)
* Let the boundary ask a person, instead of only saying yes or no The action policy had two answers, so every action a deployment was unsure about had to be permanently forbidden or permanently permitted. The cases people actually have are in between: it may do this, but I want to see the first one; it may spend money, but ask me over fifty pounds. The only approximation was to deny the action and have somebody take the wheel, which throws away the Bot's turn and everything it had worked out to reach it. Adds a third list. `ask` is evaluated after `deny` and before `allow`, and both halves of that position are load-bearing. It must not soften a deny, because a thing a deployment has forbidden is not up for renegotiation at a prompt. It must beat allow, because the shipped policy is `allow: ["true"]` and an ask checked afterwards would be unreachable, so the first rule anybody wrote would silently do nothing. A broken ask expression asks, the same way a broken deny denies. An answer is bound to a fingerprint of the exact action it was given for: the Bot, the tool, the ref, the key, the file path and the page. That binding is what makes this more than a dialog box, because without it an id granted for "click Place order" is spendable on "click Delete account". Approvals are single use and expire after ten minutes, and they live in memory: a pending question is about a live browser session and a live turn, and an approval that outlived the process would be a grant nobody remembers giving. Answering is audited as its own act by its own actor. The person who approves is usually not the one whose turn raised the question, the two happen minutes apart, and an approval that is given and never spent leaves no action row at all, so the request, the answer and the action are three rows joined by an approval id rather than one row with a flag on it. The surface holds the tool call open, polls for the answer, and re-issues the identical request with the approval attached, so an approved action costs the Bot some seconds rather than its turn. A declined one comes back as a refusal and Stop still works out of the wait. In dry-run an ask interrupts nobody and is only recorded. The whole promise of dry-run is that switching a policy on changes nothing, and a mode that started stopping people to ask questions would be a mode nobody dares switch on. * Ask about tool calls too, and put each question on the line that raised it The ask list was only half wired. `evaluateActionPolicy` judges a Bot's calls to other people's MCP servers as well as what it does in a browser, and that call site only knew about yes and no: an ask verdict does not forward, so it was thrown as a permanent refusal, with a sentence that read "The Bot wants to call ." because the neutral blanks a tool call is judged against were mistaken for a file path. Nobody was asked, no id existed to grant, and the third list quietly became a second deny list for every tool a deployment has added. That is the outcome the list exists to prevent, and it lands on the first rule most people write, which is about somebody else's system rather than about a button. The MCP path now asks the same way the computer does: the question is opened against the same registry, the route reports it as 409 with the same body the acting routes use, and the surface holds the call open and re-issues it with the answer attached. Its binding covers the arguments as well as the tool, because a call to somebody else's server is identified by what it says rather than by what it lands on, and "post the release note in the team channel" is not permission to post something else somewhere else. Answering moved to `/api/approvals`, its own surface rather than a pair of handlers under the computer. A deployment can run plugins without a browser, and a question raised where nobody can answer it is worse than a rule that never fired: the Bot waits out the full ten minutes and then reports that nobody answered, having never asked anybody. The one projection lives beside the registry now, so the fingerprint cannot leave the process through one handler while its sibling four lines away is careful to keep it in, and an answer names the Bot the question was actually about rather than the address it arrived at: otherwise the trail holds a grant filed under one Bot and the action it paid for filed under another. The three rows are `approval.requested`, `approval.granted` and `approval.denied`, no longer named for the computer, each filed against the thing the question was about. The card in the transcript is handed its question by the tool call that raised it. It used to poll the Bot's list and show the oldest unanswered entry, and nothing withdraws a question when a wait ends, so pressing Stop or reloading a tab left one open for the rest of its ten minutes: the next turn's card offered a person a stale question, recorded their Allow against it, and left the action they were actually looking at waiting for an answer that never came. `submit` is a policy attribute now. The type tool takes a flag meaning "and press Enter", the computer presses it itself, and no keypress ever arrives as an action of its own, so a boundary written about clicking and about `key` watched the one call that submits a single-field form go straight past it. Both form presets say it, and it is in the binding, because "fill the postcode in" is not "fill it in and send the form". --------- Co-authored-by: David McKay <davidmckayv@users.noreply.github.com>
J
Jerel Velarde committed
fcebb4a93f83c89f567e5da2529726c46120c07e
Parent: 6b9e4cc
Committed by GitHub <noreply@github.com>
on 8/20/2026, 1:40:59 AM