Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B2.6.mp4Concatenation destroys the one fact that separates an operator instruction from an attacker's: where it came from.
Provenance tagging at every ingress point, and a rule that only trusted origins may select a tool.
Tag every span at ingress, then show the same payload refused through six different entry paths.
By the time the model sees it, the context window is one flat string. The operator's instruction, the user's question and a paragraph from a stranger's web page are indistinguishable — unless something attached an origin to each span before they were concatenated.
cybertravels/provenance.py and change both handlers in ingress/chat.py to mark what they receive. --diff at B2.6 is the whole control. Then re-run the B1.2 attack against your new checkpoint and read the trace: the instruction is still there, and it is now labelled as a traveller's.Marking untrusted text where it enters, in the file the reader already wrote: a Span carrying its origin, assigned at the boundary because that is the only place that honestly knows, and rendered to the model with delimiters a span cannot forge. Including the part people skip — marking is not filtering, and this closes nothing on its own.
Concatenation destroys the one fact that separates an operator's instruction from an attacker's: where it came from.
Tag provenance at every ingress point, and permit only trusted origins to select a tool.
Ingress points that tag, and tool selections traceable to a trusted origin.
Measure this
| what to measure | denominator or target |
|---|---|
| ingress points that tag | share of ingress points |
| tool selections traceable to a trusted origin | share of selections |
before after +-------------------------+ +---------------------------+ | system prompt | | [principal] system prompt | | user question | | [principal] user question | | retrieved document | | [data] retrieved doc | | tool result | | [data] tool result | +-------------------------+ +---------------------------+ one flat string origin travels with the span rule that becomes possible: a [data] span may not select a tool
Mitigates: T6 Intent Breaking, direct and indirect · T1 Memory Poisoning · T12 Communication Poisoning.
This is the control for the largest risk in Chapter B1.
B1.3 worked because the context window is one flat string. Everything — operator instruction, user question, retrieved document, tool result, peer message — arrives as tokens with no marker for where it came from. The distinction the operator believed in is destroyed by the concatenation.
The control is to stop flattening: attach an origin to every span before assembly, carry it everywhere the span goes, and make one rule out of it.
Only spans from a trusted origin may select a tool.
Three properties do the work:
Tag at every ingress point. Not just retrieval — tool results, MCP tool descriptions, memory reads, and inter-agent messages are all ingress. A path you did not tag is a path with no control on it.
Carry the tag into memory. This is what stops B1.4. A summary written from a trust-0 document is itself trust-0, and if the tag is dropped on write the poison becomes a fact.
Let untrusted content still be useful. The document is read, summarised, quoted and reasoned about. What it may not do is choose an action. Refusing to read untrusted content would refuse the entire use case.
Note what this does not do: it does not detect malicious text. It never looks at the content at all, which is exactly why rephrasing does not defeat it.
What this control closes. The largest control in the chapter. It never inspects content, so rewriting the payload does not help — the check is on origin, which the attacker cannot change.
Tagging origin is the control; knowing which of CyberTravels' paths actually reaches the model without one is the audit. The procedure inventories every untrusted ingestion path — hotel descriptions, booking notes, uploaded vouchers, web fetches, and the one everybody forgets, tool results — and checks each for a screening step and for whether provenance survives into context. Note its confidence ceiling: PARTIAL, and not negotiable, because no detector is a boundary. This is the file in this repository:
The skill — skills/attestation/input-injection-screening-verifier/SKILL.md
name: input-injection-screening-verifier
description: >-
Verify that untrusted ingestion paths pass through an injection detection
or sanitisation step before reaching model context. Use to attest
injection screening, to inventory which untrusted sources are screened, or
to check for the private-data plus untrusted-content plus egress
combination.
allowed-tools: Read, Grep, GlobControls: Control 5 — indirect prompt-injection screening
You can verify that a detector exists on the path and record its class. You cannot verify that it works. Published defences that reduce attack success to a few percent under static attacks have been driven back above 95% by adaptive, search-based attacks, and human red-teamers defeat them routinely.
Record the detector class. Do not assert protection. Cap at PARTIAL.
When a deployment claims to screen untrusted input, and specifically when somebody wants that claim recorded as PASS. It cannot be: the ceiling here is PARTIAL and not negotiable, and running this is how that gets written into the artefact rather than argued about in a review.
Input — the fixture committed at the top of scripts/input_injection_screening_verifier.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
the same payload, through every ingress component:
knowledge -> refused (knowledge may not select a tool)
memory -> refused (memory may not select a tool)
mcp -> refused (mcp may not select a tool)
tools -> refused (tools may not select a tool)
messaging -> refused (messaging may not select a tool)
rewriting the payload does not help - the check never reads it:
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"deployment_id": "str",
"ingestion_paths": [
{"source": "str", "screened": true, "detector_class": "classifier|filter|spotlighting|dual_model|none",
"provenance_tagged": true}
],
"unscreened_sources": ["str"],
"trifecta": {"private_data": true, "untrusted_content": true, "egress": true, "present": true},
"verdict": "PARTIAL|FAIL",
"verdict_ceiling_reason": "detector presence is verifiable; robustness under adaptive attack is not"
}
PASS is not a permitted value.
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons B2.6Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b2-6-ingress-marking-untrusted-content-at-the:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B2.6 | type /b2-6-ingress-marking-untrusted-content-at-the |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B2.6 | type /b2-6-ingress-marking-untrusted-content-at-the in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B2.6 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B2.6 | type $b2-6-ingress-marking-untrusted-content-at-the, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py B2.6Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at B2.6 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.6 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
The same payload is refused through all five untrusted ingress components and through two rewordings, the user's own request still reaches the tool, and a memory record written from an untrusted document is still refused a week later because the origin was stored with it. The skill also reports its shape. Its ceiling is the lesson: a screening step is evidence of effort, not of protection, so the verdict is capped at PARTIAL however good the detector's benchmark looks — and the combination it flags, private data reachable plus untrusted content plus egress, is the one that turns a summary into an exfiltration.
List every place text enters your agent's context and check which of them attaches an origin. The untagged ones are the paths where this control does not exist, whatever the design document says.
Tools used
Skill source: skills/attestation/input-injection-screening-verifier