Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

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.

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 :

  1. 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 (PluginState dans crates/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.
  2. 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.
  3. 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 le bindgen! de wasmtime ; un plugin génère les liaisons invité en miroir avec wit-bindgen en Rust ou l’outil équivalent dans tout langage compilant vers un composant wasm32-wasip2. Les types records, variants, results et options traversent la limite en conservant leurs types intacts.
  4. 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 un Channel (wasm_channel.rs), un plugin de mémoire un Memory (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ération PluginCapability dans crates/zeroclaw-plugins/src/lib.rs). Chaque capacité WASM sélectionne le monde WIT que le composant doit exporter. La capacité skill fait 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.
  • Permissions : les services hôtes que le code du plugin peut atteindre. L’énumération PluginPermission dans le même fichier. Aujourd’hui, config_read (les adaptateurs d’outil et de canal reçoivent leur propre configuration publique matérialisée et validée selon le schéma, et peuvent résoudre les secrets désignés par le schéma dans les appels de service autorisés) ainsi que http_client, state_read et state_write ont un effet comportemental. La permission HTTP est l’autorisation nécessaire pour les adaptateurs qui implémentent le wasi:http sortant : les adaptateurs d’outil et de canal activent cette surface, tandis que l’adaptateur de mémoire ne le fait volontairement pas encore. Les permissions d’état contrôlent l’accès à l’état durable chiffré appartenant exactement au package, à la capacité et à la liaison (binding). config_read doit être associée au config_schema du manifeste ; l’une sans l’autre est rejetée. Les permissions de système de fichiers et d’accès à la mémoire sont acceptées par le schéma mais ne sont pas encore adossées à des fonctions hôtes, donc les déclarer n’accorde rien.

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 :

MondeExportationsCycle de vie du store
tool-plugintool : nom, description, schéma-des-paramètres, exécuterNouveau magasin par execute ; imports limités à secrets et state durables
channel-pluginchannel : configurer, envoyer, poll-message, ainsi que 23 méthodes soumises à des capacitésMagasin chaud protégé par un mutex asynchrone, réapprovisionné à chaque appel ; imports délimités config, secrets, state durable et inbound alimenté par l’hôte
memory-pluginmemory: store, recall, get, forget, plus 11 méthodes contrôlées par des capacitésMagasin 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.

Vérification que le plugin se charge toujours ici

wit/v0 est expérimental : un plugin compilé avec une copie vendored ayant dérivé de cet hôte compile sans erreur, puis échoue à l’instanciation. zeroclaw plugin install refuse un tel plugin, mais ce contrôle ne peut rien pour un plugin installé avant l’introduction de ce contrôle, installé avec --no-verify, ou laissé en place lors d’une mise à niveau de l’hôte. zeroclaw plugin info <name> exécute toujours la même vérification d’instanciation que celle lancée par le daemon au démarrage, puis affiche le verdict, la chaîne complète des causes wasmtime et l’indication de recompilation ; il se termine avec un code non nul lorsque le plugin ne se charge pas, afin qu’un script puisse s’y adapter. zeroclaw plugin list --verify exécute cette vérification pour chaque paquet installé et annote chaque ligne avec le verdict, ou avec la première ligne de la cause en cas d’échec. zeroclaw plugin list simple est inchangé et ne coûte toujours qu’une lecture de répertoire, car la vérification compile et instancie chaque composant. Un bundle de compétences ne contient aucun composant : il est signalé comme non applicable plutôt que comme échec.

Récupération d’une installation incomplète

zeroclaw plugin install construit un paquet dans une transaction cachée et unique à l’intérieur du répertoire des plugins, et conserve un bail du système d’exploitation jusqu’à la publication. Il déplace le paquet à son emplacement final sans remplacer aucun élément existant, et seulement après l’écriture du manifeste, du composant et de toute arborescence skills/. Si la plateforme ou le système de fichiers ne peut pas fournir un déplacement atomique sans remplacement, comme sur FreeBSD et les autres systèmes Unix autres que Linux, Android et macOS, sur un montage NFS sans prise en charge du déplacement sans remplacement, ou sur Linux antérieur à la version 3.15, l’installation est refusée, supprime sa copie de staging et l’indique explicitement ; réessayer échoue de la même manière. Une écriture échouée ou interrompue ne laisse aucun paquet partiel sous le nom final. La découverte ignore les transactions cachées, et une installation ultérieure utilise un nouveau staging au lieu de remplacer le staging d’un autre processus.

Un déplacement non pris en charge n’est pas une raison de réessayer sur le même système de fichiers non pris en charge. Si le nettoyage échoue également, l’installation signale l’emplacement du nettoyage en échec et exige que cet échec ainsi que la prise en charge des déplacements sûrs soient résolus au préalable. L’emplacement est le répertoire du paquet en cas de vidage partiel ou d’échec de la suppression d’un répertoire vide, ou l’entrée de transaction si son paquet est absent ou a déjà été supprimé. Le message n’affirme pas que la charge utile d’origine est toujours intacte. Les vérifications de nettoyage traitent une racine des plugins ou un verrou de coordination remplacé ou supprimé comme un refus d’espace de noms avant de poursuivre. La transaction cachée signalée appartient à cette installation échouée et ne bloque pas une installation ultérieure : plugin remove ne la nettoie pas tant qu’aucun paquet de ce nom n’existe. Par conséquent, une fois qu’aucune installation n’est en cours, vous pouvez inspecter et supprimer manuellement cette transaction spécifique.

Les versions précédentes écrivaient directement dans le répertoire final : elles le créaient, puis écrivaient manifest.toml en premier. Une installation interrompue pouvait donc laisser un répertoire vide, un manifest.toml tronqué ou un manifest.toml sans son composant. La découverte ignore un tel répertoire, qui n’est donc pas un paquet installé, mais il réserve tout de même le nom. zeroclaw plugin install n’écrase jamais rien qui occupe déjà un nom de paquet : il refuse l’opération et indique zeroclaw plugin remove <name> comme procédure de récupération.

Pour un nom que l’hôte n’a pas chargé, plugin remove réserve la génération de répertoire sélectionnée dans une transaction de récupération cachée et applique les mêmes contrôles d’admission à ces contenus réservés. Elle ne supprime cette génération que dans deux cas :

  • Le répertoire est vide.
  • Il contient un manifest.toml et échoue aux contrôles d’admission de install et de discovery apply, à cause de son propre contenu : un manifeste tronqué ou non analysable, un composant manquant, un composant qui ne correspond pas à son wasm_sha256 déclaré, ou un paquet que cet hôte ne peut pas accepter tel qu’il est écrit, par exemple un composant dépassant la limite de taille d’admission, un config_schema qu’il ne peut pas compiler, ou un ensemble de compétences incomplet. Il ne doit également rien contenir qu’une installation n’écrit jamais. Une installation écrit un paquet sous le nom de son manifeste : d’abord le manifeste, puis uniquement le composant à son wasm_path, sous forme de fichier avec les dossiers intermédiaires, et, pour un plugin de compétences, un dossier skills/ contenant des fichiers et des sous-dossiers. Ainsi, à côté d’un manifeste qui ne s’analyse pas, il ne peut rien y avoir d’autre, et à côté d’un manifeste qui s’analyse, seules ces entrées peuvent exister, sous le nom que ce manifeste donne.

La récupération nettoie également les étapes provisoires manifestement abandonnées, créées par le protocole de bail. Les étapes actives et les répertoires .<name>.installing-<pid> hérités, dont l’état est ambigu, sont conservés, et la commande signale leurs chemins. Un PID dans un nom de fichier ne prouve pas l’abandon. Ces étapes conservées n’empêchent pas une nouvelle installation une fois le paquet final incomplet récupéré. Si aucun paquet final n’existe, plugin remove ne supprime pas les répertoires de préparation, sauf s’il termine une suppression qu’un plugin remove précédent avait commencée.

Les autres occupants sont refusés, et la commande affiche la raison : un lien symbolique ou un fichier portant ce nom, un répertoire qui contient des fichiers mais aucun manifest.toml, un répertoire qui contient des fichiers qu’une installation n’écrit jamais, comme la copie des sources d’un plugin dont le composant n’est pas encore compilé ou un .DS_Store ajouté par un gestionnaire de fichiers, un répertoire dont le manifeste désigne un autre paquet, un répertoire qu’il ne peut ni inspecter ni lister, le répertoire d’où un paquet chargé a été chargé, un paquet que l’admission accepte, et un paquet que cet hôte rejette selon sa politique de signature (non signé, provenant d’un éditeur non approuvé, portant une signature invalide, ou sans wasm_sha256 signé en mode strict). L’admission vérifie la signature avant de lire le composant ; ainsi, en mode strict, un manifest orphelin issu d’un paquet non signé, ou un manifest altéré de sorte que sa signature ne se vérifie plus, est refusé pour sa signature même si son composant est absent.

Un paquet réclamé puis refusé est restauré sans remplacer aucun occupant concurrent, y compris un répertoire vide. La récupération refuse une revendication ou une restauration lorsque la plateforme ou le système de fichiers ne peut pas fournir un déplacement atomique sans remplacement. Si la restauration ne peut pas se terminer, ses octets restent à l’emplacement caché signalé ; le nom d’origine n’est pas forcément intact. Relancer plugin remove restaure une transaction de récupération conservée avant de vérifier à nouveau l’admission. Une suppression que plugin remove avait commencée est plutôt achevée : une fois qu’elle a commencé à supprimer un paquet jugé incomplet, une nouvelle tentative termine cette suppression. Résolvez le conflit de destination ou l’erreur du système de fichiers signalé avant de réessayer ; ne supprimez pas les transactions cachées au seul motif qu’elles ressemblent à des résidus.

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 Store contenant le contexte WASI sandboxé, la table de ressources, le contexte HTTP optionnel et le budget de carburant ;
  • un Linker avec exactement les imports que son world, ses grants et la prise en charge des adaptateurs appellent : logging toujours, secrets pour les outils et les canaux, config et inbound pour les canaux, et wasi:http pour les adaptateurs d’outils et de canaux uniquement lorsque le manifeste accorde http_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 neuf, exécute l’appel, puis le supprime. Les canaux et les backends de mémoire sont par nature avec état ; ils conservent donc un store « chaud » unique pendant toute la durée de vie du plugin. L’hôte le recharge en carburant avant chaque appel, afin qu’un plugin à longue durée de vie dispose d’un budget complet à chaque appel au lieu de s’épuiser avec le temps. Une interruption due à une échéance supprime le store chaud au lieu de reprendre un état invité partiellement déroulé. Les canaux et les backends de mémoire suppriment aussi un store dont l’appel a échoué dans Wasmtime : Wasmtime refuse tout appel ultérieur vers un store après un trap, un carburant épuisé ou un import hôte en échec, et un résultat qu’il n’a pas pu convertir ignore le nettoyage de l’export. Les canaux recréent l’instance lors de l’appel suivant, en attendant lorsque les échecs dépassent le budget de reconstruction. La mémoire reste indisponible tant que son propriétaire ne l’a pas reconstruite, et tout appel atteignant le plugin entre-temps échoue avec la même erreur plugin instance is unavailable. Un plugin de mémoire ne conserve ses entrées que dans sa propre instance ; une instance reconstruite démarre donc vide. Pendant 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 supprime cette 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é chaud. L’hôte ne peut pas faire respecter la non-conservation après avoir renvoyé des 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ôteCâblage d’exécution
toolWasmToolEnregistré de bout en bout ; les plugins d’outils découverts apparaissent dans l’ensemble d’outils de l’agent
skillchargeur MarkdownEnregistré de bout en bout ; les skills se chargent sous l’espace de noms plugin:<plugin>/<skill>
channelWasmChannel, complet et couvert par des tests unitairesLa 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.
memoryWasmMemory, implémente le trait Memory completLe runtime ne le construit pas encore en tant que backend configurable
observeraucunPluginCapability::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.limits.max_connections_per_instance (par défaut 16) limite le nombre de connexions sortantes qu’une instance de plugin logique peut maintenir ouvertes simultanément. Le plafond s’applique par instance, et non par appel : une réponse conserve sa connexion tant que le guest n’a pas consommé le corps ou abandonné la réponse. Ainsi, un outil qui garde seize réponses actives au sein d’une même invocation ne peut pas ouvrir une dix-septième connexion. Des requêtes séquentielles qui consomment ou abandonnent chaque réponse au fur et à mesure n’approchent jamais la limite. Lorsque le plafond est atteint, le guest reçoit l’erreur connection-limit-reached propre à wasi:http, et non un refus d’accès en sortie : la destination a été autorisée, et c’est l’hôte qui signale une ressource qu’il a comptabilisée.

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 propres à chaque instance sont stockés sous plugins.entries, indexés par une chaîne versionnée zpi1_… dérivée de l’identité du paquet, de la capacité et de la liaison, toutes détenues par l’hôte. L’installation affiche et initialise la clé de la liaison d’outil par défaut du paquet. La liaison d’une instance de canal correspond à son alias configuré, et non à un nom de paquet inventé. Sa clé n’existe donc qu’une fois un alias lié : zeroclaw plugin bind <package> --channel-alias <alias> ou zeroclaw plugin install <source> --channel-alias <alias> crée la liaison [channels.plugin.<alias>] et initialise la ligne de cette instance (voir Liaison d’une instance de canal). Un démon résout la configuration typée d’une instance [channels.plugin.<alias>] explicitement déclarée grâce à cette même clé, via le chemin d’exécution introduit dans #10146. zeroclaw plugin info <package> affiche toutes les clés que possède le paquet : celle de sa liaison d’outil et une par alias lié, lorsque la version installée possède un état d’instance et qu’une instance peut être désignée par cet alias. Le rapport de disponibilité d’un alias lié, désignable par une telle instance, suit dans les deux cas. Des clés à identité complète permettent à différents paquets et mondes de capacités de réutiliser sans risque des alias comme main sans partager d’identifiants. Les valeurs canoniques de l’opérateur sont une table de chaînes marquée comme secrète ; elles restent chiffrées au repos (enc2:…). Un plugin qui demande config_read déclare le contrat de type unique de cette table dans config_schema : un objet Draft 2020-12 fermé, dont les propriétés de premier niveau utilisent explicitement string, boolean, integer, number, array ou object. Un consommateur d’outil ou de canal peut définir x-secret = true sur une propriété string de premier niveau ; l’hôte valide l’objet complet, retire cette propriété de la configuration publique et ne la rend accessible que via l’import secrets.get de l’instance admise. Les outils peuvent lire les secrets pendant execute ; les canaux obtiennent la configuration publique via config.get et les secrets via secrets.get pendant configure et lors des appels opérationnels. Sans la permission effective config_read, l’un ou l’autre import 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. Les chaînes sont stockées telles quelles, les scalaires booléens et numériques sous forme de texte JSON scalaire, et les tableaux et objets sous forme de texte JSON. L’hôte matérialise et valide l’objet typé résultant avant de l’utiliser dans le code invité de l’outil ou du canal ; les valeurs inconnues, mal formées ou hors plage échouent au lieu d’atteindre le plugin. Les plugins de mémoire ne disposent pas encore d’import de configuration et ne doivent pas demander config_read tant que cette ABI n’est pas ajoutée.

Les composants d’outil et de canal peuvent demander state_read et/ou state_write pour l’import state. L’hôte, et non l’invité, fournit le paquet admis, la capacité et l’espace de noms de liaison. Les clés logiques portables adressent des valeurs d’octets authentifiées au sein de cette instance exacte. Les lectures renvoient une révision ; les opérations de création, de remplacement et de suppression utilisent des révisions exactes de comparaison et d’échange (CAS). Les lignes durables sont stockées sous forme de texte chiffré enc2: derrière des index aveugles à clé, dans data/plugin-state.db, à l’aide de .secret_key de l’installation. Les quotas fixes, ainsi que toute défaillance de clé, de stockage ou d’intégrité, échouent en mode fermé.

L’état appartient à l’identité de l’instance (nom du paquet, capacité, liaison), c’est-à-dire la même identité qu’utilise son entrée de configuration, et non à un éditeur. zeroclaw plugin update conserve les deux, de même que zeroclaw plugin remove, de sorte qu’une mise à niveau conserve son état, qu’il s’agisse d’une mise à jour ou d’une suppression suivie d’une installation. Cela signifie aussi qu’un paquet différent installé ultérieurement sous le même nom hérite de l’état de cette instance et de ses secrets configurés. La suppression conserve également chaque [channels.plugin.<alias>] qui désigne le paquet, de sorte que le nouveau paquet reste lié à ces alias et à leurs entrées. plugin remove affiche chaque liaison qu’il a laissée en place et chaque entrée conservée qui accorde encore un accès sortant, en indiquant ce qu’il faut supprimer : lorsque la version installée fournit un canal, l’entrée sous la clé de chaque alias lié, tout le reste de ce que cette version possède, et l’entrée de la liaison d’outil par défaut lorsque la version installée possède l’état de l’instance. Avant d’installer un plugin sans rapport sous le nom d’un plugin supprimé, supprimez ses lignes [[plugins.entries]] ainsi que ces liaisons, et considérez que son état est lisible par le nouveau plugin.

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.

Déclarer et accorder l’accès sortant

Le périmètre réseau d’un plugin repose sur deux faits distincts, dont un seul vous appartient :

  • La déclaration est la table [egress] du manifeste (hosts = [...]) : les destinations que l’auteur indique comme nécessaires au plugin. Elle fait partie des octets canoniques du manifeste, donc un manifeste signé la couvre. Elle n’accorde rien. Un composant non signé qui écrit sa propre table [egress] n’atteint toujours rien.
  • La autorisation est plugins.entries.<instance-key>.egress_hosts sur la ligne zpi1_… propre à l’instance, avec l’exception plus restreinte egress_allow_private à côté. Il s’agit de la liste appliquée par l’hôte, lue depuis la configuration active à chaque requête, de sorte que votre modification prend effet sans redémarrer le plugin.

Dans cette version, seule l’autorisation (grant) fait foi. L’hôte vérifie une destination par rapport à egress_hosts et n’exige pas en plus qu’elle figure dans le manifeste. Ainsi, une déclaration n’accorde aucun hôte et ne limite pas une autorisation que vous avez rédigée. Le rôle de la déclaration ici est d’amorcer et de comparer, comme décrit ci-dessous. Restreindre la portée effective à l’intersection de la déclaration et de l’autorisation relève d’un travail de déploiement ultérieur, suivi dans #8850.

Un egress_hosts vide est la valeur par défaut et signifie aucun accès réseau du tout : une permission de transport telle que http_client accorde la surface, ce champ accorde les destinations. Les entrées sont des hôtes exacts (api.example.com, 10.0.0.5) ou des motifs de suffixe explicites (*.example.com, qui correspond aux sous-domaines mais pas au domaine apex, donc listez le domaine apex séparément si nécessaire). Les ports ne font pas partie d’une entrée : accorder un hôte accorde tous les ports de celui-ci, et il n’existe pas de * signifiant « n’importe où ».

egress_hosts est volontairement un élément en clair au même niveau que le dictionnaire chiffré config, et non une clé à l’intérieur de celui-ci. La liste d’autorisation est ce qu’un opérateur audite : elle reste lisible dans le même fichier que celui qui est audité, tandis que les secrets à côté restent enc2:….

Que font install et list ?

zeroclaw plugin install est le seul moment où les deux côtés sont réconciliés pour la liaison d’outil par défaut du paquet, sans que vous ayez à taper quoi que ce soit (la ligne d’une instance de canal est créée lorsque vous liez son alias, avec une décision d’autorisation que vous indiquez sur la ligne de commande, comme décrit dans Liaison d’une instance de canal) :

  • Pour une ligne qu’il crée, il insère la déclaration dans egress_hosts, affiche chaque destination accordée et affiche la commande zeroclaw config set qui permet de modifier ultérieurement cette autorisation.
  • Pour une ligne qui existe déjà (une mise à niveau, une réinstallation ou une ligne que vous avez créée), elle ne modifie rien. Elle affiche à la place la différence : les destinations que le manifeste déclare et que la ligne n’autorise pas, les destinations autorisées par la ligne que le manifeste ne déclare plus, ainsi que la commande exacte qui applique l’ajout. Une mise à jour de paquet ne peut donc pas élargir sa propre portée réseau ; vous appliquez la différence de manière délibérée.
  • Une réinstallation qui trouve une ligne de nom de paquet pré-1.0 non prise en charge refuse avant de créer une ligne canonique et affiche les mêmes étapes de mise à jour ordonnées que plugin list. Comme plugin list, elle signale un refus à l’échelle du déploiement (un security.nat64_prefixes malformé, une valeur plugins.limits.max_connections_per_instance égale à zéro) une seule fois, seule, et n’affiche aucune étape par ligne qui ne pourrait pas prendre effet tant que cela n’est pas corrigé. L’installation échouée annule ses modifications avant d’annoncer quoi que ce soit, et les anciennes valeurs de config, egress_hosts et egress_allow_private restent inchangées jusqu’à ce que vous mettiez à jour la configuration bêta et relanciez l’opération.

La commande affichée contient l’union de l’autorisation existante et de la déclaration, car zeroclaw config set remplace une liste au lieu d’y ajouter des éléments. L’exécuter telle quelle ajoute les destinations déclarées sans supprimer un hôte que vous avez configuré vous-même.

Chaque commande affichée commence aussi par zeroclaw --config-dir '<dir>', qui indique le répertoire de configuration pour lequel elle a été calculée. --config-dir (et la variable ZEROCLAW_CONFIG_DIR qu’il définit) n’affecte que le processus auquel vous le passez ; ainsi, une commande copiée depuis zeroclaw --config-dir /srv/a plugin list agirait sinon sur la configuration que votre shell résout par défaut. La clé de ligne zpi1_… désigne le paquet, la capacité et la liaison, mais pas le profil ; cette commande remplacerait donc la liste d’autorisation d’un autre profil par une liste calculée à partir de celui-ci. Le répertoire, la liste d’hôtes et chaque autre argument portés par une commande (un chemin de configuration désignant une propriété du schéma, un paquet, un alias) sont chacun entre guillemets comme un argument littéral unique ; ainsi, rien de ce que déclare un manifeste ne peut être développé ou substitué par votre shell. Collez la commande telle qu’affichée. La seule exception concerne l’autorisation d’un paquet qui ne déclare aucune destination, affichée sous forme de commande config set --no-interactive pour egress_hosts de la ligne, sans valeur : ajoutez les hôtes utilisés par votre déploiement, sous forme d’une valeur unique séparée par des virgules. Exécutée telle qu’affichée, elle est refusée faute de valeur et n’écrit rien. Une commande plugin bind affichée est également complète. Lorsque l’alias vous appartient de le choisir, la commande porte le mot ALIAS à sa place : votre shell transmet ce mot tel quel et, comme un alias doit être en minuscules, la commande le refuse comme alias invalide et n’écrit rien tant que vous ne le remplacez pas par l’alias de votre choix. Lorsque la création de la ligne de l’instance implique une décision d’egress, deux commandes sont affichées : d’abord celle se terminant par --egress none, puis celle se terminant par --egress declared ; exécutez celle que vous avez choisie. Les coller toutes les deux à la fois laisse l’instance sans accès réseau, car la seconde commande trouve la ligne créée par la première, et une ligne existante n’est jamais étendue.

Le quoting suit le shell de la plateforme sur laquelle la commande a été affichée. Sous Linux et macOS, il s’agit de la forme POSIX entre guillemets simples (sh, bash, zsh, fish), où un guillemet intégré s’écrit '\''. Sous Windows, ZeroClaw ne peut pas savoir si vous êtes dans cmd.exe ou PowerShell. Il affiche donc la seule forme que les deux acceptent littéralement : chaque valeur entre guillemets doubles, qui peut être collée dans l’un ou l’autre shell tant que la valeur ne contient rien que l’un ou l’autre shell interpréterait à l’intérieur de guillemets doubles. Une liste d’hôtes ordinaire et un chemin de profil ordinaire, espaces compris, respectent toujours cette condition. Lorsqu’une valeur ne la respecte pas (elle contient ", %, !, $ ou un backtick, ou se termine par une barre oblique inverse ; $(id).example.com est la forme qu’un manifeste malveillant pourrait déclarer), la ligne entière est affichée à la place, précédée du marqueur # PowerShell only, cmd.exe cannot pass this value literally:, suivi d’un espace et de la forme PowerShell, où un guillemet intégré est doublé (''). Collée telle quelle, cette ligne n’exécute rien dans aucun des deux shells (cmd.exe ne peut pas exécuter #, et PowerShell la lit comme un commentaire) ; copiez la commande située après le marqueur dans PowerShell uniquement. cmd.exe ne dispose d’aucun quoting qui garde une telle valeur littérale (%name% est interprété à l’intérieur de ses guillemets doubles, et un guillemet simple y est un caractère ordinaire), c’est pourquoi il n’est jamais utilisé avec une telle valeur.

zeroclaw plugin list répète la même comparaison en tant que diagnostic permanent, une instance à la fois : la liaison d’outil par défaut de chaque plugin d’outil installé qui détient http_client, et chaque alias de canal lié dont le paquet détient http_client, websocket_client ou socket_client, affiché sous la forme package (plugin.<alias>). Pour chacun, une ligne nomme les destinations qu’il déclare et que sa ligne n’accorde pas, puisque les requêtes vers ces destinations sont refusées, ainsi que la commande qui comble l’écart ; pour une instance qui n’a pas encore de ligne, cette commande crée la ligne avec l’autorisation. Une instance de canal sans ligne encore, dont l’alias est rejeté par la grammaire d’alias de configuration, comme un [channels.plugin.ops-team] écrit à la main, reçoit la raison de la grammaire et la table à renommer à la place, car la commande qui crée la ligne valide l’intégralité de la configuration et refuserait cet alias. Une instance de canal compte chaque transport que l’autorité egress régit, afin qu’un canal qui ne parle que par socket ou WebSocket, comme IRC ou MQTT, ne soit jamais signalé ; une ligne d’outil conserve la règle http_client avec laquelle elle a été livrée. L’inverse n’est jamais signalé. Une autorisation sans déclaration correspondante est un chemin de premier ordre, et non un constat : le plugin dont la destination relève de la configuration du déploiement (un Gitea auto-hébergé, un Nextcloud du LAN) ne peut pas voir cet hôte déclaré par son auteur, c’est vous qui le rédigez.

Lorsque la ligne de l’instance porte encore une clé pré-1.0 (un nom de paquet au lieu de la clé zpi1_…), plugin list affiche toujours le renommage décrit ci-dessus, car l’environnement d’exécution ne résout une autorisation qu’à partir de la seule clé zpi1_… : ce que cette ligne autorise n’est pas en vigueur, et les requêtes sont refusées tant qu’elle n’a pas été renommée. Si l’autorisation de la ligne couvre déjà tout ce que le manifeste déclare, le renommage constitue à lui seul toute la marche à suivre : aucun zeroclaw config set n’est proposé, car il ne ferait que remplacer une liste déjà correcte. Si la déclaration cite encore des destinations que la ligne n’autorise pas, le renommage est la première étape et la commande d’autorisation la deuxième ; cette commande reprend l’autorisation existante de la ligne afin de ne rien révoquer de ce que vous avez configuré. Les chemins pointés plugins.entries.<key>.… ne se résolvent que pour les lignes déjà présentes dans la configuration active ; exécuter la commande d’autorisation avant le renommage échoue donc avec Unknown property, alors qu’appliquer les étapes affichées dans l’ordre indiqué fonctionne. plugin list diagnostique ce problème et ne modifie jamais lui-même votre configuration. Seule la ligne d’une liaison d’outil par défaut peut se trouver dans cet état : les instances de canal sont postérieures au format de clé pré-1.0, si bien qu’aucune ligne de canal n’est signalée pour un renommage.

plugin list évalue une ligne avec le même constructeur de politique que celui utilisé par le runtime au moment de la requête ; son verdict est donc celui du runtime. Si le runtime refuserait la ligne (un joker à un seul label tel que *.com, une entrée contenant des espaces en bordure, ou une exception egress_allow_private qu’aucun hôte accordé ne couvre), le chargeur de configuration se contente d’émettre un avertissement, mais le runtime refuse l’intégralité de la liste d’autorisation et rejette chaque requête. plugin list affiche le motif du runtime ainsi qu’une commande d’octroi construite uniquement à partir des entrées que le runtime accepte ; sur une ligne en version antérieure à 1.0, cette commande suit le renommage, et le renommage n’est jamais proposé seul pour une ligne que le runtime refuserait. La commande affichée ne remplace que egress_hosts. Si une exception privée serait encore refusée après cela, le rapport l’indique et nomme le chemin egress_allow_private de la ligne, car celui-ci doit être corrigé manuellement. Un refus qui ne concerne pas la ligne elle-même, à savoir un security.nat64_prefixes mal formé ou un plugins.limits.max_connections_per_instance égal à zéro, refuse de la même manière la politique de chaque plugin ; plugin list le signale une seule fois en nommant ces deux chemins, n’affiche aucune ligne par plugin tant que le problème n’est pas corrigé, et ne propose jamais de réparation d’octroi par plugin pour ce cas. plugin install et plugin bind signalent une ligne existante avec le même verdict, dans les mêmes termes, afin que ces commandes et plugin list ne divergent jamais quant à l’utilisabilité d’un octroi.

Un paquet de canal sans alias lié n’a pas encore d’instance ; plugin list n’affiche donc aucune ligne de canal pour lui : il n’y a pas de ligne à comparer, et l’installation n’en crée pas.

Liaison d’une instance de canal

A channel package n’a pas d’instance tant qu’une table [channels.plugin.<alias>] ne la désigne pas, et l’alias est à votre choix ; l’installation du paquet ne peut donc pas, à elle seule, créer la ligne d’une instance de canal. La liaison d’un alias est la cérémonie qui s’en charge :

zeroclaw plugin bind acme.chat --channel-alias operations --egress declared

plugin bind s’applique à un paquet déjà installé ; c’est aussi la commande pour ajouter un second alias et pour réparer une liaison partielle. zeroclaw plugin install <source> --channel-alias <alias> exécute la même procédure dans le cadre de l’installation et accepte le même drapeau --egress, que install n’accepte qu’avec --channel-alias. Ce drapeau ne régit que la ligne de l’instance de canal : une ligne d’outil du même paquet est initialisée selon la règle propre à install, comme c’était déjà le cas. Dans ce cas, tous les refus décrits ci-dessous interviennent avant la publication du paquet ; la ligne d’outil, la liaison et la ligne de canal sont écrites en une seule sauvegarde ; et un échec pendant cette écriture annule la publication du paquet. Une installation d’un paquet de canal sans --channel-alias publie le paquet comme auparavant. Si des liaisons désignent déjà le paquet, comme le laisse une commande plugin remove, elle affiche alors le rapport de disponibilité de chaque alias lié, décrit ci-dessous ; sinon, elle affiche la commande plugin bind à exécuter, avec ALIAS à la place de l’alias que vous choisissez. Lorsque le manifeste déclare des destinations que le canal peut atteindre, elle les nomme et affiche cette commande une fois par décision d’egress, la première se terminant par --egress none, puis la suivante par --egress declared. La procédure ne pose jamais de question : chaque décision passe par un drapeau.

La commande est refusée et n’écrit rien lorsque :

  • le fichier de configuration utilise une ancienne version de schéma ; exécutez d’abord zeroclaw config migrate, car zeroclaw config set l’exige, une sauvegarde apposerait sinon la version actuelle sur les sections encore dans l’ancienne structure ;
  • le paquet ne fournit pas de canal ;
  • L’alias ne doit pas comporter entre 1 et 63 lettres minuscules, chiffres et underscores simples, et doit commencer et se terminer par une lettre ou un chiffre ;
  • the alias est déjà lié à un autre paquet, que le refus désigne ; le package d’une autre liaison n’est jamais réécrit ;
  • le fichier de configuration, ou sa section [plugins], [channels], [channels.plugin] ou [channels.plugin.<alias>], est mal formé et n’a pas été chargé, car la commande n’écrase jamais une section que le chargeur a ignorée.

Sinon, il crée la liaison, écrite avec package et la valeur par défaut enabled = true, ou conserve celle qui désigne déjà le paquet exactement tel quel, y compris enabled = false. Il crée ensuite la ligne [[plugins.entries]] de l’instance, lorsque cette ligne n’existe pas encore et que l’instance a un état détenu par l’hôte à y conserver : un config_schema, une destination déclarée, ou un transport que l’autorité d’egress régit (http_client, websocket_client ou socket_client).

Créer cette ligne à partir d’une déclaration utilisable par l’instance demande une décision. Lorsque le manifeste déclare des destinations et que le paquet contient un transport régi :

  • --egress declared initialise la déclaration dans egress_hosts et affiche chaque destination avec la commande qui modifie ensuite l’autorisation, soit le même affichage que celui que install produit pour une ligne d’outil.
  • --egress none crée la ligne avec un droit vide, de sorte que l’instance n’a aucune accessibilité réseau, et affiche les destinations déclarées ainsi que la commande permettant de les accorder ultérieurement.
  • Sans l’un ni l’autre de ces indicateurs, la commande refuse de s’exécuter, affiche la liste des destinations déclarées et mentionne les deux indicateurs. Rien n’est écrit.

Sans une telle déclaration, aucune décision n’est nécessaire : la ligne est créée avec une autorisation vide, et si le paquet contient un transport régi mais ne déclare aucune destination, la commande affiche une commande config set --no-interactive pour la ligne egress_hosts sans valeur, à compléter avec les hôtes utilisés par votre déploiement sous la forme d’une valeur unique séparée par des virgules. Telle quelle, elle est refusée et n’écrit rien.

Cet indicateur ne s’applique qu’à une ligne créée par la commande. Une ligne qui existe déjà, issue d’une exécution antérieure, écrite par vous ou laissée par un paquet supprimé, n’est jamais étendue : la commande affiche la même différence que install affiche pour une ligne existante et, si vous avez passé --egress, indique qu’il n’a pas été appliqué. Un script qui passe toujours --egress declared ne peut donc pas élargir une autorisation. La ligne de chaque alias est initialisée uniquement à partir du manifeste, jamais copiée depuis la ligne d’un autre alias ou depuis la ligne d’outil du paquet, et egress_allow_private n’est jamais écrit. Une seconde exécution avec les mêmes arguments laisse le fichier de configuration identique octet pour octet, et une exécution qui trouve une liaison sans sa ligne, ou une ligne sans sa liaison, crée la moitié manquante sans toucher à l’autre.

After binding, the command reports whether the instance can start, reading each answer from the source the runtime uses:

  1. Les autorisations dont dispose l’instance, à savoir celles que son manifeste demande. Elles sont affichées ici à titre informatif, elles ne sont pas décidées : la décision que vous prenez porte sur la liste d’autorisation des connexions sortantes (egress allowlist).
  2. Chaque clé que le schéma liste comme required, définie ou manquante. Une clé manquante est accompagnée de la commande config set plugins.entries.<key>.config.<name> qui la définit ; la commande d’une clé secrète demande la valeur sans l’afficher. Aucune commande n’est affichée pour un nom de propriété hors de la grammaire portable des clés ; le rapport indique à la place la ligne à modifier dans le fichier de configuration. Une instance dont la ligne n’existe pas encore, comme une liaison écrite à la main, reçoit la commande plugin bind qui crée la ligne à la place des commandes par clé, car config set ne résout que les lignes existantes. Lorsque le manifeste déclare des destinations utilisables par la ligne, le rapport les nomme et affiche cette commande une fois par décision d’egress, d’abord se terminant par --egress none, puis par --egress declared, afin que vous exécutiez celle que vous choisissez. Une liaison écrite à la main dont l’alias est refusé par plugin bind, comme [channels.plugin.ops-team], reçoit le motif de la grammaire des alias et la table à renommer à la place. Les valeurs ne sont jamais affichées.
  3. Le verdict du résolveur de configuration d’exécution sur la ligne, qui nomme le chemin du schéma ou la propriété en échec lorsque le résolveur en signale un, et jamais une valeur. Cela inclut une ligne conservée par plugin remove qui contient encore des valeurs lorsque le manifeste réinstallé ne déclare pas de config_schema : l’environnement d’exécution refuse toute valeur à cet endroit, et le rapport nomme la ligne par sa clé, avec la solution appropriée. Lorsque l’instance ne possède aucun autre état, supprimer la ligne. Lorsque la ligne conserve l’autorisation de sortie de l’instance, parce que le manifeste déclare des destinations ou demande un transport régi, supprimer uniquement les valeurs du mappage config et conserver la ligne.
  4. Pour une instance dont la ligne existe, l’écart d’egress, dans les termes qu’utilise plugin list. Une instance sans ligne encore n’affiche aucune ligne d’écart ici : les commandes plugin bind de l’élément 2 créent la ligne avec votre décision d’egress, alors que plugin list affiche une réparation config patch qui crée la ligne déjà autorisée. plugin bind et plugin install --channel-alias omettent l’écart, car ils viennent de signaler l’autorisation de la ligne avec leur commande.
  5. Le verdict propre au plan d’activation, qui nomme la première condition préalable que le runtime constate non remplie. Il vérifie, dans l’ordre, plugins.enabled, le enabled de la liaison, la présence d’un agent activé qui liste plugin.<alias> dans ses channels, l’installation du paquet qui fournit un canal, et la disponibilité d’un emplacement libre sous plugins.max_active_instances. Un système de plugins désactivé s’accompagne de la commande qui l’active, et il en va de même pour une liaison désactivée dont l’alias est accepté par la grammaire d’alias de configuration. Aucune commande n’est affichée pour un autre alias, comme un [channels.plugin."a.b"] écrit à la main : le rapport indique la table dans laquelle définir enabled = true manuellement, et pour un alias pointé comme celui-ci, config set créerait une autre liaison. Un propriétaire manquant est décrit plutôt que doté d’une commande, car config set sur la liste channels d’un agent remplace la liste entière. Lorsque toutes les conditions préalables sont remplies, le plan admet l’instance, qui démarre au prochain démarrage ou rechargement du démon si son composant se charge. Si le runtime ne parvient pas du tout à construire son plan d’activation, le rapport l’indique avec l’erreur du runtime.
  6. Un démon en cours d’exécution ne démarre l’instance qu’après un redémarrage ou un rechargement.

La cérémonie n’active jamais plugins.enabled et ne modifie jamais les channels d’un agent : ces deux éléments restent de votre ressort. Une liaison réussie se termine avec le code 0, même si l’instance ne peut pas encore démarrer ; seul un refus ou une écriture échouée se termine avec un code non nul. zeroclaw plugin info <package> affiche le même rapport après la clé de chaque alias lié, et pour un paquet de canal sans alias lié, il affiche le même indice plugin bind que l’installation.

Une liaison dont l’alias ne permet de désigner aucune instance de plugin, par exemple un alias contenant un caractère de contrôle (ce que la validation de la configuration se contente de signaler par un avertissement), n’empêche pas la remontée des autres instances du paquet. Les commandes plugin list, plugin info, plugin install et plugin remove affichent une ligne indiquant que la liaison est ignorée, avec l’alias échappé, et désignent la table [channels.plugin.<alias>] à renommer ou à supprimer.

Installation depuis le guide de démarrage rapide

Un build avec l’hôte de plugins peut également installer des plugins d’outils depuis la ligne optionnelle Plugins du Démarrage rapide de la CLI ; Démarrage rapide → Plugins explique la procédure. Le Démarrage rapide exécute la même transaction de téléchargement, d’admission, de vérification de chargement et de publication-et-amorçage que zeroclaw plugin install, et les règles ci-dessus restent applicables :

  • Déclaration et octroi. Quickstart vous montre la déclaration [egress] du manifeste, qui n’accorde rien à elle seule. Chaque fois qu’il crée une ligne pour un paquet dont le manifeste demande http_client et déclare des destinations, la seule déclaration avec laquelle une ligne créée est initialisée, vous choisissez si la nouvelle ligne accorde ces destinations, que le paquet soit nouveau ou qu’il ait déjà été installé sans sa ligne. La question se pose dès l’installation sans accès réseau, qui laisse egress_hosts vide et affiche la commande qui accorde les destinations plus tard, comme plugin bind --egress none le fait pour une instance de canal. Ignorer un paquet déjà installé laisse sa ligne absente et affiche la commande qui la crée avec un octroi vide, puis, sur sa propre ligne, la commande qui accorde les destinations déclarées. La commande que Quickstart affiche pour créer une ligne manquante n’accorde jamais l’accès réseau ; un octroi est toujours une commande distincte. La commande plugin install --no-verify que Quickstart affiche pour un plugin qui échoue au contrôle de chargement est l’installation de la CLI de plugins qui, comme toujours, initialise une ligne qu’elle crée avec les destinations que le manifeste déclare pour http_client.
  • Une ligne existante n’est jamais étendue. Lorsque la ligne [[plugins.entries]] de l’instance existe déjà, Quickstart laisse ses egress_hosts, egress_allow_private et ses paramètres exactement tels quels. Il ne télécharge, ne met à niveau et ne reconfigure jamais un paquet déjà installé, et ne crée la ligne de ce paquet que si elle est absente.
  • Activation needs your consent. Quickstart never turns on plugins.enabled or plugins.auto_discover without asking. Before it asks, it lists every tool, skill, and channel plugin those settings would activate, including plugins installed earlier, and the question defaults to no when that list goes beyond the plugins the run installed or kept. A plugin whose missing row you skipped, or that Quickstart could not finish setting up, is not one of them. The list comes from the plugins directory as a daemon would load it; when that directory cannot be read, the question also defaults to no.

Quickstart est plus strict que plugin install sur trois points. Il refuse une entrée de registre qui ne comporte aucun digest sha256 pour son archive, au lieu d’installer une archive qu’il ne peut pas vérifier. Lorsque la section [plugins] du fichier de configuration ne peut pas être lue, il n’installe rien, alors que plugin install installe le package et se contente d’avertir qu’il n’a pas créé la ligne [[plugins.entries]] du package. Enfin, quelle que soit la valeur de plugins.security.signature_mode, le résumé du package indique si la signature du manifeste se vérifie avec plugins.security.trusted_publisher_keys, et il n’installe un package dont la signature ne se vérifie pas qu’après que vous avez répondu oui à une question dont la réponse par défaut est non ; signature_mode détermine toujours ce que l’admission refuse.

Mise à jour d’un plugin

zeroclaw plugin update remplace un paquet installé par une autre version du même paquet et conserve tout ce qui appartient à ses instances :

# la version que le registre sélectionne pour chaque plugin nommé
zeroclaw plugin update my-plugin other-plugin

# une version exacte, plus récente ou plus ancienne que celle installée
zeroclaw plugin update my-plugin@0.2.0

# chaque plugin installé
zeroclaw plugin update --all

# un répertoire de paquet local, par exemple un plugin recompilé
zeroclaw plugin update my-plugin --from ./my-plugin/

--registry <url> sélectionne le registre comme pour plugin install. La mise à jour ne remplace que les paquets installés : un nom inconnu échoue avec un renvoi vers plugin install, et plugin install sur un paquet déjà installé échoue avec un renvoi vers plugin update. Les paquets sont identifiés par leur nom seul, donc --all interroge le registre au sujet de chaque paquet installé, y compris ceux installés depuis un répertoire ; nommez explicitement les plugins lorsque vous exécutez des builds locaux. La commande de mise à jour fait partie du cycle de vie du registre suivi dans #7432 (R3).

Sélection de version. Un nom sans version prend la version que plugin install choisirait, c’est-à-dire la dernière entrée listée par le registre pour ce nom, et name@version prend exactement cette entrée. ZeroClaw ne trie pas les versions pour en choisir une, donc une version sélectionnée différente de celle installée est installée même si elle est plus ancienne. Lorsque les deux sont des versions sémantiques et que la nouvelle est plus ancienne, la ligne de résultat l’indique au lieu de signaler une mise à jour. Épingler la version que vous aviez est la façon de revenir en arrière. Lorsque la version sélectionnée est celle installée, le plugin est signalé à jour et rien n’est téléchargé. Un package --from remplace toujours celui installé, même à la même version, de sorte qu’un plugin recompilé peut être réinstallé sans changement de version.

Vérification. Un remplacement passe les mêmes contrôles qu’une nouvelle installation avant que le paquet installé ou sa configuration ne soit modifié : l’empreinte de l’archive lorsque l’entrée du registre en fournit une, le nom et la version du manifeste par rapport à l’entrée du registre, la politique de signature, l’empreinte de la charge utile, le schéma de configuration, et la vérification de chargement par rapport au monde WIT de cet hôte avec vos plugins.limits. La mise à jour ne propose pas de --no-verify. Pour installer un paquet qui ne se charge pas, supprimez le plugin puis installez-le avec --no-verify.

Autorité ajoutée. L’hôte accorde à un plugin toutes les permissions que son manifeste demande. Une mise à jour est donc refusée, avant que son composant ne soit compilé, lorsque le nouveau manifeste demande une permission ou une capacité que la version installée ne demande pas, déclare un nouveau canal provides, ou désigne une clé d’éditeur différente de celle de la version installée (ou aucune, si la version installée en désignait une). La clé n’identifie un éditeur que lorsque plugins.security.signature_mode vaut strict, mode dans lequel elle est vérifiée ; dans les modes permissive et disabled, il s’agit d’une affirmation non vérifiée, et une clé qui apparaît alors que la version installée n’en désignait aucune n’est pas considérée comme un changement. Le refus liste ce que la nouvelle version ajoute, ainsi que la commande qui accepte exactement ces ajouts, épinglée à la version que vous avez examinée :

'my-plugin' 0.2.0 requests authority the installed version does not have:
  + permission:http_client
Nothing was changed. To accept it and update, run: zeroclaw --config-dir '/home/user/.zeroclaw' plugin update 'my-plugin@0.2.0' --allow-token 'permission:http_client'

--allow-token accepte un seul élément complet par option, et peut être répété : permission:<name>, capability:<name>, provides:<id>, ou publisher:<key> (publisher:unsigned pour un paquet qui ne nomme aucune clé). --allow accepte les mêmes éléments sous forme de liste séparée par des virgules. La commande affichée utilise --allow-token, de sorte qu’un élément dont la valeur contient une virgule, comme un identifiant provides, parvient à la commande en entier. Chacun de ces deux options s’applique à un seul plugin à la fois. Pour une mise à jour du registre, ce plugin doit être épinglé sous la forme name@version, afin que ce que vous acceptez soit la version que vous avez examinée. Supprimer le plugin puis installer la nouvelle version constitue l’autre façon de l’accepter. Chaque valeur de la commande affichée est mise entre guillemets pour votre shell, selon les règles décrites dans Déclaration et octroi de l’egress.

Ce que la mise à jour conserve. L’identité d’une instance est constituée du nom du paquet, de la capacité et du binding, et non de la version. Ainsi, les lignes [[plugins.entries]] du plugin, leurs valeurs de configuration et secrets, egress_hosts, egress_allow_private, les profils TLS et l’état durable sont conservés sans modification. La règle d’egress de plugin install s’applique : une mise à jour n’étend jamais une autorisation, pas même dans une ligne qu’elle crée pour une instance qui n’en avait aucune. Elle affiche la différence entre la nouvelle déclaration et l’autorisation, avec la commande qui l’applique. Lorsque le schéma de configuration de la nouvelle version rejette une configuration qu’une instance possède déjà, par exemple un paramètre nouvellement obligatoire, la mise à jour se termine quand même et désigne l’instance ainsi que le problème. Cette instance ne se charge pas tant que sa configuration n’est pas conforme à la nouvelle version. Un paquet dont la ligne de configuration utilise encore la clé pré-1.0 (nommée d’après le paquet) n’est pas mis à jour tant que cette ligne n’a pas été renommée, et le refus affiche les mêmes indications que plugin install.

Résultats. Chaque plugin reçoit un résultat : mis à jour, déjà à jour, ignoré (avec --all, un paquet que le registre ne répertorie pas, ou un paquet qu’une mise à jour interrompue a laissé déplacé, ce qui est signalé comme déplacé), approbation requise, ou non mis à jour, avec la raison et la version qui reste installée. L’échec d’un plugin n’interrompt pas les autres. La commande se termine avec un code de sortie non nul si un plugin n’a pas été mis à jour ou nécessite une approbation, afin qu’un script puisse se brancher sur ce résultat.

Remplacement et récupération. Une mise à jour ne modifie le répertoire des plugins que si elle détient le même verrou masqué que plugin install et plugin remove, de sorte que ces modifications ne se chevauchent jamais, même entre processus. Le nouveau paquet est d’abord vérifié, puis écrit intégralement dans une transaction de préparation masquée, comme le fait l’installation. Ce n’est qu’ensuite que la mise à jour revendique le répertoire installé dans une transaction masquée qui lui est propre, et vérifie qu’il s’agit toujours du paquet sur lequel la mise à jour a été préparée, avec le même manifeste et le même composant. Elle déplace alors le nouveau paquet à sa place sans rien écraser ; si ce déplacement échoue, elle remet le paquet précédent en place. Un échec avant la revendication laisse le paquet installé inchangé, et un paquet modifié sur le disque depuis le début de la mise à jour est remis en place et la mise à jour est refusée. Si le processus est arrêté après la revendication et avant que le nouveau paquet soit en place, le paquet précédent subsiste dans cette transaction masquée, que la découverte ignore, et le bail du système d’exploitation qui le concerne prend fin avec le processus. plugin list signale alors le paquet comme déplacé, après avoir attendu toute installation, mise à jour ou récupération en cours ; la commande zeroclaw plugin update <name> suivante le remet en place avant toute autre action, y compris avant d’atteindre le registre, de sorte que la récupération fonctionne hors ligne. La mise à jour se poursuit ensuite normalement ; pour un paquet installé depuis un répertoire, ajoutez --from avec ce répertoire, faute de quoi le paquet du registre portant ce nom pourrait le remplacer. --all ne s’applique qu’à cette commande : un paquet déplacé n’est jamais remis en place sans que vous le nommiez, même s’il a été déplacé par une mise à jour pendant l’exécution du lot. Une version remplacée est marquée pour suppression à l’intérieur de sa transaction avant d’être supprimée, de sorte qu’une version qui ne peut pas être supprimée n’est jamais remise en place : l’avertissement indique où elle se trouve, et la mise à jour suivante du plugin la supprime. Si même le marquage échoue, supprimez manuellement le répertoire désigné par l’avertissement, car une fois le plugin retiré, zeroclaw plugin update <name> remettrait cette copie en place. Un processus arrêté après la mise en place du nouveau paquet mais avant le marquage de la version remplacée laisse celle-ci dans le même état : la mise à jour suivante du plugin la supprime, mais après un plugin remove, plugin list la signale comme déplacée et une mise à jour nommée la remet en place.

Les fichiers ne sont pas synchronisés sur le disque avant les renommages, contrairement à plugin install, si bien qu’une coupure de courant juste après une mise à jour peut laisser le nouveau paquet incomplet. Mettre à jour à nouveau ne le répare pas, car le paquet endommagé porte déjà la version que le registre sélectionne. Si zeroclaw plugin info <name> indique que le plugin ne se charge pas, exécutez zeroclaw plugin remove <name>, puis réinstallez-le ; ses lignes de configuration et ses autorisations sont conservées lors de la suppression. Si le plugin n’apparaît plus du tout dans la liste, supprimez manuellement son répertoire dans plugins_dir, puis réinstallez-le.

Daemon en cours d’exécution. La mise à jour modifie uniquement le paquet installé et n’envoie jamais de signal à un daemon en cours d’exécution. Les ensembles d’outils que le daemon construit après la mise à jour chargent la nouvelle version, tandis que les instances de canal conservent le composant chargé jusqu’au redémarrage du daemon ou jusqu’à son rechargement des canaux ; redémarrez-le donc pour faire passer toutes les instances à la nouvelle version. Un ensemble d’outils construit à l’instant précis du remplacement peut omettre le plugin ou, pour un paquet dont le manifeste ne lie pas wasm_sha256, associer l’ancien manifeste au nouveau composant ; l’ensemble d’outils suivant est cohérent.

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.enabled false (par défaut) : aucun code de plugin ne s’exécute jamais.
  • plugins.auto_discover false (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 lorsque auto_discover = true est é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.

Confiance des certificats pour le plugin HTTPS

Une requête de plugin effectuée via HTTPS est vérifiée à l’aide du programme de racines webpki inclus, ainsi que des racines que cette machine approuve déjà. Un point de terminaison dont le certificat remonte à une CA installée localement, comme une racine MDM d’entreprise ou une PKI privée, fonctionne donc pour les plugins exactement comme pour les requêtes vers le fournisseur. La vérification elle-même est inchangée : la construction de la chaîne et la vérification du nom d’hôte restent en vigueur, et la politique de sortie (egress) détermine toujours les destinations qu’un invité peut atteindre.

Les sockets et les connexions WebSocket partent des mêmes bases. Un plugin qui doit atteindre un service derrière une autorité de certification privée, ou présenter un certificat client, indique un profil TLS que l’opérateur a configuré sur son instance :

[[plugins.entries]]
name = "zpi1_…"                       # la clé de l'instance
egress_hosts = ["imap.corp.example.com"]

[[plugins.entries.tls_profiles]]
name = "corp"
hosts = ["imap.corp.example.com"]     # doit être inclus dans egress_hosts
system_roots = false                  # ne faire confiance qu'à l'AC ci-dessous
custom_ca_secret = "corp_ca"          # propriétés x-secret du schéma de configuration du plugin
client_certificate_secret = "cert"    # contenant le matériel PEM
client_private_key_secret = "key"

Un profil ne sélectionne que des certificats. Chacun de ses hosts doit être autorisé par egress_hosts, ce que vérifie la validation de la configuration, et une requête qui le nomme passe tout de même d’abord par l’autorisation ordinaire. Le matériel des certificats reste dans la configuration chiffrée de l’instance ; les champs du profil ne sont que les noms de propriétés. L’hôte lit ce matériel lors de la construction d’une connexion, ce que le plugin ne peut pas faire : secrets.get refuse toute propriété nommée par un profil, si bien qu’une clé privée client n’entre jamais dans l’invité.

Ces racines sont lues une seule fois par processus. Réécrire le fichier de certificats au même chemin, ou modifier le magasin de certificats du système d’exploitation, n’a aucun effet sur un démon en cours d’exécution ; redémarrez-le avant de vous attendre à ce que HTTPS des plugins prenne en compte la modification. Il en va de même pour une première lecture malchanceuse : une machine dont le magasin était brièvement illisible n’utilise que les certificats fournis avec l’application jusqu’au redémarrage du processus, et la ligne de journal plugin_egress_trust_anchors indique laquelle des deux situations s’est produite.