Plugins
Le système de plugins de ZeroClaw vous permet d’ajouter des capacités à l’agent sans toucher au binaire principal. Cette page explique le choix technologique : de quoi se compose un plugin, pourquoi il s’agit de WebAssembly, et comment l’hôte maintient confiné un composant non digne de confiance. Les guides ci-dessous détaillent la construction de chaque type de plugin, en devenant de plus en plus techniques au fur et à mesure.
- Écrire un plugin d’outil: un outil appelable que le modèle peut invoquer. Commencez ici ; c’est le parcours complet d’une crate vide à un outil installé.
- Écrire un plugin de canal : une intégration de plateforme de messagerie avec la surface complète des drapeaux de capacité.
- Écrire un plugin de mémoire : un backend de stockage implémentant le rappel attribué à l’agent.
- Distribution de plugins : signature, registres et sécurité d’installation.
Les bundles de compétences uniquement en Markdown ne sont pas des plugins, mais ils transitent par les mêmes mécanismes de manifeste, de signature et d’installation ; cette page fait partie de la documentation Skills.
Pour la vue de l’opérateur sur la découverte, la politique de signature et la configuration, voir Comment fonctionnent les plugins. Pour la référence du contrat normatif, voir Protocole des plugins.
Pourquoi WebAssembly
Un plugin exécute du code tiers arbitraire dans un processus qui contient vos clés API, l’historique de vos conversations et un accès shell. La limite d’isolation doit être effective et non théorique. ZeroClaw utilise le WASI Component Model sur wasmtime car il offre quatre propriétés qu’aucun schéma basé sur des bibliothèques dynamiques ou des sous-processus ne satisfait simultanément :
- Sandboxing basé sur les capacités. Un composant WebAssembly ne dispose d’aucune autorité ambiante. Il ne peut pas ouvrir de fichiers, de sockets ou de variables d’environnement, sauf si l’hôte intègre explicitement cette capacité dans son lieur. L’hôte de ZeroClaw configure chaque magasin de plugins avec un contexte WASI qui ne comporte aucune pré-ouverture système de fichiers et aucun accès réseau (
PluginStatedanscrates/zeroclaw-plugins/src/component.rs). Ce qu’un plugin peut atteindre correspond exactement à l’ensemble des imports hôte déclarés par son monde, plus les permissions ajoutées par son manifeste, et rien d’autre. - Exécution avec comptage. Le moteur est compilé avec le comptage du fuel activé, et chaque appel reçoit un nouveau budget de fuel ainsi qu’une échéance d’horloge murale qui inclut le travail côté hôte en attente. Un plugin qui boucle indéfiniment ou attend indéfiniment échoue ; il ne peut pas bloquer l’agent. Les plafonds de mémoire, de tables et d’instances sont appliqués par un limiteur de store. Les cinq limites proviennent de la configuration de l’opérateur (
plugins.limits.*) et sont validées comme étant non nulles, et un store ne peut pas être construit sans elles ; aucun chemin de chargement ne peut donc produire un plugin sans bac à sable. - Un ABI typé et indépendant du langage. Le contrat entre l’hôte et le plugin est un ensemble de fichiers d’interface WIT (
wit/v0/dans le dépôt ZeroClaw), et non une API Rust. L’hôte génère ses liaisons à partir de ces fichiers avec lebindgen!de wasmtime ; un plugin génère les liaisons invité en miroir avecwit-bindgenen Rust ou l’outil équivalent dans tout langage compilant vers un composantwasm32-wasip2. Les typesrecords,variants,resultsetoptionstraversent la limite en conservant leurs types intacts. - Comportement identique aux composants intégrés. Chaque type de plugin est adapté sur le même trait Rust utilisé par les implémentations de premier parti : un plugin d’outil devient un
Tool(wasm_tool.rs), un plugin de canal unChannel(wasm_channel.rs), un plugin de mémoire unMemory(wasm_memory.rs). La boucle agent, l’attribution, les reçus et la politique de sécurité ne font aucune différence.
Les pièces
Un plugin sur disque est un répertoire contenant un manifeste et un composant compilé :
~/.zeroclaw/plugins/
└── my-plugin/
├── manifest.toml # identity, capabilities, permissions, signature
└── my-plugin.wasm # wasm32-wasip2 component
Le manifeste déclare deux choses orthogonales :
- Capacités : ce que le plugin est. Une ou plusieurs parmi
tool,channel,memory,observer,skill(l’énumérationPluginCapabilitydanscrates/zeroclaw-plugins/src/lib.rs). Chaque capacité WASM sélectionne le monde WIT que le composant doit exporter. La capacitéskillfait figure d’exception : elle indique un skill bundle en markdown qui s’appuie sur le mécanisme d’installation, et non du code, et ne nécessite aucun composant. - Autorisations : les services hôtes que le code du plugin peut atteindre. L’énumération
PluginPermissiondans le même fichier. Actuellement,config_read(les adaptateurs d’outils et de canaux reçoivent leur propre configuration publique issue du schéma et validée, et peuvent résoudre les secrets désignés par le schéma lors d’appels de services autorisés) ethttp_clientont un effet comportemental. L’autorisation HTTP est l’autorisation nécessaire pour les adaptateurs qui implémententwasi:httpen sortie : les outils et les canaux activent cette interface, tandis que la mémoire ne le fait volontairement pas encore.config_readdoit être associé auconfig_schemadu manifeste ; l’un ou l’autre sans l’autre est rejeté. Les autorisations d’accès au système de fichiers et à la mémoire sont acceptées par le schéma, mais ne sont pas encore prises en charge par des fonctions hôtes ; leur déclaration n’accorde donc aucun accès.
Les mondes
wit/v0/ définit un monde par capacité WASM. Chaque monde importe l’interface logging de l’hôte, dont les événements log-record arrivent dans le journal structuré en portant l’attribution de span du site d’appel de l’hôte, et exporte plugin-info (nom et version auto-déclarés) ainsi que son interface principale :
| Monde | Exportations | Cycle de vie du store |
|---|---|---|
tool-plugin | tool : nom, description, schéma-des-paramètres, exécuter | Nouveau stockage pour chaque execute ; imports de secrets limités à la portée |
channel-plugin | channel : configure, send, poll-message, plus 22 méthodes soumises à des capacités | Stockage maintenu à chaud derrière un mutex asynchrone, réalimenté à chaque appel ; imports limités à config, secrets et à inbound fourni par l’hôte |
memory-plugin | memory: store, recall, get, forget, plus 11 méthodes contrôlées par des capacités | Magasin chaud derrière un mutex asynchrone, ravitaillé à chaque appel |
Les mondes channel et memory utilisent des drapeaux de capacités : un masque de bits que l’hôte lit une seule fois au chargement (get-channel-capabilities / get-memory-capabilities). Pour chaque drapeau non défini, l’hôte utilise la valeur par défaut du trait Rust et n’appelle jamais l’export du plugin. C’est ainsi que le contrat WIT reste additif : une nouvelle méthode optionnelle est un nouveau drapeau plus une nouvelle fonction, jamais une rupture.
Modèle d’exécution
L’hôte (crates/zeroclaw-plugins/src/component.rs) possède un wasmtime::Engine asynchrone pour le processus. Le chargement dépend du backend : un build avec le JIT Cranelift compile le .wasm au chargement ; un build runtime-only désérialise un .cwasm précompilé. Chaque instanciation de plugin obtient :
- un
Storecontenant le contexte WASI sandboxé, la table de ressources, le contexte HTTP optionnel et le budget de carburant ; - un
Linkeravec exactement les imports que son world, ses grants et la prise en charge des adaptateurs appellent :loggingtoujours,secretspour les outils et les canaux,configetinboundpour les canaux, etwasi:httppour les adaptateurs d’outils et de canaux uniquement lorsque le manifeste accordehttp_client. Memory ne crée ni contexte HTTP ni linker HTTP. Chaque adaptateur vérifie la cohérence de son contexte et de son linker lors de l’instanciation (ensure_http_coherent).
Les appels d’outils sont sans état par construction : WasmTool::execute crée un store vierge, exécute l’appel, puis le libère. Les canaux et les backends mémoire sont par nature dotés d’un état ; ils conservent donc un store persistant pendant toute la durée de vie du plugin. L’hôte le réapprovisionne en carburant avant chaque appel afin qu’un plugin à longue durée de vie dispose d’un budget complet à chaque appel plutôt que de voir ce budget s’épuiser au fil du temps. Une interruption liée à l’expiration du délai détruit le store persistant au lieu de reprendre un état invité partiellement déroulé. Les canaux recréent l’instance lors de l’appel suivant ; la mémoire reste indisponible jusqu’à ce que son propriétaire la reconstruise. Lors d’un appel de canal autorisé, config.get et secrets.get matérialisent au plus une révision de la configuration canonique de cette instance admise. L’hôte libère la vue à la fin de l’appel. Un plugin de canal conforme doit résoudre les deux à chaque point d’utilisation et ne doit pas conserver la configuration renvoyée ni le secret en clair dans l’état invité persistant. L’hôte ne peut pas imposer la non-conservation après avoir renvoyé les données au code invité de confiance.
La frontière est en 32 bits : wasm32-wasip2 est la seule cible WASI Preview 2 fournie par la chaîne d’outils Rust, et l’ABI des composants abaisse les offsets en 32 bits indépendamment de la taille de mot de l’hôte. Les grandes valeurs (les octets d’une pièce jointe de canal) passent par valeur. Voir la page du protocole pour comprendre pourquoi c’est une contrainte amont.
État actuel du câblage
Soyez conscient de ce qui est enregistré de bout en bout par rapport à ce qui est host-complete mais encore inaccessible depuis un daemon en cours d’exécution :
| Capacité | Adaptateur hôte | Câblage d’exécution |
|---|---|---|
tool | WasmTool | Enregistré de bout en bout ; les plugins d’outils découverts apparaissent dans l’ensemble d’outils de l’agent |
skill | chargeur Markdown | Enregistré de bout en bout ; les skills se chargent sous l’espace de noms plugin:<plugin>/<skill> |
channel | WasmChannel, complet et couvert par des tests unitaires | La construction et la résolution de la configuration à l’exécution gérées par l’alias ont été intégrées (#10146) ; l’écouteur hôte propre à chaque fournisseur, qui achemine chaque transport vers la file inbound du canal, fera l’objet d’un suivi. |
memory | WasmMemory, implémente le trait Memory complet | Le runtime ne le construit pas encore en tant que backend configurable |
observer | aucun | PluginCapability::Observer est réservé ; aucun monde WIT ou adaptateur n’existe encore |
Configuration
Les paramètres statiques de l’hôte de plugins utilisent le même miroir de schéma que tout le reste. Les valeurs par instance utilisent actuellement le TOML générique ou zeroclaw config set ; le schéma du manifeste du plugin n’est pas encore rendu sous forme de zerocode ou de formulaire de passerelle. Soyez prudent lors des modifications manuelles : une erreur de syntaxe dans une section (par exemple [plugins.entries] au lieu de [[plugins.entries]]) fait actuellement échouer la désérialisation de toute la section [plugins] et entraîne silencieusement un retour aux valeurs par défaut, qui sont alors relues comme plugins.enabled = false sans aucun avertissement (suivi dans l’issue #8636). Opérations courantes :
# allumer le système
zeroclaw config set plugins.enabled true
# charger les plugins d’outils et de compétences découverts automatiquement à l’exécution (par défaut : false)
zeroclaw config set plugins.auto_discover true
# répertoire où les plugins sont recherchés (par défaut : ~/.zeroclaw/plugins)
zeroclaw config set plugins.plugins_dir /srv/zeroclaw/plugins
# politique de signature: disabled | permissive | strict
zeroclaw config set plugins.security.signature_mode strict
# limites du sandbox par appel
zeroclaw config set plugins.limits.call_fuel 1000000000
zeroclaw config set plugins.limits.call_timeout_ms 30000
zeroclaw config set plugins.limits.max_memory_mb 256
plugins.enabled = true active l’hôte de plugins, mais les capacités d’outils et de compétences découvertes automatiquement ne sont chargées que si plugins.auto_discover = true l’est également. Cet indicateur vaut false par défaut (avec fermeture en cas d’échec), de sorte que enabled = true seul vous fournit les canaux que vous déclarez sous [channels.plugin.<alias>], mais aucun outil ni aucune compétence de plugin : un package d’outil ou de compétence peut être listé et fournir des informations correctes via info, tout en ne contribuant en rien à l’exécution. Les liaisons de canaux explicites sont nommées par l’opérateur plutôt que découvertes automatiquement ; elles n’ont donc pas besoin de auto_discover ; l’indicateur ne contrôle que les outils et compétences découverts automatiquement.
Les paramètres par instance se trouvent sous plugins.entries, indexés par une chaîne versionnée zpi1_… dérivée de l’identité du package, de la capacité et de la liaison, qui est gérée par l’hôte. L’installation affiche et initialise les clés pour la liaison d’outil par défaut du package ; zeroclaw plugin info <package> réaffiche cette clé d’outil. Ces surfaces automatiques sont réservées aux outils. La construction de canaux associés à un alias dérive la clé de l’alias effectivement configuré plutôt que d’inventer une liaison basée sur le nom du package. Cette voie d’exécution a été intégrée dans #10146 : un démon construit désormais une instance [channels.plugin.<alias>] déclarée explicitement et résout sa configuration typée à partir de cet alias. L’affichage automatique de la clé par plugin info et l’initialisation des clés lors de l’installation pour les instances de canal doivent encore être effectués manuellement jusqu’à la cérémonie d’octroi dans #9584. Les clés à identité complète permettent à différents packages et univers de capacités de réutiliser en toute sécurité des alias tels que main sans partager d’identifiants. Les valeurs canoniques de l’opérateur constituent une mappe de chaînes marquée comme secrète et restent chiffrées au repos (enc2:…). Un plugin qui demande config_read déclare l’unique contrat de type de la mappe dans config_schema : un objet fermé Draft 2020-12 dont les propriétés de premier niveau utilisent explicitement les types string, boolean, integer, number, array ou object. Un consommateur côté outil ou canal peut définir x-secret = true sur une propriété de type chaîne de premier niveau ; l’hôte la valide avec l’objet complet, la retire de la configuration publique et en rend la valeur disponible uniquement via l’import secrets.get de l’instance autorisée. Les outils peuvent lire les secrets lors de execute ; les canaux obtiennent la configuration publique via config.get et les secrets via secrets.get lors de configure et des appels opérationnels. Sans l’autorisation effective pour config_read, chacun des deux imports renvoie access-denied ; l’instanciation, la découverte des métadonnées statiques, l’échec de résolution et l’épuisement du budget d’appels à l’hôte renvoient unavailable. Stockez directement les chaînes, les booléens et les nombres sous forme de texte scalaire JSON, et les tableaux et objets sous forme de texte JSON. L’hôte matérialise et valide l’objet typé résultant avant d’utiliser le code invité de l’outil ou du canal ; les valeurs inconnues, malformées ou hors limites sont rejetées au lieu d’atteindre le plugin. Les plugins de mémoire ne disposent pas encore d’un import de configuration et ne doivent pas demander config_read tant que cette ABI n’a pas été ajoutée.
Les auteurs de plugins antérieurs à la version 1.0 doivent effectuer explicitement la migration : un manifeste qui demande config_read sans config_schema n’est plus détecté. Ajoutez un schéma fermé correspondant aux valeurs actuelles, mettez à jour les composants invités des outils/canaux afin qu’ils désérialisent du JSON typé plutôt qu’une table de chaînes, reconstruisez et signez de nouveau, car le schéma est couvert par la signature. Les intégrations hôtes injectent PluginHostServices, qui encapsule un PluginConfigResolver, au lieu d’une table de configuration détenue localement. Chaque trame autorisée d’outil ou de canal matérialise au plus une ResolvedPluginConfig associée à une portée, utilise cette vue pour chaque lecture de configuration dans la trame, puis la libère à la fin de celle-ci. Un canal appelle config.get et secrets.get au point d’utilisation ; ainsi, une configuration publique et une rotation des identifiants au sein de la même liaison logique apparaissent comme une seule révision lors de l’opération suivante. Les exports statiques d’identité et de capacités sont lus une seule fois au chargement ; modifier l’identité du bot/compte ou d’autres métadonnées statiques nécessite de reconstruire le cycle de vie du canal. Migration vers une configuration typée est la procédure détaillée étape par étape, y compris la décision de mise en production d’appliquer cette exigence sans couche de compatibilité.
Il s’agit d’un format de clé strict antérieur à la version 1.0 : les entrées héritées nommées uniquement d’après un paquet ou une liaison ne sont pas prises en compte. Pour un paquet d’outil existant, exécutez zeroclaw plugin info <package> pour obtenir sa clé d’instance complète, renommez l’ancienne entrée avec cette clé, puis enregistrez la configuration. Les nouvelles installations d’outils la renseignent automatiquement.
Les autorisations effectives sont vérifiées séparément des demandes du manifeste. Si config_read est refusé, l’hôte valide un objet vide. Les propriétés obligatoires font échouer le démarrage en mode fermé. Lorsque l’objet vide est valide, un outil omet la clé __config vide et les importations de configuration/secrets du canal renvoient access-denied. La liste canonique des champs de l’hôte et les valeurs par défaut figurent dans la référence de configuration ; zeroclaw config list affiche les valeurs actuellement stockées.
Où se trouve réellement la frontière de confiance
Le bac à sable limite ce qu’un plugin chargé peut faire ; la politique de signature limite ce qui se charge tout court. Les deux sont des décisions de l’opérateur, et elles se composent :
plugins.enabledfalse (par défaut) : aucun code de plugin ne s’exécute jamais.plugins.auto_discoverfalse (la valeur par défaut) : les fonctionnalités d’outils et de compétences découvertes automatiquement ne sont pas chargées.plugins.enabled = trueà lui seul active uniquement les canaux que vous déclarez sous[channels.plugin.<alias>]; les outils et les compétences ne sont chargés que lorsqueauto_discover = trueest également défini.- Signature
strict: seuls les composants dont le manifeste porte une signature Ed25519 valide d’une clé de votre ensemble de confiance se chargent. - Plugin chargé : limité par le fuel, les plafonds de mémoire, le WASI sans pré-ouverture et l’ensemble des imports soumis au contrôle des permissions.
Ce que le bac à sable ne limite pas, c’est le comportement sémantique d’un outil que le modèle choisit d’appeler : un outil disposant de l’autorisation http_client et la surface HTTP de l’adaptateur d’outil peuvent envoyer tout ce que le modèle leur transmet vers l’endroit que leur code décide. La politique de signature existe parce que « quel code vais-je charger » est la décision qui compte le plus ; prenez-la de manière délibérée.