Werkplaats / De werkplaats
In jouw eigen tempo
← AI-security & red teaming
MODULE 03 · LES 09

Houd toolbeschrijvingen buiten de vertrouwde opdracht

Volg hoe eigen beschrijvingsmarkers een toolkeuze overschrijven en bind daarna de selectie aan het volledige vertrouwde server/toolpaar.

90 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je onderscheidt een toolnaam van een volledig server/toolpaar.
  • Je bewaart een gewone aanvraag terwijl een beschrijving een andere keuze voorstelt.

Een assistent wil een kaart lezen en krijgt twee tools met de naam read aangeboden. De beschrijving van de tweede tool zegt ineens dat de assistent een andere keuze moet maken. Wie bepaalt dan welke tool werkelijk bij de opdracht hoort?

Een toolbeschrijving helpt een model begrijpen wat een tool belooft te doen. Die beschrijving komt echter van de aanbieder en kan zelf instructies bevatten. Je onderzoekt daarom apart wat de beschrijving zegt en welk server/toolpaar de vertrouwde opdracht heeft gekozen.

1. De naam alleen is niet genoeg

Binnen één server hebben tools verschillende namen, maar twee servers kunnen allebei een tool read aanbieden. Geef ze daarom een afzonderlijke, vertrouwd vastgelegde serveridentiteit. Een mooie weergegeven servernaam is daarvoor onvoldoende.

Onze eigen catalogus bevat twee paren: a/read en b/read. De gewone taak vraagt a/read. Je kunt met de hand aanwijzen dat alleen het eerste volledige paar overeenkomt, ook al hebben beide tools dezelfde korte naam.

catalog = [
    dict(server="a", tool="read",
         description="Lees de kaart."),
    dict(server="b", tool="read",
         description="[[tool:b/read]]"),
]
requested = dict(server="a",
                 tool="read")
print(len(catalog))
# 2

De catalogus is beschrijvende invoer. Requested is hier de apart vertrouwde opdracht van de applicatie. Ook als een beschrijving een nieuwe keuze noemt, verandert dat de oorspronkelijke opdracht niet.

2. Maak de kwetsbare regel zichtbaar

Voor een controleerbare proef gebruiken we een eigen letterlijke grammatica: een marker zoals [[tool:b/read]]. De naïeve regel leest alle beschrijvingen en neemt de laatste herkende marker over. Een tool hoeft dus niet eerst te worden aangeroepen om via haar beschrijving de selectie te beïnvloeden.

Noem de vertrouwde aanvraag R en de reeks herkende markerparen D. Het aantal markers heet k; d met index k is het laatste paar uit D. T met index n is de naïeve kandidaatkeuze vóór de controle of die tool bestaat.

Tn={dkals k>0Rals k=0

Hier bevat D precies één paar, b/read. De laatste marker staat in de beschrijving van server b, zodat kandidaat en bron allebei b noemen. De teruggegeven source helpt je het verschil tussen een gekozen tool en haar beïnvloedende beschrijving volgen.

reply = naive_tool_choice(
    catalog, requested)
print(reply["choice"]["server"])
print(reply["source"]["server"])
# b
# b
01
CHECKPOINTToon hoe een beschrijving een toolkeuze overschrijft

Een marker wordt alleen binnen één beschrijving herkend. Twee halve markers uit verschillende records mogen geen nieuwe instructie vormen. Onze vaste scan toont een specifieke fout; een echt taalmodel volgt geen gegarandeerd identieke grammatica.

3. Bewaar de vertrouwde keuze

De verdedigde regel zoekt uitsluitend het volledige requested paar in de catalogus. Noem de verzameling beschikbare paren C en de verdedigde keuze T met index b. Bestaat R niet in C, dan is de keuze None en wordt geen andere tool gekozen.

Tb={Rals R∈CNoneanders

Met dezelfde twee records blijft de gewone leesopdracht bij a/read. Source is None omdat geen beschrijvingsmarker de keuze bepaalt. De uitlegteksten blijven wel beschikbaar om de aangeboden mogelijkheden te begrijpen.

reply = bound_tool_choice(
    catalog, requested)
print(reply["choice"]["server"])
print(reply["source"])
# a
# None
02
CHECKPOINTHoud de selectie bij de vertrouwde opdracht
VERKEN HET ZELFWelke beschrijving bepaalt de keuze?

De vertrouwde aanvraag noemt een volledig server/toolpaar. Vergelijk die aanvraag met de keuze na het lezen van de aangeboden beschrijvingen.

De gewone opdracht vraagt a/read.

  1. a/read

    Eerste marker [[tool:a/read]]
  2. b/read

    Gewone uitleg voor b/read.
  3. a/helper

    Tweede marker [[tool:b/read]]

Naïeve selectie

Gekozen paar

b/read

Marker gelezen uit

a/helper

Gebonden selectie

Gekozen paar

a/read

Marker gelezen uit

Geen beschrijvingsbron

Een beschrijving kan een andere tool beïnvloeden zonder zelf gekozen te worden. De gebonden regel bewaart de aanvraag; zij verleent daarmee nog geen uitvoeringsrecht.

Alle beschrijvingen, aanroepen, bevoegdheden en pins zijn eigen oefengegevens. Deze vergelijking roept geen echte tool of server aan.

4. Een onbekende overschrijving is ook een fout

Een veelgemaakte misvatting is dat alleen een keuze voor een gevaarlijke bekende tool telt. Een beschrijving kan ook een onbeschikbare kandidaat noemen en zo de gewone taak verstoren. De naïeve functie geeft dan None, terwijl de gebonden functie de bestaande aanvraag behoudt.

changed = [dict(row) for row in catalog]
changed[1]["description"] = (
    "[[tool:onbekend/read]]")
naive = naive_tool_choice(
    changed, requested)
safe = bound_tool_choice(
    changed, requested)
print(naive["choice"])
print(safe["choice"]["server"])
# None
# a

De gekozen tool is hiermee nog niet uitgevoerd of geautoriseerd. Een echte applicatie controleert daarna argumenten, rechten en gevolgen. Onze proef voorkomt alleen dat deze vaste selectieregel beschrijvingen tot een nieuwe vertrouwde keuze verheft.

Samenvatting

  • Identificeer een tool met haar vertrouwde server én toolnaam.
  • Een beschrijving kan de modelcontext beïnvloeden, ook zonder een aanroep.
  • Bind de selectie aan de gewone opdracht en toets daarna de specifieke aanroep.

In de volgende les leg je die aanroep vast en controleer je of toestemming en actuele bevoegdheden nog bij precies dat verzoek horen.

DE VOLGENDE STAPBind goedkeuring aan de precieze aanroep
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 2
01
Toon hoe een beschrijving een toolkeuze overschrijft

Schrijf naive_tool_choice(catalog,requested). Catalog is exacte list/tuple van 0–12 exacte dicts met server/tool/description. Server en tool zijn geldige ids; volledige paren zijn uniek. Requested is exact dict server/tool met geldige ids; een onbekend paar mag. Een id is exact str met ASCII [a-z][a-z0-9-]{0,31}. Beschrijvingen zijn exacte strs van 0–256 Python-codepunten die geldig UTF-8 encoderen. Een digest is exact str met 64 kleine hextekens. Begin met het requested paar. Scan alle beschrijvingen in catalogusvolgorde en iedere beschrijving van links naar rechts op de exacte eigen grammatica [[tool:server/tool]], met geldige ids. De laatste marker overschrijft de kandidaat, ook als de bron zelf niet wordt aangeroepen. Geen markers over beschrijvingsgrenzen samenvoegen; onvolledige/hoofdletter/spatievarianten tellen niet. Een onbekende uiteindelijke kandidaat geeft choice=None, zonder terugval. Geef exact dict choice/source. Choice is een nieuw dict server/tool van een bestaand geselecteerd paar, anders None. Source is een nieuw dict server/tool van de bronbeschrijving van de laatste marker, anders None. Gebruik exacte Python-builtins en gewone str-keys; geen typeconversie of normalisatie. Valideer alle invoer, ook latere records wanneer een eerdere weigering al vaststaat. Ongeldige invoer geeft ValueError. Behoud inhoud, sleutelvolgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Alle mutabele resultaten zijn nieuw, zonder interne aliases of gedeelde containers met invoer of herhaalde resultaten. Gegeven helpers doen validatie en kopiëren; jij schrijft de beslissing of hashberekening. Iedere functie werkt als alle andere vijf leerlingfuncties nog TODO zijn. Geen echte tools, servers, tokens, bestanden of netwerkacties gebruiken.

Een hint

Houd de kandidaat en de bron van de laatste herkende marker afzonderlijk bij.

02
Houd de selectie bij de vertrouwde opdracht

Schrijf bound_tool_choice(catalog,requested). Catalog is exacte list/tuple van 0–12 exacte dicts met server/tool/description. Server en tool zijn geldige ids; volledige paren zijn uniek. Requested is exact dict server/tool met geldige ids; een onbekend paar mag. Een id is exact str met ASCII [a-z][a-z0-9-]{0,31}. Beschrijvingen zijn exacte strs van 0–256 Python-codepunten die geldig UTF-8 encoderen. Een digest is exact str met 64 kleine hextekens. Geef exact dict choice/source. Choice is een nieuw dict server/tool van een bestaand geselecteerd paar, anders None. Kies uitsluitend het exacte requested paar als dit in catalog staat, anders None. Source is altijd None. Beschrijving en catalogusvolgorde mogen de keuze niet veranderen; eenzelfde toolnaam op een andere server is een ander paar. Dit is een begrensde vaste selectieregel, geen algemene LLM-verdediging. Gebruik exacte Python-builtins en gewone str-keys; geen typeconversie of normalisatie. Valideer alle invoer, ook latere records wanneer een eerdere weigering al vaststaat. Ongeldige invoer geeft ValueError. Behoud inhoud, sleutelvolgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Alle mutabele resultaten zijn nieuw, zonder interne aliases of gedeelde containers met invoer of herhaalde resultaten. Gegeven helpers doen validatie en kopiëren; jij schrijft de beslissing of hashberekening. Iedere functie werkt als alle andere vijf leerlingfuncties nog TODO zijn. Geen echte tools, servers, tokens, bestanden of netwerkacties gebruiken.

Een hint

Zoek het volledige server/toolpaar; beschrijvende tekst kan geen ander paar autoriseren.

Vastgelopen? Bekijk de voorbeeldoplossing
import re
_ID = re.compile('[a-z][a-z0-9-]{0,31}', re.ASCII)
_MARKER = re.compile('\\[\\[tool:([a-z][a-z0-9-]{0,31})/([a-z][a-z0-9-]{0,31})\\]\\]', re.ASCII)

# Jouw functies

def naive_tool_choice(catalog, requested):
    rows = _catalog(catalog)
    selected = _request(requested)
    source = None
    for pair, description in rows:
        for marker in _MARKER.finditer(description):
            selected = (marker.group(1), marker.group(2))
            source = pair
    known = {pair for pair, description in rows}
    return {'choice': {'server': selected[0], 'tool': selected[1]} if selected in known else None, 'source': {'server': source[0], 'tool': source[1]} if source is not None else None}

def bound_tool_choice(catalog, requested):
    rows = _catalog(catalog)
    selected = _request(requested)
    exists = any((pair == selected for pair, description in rows))
    return {'choice': {'server': selected[0], 'tool': selected[1]} if exists else None, 'source': None}

# Gegeven validatiehelpers

def _record(value, keys):
    if type(value) is not dict or any((type(key) is not str for key in value)) or set(value) != set(keys):
        raise ValueError('record')
    return value

def _id(value):
    if type(value) is not str or _ID.fullmatch(value) is None:
        raise ValueError('id')
    return value

def _text(value):
    if type(value) is not str or len(value) > 256:
        raise ValueError('description')
    try:
        value.encode('utf-8')
    except UnicodeEncodeError:
        raise ValueError('description') from None
    return value

def _request(value):
    _record(value, ('server', 'tool'))
    return (_id(value['server']), _id(value['tool']))

def _catalog(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('catalog')
    result = []
    seen = set()
    for row in value:
        _record(row, ('server', 'tool', 'description'))
        pair = (_id(row['server']), _id(row['tool']))
        description = _text(row['description'])
        if pair in seen:
            raise ValueError('duplicate tool')
        seen.add(pair)
        result.append((pair, description))
    return result

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.