8 Min. Lesezeit

So widerstehen GraphQL-Mutationen für KI-Agenten destruktiven Aufrufen

GraphQL-Mutationen für KI-Agenten brauchen typisierte Inputs, enge Scopes, echte Vorschauen und bedingte Schreibvorgänge, damit generierte Anfragen keinen Schaden anrichten.

So widerstehen GraphQL-Mutationen für KI-Agenten destruktiven Aufrufen

Generierte GraphQL-Anfragen scheitern auf vorhersehbare Weise: Das Modell hat genug Informationen, um syntaktisch gültige Anfragen zu erzeugen, aber nicht genug Widerstand, um eine gefährliche Aktion schwierig zu machen. Eine Mutation namens updateProject mit einem optionalen Flag archived wirkt für Menschen flexibel. Für einen Agenten, der eine Anfrage aus unvollständigem Kontext zusammensetzt, ist sie eine Einladung, den Projektzustand als Nebenwirkung einer gewöhnlichen Bearbeitung zu verändern.

Baue diese Widerstandsfähigkeit in Schema und Resolver ein, nicht in einen Prompt, der den Agenten zur Vorsicht auffordert. Typisierte Inputs, eng begrenzte Aktionsrechte, aussagekräftige Vorschauen, bedingte Schreibvorgänge und Fehler mit einem klaren nächsten Schritt machen es leichter, korrekte statt schädlicher Anfragen zu erzeugen. Dieses Design hilft auch Entwicklern gewöhnlicher Clients. Agenten machen lediglich die Abkürzungen sichtbar, die lockere APIs jahrelang zugelassen haben.

Eine gültige Anfrage kann trotzdem die falsche Aktion ausdrücken

GraphQL prüft, ob eine Anfrage zum Schema passt. Es stellt nicht fest, ob der Aufrufer den richtigen Kunden ausgewählt, den Zustand des Datensatzes verstanden oder tatsächlich etwas entfernen wollte. Teams verwechseln Typsicherheit häufig mit Aktionssicherheit und legen dann ein Feld namens delete, archive oder status in eine breit angelegte Update-Mutation.

Betrachte dieses verbreitete Schema:

input ProjectPatchInput {
  name: String
  description: String
  archived: Boolean
  ownerId: ID
}

type Mutation {
  updateProject(id: ID!, input: ProjectPatchInput!): Project!
}

Hier werden harmlose Änderungen mit einer Übertragung des Eigentums und einem Lebenszykluswechsel vermischt. Wenn ein Agent gebeten wird, «alte Projekte aufzuräumen», kann er nachvollziehbar schließen, dass archived: true passend ist. Wenn er einen Projektnamen korrigieren soll, kann er versehentlich ein archived-Feld aus einem früher erzeugten Objekt übernehmen. Das Typsystem akzeptiert beide Anfragen, weil beide korrekt aufgebaut sind.

Verstecke destruktives Verhalten nicht in einem flexiblen Patch. Gib jeder Aktion einen Namen, der ihre Folge benennt, und ein Input, das nur die dafür nötigen Nachweise enthält:

type Mutation {
  renameProject(input: RenameProjectInput!): RenameProjectPayload!
  archiveProject(input: ArchiveProjectInput!): ArchiveProjectPayload!
  transferProjectOwnership(input: TransferProjectOwnershipInput!): TransferProjectOwnershipPayload!
}

input RenameProjectInput {
  projectId: ID!
  expectedVersion: Int!
  name: String!
}

input ArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
  confirmation: String!
  idempotencyKey: String!
}

Das ist kein Selbstzweck. Eine eng gefasste Mutation begrenzt, was eine generierte Anfrage aussagen kann. Sie schafft einen klaren Unterschied zwischen «eine Bezeichnung ändern» und «dieses Projekt aus der normalen Nutzung entfernen». Toolbeschreibungen können den Unterschied erklären, aber das Schema sollte ihn erzwingen.

Die GraphQL-Spezifikation hilft dabei auf begrenzte, aber nützliche Weise. Die Validierung von Input-Objekten lehnt Felder ab, die das Schema nicht definiert. Wenn ArchiveProjectInput kein ownerId enthält, kann ein Client keine Änderung des Eigentümers in einem Archivierungsaufruf verstecken. Betrachte diese Eigenschaft als Schutzvorrichtung, nicht als Sicherheitsgrenze. Der Resolver entscheidet weiterhin, ob der Akteur dieses konkrete Projekt archivieren darf.

Vermeide ein allgemeines Feld wie action: String!, etwa bei mutateProject(action: "ARCHIVE"). Es wirkt kompakt, bis jede Aktion andere Felder, Validierungen, Berechtigungen, Vorschauinformationen und Fehlerbehandlungen braucht. Dann entsteht ein privates RPC-Protokoll innerhalb eines Input-Objekts, mit weniger Unterstützung durch die GraphQL-Werkzeuge.

Inputs müssen Ziel und Grenze benennen

Ein destruktives Input sollte genau angeben, was geändert wird, welche Version der Aufrufer gesehen hat und welche Grenze eine wachsende Auswahl verhindert. IDs allein enthalten nicht genug Absicht, wenn ein Resolver auf untergeordnete Datensätze, externe Systeme oder eine Abfrage über den gesamten Mandanten ausweiten kann.

Beginne mit einem Objekt, das eine einzelne Ressource im Mandanten des Aufrufers identifiziert. Akzeptiere in einer Löschmutation keinen beliebigen Filter, sofern das Produkt keine Sammelaktion wirklich benötigt. Ein Filter wie where: { status: INACTIVE } lässt zu viel offen: Inaktiv nach welchem Zeitstempel, in welchem Mandanten und mit welchem versteckten Standardwert? Ein Modell kann ihn liefern, weil das Feld existiert, nicht weil es die resultierende Menge geprüft hat.

Bei Operationen für einen Datensatz sollte das Input ein Versions-Token enthalten. Eine Ganzzahl lässt sich leicht prüfen, aber auch eine undurchsichtige Revisionszeichenfolge funktioniert. Der Resolver vergleicht sie in derselben Transaktion, die die Änderung schreibt, mit der gespeicherten Version. Wenn sie nicht übereinstimmen, gibt er einen Konflikt zurück und ändert nichts.

input ArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
  confirmation: String!
  idempotencyKey: String!
}

enum ArchiveReason {
  CUSTOMER_REQUEST
  DUPLICATE
  END_OF_LIFE
}

Das Enum reason verbessert nicht nur die Berichte. Es verhindert, dass eine Anfrage eine frei erfundene Begründung einführt, die spätere Automatisierung als relevant behandelt. Verwende Freitext für eine Notiz, wenn Menschen ihn brauchen, aber halte operative Kategorien als Enum-Werte fest.

Ein Bestätigungsfeld sollte an das tatsächliche Ziel gebunden sein. Die wörtliche Zeichenfolge ARCHIVE erkennt nur nachlässige Konstruktionen. archive acme-project-42 zwingt den Client, eine Ressourcen-ID aufzulösen und erneut anzugeben. Das hält keinen böswilligen Client auf und ersetzt niemals die Autorisierung. Es fängt jedoch viele generierte Anfragen ab, die die richtige Aktion an die falsche ID gebunden haben.

Fordere bei Routine-Mutationen wie der Änderung eines Anzeigenamens keine Bestätigung. Zu viele Bestätigungen bringen Agenten und Menschen dazu, jedes Feld mechanisch auszufüllen. Hebe sie für Aktionen mit erheblichen oder schwer umkehrbaren Folgen auf: Löschung, Veröffentlichung, finanzielle Bewegungen, Widerruf von Zugangsdaten und Änderungen, die andere Nutzer betreffen.

Bei einer Sammelaktion müssen Obergrenze und Vorschau-Token für die exakte Auswahl ausdrücklich festgelegt sein. Dieses Input sagt deutlich mehr als ein roher Filter:

input DeleteDormantProjectsInput {
  previewToken: ID!
  expectedCount: Int!
  confirmation: String!
  idempotencyKey: String!
}

Der Ausführungsresolver muss ein Token ablehnen, das abgelaufen ist, zu einem anderen Akteur gehört, einen anderen Mandanten beschreibt oder eine andere Anzahl als expectedCount liefert. Sonst kann ein Agent fünf Datensätze anzeigen und anschließend eine sich verändernde Abfrage ausführen, die nun fünfzig Treffer liefert.

Berechtigungen sollten der Mutation folgen, nicht dem Substantiv

Ein Scope wie projects:write ist für autonome Aktionen meist zu weit gefasst. Er erlaubt einem Aufrufer, Projekte umzubenennen, zu archivieren, zu übertragen, zu löschen und möglicherweise abrechnungsbezogene Einstellungen zu ändern, weil alle Aktionen ein Projekt betreffen. Diese Gruppierung folgt dem Datenbankobjekt, nicht dem Risiko der Operation.

Vergib Berechtigungen, die eine Aktion beschreiben. Ein Servicetoken für die Release-Automatisierung könnte etwa project:rename und project:archive besitzen, während ein Support-Workflow keines von beiden erhält. Ein separater Scope project:delete sollte selten sein. Wenn dein Identitätssystem keine ausreichend engen Scopes ausstellen kann, ergänze serverseitig eine Capability-Prüfung, die mit dem Mutationsnamen verbunden ist, und protokolliere sie in der Autorisierungsentscheidung.

Ein Scope allein entscheidet nie über den Zugriff. Jeder Resolver braucht mehrere Prüfungen in einer bewusst festgelegten Reihenfolge:

  1. Authentifiziere den Aufrufer und ermittle Mandant und Principal.
  2. Prüfe, ob der Principal für diese Mutation berechtigt ist.
  3. Lade das Ziel innerhalb der Mandantengrenze, statt es global zu laden und später zu prüfen.
  4. Prüfe den Zustand des Datensatzes und jede für die Geschäftsregel nötige Rollenbeziehung.
  5. Führe das bedingte Schreiben aus und hänge dasselbe Auditereignis in derselben Transaktion an.

Das Laden innerhalb der Mandantengrenze ist wichtig. Ein Resolver, der findProjectById(id) aufruft, bevor er die Mandantenzugehörigkeit prüft, kann die Existenz eines Datensatzes über Timing oder Fehlermeldungen verraten. Außerdem kann er ein global geladenes Objekt an einen Helfer übergeben, der davon ausgeht, dass die Autorisierung bereits erfolgt ist. Mache die Mandantenzugehörigkeit zu einem Teil der Suchbedingung.

Leite Berechtigungen nicht aus der behaupteten Aufgabe des Agenten ab. Ein Request-Header wie X-Agent-Goal: cleanup ist ein Hinweis für das Auditprotokoll, keine Berechtigung. Prompts, Aufgabenbezeichnungen und die Modellidentität können einem Menschen bei der Prüfung helfen, aber jeder Client kann sie fälschen.

Dasselbe gilt für den Zugriff auf Tools. Ein Agent darf möglicherweise einen GraphQL-Endpunkt aufrufen, aber nicht jede einzelne Mutation. Beschreibe Lese- und Aktionswerkzeuge getrennt, wenn die Laufzeit des Agenten das erlaubt. Die endgültige Entscheidung muss in der API bleiben, weil ein Client Metadaten zum Tool umgehen und die HTTP-Anfrage direkt senden kann.

Ein Dry Run muss denselben Plan wie die Ausführung erstellen

Eine Vorschau ist nur dann nützlich, wenn sie beantwortet: «Was würde genau diese Anfrage jetzt tun?» Eine künstliche Vorschau, die Datensätze mit einer vereinfachten Abfrage zählt, vermittelt Agenten ein falsches Sicherheitsgefühl. Die spätere Mutation kann andere Zulassungsregeln verwenden, einen anderen Autorisierungspfad nehmen oder eine externe Aktion auslösen, die in der Vorschau nie berücksichtigt wurde.

Erstelle eine gemeinsame Planungsfunktion. Sie erhält den authentifizierten Akteur und das Input, validiert jede Bedingung, löst Ziele auf, berechnet Nebenwirkungen und erzeugt einen unveränderlichen Plan. Die Vorschau gibt eine bereinigte Darstellung dieses Plans zurück. Der Ausführungspfad verwendet den Plan erst, nachdem der Aufrufer das kurzlebige Token und die Bestätigung vorgelegt hat.

type Mutation {
  previewArchiveProject(input: PreviewArchiveProjectInput!): ArchivePreviewPayload!
  archiveProject(input: ArchiveProjectInput!): ArchiveProjectPayload!
}

input PreviewArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
}

type ArchivePreviewPayload {
  previewToken: ID!
  project: Project!
  affectedMemberCount: Int!
  plannedEffects: [ArchiveEffect!]!
  expiresAt: DateTime!
}

enum ArchiveEffect {
  PROJECT_HIDDEN_FROM_DEFAULT_LISTS
  PENDING_INVITATIONS_CANCELLED
}

Ein Plan sollte Ziel-IDs, ihre Versionen, die Identität des Akteurs, den Mandanten, den Input-Digest, geplante Auswirkungen und den Ablaufzeitpunkt enthalten. Speichere ihn serverseitig oder signiere ein undurchsichtiges Token, das auf den gespeicherten Zustand verweist. Lege den vollständigen Plan nicht in ein vom Client kontrolliertes JSON-Objekt und vertraue ihm bei der Ausführung.

Das Ausführungs-Input sollte auf das Vorschau-Token verweisen und keinen losen Selektor wiederholen:

input ArchiveProjectInput {
  previewToken: ID!
  confirmation: String!
  idempotencyKey: String!
}

Dieser Ablauf mit zwei Aufrufen erzeugt Reibung. Genau das ist bei folgenreichen Aktionen beabsichtigt. Erzwinge ihn nicht bei jeder Mutation. Eine einfache Regel genügt: Fordere eine Vorschau, wenn eine Operation mehr als einen Datensatz betrifft, eine irreversible externe Wirkung hat oder eine Ressource für andere Nutzer unzugänglich macht.

Auch Vorschauantworten brauchen Zugriffskontrollen. Eine Liste betroffener Datensätze kann genauso Daten preisgeben wie die Ausführung der Mutation. Wende bei der Planung dieselben Mandanten- und Rollenregeln an. Eine Vorschau kann Felder auslassen, die der Akteur nicht lesen darf, und trotzdem die nötige Anzahl und Wirkungskategorien liefern.

Idempotenz und Versionen lösen unterschiedliche Fehler

SSH-Schlüssel im Tresor aufbewahren
Leite SSH-Befehle über Sallyports zustandslosen Helfer sp-ssh, statt dem Agenten Schlüssel zu geben.

Idempotenz verhindert die doppelte Anwendung derselben Anfrage. Eine Versionsprüfung verhindert die Anwendung auf einen Zustand, der sich geändert hat, nachdem der Aufrufer ihn geprüft hat. Teams ergänzen häufig eines von beidem und glauben dann, beide Probleme gelöst zu haben.

Ein Agent kann einen erneuten Versuch starten, weil die HTTP-Verbindung geschlossen wurde, nachdem der Server die Mutation bereits festgeschrieben hatte. Ohne Idempotenz kann die zweite Anfrage eine zweite Rückerstattung erzeugen, eine Nachricht duplizieren oder dieselbe externe API zweimal aufrufen. Gib jeder Mutation mit Wirkung einen vom Aufrufer gelieferten idempotencyKey. Der Server sollte ihn zusammen mit dem authentifizierten Akteur, dem Mutationsnamen, einem Digest des normalisierten Inputs und der vollständigen Antwort oder einem stabilen Fehler speichern.

Wenn der Server denselben Akteur, dieselbe Mutation, denselben Schlüssel und denselben Input-Digest erneut sieht, gibt er das ursprüngliche Ergebnis zurück. Erkennt er denselben Schlüssel mit einem anderen Digest, gibt er IDEMPOTENCY_KEY_REUSED zurück und tut nichts. Eine geänderte Eingabe unter einem bereits verwendeten Schlüssel zerstört die Eigenschaft, auf die Clients bei Wiederholungen angewiesen sind.

Eine Versionsprüfung behandelt einen anderen Ablauf. Ein Agent liest Projektversion 7, erstellt eine Archivvorschau, und ein Mensch benennt das Projekt um oder stellt eine Einladung wieder her. Bei der Ausführung vergleicht der Resolver Version 7 mit der aktuell gespeicherten Version. Ist der Wert inzwischen 8, gibt er einen Konflikt zurück. Der Agent muss den aktuellen Zustand erneut abrufen, seine Absicht überdenken und bei Bedarf eine neue Vorschau erstellen.

Die GraphQL-Spezifikation führt Top-Level-Felder innerhalb einer Mutation-Operation seriell aus. Dadurch werden getrennte HTTP-Anfragen nicht serialisiert. Zwei Agenten können weiterhin nahezu gleichzeitig zwei Mutation-Operationen senden. Verwende ein bedingtes Datenbank-Update, eine Zeilensperre oder eine transaktionale Einschränkung. Eine Prüfung im Anwendungsspeicher und ein getrenntes Schreiben lassen eine Race Condition offen.

UPDATE projects
SET archived_at = CURRENT_TIMESTAMP,
    version = version + 1
WHERE id = :project_id
  AND tenant_id = :tenant_id
  AND version = :expected_version
  AND archived_at IS NULL;

Wenn dies null Zeilen verändert, prüfe den aktuellen Datensatz innerhalb der Mandantengrenze und gib ein konkretes Ergebnis zurück: nicht vorhanden, verboten, bereits archiviert oder Versionskonflikt. Melde nicht jedes Ergebnis mit null Zeilen als allgemeinen Serverfehler. Agenten müssen wissen, ob ein erneuter Versuch schädlich, sinnvoll oder nutzlos ist.

Fehlerantworten sollten einem Agenten den nächsten Schritt zeigen

GraphQLs oberstes errors-Array eignet sich für Parserfehler, Validierungsfehler und Resolverfehler. Es ist kein guter Ort, um Clients zum Auslesen englischer Nachrichten für geschäftliche Ergebnisse zu zwingen. Erwartete Mutationsergebnisse gehören in ein typisiertes Payload mit stabilem Code und strukturierten Details.

type ArchiveProjectPayload {
  outcome: ArchiveProjectOutcome!
  project: Project
  error: MutationError
}

enum ArchiveProjectOutcome {
  ARCHIVED
  VERSION_CONFLICT
  CONFIRMATION_REQUIRED
  PREVIEW_EXPIRED
  FORBIDDEN
  IDEMPOTENCY_KEY_REUSED
}

type MutationError {
  code: String!
  message: String!
  currentVersion: Int
  requiredConfirmation: String
}

Verwende Transport- und GraphQL-Ausführungsfehler für Bedingungen, unter denen der Client die Operation nicht korrekt ausführen konnte. Verwende ein typisiertes Ergebnis für eine Anfrage, die normal ausgeführt wurde, aber wegen einer Geschäftsregel keinen Zustand geändert hat. Entscheide dich für eine Konvention und dokumentiere sie. Wenn manche Konflikte über errors.extensions.code und andere über Payload-Enums gemeldet werden, wird das Verhalten von Agenten brüchig.

Ein Agent sollte ein Ergebnis einer sicheren Aktion zuordnen können. VERSION_CONFLICT bedeutet: Objekt erneut lesen und die Absicht überdenken. PREVIEW_EXPIRED bedeutet: neue Vorschau erstellen. CONFIRMATION_REQUIRED bedeutet: den erforderlichen Ausdruck einem Menschen zeigen oder danach fragen, nicht raten. FORBIDDEN bedeutet: stoppen. IDEMPOTENCY_KEY_REUSED bedeutet: erst dann einen neuen Schlüssel erzeugen, wenn der Aufrufer ausdrücklich eine andere Operation beabsichtigt.

Gib keine internen Richtliniennamen, SQL-Fragmente oder Details des Autorisierungsgraphen zurück. Stabile externe Codes können präzise sein, ohne interne Implementierungsdetails offenzulegen. Führe eine Korrelations-ID in den Response-Erweiterungen und einen passenden Auditdatensatz auf dem Server. So hat ein Betreiber etwas Konkretes, das er untersuchen kann, wenn ein Agent einen Fehler meldet.

Erfolgs-Payloads müssen genug Informationen liefern, um Unsicherheit zu beenden. Gib den resultierenden Datensatz, die neue Version, die Operations-ID und die tatsächlich eingetretenen Auswirkungen zurück. Ein bloßer boolescher Wert zwingt den Client zu einer weiteren Abfrage und lässt Raum für eine veraltete Lesung. Auch die menschliche Prüfung wird dadurch unnötig erschwert.

Löschen braucht einen Lebenszyklus, keinen booleschen Schalter

Eine riskante Agentensitzung widerrufen
Sitzungen protokollieren Agentenläufe getrennt, sodass sich ein fragwürdiger Lauf sofort widerrufen lässt.

Endgültiges Löschen ist beliebt, weil eine Tabelle danach ordentlich aussieht. Es ist aber auch die Aktion, die bei einem missverstandenen Auftrag eines Agenten am ehesten ein irreparables Supportproblem erzeugt. Viele Produkte sollten zunächst archivieren, eine serverseitig kontrollierte Rückgängig-Frist anbieten und das endgültige Entfernen über einen getrennten, stärker eingeschränkten Workflow abwickeln.

Nenne eine Archivierungsoperation nicht deleteProject, wenn sie einen Datensatz nur ausblendet. Namen vermitteln Clients, welchen Zustand sie erwarten können. archiveProject sollte ARCHIVED zurückgeben; purgeProject sollte bedeuten, dass die Daten nicht mehr verfügbar sein werden. Wenn eine API für jede Lebenszyklusphase delete verwendet, kann ein Agent eine umkehrbare Bereinigung nicht zuverlässig von einer endgültigen Entfernung unterscheiden.

Ein endgültiges Entfernen braucht strengere Eingaben und Berechtigungen als eine Archivierung. Es kann voraussetzen, dass die Ressource bereits eine Aufbewahrungsfrist lang archiviert war, keine rechtliche oder abrechnungsbezogene Sperre besteht und ein Betreiber mit einem eigenen Scope zustimmt. Der Resolver muss jede Bedingung durchsetzen. Ein clientseitiger Countdown oder eine Toolanweisung hat keine Autorität.

Externe Nebenwirkungen verdienen dieselbe Behandlung. Wenn das Archivieren Einladungen storniert, eine entfernte Umgebung löscht oder einen Webhook auslöst, gib diese Auswirkungen in Vorschau und endgültigem Payload zurück. Hänge sie nicht still an einen allgemeinen Update-Resolver. Wer eine Agentenanfrage prüft, muss die Folgen vor der Genehmigung sehen, und der Agent braucht nach der Ausführung Fakten, die er melden kann.

Bei Finanz- oder Zugangsdatenaktionen solltest du keinen vorgetäuschten Dry Run anbieten, der den Live-Endpunkt des Anbieters aufruft und auf Wirkungslosigkeit hofft. Verwende, sofern vorhanden, die dokumentierte Vorschau- oder Autorisierungsfunktion des Anbieters. Andernfalls kennzeichne das Ergebnis als lokale Schätzung und liste auf, was der Server nicht prüfen konnte. Vorgetäuschte Gewissheit ist schlechter, als eine menschliche Entscheidung zu verlangen.

Resolver-Prüfungen machen Versprechen des Schemas real

Das Schema begrenzt fehlerhafte Absichten. Das Resolver-Design verhindert, dass eine scheinbar autorisierte Anfrage eine echte Grenze überschreitet. Halte diese Ebenen im Code getrennt, damit ein späterer Refactor keine Berechtigungsprüfung durch einen Kommentar in einer Tooldefinition ersetzt.

Ein Resolver für eine destruktive Aktion sollte eine Reihenfolge befolgen, die Ablehnungen früh und Schreibvorgänge spät ausführt. Authentifiziere zuerst die Anfrage, ermittle dann den Mandanten des Akteurs, validiere die Eingabe, lade das Ziel innerhalb dieses Mandanten, prüfe Scope und Zustand, validiere Vorschau-Token und Bestätigung, reserviere den Idempotenzeintrag und führe die bedingte Transaktion aus. Die Reihenfolge kann je nach Speichermodell abweichen, aber führe keine externe Wirkung aus, bevor feststeht, dass die Transaktion festgeschrieben werden kann.

Ein Idempotenzeintrag braucht besondere Sorgfalt, wenn eine Aktion Datenbank und externen Anbieter umfasst. Wird ein Schlüssel vor dem externen Aufruf als abgeschlossen markiert, kann ein Fehlschlag fälschlich als Erfolg gelten. Wird der Anbieter zuerst aufgerufen, droht eine Duplikation, wenn der Prozess vor dem Speichern des Abschlusses abstürzt. Verwende, sofern möglich, ein Outbox-Muster oder die Idempotenzunterstützung des Anbieters. Speichere eine dauerhafte ausstehende Operation, schreibe die lokale Entscheidung fest und sende die externe Wirkung mit einer Operations-ID, die Wiederholungen überlebt.

Auditdatensätze sollten den authentifizierten Principal, gegebenenfalls den Agentenlauf, Mutationsnamen, normalisiertes Ziel, Input-Digest, Autorisierungsergebnis, Vorschauverweis, Idempotenzschlüssel, Ergebnis und neue Version enthalten. Notizen und Felder mit sensiblen Inhalten müssen nach deinen Aufbewahrungsregeln redigiert werden. Ein Auditereignis, das nur «Mutation erfolgreich» sagt, ist bei einem Vorfall beinahe wertlos.

Bei Agenten, die über authentifizierte HTTP- oder SSH-Aufrufe handeln, solltest du die Zugangsdaten möglichst außerhalb des Modellprozesses aufbewahren. Sallyport leitet unterstützte Aktionen über seinen lokalen Tresor und protokolliert einzelne Aufrufe. Das ist hilfreich, wenn eine GraphQL-Mutation eine für Menschen sichtbare Autorisierung braucht, die über ein Bearer-Token hinausgeht.

Teste die generierte Anfrage, nicht nur den Resolver

Ein Gateway vor Aktionen setzen
Mit dem enthaltenen sp mcp-Shim stellst du Sallyport zwischen einen MCP-fähigen Agenten und externe Aktionen.

Unit-Tests, die einen Resolver mit sorgfältig erstellten Objekten aufrufen, übersehen genau den Fehlerfall, der dich interessiert. Generierte Clients senden ausgelassene Felder, Nullwerte, veraltete IDs, Aliase, wiederholte Anfragen und Variablen, die aus früheren Toolausgaben zusammengesetzt wurden. Teste die öffentliche GraphQL-Grenze mit denselben Formen.

Erstelle eine Mutation-Testmatrix nach Verhalten und nicht nach Codezweigen. Decke mindestens einen Aufrufer aus einem anderen Mandanten, einen Aufrufer mit Leserechten ohne Aktionsscope, eine abgelaufene Vorschau, eine geänderte Zielversion, einen falschen Bestätigungstext, einen wiederverwendeten Idempotenzschlüssel und zwei parallele Aufrufe mit derselben erwarteten Version ab. Prüfe sowohl die Antwort als auch den dauerhaften Zustand nach jedem Test.

Diese Anfrage sollte bei der GraphQL-Validierung scheitern, weil das Input kein ownerId definiert:

mutation BadArchive($input: ArchiveProjectInput!) {
  archiveProject(input: $input) {
    outcome
  }
}
{
  "input": {
    "projectId": "prj_42",
    "expectedVersion": 7,
    "reason": "DUPLICATE",
    "confirmation": "archive prj_42",
    "idempotencyKey": "run-18-archive-42",
    "ownerId": "usr_9"
  }
}

Die erwartete Antwort gehört in das oberste GraphQL-Fehlerformat, weil das Dokument ein ungültiges Input-Objekt geliefert hat. Dieser Test zeigt, dass das Schema eine fremde Fähigkeit aus der Mutation heraushält. Ein separater Test muss nachweisen, dass eine korrekt aufgebaute Archivierungsanfrage trotzdem scheitert, wenn der Akteur zu einem anderen Mandanten gehört.

Führe Nebenläufigkeitstests gegen das echte Transaktionsverhalten aus, nicht gegen eine In-Memory-Fälschung. Sende zwei Archivierungsanfragen mit derselben ID und erwarteten Version und prüfe, dass eine ARCHIVED zurückgibt und die andere einen Konflikt oder ein idempotentes Wiederholungsergebnis. Wenn beide Aufrufe mit unterschiedlichen Operations-IDs Erfolg melden, erfüllt dein bedingtes Schreiben seine Aufgabe nicht.

Teste auch die Auditprüfung. Wenn dein Aktionsgateway ein manipulationssicheres, verschlüsseltes Auditprotokoll erzeugt, muss die Prüfung Teil von Vorfallübungen sein und darf kein Befehl bleiben, den niemand verwendet hat. Sallyport stellt sp audit verify für die Offlineprüfung seiner Hashkette ohne Tresorschlüssel bereit. Führe es gegen ein kopiertes Journal aus und stelle sicher, dass die Betreiber die Bedeutung einer fehlgeschlagenen Prüfung kennen.

Generierte Tools brauchen weniger Auswahl, keine längeren Warnungen

Agenten arbeiten besser, wenn ein Tool-Schema die kleinste sichere Aktion anbietet, die zur Aufgabe passt. Ein riesiger Mutationskatalog mit allgemeinen Filtern, Flags und optionalen Nebenwirkungen zwingt das Modell, Richtlinien aus Feldnamen abzuleiten. Ein kompakter Katalog aus klaren Lese-, Vorschau-, Ausführungs- und Wiederherstellungsoperationen gibt ihm einen Weg, dem es folgen kann.

Stelle Leseoperationen bereit, die IDs, Versionen, Status und Namen zurückgeben, die ein Agent benötigt, bevor er eine Mutation vorschlägt. Wenn der Agent eine ID aus einer menschlichen Bezeichnung erraten muss, kann das Mutationsdesign dich nicht retten. Gib stabile IDs deutlich zurück und mache mehrdeutige Suchergebnisse sichtbar, statt stillschweigend eines auszuwählen.

Toolbeschreibungen sollten eine Voraussetzung und eine Folge nennen, aber der Server bleibt der Durchsetzungspunkt. Zum Beispiel: «Archiviert ein Projekt nach erfolgreicher Vorschau. Storniert die in der Vorschau aufgeführten ausstehenden Einladungen.» Das ist besser als «Mit Vorsicht verwenden», denn diese Formulierung sagt einem Agenten operativ nichts.

Löse nicht jedes Risiko mit einem Dialog zur menschlichen Genehmigung. Eine Genehmigung ist richtig, wenn ein Mensch die Entscheidung verantwortet, aber wiederholte Abfragen werden zu Hintergrundrauschen. Verankere den routinemäßigen Schutz in Scopes, Mandantenprüfungen, Versionen und Idempotenz. Bitte einen Menschen nur bei den wenigen Aktionen um Prüfung, deren Absicht sich nicht aus den Daten ableiten lässt, deren Folge dauerhaft ist oder die eine Organisationsgrenze überschreiten.

Beginne mit der Mutation, die den größten Schaden anrichten würde, wenn ein Agent sie zweimal, mit veraltetem Zustand oder für den falschen Mandanten aufruft. Teile ihr Input auf, ergänze eine echte Vorschau, wenn die Aktion sie verdient, mache das Schreiben bedingt und schreibe den Wiederholungstest. Diese Arbeit zeigt, ob deine API eine Aktion klar modelliert oder lediglich Datenbankfelder offenlegt.

FAQ

Macht die GraphQL-Validierung destruktive Mutationen sicher?

Nein. GraphQL prüft die Form der Anfrage, Feldargumente und die Konvertierung von Skalaren. Es weiß jedoch nicht, ob eine Aktion im aktuellen Geschäftszustand sinnvoll ist. Resolver müssen weiterhin Berechtigungen, Zustandsübergänge, Mandantengrenzen und Regeln für nebenläufige Änderungen durchsetzen.

Wann sollte ich ein eigenes GraphQL-Mutations-Input anlegen?

Verwende ein mutationsspezifisches Input-Objekt, wenn eine Aktion den Zustand auf eine bestimmte Weise verändert, besonders bei Löschung, Sperrung, Rückerstattung, Übertragung oder Veröffentlichung. Ein allgemeines Patch-Input eignet sich für gewöhnliche bearbeitbare Felder, sollte aber keine Lebenszyklusaktionen enthalten, die als boolesche Werte getarnt sind.

Was sollte ein GraphQL-Dry-Run tatsächlich tun?

Ein Dry Run sollte dieselbe Validierung, Autorisierung, Zielauswahl und Planung wie die echte Mutation ausführen und erst vor dem externen Schreiben oder dem Datenbank-Commit stoppen. Wenn er einen anderen Codepfad nimmt, lernen Agenten eine Vorschau kennen, die nicht zum Verhalten in der Produktion passt.

Wie funktionieren Idempotenzschlüssel bei GraphQL-Mutationen?

Ein Idempotenzschlüssel ermöglicht es dem Server, einen erneuten Versuch derselben beabsichtigten Aktion zu erkennen und das ursprüngliche Ergebnis zurückzugeben, statt die Aktion zweimal auszuführen. Speichere den Schlüssel zusammen mit Akteur, Mutationsnamen, normalisiertem Input-Digest, Ergebnis und Ablaufregel. Eine Wiederverwendung mit verändertem Input muss abgelehnt werden.

Sollten GraphQL-Scopes Ressourcen oder Mutationen abbilden?

Ein mandantenweiter Scope wie write:projects gibt einem Agenten meist zu viele Rechte, wenn er nur ein Projekt archivieren soll. Begrenze destruktive Fähigkeiten auf die Mutation oder Aktion und setze Eigentums- und Mandantenprüfungen im Resolver als separate Entscheidung durch.

Wie kann ein KI-Agent viele Datensätze sicher mit GraphQL löschen?

Behandle das Löschen vieler Datensätze als eigene Operation mit Zielanzahl, begrenzter Auswahlregel, ausdrücklichem Bestätigungstext und Vorschau. Verwandle ein Löschen einzelner Objekte niemals in eine unbeabsichtigte Schleife, nur weil ein Agent eine lange ID-Liste erzeugen kann.

Welche Fehler sollten destruktive GraphQL-Mutationen an einen Agenten zurückgeben?

Gib maschinenlesbare Codes, eine stabile verständliche Meldung und strukturierte Details wie die aktuelle Version oder den erforderlichen Bestätigungstext zurück. Ein Agent sollte keine Prosa analysieren müssen, um zu entscheiden, ob er den Zustand aktualisieren, erneut versuchen oder einen Menschen fragen darf.

Können zwei GraphQL-Mutationsanfragen miteinander konkurrieren?

GraphQL serialisiert Top-Level-Felder innerhalb einer Mutation-Operation, aber getrennte Anfragen können weiterhin gleichzeitig eintreffen. Nimm die erwartete Version in das Input auf, führe das Schreiben bedingt aus und gib einen Konflikt zurück, wenn sich die gespeicherte Version geändert hat.

Kann ein Agenten-Prompt die Sicherheit von GraphQL-Mutationen erzwingen?

Nein. Das Schema ist eine öffentliche Schnittstelle, und Clients können die API direkt aufrufen und jeden Agenten-Prompt oder jede Toolbeschreibung umgehen. Der Server muss Identität, Scope, Mandantenbeziehung, Zustand und Bestätigung prüfen, bevor er etwas ändert.

Wie halte ich Agentenzugangsdaten aus generierten API-Anfragen heraus?

Bewahre langlebige API- und SSH-Zugangsdaten außerhalb des Agentenprozesses auf, genehmige den Agentenlauf bei Bedarf und protokolliere jeden ausgehenden Aufruf. Sallyport ist eine macOS-Option, um unterstützte HTTP- und SSH-Aktionen über einen lokalen verschlüsselten Tresor zu leiten, statt Geheimnisse an den Agenten zu übergeben.

Sallyport

Sallyport führt API-Aufrufe und SSH-Befehle für Ihren KI-Agenten aus. Die Schlüssel bleiben in einem lokalen Tresor auf Ihrem Mac; Sie geben jeden Lauf frei, und jede Aktion landet in einem versiegelten Journal.

© 2026 Sallyport · Open Source unter Apache-2.0 · Oleg Sotnikov