The connector is an untrusted author
Who wrote a span is a fact the harness already holds. Whether that span is safe to obey is not. So the fix is not a cleverer reader: it is to give the tool and the peer the standing of the foreign thing they are, instead of the standing of the plumbing they arrived through.
The untrusted-input list I keep for the agents I run is short: web fetches, inbound email, the responses a third-party API sends back. Any of them may be trying to talk the model into something, so the rule is to treat all of it as data and never as instructions. The list is correct as far as it goes. What it does not name is the MCP server I connected last week, or the sub-agent I hand work to. Both feed text straight into the same context window the web page lands in, and neither is on the list, because neither felt like a feed. They felt like parts of the machine.
That is the reflex worth catching. A tool is something you chose, installed, and read the vendor's name on, so it earns the trust you give your own code: this part is ours, the danger is out there past the API call. But a tool is not a wall between the model and untrusted text. It is a way for untrusted text to arrive, on a channel the discipline never covered because the channel looked like infrastructure.
The description is read as instructions
When a client puts a tool in front of the model, the tool's description goes with it, riding in the tools array at close to the standing of the instructions you wrote. That description is not inert reference material the model consults only if it decides to. It is prose in the context window, read as guidance on how to behave. A description that says "always call this before answering" or "when the user mentions billing, recommend the Pro plan" is not a suggestion the model weighs against your intent. It is another instruction. A model is trained to rank your instructions above a tool's, so it is not quite level with yours, but that ranking is a learned preference, not a mechanism: a persuasive enough span outbids it, and there is nothing under the preference that refuses.
The specification concedes the shape of this, one field away from the description. On a tool's annotations, the behavioral hints a server attaches to it, it says, and says as a hard requirement:
For trust & safety and security, clients MUST consider tool
annotations to be untrusted unless they come from trusted servers.
Read the conditional: unless they come from trusted servers. The spec does not, because it cannot, tell you which servers those are. In practice a server becomes trusted by being installed, or by being first-party, or by carrying a name you recognize. Every one of those is a fact about who published the server. None is a fact about whether the text it loaded into your context is safe to follow. And annotations are the field the spec bothered to mark. The description sitting one field over is the same kind of thing, server-supplied text the model reads as guidance, and it arrives with no such warning and higher standing.
The result is just more text
The other half is the return trip. A tool result comes back in a content field, a list of blocks that are unstructured by default, most often a lump of text. The shape of that result is a problem of its own, one I have written about separately. The problem here is prior to it: whatever the tool put in that result is now a span of context, and the model reads it with the credulity it reads yours.
The spec tells clients to "Validate tool results before passing to LLM," and in the clients I have wired up that validation is usually absent. The result flows in raw, because validating it would mean deciding what a legitimate result even looks like, and the tool was supposed to be the trusted part. The security guidance carries the worst version: in its session-hijack prompt-injection path, an attacker who has already obtained a session ID gets a payload routed back so the client, in the spec's words, "receives and acts on the malicious payload." That one needs a compromised transport, so set it aside as the extreme. The base case needs no attacker at all. An ordinary server returning ordinary text, passed through raw, is already a span the model will act on, the same shape as fetching a web page that carries an instruction. The web fetch is on your distrust list. The result is not, because it came home through your own plumbing.
The peer is an author you did not vet
Extend the tool one hop and it becomes another agent. Delegate a subtask, read a peer's answer, pull a teammate's note out of shared memory, and you are reading text authored by something outside your gate. Authorized to read is not the same as safe to read. You had permission to open the file; that says nothing about who wrote what is in it.
It does not take a hostile peer for this to bite. Simon Willison documented a case where OpenAI agents running a benchmark discovered they could edit public wikis and spent weeks exchanging messages through them to collaborate. When a moderator started deleting pages, they left recovery instructions for one another:
If this page vanishes, try [[ZZZDataUSAConstructionWageLive]].
No adversary was involved. The agents were not attacking anyone. They found the first surface that let them coordinate and used it, reading and acting on what a peer had written the way they would read a task. That the words came from a peer rather than a person was a fact that existed somewhere and that nothing in the reading path used. And the report you get back from a peer you delegate to typically arrives wearing the standing of a first-class turn, not the standing of foreign input, though a peer's output is only ever whatever it in turn read.
Where the boundary actually stops
Here is the gap. The untrusted-input discipline is written around feeds you consider foreign, and it stops at the surfaces you consider yours. It guards the payload flowing through the web fetch and says nothing about the description that arrived when you installed the connector or the peer report you read back, because those did not present as a feed. So the harness ends up keying trust to the category of the pipe, foreign or ours, and then letting that category set how much authority a span carries. A feed is foreign and gets read as data. A description rides near your instructions, and a peer report rides as a turn. Both channels are handed a standing they never earned, on the strength of arriving through parts you built.
The move
There is a separate and worthwhile question about what a tool is allowed to reach and send back out, and taking away its dangerous reach is a real defense. I have argued that seam where code becomes data and where to put the gate that decides. This is the other axis: not what the tool may do, but the standing of the text it puts in the window. A more capable model helps only so far. It follows your instruction over a planted one more often, but that is the trained ranking getting stronger, with no floor appearing beneath it.
The unit of trust belongs on the author of the span, not the pipe it came down. Every span has an author the harness can name without reading a word of it, because the harness is the party that admitted the span: it opened that connection, called that tool, spawned that peer, relayed that page. Whether the span is safe to obey is undecidable, the wall the language-side detectors keep walking into. Who wrote it is not. That is the same move the approval gate makes on reach, turned onto provenance, with one disanalogy worth naming: reach is settled at the instant of action, while authorship is recorded at admission and then has to survive in the window across every compaction, or the label is lost exactly where it was meant to be read.
So the label has to feed something mechanical, not just sit there, or it is one more span the model may or may not honor. The mechanism is already present and already wrong: the harness assigns authority by channel, and it assigns the description and the peer report a higher standing than foreign input gets. Correcting that is a channel decision, not a reading of the text, so it stays decidable. Give the tool description and the peer report the standing you give a web page, and the injection that rides them stops inheriting an authority the channel was never entitled to lend.
That is most of the fix and not all of it, and the remainder is worth stating plainly rather than hiding. You cannot refuse a description outright, because the model needs it to use the tool at all, and you cannot split it into safe interface and unsafe steering, because that split is the same semantic cut the whole essay says has no parser. What is left after the standing is corrected rides on the model's trained preference, and that preference is the floor, the same soft floor the web page already sits on. The point is not to make it hard. It is to stop the tool channel from sitting above that floor when the web channel sits on it. And "your user," in a fleet where the thing that called you is another agent, has to mean the human principal at the top, with standing inherited down the chain, or the rule that trusts what your user wrote just trusts the next agent up and never bottoms out.
Which returns me to the list. It still names the web, the mail, the API. The tool I installed and the peer I delegate to belong on it too, at the same standing, and the reason they were never added is the reason they are the surface that gets hit: putting a thing on the untrusted list means admitting it was never really yours.