8 min de lecture

Comment les mutations GraphQL pour agents IA résistent aux appels destructeurs

Les mutations GraphQL destinées aux agents IA ont besoin d'entrées typées, de portées limitées, de vrais aperçus et d'écritures conditionnelles pour éviter les requêtes générées destructrices.

Comment les mutations GraphQL pour agents IA résistent aux appels destructeurs

Les requêtes GraphQL générées échouent de manière prévisible : le modèle dispose de suffisamment d'informations pour produire une syntaxe valide, mais pas de suffisamment de garde-fous pour rendre une action dangereuse difficile. Une mutation nommée updateProject avec un indicateur archived facultatif semble flexible pour un humain. Pour un agent qui assemble une requête à partir d'un contexte partiel, c'est une invitation à modifier l'état du projet comme effet secondaire d'une simple retouche.

Placez les garde-fous dans le schéma et le resolver, pas dans une invite demandant à l'agent d'être prudent. Des entrées typées, une autorité d'action limitée, des aperçus utiles, des écritures conditionnelles et des erreurs proposant une voie de récupération facilitent la génération de requêtes correctes plutôt que destructrices. Cette conception aide aussi les développeurs de clients classiques. Les agents ne font que révéler les raccourcis que les API trop permissives tolèrent depuis des années.

Une requête valide peut quand même exprimer la mauvaise action

GraphQL vérifie qu'une requête correspond au schéma. Il ne garantit pas que l'appelant a sélectionné le bon client, compris l'état de l'enregistrement ou voulu supprimer quoi que ce soit. Les équipes confondent souvent sécurité des types et sécurité des actions, puis placent un champ nommé delete, archive ou status dans une large mutation de mise à jour.

Prenons ce schéma courant :

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

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

Il mélange des modifications inoffensives, un transfert de propriété et une transition de cycle de vie. Un agent auquel on demande de « nettoyer les anciens projets » peut raisonnablement déduire que archived: true convient. Un agent chargé de corriger le nom d'un projet peut conserver par accident un champ archived provenant d'un objet généré précédemment. Le système de types accepte les deux requêtes, car elles sont toutes deux bien formées.

Ne dissimulez pas un comportement destructeur dans un patch flexible. Donnez à chaque action un nom qui indique sa conséquence et une entrée qui ne contient que les éléments nécessaires à cette conséquence :

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!
}

Ce n'est pas du cérémonial gratuit. Une mutation limitée réduit ce qu'une requête générée peut exprimer. Elle distingue clairement « modifier une étiquette » de « retirer cet élément de l'usage normal ». Les descriptions des outils peuvent expliquer cette différence, mais le schéma doit l'imposer.

La spécification GraphQL aide ici de façon limitée, mais utile. La validation des objets d'entrée refuse les champs que le schéma ne définit pas. Si ArchiveProjectInput ne contient pas ownerId, un client ne peut pas dissimuler un changement de propriétaire dans un appel d'archivage. Considérez cette propriété comme un garde-fou, pas comme une frontière de sécurité. Le resolver doit encore décider si l'acteur peut archiver ce projet précis.

Évitez un champ générique action: String! tel que mutateProject(action: "ARCHIVE"). Il paraît compact jusqu'à ce que chaque action nécessite des champs, des validations, des autorisations, des données d'aperçu et une gestion d'erreurs différentes. Le résultat devient un protocole RPC privé enfermé dans un objet d'entrée, avec moins d'aide de la part des outils GraphQL.

Les entrées doivent nommer la cible et la limite

Une entrée destructive doit indiquer exactement ce qui va changer, la version examinée par l'appelant et la limite qui empêche une sélection de s'étendre. Les identifiants seuls ne portent pas assez d'intention lorsqu'un resolver peut se propager aux enregistrements enfants, aux systèmes externes ou à une requête couvrant tout un tenant.

Commencez par un objet qui identifie une seule ressource dans le tenant de l'appelant. N'acceptez pas de filtre arbitraire dans une mutation de suppression, sauf si le produit a réellement besoin d'opérations en masse. Un filtre comme where: { status: INACTIVE } crée de l'ambiguïté : inactif selon quelle date, quel tenant et quelle valeur par défaut cachée ? Un modèle peut le fournir parce que le champ existe, pas parce qu'il a examiné l'ensemble obtenu.

Pour les opérations portant sur un seul enregistrement, transmettez un jeton de version dans l'entrée. Un entier est facile à examiner, mais une chaîne de révision opaque fonctionne aussi. Le resolver la compare à la version stockée dans la même transaction que celle qui écrit la modification. Si elles diffèrent, il renvoie un conflit et ne change rien.

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

enum ArchiveReason {
  CUSTOMER_REQUEST
  DUPLICATE
  END_OF_LIFE
}

L'énumération reason ne sert pas uniquement à améliorer les rapports. Elle empêche une requête d'inventer une justification libre que l'automatisation pourrait ensuite traiter comme significative. Utilisez du texte libre pour une note destinée aux personnes, mais gardez les catégories opérationnelles sous forme d'énumération.

Un champ de confirmation doit être lié à la cible réelle. Exiger la chaîne littérale ARCHIVE ne repère que les constructions négligentes. Exiger archive acme-project-42 oblige le client à résoudre puis à répéter l'identifiant d'une ressource. Cela n'arrête pas un client malveillant et ne doit jamais remplacer l'autorisation. En revanche, cette exigence intercepte de nombreuses requêtes générées qui associent la bonne action au mauvais identifiant.

Ne demandez pas de confirmation pour les mutations courantes, comme la modification d'un nom affiché. Trop de confirmations habituent les agents et les personnes à remplir mécaniquement tous les champs. Réservez-les aux actions dont le résultat est important ou difficile à inverser : suppression, publication, mouvement financier, révocation d'identifiants et changements qui touchent d'autres utilisateurs.

Pour une opération en masse, rendez la limite supérieure explicite et renvoyez un jeton d'aperçu lié à la sélection exacte. Cette entrée en dit beaucoup plus qu'un filtre brut :

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

Le resolver d'exécution doit refuser un jeton expiré, appartenant à un autre acteur, décrivant un autre tenant ou produisant un nombre différent de expectedCount. Sinon, un agent peut prévisualiser cinq enregistrements puis exécuter une requête évolutive qui en correspond maintenant à cinquante.

L'autorité doit suivre la mutation, pas le nom de la ressource

Une portée appelée projects:write est généralement trop large pour un travail autonome. Elle permet à un appelant de renommer, archiver, transférer, supprimer et éventuellement modifier les paramètres de facturation d'un projet sous une seule permission, puisque toutes ces actions touchent un projet. Ce regroupement suit le nom de la table, pas le risque de l'opération.

Accordez une autorité qui décrit l'action. Par exemple, un jeton de service pour l'automatisation des versions peut porter project:rename et project:archive, tandis qu'un flux de support ne porte aucune de ces portées. Une portée distincte project:delete doit rester rare. Si votre système d'identité ne peut pas émettre de portées aussi précises, ajoutez un contrôle de capacité côté serveur associé au nom de la mutation et consignez-le dans la décision d'autorisation.

La portée ne suffit jamais à déterminer l'accès. Chaque resolver doit effectuer plusieurs contrôles dans un ordre réfléchi :

  1. Authentifier l'appelant et identifier son tenant et son principal.
  2. Vérifier que le principal dispose de l'autorité nécessaire pour cette mutation.
  3. Charger la cible à l'intérieur de la frontière du tenant, plutôt que de la charger globalement puis de vérifier plus tard.
  4. Vérifier l'état de l'enregistrement et toute relation de rôle exigée par la règle métier.
  5. Effectuer l'écriture conditionnelle et ajouter un événement d'audit dans la même transaction.

Le chargement à l'intérieur de la frontière du tenant est important. Un resolver qui appelle findProjectById(id) avant de vérifier le tenant peut révéler l'existence d'une ressource par le temps de réponse ou le texte de l'erreur. Il peut aussi transmettre un objet chargé globalement à un helper qui suppose que l'autorisation a déjà été contrôlée. Intégrez l'appartenance au tenant dans le prédicat de recherche.

Ne déduisez pas les permissions de la tâche déclarée par l'agent. Un en-tête de requête indiquant X-Agent-Goal: cleanup est une information pour la piste d'audit, pas une autorisation. Les invites, les libellés de tâches et l'identité du modèle peuvent aider une personne à examiner une action, mais n'importe quel client peut les falsifier.

La même distinction s'applique à l'accès aux outils. Un agent peut avoir le droit d'appeler un endpoint GraphQL sans avoir le droit d'utiliser une mutation précise. Lorsque votre environnement d'agent le permet, décrivez séparément les outils de lecture et les outils d'action. La décision finale doit rester dans l'API, car un client peut contourner les métadonnées de l'outil et envoyer directement la requête HTTP.

Une simulation doit construire le même plan que l'exécution

Une simulation n'est utile que si elle répond à la question : « Que ferait exactement cette requête, maintenant ? » Un faux aperçu qui compte les lignes avec une requête simplifiée donne aux agents une fausse impression de sécurité. La mutation finale peut appliquer d'autres règles d'éligibilité, emprunter une autre branche d'autorisation ou déclencher une action externe que l'aperçu n'a jamais prise en compte.

Construisez une fonction de planification partagée. Elle reçoit l'acteur authentifié et l'entrée, valide toutes les conditions, résout les cibles, calcule les effets secondaires et produit un plan immuable. L'aperçu renvoie une représentation assainie de ce plan. Le chemin d'exécution ne consomme le plan qu'après présentation de son jeton à courte durée de vie et de la confirmation.

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
}

Un plan doit inclure les identifiants des cibles, leurs versions, l'identité de l'acteur, le tenant, l'empreinte de l'entrée, les effets prévus et l'heure d'expiration. Stockez-le côté serveur ou signez un jeton opaque qui fait référence à l'état stocké. Ne placez pas le plan complet dans un objet JSON contrôlé par le client en lui faisant confiance lors de l'exécution.

L'entrée d'exécution doit faire référence au jeton d'aperçu, sans répéter un sélecteur trop permissif :

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

Ce flux en deux appels ajoute de la friction. C'est précisément le but pour les actions importantes. Ne l'imposez pas à toutes les mutations. Une règle simple fonctionne bien : exigez un aperçu lorsqu'une opération touche plusieurs enregistrements, produit un effet externe irréversible ou rend une ressource indisponible pour d'autres utilisateurs.

Les réponses d'aperçu doivent aussi être protégées. Renvoyer la liste des enregistrements concernés peut divulguer des données aussi sûrement que l'exécution de la mutation. Appliquez les mêmes règles de tenant et de rôle pendant la planification. Un aperçu peut omettre les champs que l'acteur n'a pas le droit de lire tout en renvoyant le nombre et les catégories d'effets nécessaires à la décision.

L'idempotence et les versions corrigent deux échecs différents

Contrôlez chaque appel sensible
Exigez Touch ID ou un clic à chaque utilisation d'une clé API sensible.

L'idempotence empêche l'application répétée d'une même requête. Le contrôle de version empêche l'application sur un état qui a changé depuis que l'appelant l'a examiné. Les équipes ajoutent souvent l'un des deux et pensent avoir résolu les deux problèmes.

Un agent peut réessayer parce qu'une connexion HTTP s'est fermée après que le serveur a validé une mutation. Sans idempotence, la seconde requête peut créer un second remboursement, dupliquer un message ou appeler deux fois la même API externe. Donnez à chaque mutation produisant un effet une idempotencyKey fournie par l'appelant. Le serveur doit la stocker avec l'acteur authentifié, le nom de la mutation, l'empreinte de l'entrée normalisée et le résultat final ou une erreur stable.

Lorsque le serveur reçoit à nouveau le même acteur, la même mutation, la même clé et la même empreinte, il renvoie le résultat initial. S'il reçoit la même clé avec une empreinte différente, il renvoie IDEMPOTENCY_KEY_REUSED et ne fait rien. Accepter une entrée modifiée avec une clé réutilisée détruit la garantie sur laquelle les clients comptent lors des nouvelles tentatives.

Le contrôle de version concerne une autre séquence. Un agent lit la version 7 d'un projet, prépare un aperçu d'archivage, puis une personne renomme le projet ou restaure une invitation. Lors de l'exécution, le resolver compare la version 7 à la version actuellement stockée. Si celle-ci vaut 8, il renvoie un conflit. L'agent doit relire l'état actuel, réévaluer son intention et produire un nouvel aperçu si nécessaire.

La spécification GraphQL exécute en série les champs de premier niveau d'une même opération de mutation. Cet ordre ne sérialise pas des requêtes HTTP distinctes. Deux agents peuvent toujours envoyer deux opérations de mutation presque simultanément. Utilisez une mise à jour conditionnelle en base, un verrou de ligne ou une contrainte transactionnelle. Un contrôle en mémoire suivi d'une écriture séparée laisse une fenêtre de concurrence.

Une mise à jour conditionnelle de type SQL rend l'invariant explicite :

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;

Si aucune ligne n'est modifiée, examinez l'enregistrement actuel dans la frontière du tenant et renvoyez un résultat précis : absent, interdit, déjà archivé ou conflit de version. Ne transformez pas chaque résultat nul en erreur serveur générique. Les agents doivent savoir si une nouvelle tentative est dangereuse, utile ou inutile.

Les erreurs doivent indiquer à l'agent quoi faire ensuite

Le tableau errors de premier niveau de GraphQL convient aux erreurs d'analyse, de validation et d'exécution du resolver. C'est un mauvais endroit pour obliger les clients à extraire des résultats métier de messages en anglais. Placez les résultats attendus d'une mutation dans un payload typé, avec un code stable et des détails structurés.

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
}

Utilisez les erreurs de transport et d'exécution GraphQL lorsque le client n'a pas pu exécuter correctement l'opération. Utilisez un résultat typé lorsqu'une requête s'est exécutée normalement, mais n'a pas modifié l'état parce qu'une règle métier l'a refusée. Choisissez une convention et documentez-la. Mélanger errors.extensions.code pour certains conflits et des énumérations de payload pour d'autres rend le comportement des agents fragile.

Un agent doit pouvoir associer un résultat à une action sûre. VERSION_CONFLICT signifie relire l'objet et réévaluer l'intention. PREVIEW_EXPIRED signifie créer un nouvel aperçu. CONFIRMATION_REQUIRED signifie afficher la phrase attendue à une personne ou la lui demander, pas la deviner. FORBIDDEN signifie s'arrêter. IDEMPOTENCY_KEY_REUSED signifie générer une nouvelle clé uniquement si l'appelant veut explicitement effectuer une autre opération.

Ne renvoyez pas les noms de règles internes, les fragments SQL ou les détails du graphe d'autorisation. Des codes externes stables peuvent être précis sans révéler les détails d'implémentation. Conservez un identifiant de corrélation dans les extensions de la réponse et un enregistrement d'audit correspondant sur le serveur. L'opérateur disposera ainsi d'un élément concret à examiner lorsqu'un agent signale un échec.

Les payloads de réussite doivent fournir assez d'informations pour lever toute incertitude. Renvoyez l'état obtenu, la nouvelle version, l'identifiant de l'opération et les effets réellement produits. Un simple booléen oblige le client à effectuer une autre requête et laisse la place à une lecture obsolète. Il complique aussi inutilement la vérification humaine.

La suppression nécessite un cycle de vie, pas un booléen

Vérifiez la piste d'audit hors ligne
Vérifiez hors ligne le journal d'audit chiffré et chaîné par hachage de Sallyport avec sp audit verify, sans clé de coffre.

La suppression définitive est appréciée parce qu'elle donne une table bien rangée. C'est aussi l'action la plus susceptible de créer un problème de support impossible à résoudre lorsqu'un agent comprend mal une demande. De nombreux produits devraient d'abord archiver la ressource, conserver une période d'annulation contrôlée par le serveur, puis effectuer la purge définitive dans un flux séparé et restreint.

N'appelez pas deleteProject une opération d'archivage qui masque simplement un enregistrement. Les noms apprennent aux clients à quel état s'attendre. archiveProject doit renvoyer ARCHIVED; purgeProject doit signifier que les données ne seront plus disponibles. Lorsqu'une API utilise delete pour toutes les étapes du cycle de vie, un agent ne peut pas distinguer de manière fiable un nettoyage réversible d'une suppression définitive.

Une purge définitive nécessite une entrée et une autorisation plus strictes qu'un archivage. Elle peut exiger que la ressource soit restée archivée pendant une période de conservation, qu'aucune obligation légale ou de facturation ne s'y oppose et qu'un opérateur disposant d'une portée distincte l'approuve. Le resolver doit appliquer chacune de ces conditions. Un compte à rebours côté client ou une instruction d'outil n'a aucune autorité.

Les effets externes doivent être traités de la même façon. Si l'archivage annule des invitations, supprime un environnement distant ou déclenche un webhook, renvoyez ces effets dans l'aperçu et dans le payload final. Ne les rattachez pas discrètement à un resolver de mise à jour générique. La personne qui examine la demande d'un agent doit voir les conséquences avant de l'approuver, et l'agent doit disposer des faits à signaler après l'exécution.

Pour les actions financières ou liées aux identifiants, ne proposez pas une fausse simulation qui appelle le endpoint réel du fournisseur en espérant qu'aucun effet ne se produira. Utilisez le mécanisme d'aperçu ou d'autorisation documenté par le fournisseur lorsqu'il existe. Sinon, présentez le résultat comme une estimation locale et indiquez ce que le serveur n'a pas pu vérifier. Faire semblant d'être certain est pire que demander une décision humaine.

Les contrôles du resolver doivent rendre les promesses du schéma réelles

La conception du schéma limite les intentions mal formées. La conception du resolver empêche une requête apparemment autorisée de franchir une frontière réelle. Gardez ces couches séparées dans le code afin qu'une future refactorisation ne remplace pas un contrôle de permission par un commentaire dans la définition d'un outil.

Un resolver pour une action destructive doit suivre une séquence qui rend le refus peu coûteux et les écritures tardives. Authentifiez d'abord la requête, résolvez ensuite le tenant de l'acteur, validez l'entrée, chargez la cible dans ce tenant, vérifiez la portée et l'état, validez le jeton d'aperçu et la confirmation, réservez l'enregistrement d'idempotence, puis exécutez la transaction conditionnelle. L'ordre peut varier selon votre modèle de stockage, mais ne produisez pas d'effet externe avant de savoir que la transaction peut être validée.

Un enregistrement d'idempotence doit être géré avec soin lorsque le travail implique une base de données et un fournisseur externe. Marquer une clé comme terminée avant l'appel externe risque d'indiquer une réussite alors que l'appel a échoué. Appeler d'abord le fournisseur risque de provoquer un doublon si le processus s'arrête avant l'enregistrement de la fin. Utilisez un modèle outbox ou la prise en charge de l'idempotence par le fournisseur lorsqu'elle existe. Enregistrez une opération en attente durable, validez la décision locale puis envoyez l'effet externe avec un identifiant d'opération qui résiste aux nouvelles tentatives.

Les enregistrements d'audit doivent identifier le principal authentifié, l'exécution de l'agent lorsqu'elle existe, le nom de la mutation, la cible normalisée, l'empreinte de l'entrée, le résultat de l'autorisation, la référence de l'aperçu, la clé d'idempotence, le résultat et la version obtenue. Masquez les notes et les champs contenant des informations sensibles conformément à vos règles de conservation. Un événement d'audit qui indique seulement « mutation réussie » est presque inutile lors d'un incident.

Pour les agents qui agissent par appels HTTP ou SSH authentifiés, gardez autant que possible les identifiants en dehors du processus du modèle. Sallyport fait passer les actions prises en charge par son coffre local et consigne chaque appel, ce qui est utile lorsqu'une mutation GraphQL nécessite une autorisation visible par une personne au-delà de ce qu'un simple jeton bearer peut fournir.

Testez la requête générée, pas seulement le resolver

Placez une passerelle devant les actions
Utilisez le relais sp mcp inclus pour placer Sallyport entre un agent compatible MCP et les actions externes.

Les tests unitaires qui appellent un resolver avec des objets soigneusement construits ne couvrent pas le mode d'échec qui vous intéresse. Les clients générés envoient des champs omis, des valeurs nulles, des identifiants obsolètes, des alias, des requêtes répétées et des variables assemblées à partir de résultats d'outils précédents. Testez la frontière GraphQL publique avec les mêmes formes.

Construisez une matrice de tests de mutation centrée sur le comportement plutôt que sur les branches du code. Couvrez au minimum un appelant d'un autre tenant, un appelant qui peut lire mais ne possède pas la portée d'action, un aperçu expiré, une version de cible modifiée, une mauvaise chaîne de confirmation, une clé d'idempotence rejouée et deux appels concurrents utilisant la même version attendue. Vérifiez la réponse et l'état durable après chaque test.

Cette requête doit échouer pendant la validation GraphQL, car l'entrée ne définit pas ownerId :

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"
  }
}

La réponse attendue relève du format d'erreur GraphQL de premier niveau, car le document contient un objet d'entrée invalide. Ce test montre que le schéma écarte une capacité sans rapport de la mutation. Un autre test doit démontrer qu'une requête d'archivage bien formée échoue quand même si l'acteur appartient à un autre tenant.

Exécutez les tests de concurrence avec le véritable comportement transactionnel, pas avec un faux stockage en mémoire. Envoyez deux requêtes d'archivage avec le même identifiant et la même version attendue, puis vérifiez que l'une renvoie ARCHIVED et que l'autre renvoie un conflit ou le résultat d'une nouvelle tentative idempotente. Si les deux appels signalent une réussite avec des identifiants d'opération différents, votre écriture conditionnelle ne joue pas son rôle.

Testez aussi la vérification des audits. Si votre passerelle d'action produit une piste d'audit chiffrée et résistante aux altérations, intégrez sa vérification aux exercices de gestion d'incident plutôt que de la laisser sous la forme d'une commande que personne n'a utilisée. Sallyport expose sp audit verify pour vérifier hors ligne sa chaîne de hachage sans clé de coffre ; exécutez-la sur un journal copié et assurez-vous que les opérateurs savent interpréter un échec.

Les outils générés ont besoin de moins de choix, pas d'avertissements plus longs

Les agents sont plus performants lorsqu'un schéma d'outil présente l'action sûre la plus réduite correspondant à la tâche. Un immense catalogue de mutations avec des filtres génériques, des indicateurs et des effets secondaires facultatifs oblige le modèle à déduire la politique à partir des noms de champs. Un catalogue compact d'opérations explicites de lecture, d'aperçu, d'exécution et de récupération lui offre un chemin clair.

Exposez des opérations de lecture qui renvoient les identifiants, versions, états et noms nécessaires à l'agent avant qu'il ne propose une mutation. Si l'agent doit inventer un identifiant à partir d'un libellé humain, la conception de la mutation ne pourra pas vous sauver. Renvoyez clairement les identifiants stables et signalez les résultats de recherche ambigus au lieu d'en choisir un silencieusement.

Rédigez des descriptions d'outils qui indiquent une condition préalable et une conséquence, tout en conservant le serveur comme point d'application. Par exemple : « Archive un projet après un aperçu réussi. Annule les invitations en attente indiquées dans l'aperçu. » C'est plus utile que « À utiliser avec prudence », qui ne donne aucune indication opérationnelle à un agent.

Ne cherchez pas à régler tous les risques avec une boîte de dialogue d'approbation humaine. L'approbation convient lorsqu'une personne prend la décision, mais les demandes répétitives deviennent du bruit de fond. Placez les protections courantes dans les portées, les contrôles de tenant, les versions et l'idempotence. Demandez à une personne d'examiner le petit nombre d'actions dont l'intention ne peut pas être déduite des données, dont la conséquence est définitive ou qui franchissent une frontière organisationnelle.

Commencez par la mutation qui ferait le plus de dégâts si un agent l'appelait deux fois, l'appelait sur un état obsolète ou l'appelait contre le mauvais tenant. Séparez son entrée, ajoutez un véritable aperçu lorsque l'action le justifie, rendez l'écriture conditionnelle et écrivez le test de rejeu. Ce travail montrera si votre API modélise clairement une action ou si elle ne fait qu'exposer des champs de base de données.

FAQ

La validation GraphQL rend-elle les mutations destructrices sûres ?

Non. GraphQL valide la forme de la requête, les arguments des champs et la conversion des scalaires, mais ne sait pas si une opération est pertinente dans l'état actuel du système. Votre resolver doit toujours appliquer les règles d'autorisation, de transition d'état, de séparation des tenants et de concurrence.

Quand dois-je créer une entrée GraphQL distincte pour une mutation ?

Utilisez une entrée propre à la mutation lorsqu'une action modifie l'état d'une manière distincte, notamment pour une suppression, une suspension, un remboursement, un transfert ou une publication. Une entrée de patch générique convient aux champs ordinaires, mais elle ne doit pas dissimuler des actions de cycle de vie derrière des booléens.

Que doit réellement faire une simulation GraphQL ?

Une simulation doit appliquer les mêmes validations, autorisations, sélections de cibles et règles de planification que la mutation réelle, puis s'arrêter avant l'écriture externe ou la validation de la transaction. Si elle suit un chemin différent, les agents apprendront à utiliser un aperçu qui ne correspond pas au comportement en production.

Comment fonctionnent les clés d'idempotence dans les mutations GraphQL ?

Une clé d'idempotence permet au serveur de reconnaître une nouvelle tentative de la même action et de renvoyer le résultat initial au lieu d'appliquer l'action deux fois. Stockez la clé avec l'acteur, le nom de la mutation, l'empreinte de l'entrée normalisée, le résultat et la politique d'expiration. Refusez sa réutilisation avec une entrée différente.

Les portées GraphQL doivent-elles correspondre aux ressources ou aux mutations ?

Une portée globale comme write:projects accorde généralement trop de pouvoir à un agent qui doit seulement archiver un projet. Limitez les capacités destructrices à la mutation ou à l'action, puis appliquez séparément les contrôles de propriété et de tenant dans le resolver.

Comment un agent IA peut-il supprimer plusieurs enregistrements avec GraphQL en toute sécurité ?

Traitez la suppression en masse comme une opération distincte, avec un nombre de cibles, une règle de sélection limitée, un texte de confirmation explicite et un aperçu. Ne transformez jamais un champ de suppression d'un seul objet en boucle accidentelle simplement parce qu'un agent peut construire une longue liste d'identifiants.

Quelles erreurs les mutations GraphQL destructrices doivent-elles renvoyer à un agent ?

Renvoyez des codes lisibles par machine, un message humain stable et des détails structurés, comme la version actuelle ou la phrase de confirmation attendue. Un agent ne devrait pas avoir à analyser du texte pour décider s'il peut réessayer, actualiser l'état ou demander l'intervention d'une personne.

Deux requêtes de mutation GraphQL peuvent-elles entrer en concurrence ?

GraphQL sérialise les champs de premier niveau au sein d'une même opération de mutation, mais des requêtes distinctes peuvent toujours entrer en concurrence. Placez la version attendue dans l'entrée et rendez l'écriture conditionnelle, puis renvoyez un conflit lorsque la version stockée a changé.

Une invite destinée à un agent peut-elle garantir la sécurité d'une mutation GraphQL ?

Non. Le schéma est une interface publique et les clients peuvent appeler directement l'API, en contournant toute instruction de l'agent ou description d'outil. Le serveur doit vérifier l'identité de l'appelant, sa portée, sa relation avec le tenant, l'état et la confirmation avant toute modification.

Comment empêcher les identifiants d'un agent d'apparaître dans les requêtes API générées ?

Conservez les identifiants API et SSH à longue durée en dehors du processus de l'agent, approuvez l'exécution lorsque c'est nécessaire et consignez chaque action sortante. Sallyport est une option macOS qui fait passer les actions HTTP et SSH prises en charge par un coffre chiffré local, sans remettre les secrets à l'agent.

Sallyport

Sallyport exécute les appels d'API et les commandes SSH à la place de votre agent IA. Les clés restent dans un coffre-fort local sur votre Mac ; vous approuvez chaque exécution et chaque action est consignée dans un journal scellé.

© 2026 Sallyport · Open source sous Apache-2.0 · Oleg Sotnikov