# Des outils d'agent sans secret : des contrats d'action qui tiennent la route

Un agent doit demander des actions, et non recevoir les moyens d'usurper l'identité d'une personne ou d'un compte de service. Cela paraît évident jusqu'à ce qu'on examine un outil d'agent courant : une fonction `http_request` accepte une URL, des en-têtes, une méthode et un corps, tandis que l'agent reçoit un jeton bearer depuis une variable d'environnement. L'appel de l'outil semble propre. L'autorité, elle, est dispersée entre le texte du prompt, la mémoire du processus, les journaux, l'historique du shell et le sous-processus que l'agent lancera ensuite.

Une conception sans secret place une frontière nette entre l'intention et l'exécution. L'agent dit : « crée un déploiement pour ce service dans cet environnement ». Une couche qui détient les identifiants décide si l'action peut être exécutée, choisit la bonne identité, effectue l'appel authentifié et renvoie le résultat. Cela transforme ce que vous pouvez auditer, approuver et révoquer. Cela vous oblige aussi à concevoir des interfaces qui méritent l'autonomie.

## Un contrat doit décrire l'intention, pas le transport

Un contrat d'action nomme l'opération qu'un utilisateur reconnaît et limite ses entrées aux informations nécessaires à cette opération. Les détails du transport restent derrière la frontière. La différence est facile à manquer, car HTTP fait ressembler chaque action à une méthode, une URL, des en-têtes et un corps JSON.

Prenons deux interfaces d'outil pour ouvrir une demande de modification. La première est courante et dangereuse pour un processus autonome :

```json
{
  "name": "http_request",
  "input": {
    "method": "POST",
    "url": "https://code.example/api/projects/alpha/changes",
    "headers": {
      "Authorization": "Bearer ${TOKEN}",
      "Content-Type": "application/json"
    },
    "body": {
      "title": "Fix timeout",
      "branch": "agent/fix-timeout"
    }
  }
}
```

Cette interface donne à l'agent le contrôle de la destination, de la méthode d'authentification et de la forme de la requête. Supprimer le jeton en clair ne règle rien si l'agent peut choisir un alias d'en-tête, un identifiant d'identifiant, une URL de proxy ou une commande shell qui lit un jeton ailleurs. Vous avez déplacé le secret, mais vous n'avez pas réduit l'autorité.

Une interface orientée contrat ressemble plutôt à ceci :

```json
{
  "name": "create_change_request",
  "input": {
    "project": "alpha",
    "source_branch": "agent/fix-timeout",
    "title": "Fix timeout in retry path",
    "description": "Adds a bounded retry and a regression test."
  }
}
```

La couche d'exécution associe `project` à un point de terminaison connu et à un compte approuvé. Elle fournit elle-même l'en-tête d'authentification. Elle peut refuser le nom de branche, vérifier le dépôt cible, demander une approbation ou renvoyer une erreur du service distant. L'agent ne possède aucun paramètre qui signifie « utilise l'identifiant qui donne le plus de droits ».

Cette distinction est souvent brouillée : **sans secret ne signifie pas la même chose que jeton masqué**. Le masquage tente de contrôler ce que l'agent voit après lui avoir accordé une autorité. Un contrat d'action empêche l'agent de détenir cette autorité dès le départ. Si un prompt est divulgué, si la transcription d'un outil est copiée ou si un sous-processus lit son environnement, la première conception a déjà perdu un identifiant. La seconde peut exposer des données opérationnelles, qui nécessitent leurs propres contrôles, mais elle ne remet pas le matériel de signature.

Un contrat ne doit pas non plus prétendre que chaque point de terminaison mérite un outil personnalisé. Les actions personnalisées sont pertinentes lorsqu'une personne peut exprimer le résultat voulu en une phrase. « Redémarre cette charge de travail de staging » décrit un résultat. « Envoie un PATCH vers n'importe quelle URL » est une primitive de transport. Si vous avez besoin de cette primitive pour une tâche de maintenance, donnez-la à une intégration distincte et étroitement limitée, pas à un agent de programmation généraliste.

## Le détenteur des identifiants doit exécuter la requête

Un contrat ne protège rien si l'agent effectue toujours l'appel réseau final avec un secret monté dans son processus. Le composant qui stocke l'identifiant doit effectuer lui-même la requête HTTP ou la connexion SSH.

La frontière d'exécution assume donc cinq tâches :

- Elle associe le nom de l'action à une destination fixe et à un comportement de protocole défini.
- Elle choisit une identité stockée dans un petit ensemble approuvé.
- Elle injecte l'identifiant uniquement dans la requête sortante ou dans l'échange d'authentification SSH.
- Elle enregistre la requête, la décision et le résultat sans écrire de données secrètes dans l'enregistrement.
- Elle renvoie une réponse adaptée à l'action plutôt qu'une copie de son état interne.

Le processus du modèle ne doit recevoir ni jeton ni clé privée, même temporaire. Évitez les conventions shell comme `TOKEN=$(vault read ...)`, les fichiers d'identifiants dans un répertoire de travail, les valeurs `Authorization` dans des commandes curl générées et les agents SSH partagés avec un shell lancé par l'agent. Chacune semble pratique parce qu'elle préserve les scripts existants. Chacune transforme le processus de l'agent en détenteur d'identifiants.

Les bonnes pratiques actuelles de sécurité OAuth 2.0 de l'IETF font le même constat pratique dans un autre contexte : les jetons bearer doivent être protégés au repos et en transit, car quiconque en possède un peut l'utiliser. Un jeton bearer ne devient pas sûr parce qu'on demande à un modèle de ne pas l'afficher. La possession tient lieu de contrôle d'autorisation. Pour un outil d'agent, il vaut mieux éviter que le processus en soit propriétaire.

Pour SSH, la frontière doit posséder davantage que la clé privée. Une interface brute `ssh host command` donne à l'agent une portée étendue même si la clé ne quitte jamais un assistant. Celui-ci doit sélectionner une définition d'hôte et une identité stockées, puis imposer une forme de commande adaptée à cet hôte. Un hôte de déploiement peut autoriser `status`, `restart-service` et `tail-release-log` avec un nom de service. Il ne doit pas accepter discrètement `bash -c` parce que quelqu'un voulait un raccourci.

Ne confondez pas cela avec un proxy homme du milieu. Un proxy relaie un trafic client arbitraire et voit généralement les identifiants en transit. Une couche d'actions qui détient les identifiants reçoit une demande pour une opération nommée, construit l'appel sortant et conserve l'identifiant dans son propre coffre. Cette distinction détermine si l'agent peut transformer une action approuvée en une autre.

## La conception des paramètres détermine la quantité d'autorité qui fuit

Chaque champ d'un contrat crée une liberté supplémentaire. Les bons champs identifient l'objet à traiter ou fournissent le contenu dont l'action a réellement besoin. Les mauvais champs modifient la destination de l'autorité, l'identité utilisée ou l'opération de niveau inférieur exécutée.

Appliquez ce test à chaque entrée proposée : si l'agent modifie cette valeur, peut-il rediriger une requête privilégiée vers un autre système, élargir l'ensemble des ressources concernées ou modifier l'authentification ? Si oui, supprimez le champ, transformez-le en énumération associée par l'exécuteur ou séparez l'opération en plusieurs contrats.

Une interface de déploiement illustre ce point :

```json
{
  "name": "deploy_release",
  "input_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["service", "environment", "version", "reason"],
    "properties": {
      "service": {"type": "string", "enum": ["api", "worker"]},
      "environment": {"type": "string", "enum": ["test", "production"]},
      "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
      "reason": {"type": "string", "maxLength": 500}
    }
  }
}
```

Le schéma empêche l'apparition de champs inattendus comme `url`, `headers`, `credential_name` ou `command`. L'exécuteur peut associer `service` et `environment` à une cible de déploiement connue. `additionalProperties: false` compte plus qu'il n'y paraît. Sans cette règle, un validateur permissif peut conserver un champ non reconnu, puis quelqu'un le reliera plus tard à un client HTTP « pour plus de flexibilité ». C'est ainsi qu'un simple point d'extension devient une porte de sortie pour les identifiants.

Les énumérations ne sont pas toujours la solution. Un nom de dépôt, une branche, un numéro de ticket ou un chemin de fichier peut devoir varier. Validez ces valeurs selon leur domaine et appliquez un contrôle de frontière après résolution. Par exemple, résolvez un identifiant de dépôt à l'aide d'une liste locale autorisée, puis utilisez l'emplacement distant associé. N'acceptez pas une URL de dépôt en essayant de déterminer si elle semble acceptable.

Le texte libre mérite un examen distinct. Un agent peut devoir rédiger la description d'un ticket, le résumé d'une demande de modification ou une réponse d'assistance. Ce texte est du contenu, pas une autorité, mais il peut tout de même causer des dommages par des mentions, du balisage, des modèles ou des commandes intégrées consommées par le service en aval. Limitez sa longueur, définissez clairement son rendu et ne l'interpolez pas dans une commande shell. Lorsqu'une action doit exécuter une commande, construisez directement le vecteur d'arguments et gardez le texte non fiable dans un argument de données, jamais dans une chaîne de commande.

## Les outils de requête génériques créent des moteurs de politiques cachés

Un outil HTTP générique est populaire parce que les équipes peuvent connecter un agent à n'importe quel service en un après-midi. Il convient mal à la plupart des tâches d'agent privilégiées, car chaque prompt, description d'outil et branche de code devient une politique d'autorisation officieuse.

Une équipe commence souvent par un wrapper comme celui-ci :

```text
request(method, url, headers, body)
```

Puis elle ajoute des garde-fous. Bloquer quelques domaines. Supprimer `Authorization`. Autoriser certaines méthodes. Analyser un préfixe d'URL. Refuser `localhost`. Exiger une boîte de dialogue d'approbation pour les appels risqués. Quelques mois plus tard, quelqu'un a besoin d'un nouveau point de terminaison avec un en-tête personnalisé, ajoute une exception et le wrapper possède désormais un langage de politiques sans tests ni responsable clairement désigné.

Le problème n'est pas que les outils génériques soient toujours mauvais. Ils conviennent à une console de débogage utilisée par une personne, lorsque l'opérateur détient déjà l'autorité et peut inspecter chaque octet. Ils conviennent aussi à un service d'intégration qui reçoit des appels provenant d'un code que vous contrôlez et possède une identité réseau étroite. Un agent autonome est différent, car il peut effectuer de nombreux appels, découvrir des chemins inattendus et agir sur du texte non fiable. Il a besoin de moins de degrés de liberté.

Écrivez des actions nommées autour d'unités de travail stables. Pour un service de gestion de code source, préférez `read_merge_request`, `comment_on_merge_request` et `create_branch` à un client REST universel. Pour les opérations, préférez `get_service_status`, `fetch_release_logs` et `request_deployment`. Vous aurez peut-être besoin de davantage de contrats, mais chacun aura un responsable, un ensemble de tests, une étiquette d'approbation claire et un rayon d'impact que l'on peut examiner.

Ne cachez pas non plus une requête générique derrière une action nommée. Un outil appelé `update_ticket` qui accepte un `path`, une `method` et un `body` arbitraires n'a fait que changer l'étiquette. Le contrat doit fixer ces détails. Il peut exposer un objet de modification contrôlé lorsque l'API en aval l'exige, mais l'exécuteur doit décider du point de terminaison, de la méthode HTTP, du type de contenu et du compte.

La spécification du Model Context Protocol facilite la découverte des outils, car elle permet à un serveur de publier auprès d'un client les noms des outils, leurs descriptions et leurs schémas d'entrée JSON. Ce schéma est utile, mais il ne peut pas rendre sûre une action trop large. Un JSON Schema peut vous dire qu'une URL est une chaîne. Il ne peut pas vous dire que cette URL est le seul point de terminaison de facturation auquel votre identifiant de production doit accéder. L'autorisation reste du ressort de la couche d'exécution.

## L'approbation doit nommer l'action qu'une personne peut juger

L'approbation humaine fonctionne lorsque la personne voit une demande reconnaissable et peut la refuser rapidement. Elle échoue lorsque le prompt demande d'approuver un paquet opaque de détails de transport après que l'agent a déjà pris les décisions importantes.

Comparez ces cartes d'approbation :

```text
Allow POST https://api.example/v1/resources/882?
Headers: Authorization, X-Region, X-Client
```

```text
Deploy version 2.14.3 of api to production
Reason: Fixes failed payment retries
Requested by: signed agent process build-worker
```

La seconde carte permet à l'opérateur de juger l'intention. Elle donne aussi à l'enregistrement d'audit une phrase utile. La première demande de reconstruire le sens à partir d'une URL et d'une liste d'en-têtes, ce qui favorise la lassitude face aux approbations. Les gens valident des prompts illisibles sans les lire, surtout lorsqu'une exécution normale de l'agent en crée plusieurs.

Utilisez l'approbation au point où une décision modifie l'autorité. Une couche d'exécution peut autoriser une fois un nouveau processus d'agent pour une session, puis exiger une nouvelle décision pour certains identifiants sensibles ou certaines actions destructrices. Cette séparation permet de garder les tâches courantes utilisables sans traiter tous les identifiants comme équivalents. Un jeton de projet en lecture seule et une identité de déploiement en production ne doivent pas partager la même règle d'approbation simplement parce qu'ils circulent tous deux dans des en-têtes HTTP.

Le texte de l'approbation doit indiquer qui a demandé l'opération. L'identité du processus est utile, car un agent dans un terminal, un assistant en arrière-plan et un exécutable inconnu ne méritent pas la même confiance. Sur macOS, l'autorité de signature du code peut fournir à la personne qui approuve une exécution un signal concret sur son origine. Elle ne prouve pas que chaque instruction du prompt est sûre, mais elle répond à la première question : quel processus demande d'agir avec ce compte ?

Ne faites jamais de l'approbation votre seul contrôle. Une personne peut mal lire un prompt, approuver sous pression ou laisser une session ouverte. Le contrat doit toujours conserver des entrées contraintes et un chemin d'identifiants fixe. À l'inverse, n'ajoutez pas un langage de politiques lorsqu'un contrat d'action clair et un choix d'approbation suffisent. Les règles qui comparent des champs arbitraires, des plages horaires, des expressions régulières et des déclarations d'utilisateur deviennent vite un autre programme que personne ne peut examiner avec assurance pendant un incident.

## Un déploiement échoué montre où les contrats trop lâches se brisent

Un échec bien connu commence avec un agent capable de déployer en test via un outil shell. L'équipe stocke un jeton cloud dans l'environnement de l'agent, car la CLI de déploiement l'exige. Le schéma de l'outil accepte `environment` et `extra_args`, ce qui semblait inoffensif lorsque seul l'environnement de test existait.

Un ticket demande à l'agent de « vérifier le correctif urgent en test et de partager le résultat ». L'agent exécute la commande attendue. Il voit ensuite un ancien message de déploiement dans le dépôt et tente un argument supplémentaire copié d'un vieux script. Cet argument sélectionne la production, modifie un compte cible ou injecte une expansion shell. Le jeton avait une portée de production, car gérer des identifiants séparés semblait trop contraignant. À ce stade, la formulation du prompt ne vous protège plus. Le processus détient déjà une autorité étendue et l'interface lui permet de choisir la cible.

Une frontière contractuelle modifie la séquence :

1. L'agent appelle `deploy_release` avec un service, un environnement, une version et une raison énumérés.
2. L'exécuteur résout l'environnement vers une cible fixe et choisit l'identité affectée à cette cible.
3. L'exécuteur demande une décision si cette identité l'exige, puis enregistre le résultat avec l'exécution de l'agent qui l'a demandé.
4. L'exécuteur renvoie un identifiant de déploiement et son état, ou un refus structuré expliquant à l'agent pourquoi il ne peut pas continuer.

L'agent ne peut pas ajouter `--account`, définir un point de terminaison cloud ou lire un jeton. Une valeur `production` erronée reste possible, car les humains et les modèles peuvent demander la mauvaise chose. Mais le texte d'approbation mentionne désormais la production en termes simples, l'identité sélectionnée peut ne disposer que de l'autorité prévue pour les déploiements de production et l'enregistrement de l'action relie la décision au processus et à la requête.

Cette distinction compte lors du diagnostic. Dans la conception lâche, les enquêteurs trouvent souvent des fragments : une transcription shell, des événements d'audit cloud, un journal CI et peut-être une valeur de jeton qu'il faut maintenant faire tourner. Dans la conception contractuelle, ils peuvent examiner l'action demandée, la cible résolue, le libellé de l'identité, le résultat de l'approbation, l'état de la réponse et la session à l'origine de l'appel. Une piste d'audit n'efface pas une erreur, mais elle réduit le temps passé à deviner quel chemin a été exécuté.

## Les réponses d'erreur doivent guider la récupération sans exposer les détails internes

Une couche d'action sécurisée doit renvoyer des erreurs sur lesquelles un agent peut agir sans recevoir le secret, les données de signature de la requête ou la structure interne du coffre. Des échecs vagues poussent les agents vers les nouvelles tentatives et les contournements. Des échecs trop détaillés transforment les journaux d'erreur en canal d'information.

Utilisez des codes d'erreur stables et une petite structure publique :

```json
{
  "ok": false,
  "error": {
    "code": "APPROVAL_REQUIRED",
    "message": "Deployment to production needs user approval.",
    "retryable": true,
    "request_id": "act_01H..."
  }
}
```

Un coffre verrouillé doit renvoyer `VAULT_LOCKED` ; une décision utilisateur refusée doit renvoyer `APPROVAL_DENIED` ; une violation du contrat doit renvoyer `INVALID_ARGUMENT` ; un 429 du service distant peut renvoyer `REMOTE_RATE_LIMITED`. L'agent peut signaler l'état, attendre, réessayer après la modification de la condition indiquée ou choisir une alternative non destructive. Il ne doit pas recevoir une erreur contenant un en-tête d'autorisation brut, un relevé du sujet d'un jeton, une configuration d'hôte privée ou une requête signée complète.

Séparez l'échec d'autorisation de l'échec distant. « Permission refusée » peut signifier que l'exécuteur local a refusé l'action, que le compte distant sélectionné n'a pas les droits nécessaires ou que le service en aval a rejeté une authentification mal formée. Ces situations exigent des corrections différentes. Le message public peut rester concis tandis que l'enregistrement d'audit protégé de l'exécuteur conserve un motif précis et l'état distant.

Les nouvelles tentatives doivent respecter la sémantique du contrat. Les opérations de lecture tolèrent souvent les répétitions. La création d'un ticket, l'envoi d'un message ou le démarrage d'un déploiement peuvent ne pas les tolérer. Ajoutez un identifiant d'idempotence lorsque l'API distante le permet, généré par l'exécuteur ou fourni comme identifiant de requête contraint. Enregistrez l'association avant l'envoi, puis réutilisez-la lors d'une nouvelle tentative. Ne laissez pas un agent inventer un nouvel identifiant chaque fois qu'il voit un délai d'attente, car il pourrait créer du travail en double en voulant aider.

Pour les actions sans prise en charge de l'idempotence distante, utilisez une organisation en deux temps, préparation puis confirmation. L'action de préparation renvoie un plan de courte durée décrivant la cible et la différence. L'action de confirmation fait référence à ce plan et exige une approbation actuelle. Cela ajoute un aller-retour, mais ce coût est inférieur à celui d'un paiement, d'une suppression ou d'une modification en production répété après un échec réseau ambigu.

## Les enregistrements d'audit ont besoin de deux vues et d'une seule source de vérité

Un système d'audit utile répond à deux questions différentes : qu'a tenté d'exécuter cette session d'agent, et qu'a fait chaque appel privilégié ? Si vous fusionnez ces informations dans un flux d'événements indifférencié, il devient difficile soit de reconstruire une session, soit de retrouver une requête donnée.

Conservez un journal de session pour l'exécution. Il doit indiquer l'identité du processus, le début et la fin, la décision d'autorisation, l'état de révocation et les actions demandées pendant cette exécution. Conservez un journal d'activité pour les appels. Il doit indiquer le nom de l'action, les paramètres normalisés, le libellé de l'identifiant sélectionné sans son secret, le résultat de l'approbation, les horaires, la classe de destination et le résultat.

Les deux vues doivent provenir du même enregistrement en ajout uniquement. Sinon, l'écran de session et le journal des appels peuvent diverger lorsqu'un écrivain tombe en panne ou filtre les événements différemment. Un journal chiffré à écriture aveugle offre aussi un avantage concret : le composant qui ajoute un événement n'a pas besoin de déchiffrer les anciens enregistrements pour en écrire un nouveau.

La preuve d'intégrité nécessite une vérification hors ligne. Une chaîne de hachage permet à un vérificateur de détecter la suppression, la substitution ou le réordonnancement d'enregistrements lorsqu'il dispose de la séquence du journal. La vérification doit porter sur le texte chiffré afin qu'un auditeur puisse vérifier la continuité sans recevoir la clé du coffre. Cela ne prouve pas qu'une machine compromise n'a jamais omis d'enregistrer un événement. Cela prouve qu'une chaîne conservée n'a pas été modifiée silencieusement après coup. Considérez ces deux affirmations séparément.

Un vérificateur en ligne de commande doit rendre les échecs compréhensibles. Sa sortie peut être aussi simple que :

```text
$ sp audit verify audit.log
verified: 184 records
first sequence: 9012
last sequence: 9195
chain: valid
```

Si l'enregistrement 9137 a été modifié, la commande doit identifier la première séquence rompue et retourner un code différent de zéro. Ne signalez pas seulement « échec de la vérification ». Les personnes qui répondent à l'incident ont besoin de savoir à quel endroit les preuves cessent d'être fiables.

Sallyport utilise cette séparation entre les sessions d'agents et l'activité individuelle, en projetant les deux à partir d'un journal d'audit chiffré et chaîné par hachage que `sp audit verify` peut vérifier sans la clé du coffre. C'est la bonne forme pour une passerelle d'agent locale, car révoquer une session active et enquêter sur un appel précis sont deux tâches différentes.

## Les contrats ont besoin de tests qui tentent de s'en échapper

Les tests du chemin nominal prouvent qu'une action fonctionne. Les tests de sécurité prouvent que ses entrées déclarées sont les seuls contrôles dont dispose l'appelant. Écrivez ces tests avant d'ajouter un paramètre de confort, car c'est par ces paramètres que l'autorité revient le plus souvent.

Pour chaque action, testez au moins les cas suivants :

- Refuser un champ inattendu, notamment `headers`, `url`, `command` et les références d'identifiants.
- Refuser les valeurs qui se résolvent en dehors de l'ensemble de ressources autorisé par l'action.
- Confirmer que les requêtes HTTP sortantes ne reçoivent les identifiants qu'après la construction de la destination par l'exécuteur.
- Confirmer que les entrées d'audit omettent les valeurs de jetons, le matériel de clé privée et les champs d'autorisation signés.
- Confirmer qu'une session refusée ou révoquée ne peut pas réutiliser une approbation précédente.

Utilisez un faux serveur sortant dans les tests et inspectez la requête reçue. Le test doit vérifier l'URL réelle, la méthode, les en-têtes construits par l'exécuteur et l'absence d'authentification contrôlée par l'appelant. Se moquer uniquement du client interne de l'exécuteur ne répond pas à la question importante : que quitterait la machine si l'agent fournissait des paramètres hostiles ?

Testez aussi les entrées inconfortables qu'un modèle finira par produire : un identifiant de dépôt avec un préfixe de schéma, un nom de branche contenant de la ponctuation shell, un caractère Unicode ressemblant à un autre dans un libellé d'environnement, des champs JSON répétés, une description énorme et un délai d'attente après que le service distant a accepté l'action. La validation du contrat doit échouer par défaut. Si l'exécuteur ne peut pas résoudre une cible demandée avec certitude, il doit refuser l'appel et renvoyer une erreur utile.

Examinez les contrats comme du code auquel est attachée une autorité. Demandez-vous si un nouveau champ donne à l'appelant un chemin vers un autre hôte, un compte plus étendu, une autre commande ou une autre classe d'objet. Si c'est le cas, rendez l'autorité explicite dans le nom de l'action et le comportement d'approbation. Une action `delete_file` avec un chemin absolu libre est beaucoup plus difficile à comprendre qu'une action `remove_preview_asset` avec un identifiant de ressource résolu à l'intérieur d'un projet connu.

Le premier contrat à corriger est généralement celui qui accepte une URL arbitraire ou une chaîne shell. Remplacez-le par l'action nommée la plus petite qui couvre le travail réellement nécessaire. L'interface devient moins habile, et l'agent devient moins puissant d'une manière que vous pourrez expliquer plus tard. C'est un progrès.
