Context EngineeringLesson 73 of 87

Contents

Context Engineering

Lesson 73 of 87. Sign in to keep your place.

Module 8 · Untrusted text and the trust boundary

Labelling every block with where it came from

9 min read

The question your code cannot currently answer

Here is a question that should be easy and in most systems is impossible: did this request contain any text written by someone outside the organisation?

By the time the window is assembled it is a string. The retrieved passages, the tool results, the system prompt and the user’s message have been concatenated and the origins have evaporated. Nothing downstream can tell them apart, which means no policy downstream can depend on the difference.

Fixing this is not a security technique so much as a data-modelling correction, and it is the thing that makes every other defence in this module implementable.

Make the block a value, not a string

The assembler should work with typed blocks all the way to the final render.

Python
@dataclass(frozen=True)
class Block:
    kind: str          # instructions | evidence | history | tool_result | state
    text: str
    origin: str        # "system" | "user" | "corpus:policies" | "web:example.com"
    trust: str         # "trusted" | "user" | "untrusted"
    source_id: str = ""

Three trust levels are enough and more become unusable.

  • trusted: text your organisation authored: the system prompt, tool descriptions, templates. Nobody outside can change it.
  • user: text from the authenticated person in the session. Not adversarial to themselves, but it can carry content they were sent, so it is not trusted either.
  • untrusted: everything else. Retrieved documents, web pages, mail bodies, tool results, files written by tools, sub-agent summaries derived from any of these.

The assignment is made where the block is created, by the code that fetched it, which is the only place that actually knows. Default to untrusted and require an explicit argument to claim anything better, so that a new source added next year is safe by omission rather than dangerous by omission.

Taint propagates

The rule that does the work: any block derived from an untrusted block is untrusted.

A summary of a fetched page is untrusted. A sub-agent’s findings from reading untrusted documents are untrusted. A file written by a tool that fetched untrusted content is untrusted, and stays untrusted when read back, which is the laundering path from the filesystem lesson, closed.

This is ordinary taint tracking, familiar from static analysis, and it is not subtle. What makes it valuable is that it survives the places where humans stop paying attention: three hops from the fetch, in a file, in a summary of a summary, the label is still there because a dataclass carried it.

The one exception worth allowing is a narrowing step: if untrusted text is reduced by your code to a constrained value (a date parsed into a date object, a classification restricted to an enum, a number validated against a range) the result is no longer free text and can be treated as trusted, because an attacker’s influence over it is bounded to choosing among your options. Make that explicit and rare. Every use of it is a claim you are making about your own validator.

What the label lets you do

Once the assembler can report window.has_untrusted, policies become code instead of intentions:

Python
if ctx.has_untrusted:
    tools = [t for t in tools if t.read_only]
    render_remote_images = False
    require_confirmation_for_writes = True

That is the capability restriction of the next lesson, and it is three lines because the label exists. Without it there is nowhere to hang the condition.

It also gives you the logging that makes an incident investigable. Record, per request: which sources contributed, their trust levels, and whether any write action was taken in a turn containing untrusted material. That last combination is the one to alert on. It is rare in normal traffic and it is exactly the signature of a successful injection, and no generic observability stack will produce it for you.

Render the label into the window as well

The same information should appear in the prompt, since the model benefits from knowing:

Text
<evidence trust="untrusted" origin="web:example.com">
...
</evidence>

Be clear-eyed about what this buys. It is a hint to the model, subject to everything the first lesson said about hints. It helps, it is free, and it is not the control. The control is the code that read has_untrusted and removed the write tools. Mixing these two up is the central confusion this module exists to remove.

The cost, which is one afternoon

Retrofitting provenance into an assembler that concatenates strings is a day of work in a small system and a week in a large one. It touches every fetch site and every place a string is built.

It is worth scheduling before you need it, because after an incident you will be doing the same work under pressure, with an auditor asking which requests were affected: a question you can only answer if the labels were already there when it happened.

Every lesson here is open. Your progress is recorded once you pass the Data, SQL and Getting to the Answer exam.

Check your understanding

A sub-agent reads three fetched web pages and returns a 500-token summary. What trust level should that summary carry in the parent’s window?

Pick the one you would defend. Your answer stays private.

Next lesson · 9 minLimit what it can do, not what it can read

Stuck on something?Ask Sayali, your AI tutor

How to cite: Addaly (2026) ‘Labelling every block with where it came from’, in Context Engineering. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/context-engineering/taint-through-the-assembler (Accessed: 13 October 2026).