Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Écriture d’un plugin de canal

Un plugin de canal est une intégration de plateforme de messagerie : il transmet les réponses de l’agent vers une plateforme et expose les messages de la plateforme à l’agent. Il s’agit du type de plugin le plus complexe, car un canal est de longue durée de vie, avec état, et interagit avec le runtime via une interface de 27 fonctions dont seules 5 sont obligatoires.

Ce guide suppose que vous avez construit le plugin d’outil et que vous comprenez la configuration des crates, la règle __config, la journalisation et l’installation. Il est vérifié par rapport à wit/v0/channel.wit et l’adaptateur hôte dans crates/zeroclaw-plugins/src/wasm_channel.rs.

État de l’intégration. Les plugins de canal sont créés par un démon en cours d’exécution. Un package installé rattaché via [channels.plugin.<alias>] est admis au démarrage et supervisé exactement comme un canal natif. Voir Activation d’un plugin de canal ci-dessous.

Le cycle de vie

La structure à l’exécution d’un plugin de canal diffère de celle d’un outil sur trois aspects fondamentaux, et chacun conditionne une décision de conception dans votre code :

  1. Un store maintenu à chaud pendant toute la durée de vie du plugin. L’hôte instancie votre composant une seule fois (WasmChannel::from_wasm) et conserve le store derrière un mutex asynchrone. Le composant peut conserver un état de protocole géré par l’invité entre les appels, mais la configuration de l’opérateur reste gérée par l’hôte. Un plugin conforme doit appeler config.get et secrets.get dans chaque opération qui en a besoin et ne doit pas copier leurs résultats dans l’état persistant de l’invité. L’hôte supprime sa vue matérialisée après chaque appel, mais il ne peut pas empêcher du code invité malveillant de conserver le JSON ou le texte en clair renvoyé. Le store est réapprovisionné en fuel avant chaque appel (call_channel! dans component.rs), de sorte qu’un canal de longue durée de vie reçoit un nouveau budget de fuel à chaque appel au lieu de l’épuiser pendant toute sa durée de vie.
  2. La configuration est demandée au moment de son utilisation. L’hôte appelle votre export configure sans argument exactement une fois, au chargement, avant tout autre export. Appelez config.get pour obtenir l’objet JSON public typé validé selon le config_schema de votre manifeste ; les propriétés marquées x-secret = true sont omises et doivent être lues via secrets.get. Les lectures publiques et secrètes effectuées dans configure, ou dans tout export opérationnel ultérieur, partagent une même révision de configuration résolue. Une configuration publique du même binding et une rotation des identifiants sont donc visibles ensemble lors de l’opération suivante. Les appels effectués pendant l’instanciation et la découverte statique renvoient unavailable sans résoudre la configuration. La découverte statique inclut name, plugin-info, get-channel-capabilities, self-handle, self-addressed-mention et multi-message-delay-ms ; la modification de l’identité du bot ou du compte, ou d’autres métadonnées statiques, nécessite de reconstruire le cycle de vie du canal.
  3. Vous n’écoutez pas ; l’hôte vous alimente. Le contexte WASI ne dispose d’aucune capacité d’écoute réseau. Le trafic entrant vous parvient via l’interface importée inbound : l’hôte gère le véritable écouteur (serveur webhook, tunnel fournisseur, client de polling), place chaque message reçu dans une InboundQueue, et votre export poll-message la vide en appelant inbound-poll. Utilisez le drainage par lots avec inbound-pending si utile.

Exports requis

Cinq fonctions n’ont pas d’implémentation par défaut pour le trait Rust et doivent réellement fonctionner (world channel-plugin doc, channel.wit) :

ExporterContrat
nameNom de canal lisible par un humain.
configureInitialisation complète lors du chargement. Elle ne prend aucun argument ; appelez config.get et secrets.get pour une seule révision actuelle. Une chaîne d’erreur entraîne l’échec du chargement.
sendEnvoyer un send-message (contenu, destinataire, sujet/fil/pièces jointes facultatifs) à la plateforme.
poll-messageNon-bloquant : retourne le prochain message entrant ou none immédiatement. Ne jamais bloquer ; le pont de polling de l’hôte gère le rythme.
get-channel-capabilitiesRetourne le masque de bits des méthodes optionnelles que vous implémentez réellement. Appelé une fois au chargement.

Le pont de poll mérite une remarque : l’hôte exécute une boucle poll-to-push (listen dans wasm_channel.rs) qui appelle poll-message avec un backoff exponentiel de 50 ms à 500 ms tant que la file d’attente est vide, en se réinitialisant en cas de trafic. Si votre poll-message trap, l’hôte marque le canal poll-unhealthy, journalise et applique un backoff ; un plugin dont le poll continue de trapper signale unhealthy via health_check même s’il n’exporte pas de health-check propre. Un trap dans poll-message est donc visible, non fatal, mais il rend votre canal inutilisable. Restez simple : videz la file d’attente, traduisez, retournez.

Indicateurs de capacité : les 22 méthodes optionnelles

Tout le reste de l’interface est contrôlé par les indicateurs channel-capabilities. Le motif (identique à celui du monde de la mémoire) :

  • L’hôte lit vos drapeaux une fois au chargement.
  • Pour chaque non défini flag, le host utilise la valeur par défaut du trait Rust et n’appelle jamais votre export.
  • Vous devez toujours exporter chaque fonction ; un stub renvoyant la valeur par défaut documentée compile et n’est jamais appelé.

Les valeurs par défaut flag par flag sont documentées en ligne dans channel.wit à côté de la déclaration des flags, qui est la source de vérité. En résumé, les groupes :

GroupeDrapeauxCe que l’implémentation vous apporte
Étathealth-checkRapporter l’accessibilité de la plateforme ; combinée avec la santé du poll par l’adaptateur hôte.
Identitéself-handle, self-addressed-mention, drop-self-messageProtection contre les self-loops (le runtime ignore les messages du bot lui-même) et formes correctes de @-mention dans le prompt système par canal. L’hôte met en cache self-handle et self-addressed-mention au chargement ; ils sont lus une seule fois.
Typagestart-typing, stop-typingAssemblage des indicateurs pendant que l’agent réfléchit.
Brouillonssupports-draft-updates, send-draft, update-draft, update-draft-progress, finalize-draft, cancel-draftÉdition progressive de message : le runtime streame la réponse dans un message de plateforme éditable au lieu d’attendre la fin. Implémentez les six ensemble ou aucun.
Streaming multi-messagessupports-multi-message-streaming, multi-message-delay-msLivraison paragraphe par paragraphe avec un délai minimal inter-messages (par défaut 800ms, mis en cache au chargement).
Modérationadd-reaction, remove-reaction, pin-message, unpin-message, redact-messageRéactions emoji, épinglage, suppression des messages.
Interactionrequest-approval, request-choice, supports-free-form-askInvites d’approbation d’appels d’outils et questions à choix multiples présentées nativement sur la plateforme.

Commencez par les 5 requis plus health-check, et ajoutez des groupes au fur et à mesure que la plateforme les prend en charge. Annoncer un flag que vous n’avez pas implémenté est pire que de l’omettre : l’hôte appellera votre export et fera confiance à la réponse.

La surface d’approbation

request-approval est le point d’intégration le plus profond. Le runtime présente une approval-request compacte (nom de l’outil, résumé des arguments, arguments JSON bruts optionnels) et votre canal l’affiche de la manière autorisée par la plateforme (boutons, réactions, une convention de réponse). La variante approval-response que vous renvoyez pilote le mécanisme de sécurité :

  • approve : exécuter cet unique appel
  • deny : le refuser
  • always-approve : exécuter et ajouter l’outil à la liste d’autorisation à portée de session
  • deny-with-edit(string): refuse, mais fournit des arguments de remplacement édités

Retourner none lorsque le prompt ne peut pas être présenté ; l’appelant se rabat sur le refus automatique. Échec fermé.

Structure du message entrant

Traduisez fidèlement les événements de plateforme en enregistrements inbound-message. La logique de threading du runtime repose sur les champs de payload de la plateforme, tandis que l’identité de routage provient uniquement du point de terminaison émis par l’hôte (channel.wit, from_wit_inbound dans wasm_channel.rs) :

  • id, sender, content : les bases. reply-target est l’endroit où une réponse doit aller (ID de canal, ID de chat, adresse e-mail).
  • channel et channel-alias sont des indications héritées conservées dans l’enregistrement v0. L’hôte ignore les deux pour le routage et applique le type de canal admis et la liaison configurée, de sorte qu’un plugin ne peut pas sélectionner un autre propriétaire ou une autre session.
  • thread-ts porte l’identifiant de fil de discussion de la plateforme pour les réponses enfilées ; subject existe pour le fil de discussion des e-mails.
  • interruption-scope-id regroupe les messages pour l’interruption/annulation. Laissez-le à none pour les messages de premier niveau.
  • attachments transportent les octets bruts complets à travers la frontière (media-attachment: nom de fichier, octets, type MIME optionnel). Une note vocale fait plusieurs mégaoctets traversant par valeur ; c’est le coût documenté de la frontière 32 bits, et un modèle de handle de ressource est explicitement différé à une future révision WIT.

Du côté sortant, send-message reflète les mêmes champs ; le jeton d’annulation du SendMessage Rust est délibérément omis du record WIT car il s’agit d’un concept côté hôte sans signification à l’intérieur du plugin.

Squelette

La structure, en omettant la traduction par plateforme qui est votre véritable travail :

#![allow(unused)]
fn main() {
#[cfg(target_family = wasm)]
mod component {
    wit_bindgen::generate!({
        path: wit/v0,
        world: "channel-plugin",
        features: [plugins-wit-v0],
    });

    use exports::zeroclaw::plugin::channel::{
        ApprovalRequest, ApprovalResponse, ChannelCapabilities,
        Guest as Channel, InboundMessage, SendMessage,
    };
    use exports::zeroclaw::plugin::plugin_info::Guest as PluginInfo;
    use zeroclaw::plugin::config::get as config_get;
    use zeroclaw::plugin::inbound::inbound_poll;
    use zeroclaw::plugin::secrets::get as secret_get;

    #[derive(serde::Deserialize)]
    #[serde(deny_unknown_fields)]
    struct ChannelConfig {
        api_base: String,
    }

    fn current_config() -> Result<ChannelConfig, String> {
        let json = config_get().map_err(|_| "la configuration publique n’est pas disponible".to_string())?;
        serde_json::from_str(&json).map_err(|e| format!(configuration JSON invalide : {e}))
    }

    fn current_api_token() -> Result<String, String> {
        secret_get("api_token").map_err(|_| "api_token n’est pas disponible".to_string())
    }

    fn current_inputs() -> Result<(ChannelConfig, String), String> {
        // Les deux imports de cette exportation partagent une seule révision canonique résolue.
        Ok((current_config()?, current_api_token()?))
    }

    struct MyChannel;

    impl Channel for MyChannel {
        fn name() -> String {
            my-platform.to_string()
        }

        fn configure() -> Result<(), String> {
            let (config, api_token) = current_inputs()?;
            validate_configuration(&config.api_base, &api_token)
        }

        fn send(message: SendMessage) -> Result<(), String> {
            let (config, api_token) = current_inputs()?;
            // Livraison sortante de la plateforme via wasi:http
            // (nécessite l’autorisation http_client dans le manifeste). Construisez la
            // requête à partir des valeurs de cet appel ; ne conservez jamais une seconde copie.
            send_to_platform(&config.api_base, &api_token, message)
        }

        fn poll_message() -> Option<InboundMessage> {
            // Vider la queue alimentée par l'hôte et traduire.
            inbound_poll().map(translate_inbound)
        }

        fn get_channel_capabilities() -> ChannelCapabilities {
            ChannelCapabilities::HEALTH_CHECK
        }

        fn health_check() -> bool {
            current_inputs().is_ok()
        }

        // Chaque autre méthode : un stub retournant la valeur par défaut documentée par WIT.
        // L'hôte ne les appelle jamais tant que leur flag n'est pas défini.
        // ...
    }

    export!(MyChannel);
}
}

current_inputs est appelé délibérément au point d’utilisation. L’hôte lie les deux imports à ce package admis, à la capacité channel et à l’alias ; les lectures dans un export partagent une même révision de configuration résolue, tandis que l’export suivant peut observer une configuration publique liée identique ainsi qu’une rotation des identifiants. ChannelConfig est une vue typée propre à chaque appel et est supprimée avec le jeton. N’ajoutez pas de cache de configuration ou d’identifiants thread_local.

Manifeste et autorisations

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.
capabilitiesoui, non videCe qu’est le plugin : l’un de tool, channel, memory, observer, skill (PluginCapability, sérialisé en snake_case).
permissionsnonServices de l’hôte auxquels le code peut accéder : http_client, config_read, file_read, file_write, memory_read, memory_write (PluginPermission). Seuls les deux premiers sont appliqués aujourd’hui ; les autres sont acceptés mais inactifs. Déclarer config_read nécessite config_schema, et seuls les adaptateurs d’outils/de canaux le fournissent actuellement.
config_schemaexactement avec config_readRédigez un schéma JSON Draft 2020-12 pour la configuration privée de ce plugin ; il est inclus dans les octets du manifeste canonique et est donc couvert lorsque le manifeste est signé. La racine doit être un objet avec un mappage properties et additionalProperties = false. Chaque propriété de premier niveau doit avoir un type pris en charge explicite, directement ou via un pointeur JSON 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é de type chaîne de premier niveau pour la retirer de la configuration publique et l’exposer via l’import d’hôte à portée limitée secrets.get. Les outils reçoivent la configuration publique sous __config et peuvent lire les secrets pendant execute. Les canaux lisent l’objet public actuel via config.get et les secrets via secrets.get pendant configure et les appels opérationnels ; ces deux imports sont indisponibles lors de l’instanciation et de 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 qui ne sont pas de type chaîne sont rejetés. Un schéma sans config_read, ou 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.

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.

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.

Pour un canal : capabilities contenant channel, et presque certainement aussi config_read (aucune plateforme ne fonctionne sans identifiants) et http_client. L’adaptateur de canal implémente wasi:http en sortie, mais ne le lie qu’après validation de cette autorisation ; sans ces deux éléments, send n’a aucun chemin réseau vers la plateforme.

Associez config_read au schéma consommé par ChannelConfig :

name = "my-platform"
version = "0.1.0"
wasm_path = "my_platform.wasm"
capabilities = ["channel"]
permissions = ["config_read", "http_client"]

[config_schema]
"$schema" = "https://json-schema.org/draft/2020-12/schema"
type = "object"
additionalProperties = false
required = ["api_base", "api_token"]

[config_schema.properties.api_base]
type = "string"
minLength = 1

[config_schema.properties.api_token]
type = "string"
minLength = 1
x-secret = true

L’hôte valide les deux propriétés comme un seul objet. config.get renvoie un JSON typé contenant api_base et omet api_token, qui n’est disponible que via secrets.get. Comme les deux sont obligatoires, l’absence de config_read entraîne un refus par défaut avant l’exécution du code invité, au lieu de démarrer un canal sans la configuration requise. Chaque instance de canal sélectionne la clé plugins.entries dérivée de son package complet, de la capacité channel et de l’identité de liaison, tout en réutilisant ce schéma unique appartenant au package. Les alias identiques dans des packages différents restent donc isolés. Les commandes install et info ne peuvent pas créer cette clé, car elles ne possèdent pas l’alias de canal configuré ; leur comportement automatique d’affichage et d’initialisation est propre à l’outil, de sorte que l’entrée d’une instance de canal est écrite manuellement.

Appelez config.get et secrets.get dans chaque opération qui les utilise. L’hôte ne résout au maximum qu’une révision canonique pour cet appel, puis abandonne sa vue. Une configuration publique et une rotation des informations d’identification au sein de la même liaison logique sont visibles ensemble lors de l’opération suivante, sans rechargement du daemon ni reconstruction du canal. La modification de l’identité du bot/compte, des capacités annoncées, de son propre identifiant, de la mention ou d’autres métadonnées chargées au démarrage nécessite de reconstruire le cycle de vie du canal, car ces exports ne sont lus qu’une seule fois lors de la découverte statique.

Pour un schéma facultatif dont l’objet vide est valide, une instance à laquelle l’autorisation effective config_read est refusée peut être chargée, mais config.get et secrets.get renvoient access-denied. L’un ou l’autre des imports renvoie unavailable lors de l’instanciation ou de la découverte statique, après un échec de résolution/validation, ou lorsque le budget partagé d’appels à l’hôte est épuisé. secrets.get renvoie en outre not-found pour un nom absent ou non marqué x-secret = true.

Activation d’un plugin de canal

Un paquet installé ne fait rien tant qu’un opérateur ne l’a pas associé à une instance de canal logique. La liaison nomme le paquet et rien d’autre ; l’alias constitue l’identité de l’instance :

[plugins]
enabled = true

[channels.plugin.operations]
package = "acme.chat"
enabled = true

[agents.support]
channels = ["plugin.operations"]

L’alias devient une référence de canal ordinaire, donc plugin.operations est routé, supervisé, redémarré et adressé exactement comme telegram.main. Deux alias peuvent désigner un même paquet ; chacun dispose de sa propre instance, de son propre stockage et de sa propre clé plugins.entries, et ils ne partagent donc aucun état.

Une instance n’est admise que lorsque toutes les conditions suivantes sont réunies. Chacune constitue une barrière délibérée à refus par défaut, et une déclaration qui n’en respecte pas une reste inactive plutôt que de démarrer partiellement :

  • plugins.enabled est défini sur true.
  • Le champ enabled de la déclaration vaut true.
  • Le paquet nommé est installé et son manifeste déclare la capacité channel.
  • Certains agents activés répertorient plugin.<alias> dans leurs channels. Une liaison non référencée lancerait un écouteur sans destination vers laquelle acheminer les événements.

L’admission a lieu avant l’exécution de tout code invité : elle est décidée à partir des manifestes que l’hôte de paquets a déjà vérifiés, de sorte qu’un paquet dont le composant est corrompu est planifié puis rejeté de la même manière qu’un paquet intact. Un paquet qui réussit l’admission mais échoue ensuite lors de sa construction est journalisé et ignoré, de sorte qu’un seul plug-in défectueux ne peut pas empêcher le démon de démarrer vos autres canaux.

plugins.max_active_instances limite le nombre d’instances logiques admises pour l’ensemble des fonctionnalités. Les liaisons de canaux explicites sont prioritaires sur les outils et compétences découverts automatiquement ; ainsi, un répertoire de plugins complet ne peut pas évincer un canal configuré manuellement par l’opérateur.

Le même ensemble admis alimente les trois chargeurs : le chargeur de canaux, le registre d’outils et le chargeur de compétences de plugins. Le plafond constitue donc un budget partagé plutôt qu’un budget par capacité. Un paquet qui fournit à la fois un canal et un outil consomme réellement deux emplacements, et un outil ou une compétence qui dépasse le plafond n’est pas construit du tout. L’admission est une fonction pure de votre configuration actuelle et des paquets installés : elle ne conserve aucun compteur. Ainsi, les registres d’outils reconstruits pour chaque agent, chaque exécution de CLI, chaque délégué et chaque exécution de SOP recalculent le même ensemble au lieu d’épuiser le plafond pendant toute la durée de vie d’un démon de longue durée.

Les instances d’outils et de compétences sont découvertes automatiquement et ne sont donc admises que lorsque plugins.auto_discover vaut true. Les déclarations explicites [channels.plugin.<alias>] n’en ont pas besoin. Avec plugins.enabled = true et auto_discover = false, vous obtenez exactement les liaisons de canaux que vous avez déclarées, et rien d’autre.

Migration depuis plugins.max_plugins. L’ancienne clé n’a jamais été appliquée et a été remplacée par plugins.max_active_instances. Les deux comptent des éléments différents : l’ancienne clé comptait les paquets installés, tandis que la nouvelle compte les instances logiques admises ; ainsi, un paquet fournissant à la fois un canal et un outil consomme deux instances. Comme les unités diffèrent, une valeur max_plugins existante n’est pas reprise : elle est ignorée et la nouvelle clé utilise sa valeur par défaut. Définissez explicitement max_active_instances si vous dépendiez d’un plafond différent de la valeur par défaut.

Qu’est-ce qui n’est pas encore connecté ?

Les canaux Plugin sont construits de manière asynchrone, après que les surfaces synchrones de la carte des canaux ont déjà été créées. Les outils adressés par canal ne peuvent donc pas encore cibler un canal Plugin. La scrutation des entrées et la remise des sorties via l’écouteur supervisé ne sont pas affectées ; seul l’adressage côté outil manque.

Construire et installer

Installez la cible WASI Preview 2 une fois, puis générez le composant :

rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2

Le composant se trouve à target/wasm32-wasip2/release/<crate_name>.wasm (les tirets dans le nom du crate deviennent des underscores). Renommez-le selon ce que déclare le wasm_path de votre manifeste lorsque vous assemblez le répertoire du plugin.

[!IMPORTANT] Les fichiers .wasm et .cwasm compilés sont des artefacts binaires, souvent de plusieurs mégaoctets chacun. Ne les versionnez pas dans un arbre source git sans Git LFS : chaque reconstruction validée en tant que blob simple gonfle l’historique du dépôt de manière permanente, et les outils de git diff/revue peinent avec eux. Traitez-les comme n’importe quelle autre sortie de compilation : ajoutez target/ et *.wasm/*.cwasm à .gitignore, et distribuez-les plutôt via un artefact de release ou une archive de registre de plugins. Si un artefact doit vraiment vivre dans l’arbre, suivez le motif avec LFS (git lfs track "*.wasm") avant le premier commit.

Si l’hôte cible est un build d’exécution uniquement (sans backend JIT compilé), il ne peut pas compiler de .wasm au chargement ; il désérialise plutôt un .cwasm précompilé. Précompilez-le avec une wasmtime CLI dont la version correspond à celle de l’hôte et fournissez le .cwasm en tant qu’artefact wasm_path. Un artefact dont la version ne correspond pas sera rejeté par le contrôle de désérialisation de wasmtime et ne sera pas chargé à tort silencieusement.

Ces commandes nécessitent un binaire dans lequel l’hôte de plugins a été compilé. Les binaires de publication précompilés fournis par le programme d’installation sont compilés sans la fonctionnalité plugins-wasm, donc zeroclaw plugin ... y est une sous-commande non reconnue et les plugins installés ne sont jamais découverts. Compilez depuis les sources avec un backend d’exécution de plugins, par ex. cargo build --release --features plugins-wasm-cranelift.

Chaque plugin réside dans son propre sous-répertoire du répertoire des plugins (par défaut ~/.zeroclaw/plugins/, résolu via plugins.plugins_dir), contenant le manifeste et le composant dont le nom correspond au wasm_path du manifeste :

~/.zeroclaw/plugins/
└── my-plugin/
    ├── manifest.toml
    └── my-plugin.wasm

Installer à partir d’un répertoire local (cela valide la structure du manifeste et applique la politique de signature avant de copier quoi que ce soit) :

zeroclaw plugin install ./my-plugin/

Activer le système de plugins et confirmer la découverte :

zeroclaw config set plugins.enabled true
zeroclaw plugin list
zeroclaw plugin info my-plugin

zeroclaw plugin list et zeroclaw plugin info confirment qu’un package est installé et détectable, mais la découverte n’équivaut pas à l’activation. plugins.enabled = true active l’hôte de plugins ; les fonctionnalités d’outils et de compétences découvertes automatiquement sont chargées à l’exécution uniquement lorsque plugins.auto_discover = true l’est également, et cet indicateur vaut false par défaut (refus en cas d’échec) :

zeroclaw config set plugins.auto_discover true

Ainsi, plugins.enabled = true à lui seul vous fournit les canaux que vous déclarez sous [channels.plugin.<alias>], mais aucun outil ni aucune compétence de plugin : un paquet d’outils ou de compétences peut apparaître dans zeroclaw plugin list tout en n’apportant 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 ; cet indicateur contrôle uniquement les outils et compétences découverts automatiquement.

Un plugin absent de zeroclaw plugin list a été ignoré lors de la découverte : consultez le journal de démarrage pour l’avertissement d’ignorance (manifeste corrompu, fichier wasm_path manquant ou rejet par la politique de signature).

Tests par rapport au contrat hôte

Les tests de l’adaptateur hôte et du résolveur de configuration constituent la spécification exécutable : ils couvrent la matérialisation typée et la validation du schéma, les portées publique et secrète au point d’utilisation, la rotation cohérente sur une même révision, les autorisations refusées, le refus de la découverte statique, le transfert vers la file d’attente entrante, l’acheminement conditionné par les capacités et le suivi de l’état de santé des interrogations.

Pour exécuter votre propre composant selon ces sémantiques exactes, écrivez un test d’intégration qui l’instancie via l’adaptateur hôte réel. zeroclaw-plugins n’est pas publié sur crates.io, tirez-le donc en tant que dépendance de développement git épinglée au tag correspondant à votre hôte cible :

cargo add --dev zeroclaw-plugins \
  --git https://github.com/zeroclaw-labs/zeroclaw --tag <host-version> \
  --no-default-features --features plugins-wasm-cranelift

Le test encapsule ensuite un PluginConfigResolver::new alimenté par le manifeste et les valeurs de l’opérateur de test dans PluginHostServices, charge votre composant via WasmChannel::from_wasm, met en file le message dans le handle InboundQueue qu’il expose et vérifie que votre poll-message vide la file et traduit le message. Il s’agit du même chemin d’exécution qu’empruntera un démon de production ; sa réussite constitue le signal préalable à la distribution le plus fiable que vous puissiez obtenir sans hôte actif.

Suivant