Werkplaats / De werkplaats
In jouw eigen tempo
← Agents en MCP
MODULE 01 · LES 02

Bind toestemming aan de exacte handeling

Valideer voorstellen, vergelijk een goedkeuringsvingerafdruk en voorkom een dubbel fictief effect.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je onderscheidt een modelvoorstel van de hostactor, het toolregister en een vertrouwd goedkeuringsbesluit.
  • Je test gewijzigde argumenten, verouderde versies en herhaalde call-IDs zonder echte externe acties.

Je assistent stelt voor een reparatieticket af te sluiten. Je hebt juist dát ticket en die status bekeken. Als de argumenten daarna veranderen, mag het oude akkoord geen andere handeling goedkeuren.

We oefenen met een Python-dictionary met twee fictieve tickets. De host, de applicatie rond het model, beheert identiteit, toestemmingen en uitvoering. Modelvoorstellen en toolbeschrijvingen vullen dat register niet zelf; deze oefening verstuurt niets naar een externe dienst.

1. Benoem de handeling vóór je haar beoordeelt

Een voorstel bevat call_id, actor, name, version en arguments. De geclaimde actor is vergelijkbaar met een naam op een formulier: de host moet haar nog vergelijken met de afzonderlijk vertrouwde sessieactor. De naam set_ticket is de enige toegestane schrijfhandeling.

p = {
    'call_id': 'c1', 'actor': 'learner',
    'name': 'set_ticket', 'version': 0,
    'arguments': {
        'ticket': 'lamp',
        'status': 'done'}
}
clean = validate_ticket_proposal(p)
p['arguments']['status'] = 'open'
print(clean['arguments']['status'])
p = clean
# done

Schrijf validate_ticket_proposal volgens het exacte contract. Alleen open en done zijn geldige statussen; identificaties zijn begrensde ASCII-strings en version is een gewone integer. Een syntactisch geldige onbekende toolnaam blijft hier geldig: pas de dispatcher beoordeelt beschikbaarheid.

De gekopieerde arguments bewaren de beoordeelde inhoud. Een extra veld dat beweert dat het voorstel al goedgekeurd is, maakt het voorstel juist ongeldig.

01
CHECKPOINTBenoem wie, wat en welke toestand

2. Bind een akkoord aan dezelfde inhoud

Denk aan een verzegelde kopie van het volledige formulier. Een vingerafdruk (fingerprint) helpt inhoud vergelijken, maar iedereen kan die hash berekenen. Alleen een afzonderlijk hostbesluit maakt een vingerafdruk onderdeel van het goedkeuringsregister.

Noem het voorstel p en de vaste JSON-weergave C(p). Sorteer alle sleutels, gebruik ASCII-escapes en verwijder de optionele spaties met de opgegeven separators; H is de SHA-256-hash van die bytes:

H(p)=SHA256(C(p))
encoded = json.dumps(
    p, sort_keys=True,
    ensure_ascii=True,
    separators=(',', ':'))
print(','.join(json.loads(encoded)))
print(len(proposal_fingerprint(p)))
other = dict(reversed(list(p.items())))
print(proposal_fingerprint(p) ==
      proposal_fingerprint(other))
# actor,arguments,call_id,name,version
# 64
# True

De andere invoervolgorde verandert de inhoud niet. Neem ook call-ID, actor, tool, versie en beide argumenten mee; alleen de ticketnaam hashen zou een gewijzigde status niet onderscheiden. Schrijf proposal_fingerprint en laat het goedkeuringsregister buiten de policy.

FICTIEVE TICKETS · ALLEEN IN DIT VOORBEELDGoedkeuring volgt het voorstel tot op de letter

Je speelt de host. Keur een voorstel goed, verander daarna één veld en probeer de actie. De vingerafdruk wordt echt met SHA-256 berekend.

Huidig voorstel · set_ticketlearner · c1 · versie 0
lamp → done
VingerafdrukBerekenen…Geen passende goedkeuring
Uitkomst laatste pogingNog niet uitgevoerd
LampopenSensoropenActuele versie0Voltooide calls0

Een hash is geen toestemming. Alleen jouw knopbesluit vult hier het aparte register. Herhalen van dezelfde geslaagde call leest het bewaarde resultaat en verandert niets opnieuw.

02
CHECKPOINTEen vingerafdruk bindt inhoud, maar verleent geen recht

3. Voer precies één beoordeelde wijziging uit

Aanvankelijk staan lamp en sensor open op versie 0. Voor een nieuwe call controleert dispatch_ticket eerst hostinvoer, actor en toolnaam. Daarna volgen herhaling of conflict, actuele versie, toestemming, bestaand ticket en logruimte.

De volgende code speelt expliciet de host die het voorstel goedkeurt. Met dat besluit wordt uitsluitend lamp done; sensor blijft open en de globale versie wordt 1.

approved = {proposal_fingerprint(p)}
state = {'version': 0, 'tickets': {
    'lamp': 'open', 'sensor': 'open'}}
log = {}
r = dispatch_ticket(
    p, 'learner', approved, state, log)
print(r['status'], state['version'])
print(state['tickets']['lamp'],
      state['tickets']['sensor'])
# ok 1
# done open

Bewaar bij succes de vingerafdruk en een gekopieerd resultaat in completed. Een weigering verandert geen ticket, versie of voltooid-log. Onze globale versie kan ook door een wijziging aan een ander ticket verouderen; dat is een bewuste vereenvoudiging.

4. Herhaal het resultaat zonder het effect

Idempotentie betekent hier dat dezelfde geslaagde call geen tweede wijziging uitvoert. De identieke call krijgt het opgeslagen resultaat met replayed=True, ook zonder opnieuw goedkeuring te gebruiken. De actorcontrole blijft wél vóór deze replay staan.

r = dispatch_ticket(
    p, 'learner', set(), state, log)
print(r['replayed'], state['version'])
r['version'] = 900
print(log['c1']['result']['version'])
r = dispatch_ticket(
    p, 'other', set(), state, log)
print(r['status'])
# True 1
# 1
# forbidden

De teruggegeven kopie beschermt het opgeslagen resultaat. Een nieuwe uitvoering met dezelfde call-ID maar andere inhoud wordt geweigerd; een nieuwe call-ID met de oude versie eveneens:

changed = {**p, 'arguments': {
    'ticket': 'lamp', 'status': 'open'}}
r = dispatch_ticket(
    changed, 'learner', approved,
    state, log)
print(r['status'])
new = {**p, 'call_id': 'c2'}
r = dispatch_ticket(
    new, 'learner', approved,
    state, log)
print(r['status'], state['version'])
# call-id-conflict
# stale-version 1

Schrijf dispatch_ticket met deze controlevolgorde en de overige begrenzingen uit de opdracht. Het interne completed-log is vertrouwd, beperkt en alleen in geheugen aanwezig. Crashbestendige opslag, transacties en gelijktijdige uitvoering zijn daarmee nog niet opgelost.

03
CHECKPOINTHerhaal het resultaat, niet het effect

Wat neem je mee?

  • Vormcontrole bewijst geen identiteit of toestemming.
  • Een hash bindt het bekeken voorstel, maar verleent zelf geen bevoegdheid.
  • Replay herhaalt een resultaat; alleen een nieuwe geslaagde call verandert toestand.

Hierna plannen we afhankelijkheden en beslissen we welke mislukte taken veilig opnieuw mogen.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPHervat een plan zonder geslaagd werk te herhalen
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 / 3
01
Benoem wie, wat en welke toestand

Schrijf validate_ticket_proposal(proposal). Exact vijf velden zoals hierboven; call_id,actor,name zijn ASCII [A-Za-z][A-Za-z0-9_-]{0,39}; version is gewone int 0..1000000. arguments bevat exact ticket/status: ticket voldoet aan [a-z][a-z0-9-]{0,31}, status is open of done. Geef een nieuwe dictionary met gekopieerde arguments; anders ValueError. Een syntactisch geldige onbekende naam laat deze stap door; de dispatcher weigert hem.

Een hint

Validatie bewijst geen identiteit. Een extra veld approved=True maakt een voorstel juist ongeldig.

02
Een vingerafdruk bindt inhoud, maar verleent geen recht

Schrijf proposal_fingerprint(proposal). Valideer, gebruik json.dumps(...,sort_keys=True,ensure_ascii=True,separators=(',',':')) en hash de ASCII-bytes met hashlib.sha256. Geef de hexstring terug. Deze functie geeft nooit zelf toestemming.

Een hint

Neem alle vijf velden mee, ook de nested arguments. Het goedkeuringsregister wordt uitsluitend door de host gevuld.

03
Herhaal het resultaat, niet het effect

Schrijf dispatch_ticket(proposal,actor,approved,state,completed). Hostactor heeft hetzelfde ID-patroon; approved is set van maximaal 64 SHA-256-hexstrings. state heeft exact version (int 0..1000001) en tickets (1..16 geldige ticket/status-paren). completed is een intern vertrouwd dictionarylog tot 64 rijen. Volg bovengenoemde volgorde. Foutstatussen: forbidden,unknown-tool,call-id-conflict,stale-version,approval-required,missing-ticket,log-full. Succes: status=ok,version,ticket,value,replayed=False. Bewaar per call-ID fingerprint en een kopie van result. Wijzig alleen bij succes state en completed. Geef bij replay een kopie met replayed=True.

Een hint

Gebruik validate_ticket_proposal en proposal_fingerprint. Sla geweigerde voorstellen niet als voltooide call op. De fout is een gecontroleerd resultaat, maar ongeldige invoervorm geeft ValueError.

Vastgelopen? Bekijk de voorbeeldoplossing
import copy
import json
import math
import re
import hashlib

def validate_ticket_proposal(proposal):
    fields = {'call_id', 'actor', 'name', 'version', 'arguments'}
    if not isinstance(proposal, dict) or set(proposal) != fields:
        raise ValueError('Exact voorstelcontract vereist')
    for key in ['call_id', 'actor', 'name']:
        if not isinstance(proposal[key], str) or not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,39}', proposal[key]):
            raise ValueError('Begrensde ASCII-identificatie vereist')
    if type(proposal['version']) is not int or not 0 <= proposal['version'] <= 1000000:
        raise ValueError('Gehele versie 0..1000000 vereist')
    args = proposal['arguments']
    if not isinstance(args, dict) or set(args) != {'ticket', 'status'}:
        raise ValueError('Exact ticket/status vereist')
    if not isinstance(args['ticket'], str) or not re.fullmatch(r'[a-z][a-z0-9-]{0,31}', args['ticket']) or args['status'] not in ['open', 'done']:
        raise ValueError('Ongeldige ticket-ID of status')
    return {**proposal, 'arguments': dict(args)}

def proposal_fingerprint(proposal):
    clean = validate_ticket_proposal(proposal)
    encoded = json.dumps(clean, sort_keys=True, ensure_ascii=True, separators=(',', ':'))
    return hashlib.sha256(encoded.encode('ascii')).hexdigest()

def dispatch_ticket(proposal, actor, approved, state, completed):
    proposal = validate_ticket_proposal(proposal)
    if not isinstance(actor, str) or not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,39}', actor):
        raise ValueError('Hostactor vereist')
    if not isinstance(approved, set) or len(approved) > 64 or any(not isinstance(x, str) or not re.fullmatch(r'[0-9a-f]{64}', x) for x in approved):
        raise ValueError('Begrensde hostgoedkeuringen vereist')
    if not isinstance(state, dict) or set(state) != {'version', 'tickets'} or type(state['version']) is not int or not 0 <= state['version'] <= 1000001:
        raise ValueError('Ongeldige hosttoestand')
    if not isinstance(state['tickets'], dict) or not 1 <= len(state['tickets']) <= 16 or any(not isinstance(k, str) or not re.fullmatch(r'[a-z][a-z0-9-]{0,31}', k) or v not in ['open', 'done'] for k, v in state['tickets'].items()):
        raise ValueError('Ongeldige eigen tickets')
    if not isinstance(completed, dict) or len(completed) > 64:
        raise ValueError('Begrensd intern uitvoerlog vereist')
    if proposal['actor'] != actor:
        return {'status': 'forbidden'}
    if proposal['name'] != 'set_ticket':
        return {'status': 'unknown-tool'}
    digest = proposal_fingerprint(proposal)
    prior = completed.get(proposal['call_id'])
    if prior is not None:
        if prior['fingerprint'] != digest:
            return {'status': 'call-id-conflict'}
        return {**copy.deepcopy(prior['result']), 'replayed': True}
    if proposal['version'] != state['version']:
        return {'status': 'stale-version'}
    if digest not in approved:
        return {'status': 'approval-required'}
    if proposal['arguments']['ticket'] not in state['tickets']:
        return {'status': 'missing-ticket'}
    if len(completed) == 64:
        return {'status': 'log-full'}
    state['tickets'][proposal['arguments']['ticket']] = proposal['arguments']['status']
    state['version'] += 1
    result = {'status': 'ok', 'version': state['version'], 'ticket': proposal['arguments']['ticket'], 'value': proposal['arguments']['status'], 'replayed': False}
    completed[proposal['call_id']] = {'fingerprint': digest, 'result': copy.deepcopy(result)}
    return result

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