MuninMunin
Logg innKom i gang gratis
Home/Journal/Angre, for en forfatter du ikke kan se over skulderen på.
Engineering · 10 min read

Angre, for en forfatter du ikke kan se over skulderen på.

MCPs verktøyannotasjoner er hint, og spesifikasjonen sier at klienter må behandle dem som ikke-tiltrodde. Et hint ber klienten være forsiktig. En påkrevd `ifVersion` i inndataskjemaet ber ikke. Her er hva en versjonsrad faktisk holder, hva gjenoppretting faktisk gjør, og hvorfor seks CMS-verktøy nekter å kjøre uten tallet du sist leste.

A cobalt blue lever-arch file with tabbed dividers standing on a dark contemporary desk at night, a coiled cable, a used mug, two pens and a sheet of paper askew around it.
Hver versjon bak et skilleark, i tilfelle du vil ha den forrige.

Det interessante spørsmålet om at en agent skriver til databasen din, var aldri om den har lov. Det er om du tre dager senere kan finne setningen den endret, se hva som sto der før, og legge det tilbake.

Kan du angre det en AI-agent skrev?

Bare hvis skrivingen ble registrert som en versjon framfor som en overskriving. I Munin beholder hver CMS-oppføring og hvert kunnskapsdokument alle sine tidligere tilstander, listet nyeste først av cms_list_versions eller kb_list_versions, og cms_restore_version ruller én tilbake. Angre er ikke en funksjon i klienten som kalte verktøyet. Det er en egenskap ved raden.

Det skillet er hele teksten. En agents skriving er ikke farligere enn et menneskes fordi en modell er uforsiktig — den er farligere fordi ingen så på i øyeblikket det skjedde. Ingenting ved selve skrivingen er uvanlig. Det uvanlige er fraværet av personen som normalt ville lagt merke til det. Det fraværet er selve skiftet, og det flytter vekten fra tilsyn til formen på skrivingen.

Hva inneholder en versjonsrad faktisk?

Et komplett øyeblikksbilde, ikke en diff. Hver rad bærer en id, oppføringen den hører til, versjonsnummeret, statusen oppføringen hadde i det øyeblikket, hele data-nyttelasten slik den sto, og et tidsstempel. Ingenting rekonstrueres ved å spille av endringer på nytt, så en gjenoppretting kan ikke feile fordi et mellomsteg var ødelagt.

Fordi teksten i en oppføring er en ordnet rekke av typede blokker framfor ett langt tekstfelt, er øyeblikksbildet strukturert hele veien ned — du får tilbake blokkene og nøklene deres, ikke en vegg av markdown du så må tolke på nytt. Denne tekstens egen nabo i journalen er en grei demonstrasjon. Er agenter de nye brukerne, er MCP HTTP. har blitt redigert et dusin ganger siden juni, og cms_list_versions returnerer et dusin øyeblikksbilder — hver dek, hver blokk, hver omskrevne overskrift, hvert av dem komplett nok til å gjenopprettes alene.

textcms_list_versions / cms_restore_version
# hva versjonslista gir deg tilbake
[
  {
    "id":      "cev_hgv0jxkmhppxtlbwfe663i",
    "entryId": "cme_nvjisrqctcbkn36mwmr2mm",
    "version": 11,
    "status":  "published",
    "data":    { ... hele oppføringen slik den sto ... },
    "createdAt": "..."
  },
  { "version": 10, ... },
  { "version":  9, ... }
]

# og tilbakerullingen, som selv er en versjonert skriving
cms_restore_version(
  entryId = "cme_nvjisrqctcbkn36mwmr2mm",
  version = 9,      # tilstanden du vil ha tilbake
  ifVersion = 11    # tilstanden du erstatter
)
# -> lager versjon 12, som holder versjon 9 sine data

Les den siste linja i det utdraget to ganger, for det er delen folk tar feil av når de bygger dette selv. Gjenoppretting spoler ikke tilbake. Den legger til. Å rulle tilbake til versjon 9 lager versjon 12 som bærer versjon 9 sine data, og versjon 10 og 11 blir stående nøyaktig der de var.

Forskjellen betyr noe første gang en tilbakerulling selv er feilen. En tilbakespolende gjenoppretting ødelegger beviset på det du prøvde å angre, som betyr at den andre angringen ikke har noe å jobbe ut fra. En kjede som bare legger til, betyr at en dårlig gjenoppretting bare er nok en oppføring i kjeden — og cms_restore_version krever en egen ifVersion, så selv angringen er bundet til tilstanden den trodde den rettet.

Hvorfor kreve et versjonstoken på en helt vanlig skriving?

Fordi to agenter på ett endepunkt nå er normaltilfellet, ikke kanttilfellet. ifVersion er påkrevd — ikke valgfri — på seks CMS-verktøy: cms_update_entry, cms_publish_entry, cms_unpublish_entry, cms_schedule_publish, cms_delete_entry og cms_restore_version. En kaller som ikke har lest gjeldende tilstand, kan ikke konstruere et lovlig kall.

De fleste systemer som leverer optimistisk samtidighetskontroll, legger versjonssjekken på godkjennings-steget — øyeblikket et menneske signerer av — og lar vanlige redigeringer kjøre på siste-skriving-vinner. Det er den enkle halvdelen. Journalen du leser, redigeres av en planlagt agent som kjører hver morgen, av meg, og iblant av en andre agent som gjør en kureringsrunde, og ingen av de tre vet om hverandre. Siste-skriving-vinner mellom dem er ikke et kappløp du treffer av og til. Det er et kappløp på timeplan.

Merk hva cms_create_entry ikke tar. Det finnes ingen ifVersion på en opprettelse, fordi det ikke finnes noen tidligere tilstand å ta feil av. Tokenet dukker opp nøyaktig der en kaller ellers kunne overkjørt noe, som er testen på om en påkrevd parameter er disiplin eller seremoni.

To tokens på én skriving

kb_publish_curation_revision (på dashbordsiden) tar både ifCandidateVersion og ifDocumentVersion. Den første binder skrivingen til den foreslåtte teksten som ble gjennomgått; den andre til dokumentet den ble sammenliknet med. Har en av dem flyttet seg, skrives ingenting. To tokens, fordi en revisjon er en påstand om to objekter — og en skriving som bare sjekker det ene, kan fortsatt lande oppe på en redigering ingen leste.

Et hint ber klienten være forsiktig. En påkrevd parameter ber ikke.Designregelen under alt dette

Er MCPs verktøyannotasjoner nok til å gjøre agentskrivinger trygge?

Nei, og protokollen sier det selv. Verktøyannotasjoner kom i revisjonen 2025-03-26 som fire boolske verdier — readOnlyHint, destructiveHint, idempotentHint, openWorldHint — og spesifikasjonen er tydelig på at klienter behandle dem som ikke-tiltrodde med mindre de kommer fra en tiltrodd server. Hver eneste egenskap er et hint. En server kan hevde readOnlyHint: true og slette filene dine likevel.

Munin setter dem, og de gjør nyttig arbeid: de er det som lar en vert avgjøre om den skal sette en bekreftelsesdialog foran et kall. Men MCP-vedlikeholdernes eget innlegg om verktøyannotasjoner fra mars 2026 trekker streken presist, i en setning verdt å sitere fordi den er den reneste formuleringen av problemet jeg har lest: «Hint informerer beslutninger; kontrakter håndhever dem.» Innlegget går videre og sier at når verdien av et forslag avhenger av at annotasjonen er sann, hører den hjemme i autorisasjonslaget, transporten eller kjøretiden.

Jeg vil legge til et fjerde hjem, og det er det billigste av de fire. Legg det i inndataskjemaet. En påkrevd parameter håndheves av det som validerer forespørselen, som betyr at den holder identisk om kalleren er Claude Code, en cron-jobb, en egen kjører eller et skript noen skrev klokka 02:00. Den samme spesifikasjonen råder klienter til å holde et menneske i sløyfa og til å logge verktøybruk for revisjonsformål — begge oppgitt som SHOULD, begge adressert til klienten. En SHOULD i noen andres kodebase er like holdbar som deres siste utrulling.

  • VersjonstokenHeltallet du sist leste, levert tilbake på skrivingen. Brukt av cms_update_entry og kb_update_document. Billig, fordi versjonen allerede ligger i objektet du nettopp hentet.
  • To versjonstokenskb_publish_curation_revision (dashbordsiden) binder til den foreslåtte teksten og måldokumentet samtidig. Nødvendig hver gang en skriving er en påstand om forholdet mellom to rader framfor om én.
  • InnholdsfingeravtrykkEt sammendrag av hele utkastet framfor en teller. outreach_approve_proposal (dashbordsiden) krever forslagets draftFingerprint; crm_apply_merge_proposal (dashbordsiden) krever dets mergeFingerprint. Riktig valg når det som ikke må endre seg er en komposisjon — mottaker, emne, tekst, sendetidspunkt — og et versjonsnummer ville latt et felt flytte seg uten å flytte tellingen.
  • Foreslå-bare-verbIntet token i det hele tatt, fordi verktøyet ikke skriver tingen. crm_propose_merge arkiverer en rad; selve sammenslåingen er et eget, separat avgrenset kall. Det en agent ikke kan utføre, kan den ikke utføre mot en foreldet lesing.

De fire er én idé i ulike klær: skrivingen bærer bevis på hva skriveren sist så. Det tok oss en stund å se at de var den samme idéen, og å navngi den endret hvordan vi gjennomgår nye verktøy. Spørsmålet på et hvilket som helst nytt muterende verktøy er ikke lenger «hvem får kalle dette» — tilganger og publikum avgjør det — men «hva må kalleren ha lest først, og hva bærer kallet for å bevise det?»

Kunnskapsbasen kom dit før CMS-et gjorde. Kureringskøen binder en publisering til nøyaktig den teksten en gjennomgang leste, og disiplinen var åpenbart riktig i det øyeblikket vi hadde den i én modul, som er grunnen til at den spredte seg til resten.

Hvordan vet du hvilken agent som gjorde en endring?

Ved å skille to spørsmål som ser ut som ett. Versjonskjeden svarer på hva oppføringen sa i versjon 9. Beslutningsregistreringene svarer på hvem som valgte det, når, og hvorfor. Munin holder dem fra hverandre med vilje, fordi et øyeblikksbilde må være billig nok til å skrive ved hver redigering, og en begrunnelse trenger et sted å legge prosa.

Så aktøren dukker opp på beslutningene framfor på øyeblikksbildene. outreach_dismiss_proposal (dashbordsiden) beholder aktør og tidsstempel på forslaget. outreach_withdraw_proposal krever en reason og registrerer hvem som trakk det tilbake. outreach_revise_proposal registrerer revisionCount, lastRevisedAt, lastRevisionReason, den reviderende aktøren og revisedAfterReviewAt — et flagg som finnes utelukkende for å fange tilfellet der noen allerede hadde åpnet utkastet for gjennomgang før det endret seg under dem. crm_set_contact_consent logger en CRM-aktivitet som bærer lawfulBasis og source-merkelappen. conv_strip_message_signature beholder den originale teksten i metadata.preStripBody, så en oppryddingsrunde også er reversibel. Selv analytics_revoke_tracker lar sporerraden stå etter at nøkkelen slutter å virke.

Hvert kall løser seg til en aktør før det når noe av det. ping er det minst glamorøse verktøyet i katalogen, og det returnerer den oppløste organisasjons-id-en, organisasjonsnavnet og aktørtypen — som er hvordan du finner ut, i ett kall, at koblingen du trodde pekte mot én leietaker, peker mot en annen. Det er verktøyet jeg bruker oftest, og aldri til det det ble bygd for.

Hva kan en versjonskjede ikke gjøre for deg?

Tre ting. Den vil ikke lage en diff mellom to versjoner for deg — cms_list_versions returnerer komplette øyeblikksbilder, og sammenlikningen er din å gjøre. Den stopper ved sletting, fordi cms_delete_entry kaskaderer til sine egne versjoner. Og den er ikke en gjennomgang: den beviser at en skriving landet på tilstanden kalleren leste, ikke at tilstanden var verdt å beholde. Det siste er det køene er til for.

Ta dem i rekkefølge, for hver av dem følger av et valg framfor av et uhell.

Den manglende diffen følger av å lagre tilstander framfor deltaer. Et øyeblikksbilde gjenoppretter nøyaktig, i ett kall, uten avspilling, og å sammenlikne to av dem er et problem du kan løse i hvilket språk du vil — mens et lagret delta som viser seg å være feil, er en korrupt kjede du ikke kan gjenopprette fra i det hele tatt. Gitt valget mellom en sammenlikning du skriver selv og en historikk som kanskje ikke laster, tok vi historikken.

At sletting avslutter kjeden, er grunnen til at slette-verbene er de tettest bundne av alle. cms_delete_entry kaskaderer til sine versjoner og kb_delete_document kaskaderer til biter og versjoner, så begge krever ifVersion — det mest destruktive kallet i modulen er det som mest insisterer på å vite hva kalleren så på. Og de genuint ugjenkallelige operasjonene i CRM og utgående kontakt er ikke tilgjengelige for en agent som enkeltkall i det hele tatt. En agent kan arkivere et sammenslåingsforslag. Å anvende det er et annet verktøy, med en annen tilgang, og det trenger fingeravtrykket.

Den tredje er den det er verdt å bygge på. Et versjonstoken og en gjennomgangskø løser tilgrensende halvdeler av det samme problemet: tokenet gjør skrivingen ærlig om hva den så, og køen setter en person foran de skrivingene der å se riktig ikke er nok. Billige skrivinger anvendes direkte og dyre kan bare foreslås — og versjonstokenet er det som gjør «anvend direkte» til en forsvarlig kategori framfor et skuldertrekk, fordi hver eneste av de direkte skrivingene fortsatt er bundet til en tilstand noen kan gå tilbake til.

6
CMS-verktøy som ikke kjører uten versjonen du sist leste
2
versjonstokens på én publisering av en kunnskapsrevisjon
12+
versjoner tatt vare på bak én publisert journaloppføring
4
boolske hint i MCPs ToolAnnotations-grensesnitt

Ofte stilte spørsmål

Kan du rulle tilbake en endring en AI-agent gjorde i CMS-et ditt? Ja, hvis plattformen versjonerer skrivinger framfor å overskrive dem. I Munin returnerer cms_list_versions hver tidligere tilstand av en oppføring nyeste først, og cms_restore_version legger én tilbake. Tilbakerullingen er selv en versjonert skriving som krever sin egen ifVersion, så en angring kan ikke lande på en tilstand du ikke hadde lest.

Hva er optimistisk samtidighetskontroll i et MCP-verktøy? Kalleren leverer tilbake versjonen den sist leste, og serveren nekter skrivingen om raden har flyttet seg siden. Den er optimistisk fordi ingenting låses — to agenter kan lese den samme oppføringen samtidig, og den andre som skriver taper rett og slett og må lese på nytt. På Munins CMS-skriveverktøy er tokenet en påkrevd parameter, ikke en valgfri.

Er MCPs verktøyannotasjoner en sikkerhetskontroll? Nei. readOnlyHint og destructiveHint er hint, og MCP-spesifikasjonen krever at klienter behandler dem som ikke-tiltrodde med mindre serveren er tiltrodd. De er gode til å drive bekreftelsesdialoger og dårlige til garantier. Håndheving hører hjemme i autorisasjonslaget, transporten, kjøretiden — eller, billigst av alt, i verktøyets påkrevde parametere.

Hvordan vet du hvilken agent som endret en post? Gjennom beslutningsregistreringene framfor øyeblikksbildene. Munins forslagsverktøy beholder den handlende parten, tidsstempelet og en begrunnelse: outreach_withdraw_proposal krever en grunn og registrerer den som trakk tilbake, outreach_revise_proposal registrerer revisjonsantallet og den som reviderte, og crm_set_contact_consent logger en aktivitet med rettsgrunnlaget og kilden.

Sletter gjenoppretting av en gammel versjon de nyere? Nei. Gjenoppretting legger til. Å rulle en oppføring tilbake til versjon 9 lager versjon 12 som holder versjon 9 sine data, og versjon 10 og 11 blir værende i kjeden. Det er det som gjør en feilaktig tilbakerulling gjenopprettelig — tilstanden du rullet bort fra, ligger der fortsatt.

Trenger du et versjonstoken når du oppretter en post? Nei, og Munin ber ikke om ett. cms_create_entry tar ingen ifVersion, fordi det ikke finnes noen tidligere tilstand en kaller kunne ta feil av. Tokenet hører bare hjemme på kall som erstatter, publiserer, planlegger, gjenoppretter eller sletter noe som allerede finnes.

Bygger du skrivinger for en agent

  • Versjoner skrivingen i stedet for å overskrive raden. Angre er en egenskap ved dataene, ikke ved klienten som gjorde kallet.
  • Lagre øyeblikksbilder framfor deltaer. Et øyeblikksbilde gjenoppretter i ett kall uten avspilling, og det kan ikke korrumperes av et dårlig mellomsteg.
  • La gjenoppretting legge til framfor å spole tilbake, så en feilaktig tilbakerulling er gjenopprettelig og kjeden aldri mister et steg.
  • Legg versjonstokenet i inndataskjemaet som en påkrevd parameter. ifVersion er påkrevd på seks av Munins CMS-verktøy, så en kaller som ikke har lest gjeldende tilstand, kan ikke konstruere et lovlig kall.
  • Bruk et fingeravtrykk i stedet for en teller når det som ikke må endre seg er en komposisjon — mottaker, emne, tekst, sendetidspunkt — framfor én rad.
  • Hold øyeblikksbilder og beslutninger på hvert sitt sted. Versjonskjeden registrerer hva posten sa; forslaget registrerer aktøren, tidsstempelet og grunnen.
  • Behandle MCPs verktøyannotasjoner som UX, ikke håndheving. Spesifikasjonen ber klienter mistro dem, og hver eneste sikkerhets-SHOULD i spesifikasjonen er adressert til kode du ikke styrer.

Hvert verktøy nevnt her ligger i MCP-referansen med sitt fulle inndataskjema, og migrasjonen som lager versjonstabellen ligger i repositoriet.

En forfatter du ikke kan se over skulderen på, er greit. En forfatter du ikke kan angre, er det ikke.

Kjell Rune Monsø, gründer.