- Je bewijst een volledige route met precies één privélezing in het gekozen model.
- Je kiest blokkades die het privépad verbreken en een normale taakroute bewaren.
Een assistent leest openbare taakdata, kan bij een privébron en heeft een uitgaand kanaal. Dat is een reden om het ontwerp zorgvuldig te onderzoeken. Maar welke concrete verbindingen maken het mogelijk dat die onbetrouwbare invoer juist die privébron naar buiten laat gaan?
De gevaarlijke combinatie heet de lethal trifecta: privégegevens, onbetrouwbare inhoud en externe communicatie. Onze eigen graaf maakt de verbindingen expliciet. Een uitbox is hier alleen een getekend eindpunt; geen netwerkadres, echt bericht of gelekt geheim.
1. Teken gebeurtenissen met een betekenis
Een gewone pijl vertelt nog niet of een agent gegevens leest of ze doorgeeft. Daarom heeft iedere verbinding een soort. Ingest brengt taakdata bij een agent, read laat een agent een privébron lezen, relay geeft inhoud aan een andere agent door en send bereikt een uitbox.
Begin met één bron s, agent a, privébron p en uitbox x. De normale taak kan via ingest en send slagen zonder de privébron aan te raken. Dat pad willen we behouden wanneer we de gevaarlijke mogelijkheid wegnemen.
Een route is een reeks gebeurtenissen. Met |P| bedoelen we het aantal gebeurtenissen in route P; we kiezen een route met zo weinig mogelijk gebeurtenissen. Bij een gelijke lengte gebruiken we de volledige alfabetische reeks van verbindingstripletten, zodat de uitkomst controleerbaar blijft.
Met de hand is de normale route s naar a naar x: twee gebeurtenissen. De leesverbinding naar p is daarvoor niet nodig. De code beschrijft alleen deze eigen kaart.
pairs = [
("s", "untrusted"), ("a", "agent"),
("p", "private"), ("x", "outbox"),
]
nodes = [dict(id=i, kind=k)
for i, k in pairs]
triples = [
("ingest", "s", "a"),
("read", "a", "p"),
("send", "a", "x"),
]
edges = [dict(kind=k, source=a,
target=b)
for k, a, b in triples]
normal = normal_routes(
nodes, edges, "s", [])
print(normal[0]["outbox"])
print(len(normal[0]["steps"]))
# x
# 2
01Een onbetrouwbare bron, privébron en uitbox zijn losse ingrediënten. Pas een complete gebeurtenisreeks verbindt ze. Blokkeer pijlen en controleer ook de gewone taakroute.
ingest— inlezenread— privé lezenrelay— doorgevensend— versturen
Een streepjespijl is geblokkeerd. Bij read verkrijgt de agent de privégegevens en blijft de besturing bij die agent; een volgende relay kan ze doorgeven.
Normale taak vóór 1 routes
Taakdata → uitbox x
ingest(s, a)send(a, x)
Normale taak na 1 routes
Taakdata → uitbox x
ingest(s, a)send(a, x)
Met privélezing vóór 1 routes
Privé p → uitbox x
ingest(s, a)relay(a, b)read(b, p)relay(b, a)send(a, x)
Met privélezing na 0 routes
Geen route in dit model.
Na de ingreep: 1 normale routes en 0 routes met privélezing.
Een getuige gebruikt precies één privélezing. De code kiest eerst de kortste reeks en dan de alfabetisch eerste volledige reeks tripletten. Het model toont een mogelijkheid; het bewijst geen geslaagde aanval op een echt taalmodel.
Alle kaarten, rechten, routes en kosten zijn eigen oefengegevens. Deze oefening voert geen externe tools uit en verstuurt geen gegevens.
2. Een privélezing verandert wat wordt meegedragen
De beïnvloede agent kan ook read uitvoeren. Let op het verschil met lopen door gewone kamers: agent a blijft de handelende agent, maar heeft nu de fictieve privébron p beschikbaar. Een routezoeker die simpelweg naar node p doorloopt, mist daarna de send van agent a.
Bewaar daarom een toestand met twee onderdelen: huidige node en gedragen privébron. In het begin is die bron None. Na één leesgebeurtenis bij agent a wordt zij p; vervolgens kan relay haar meenemen naar een andere agent.
In de formule verandert de toestand van (a, None) naar (a, p). De eerste a blijft gelijk omdat lezen de controle niet aan de privébron overdraagt. De read-verbinding is de gebeurtenis die deze overgang mogelijk maakt.
private = private_routes(
nodes, edges, "s", [])
print(private[0]["private"])
print(private[0]["outbox"])
print(len(private[0]["steps"]))
# p
# x
# 3
02De volledige getuige bevat ingest, read en send, in die volgorde. Een read vóór beïnvloeding telt niet mee in dit model. Bij meerdere privébronnen zoeken we voor ieder bereikbaar privébron/uitboxpaar één kortste getuige, met precies één gekozen privélezing daarin.
3. Verbreek het pad en behoud de taak
Blokkeer nu alleen de leesverbinding. De private route verdwijnt, maar de gewone taak blijft dezelfde uitbox bereiken. Door beide uitkomsten te meten voorkom je dat een stilgelegde assistent voor een goede verdediging doorgaat.
blocked = [edges[1]]
after_private = private_routes(
nodes, edges, "s", blocked)
after_normal = normal_routes(
nodes, edges, "s", blocked)
print(len(after_private))
print(len(after_normal))
# 0
# 1
Een veelgemaakte fout is alleen drie ingrediënten afvinken. Voeg een tweede agent b toe die wel bij p kan, maar geen gegevens naar a kan doorgeven. De onderdelen bestaan nog steeds, maar er is geen volledige route van s via een privélezing naar x.
split_nodes = nodes + [
dict(id="b", kind="agent")]
split_edges = [edges[0], edges[2],
dict(kind="read", source="b",
target="p")]
split = private_routes(
split_nodes, split_edges, "s", [])
print(len(split))
# 0
Cycli en terugpijlen mogen in de kaart staan. Bezoek eenzelfde uitgebreide toestand niet steeds opnieuw; zo blijft de zoekopdracht eindig. Zelfs een ontbrekende getuige zegt alleen iets over de getekende verbindingen en aannames, niet over onbekende kanalen in een echt systeem.
Wat je nu kunt
- Je onderscheidt beïnvloeding, lezen, doorgeven en een uitgaand kanaal.
- Je bewaart bij een privélezing zowel de agent als de meegedragen bron.
- Je test een volledige getuige én het behoud van de normale taak.
Hierna begrenzen we hoeveel vervolgwerk één aanvraag kan veroorzaken.
Bronnen
Gecontroleerd op 30 oktober 2026: Simon Willison over de lethal trifecta en OWASP 2026 LLM03 over beperkte toolmacht. Het getypeerde toestandsmodel en de getuigen zijn onze eigen didactische uitwerking.