DEV Community

Christ-loisele Atidegla
Christ-loisele Atidegla

Posted on

Exposer vos modèles Eloquent à un agent IA sans lui donner votre base

Voici le premier outil MCP que presque tout le monde écrit :

class TicketTool extends Tool
{
    public function handle(Request $request): Response
    {
        return Response::structured(Ticket::find($request->get('id')));
    }
}
Enter fullscreen mode Exit fullscreen mode

Il fonctionne, et il contient trois failles de sécurité distinctes qui ne sont pas visibles dans le code.

Un : il renvoie toutes les colonnes

Ticket::find() vous donne le modèle. Sérialiser le modèle donne tous les attributs de la ligne.

Cela inclut internal_notes. Cela inclut le card_last_four ajouté pour un écran de support. Cela inclut la colonne ajoutée la semaine dernière pour une fonctionnalité pas encore livrée.

C'est la plus discrète des trois parce que rien n'a l'air anormal. Vos vues Blade affichent quatre champs, donc vous pensez au modèle comme ayant quatre champs. L'agent les reçoit tous, et le modèle explique volontiers leur contenu à qui le demande.

Deux : il ignore qui pose la question

Il n'y a aucun utilisateur dans ce code. La requête n'est pas filtrée, donc l'outil récupère n'importe quel ticket par identifiant, pour quiconque peut l'appeler.

Ajouter ->where('user_id', auth()->id()) corrige le cas immédiat et en crée un plus subtil : le filtre vit désormais dans l'outil et non dans vos policies. Quand quelqu'un ajoutera un deuxième outil, une relation ou un scope, il faudra s'en souvenir. On ne s'en souviendra pas.

Trois : rien n'empêche l'énumération

find($id) avec un identifiant auto-incrémenté est une invitation. Un agent qui peut appeler l'outil peut l'appeler avec 1, 2, 3, et continuer. Même une fois la policy ajoutée, la forme de l'échec fuit : un enregistrement inexistant et un enregistrement interdit répondent en général différemment, et cette différence cartographie les identifiants existants.

Ce que fait laravel/mcp, et ce qu'il ne fait pas

Rien de tout cela n'est une critique de laravel/mcp. Il compte 34,5 millions d'installations, il est officiel, et il fait très bien son travail. Son travail, c'est le protocole.

Ce qu'il ne fait délibérément pas, c'est décider ce que votre outil a le droit de renvoyer. C'est une préoccupation applicative, et il serait malvenu qu'un package de protocole la devine.

C'est aussi une préoccupation dont la forme est identique dans toutes les applications, ce qui en fait un bon candidat pour un package plutôt que quelque chose que chaque équipe réinvente un vendredi à 17 h.

Déclarer l'exposition

Ce que j'ai retenu, c'est un attribut sur le modèle :

#[AgentResource(
    fields: ['id', 'subject', 'status'],
    searchable: ['subject'],
    filterable: ['status'],
    relations: ['comments', 'author'],
    maxResults: 25,
)]
class Ticket extends Model {}
Enter fullscreen mode Exit fullscreen mode

C'est toute la configuration, et chaque argument ferme une des trois failles.

fields est une liste blanche. La projection travaille depuis cette liste seule et n'inspecte jamais le modèle pour décider quoi inclure, donc ajouter une colonne ne peut pas élargir l'exposition.

maxResults est un plafond appliqué par-dessus ce que demande l'agent, avec un second plafond global en configuration.

L'autorisation passe par vos policies existantes, enregistrement par enregistrement.

Ce que je défends le plus

Chacun de ces mécanismes échoue en se fermant.

Un modèle sans policy lève une exception. Oublier d'écrire une policy est l'erreur la plus probable de tout le processus, donc le comportement par défaut doit être le refus.

Un get refusé est indiscernable d'un enregistrement inexistant. Les deux renvoient null, donc l'outil ne peut pas servir à découvrir quels identifiants existent.

Les lignes refusées dans une liste sont signalées, pas supprimées. Si un outil retire silencieusement des lignes, l'agent croit la liste complète, puis répond avec assurance à une question à son sujet. Le compte revient donc :

['rows' => [...], 'denied' => 2, 'truncated' => false]
Enter fullscreen mode Exit fullscreen mode

Ne pas écrire les outils du tout

Puisque l'attribut décrit déjà tout ce dont un outil a besoin, les outils sont générés :

php artisan agent-kit:mcp
Enter fullscreen mode Exit fullscreen mode

Une classe d'outil laravel/mcp par capacité déclarée, avec le schéma d'entrée dérivé du même attribut. Le handler généré délègue à la ressource et ne fait rien d'autre, ce que le fichier généré indique en en-tête : toutes les garanties vivent dans la ressource, et tout ce qu'on ajoute à un outil s'exécute en dehors d'elles.

composer require catidegla/laravel-agent-kit
Enter fullscreen mode Exit fullscreen mode

58 tests sur PHP 8.2, 8.3 et 8.4, dont un qui charge une classe générée et la fait passer par le sérialiseur de laravel/mcp. Le dépôt est ici.

Top comments (0)