MuninMunin
Logga inKom igång gratis
Home/Journal/Ångra, för en författare du inte kan se över axeln på.
Engineering · 10 min read

Ångra, för en författare du inte kan se över axeln på.

MCP:s verktygsannoteringar är ledtrådar, och specifikationen säger att klienter måste behandla dem som otillförlitliga. En ledtråd ber klienten vara försiktig. Ett obligatoriskt `ifVersion` i indataschemat ber inte. Här är vad en versionsrad faktiskt innehåller, vad en återställning faktiskt gör, och varför sex CMS-verktyg vägrar köra utan numret du senast läste.

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.
Varje version bakom en flik, ifall du vill ha den förra.

Den intressanta frågan om att en agent skriver till din databas var aldrig om den får lov. Den är om du, tre dagar senare, kan hitta meningen den ändrade, se vad som stod där förut, och sätta tillbaka det.

Kan du ångra det en AI-agent skrev?

Bara om skrivningen registrerades som en version snarare än som en överskrivning. I Munin behåller varje CMS-post och varje kunskapsbasdokument alla sina tidigare tillstånd, listade nyast först av cms_list_versions eller kb_list_versions, och cms_restore_version rullar tillbaka ett av dem. Ångra är inte en funktion hos klienten som anropade verktyget. Det är en egenskap hos raden.

Den skillnaden är hela inlägget. En agents skrivning är inte farligare än en människas för att en modell är slarvig — den är farligare för att ingen tittade på i det ögonblick det hände. Ingenting med själva skrivningen är ovanligt. Det som är ovanligt är frånvaron av personen som normalt hade lagt märke till det. Den frånvaron är själva skiftet, och den flyttar tyngden från övervakning till skrivningens form.

Vad innehåller en versionsrad egentligen?

En komplett ögonblicksbild, inte en diff. Varje rad bär ett id, posten den tillhör, versionsnumret, statusen posten hade i det ögonblicket, hela data-nyttolasten som den stod, och en tidsstämpel. Ingenting rekonstrueras genom att spela upp ändringar igen, så en återställning kan inte misslyckas för att ett mellansteg var felformat.

Eftersom en posts brödtext är en ordnad array av typade block snarare än ett enda långt textfält är ögonblicksbilden strukturerad hela vägen ned — du får tillbaka blocken och deras nycklar, inte en vägg av markdown som du sedan måste tolka på nytt. Det här inläggets egen granne i Journalen är en hygglig demonstration. Är agenter de nya användarna är MCP HTTP. har redigerats ett dussin gånger sedan juni, och cms_list_versions returnerar ett dussin ögonblicksbilder — varje ingress, varje block, varje omskriven rubrik, var och en komplett nog att återställas på egen hand.

textcms_list_versions / cms_restore_version
# vad versionslistan ger dig tillbaka
[
  {
    "id":      "cev_hgv0jxkmhppxtlbwfe663i",
    "entryId": "cme_nvjisrqctcbkn36mwmr2mm",
    "version": 11,
    "status":  "published",
    "data":    { ... hela posten som den stod ... },
    "createdAt": "..."
  },
  { "version": 10, ... },
  { "version":  9, ... }
]

# och tillbakarullningen, som själv är en versionerad skrivning
cms_restore_version(
  entryId = "cme_nvjisrqctcbkn36mwmr2mm",
  version = 9,      # tillståndet du vill ha tillbaka
  ifVersion = 11    # tillståndet du ersätter
)
# -> skapar version 12, som innehåller version 9:s data

Läs sista raden i det utdraget två gånger, för det är den del folk får fel när de bygger det här själva. Återställning spolar inte tillbaka. Den lägger till. Att rulla tillbaka till version 9 skapar version 12 som bär version 9:s data, och version 10 och 11 ligger kvar exakt där de var.

Skillnaden spelar roll första gången en tillbakarullning själv är misstaget. En återställning av tillbakaspolningstyp förstör beviset på det du försökte ångra, vilket betyder att den andra ångringen inte har något att utgå från. En kedja som bara växer betyder att en dålig återställning bara är ännu ett led i kedjan — och cms_restore_version kräver ett eget ifVersion, så till och med ångringen är bunden till det tillstånd den trodde att den rättade.

Varför kräva en versionstoken på en vanlig skrivning?

För att två agenter på en ändpunkt nu är normalfallet, inte undantaget. ifVersion är obligatoriskt — inte valfritt — på sex CMS-verktyg: cms_update_entry, cms_publish_entry, cms_unpublish_entry, cms_schedule_publish, cms_delete_entry och cms_restore_version. En anropare som inte har läst det aktuella tillståndet kan inte konstruera ett giltigt anrop.

De flesta system som levererar optimistisk samtidighet lägger versionskontrollen på godkännande-steget — ögonblicket då en människa skriver under — och lämnar vanliga redigeringar åt sist-skriver-vinner. Det är den enklare halvan. Journalen du läser redigeras av en schemalagd agent som kör varje morgon, av mig, och då och då av en andra agent som gör en kurateringsrunda, och ingen av de tre känner till de andra. Sist-skriver-vinner mellan dem är inte ett kapplöpningstillstånd du träffar då och då. Det är ett kapplöpningstillstånd på schema.

Lägg märke till vad cms_create_entry inte tar. Det finns inget ifVersion på en skapa-operation, eftersom det inte finns något tidigare tillstånd att ha fel om. Token dyker upp exakt där en anropare annars kunde köra över något, vilket är testet för om en obligatorisk parameter är disciplin eller ceremoni.

Två tokens på en skrivning

kb_publish_curation_revision (dashboard-sidan) tar både ifCandidateVersion och ifDocumentVersion. Den första binder skrivningen till den föreslagna texten som granskades; den andra till dokumentet den jämfördes mot. Om någon av dem har flyttat sig skrivs ingenting. Två tokens, för att en revision är ett påstående om två objekt — och en skrivning som bara kontrollerar det ena kan fortfarande landa ovanpå en redigering ingen läst.

En ledtråd ber klienten vara försiktig. En obligatorisk parameter ber inte.Designregeln under alltihop

Räcker MCP:s verktygsannoteringar för att göra agenters skrivningar säkra?

Nej, och protokollet säger det själv. Verktygsannoteringar kom i revisionen 2025-03-26 som fyra booleaner — readOnlyHint, destructiveHint, idempotentHint, openWorldHint — och specifikationen är tydlig med att klienter måste behandla dem som otillförlitliga om de inte kommer från en betrodd server. Varje egenskap är en ledtråd. En server kan hävda readOnlyHint: true och radera dina filer ändå.

Munin sätter dem, och de gör nytta: de är det som låter en värd avgöra om en bekräftelsedialog ska ställas framför ett anrop. Men MCP-underhållarnas eget inlägg om verktygsannoteringar från mars 2026 drar gränsen exakt, i en mening värd att citera för att den är den renaste formuleringen av problemet jag har läst: ”Hints inform decisions; contracts enforce them.” Inlägget fortsätter med att säga att när ett förslags värde hänger på att annoteringen är sann, hör det hemma i auktorisationslagret, transporten eller körtiden.

Jag skulle lägga till ett fjärde hem, och det är det billigaste av de fyra. Lägg det i indataschemat. En obligatorisk parameter upprätthålls av det som validerar begäran, vilket betyder att den håller likadant oavsett om anroparen är Claude Code, ett cron-jobb, en egen körare eller ett skript någon skrev klockan 02:00. Samma specifikation råder klienter att hålla en människa i loopen och att logga verktygsanvändning för revisionsändamål — båda formulerade som SHOULD, båda riktade till klienten. Ett SHOULD i någon annans kodbas är lika hållbart som deras senaste driftsättning.

  • VersionstokenHeltalet du senast läste, lämnat tillbaka på skrivningen. Används av cms_update_entry och kb_update_document. Billigt, eftersom versionen redan finns i objektet du just hämtade.
  • Två versionstokenskb_publish_curation_revision (dashboard-sidan) binder till den föreslagna texten och måldokumentet samtidigt. Nödvändigt när en skrivning är ett påstående om förhållandet mellan två rader snarare än om en enda.
  • InnehållsfingeravtryckEn digest av hela utkastet i stället för en räknare. outreach_approve_proposal (dashboard-sidan) kräver förslagets draftFingerprint; crm_apply_merge_proposal (dashboard-sidan) kräver dess mergeFingerprint. Rätt val när det som inte får ändras är en komposition — mottagare, ämne, brödtext, sändningstid — och ett versionsnummer skulle låta ett fält flytta sig utan att räknaren rörde sig.
  • Endast-föreslå-verbIngen token alls, eftersom verktyget inte skriver saken. crm_propose_merge lägger upp en rad; sammanslagningen själv är ett separat anrop med egen scope. Det en agent inte kan utföra kan den inte utföra mot en gammal läsning.

De fyra är en enda idé i olika kläder: skrivningen bär bevis på vad skribenten senast såg. Det tog ett tag innan vi märkte att de var samma idé, och att sätta namn på den ändrade hur vi granskar nya verktyg. Frågan på ett nytt muterande verktyg är inte längre ”vem får anropa det här” — scopes och audiences avgör den saken — utan ”vad måste anroparen ha läst först, och vad bär anropet med sig för att bevisa det?”

Kunskapsbasen kom dit före CMS:et. Dess kurateringskö binder en publicering till exakt den text en granskare läste, och disciplinen var uppenbart riktig i samma stund vi hade den i en modul, vilket är varför den spred sig till resten.

Hur vet du vilken agent som gjorde en ändring?

Genom att skilja på två frågor som ser ut som en. Versionskedjan svarar på vad posten sa vid version 9. Beslutsposterna svarar på vem som valde det, när och varför. Munin håller dem åtskilda med avsikt, eftersom en ögonblicksbild måste vara billig nog att skriva vid varje redigering, och en motivering behöver en plats att lägga prosa på.

Alltså dyker aktören upp på besluten snarare än på ögonblicksbilderna. outreach_dismiss_proposal (dashboard-sidan) behåller aktören och tidsstämpeln på förslaget. outreach_withdraw_proposal kräver en reason och registrerar vem som drog tillbaka det. outreach_revise_proposal registrerar revisionCount, lastRevisedAt, lastRevisionReason, den reviderande aktören och revisedAfterReviewAt — en flagga som finns enbart för att fånga fallet där någon redan hade öppnat utkastet för granskning innan det ändrades under dem. crm_set_contact_consent loggar en CRM-aktivitet som bär lawfulBasis och source-etiketten. conv_strip_message_signature behåller den ursprungliga brödtexten i metadata.preStripBody, så att en städrunda också går att ångra. Till och med analytics_revoke_tracker lämnar trackerraden kvar efter att nyckeln slutat fungera.

Varje anrop knyts till en aktör innan det når något av det. ping är det minst glamorösa verktyget i katalogen och det returnerar det funna org-id:t, organisationsnamnet och aktörstypen — vilket är hur du får reda på, i ett enda anrop, att kopplingen du trodde pekade på en tenant pekar på en annan. Det är verktyget jag använder oftast, och aldrig till det det byggdes för.

Vad kan en versionskedja inte göra för dig?

Tre saker. Den gör ingen diff mellan två versioner åt dig — cms_list_versions returnerar kompletta ögonblicksbilder, och jämförelsen är din att göra. Den tar slut vid radering, eftersom cms_delete_entry kaskaderar till sina egna versioner. Och den är ingen granskning: den bevisar att en skrivning landade på det tillstånd anroparen läste, inte att tillståndet var värt att behålla. Det sista är vad köerna är till för.

Ta dem i ordning, för var och en är en följd av ett val snarare än en olyckshändelse.

Den saknade diffen faller ut ur att lagra tillstånd i stället för deltan. En ögonblicksbild återställer exakt, i ett anrop, utan uppspelning, och att jämföra två av dem är ett problem du kan lösa i vilket språk du vill — medan ett lagrat delta som visar sig vara fel är en trasig kedja du inte kan återställa från alls. Ställda inför valet mellan en jämförelse du skriver själv och en historik som kanske inte laddas tog vi historiken.

Att raderingen avslutar kedjan är varför radera-verben är de hårdast bundna av alla. cms_delete_entry kaskaderar till sina versioner och kb_delete_document kaskaderar till chunkar och versioner, så båda kräver ifVersion — det mest destruktiva anropet i modulen är det som mest insisterar på att veta vad anroparen tittade på. Och de genuint oåterkalleliga operationerna i CRM och Outreach finns över huvud taget inte tillgängliga för en agent som enskilda anrop. En agent kan lägga upp ett sammanslagningsförslag. Att tillämpa det är ett annat verktyg, med en annan scope, och det behöver fingeravtrycket.

Den tredje är den värd att bygga vidare på. En versionstoken och en granskningskö löser angränsande halvor av samma problem: token gör skrivningen ärlig om vad den såg, och kön sätter en person framför de skrivningar där det inte räcker att se rätt. Billiga skrivningar tillämpas direkt och dyra bara föreslår — och versionstoken är det som gör ”tillämpa direkt” till en försvarbar kategori snarare än en axelryckning, eftersom var och en av de direkta skrivningarna fortfarande är bunden till ett tillstånd någon kan gå tillbaka till.

6
CMS-verktyg som inte kör utan versionen du senast läste
2
versionstokens på en enda publicering av en kunskapsbasrevision
12+
versioner bevarade bakom en publicerad journalpost
4
booleska ledtrådar i MCP:s ToolAnnotations-gränssnitt

Vanliga frågor

Kan man rulla tillbaka en ändring som en AI-agent gjorde i ditt CMS? Ja, om plattformen versionerar skrivningar i stället för att skriva över dem. I Munin returnerar cms_list_versions varje tidigare tillstånd hos en post med det nyaste först, och cms_restore_version sätter tillbaka ett av dem. Tillbakarullningen är själv en versionerad skrivning som kräver ett eget ifVersion, så en ångring kan inte landa på ett tillstånd du inte hade läst.

Vad är optimistisk samtidighet i ett MCP-verktyg? Anroparen lämnar tillbaka versionen den senast läste, och servern vägrar skrivningen om raden har flyttat sig sedan dess. Den är optimistisk eftersom ingenting låses — två agenter kan läsa samma post samtidigt, och den andra som skriver förlorar helt enkelt och måste läsa om. På Munins CMS-skrivverktyg är token en obligatorisk parameter, inte en valfri.

Är MCP:s verktygsannoteringar en säkerhetskontroll? Nej. readOnlyHint och destructiveHint är ledtrådar, och MCP-specifikationen kräver att klienter behandlar dem som otillförlitliga om inte servern är betrodd. De är bra för att driva bekräftelsedialoger och dåliga för garantier. Upprätthållandet hör hemma i auktorisationslagret, transporten, körtiden — eller, billigast av allt, i verktygets obligatoriska parametrar.

Hur vet du vilken agent som ändrade en post? Genom beslutsposterna snarare än ögonblicksbilderna. Munins förslagsverktyg behåller den agerande parten, tidsstämpeln och ett skäl: outreach_withdraw_proposal kräver ett skäl och registrerar vem som drog tillbaka det, outreach_revise_proposal registrerar antalet revisioner och den som reviderade, och crm_set_contact_consent loggar en aktivitet med laglig grund och källa.

Raderar en återställning av en gammal version de nyare? Nej. Återställning lägger till. Att rulla tillbaka en post till version 9 skapar version 12 som innehåller version 9:s data, och version 10 och 11 finns kvar i kedjan. Det är det som gör en felaktig tillbakarullning räddningsbar — tillståndet du återställde bort från finns fortfarande där.

Behöver du en versionstoken när du skapar en post? Nej, och Munin ber inte om en. cms_create_entry tar inget ifVersion eftersom det inte finns något tidigare tillstånd en anropare kunde ha fel om. Token hör bara hemma på anrop som ersätter, publicerar, schemalägger, återställer eller raderar något som redan finns.

Om du bygger skrivningar för en agent

  • Versionera skrivningen i stället för att skriva över raden. Ångra är en egenskap hos datan, inte hos klienten som gjorde anropet.
  • Lagra ögonblicksbilder i stället för deltan. En ögonblicksbild återställs i ett anrop utan uppspelning, och den kan inte förstöras av ett dåligt mellansteg.
  • Låt återställning lägga till snarare än spola tillbaka, så att en felaktig tillbakarullning går att rädda och kedjan aldrig tappar ett steg.
  • Lägg versionstoken i indataschemat som en obligatorisk parameter. ifVersion är obligatoriskt på sex av Munins CMS-verktyg, så en anropare som inte har läst det aktuella tillståndet kan inte konstruera ett giltigt anrop.
  • Använd ett fingeravtryck i stället för en räknare när det som inte får ändras är en komposition — mottagare, ämne, brödtext, sändningstid — snarare än en enskild rad.
  • Håll ögonblicksbilder och beslut på skilda platser. Versionskedjan registrerar vad posten sa; förslaget registrerar aktören, tidsstämpeln och skälet.
  • Behandla MCP:s verktygsannoteringar som UX, inte som upprätthållande. Specifikationen säger åt klienter att misstro dem, och varje säkerhets-SHOULD i specifikationen är riktat till kod du inte kontrollerar.

Varje verktyg som nämns här finns i MCP-referensen med sitt fullständiga indataschema, och migrationen som skapar versionstabellen ligger i repot.

En författare du inte kan se över axeln på är okej. En författare du inte kan ångra är det inte.

Kjell Rune Monsø, grundare.