Écrire un plugin d’outil
Ceci est le guide de niveau débutant de la série : un parcours complet depuis un crate vide jusqu’à un outil que le modèle appelle en conversation. L’outil construit ici est redact, qui masque les e-mails, les préfixes d’identifiants connus et les motifs fournis par l’opérateur dans le texte. Il est délibérément piloté par la configuration, car lire sa propre section de configuration en bac à sable est ce dont chaque plugin non trivial a besoin et ce qu’il est le plus facile de rater.
Tout ce qui figure sur cette page est vérifié par rapport au code source du contrat : le monde tool-plugin dans wit/v0/tool.wit, le chemin d’appel côté hôte dans crates/zeroclaw-plugins/src/runtime.rs et wasm_tool.rs, ainsi que la validation du manifeste dans host.rs. Les chemins vers les sources sont des références au dépôt ZeroClaw à des fins de vérification ; le plugin en lui-même est votre propre crate dans votre propre dépôt. Vous n’avez jamais besoin d’un checkout de ZeroClaw pour en construire un, seuls les fichiers de contrat wit/ (récupérés à l’étape 1) et un binaire zeroclaw installé avec l’hôte de plugin compilé sont requis pour l’exécuter.
Le binaire de la version n’est pas ce binaire. Les binaires précompilés fournis par l’installateur n’incluent pas le plugin host (
zeroclaw plugin …est une sous-commande non reconnue), etplugins-wasmne fait pas partie des fonctionnalités par défaut de la crate. Compilez le côté host depuis les sources avec un backend d’exécution ; chaque fonctionnalité de backend embarque elle-même le parapluieplugins-wasm, donc un seul flag suffit :cargo build --release --features plugins-wasm-craneliftLa page du protocole documente les choix de backend.
Déroulement d’un appel d’outil
Comprenez la forme à l’exécution avant d’écrire du code :
- Au démarrage, la découverte localise le répertoire de votre plugin, valide la structure du manifeste, applique la politique de signature, puis valide
config_schema. Avant l’enregistrement, l’hôte matérialise les valeurs d’opérateur du plugin en JSON typé et les valide. Les éléments retenus deviennent des instances deWasmTool. - Lors de l’enregistrement, l’hôte instancie le composant une fois pour lire
name,descriptionetparameters-schema. Ces éléments sont mis en cache ; ils ne sont jamais redemandés. Si cette sonde échoue, l’enregistrement échoue ; l’hôte ne substitue jamais de métadonnées synthétiques à un composant défaillant. - À chaque appel,
WasmTool::executerésout et valide la configuration à partir de l’état canonique, crée un nouveau store (un nouveau contexte WASI, un nouveau budget de fuel, sans état de l’appel précédent) et instancie le composant. Cet objet résolu unique sert pendant toute la trame : l’hôte n’injecte que ses valeurs non secrètes sous__config, fournit les secrets marqués par le schéma via l’importsecretslimité à cette portée, et appelleexecute.
Le modèle fresh-store-per-call est la contrainte de conception qui importe le plus : un plugin d’outil est sans état par construction. Tout ce que vous voulez faire persister entre les appels doit vivre en dehors du plugin (dans le texte que vous renvoyez, ou dans la config de l’opérateur).
1. Configuration du crate
Créez le crate et ajoutez les dépendances côté invité :
cargo new --lib my-plugin
cd my-plugin
cargo add wit-bindgen@0.46
cargo add serde --features derive
cargo add serde_json
Ensuite, effectuez deux modifications manuelles au manifeste du package :
- Définissez le
crate-typede la bibliothèque à["cdylib", "rlib"].cdylibest ce que produit le build du composant ;rlibpermet aux modules de pure logique du même crate de se compiler et d’être testés unitairement nativement sur l’hôte. - Dans le profil release, définissez
opt-level = "s",lto = trueetstrip = true. La taille du composant impacte le temps de téléchargement et de chargement ; il n’y a aucune raison de distribuer les symboles de débogage au-delà de la frontière du plugin.
Copiez le répertoire wit/v0/ du dépôt ZeroClaw à la racine du crate sous wit/. Vous n’avez pas besoin d’un checkout complet ; ne récupérez que ce répertoire depuis le tag correspondant à la version de votre hôte cible :
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/zeroclaw-labs/zeroclaw /tmp/zeroclaw-wit
git -C /tmp/zeroclaw-wit sparse-checkout set wit
cp -r /tmp/zeroclaw-wit/wit .
Les fichiers WIT sont l’ABI : l’hôte a généré ses bindings à partir de ces fichiers exacts, donc vos bindings guest doivent provenir des mêmes. Épinglez la version : les mondes WIT évoluent avec l’hôte, et un composant construit contre des mondes plus récents que ceux que l’hôte lie échouera à s’instancier.
2. Séparer la logique du code de liaison
Placez le comportement réel dans un module Rust simple sans imports wit-bindgen, et gardez la glue du composant mince. La raison est la testabilité : la cible composant ne peut pas exécuter cargo test nativement, donc la logique piégée dans la glue est une logique que vous ne pouvez vérifier que de bout en bout via un hôte wasm. La glue doit être trop mince pour être fausse.
src/redact.rs contient une structure de configuration et une fonction pure :
#![allow(unused)]
fn main() {
pub const DEFAULT_REPLACEMENT: &str = [RÉDACTÉ];
/// Politique de masquage résolue depuis la section de configuration du plugin.
#[derive(Debug, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RedactConfig {
pub replacement: String,
pub redact_emails: bool,
pub patterns: Vec<String>,
}
impl Default for RedactConfig {
fn default() -> Self {
Self {
replacement: DEFAULT_REPLACEMENT.to_string(),
redact_emails: true,
patterns: Vec::new(),
}
}
}
/// Masquer l'entrée. Renvoie la sortie et le nombre de segments masqués.
pub fn redact(input: &str, cfg: &RedactConfig) -> (String, usize) {
// Masquer les e-mails si cfg.redact_emails, les préfixes d'identifiants
// (sk-, ghp_, AKIA, xoxb-), et chaque littéral de cfg.patterns,
// en remplaçant chaque correspondance par cfg.replacement.
// ...
}
}
Le code invité reçoit l’objet JSON public matérialisé par le schéma ; désérialisez-le donc une seule fois au lieu de répéter l’analyse de la chaîne. Le schéma de cet exemple rend chaque champ facultatif, et Default contrôle leur comportement lorsque l’hôte fournit {}. Un objet vide est normal lorsque l’opérateur n’a pas configuré le plugin ou lorsque l’hôte refuse l’autorisation config_read demandée. Si un plugin ne peut pas fonctionner sans une valeur, indiquez-la comme obligatoire dans config_schema ; l’hôte rejettera alors un objet vide avant le démarrage du code invité.
3. Implémenter le monde
wit/v0/tool.wit définit la surface que vous devez exporter. Le world est :
world tool-plugin {
import logging;
import secrets;
export plugin-info;
export tool;
}
et l’interface tool se compose de quatre fonctions :
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>;
src/lib.rs génère les liaisons guest et implémente les deux exports :
#![allow(unused)]
fn main() {
pub mod redact;
#[cfg(target_family = wasm)]
mod component {
wit_bindgen::generate!({
path: wit/v0,
world: tool-plugin,
features: [plugins-wit-v0],
});
use crate::redact::{redact, RedactConfig};
use exports::zeroclaw::plugin::plugin_info::Guest as PluginInfo;
use exports::zeroclaw::plugin::tool::{Guest as Tool, ToolResult};
use zeroclaw::plugin::logging::{
log_record, LogLevel, PluginAction, PluginEvent, PluginOutcome,
};
struct RedactPlugin;
#[derive(serde::Deserialize)]
struct ExecuteArgs {
text: String,
#[serde(rename = "__config", default)]
config: RedactConfig,
}
impl PluginInfo for RedactPlugin {
fn plugin_name() -> String {
my-redact-plugin.to_string()
}
fn plugin_version() -> String {
0.1.0.to_string()
}
}
impl Tool for RedactPlugin {
fn name() -> String {
Masquer.to_string()
}
fn description() -> String {
Masquer les secrets et les PII du texte avant qu'il n'atteigne un journal, \
un canal ou un modèle. Masque les adresses e-mail, les préfixes d'identifiants et \
les motifs littéraux configurés par l'opérateur.
.to_string()
}
fn parameters_schema() -> String {
serde_json::json!({
"type": "objet",
"propriétés": {
« texte »: {
"type": "chaîne",
"description": Le texte à masquer.
}
},
"obligatoire": [« texte »]
})
.to_string()
}
fn execute(args: String) -> Result<ToolResult, String> {
let parsed: ExecuteArgs = match serde_json::from_str(&args) {
Ok(a) => a,
Err(e) => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!(arguments invalides : {e})),
});
}
};
let (output, count) = redact(&parsed.text, &parsed.config);
log_record(
LogLevel::Info,
&PluginEvent {
function_name: my_redact_plugin::tool::execute.into(),
action: PluginAction::Complete,
outcome: Some(PluginOutcome::Success),
duration_ms: None,
attrs: Some(format!({{\"redactions\":{count}}})),
message: redacted input.into(),
},
);
Ok(ToolResult { success: true, output, error: None })
}
}
export!(RedactPlugin);
}
}
Points de contrat, chacun ancré dans le code source de l’hôte :
plugin-infoest un export requis de chaque world. Il indique le nom et la version du composant. Maintenez-les synchronisés avec le manifest.- Les métadonnées sont lues une seule fois.
call_tool_metadatadansruntime.rslitname,descriptionetparameters-schemalors de l’enregistrement et les met en cache. Ne les calculez pas à partir de données dynamiques ; elles ne seront jamais lues à nouveau. - Le schéma constitue la vue complète du modèle sur votre outil. L’hôte l’analyse au format JSON lors du chargement (
tool parameters-schema is not valid JSONest un échec critique de l’enregistrement) et le transmet au LLM tel quel. Décrivez chaque propriété. Ne déclarez jamais__configdans celui-ci : cette clé est réservée à l’hôte, et l’hôte supprime toute valeur fournie par l’appelant avant l’injection, précisément pour empêcher le modèle de se faire passer pour votre opérateur. success: falseversusErr. UnToolResultavecsuccess: falseest retourné au modèle en tant que réponse d’outil normale à laquelle il peut réagir (réessayer avec des arguments fixes, s’excuser, sélectionner un autre outil). UnErr(String)traverse la frontière en tant que défaillance du plugin : l’hôte le retourne sous la formeplugin execute returned erroret l’appel échoue. RéservezErrpour les états vraiment cassés, et signalez les entrées invalides viasuccess: false.- Journalisez via l’interface
loggingimportée, et jamaiswasi:logging.log-recordest fire-and-forget ; l’hôte absorbe toutes les erreurs afin qu’une écriture de journal échouée ne puisse jamais interrompre votre appel, et les événements sont transmis à toutes les destinations vers lesquelleszeroclaw_logécrit, portant l’attribution zeroclaw.*(agent_alias,session_key, provider, channel) associée au span hôte sous lequel votre appel s’exécute. Notez que le champattrsdeplugin-eventn’est pas une attribution : il s’agit du payloadattributesau format libre de la ligne de journal. L’attribution est liée à l’alias et héritée du span de traçage ambiant côté hôte ; aucune donnée envoyée par un plugin ne peut la définir ou l’écraser.PluginActionetPluginOutcomesont des énumérations fermées reflétant les taxonomies de l’hôte ; aucune variante au format libre n’est prévue par conception. Sélectionnez l’option la plus proche.
4. Le __config jail
Un plugin ne lit jamais les variables d’environnement du processus et n’a jamais accès à la configuration globale. Un manifeste qui demande config_read doit également déclarer config_schema ; un schéma sans cette permission est tout aussi invalide. Le schéma est au format Draft 2020-12, sa racine doit être un objet avec une map properties et additionalProperties = false, et chaque propriété de niveau supérieur doit explicitement se résoudre en string, boolean, integer, number, array ou object.
L’hôte résout la section stockée sous la clé d’entrée de configuration versionnée dérivée du package de cette instance, de la capacité tool et de la liaison, la matérialise conformément au schéma du package, valide l’objet typé complet, puis seulement le partitionne. Seules les propriétés non secrètes sont fusionnées dans execute sous la clé réservée __config :
- Tout
__configdéjà présent dans les arguments fournis par le modèle est d’abord supprimé. Le spoofing est structurellement impossible. - Le stockage de l’opérateur reste une map chiffrée de chaînes. Stockez les chaînes directement ; encodez les booléens et les nombres comme des scalaires JSON (
"true","4","0.5") et les tableaux et objets au format JSON ('["secret-a","secret-b"]'). Le guest reçoit de vrais booléens, nombres, tableaux et objets JSON, et non ces chaînes de stockage. - Une propriété de chaîne de caractères directe de premier niveau marquée
x-secret = trueest exclue de__config. Lisez-la explicitement avec la fonctionzeroclaw::plugin::secrets::getgénérée. Les marqueurs imbriqués, les marqueurs définis à false ou non booléens, ainsi que les propriétés secrètes qui ne sont pas de type chaîne font échouer l’admission du manifeste. - L’hôte n’autorise la lecture des secrets que lors de l’exécution de
execute. Les appels provenant de l’initialisation du composant ou des exportations de métadonnées renvoientunavailablesans résoudre la configuration. Les accès publics à__configet les lectures de secrets au cours d’une même exécution utilisent la même vue de configuration résolue. - Si
config_reada été demandé mais n’a pas été effectivement accordé, l’hôte résout{}et le valide. Le schéma facultatif de cet exemple amène donc l’outil à omettre__config, et#[serde(default)]sélectionneRedactConfig::default. Un schéma obligatoire échoue en mode sécurisé au lieu de s’exécuter sans identifiants. - Les clés inconnues, les encodages JSON invalides, les types incorrects et les échecs liés aux contraintes du schéma rejettent le plugin avant l’exécution de son code. Les opérateurs définissent actuellement les valeurs sous la clé d’instance affichée lors de l’installation via TOML ou le chemin générique
zeroclaw config set; ces valeurs sont chiffrées au repos à l’aide de la clé secrète de la configuration. Les éditeurs zerocode et de passerelle pilotés par schéma relèvent de futurs travaux sur le SDK et la surface de configuration.
Pour cet outil, la section typée comporte trois clés facultatives : replacement est une chaîne, redact_emails est un booléen et patterns est un tableau de chaînes.
5. Le 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é :
| Champ | Obligatoire | Signification |
|---|---|---|
name | oui | Slug 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. |
version | oui | Chaîne de version, p. ex. 0.1.0. |
description | non | Description lisible affichée par zeroclaw plugin list. |
author | non | Nom de l’auteur ou de l’organisation. |
wasm_path | pour les capacités WASM | Nom 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. |
capabilities | oui, non vide | Ce qu’est le plugin : l’un de tool, channel, memory, observer, skill (PluginCapability, sérialisé en snake_case). |
permissions | non | Services 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_schema | exactement avec config_read | Ré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é. |
signature | non | Signature Ed25519 en Base64url sur les octets canoniques du manifest. Défini lors de la signature pour la distribution. |
publisher_key | non | Clé 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 ce plugin : name et version correspondant à ce que plugin-info indique, wasm_path désignant le fichier de composant que vous livrerez à côté, capabilities contenant exactement tool, et permissions contenant exactement config_read. N’ajoutez http_client que si votre outil effectue des appels HTTP sortants. L’adaptateur d’outil implémente wasi:http, mais ne l’associe qu’après validation de cette autorisation ; sans à la fois la prise en charge de l’adaptateur et l’autorisation, il n’y a aucune surface HTTP.
Le contrat de manifeste correspondant à RedactConfig typé est :
name = "my-redact-plugin"
version = "0.1.0"
wasm_path = "my_redact_plugin.wasm"
capabilities = ["tool"]
permissions = ["config_read"]
[config_schema]
"$schema" = "https://json-schema.org/draft/2020-12/schema"
type = "object"
additionalProperties = false
[config_schema.properties.replacement]
type = "string"
minLength = 1
[config_schema.properties.redact_emails]
type = "boolean"
[config_schema.properties.patterns]
type = "array"
items = { type = "string" }
Ces propriétés sont facultatives, conformément aux valeurs par défaut de l’invité. Pour un identifiant qui doit exister, ajoutez son nom à required dans [config_schema] ; une autorisation refusée ou une valeur manquante empêchera alors le démarrage du composant.
Outils qui appellent le réseau
On peut soutenir que la forme d’outil la plus courante dans le monde réel n’est pas une transformation pure comme redact, mais un pont vers une API externe : déclarez http_client dans le manifeste, lisez les identifiants via le service de secrets à portée limitée, puis effectuez une requête sortante. Marquez l’identifiant dans le schéma signé :
[config_schema]
required = ["api_key"]
[config_schema.properties.api_key]
type = "string"
minLength = 1
x-secret = true
L’élément manquant par rapport à ce guide est un client HTTP qui fonctionne à l’intérieur d’un composant : reqwest et les bibliothèques associées ne conviennent pas, car il n’y a pas de surface de socket, uniquement wasi:http. Un client dont le fonctionnement avec cet hôte est connu est waki, qui est bloquant et s’adapte donc directement à la signature synchrone de execute. Ajoutez-le en le conditionnant à la cible component afin que vos modules de logique pure restent testables nativement :
cargo add waki --target cfg(target_family = "wasm")
La structure d’un appel, dans execute après l’analyse de __config public :
#![allow(unused)]
fn main() {
let api_key = zeroclaw::plugin::secrets::get(api_key)
.map_err(|_| "api_key est indisponible".to_string())?;
let resp = waki::Client::new()
.get("https://api.example.com/search")
.query([(q, term.as_str())])
.header(« Autorisation », format!(Bearer {api_key}))
.connect_timeout(std::time::Duration::from_secs(5))
.send()
.map_err(|e| format!(échec de la requête : {e}))?;
}
Deux faits de version qui ressemblent à des ruptures mais n’en sont pas : waki vendore son propre wit-bindgen (0.34) aux côtés de la 0.46 que vos bindings de world utilisent ; les deux coexistent, chacun générant ses propres bindings. Et waki émet des imports wasi:http@0.2.4 tandis que la baseline actuelle de la toolchain est @0.2.6 ; l’hôte lie les deux sans problème. Aucun des deux ne nécessite d’action.
Rappelez-vous le cadre de confiance de l’aperçu : http_client est tout ou rien. Le bac à sable ne limite pas où un plugin autorisé envoie des données, donc les opérateurs exécutant la politique de signature strict font confiance à votre code, pas à une liste d’autorisation d’URL.
6. Testez la logique de manière native
Étant donné que redact.rs n’a aucune dépendance wasm, un simple cargo test suffit pour le tester sur l’hôte :
#![allow(unused)]
fn main() {
#[test]
fn empty_config_falls_back_to_defaults() {
let cfg: RedactConfig = serde_json::from_str("{}").unwrap();
let (out, n) = redact(Envoyez-moi un e-mail à a@b.example, &cfg);
assert_eq!(n, 1);
assert!(out.contains([RÉDACTÉ]));
}
}
Couvrir au minimum : le cas jail (section vide), le cas configuré et le transfert direct du texte sans rien à masquer. Chaque comportement transmis par le glue doit pouvoir être vérifié ici, sans qu’aucune toolchain wasm ne soit nécessaire.
7. Compilation
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
.wasmet.cwasmcompilé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 degit diff/revue peinent avec eux. Traitez-les comme n’importe quelle autre sortie de compilation : ajouteztarget/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.
8. Installer et vérifier
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, donczeroclaw 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).
9. Exécutez-le
Demander à l’agent d’utiliser l’outil :
> redact this before you log it: key sk-live-abc123, mail ops@example.com
Le modèle voit redact dans son catalogue avec votre schéma, l’appelle, et l’hôte exécute le composant dans un store frais sous les limites de fuel et de mémoire configurées. Les outils de plugin ne font pas partie de l’ensemble d’auto-approbation en lecture seule intégré, donc en autonomie non totale l’appel fait apparaître l’invite d’approbation de l’opérateur comme tout autre outil privilégié ; anticipez cela dans la description de votre outil plutôt que d’en être surpris. Vos événements log-record apparaissent dans le journal structuré avec l’attribution de span du site d’appel de l’hôte.
Deux contraintes opérationnelles qu’il vaut la peine de répéter depuis la vue d’ensemble des plugins :
- Les noms d’outils ne doivent pas entrer en conflit avec les outils intégrés. Les outils intégrés s’enregistrent en premier et la dispatch résout la première correspondance (
find_tooldans le runtime), de sorte qu’un outil de plugin nommé comme un outil intégré ne soit jamais sélectionné. Aucune erreur n’est renvoyée ; il n’y a tout simplement aucun retour. Choisissez un nom unique. - Un outil par composant. Le monde
tool-pluginexporte une seule interfacetool. Une boîte à outils est composée de plusieurs répertoires de plugins, un par composant.
Dépannage
| Symptôme | Cause probable |
|---|---|
Plugin manquant dans zeroclaw plugin list | Système de plugins désactivé; manifeste malformé; fichier wasm_path manquant; politique de signature l’a rejeté. Le journal de démarrage contient l’avertissement spécifique d’omission. |
Présent dans zeroclaw plugin list, mais l’outil ne le charge jamais | plugins.auto_discover est false (la valeur par défaut). Les capacités d’outils et de compétences découvertes automatiquement sont chargées uniquement lorsque plugins.auto_discover = true ; plugins.enabled = true active à lui seul uniquement les canaux déclarés explicitement. Exécutez zeroclaw config set plugins.auto_discover true. |
| Outil rejeté lors de l’enregistrement | La validation de la configuration ou la sonde des métadonnées a échoué. Consultez le journal pour connaître l’erreur précise ; un échec de la sonde signifie généralement que le composant a été compilé avec un WIT incompatible. |
| Outil jamais sélectionné par le modèle | Le nom est en conflit avec un outil intégré, ou la description/le schéma n’indiquent pas au modèle quand l’outil est applicable. |
__config absent malgré une section configurée | La portée effective a refusé config_read, l’entrée n’utilise pas la clé complète de l’instance affichée lors de l’installation, l’objet validé est vide ou chaque propriété validée est marquée comme secrète. Une incompatibilité entre config_schema et les permissions rejette plutôt le plug-in. |
secrets.get renvoie not-found | La propriété est absente ou n’est pas une chaîne de caractères directe de niveau supérieur marquée x-secret = true dans le schéma admis. |
secrets.get renvoie unavailable | L’appel s’est exécuté en dehors de execute, la résolution de la configuration a échoué ou l’exécution a épuisé son budget fixe d’appels à l’hôte. |
| L’appel échoue ou déclenche une trappe | La limite de fuel, de temps réel ou de mémoire a été atteinte. Augmentez plugins.limits.call_fuel, plugins.limits.call_timeout_ms ou plugins.limits.max_memory_mb selon le cas, ou effectuez moins de travail par appel. |
| Le chargement échoue sur un hôte en mode runtime uniquement | Vous avez livré un .wasm à un hôte sans JIT ; livrez plutôt un .cwasm correspondant à la version. |
Suivant
- Écrire un plugin de canal pour le cycle de vie du warm-store, les drapeaux de capacité et l’inbound alimenté par l’hôte.
- Distribution des plugins lorsque cet outil doit quitter votre machine.