Comment les plugins fonctionnent
Cette page explique le système de plugins du point de vue de l’opérateur : comment un plugin est découvert, ce qu’il est autorisé à faire, et comment l’hôte confine un plugin non fiable. Pour le contrat sur disque qu’un auteur de plugin implémente (champs du manifeste, exports du bridge, fonctions hôte), voir Protocole de plugin.
La forme du système
Un plugin est un module WebAssembly sandboxé et un manifeste. L’hôte le charge, lit les capacités et les permissions qu’il déclare, et expose ses outils à l’agent uniquement lorsque l’opérateur a activé le système de plugins. Rien dans un plugin n’est implicite : un plugin ne dispose que des capacités déclarées dans son manifeste et autorisées par la politique de l’opérateur, et rien d’autre. Pour le développer vous-même, commencez par les guides des plugins.
Trois propriétés s’appliquent à chaque couche :
- Désactivé par défaut. Le système de plugins ne charge rien tant que
[plugins] enabled = truen’est pas activé. Une build par défaut sans configuration de plugins n’exécute aucun code de plugin. - Refuser par défaut. Un plugin accède à une capacité hôte (sortie HTTP, configuration, mémoire) uniquement en déclarant la permission correspondante dans son manifeste. Une capacité non déclarée est inaccessible, pas simplement inutilisée.
- Vérifié par la politique. Le fait qu’un plugin non signé ou non fiable soit chargé ou non relève de la décision de l’opérateur, définie une seule fois dans la configuration et appliquée uniformément lors de la découverte.
Cycle de vie du chargement d’un plugin
Lorsque le runtime construit son ensemble d’outils, le chargeur de plugins parcourt ces étapes dans l’ordre. Un plugin qui échoue à une étape antérieure n’atteint jamais une étape ultérieure.
- Gate. Si
`[plugins] enabled`estfalse, le chargeur ne fait rien. Il s’agit de la première vérification, et la plus légère. - Découverte. Le chargeur analyse le répertoire de plugins résolu (
[plugins] plugins_dir, par défaut~/.zeroclaw/plugins/) à la recherche de sous-répertoires contenant unmanifest.toml. Il ignore les répertoires dont le nom commence par un point, y compris le répertoire de préparation caché dans lequelzeroclaw plugin installconstruit un paquet avant de le renommer à son emplacement final, afin qu’une installation incomplète ne soit jamais chargée. - Valider la structure. Chaque manifeste doit déclarer au moins une capacité, et un plugin qui n’est pas une compétence doit désigner un
wasm_pathrelatif et confiné. Les chemins contenant une traversée ou un lien symbolique sont rejetés. Un manifeste mal formé est ignoré avec un avertissement, et n’est jamais chargé. - Appliquer la politique de signature. Chaque plugin est vérifié par rapport à la
[plugins.security] signature_modeettrusted_publisher_keysconfigurées. Un plugin ne respectant pas la politique est exclu de l’ensemble chargé et n’est pas exposé en tant qu’outil. - Admettre les octets exécutables. L’hôte ouvre le composant confiné une seule fois, vérifie tout
wasm_sha256déclaré et conserve exactement ces octets. En modestrict, le manifeste signé doit déclarer cette empreinte. Les adaptateurs compilent le tampon admis au lieu de rouvrir son chemin. Sous Unix, l’hôte ouvre le répertoire du paquet une seule fois et atteint le composant à partir de ce descripteur, un composant de chemin à la fois, sans jamais suivre un lien symbolique. Ainsi, déplacer ou remplacer un répertoire pendant l’admission soit fait échouer celle-ci, soit laisse la lecture à l’intérieur du paquet confiné. Le composant doit être un fichier ordinaire, et un FIFO ou un périphérique échoue immédiatement au lieu de bloquer. Le répertoire du paquet et le manifeste sont toujours retrouvés par leur chemin au démarrage de l’admission ; en modestrict, c’est lewasm_sha256signé qui lie le manifeste aux octets. Les autres plateformes conservent des vérifications de chemin, qui refusent un remplacement encore en place au moment de leur exécution, mais elles ne rendent pas la recherche atomique et n’empêchent pas un FIFO de bloquer l’ouverture. - Enregistrer les outils. Les plugins d’outils retenus sont encapsulés comme outils de l’agent et ajoutés après les outils intégrés. Un plugin dont le nom de package ou le nom d’outil entre en conflit avec un outil déjà enregistré est refusé avec un avertissement au lieu d’être enregistré ; voir Conflits de noms d’outils. Les plugins d’outils et de compétences sont auto-découverts, de sorte que cette énumération n’a lieu que si
[plugins] auto_discover = true(valeur par défautfalse, fail-closed) : avecenabled = truemaisauto_discover = false, aucun outil ni compétence de plugin n’est chargé, bien que les canaux que vous déclarez sous[channels.plugin.<alias>], manuellement ou aveczeroclaw plugin bind, restent activés. Le chargeur de compétences applique la même conditionauto_discover.
L’étape de signature est celle qui est le plus facilement mal configurée, il vaut donc la peine de la comprendre isolément.
Récupération des installations héritées
Exécutez zeroclaw plugin migrate pour déplacer les répertoires de paquets hérités vers [plugins] plugins_dir. La récupération automatique recherche le dossier plugins/ de la racine de données configurée, le dossier <install>/workspace/plugins non séparé, ainsi que la destination <install>/data/plugins réellement produite par V2, même si le champ data diffère. Les nouvelles séparations du système de fichiers V2 déplacent les plugins vers les données de l’hôte, de sorte que les séparations nouvelles et reprises restent accessibles sans utiliser l’espace de travail inscriptible actif de l’agent par défaut comme source de récupération. Les données de l’hôte ne correspondent pas nécessairement au répertoire de plugins d’exécution configuré ; plugin migrate effectue ce déplacement ultérieur. La cible configurée et ses alias canoniques sont exclus, et les racines en double ne sont signalées qu’une seule fois.
Les alias pris en charge de ces racines héritées sont suivis. Les alias cibles canoniques sont exclus, et les alias de même racine sont dédupliqués. Une racine qui chevauche un espace de travail d’agent connu est exclue, qu’elle se trouve à l’intérieur de cet espace de travail ou qu’elle le contienne, y compris pour ses alias. Les rôles connus proviennent du chemin réel de l’espace de travail de chaque agent configuré, de la valeur par défaut implicite et de l’espace de travail par défaut historique. Les chemins existants sont comparés de manière canonique ; les espaces de travail déclarés mais absents sont comparés de façon prudente, sous forme de chemins lexicalement normalisés, en résolvant un alias d’ancêtre existant lorsqu’il est disponible. La sélection ne crée pas d’espace de travail manquant et ne considère pas le chemin déclaré comme une preuve d’une ancienne migration.
Ceci concerne la sélection de la racine : le déplaceur existant réanalyse une racine éligible avec ses propres vérifications de paquets. La suggestion de récupération et le déplaceur exigent tous deux un répertoire de paquet enfant ordinaire, conformément à la découverte à l’exécution. Les liens symboliques de répertoires enfants restent à la source : ils ne sont ni déplacés ni matérialisés, même dans une racine contenant des paquets ordinaires. Un alias de manifeste situé dans un répertoire de paquet ordinaire continue d’utiliser les vérifications existantes de chargement et d’admission du manifeste.
Cela ne crée pas de garantie globale de non-suivi des liens symboliques, ni d’instantané authentifié, ni de nouveau contrat de non-écrasement concurrent. Sa vérification existante de destination portant le même nom laisse les paquets en conflit à la source, afin que l’opérateur les résolve puis relance l’opération.
L’assistant de migration de répertoires ne modifie ni la configuration des plugins, ni les autorisations, ni la confiance accordée aux éditeurs, ni l’activation. Le chargement de la configuration avant la commande conserve ses effets normaux d’initialisation et de migration. Le déplacement des paquets les rend éligibles à la découverte normale : les plugins d’outils et de compétences peuvent devenir disponibles lorsque les plugins sont activés et que auto_discover = true, sous réserve d’admission. Le déplacement lui-même ne vérifie ni la compatibilité ABI ni l’authenticité de l’origine.
Paquets déjà répartis dans un espace de travail d’agent
Les versions antérieures pouvaient déplacer des paquets vers <install>/agents/default/workspace/plugins. Cet emplacement historique et un espace de travail d’agent personnalisé en cours d’utilisation ne sont plus des sources automatiques pour plugin migrate. Un manifeste, un nom de sauvegarde correspondant ou un marqueur écrit par l’appelant n’autorise pas l’adoption de l’ensemble d’un répertoire. Inspectez chaque paquet que vous souhaitez adopter, puis sélectionnez son répertoire individuel avec la commande d’installation existante :
zeroclaw --config-dir '/operator/install' plugin install \
'/operator/install/agents/default/workspace/plugins/package-name'
Utilisez les chemins réels de votre installation et de vos paquets ; ne transmettez pas le répertoire plugins/ entier. Un paquet présent dans une sauvegarde peut de même être sélectionné explicitement par son chemin réel. L’action d’installation de l’opérateur confère l’autorité, et non l’historique apparent du chemin. Cela nécessite une build prenant en charge la CLI des plugins.
Il s’agit d’une nouvelle installation d’un paquet compatible. L’hôte applique les règles actuelles de manifeste, de signature, de charge utile et de schéma de configuration, et la CLI vérifie par défaut, au chargement, les paquets exécutables par rapport à son ABI WIT et à ses limites. Les paquets non signés ou non fiables sous une politique stricte, les anciens manifestes incomplets, les composants incompatibles ou les lignes de configuration héritées non prises en charge peuvent être refusés ; suivez le diagnostic et le guide de migration vers la configuration typée avant de réessayer. Aucune conversion de formats historiques ni contournement de la politique de signature n’est fourni. Les paquets homonymes déjà installés sont refusés plutôt que remplacés silencieusement.
L’installation explicite conserve le répertoire source. Elle écrit le manifeste admis et la charge utile exécutable, et copie le contenu déclaré des compétences. Les fichiers auxiliaires arbitraires ne constituent pas un contrat de récupération complète du répertoire, et la copie des compétences n’est pas un instantané immuable de génération. Pour les liaisons d’outils nécessitant un état détenu par l’hôte, l’installation peut créer les lignes de configuration typées manquantes et y injecter les destinations de sortie HTTP déclarées. Les lignes et les autorisations existantes ne sont pas étendues automatiquement, et les anciennes lignes identifiées par le nom de paquet ne sont pas converties silencieusement. Examinez ces effets d’installation et configurez les valeurs requises avant d’activer l’utilisation à l’exécution. L’adoption explicite est donc distincte du comportement inchangé de la configuration et des autorisations de l’assistant de migration automatique.
Politique de signature
Chaque manifeste de plugin peut comporter une signature Ed25519 ainsi que la clé publique encodée en hexadécimal de l’éditeur qui l’a signée. L’opérateur décide de la rigueur avec laquelle cette signature est exigée via [plugins.security] signature_mode :
| Mode | Ce qui se charge | Utiliser lorsque |
|---|---|---|
disabled | Tout plugin bien formé, signé ou non | Développement local sur les plugins que vous avez vous-même créés |
permissive | Tous les plugins bien formés, non signés, non fiables et à signatures invalides se chargent avec un avertissement. | Migration vers la signature sans casser les installations existantes |
strict | Seuls les plugins avec une signature valide d’un éditeur de confiance se chargent | Tout hôte partagé ou de production |
En mode strict, le publisher_key du manifeste doit figurer dans [plugins.security] trusted_publisher_keys, et la signature doit être vérifiée sur les octets canoniques du manifeste. Les plugins exécutables doivent également déclarer un wasm_sha256 signé correspondant aux octets exacts admis. Un plugin qui échoue à l’une de ces vérifications est écarté lors de la découverte et ne devient jamais un outil. La valeur par défaut est disabled, afin qu’une copie locale fraîche fonctionne sans gestion de clés, mais un hôte qui charge des plugins depuis des sources que vous ne contrôlez pas doit utiliser strict.
Cette politique est appliquée de manière uniforme : le même contrôle que l’hôte applique lorsque vous listez les plugins est le contrôle que le runtime de l’agent applique lorsqu’il construit l’ensemble d’outils, de sorte qu’un plugin que vous ne pouvez pas voir en mode strict est également un plugin que l’agent ne peut pas appeler.
Conflits de noms d’outils
Le enregistrement refuse un conflit de noms au lieu de laisser un outil en masquer un autre. Les plugins d’outils s’enregistrent dans l’ordre des noms de paquets, et l’hôte vérifie chacun deux fois :
- Nom du paquet. Un plugin dont le nom de paquet (le
namedu manifeste) correspond à un outil déjà enregistré est refusé avant que son composant ne soit instancié. L’hôte journalise un événementWARNavecerror_keyplugin_package_name_conflictdans sesattributes. - Nom de l’outil. Le composant invité déclare son propre nom d’outil ; l’hôte ne le connaît donc qu’en instanciant le composant pour lire ses métadonnées. Un plugin dont le nom d’outil correspond à un outil déjà enregistré n’est pas enregistré. L’hôte journalise un événement
WARNavecerror_keyplugin_tool_name_conflictdans sesattributes. Un nom d’outil contenant__est refusé même lorsque aucun outil ne le détient encore, car les outils MCP sont nommés<server>__<tool>et s’enregistrent après les outils de plugin. Ce refus porteerror_keyplugin_tool_name_reserved.
Les noms vérifiés sont les outils que la construction du registre a déjà enregistrés, y compris les outils de plugins acceptés plus tôt dans le même passage, ainsi que execute_pipeline lorsque [pipeline] enabled = true. Ce n’est pas une liste fixe de tous les noms intégrés : un outil intégré qu’une construction n’enregistre pas, par exemple parce que sa section de configuration est désactivée, n’est pas réservé dans cette construction. Hormis l’espace de noms MCP, les outils qui rejoignent le registre après l’enregistrement des plugins ne sont pas soumis à cette vérification. Donnez aux paquets et aux outils de plugins des noms uniques d’emblée, sans vous fier à cette vérification.
Capacités et permissions
Un manifest déclare deux choses distinctes, et la distinction est importante.
- Les Capacités indiquent le type d’extension du plugin :
tool,channel,memory,observerouskill. Un plugintoolfournit des outils que le LLM peut invoquer. - Autorisations définissent les services de l’hôte auxquels le code du plugin peut accéder à l’exécution : sortie HTTP, configuration, mémoire. Toute autorisation non déclarée dans le manifeste est une fonction de l’hôte que le plugin ne peut pas atteindre.
L’hôte accorde les permissions de manière restrictive : une permission que le manifeste ne déclare pas correspond à une fonction hôte que le plugin ne peut pas atteindre. La configuration est résolue à partir d’une identité d’instance émise par l’hôte ; un plugin ne peut donc pas sélectionner un autre paquet ou une autre liaison et ne lit jamais l’environnement brut du processus. http_client contrôle la surface sortante wasi:http ; la politique d’egress partagée protégée contre les SSRF reste un travail connexe de renforcement des plugins. Cette page couvre la limite de la politique de signature.
Référence de configuration
Tous les paramètres sont définis dans les chemins de configuration plugins.* et sont configurés via n’importe quelle interface de configuration (zerocode, la passerelle ou le CLI) :
# Interrupteur maître. Rien ne se charge tant que cette valeur est false.
zeroclaw config set plugins.enabled true
Charger les plugins d’outils et de compétences découverts automatiquement lors de l’exécution (par défaut : false).
# Without this, `enabled = true` activates only explicitly-declared channels.
zeroclaw config set plugins.auto_discover true
# Emplacement de découverte des plugins (par défaut : ~/.zeroclaw/plugins).
zeroclaw config set plugins.plugins_dir ~/.zeroclaw/plugins
# désactivé | permissif | strict
zeroclaw config set plugins.security.signature_mode strict
# Clés publiques Ed25519 encodées en hexadécimal autorisées à publier des plugins en mode strict.
zeroclaw config set plugins.security.trusted_publisher_keys '["a1b2c3d4e5f6..."]'
Un hôte conçu pour charger des plugins tiers doit définir enabled = true, signature_mode = "strict" et ne répertorier que les clés des éditeurs auxquels vous faites confiance. Pour charger également les plugins d’outils et de compétences détectés automatiquement, définissez aussi auto_discover = true ; cette option est false par défaut, si bien que enabled = true seul n’active que les canaux que vous déclarez sous [channels.plugin.<alias>], sans activer d’outils ni de compétences de plugin. Un hôte qui n’exécute que les plugins que vous développez vous-même peut conserver signature_mode à sa valeur par défaut disabled pendant le développement, puis durcir ce paramètre avant de partager l’hôte.
Une liaison de canal se trouve en dehors de plugins.*, sous [channels.plugin.<alias>]. Vous pouvez écrire cette table à la main, ou laisser zeroclaw plugin bind <package> --channel-alias <alias> l’écrire pour un paquet de canal installé. La commande initialise également la ligne [[plugins.entries]] de l’instance. Elle exige un indicateur explicite --egress declared ou --egress none avant de créer cette ligne à partir d’un manifeste qui déclare des destinations et dispose d’un transport capable de les atteindre (http_client, websocket_client ou socket_client). Elle indique ensuite ce qu’il manque encore à l’instance pour pouvoir démarrer. Elle ne définit jamais plugins.enabled et ne modifie jamais les channels d’un agent. Voir Binding a channel instance.
Exécution de plugins à partir d’un binaire de version
Le système de plugins est expérimental : le contrat wit/v0 est instable, donc un plugin doit être compilé avec le WIT livré avec la version qui l’exécute, et une incompatibilité apparaît lors de la vérification de chargement à l’installation, et non dans le manifeste. Dans cette limite, les archives de version pour les cibles bureau et serveur 64 bits contiennent l’hôte de plugins et le compilateur qui charge les composants .wasm, et chaque version exécute un plugin issu de chacune de ces archives avant publication. Les archives ARM 32 bits, l’archive Android expérimentale et les compilations depuis les sources avec les valeurs par défaut de Cargo ne contiennent aucun hôte de plugins ; la page du protocole répertorie tous les artefacts. Exécuter un plugin sur un binaire de version nécessite quatre décisions de l’opérateur, chacune désactivée par défaut :
- Installer le paquet.
zeroclaw plugin install <directory>pour un paquet local, ouzeroclaw plugin install <name>pour un paquet issu du registre des plugins. L’installation valide le manifeste, applique la politique de signature, vérifie le condensé (digest) de la charge utile lorsque le manifeste en déclare un, et instancie le composant selon le WIT de cet hôte (cette étape est ignorée avec--no-verify). Un paquet qui échoue à l’une de ces étapes n’est pas installé. L’installation (sauf avec--no-verify),zeroclaw plugin info <name>etzeroclaw plugin list --verifycompilent et instancient le composant, que le système de plugins soit activé ou non. L’instanciation n’appelle aucune de ses exports, mais elle exécute la fonction de démarrage (start) éventuellement présente dans les modules cœur du composant, dans un store sans accès aux fichiers ni au réseau, et soumise aux limites par appel de carburant (fuel), de mémoire et de temps. - Activez le système de plugins.
zeroclaw config set plugins.enabled true, puiszeroclaw config set plugins.auto_discover trueafin que les plugins d’outils et de compétences soient énumérés. Un package d’outil ou de compétence installé n’apporte rien tant que les deux options ne sont pas définies. Une fois qu’elles le sont, chaque construction du registre d’outils compile et instancie chaque composant d’outil admis, puis lui demande son nom, sa description et son schéma de paramètres. Il s’agit de code invité exécuté selon les permissions déclarées par le package et sa politique de sortie réseau (egress), avant toute approbation, puis l’outil est proposé au modèle. Un package de canal nécessiteplugins.enabled, une déclaration[channels.plugin.<alias>]et un agent activé qui listeplugin.<alias>parmi ses canaux ;auto_discovern’intervient pas dans ce cas. Pour un package qui reproduit un canal natif viaprovides, l’activation sous[channels.<provides>.<alias>]est enregistrée mais n’est pas encore construite ; le même package lié sous[channels.plugin.<alias>]s’exécute. - Approuver chaque appel d’outil. Un appel à un outil de plugin nécessite l’approbation de l’opérateur, comme tout autre outil qui n’est pas pré-approuvé : la CLI demande confirmation dans le terminal, et les canaux envoient leur carte d’approbation. Pour permettre à un agent d’appeler un outil de plugin sans supervision, ajoutez l’outil à la liste
auto_approvedu profil de risque de cet agent, par exemplezeroclaw config set risk_profiles.default.auto_approve '["<tool-name>"]'.config setremplace la liste entière : les valeurs par défaut intégrées sont refusionnées dansrisk_profiles.defaultau chargement de la configuration, mais toute entrée que vous avez ajoutée vous-même doit être répétée, et un profil portant un autre nom ne conserve que ce que vous passez.level = "full"et le joker*pré-approuvent chaque outil qui ne figure pas dansalways_ask, et une réponse Always pré-approuve l’outil pour le reste de la session CLI ou du tour de conversation sur le canal, à l’exception des outils dealways_ask, qui demandent confirmation à chaque fois. Sans approbateur, l’appel est refusé et ne s’exécute pas. - Choisissez une politique de signature.
signature_modereste àdisabledpar défaut, afin qu’un plugin que vous avez créé vous-même s’installe sans gestion de clés. En modestrict, seuls les paquets signés avec une clé présente danstrusted_publisher_keysse chargent. Comme le registre officiel publie aujourd’hui des paquets non signés, aucun d’entre eux ne s’installe en modestricttant que leurs éditeurs ne les ont pas signés.
La compilation d’un composant nécessite une mémoire qui devient exécutable après son écriture. Un hôte qui interdit cela, comme une unité systemd avec MemoryDenyWriteExecute=yes (la valeur par défaut du module Nix), refuse tout chargement de plugin, car le compilateur ne peut pas mapper du code en tant qu’exécutable ; le reste du démon n’a pas besoin de ces mappages et continue de fonctionner.
Pour revenir en arrière, supprimez le paquet avec zeroclaw plugin remove <name>, ou définissez plugins.enabled = false pour empêcher le chargement de tout plugin sans le désinstaller. Pour revenir à une version sans l’hôte de plugins, épinglez le binaire avec zeroclaw update --version <previous> --force ; sans --force, le programme de mise à jour refuse de passer à une version antérieure. Le programme de mise à jour résout les hôtes Linux musl vers l’archive glibc ; une installation musl, comme celle provenant d’un gestionnaire de paquets, revient donc à la version précédente en réinstallant l’archive ou le paquet précédent.
Ce qu’un plugin ne peut toujours pas faire
Même avec toutes les autorisations accordées, le sandbox limite un plugin :
- Il s’exécute en tant que module WebAssembly sans accès implicite au processus hôte ou au système de fichiers en dehors de son espace de travail raciné. La sortie réseau est contrôlée par l’autorisation HTTP ; la limite de sortie protégée contre les attaques SSRF est elle-même assurée par les travaux de durcissement du plugin compagnon.
- Un outil de confiance ou un plugin de canal peut lire le texte en clair d’un secret désigné par le schéma via son import
secrets.getlimité à sa portée lors d’un appel de service autorisé. Les outils y accèdent pendantexecute. Les canaux reçoiventconfig.getetsecrets.getpendantconfigureet lors des appels opérationnels ; les lectures effectuées au cours d’un même appel utilisent une révision canonique unique, de sorte qu’une rotation public/secret associée à la même liaison est disponible lors de l’opération suivante. L’instanciation et la découverte des métadonnées statiques ne peuvent utiliser aucun de ces imports. L’hôte empêche l’injection de configuration publique et la sélection inter-instance, mais un import renvoyant du texte en clair ne peut pas empêcher un invité malveillant de conserver ce qu’il lit. Les plugins de canal conformes doivent résoudre la configuration et les identifiants à chaque point d’utilisation. - Il ne peut pas reprendre le nom d’un outil déjà enregistré. L’enregistrement refuse le plugin en conflit au lieu d’enregistrer son outil, dans les limites décrites dans Conflits de noms d’outils.
Les limites du bac à sable et de l’espace de noms s’appliquent quels que soient les efforts du code du plugin. La règle de non-conservation fait en revanche partie du contrat de confiance entre le canal et le plugin, raison pour laquelle l’examen par l’éditeur et la politique de signature restent importants.