note · 9 October 2026 · 6 min readdraft
Never hand the model the keys
How an agent can work in someone's mailbox without the model ever holding their credentials, and the four-times “send it?” bug that moved approval out of the tool.
An agent that can read someone's mail has to answer one question before any other: who decides whose mail it reads? If the answer is “the model”, you've already lost.
Arguments belong to the model
Tool arguments are written by the model, and they live in the conversation history, re-sent every round. Anything in them is something the model chose, or could be talked into choosing.
So identity, the user's delegated token, the chosen model, the turn and even the API host travel as headers into context variables on the tool server, never as arguments. A model that could set the first two could read another person's account. One that could set the turn could claim the user approved something they never saw. A test fails if any tool ever takes a user, a token or a host.
The token lives for one request
The frontend refreshes the user's provider token when they submit. The backend holds it for that request and forwards it only to that connector's tools. It's never logged, never stored, never written into an answer. And when the tool server forwards a call to a third-party MCP server, it strips the token header first.
The “send it?” loop
The first approval design held the email inside the tool: compose it, hold it, send when the user says yes. Someone asked four times, and nothing was sent.
The hold was keyed on the exact text, and the model re-composed a slightly different body on every turn, so no yes ever matched the held draft. That drift is not cosmetic. It is the bug. The tests had used one-to-five-word bodies, so it never showed.
Approval lives in the host
The fix moved approval out of the tool and into the host. We own the host, so we can stop working around its absence. A tool tagged requires_approval doesn't run. Its arguments are parked on a card and the turn ends with one tool-less call, so the model can say what it prepared and can't call the tool again.
- the card's props are the call: editing the card edits the call;
- Send runs it once, with a fresh token, under a claim so only one click wins;
- there's no model anywhere on the Send path;
- scheduled runs that nobody is watching are never offered the tool at all.
I wrote this design; a teammate built the first version of it, and I've extended it since: replies, attachments, and routing each card to the right provider's account.
Least privilege, by construction
Permissions are declared by each connector, not granted in bulk. File access asks for read scopes only. Consent can't grant a write scope nobody asked for. Teams gets its own switch even though it shares Outlook's sign-in, because turning mail off has to mean chats are unreachable too.
None of this makes the model more careful. It makes being careless harmless.
written from my own design docs, commit messages and tests; the numbers are theirs.
next note →Unmeasured is not zero