Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help


type: reference status: accepted last-reviewed: 2026-09-07 relates-to:

  • FND-001
  • ADR-003
  • crates/zeroclaw-plugins

Protocole de plugin

Ce document définit le protocole entre l’hôte de plugins de ZeroClaw et les composants de plugins WASM.

Qu’est-ce qu’un plugin

Un plugin est un composant WebAssembly autonome que ZeroClaw charge à l’exécution pour ajouter une fonctionnalité que le binaire principal n’inclut pas. Il réside dans son propre répertoire sous ~/.zeroclaw/plugins/, accompagné d’un manifeste qui lui donne un nom et déclare ce qu’il fournit. ZeroClaw le détecte au démarrage, le vérifie, et intègre ses fonctions exportées à l’agent en cours d’exécution afin qu’elles se comportent comme des fonctionnalités intégrées : un plugin d’outil apparaît pour le modèle comme un simple outil appelable (WasmTool implémente le même trait Tool qu’un outil natif), un plugin de canal se comporte comme un canal de messagerie, et un plugin de mémoire comme un backend de stockage.

Un plugin peut fournir une ou plusieurs des capacités définies dans PluginCapability (crates/zeroclaw-plugins/src/lib.rs) : un outil callable, un canal de messagerie, un backend mémoire, un backend d’observabilité ou un ensemble de compétences markdown. Le cas des compétences est particulier : il ne contient aucun WASM, uniquement un répertoire skills/ contenant du markdown, ce qui explique pourquoi il s’agit de la seule capacité à omettre le composant compilé.

Pourquoi en construire un

  • Étendez sans fork. Ajoutez un outil ou un canal sans modifier l’arborescence source de ZeroClaw ou attendre une version ; le plugin vous appartient et se charge depuis votre répertoire d’installation.
  • Comportement natif. Un plugin chargé n’est pas un complément de seconde classe. Le pont implémente les mêmes traits d’exécution que les fonctions intégrées, de sorte qu’un outil de plugin est proposé au modèle, attribué et invoqué exactement comme un outil officiel.
  • Choix de langage. Le contrat est le WIT et le WASI Component Model, et non une API Rust. Tout langage compilant vers un composant wasm32-wasip2 peut implémenter un world. Le guide pratique ci-dessous utilise Rust, car c’est l’approche qui offre le plus de support aujourd’hui, mais la frontière elle-même est agnostique du langage.
  • Sandboxé par défaut. L’hôte charge chaque plugin dans un contexte WASI sans préouvertures de système de fichiers ni réseau ambiant. Un plugin ne peut pas atteindre l’hôte discrètement ; il reçoit exactement les fonctions hôte câblées dans son monde, et rien de plus. Le HTTP sortant, les sockets bruts et WebSocket sont les surfaces réseau pouvant être ouvertes, et chacune est régie en deux couches : la déclaration du manifeste (http_client, socket_client ou websocket_client) détermine si l’adaptateur lie cette surface, et la déclaration par instance de l’opérateur plugins.entries.<key>.egress_hosts détermine la portée, commune aux trois. Les adaptateurs d’outils et de canaux lient les surfaces ; la mémoire les retient actuellement. Sans déclaration de sortie réseau, la portée est tout refuser, de sorte qu’une surface liée n’envoie rien à elle seule.
  • Provenance vérifiable. Les manifests peuvent être signés avec Ed25519, et un opérateur peut exiger des signatures d’éditeurs de confiance avant le chargement de tout plugin.

Ce qu’un plugin ne peut pas faire (aujourd’hui)

Ce sont de réelles limites de l’hôte actuel, pas des préférences de style. Connaissez-les avant de concevoir autour d’une capacité qui n’est pas disponible.

  • logging, configuration typée, secrets à portée d’instance et état durable, http_client et entrées pilotées par l’hôte sont câblés. Parmi les permissions qu’un manifeste peut déclarer, config_read expose la configuration publique du plugin, validée par schéma. Un schéma d’outil ou de canal peut désigner des secrets exclus de la configuration publique et résolus lors d’appels de service autorisés. state_read et state_write contrôlent l’accès à un état chiffré, avec compare-and-swap, appartenant à l’instance admise exacte. Une autorisation http_client est nécessaire pour wasi:http sortant, mais elle ne suffit pas : l’adaptateur de capacité doit lier cette surface hôte (les outils et les canaux le font ; la mémoire reste volontairement sans HTTP tant que sa frontière réseau n’est pas couverte au niveau du composant), et l’opérateur doit accorder les destinations via egress_hosts. Sans cette autorisation, toute destination est refusée avant qu’un paquet ne quitte l’hôte ou qu’un nom ne soit résolu. socket_client et websocket_client lient les imports sockets et websocket aux mêmes conditions : la même autorisation, les mêmes règles de classes d’adresses et le même budget de connexions par instance. Une autorisation désigne des hôtes, pas des ports : avec socket_client, un hôte autorisé est joignable en TCP brut sur n’importe quel port ; n’autorisez donc que les hôtes avec lesquels le plugin peut échanger quel que soit le protocole. Les permissions de système de fichiers et d’accès à la mémoire sont encore acceptées par le schéma du manifeste mais sans effet : leurs fonctions hôtes ne sont pas encore enregistrées dans l’éditeur de liens. Voir Permissions et imports hôtes ci-dessous.
  • Aucun réseau ni système de fichiers hôte ambiant. Le contexte WASI ne dispose d’aucun préouverture (preopen) et d’aucun réseau ambiant ; un plugin ne peut donc ni ouvrir de sockets bruts ni lire des fichiers de l’hôte via WASI ambiant. Un plugin d’outil ou de canal disposant d’une autorisation http_client obtient un accès sortant à wasi:http, et un plugin disposant de socket_client ou de websocket_client obtient des connexions TCP/TLS/STARTTLS ou WebSocket médiées par l’hôte, qui n’atteignent que les destinations listées dans l’autorisation d’egress accordée par l’opérateur ; les plugins de mémoire n’obtiennent actuellement aucune de ces capacités. Aucun plugin ne peut écouter sur un port. Les plugins de canal qui doivent recevoir du trafic entrant n’ouvrent pas eux-mêmes d’écouteur : l’hôte exécute l’écouteur et transmet les messages via l’import inbound, que le plugin consomme depuis son export poll-message.
  • Une frontière sur 32 bits. La cible est wasm32-wasip2. La mémoire de l’invité constitue un espace d’adressage sur 32 bits et l’ABI des composants représente les décalages sur 32 bits, indépendamment de la taille de mot de l’hôte. Les grandes valeurs (par exemple les octets bruts d’une liaison de canal) traversent la frontière par valeur. Consultez la section sur l’espace d’adressage sur 32 bits pour comprendre pourquoi il s’agit d’une contrainte imposée par la chaîne d’outils en amont, et non d’un drapeau que ce dépôt peut activer.
  • Un outil par plugin d’outil. Le monde tool-plugin exporte une seule interface tool avec un nom et un schéma. Un plugin qui doit exposer plusieurs outils livre plusieurs composants, ou un monde différent.
  • Contrat expérimental, non figé. wit/v0 ne comporte pas encore de marqueur .frozen, les interfaces peuvent donc encore évoluer avant la première version stable. Épinglez une version et prévoyez de recompiler lors d’une incrémentation de version WIT.

Architecture

Les plugins ZeroClaw sont des composants WebAssembly définis par des interfaces WIT dans wit/v0/ et hébergés directement via wasmtime (crates/zeroclaw-plugins). Un plugin est compilé en un composant WASI Preview 2 (wasm32-wasip2) qui exporte l’un des mondes de plugins (tool-plugin, channel-plugin, memory-plugin) et importe les interfaces hôtes déclarées par ce monde dans wit/v0/.

L’hôte se trouve dans crates/zeroclaw-plugins/src/component.rs. Il contient un wasmtime::Engine asynchrone, génère les liaisons du monde avec wasmtime::component::bindgen! à partir de wit/v0, et intègre une surface WASI p2 en bac à sable dans l’éditeur de liens de chaque monde. L’état hôte propre à chaque store (PluginState) contient un WasiCtx configuré sans pré-ouvertures ni réseau, ainsi que la ResourceTable requise par WASI, son périmètre attribué par l’hôte et des handles de service typés actifs. Chaque monde importe logging ; l’outil importe secrets, tandis que le canal importe config, secrets et inbound. Une autorisation http_client accordée ajoute et relie également wasi:http. Les déclarations des mondes et le périmètre admis restent les contrats de référence pour cette surface (voir Importations de l’hôte).

Les trois ponts de mondes mappent chaque monde WIT sur les traits natifs du runtime :

MondeModule de pontSurface d’exécution
tool-pluginruntime.rs, wasm_tool.rszeroclaw_api::tool::Tool
channel-pluginwasm_channel.rstrait de canal
memory-pluginwasm_memory.rstrait de backend mémoire

Les plugins d’outils utilisent un store frais par appel (sans état). Les plugins de canal et de mémoire maintiennent un store chaud protégé par un mutex asynchrone pour la durée de vie du plugin.

Les plugins d’outils sont découverts et enregistrés de bout en bout : le runtime parcourt l’équivalent outil de channel_plugin_details() et construit un WasmTool pour chacun. L’adaptateur d’hôte de canal (WasmChannel, sa surface wasi:http soumise à autorisation (grant-gated) derrière la politique d’egress détenue par l’hôte, ses services de configuration au point d’utilisation et sa file inbound alimentée par l’hôte) est terminé et couvert par un test d’egress sur un composant réel. PluginHost::channel_plugin_details() expose les plugins de canal adossés à wasm à enregistrer. Le runtime résout désormais une liaison [channels.plugin.<alias>] explicitement déclarée, construit son WasmChannel et l’enregistre à partir de l’alias configuré ; cette construction tenant compte de l’alias et la résolution de la configuration à l’exécution ont été introduites dans #10146. Un canal peut aussi opter pour l’ingress webhook POST générique détenu par la passerelle, décrit ci-dessous ; les tunnels de fournisseurs, les clients en interrogation (polling) et les autres transports restent des suites à traiter. Le pont mémoire (WasmMemory) se trouve dans la même situation une étape en amont : l’adaptateur implémente l’intégralité du trait Memory contre le monde memory-plugin, mais l’hôte n’expose pas encore d’équivalent mémoire à channel_plugin_details(), et le runtime ne construit pas encore de WasmMemory comme backend configurable.

Structure du plugin

Un plugin est un répertoire contenant :

my-plugin/
  manifest.toml    # Plugin metadata and permissions
  plugin.wasm      # Compiled WASM module (optional for skill-only plugins)

Les plugins sont découverts dans ~/.zeroclaw/plugins/ (configurable via plugins.plugins_dir dans la configuration).

Recherche et installation dans le registre

Le chemin d’installation local des plugins reste la source de vérité pour les plugins installés. Un registre n’est qu’un index JSON utilisé au moment de la commande pour découvrir et télécharger une archive de plugin :

zeroclaw plugin search calendar
zeroclaw plugin install team-calendar
zeroclaw plugin install team-calendar@0.2.0
zeroclaw plugin search calendar --registry https://example.invalid/registry.json
zeroclaw plugin install team-calendar --registry https://example.invalid/registry.json

zeroclaw plugin search récupère les métadonnées du registre et fait correspondre la requête aux noms et descriptions des plugins. Il n’installe pas, n’active pas et n’exécute pas le code des plugins.

L’index récupéré est mis en cache. zeroclaw plugin list lit ce cache et affiche une ligne de paquet à côté de l’enregistrement installé admis par l’hôte, sans effectuer de récupération et sans considérer les métadonnées du registre comme une preuve que le code a été installé.

zeroclaw plugin install <name> résout le nom depuis le registre, télécharge l’archive zip sélectionnée, vérifie le condensat SHA-256 optionnel, extrait l’archive de manière sûre, puis transmet le répertoire du plugin extrait au même chemin d’admission qu’une installation locale. L’admission (PluginHost::admit_source) analyse le manifeste et en vérifie la signature, valide sa structure et sa configuration, refuse un nom déjà installé, et lit le composant une seule fois : le chemin doit rester à l’intérieur du paquet sans lien symbolique, la lecture est limitée à 64 Mio et refusée d’après les métadonnées du fichier lorsqu’il est plus volumineux, et le condensat wasm_sha256 optionnel est contrôlé. La CLI exécute ensuite, sur ces octets admis, la vérification de chargement au moment de l’installation : la même instanciation que celle effectuée par le démon au démarrage, afin qu’un composant construit avec un WIT désynchronisé échoue dès l’installation avec son diagnostic complet, au lieu d’être ignoré silencieusement plus tard. plugin install --no-verify ignore entièrement la vérification de chargement (rien n’est compilé) et affiche une note ; il ne contourne pas l’admission. Un hôte compilé sans backend WASM n’a pas de moteur pour instancier le composant et installe sans cette vérification. L’installation (PluginHost::install_admitted) persiste ensuite les octets du manifeste admis et écrit les octets du composant admis, de sorte que ce qui a été vérifié correspond octet pour octet à ce que le démon chargera. Les installations depuis un chemin local suivent la même séquence.

Lorsqu’aucune version n’est épinglée, ZeroClaw choisit la dernière entrée correspondante dans l’index du registre, les éditeurs de registres doivent donc ordonner intentionnellement les noms répétés.

zeroclaw plugin install ./my-plugin
zeroclaw plugin install ./my-plugin/manifest.toml

zeroclaw plugin update remplace un paquet installé via la même admission (PluginHost::admit_update), mais avec une identité décidée dans le sens inverse : le paquet nommé doit être installé, et la source doit porter son nom. La CLI refuse les privilèges que le nouveau manifeste ajoute, sauf si --allow les accepte, exécute la même vérification de chargement, sans possibilité de la contourner, et PluginHost::update_admitted construit le remplacement dans un répertoire de préparation avant de le renommer à la place du paquet installé. Le contrat destiné aux opérateurs se trouve dans Mise à jour d’un plugin.

zeroclaw plugin update team-calendar
zeroclaw plugin update team-calendar@0.2.0 --registry https://example.invalid/registry.json
zeroclaw plugin update --all
zeroclaw plugin update my-plugin --from ./my-plugin

L’URL du registre par défaut est :

https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw-plugins/main/registry.jsonPour les registres privés ou de préproduction, utilisez --registry <url> par commande ou définissez ZEROCLAW_PLUGIN_REGISTRY_URL.

Les entrées du registre utilisent cette forme :

{
  extensions: [
    {
      « nom »: team-calendar,
      version: 0.8.6,
      "description": Planifier des réunions sur un calendrier d'équipe,
      auteur: Exemple d'équipe,
      capacités: [outil],
      "url": https://example.invalid/team-calendar-0.2.0.zip,
      sha256: "sha256:<hachage hexadécimal du zip>"
    }
  ]
}

L’archive doit contenir soit un manifest.toml au niveau racine, soit un répertoire de plugin imbriqué contenant manifest.toml. Les archives avec des chemins de traversée, des chemins absolus, des chemins préfixés par un lecteur Windows, ou plus d’un manifeste sont rejetées avant l’installation. Les téléchargements sont plafonnés pendant le streaming, de sorte qu’un serveur sans Content-Length ne peut pas forcer ZeroClaw à mettre en tampon une archive trop volumineuse. L’extraction est également plafonnée, de sorte qu’une archive compressée ne peut pas s’étendre sans limite dans la zone d’installation temporaire.

Les requêtes vers le registre sont limitées dans le temps. La connexion au registre ou à l’hôte des archives ne peut pas durer plus de 10 secondes, et une réponse ne peut pas rester plus de 30 secondes sans fournir de données, aussi bien avant l’arrivée de ses en-têtes qu’entre les lectures de son corps. La récupération de l’index doit se terminer en 30 secondes, et le téléchargement d’une archive en 30 minutes. Un registre ou un hôte d’archives bloqué fait donc échouer zeroclaw plugin search ou zeroclaw plugin install au lieu de les laisser attendre indéfiniment, tandis qu’un téléchargement d’archive lent mais qui continue de fournir des données n’est limité que par la borne de 30 minutes.

La recherche est une découverte non authentifiée. L’installation est la frontière de sécurité : les installations depuis le registre utilisent la politique de signature des plugins configurée et les clés d’éditeurs de confiance, de la même façon que les installations de plugins locaux via PluginHost::install.

Disposition de plugin avec compétences uniquement (bundle markdown)

Un plugin dont la seule capacité est skill fournit des skills dans un répertoire skills/ au format agentskills.io et omet wasm_path :

my-toolkit/
  manifest.toml              # declares the skill capability, no wasm_path
  README.md                  # optional bundle-level overview
  skills/
    design-review/
      SKILL.md
      scripts/
      references/
    code-review/
      SKILL.md
    data-analysis/
      SKILL.md
      references/

Chaque SKILL.md doit inclure un frontmatter YAML avec les champs name et description ; le runtime rejette les bundles dont les skills omettent l’un ou l’autre au moment de la découverte plutôt qu’à la première invocation. Les skills s’enregistrent sous des ID à espace de noms de plugin de la forme plugin:<plugin-name>/<skill-name> (par exemple plugin:my-toolkit/design-review) afin d’éviter les collisions avec les skills créés par l’utilisateur et entre les bundles.

Format du manifeste

Le manifeste est le fichier nommé manifest.toml dans le répertoire du plugin. Ses champs sont la surface serde de PluginManifest dans crates/zeroclaw-plugins/src/lib.rs, qui est la source de vérité :

ChampObligatoireSignification
nameouiSlug canonique unique du package et composant du package de chaque clé de configuration d’instance dérivée. Il ne constitue pas lui-même une clé de configuration d’opérateur. Utilisez 1–128 caractères ASCII en minuscules ; commencez et terminez par [a-z0-9], avec uniquement [a-z0-9._-] entre les deux. La découverte rejette les noms non valides ou en double.
versionouiChaîne de version, p. ex. 0.1.0.
descriptionnonDescription lisible affichée par zeroclaw plugin list.
authornonNom de l’auteur ou de l’organisation.
wasm_pathpour les capacités WASMNom du fichier du composant, relatif au répertoire du plugin. Obligatoire sauf si la seule capacité est skill. La découverte ignore le plugin si le fichier nommé n’existe pas.
wasm_sha256dans le mode strict pour les capacités WASMEmpreinte SHA-256 hexadécimale des octets exacts du composant. Lorsqu’elle est présente, elle est toujours appliquée ; la politique de signature stricte l’exige afin que le manifeste signé lie l’exécutable admis.
capabilitiesoui, non videCe qu’est le plugin : l’un de tool, channel, memory, observer, skill (PluginCapability, sérialisé en snake_case).
providesnonPour un plugin channel qui est un remplaçant direct (drop-in) d’un canal intégré à la compilation : l’identifiant de configuration en snake_case qu’il reproduit (telegram, gmail_push). L’hôte admet une instance logique par [channels.<id>.<alias>] configuré et activé qu’un agent activé route vers, de sorte que la configuration canonique du canal reste le seul emplacement de ces paramètres. Nécessite config_read. La famille plugin est réservée et refusée ; un identifiant non défini par cette version du build est refusé ; deux paquets installés revendiquant le même identifiant sont tous deux refusés. Absent (valeur par défaut) signifie un canal inédit configuré depuis [channels.plugin.<alias>]. Les miroirs sont admis mais pas encore construits ; voir le plan d’activation dans crates/zeroclaw-runtime/src/plugin_runtime.rs.
permissionsnonServices hôtes auxquels le code peut accéder : http_client, config_read, state_read, state_write, file_read, file_write, memory_read, memory_write (PluginPermission). Les permissions HTTP, de configuration et d’état d’instance exacte sont appliquées pour les adaptateurs d’outils et de canaux ; les permissions de système de fichiers et de mémoire restent sans effet. La déclaration de config_read nécessite config_schema.
config_schemaexactement avec config_readBrouillon 2020-12 de JSON Schema pour la configuration privée de ce plugin ; il est inclus dans les octets canoniques du manifeste et est donc couvert lors de la signature du manifeste. La racine doit être un objet avec une map properties et additionalProperties = false. Chaque propriété de premier niveau doit avoir un type explicite pris en charge, directement ou via un JSON Pointer local : string, boolean, integer, number, array ou object. Les consommateurs d’outils et de canaux peuvent définir x-secret = true directement sur une propriété string de premier niveau afin de la retirer de la configuration publique et de l’exposer via l’import hôte secrets.get limité à sa portée. Un nom de propriété secrète est un nom ASCII portable de 1 à 128 octets ne contenant que des lettres, des chiffres, _, - ou .. Les outils reçoivent la configuration publique sous __config et peuvent lire les secrets pendant execute. Les canaux lisent l’objet public courant via config.get et les secrets via secrets.get pendant configure et les appels opérationnels ; les deux imports sont indisponibles pendant l’instanciation et la découverte des métadonnées statiques. Les marqueurs secrets imbriqués, false ou non booléens, ainsi que les propriétés secrètes non string, sont rejetés. Un schéma sans config_read, ou un config_read sans schéma, est rejeté.
signaturenonSignature Ed25519 en Base64url sur les octets canoniques du manifest. Défini lors de la signature pour la distribution.
publisher_keynonClé publique Ed25519 encodée en hexadécimal du signataire.
sortienonTable [egress] facultative contenant une liste hosts : les destinations que ce plugin déclare requérir, sous forme d’hôtes exacts (api.example.com) ou de motifs de suffixe explicites (*.example.com, qui correspondent aux sous-domaines mais pas au domaine apex). Elle fait partie des octets canoniques du manifeste : elle est donc couverte lors de la signature du manifeste, et toute modification exige une nouvelle signature. Une grammaire invalide rejette l’intégralité du manifeste lors de la découverte et de l’installation. L’absence de la table, une table vide et une table qui « ne déclare rien » correspondent à un même état.

Déclarez uniquement les permissions effectivement utilisées par le code. Une permission non déclarée est une surface d’hôte que le composant ne peut pas atteindre ; une permission déclarée de manière superflue est une surface d’attaque que vous avez sollicitée et une charge d’audit pour quiconque révise votre plugin.

[egress] est une déclaration, et non une autorisation. Rien dans un manifeste n’accorde d’accès réseau : la liste d’autorisation appliquée par l’hôte est plugins.entries[].egress_hosts de l’opérateur, sur la ligne zpi1_… de l’instance elle-même. Ainsi, un composant non signé qui écrit sa propre table [egress] n’atteint toujours rien. Ce que la déclaration apporte, c’est le processus d’installation : zeroclaw plugin install l’insère dans une ligne qu’il crée et affiche ce qui a été accordé, et il n’élargit jamais une ligne déjà existante. Voir Déclarer et accorder l’accès réseau sortant.

Les valeurs des opérateurs restent des chaînes dans plugins.entries et sont chiffrées lors de leur persistance, avec pour clé une chaîne zpi1_… versionnée dérivée de l’identité du package détenu par l’hôte, de la capacité et de la liaison (l’installation affiche et initialise la clé d’instance complète de la liaison d’outil par défaut) : les chaînes sont stockées telles quelles, les booléens et les nombres sont représentés par du texte scalaire JSON, et les tableaux et les objets par du texte JSON. Avant l’exécution de tout code invité, l’hôte matérialise ces chaînes selon les types du schéma du package et valide l’objet complet pour les adaptateurs d’outils et de canaux. Les propriétés non secrètes de l’outil constituent __config ; un canal obtient l’objet non secret via config.get. Une propriété marquée x-secret = true est exclue des deux surfaces publiques et n’est accessible que via secrets.get("property") dans un cadre de service autorisé. Les lectures publiques et secrètes d’un canal au cours d’un même appel partagent une révision canonique unique, et l’hôte supprime cette vue matérialisée à la fin de l’appel. Un plugin de canal conforme doit résoudre les deux à chaque point d’utilisation et ne doit pas conserver les valeurs de configuration ou d’identifiants dans l’état invité conservé à chaud ; renvoyer du texte en clair à l’invité signifie que l’hôte ne peut pas imposer la non-rétention face à du code malveillant. Si config_read a été demandé mais n’a pas été effectivement accordé, l’hôte valide un objet vide ; par conséquent, un schéma comportant des propriétés obligatoires échoue en mode sécurisé au lieu de démarrer sans la configuration requise. Si l’objet vide est valide, un outil omet __config lorsqu’il est vide et les imports config/secret du canal renvoient access-denied ; les appels effectués hors d’un cadre autorisé, les échecs de résolution et l’épuisement du budget d’appels à l’hôte renvoient unavailable.

Capacités

capabilities est une liste non vide de valeurs PluginCapability, définies dans crates/zeroclaw-plugins/src/lib.rs (sérialisées en snake_case). Chaque valeur sélectionne le monde WIT que le plugin exporte (tool, channel, memory), nomme un backend d’observabilité (observer), ou marque un bundle de compétences uniquement markdown (skill). Consultez l’enum pour l’ensemble canonique ; c’est la source de vérité et cette page ne le reformule pas.

Un manifeste doit déclarer au moins une capacité. wasm_path est requis pour chaque capacité, à l’exception d’un plugin dont la seule capacité est skill, qui ne transporte aucune charge utile WASM et qui est rejeté lors de la découverte s’il omet un bundle skills/ valide (validate_manifest_shape dans host.rs).

Autorisations

permissions est une liste de valeurs PluginPermission, également définies dans crates/zeroclaw-plugins/src/lib.rs. Consultez l’enum pour l’ensemble canonique.

Gardez à l’esprit l’écart entre ce qui est déclaré et ce qui est appliqué : dans l’hôte de composants actuel, config_read, http_client, state_read et state_write ont un effet comportemental. Demander config_read exige un config_schema, et déclarer ce schéma sans cette permission est également rejeté. Avant qu’un composant d’outil ou de canal ne soit utilisé, l’hôte résout son autorisation effective, matérialise les valeurs de l’opérateur du plugin en JSON typé et valide l’objet complet. runtime.rs supprime tout __config fourni par l’appelant avant d’injecter les valeurs non secrètes validées dans un appel d’outil ; les propriétés de premier niveau de type chaîne marquées x-secret: true sont omises de la configuration publique et lues via l’import secrets délimité à l’hôte. Les outils reçoivent ce service pendant execute. Les canaux reçoivent la configuration publique via config.get et les secrets via secrets.get pendant configure et les appels opérationnels, tandis que l’instanciation et la découverte statique des métadonnées restent indisponibles. http_client est une autorisation nécessaire, mais pas une décision d’autorité complète : l’adaptateur de capacité doit également construire le contexte HTTP et lier wasi:http, et la politique de sortie (egress) gérée par l’hôte doit autoriser la destination. Les adaptateurs d’outils et de canaux lient la surface après la validation de l’autorisation ; l’adaptateur de mémoire ne la lie volontairement pas, de sorte que l’octroi de http_client à une portée mémoire n’ajoute aucune surface réseau. Pour les outils et les canaux, la surface n’atteint toujours rien tant que l’opérateur n’a pas listé les destinations sous plugins.entries.<key>.egress_hosts ; la politique refuse tout autre hôte. Un hôte autorisé qui se résout en une adresse de boucle locale, privée ou link-local reste refusé, sauf si l’opérateur le liste également sous plugins.entries.<key>.egress_allow_private, une dérogation qui assouplit la classe d’adresse pour une destination déjà autorisée sans jamais en accorder une (voir Plugins pour le côté opérateur). Les permissions d’état contrôlent indépendamment les imports state.get et state.put/delete pour les trames de service des outils et des canaux. Le package, la capacité et la liaison proviennent uniquement de la portée admise par l’hôte ; un composant invité ne fournit aucun espace de noms. Les autres variantes (file_read, file_write, memory_read, memory_write) sont acceptées par le schéma du manifeste mais ne sont pas encore reliées à un import de l’hôte : les déclarer n’accorde rien en soi. Elles réservent simplement les noms des fonctions hôtes qui les contrôleront (voir Imports de l’hôte ci-dessous).

interfaces WIT

Le contrat de plugin est l’ensemble des fichiers WIT dans wit/v0/, package zeroclaw:plugin@0.1.0. Chaque élément est soumis à @unstable(feature = plugins-wit-v0) jusqu’à ce que le package se stabilise ; voir wit/VERSIONING.md pour les règles de compatibilité. Les interfaces ci-dessous sont résumées à titre d’orientation ; les fichiers .wit font autorité pour les signatures exactes.

Mondes

wit/v0/ définit trois mondes, liés par bindgen! dans component.rs. Chacun importe logging (hôte) et exporte plugin-info ainsi que son interface principale : tool-plugin exporte tool, channel-plugin exporte channel, et memory-plugin exporte memory. Tool importe aussi secrets et state, et channel importe config, secrets, state et inbound. Tool et channel importent en outre sockets et websocket derrière leurs propres fonctionnalités (plugins-wit-v0-sockets, plugins-wit-v0-websocket) : l’hôte ne lie chacune d’elles que pour une instance détenant l’autorisation correspondante, de sorte qu’un composant qui importe l’une de ces interfaces sans l’autorisation échoue à l’instanciation. Les exports requis (sans valeur par défaut) de chaque monde sont listés dans le commentaire de documentation du monde, dans son fichier .wit.

interface tool

wit/v0/tool.wit définit la surface d’outil unique. L’hôte appelle name, description et parameters-schema une fois au moment du chargement, puis dispatche execute par invocation :

record tool-result {
    success: bool,
    output: string,
    error: option<string>,
}

name: func() -> string;
description: func() -> string;
parameters-schema: func() -> json-string;
execute: func(args: json-string) -> result<tool-result, string>;

parameters-schema renvoie une chaîne JSON Schema présentée au LLM pour l’appel d’outils. execute reçoit des arguments encodés en JSON correspondant à ce schéma et renvoie un tool-result ou une chaîne d’erreur. json-string est un alias de type string provenant de wit/v0/types.wit ; les appelants produisent du JSON valide, les récepteurs le parsent.

channel et memory interfaces

wit/v0/channel.wit et wit/v0/memory.wit définissent des surfaces contrôlées par des capacités. L’hôte appelle get-channel-capabilities / get-memory-capabilities une fois au chargement, et pour chaque flag non défini il utilise la valeur par défaut du trait Rust au lieu d’appeler le plugin. Un plugin doit toujours exporter chaque fonction (un stub renvoyant la valeur par défaut documentée est suffisant) ; l’hôte n’appelle simplement jamais celles dont le flag est absent. La valeur par défaut à laquelle se résout chaque flag non défini est documentée en ligne dans le WIT à côté des flags *-capabilities, qui est la source de vérité à la fois pour l’ensemble des flags et leurs valeurs par défaut.

Indicateurs de capacité

Les méthodes optionnelles sont annoncées via flags channel-capabilities et flags memory-capabilities. Parce que les flags sont un masque de bits, de nouvelles méthodes optionnelles peuvent être ajoutées à un package vN/ sans changement cassant, associées à une nouvelle fonction @since. Supprimer ou renommer un flag, une fonction, un champ ou un cas de variante est un changement cassant et nécessite un nouveau répertoire vN+1/.

Importations de l’hôte

Les fonctions hôtes sont importées par le plugin et fournies par le runtime. Le linker de chaque monde relie logging (via l’implémentation hôte dans component_logging.rs, liée avec add_wasi dans component.rs). Les mondes tool et channel lient le service secrets à portée d’instance. Le monde channel importe également config pour son objet public typé, et inbound pour la file de messages alimentée par l’hôte, qu’il vide via poll-message. Les adaptateurs tool et channel ne lient le wasi:http sortant qu’après que la portée admise a accordé http_client (PluginStoreSpec::with_granted_http et add_wasi_http dans component.rs). Les deux composent la politique d’accès réseau détenue par l’hôte (PluginStoreSpec::with_egress_policy), afin que l’invité ne reçoive jamais les hooks par défaut non restreints de Wasmtime. Le monde memory ne reçoit ni le contexte ni la surface du linker. Les permissions de système de fichiers et d’accès à la mémoire restent inertes : les fonctions hôtes qui les contrôleraient ne sont pas encore reliées au linker. L’autorité ambiante d’un plugin correspond au contexte WASI (sans preopens, sans accès réseau ambiant) plus exactement les imports hôtes que ses autorisations et ses options d’adaptateur activent conjointement.

Les imports gérés par ZeroClaw partagent un budget de sécurité fixe par trame de service distribuée par l’hôte. Le plafond de référence est MAX_HOST_CALLS_PER_FRAME dans crates/zeroclaw-plugins/src/component.rs. À l’épuisement, la journalisation devient une opération sans effet, l’interrogation des entrées renvoie un résultat vide, et les lectures de la configuration publique ou des secrets renvoient unavailable. Une nouvelle trame réinitialise le budget. Ce plafond relève d’une politique d’hôte fixe, et non d’une configuration opérateur dupliquée.

inbound

wit/v0/inbound.wit est importé par le world channel-plugin. Un plugin de canal s’exécute sans écouteur propre, donc l’hôte exécute l’écouteur (un serveur webhook, un tunnel fournisseur, un client de polling) et met en file d’attente chaque message reçu. Le plugin vide la file d’attente depuis son export poll-message en appelant inbound-poll, avec inbound-pending disponible pour vider par lots :

inbound-poll: func() -> option<host-inbound-message>;
inbound-pending: func() -> u32;

Le côté hôte possède une InboundQueue par canal ; WasmChannel::inbound remet un clone à la tâche d’écoute afin que le trafic mis en file d’attente soit visible pour le drain du plugin.

Ingress de webhook de canal

Un composant de canal peut ajouter webhook-ingress à ses channel-capabilities renvoyées et implémenter webhook-path ainsi que parse-webhook. L’hôte monte alors une route GET/POST sur /plugin/<path>. Le chemin est une revendication d’exécution, et non une configuration : il doit contenir de 1 à 64 lettres ASCII, chiffres, tirets ou tirets bas. L’instance de canal est rejetée si la revendication est vide, invalide ou dupliquée. Toutes les revendications sont résolues avant que le démon remplace sa table des routes, si bien que l’ordre d’itération du manifeste ne peut pas désigner un gagnant, et qu’une reconstruction échouée ne peut pas publier une génération partielle. La publication est accompagnée d’un bail de génération : un superviseur de canal plus ancien en cours de retrait ne peut pas effacer un ensemble de routes plus récent.

L’ingestion de webhooks exige le droit config_read effectif du composant. Pour chaque requête, la passerelle transmet à parse-webhook un webhook-request typé contenant la méthode HTTP, la chaîne de requête brute, les en-têtes UTF-8 en minuscules et les octets exacts du corps, dans un store configuré jetable. Le guest résout sa configuration et ses secrets courants à portée limitée lors de cet appel, vérifie l’authenticité de la plateforme, puis renvoie webhook-response.messages, webhook-response.reply, ou un rejet typé unauthorized / bad-request. L’annulation ou l’expiration de la requête HTTP supprime ce store jetable ; elle n’interrompt jamais le store chaud utilisé par les appels de polling et de canaux sortants.

Un challenge vérifié renvoie reply(body) : la passerelle répond avec HTTP 200 et ce texte, sans autorisation de l’expéditeur, sans idempotence et sans remise à l’agent. L’hôte accepte au maximum 4096 octets UTF-8 aux frontières du composant et d’HTTP ; au-delà, il renvoie le message fixe 502 invalid webhook response. La méthode et la requête proviennent des champs de la requête, jamais des en-têtes réservés. Le nom du canal d’un message n’a aucune signification de contrôle de réponse. HEAD et les autres méthodes non prises en charge renvoient 405 avec Allow: GET, POST avant d’atteindre le composant.

L’accès non authentifié à la périphérie reste régi par l’hôte :

  • Le limiteur de débit de webhook canonique, compatible avec les en-têtes transférés de confiance, s’exécute avant la recherche de route. Les corps de requête utilisent la limite de 64 KiB de la passerelle, chaque route dispose d’une file d’attente de 64 requêtes, et l’analyse plus la livraison ont un délai de 10 secondes.
  • Les réponses publiques sont fixes : le succès renvoie 200 ; l’authentification de l’invité et les rejets de charge utile renvoient 401 et 400 opaques ; les échecs de l’hôte, d’un composant ou d’un service aval renvoient 503 opaque ; le délai d’attente renvoie 504 ; la saturation de la file d’attente ou du débit renvoie 429. Les diagnostics de l’invité et de Wasmtime sont bornés, attribués et restent dans les journaux de l’hôte ; les diagnostics de l’invité ne doivent jamais contenir d’identifiants ni de secrets bruts.
  • Après l’authentification de l’invité, l’hôte attribue l’identité plugin.<alias> admise et résout [peer_groups.*].external_peers pour ce canal en temps réel. La correspondance exacte des expéditeurs du plugin s’applique ("*" reste l’entrée explicite d’autorisation totale). Les expéditeurs refusés ne réservent jamais de clé d’idempotence et n’atteignent jamais la file d’attente du canal. Le même filtre en temps réel, avec refus par défaut, s’applique aux messages renvoyés par poll-message, afin que tous les ponts d’entrée des plugins partagent une seule politique de l’hôte.
  • Les identifiants de message invité non vides sont dédupliqués dans un espace de noms dérivé de la route. Un doublon en cours d’exécution attend que la livraison propriétaire soit validée (commit) ou annulée (rollback) ; des jetons de propriété liés à une génération empêchent un propriétaire obsolète d’effacer une réservation de remplacement. Une livraison échouée ou annulée est annulée (rollback), de sorte qu’une nouvelle tentative auprès du fournisseur peut devenir le nouveau propriétaire.

Ceci modifie le canal expérimental non figé wit/v0. Chaque composant de canal, y compris celui qui ne traite pas les webhooks, doit être reconstruit avec le WIT fourni par l’hôte cible et exporter les stubs documentés, conditionnés par des capacités. La fenêtre de compatibilité des versions figées ne s’applique pas encore.

logging

wit/v0/logging.wit est importé par les trois worlds. Les plugins appellent log-record pour émettre des événements structurés vers l’hôte :

log-record: func(level: log-level, event: plugin-event);

L’appel est sans attente : il ne renvoie rien et l’hôte (component_logging.rs) absorbe toutes les erreurs, de sorte qu’une écriture de journalisation échouée ne peut jamais faire planter l’exécution du plugin. La livraison est asynchrone : l’import remet l’enregistrement à une file d’attente bornée côté hôte, vidée par un thread dédié, et retourne sans bloquer, de sorte qu’un consommateur de journaux lent ou bloqué ne peut jamais maintenir une exportation du guest bloquée au-delà de plugins.limits.call_timeout_ms. Le fait de différer l’opération ne modifie pas la signification d’un événement : chaque enregistrement capture le span hôte actif au point d’appel du guest et est écrit dans ce contexte, si bien que l’attribution de l’agent, du canal et de l’outil, ainsi que le libellé terminal, correspondent à l’émission inline. La limite est une véritable limite mémoire, car les champs des événements sont des chaînes non bornées copiées dans la mémoire de l’hôte, en dehors du plafond max_memory_mb du guest : un enregistrement dont le nombre d’octets contrôlés par le guest dépasse 64 KiB est abandonné plutôt que tronqué, et les enregistrements en file d’attente consomment un budget fixe de 8 MiB au total en octets, qui n’est libéré qu’après l’écriture d’un enregistrement. Une file pleine, un enregistrement dépassant la limite ou un budget épuisé entraînent tous l’abandon de l’enregistrement le plus récent ; le thread de vidage signale le nombre cumulé d’enregistrements abandonnés après chaque écriture et lors d’un réveil en période d’inactivité, afin que la perte reste observable, même si aucun enregistrement accepté ne suit jamais les enregistrements rejetés. plugin-action et plugin-outcome reprennent les taxonomies fermées Action / EventOutcome de zeroclaw-log ; aucune variante fourre-tout n’est prévue, volontairement. N’appelez pas directement wasi:logging : les événements des plugins seraient formatés de manière incohérente et n’atteindraient pas toutes les destinations vers lesquelles écrit zeroclaw_log.

config

wit/v0/config.wit est importé par le monde de canal. Il renvoie l’objet actuel validé par rapport au schéma et ne contenant aucun secret au format JSON :

get: func() -> result<json-string, config-error>;

L’objet conserve les types déclarés par config_schema ; les propriétés marquées x-secret: true sont omises. Le service est disponible pendant configure et les exports de canaux opérationnels. Il renvoie access-denied lorsque l’instance admise ne dispose pas de l’autorisation effective config_read. Les appels effectués pendant l’initialisation du composant ou la découverte des métadonnées statiques, en cas d’échec du résolveur ou de la validation, et en cas d’épuisement du budget d’appels à l’hôte renvoient unavailable sans divulguer de détails internes.

config.get est un accès au point d’utilisation, et non un instantané pris au moment du chargement. Un plugin de canal conforme doit l’appeler dans chaque opération qui utilise la configuration et ne doit pas conserver l’objet qu’il renvoie dans l’état à chaud du code invité. Il s’agit d’une règle de conformité des plugins : après avoir renvoyé du JSON à du code invité de confiance, l’hôte ne peut pas empêcher un composant malveillant de le copier.

secrets

wit/v0/secrets.wit est importé par les mondes tool et channel. Le composant invité fournit uniquement un nom de propriété de niveau supérieur :

get: func(name: string) -> result<string, secret-error>;

L’hôte détermine le package, la capacité, la liaison et les autorisations effectives à partir du PluginInstanceScope admis ; aucune de ces valeurs n’est fournie par l’invité. Seules les propriétés de chaîne directes de premier niveau marquées x-secret: true dans le schéma du manifeste sont lisibles. Les outils peuvent les lire pendant que l’hôte envoie execute. Les canaux peuvent les lire pendant configure et pendant les opérations telles que send, poll, l’analyse des webhooks, health et les actions soumises à une capacité. L’initialisation des composants et les exports de métadonnées statiques renvoient unavailable sans résoudre la configuration. Au sein d’une même trame de service de canal, chaque config.get et secrets.get utilise une même révision canonique de configuration résolue ; cette trame est abandonnée quelle que soit la sortie. Un plugin conforme observe donc ensemble une rotation d’une même liaison des valeurs publiques et secrètes lors de sa prochaine opération. access-denied, not-found et unavailable ne révèlent délibérément aucun détail sur le résolveur ni sur le schéma. Les lectures réussies renvoient le texte en clair à l’invité de confiance. Le service empêche l’injection de valeurs publiques et la sélection d’instances croisées ; il ne constitue pas un proxy de sortie qui garde la valeur cachée au code du plugin. Un plugin de canal conforme doit résoudre les secrets à chaque point d’utilisation et ne doit pas conserver une seconde copie dans l’état mis en cache entre les invocations. L’hôte ne peut pas garantir la non-rétention une fois le texte en clair renvoyé.

Les noms de propriétés secrètes suivent la même grammaire portable, locale au plugin, que les clés d’état : 1 à 128 octets ASCII ne contenant que des lettres, des chiffres, _, - ou .. La syntaxe d’URI, de chemin, d’espace de noms, de contrôle et de caractères non ASCII est rejetée lors de l’admission du manifeste.

state

wit/v0/state.wit est importé par les mondes tool et channel. Il fournit des valeurs d’octets durables chiffrées, appartenant au package, à la capacité et au binding exacts admis :

get: func(key: string) -> result<option<state-entry>, state-error>;
put: func(key: string, value: list<u8>, expected-revision: option<u64>)
    -> result<u64, state-error>;
delete: func(key: string, expected-revision: u64) -> result<_, state-error>;

state_read permet get ; state_write permet put et delete. none sur put correspond à une comparaison-et-échange (compare-and-swap) sur clé absente, tandis que some(revision) doit correspondre exactement à la révision courante. Les écritures renvoient la nouvelle révision. Les révisions sont opaques et ne sont jamais réutilisées au sein d’une instance, de sorte qu’une clé supprimée puis recréée ne peut pas satisfaire une comparaison-et-échange obsolète. Les clés utilisent la grammaire portable locale au plugin décrite ci-dessus et ne peuvent pas sélectionner une autre instance. L’état n’est disponible que pendant l’exécution d’un outil et les trames de service de canal ; les appels statiques, l’épuisement du budget d’appels hôte, les échecs de stockage ou de clé et les échecs d’intégrité renvoient des erreurs fermées. Les quotas fixes par instance (entrées, valeurs et taille totale) renvoient quota-exceeded plutôt que de valider partiellement une écriture.

The runtime stocke une enveloppe authentifiée par valeur dans data/plugin-state.db, chiffrée avec le .secret_key existant de l’installation. La base de données ne contient que des index aveugles à clé et des chiffrés enc2:. En cas d’absence ou de remplacement de la clé d’installation, le système échoue en mode sécurisé et ne la régénère jamais silencieusement tant que des lignes durables existent. Sauvegardez la base de données et .secret_key ensemble.

sockets

Sortie TCP sortante médiée par l’hôte. connect prend un hôte, un port, un mode et un profil TLS optionnel, et renvoie une ressource connection :

  • plaintext : TCP brut. Accessible partout où l’opérateur a accordé l’accès à l’hôte ; il n’existe aucune exception distincte pour le mode plaintext (ADR-014).
  • direct-tls : la connexion TLS est établie avant que connect ne retourne.
  • start-tls : la connexion démarre dans une phase de négociation où seuls send-negotiation et receive-negotiation fonctionnent. upgrade-tls fait passer le flux en TLS sur place ; en cas d’échec, le flux est fermé, sans nouvelle tentative en clair.

L’hôte résout la destination une seule fois, ne se connecte qu’aux adresses vérifiées et conserve le bail de connexion de l’instance jusqu’à la suppression de la ressource. La vérification TLS s’effectue avec les mêmes racines de confiance que pour le HTTPS des plugins, sauf si la requête indique un profil TLS. receive ne bloque jamais : il renvoie idle lorsque rien n’est en mémoire tampon. Les échecs correspondent aux cas typés socket-error.

websocket

WebSocket sortant médié par l’hôte. connect prend une URL WebSocket en clair (ws) ou TLS (wss), des en-têtes supplémentaires, les sous-protocoles proposés et un profil TLS facultatif, et renvoie une ressource connection avec send, receive, close et negotiated-subprotocol. L’hôte gère le DNS, la décision de destination, le TLS, la mise à niveau HTTP (handshake) et les files d’attente bornées ; les en-têtes qui relèvent de la mise à niveau (Host, Connection, Upgrade, Sec-WebSocket-*) sont refusés. receive ne bloque jamais et, une fois l’événement terminal closed ou failed consommé, renvoie l’erreur closed plutôt que none. La suppression de la ressource ferme le socket et libère le bail de connexion.

Profils TLS

Une requête sockets ou websocket peut désigner un profil TLS configuré par l’opérateur sur l’instance ([[plugins.entries.tls_profiles]]). Un profil sélectionne les autorités de certification à approuver et, éventuellement, un certificat client pour le TLS mutuel, lus depuis les propriétés x-secret de l’instance. Il n’accorde jamais une destination : la requête doit d’abord passer egress_hosts, et le profil doit également couvrir l’hôte. Désigner un profil sur une connexion en clair est une requête invalide. Les propriétés référencées par un profil sont réservées à l’hôte : secrets.get renvoie access-denied pour elles, de sorte que le plugin ne détient jamais la clé privée avec laquelle il s’authentifie.

Le hôte construit la configuration TLS de chaque connexion à son ouverture, à partir de la révision de configuration de la trame de service qui l’ouvre, et la connexion en est propriétaire. Le matériel transitoire de la trame n’est associé qu’à une autorisation dérivée d’entrées de configuration canoniques identiques. Si la politique, les références de profil ou le matériel changent entre ces deux vues (y compris pendant la résolution DNS), l’ouverture de la connexion échoue en mode sécurisé plutôt que de combiner des révisions. La comparaison porte sur l’entrée de configuration complète de l’instance (ses autorisations, ses profils TLS et toutes les valeurs de configuration), ainsi que sur les préfixes NAT64 et la limite de connexions du déploiement. Toute modification de ce type effectuée pendant qu’une trame s’exécute fait donc échouer les connexions ultérieures de cette trame qui utilisent une autorité de certification personnalisée ou une identité client. Une trame ultérieure résout le matériel roté, et les connexions déjà ouvertes ne sont pas modifiées. Les autorisations de sortie restent vérifiées à chaque requête : un profil ou une autorisation supprimé refuse donc la connexion suivante. Les racines système font exception : un profil qui les conserve, ainsi qu’une connexion sans profil, utilisent les racines fournies par le plugin HTTPS, lues une seule fois par processus. Une connexion déjà ouverte conserve l’identité présentée lors de sa négociation TLS jusqu’à sa fermeture ou son abandon. L’abandon de cette connexion, ou l’annulation de l’appel qui est encore en train de l’ouvrir, libère la configuration en même temps que le bail de connexion.

Configuration par plugin (__config et config.get)

Autorisation : config_read

Un plugin ne lit pas les variables d’environnement du processus. Son manifeste doit associer config_read à un config_schema Draft 2020-12 ; l’un sans l’autre constitue un manifeste non valide. La racine du schéma doit être un objet doté d’une table de correspondance properties et de additionalProperties = false. Chaque propriété de premier niveau doit déclarer l’un des types string, boolean, integer, number, array ou object, directement ou via un JSON Pointer local au package. Les consommateurs d’outils et de canaux peuvent définir x-secret: true sur une propriété de type chaîne située directement au premier niveau ; les marqueurs imbriqués, ayant la valeur false ou non booléens, ainsi que les propriétés secrètes qui ne sont pas de type chaîne, sont rejetés. Les clés inconnues, les encodages mal formés et les violations de contraintes entraînent le rejet de l’instance avant la transmission des valeurs au code invité.

Les valeurs canoniques plugins.entries.<instance-key>.config de l’opérateur restent en mémoire sous la forme d’une map de chaînes marquées comme secrètes et sont chiffrées lors de leur persistance. L’hôte dérive la clé d’entrée versionnée zpi1_… à partir de l’identité complète du package, de la capacité et de la liaison ; cela permet à différents packages et mondes de capacités de réutiliser en toute sécurité des alias tels que main. Le manifeste du package autorisé sélectionne le schéma. Une valeur string est stockée directement ; les valeurs boolean, integer et number utilisent un texte scalaire JSON tel que "true", "4" ou "0.5" ; les valeurs array et object utilisent du texte JSON tel que '["urgent","ops"]' ou '{"region":"us-east"}'. L’hôte matérialise et valide l’ensemble des données en JSON typé à chaque utilisation, puis partitionne chaque propriété exactement une fois. Pour un outil, les valeurs non secrètes sont injectées sous la clé réservée __config :

{
  "invite": un coucher de soleil,
  __config: {
    retry_limit: 4,
    "activé": true,
    étiquettes: [urgent, "ops"]
  }
}

La propriété api_key omise est lue explicitement avec secrets.get("api_key") si son schéma la marque comme secrète. runtime.rs supprime tout __config fourni par l’appelant avant d’injecter la section publique, afin que celle-ci ne puisse pas être usurpée. L’injection de la configuration publique dans l’outil et les lectures de secrets au sein d’un même contexte d’exécution execute partagent une même révision résolue de la configuration active ; ce contexte est libéré en cas de succès, d’erreur, de trappe, de panique ou d’annulation. L’export configure d’un canal ne comporte aucun paramètre config. Il appelle config.get pour l’objet public et secrets.get pour les propriétés secrètes, tout comme chaque export opérationnel ultérieur qui utilise la configuration. Les deux imports au sein d’un même appel partagent une même révision résolue. L’hôte libère cette vue matérialisée en cas de succès, d’erreur, de trappe, de panique ou d’annulation. Les modifications de la configuration publique et des identifiants au sein de la même liaison logique sont donc disponibles conjointement lors de l’opération suivante lorsque le module invité conforme résout les deux au point d’utilisation.

Lorsque le manifeste demande config_read, mais que l’hôte ne l’accorde pas effectivement, la résolution remplace la valeur par un objet vide et valide cet objet avant l’exécution du code invité. Un schéma comportant des champs obligatoires échoue par défaut lors de la construction. Si l’objet vide est valide, les outils omettent le __config vide, tandis que les canaux config.get et secrets.get renvoient access-denied. Un plugin ne voit jamais que sa propre section.

Les exports de métadonnées statiques des canaux ne peuvent appeler aucun des deux services de configuration et sont lus une seule fois au chargement. Modifier l’identité d’un bot ou d’un compte, ou toute capacité, tout auto-identifiant, toute mention ou tout délai entre plusieurs messages dérivé de la configuration, nécessite donc de reconstruire le cycle de vie du canal ; la configuration publique ordinaire et la rotation des identifiants pour la même liaison logique ne le nécessitent pas. Tool et channel sont les consommateurs actuels de la configuration. Le monde memory ne dispose pas encore d’un import de configuration ; les plugins memory ne doivent donc pas demander config_read tant que cette ABI et son intégration au runtime ne sont pas disponibles.

Sortie déclarée ([egress])

S’applique à : tout plugin demandant une autorisation de transport (http_client)

Un manifeste peut contenir une table [egress] facultative qui énumère les destinations que le plugin déclare avoir besoin :

[egress]
hosts = ["api.example.com", "*.cdn.example.com"]

Les entrées utilisent la grammaire stricte de sortie réseau : hôtes exacts ou motifs explicites *.suffixe, sans forme « tout autoriser ». Une grammaire invalide rejette l’intégralité du manifeste lors de la découverte et de l’installation. La table fait partie des octets canoniques du manifeste : un manifeste signé la couvre donc, et toute modification exige une nouvelle signature.

Il s’agit d’une attestation d’intention, jamais d’une autorisation. La liste d’autorisation appliquée par l’hôte est plugins.entries.<instance-key>.egress_hosts de l’opérateur, sur la même ligne zpi1_… que celle qui porte le dictionnaire config privé de l’instance, résolue à partir de la configuration active à chaque requête plutôt que figée dans le store. Un composant qui livre sa propre table [egress] sans autorisation de l’opérateur n’atteint rien. Dans cette version, seule l’autorisation régit cette vérification : la déclaration n’est pas en outre croisée avec elle au moment de la requête, de sorte que déclarer un hôte ne l’autorise pas et ne borne pas une autorisation rédigée par l’opérateur. Le croisement des deux relève d’un travail de déploiement ultérieur, dans #8850.

Ce qu’apporte la déclaration, c’est le déroulement de l’installation. zeroclaw plugin install l’injecte dans la ligne [[plugins.entries]] qu’il crée, de sorte qu’une première installation d’un plugin qui déclare ses destinations fonctionne sans que l’opérateur ait à saisir les hôtes, et les valeurs injectées sont affichées. Elle n’étend jamais une ligne qui existe déjà : une mise à jour dont la déclaration s’est allongée affiche la différence ainsi que la commande exacte zeroclaw config set, et c’est l’opérateur qui l’applique. zeroclaw plugin update n’injecte rien du tout, pas même dans une ligne qu’il crée, et affiche la même différence. zeroclaw plugin list signale le même écart comme diagnostic permanent. Déclarez les destinations que votre code contacte réellement, et considérez qu’une déclaration qui s’allonge est quelque chose que chaque opérateur doit approuver à chaque mise à jour.

La ligne d’une instance de canal est créée lorsqu’un opérateur lie son alias, avec zeroclaw plugin bind <package> --channel-alias <alias> ou avec zeroclaw plugin install <source> --channel-alias <alias>, et la déclaration n’y est injectée que sur un --egress declared explicite. --egress none lie avec un octroi vide à la place, et une liaison qui créerait la ligne sans aucun des deux indicateurs est refusée et liste vos hôtes déclarés. Pour une instance de canal, la déclaration compte avec websocket_client ou socket_client ainsi qu’avec http_client.

Si la réinstallation détecte une ligne de configuration antérieure au typage et non prise en charge, indexée par le nom du package, l’installation refuse de créer la ligne dérivée zpi1_… et affiche les mêmes étapes de mise à jour ordonnées que plugin list. L’installation du package est annulée, et l’ancienne configuration privée ainsi que les deux listes d’egress restent inchangées jusqu’à ce que l’opérateur mette à jour la configuration bêta et relance l’opération.

Les plugins dont la destination est la configuration de déploiement (un Gitea auto-hébergé, un Nextcloud sur le LAN) ne doivent rien déclarer ici. L’opérateur accorde cette autorisation directement sur la ligne de l’instance, et un manifeste qui ne déclare rien n’est jamais signalé comme ayant perdu des destinations.

Hôte de composants WASI

L’hôte (crates/zeroclaw-plugins/src/component.rs) compile et instancie les composants avec un unique wasmtime::Engine asynchrone. La façon dont un fichier .wasm est chargé dépend du backend d’exécution du build :

  • plugins-wasm-cranelift: un backend JIT est présent, donc load_component compile un composant .wasm lors du chargement via Component::from_file.`
  • Aucun backend JIT (plugins-wasm-pulley ou runtime uniquement) : il n’y a pas de compilateur dans le binaire, donc load_component désérialise directement le fichier via Component::deserialize_file, en le traitant comme un .cwasm précompilé produit par un wasmtime correspondant. Un artefact incompatible est rejeté par le contrôle de version de la désérialisation.

Les deux fonctionnalités back-end incluent plugins-wasmtime ; le chemin de chargement dépend de la présence du compilateur cranelift dans le build, et non de pulley.

Limites d’exécution par appel

Chaque export de l’invité s’exécute avec des limites de ressources par appel que l’hôte applique au store. Le moteur active le comptage du fuel, et chaque appel reçoit un nouveau budget de fuel, afin qu’un composant hors de contrôle ou malveillant déclenche un trap plutôt que de bloquer l’hôte. L’hôte applique également une échéance en temps réel autour de la future complète de l’export, y compris le temps passé à attendre des imports hôte asynchrones tels que wasi:http ; des cessions périodiques de contrôle liées au fuel empêchent un calcul invité ininterrompu de monopoliser ce minuteur, et les imports hôte accessibles depuis l’invité ne bloquent jamais l’exécuteur (les enregistrements de journal sont transmis à une file d’attente bornée et écrits par un thread hôte dédié), de sorte que l’échéance reste observable pendant l’exécution du travail de l’hôte. Un plafond StoreLimits borne la mémoire linéaire, les éléments des tables et le nombre d’instances. Le monde de l’outil reçoit un store neuf à chaque exécution ; le canal chaud et les stores de mémoire sont réapprovisionnés en fuel avant chaque appel, afin qu’un plugin de longue durée de vie dispose d’un budget neuf au lieu de l’épuiser au cours de sa durée de vie.

Les cinq bornes sont réglables par l’opérateur, et chaque valeur est validée comme non nulle : plugins.limits.call_fuel (par défaut 1 000 000 000 unités d’instruction), plugins.limits.call_timeout_ms (par défaut 30 000 millisecondes), plugins.limits.max_memory_mb (par défaut 256), plugins.limits.max_table_elements (par défaut 100 000) et plugins.limits.max_instances (par défaut 64). Un store ne peut être créé qu’avec des limites explicites, si bien qu’aucun chemin de chargement ne peut construire un plugin non sandboxé. Les options de requête wasi:http du guest peuvent mettre fin à un appel plus tôt, mais ne peuvent pas prolonger le délai imposé par l’hôte. Un store warm interrompu n’est jamais repris, et ni un channel ni un backend mémoire ne réutilise un store dont l’appel a échoué à l’intérieur de Wasmtime : après un trap du guest, un carburant épuisé ou un échec d’import hôte, Wasmtime refuse tous les appels ultérieurs vers ce store, et un résultat qu’il n’a pas pu lift ignore le nettoyage post-return de l’export. Les channels recréent l’instance à partir d’entrées détenues par l’hôte lors de l’appel suivant, et attendent lorsque les traps et les délais manqués, hormis ceux des invites d’approbation et de choix, dépassent simultanément un budget de reconstruction de trois, puis un par cinq minutes au-delà. Les instances mémoire restent indisponibles tant que leur propriétaire ne les a pas reconstruites, et les appels ultérieurs qui atteignent le plugin échouent avec l’erreur que laisse un délai manqué. Une instance mémoire reconstruite démarre vide, car le monde mémoire ne donne au plugin aucun stockage en dehors de sa propre instance. Les champs canoniques et leurs valeurs par défaut figurent dans la Référence de la configuration.

Espace d’adressage 32 bits (wasip2 est wasm32)

La cible du plugin est wasm32-wasip2, et le moteur hôte est construit avec la mesure de fuel activée (Config::consume_fuel(true)) sans wasm_memory64. La frontière du plugin est un format 32 bits fixe, et cela a des conséquences qu’il convient d’énoncer clairement :

  • L’espace d’adressage de l’invité est de 32 bits. Un plugin s’exécute dans une mémoire linéaire wasm32. Les grandes valeurs franchissent la limite par valeur : le media-attachment d’un plugin de canal transporte l’intégralité de ses octets sous forme de list<u8>, et wit/v0/channel.wit indique déjà qu’il peut s’agir de plusieurs mégaoctets et reporte un modèle resource-handle à une version ultérieure. Dans cet espace 32 bits, l’hôte applique un plafond mémoire explicite par store défini via plugins.limits.max_memory_mb (valeur par défaut : 256), de sorte qu’un invité est limité par la plus petite valeur entre l’espace d’adressage wasm32 et ce plafond configuré pour ZeroClaw.
  • L’ABI du composant représente les décalages sur 32 bits, quelle que soit la taille du mot hôte. Même sur un hôte 64 bits, les décalages des listes et des chaînes dans l’ABI canonique sont de type i32. memory64 étend l’adressage de la mémoire linéaire de l’invité, et non l’ABI canonique du modèle de composant ; son activation ne ferait donc pas passer les champs de niveau WIT sur 64 bits.
  • Il n’existe pas de cible 64 bits pour wasip2 contre laquelle se lier. wasm32-wasip2 est la seule cible WASI Preview 2 dans rustc et LLVM à ce jour ; un plugin ne peut pas être compilé en un composant p2 64 bits, il n’y a donc rien à charger pour l’hôte même si le moteur active memory64.

Il s’agit d’une contrainte de la toolchain amont, et non d’une limitation de l’hôte qu’un flag de ce dépôt peut lever. Lorsqu’une cible p2 64 bits et une ABI de composant plus large arriveront en amont, la jointure bindgen! se régénèrera par rapport à elles et les largeurs de champs seront réexaminées dans le WIT sous la fenêtre wit/VERSIONING.md. D’ici là, traitez la frontière du plugin comme 32 bits par construction.

Signatures

Les manifestes de plugin peuvent contenir une signature Ed25519 (crates/zeroclaw-plugins/src/signature.rs). La signature est encodée en base64url sur les octets canoniques du manifeste (le TOML analysé, après suppression des seules entrées racine nommées exactement signature et publisher_key) ; la clé publique de l’éditeur est encodée en hexadécimal. Les propriétés imbriquées du schéma portant ces noms restent signées. L’hôte impose l’un des trois modes suivants via plugins.security.signature_mode :

ModePlugin non signéSignature non fiable ou invalide
strictrejetérejeté
permissivechargé avec un avertissementchargé avec un avertissement
disabledchargénon coché

La vérification s’exécute à la fois lors de la découverte et de l’installation. La découverte ignore un plugin qui échoue à sa politique plutôt que d’interrompre l’hôte entier ; l’installation renvoie l’erreur.

Écrire un plugin en Rust

Un plugin est une crate cdylib qui cible le modèle de composants. Générez les bindings guest à partir du même package wit/v0 utilisé par l’hôte, implémentez le monde exporté et compilez vers wasm32-wasip2. Pour les didacticiels complets allant d’un crate vide à un plugin installé, consultez les guides des plugins ; les notes ci-dessous couvrent les mécanismes de compilation et d’installation.

Construction

sh

# Installer la cible WASI Preview 2 (une seule fois)
rustup target add wasm32-wasip2

# Construire le composant
cargo build --target wasm32-wasip2 --release

Le composant de sortie se trouve à target/wasm32-wasip2/release/<crate_name>.wasm. Copiez-le à côté de votre manifest.toml. Pour un build d’hôte runtime uniquement sans backend JIT, précompilez le composant en un .cwasm avec un wasmtime correspondant et livrez celui-ci à la place, car un tel hôte désérialise plutôt que de compiler au chargement.

Les tests du plugin d’outils de l’hôte ne dépendent pas d’un artefact publié : crates/zeroclaw-plugins/tests/fixtures/tool-fixture est un composant intégré à l’arborescence, compilé à partir des sources au moment des tests, et reference_plugin.rs et reference_plugin_e2e.rs le pilotent via les mêmes chemins PluginHost, config_schema et de résolution de la configuration que ceux utilisés par le daemon. Si la fixture ne peut pas être compilée, ces tests échouent.

Installation

sh

# Copier dans le répertoire du plugin
zeroclaw plugin install /path/to/my-plugin/

# Ou manuellement
cp -r my-plugin/ ~/.zeroclaw/plugins/my-plugin/

Configuration

Les valeurs d’opérateur passent actuellement par un stockage générique de cartes de chaînes (string-map) : soit modifier [[plugins.entries]] dans le fichier TOML, soit utiliser zeroclaw config set une fois l’entrée de l’instance créée. Une installation d’outil initialise son entrée de liaison par défaut. Une clé de canal dépend de son alias configuré : l’entrée d’une instance de canal est donc initialisée lorsque cet alias est lié, via zeroclaw plugin bind <package> --channel-alias <alias> ou zeroclaw plugin install <source> --channel-alias <alias>. La construction tenant compte de l’alias, qui résout la configuration typée d’un canal à partir de cet alias configuré, a été intégrée dans #10146. zeroclaw plugin info <package> affiche chaque clé possédée par le paquet, pour la migration et les modifications ultérieures : celle de la 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. Chaque alias lié de ce type reçoit également l’état de préparation de cette instance, avec ou sans ligne de clé : clés requises définies ou manquantes, verdicts de configuration et de sortie réseau (egress), et indication de savoir si l’environnement d’exécution l’admettra. Les formulaires pilotés par schéma et l’aide contextuelle sur les champs ne sont pas encore implémentés. Les surfaces actuelles sont :

  • L’interface en ligne de commande gère le cycle de vie des plugins avec list, search, install, bind, remove, info et migrate. zeroclaw config set écrit des valeurs brutes de plugin individuelles ; il n’interprète pas le schéma du plugin.
  • zerocode peut modifier les paramètres statiques de l’hôte de plug-ins de ZeroClaw, mais ne génère pas encore les champs propres à chaque plug-in à partir de config_schema.
  • La passerelle web est en lecture seule pour les plugins : GET /api/plugins indique les plugins chargés et si le système est activé.
  • L’hôte valide config_schema lors de l’admission du paquet et valide/matérialise à nouveau les valeurs des opérateurs avant leur utilisation par l’invité.
  • Le schéma du manifeste, pour les auteurs de plugins, est l’unique contrat de typage et de validation à la frontière avec l’invité. Définissez-y chaque clé prise en charge et chaque contrainte ; ne dupliquez pas ce contrat dans une structure de configuration d’exécution de l’hôte. Le code invité doit désérialiser le JSON validé par l’hôte dans sa structure typée native.

Le schéma de configuration statique fournit le chemin générique de stockage et de marquage des secrets, et non un éditeur dynamique propre à chaque plugin. Les types de configuration des plugins dans crates/zeroclaw-config/src/schema.rs portent #[prefix = "plugins"], #[prefix = "plugins.entries"] et #[prefix = "plugins.security"], et la dérivation Configurable transforme chaque champ préfixé en chemin de configuration générique. Les champs secrets (la map config d’une entrée de plugin est marquée #[secret]) sont chiffrés au repos sous la clé .secret_key adjacente. Les champs canoniques, les valeurs par défaut et les valeurs de signature_mode pour la configuration de l’hôte figurent dans la référence de configuration ; ce schéma est la source de vérité, tandis que chaque manifeste de plugin est la source de vérité pour la structure de sa configuration privée.

Fonctionnalités de build

L’hôte de plugins est une option d’inclusion à la compilation. Les fonctionnalités au niveau binaire dans le Cargo.toml de l’espace de travail sélectionnent si les plugins sont intégrés ou non et quel backend d’exécution est livré :

  • plugins-wasm est le parapluie qui intègre l’hôte de plugin et son intégration d’exécution dans le binaire. Chaque fonctionnalité de backend ci-dessous l’implique, donc activer n’importe quel backend d’exécution (par exemple --features plugins-wasm-cranelift) embarque toujours l’hôte de plugin et sa surface CLI ; une compilation limitée au backend ne peut pas produire silencieusement un binaire sans la sous-commande plugin. Le parapluie seul est équivalent à plugins-wasm-runtime-only : pas de JIT, donc seuls les composants .cwasm précompilés se chargent.
  • plugins-wasm-runtime-only est le plus petit et le plus rapide à démarrer : pas de JIT, donc les composants sont désérialisés à partir d’un .cwasm précompilé.
  • plugins-wasm-cranelift ajoute le compilateur Cranelift, de sorte qu’un composant .wasm est compilé au chargement. C’est la seule fonctionnalité qui inclut un compilateur, et la seule capable de charger les composants .wasm publiés par le registre de plugins.
  • plugins-wasm-pulley se compile dans l’interpréteur Pulley de wasmtime. ZeroClaw ne sélectionne jamais Pulley comme cible de compilation : sur les hôtes disposant d’un backend Cranelift natif, la fonctionnalité ne change donc rien. Seule, elle se comporte comme plugins-wasm-runtime-only, et avec plugins-wasm-cranelift, elle compile du code natif. Sur les hôtes sans backend natif, comme l’ARM 32 bits, wasmtime cible Pulley de lui-même.

Celles-ci délèguent aux features du crate zeroclaw-plugins (plugins-wasmtime, plugins-wasm-cranelift, plugins-wasm-pulley) qui branchent wasmtime. Le chemin de chargement dépend de la présence du compilateur Cranelift dans le build, comme décrit sous WASI Component Host. Consultez les commentaires des features dans le Cargo.toml du workspace pour les descriptions faisant autorité.

Prise en charge des plugins par artefact de version

Le jeu de fonctionnalités de distribution standard (cargo generate features --selection dist, résolu par cible à partir du registre dans le Cargo.toml de l’espace de travail) inclut plugins-wasm-cranelift sur les sept cibles de bureau et de serveur 64 bits, et l’exclut sur ARM 32 bits ainsi que sur la cible Android expérimentale. Quel artefact porte l’hôte, et qu’est-ce qui le prouve :

ArtefactHôte de pluginsPackage acceptéPreuve
Archives CLI pour Linux x86_64 et aarch64 (glibc et musl), macOS aarch64 et x86_64, Windows x86_64Compilateur Cranelift, .wasm compilé au chargement.wasm component compilé à partir de wit/v0 de la releasela release exécute un plugin de chaque archive sur sa propre plateforme (le job plugin-smoke) avant que publish et docker ne s’exécutent
Archives CLI pour Linux ARMv7 et arm (ARMv6)aucun : plugin n’est pas une commandeaucunla politique par cible du registre, épinglée par un test xtask
CLI pour aarch64-linux-android (expérimental)aucunaucunJe n’ai pas reçu de chaîne à traduire. Envoyez-moi le texte anglais à traduire vers le français.
Images GHCR latest, versionnées et debianCranelift : ils rempaquettent les archives Linux glibc x86_64 et aarch64.wasmhérite du test de fumée de l’archive ; rien ne s’exécute dans l’image
Variante dist GHCR, paquet AUR, paquet Nix flake, compilation depuis les sources avec setup.batCranelift : compilation depuis la liste de distribution générée.wasmaucun
Image GHCR all-featuresCranelift, Pulley et runtime uniquement, tous compilés.wasmaucun
Images GHCR minimal et default-featuresaucunaucun
Bundles de bureau (dmg, deb, AppImage, msi, setup exe)Cranelift : le build sidecar standard utilise les fonctionnalités dist générées pour la cible (plus embedded-web sur macOS).wasmsource wiring dans scripts/desktop/prepare-kernel.sh ; les bundles desktop n’ont pas de porte de test de fumée pour les plugins
cargo install zeroclaw et la compilation depuis les sources avec install.shAucun par défaut ; à activer avec --features plugins-wasm-cranelift`.wasm avec l’option d’adhésion
Homebrew coredétenu par la formule, hors de ce dépôt

Limites qui s’appliquent à chaque artefact de l’hôte :

  • wit/v0 est instable. Un plugin doit être compilé avec le WIT fourni avec la version qui l’exécute (wit/VERSIONING.md) ; le manifeste ne contient ni version de l’hôte ni version du WIT, donc une incompatibilité apparaît lors de la vérification de chargement à l’installation.
  • La compilation d’un composant nécessite une mémoire qui devient exécutable après y avoir été écrite. Les environnements qui l’interdisent, comme les unités systemd avec MemoryDenyWriteExecute=yes ou les refus SELinux execmem, bloquent tous les chargements de plugins alors que le reste du binaire fonctionne normalement.
  • Les paquets du registre officiel ne sont actuellement pas signés, donc avec signature_mode = "strict", aucun d’entre eux ne s’installe.
  • Chaque appel d’outil recompile le composant ; il n’existe pas de cache de compilation.
  • Les valeurs par défaut de l’exécution restent fermées. Un paquet d’outil ou de compétence installé n’apporte rien tant que l’opérateur n’a pas défini plugins.enabled et plugins.auto_discover. Une fois les deux définis, chaque construction du registre d’outils compile et instancie chaque composant d’outil admis, puis interroge son nom, sa description et son schéma de paramètres selon les autorisations et la politique d’egress déclarées par le paquet ; l’approbation ne contrôle que execute. Un paquet de canal s’exécute dès que plugins.enabled est défini, que l’opérateur le déclare sous [channels.plugin.<alias>], et qu’un agent activé l’inclut dans ses canaux via plugin.<alias> ; pour un paquet qui reflète un canal natif via provides, l’activation sous [channels.<provides>.<alias>] est enregistrée mais pas encore construite. Chaque appel d’outil de plugin nécessite toujours l’approbation de l’opérateur, sauf si le profil de risque de l’agent le pré-approuve (auto_approve, y compris le joker *, ou level = "full") ou si l’opérateur a répondu Toujours pour cet outil plus tôt dans la même session CLI ou le même tour de canal ; les outils figurant dans always_ask demandent confirmation à chaque fois.

Récupération après une installation interrompue

zeroclaw plugin remove <name> peut récupérer un répertoire de paquet non chargé, vide ou structurellement incomplet, qui ne contient que ce qu’une installation écrit : un manifeste désignant ce paquet, le fichier de composant situé à wasm_path avec les dossiers intermédiaires qui y mènent, et, pour un plugin de compétence, une arborescence skills/ de dossiers et de fichiers. Les refus liés à la politique de signature, les paquets sains, les alias chargés, les liens symboliques, les contenus non inspectables et les répertoires contenant autre chose restent protégés. La récupération revendique une génération unique du répertoire avant l’admission et la suppression ; elle n’utilise jamais un verdict périmé pour supprimer récursivement un remplaçant portant le nom du paquet.

L’installation et la récupération se coordonnent via un verrou persistant et caché du système d’exploitation, situé sous la racine des plugins. Seuls l’installation, la mise à jour, la suppression et l’énumération de ce qu’une mise à jour interrompue a déplacé ouvrent cette racine et son verrou, de sorte qu’un hôte qui ne fait que découvrir les plugins, comme celui que conserve le démon, ne détient rien dans le répertoire des plugins. Sous Windows, une racine ouverte verrouille le répertoire des plugins et ses ancêtres contre le renommage jusqu’à ce que son hôte la libère. Un processus qui ne parvient pas à obtenir le verrou dans les 60 secondes abandonne et nomme le fichier de verrou. Tout compte capable de lire ce fichier peut détenir le verrou. Lorsque le système d’exploitation ou le système de fichiers n’offre aucun verrouillage de fichiers, l’installation, la mise à jour et la suppression sont refusées. Les nouveaux répertoires de préparation utilisent des générations uniques et un bail détenu par le système d’exploitation, plutôt qu’un PID comme preuve de propriété. La récupération ne nettoie une étape de protocole abandonnée que lors de la récupération d’un paquet final réel, ou lors de la finalisation de la suppression d’un paquet qu’une suppression antérieure avait jugé. Les anciennes étapes ne reposant que sur un PID, sans preuve d’abandon, sont conservées et leurs chemins sont affichés ; elles n’empêchent pas une nouvelle installation d’utiliser une préparation neuve.

Un paquet revendiqué puis refusé est restauré sans écraser un occupant concurrent. Si la restauration ne peut pas aboutir, l’erreur indique l’emplacement de la transaction masquée conservée. Conservez ce répertoire, résolvez le conflit signalé, puis relancez plugin remove : la nouvelle tentative restaure le paquet avant d’appliquer la politique d’admission habituelle. Une revendication dont la suppression avait déjà commencé est marquée, et une nouvelle tentative termine cette suppression au lieu de la restaurer. Une transaction masquée n’est jamais, par son seul nom, la preuve qu’un paquet sain peut être supprimé. Les modifications de l’espace de noms du système de fichiers et le verrouillage non pris en charge refusent la récupération en toute sécurité. Les déplacements de paquets exigent un renommage atomique qui refuse toute destination existante, y compris un répertoire vide. Lorsque la plateforme ou le système de fichiers ne peut pas le fournir (autres plateformes Unix telles que FreeBSD, montage NFS sans prise en charge du non-remplacement, ou Linux antérieur à 3.15), l’installation et la récupération refusent le déplacement. Une revendication échouée laisse l’entrée finale intacte. Une installation refusée de cette façon supprime la copie qu’elle a préparée et indique que le déplacement n’est pas pris en charge ; tout autre échec de publication, ainsi qu’un échec de restauration, conserve le paquet dans sa transaction masquée et signale ce chemin. Aucun renommage de remplacement simple n’est utilisé.